diff --git a/CMake/FindJsonCpp.cmake b/CMake/FindJsonCpp.cmake new file mode 100644 index 00000000..7115c8d7 --- /dev/null +++ b/CMake/FindJsonCpp.cmake @@ -0,0 +1,40 @@ +# Find the JsonCpp include files and library. +# +# JsonCpp is a C++ library that can read/write JSON (JavaScript Object Notation) +# documents. See http://jsoncpp.sourceforge.net/ for more details. +# +# This module defines: +# JsonCpp_INCLUDE_DIRS - where to find json/json.h +# JsonCpp_LIBRARIES - the libraries to link against to use JsonCpp +# JsonCpp_FOUND - if false the library was not found. + +find_path(JsonCpp_INCLUDE_DIR "json/json.h" + PATH_SUFFIXES "jsoncpp" + DOC "Specify the JsonCpp include directory here") + +find_library(JsonCpp_LIBRARY + NAMES jsoncpp + PATHS + DOC "Specify the JsonCpp library here") +set(JsonCpp_INCLUDE_DIRS ${JsonCpp_INCLUDE_DIR}) +set(JsonCpp_LIBRARIES "${JsonCpp_LIBRARY}") + +set(_JsonCpp_version_args) +if (EXISTS "${JsonCpp_INCLUDE_DIR}/json/version.h") + file(STRINGS "${JsonCpp_INCLUDE_DIR}/json/version.h" _JsonCpp_version_contents REGEX "JSONCPP_VERSION_[A-Z]+") + foreach (_JsonCpp_version_part MAJOR MINOR PATCH) + string(REGEX REPLACE ".*# *define +JSONCPP_VERSION_${_JsonCpp_version_part} +([0-9]+).*" + "\\1" JsonCpp_VERSION_${_JsonCpp_version_part} "${_JsonCpp_version_contents}") + endforeach () + + set(JsonCpp_VERSION_STRING "${JsonCpp_VERSION_MAJOR}.${JsonCpp_VERSION_MINOR}.${JsonCpp_VERSION_PATCH}") + + set(_JsonCpp_version_args VERSION_VAR JsonCpp_VERSION_STRING) +endif () + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(JsonCpp + REQUIRED_VARS JsonCpp_LIBRARIES JsonCpp_INCLUDE_DIRS + ${_JsonCpp_version_args}) + +mark_as_advanced(JsonCpp_INCLUDE_DIR JsonCpp_LIBRARY) diff --git a/Documentation/file-specifications/camera-calibration.md b/Documentation/file-specifications/camera-calibration.md index 0a82e0ba..bd77bd15 100644 --- a/Documentation/file-specifications/camera-calibration.md +++ b/Documentation/file-specifications/camera-calibration.md @@ -1,5 +1,44 @@ # Camera Calibration File Format +## VTKCam 1.0 + +### Overview + +The camera calibration file is a yaml and contains all of the information to load a camera into Autoscoper. The file is organized with key value pairs. + +:::{warning} +The VTKCam 1.0 format is non-standard and exists purely as an +implementation detail. While we are documenting it, the file +organization may change from version to version without notice. +We mean it. +::: + +### Key value pairs + +| Key | Value | +| --- | --- | +|`version`| The version of the file. Currently only 1.0 is supported. | +|`focal-point`| The XYZ coordinates of the focal point. | +|`camera-position`| The XYZ coordinates of the camera position. | +|`view-up`| The up vector of the camera. | +|`view-angle`| The view angle of the camera. | +|`image-width`| The width of the image. | +|`image-height`| The height of the image. | + +### Example + +```{code-block} json +{ + "@schema": "https://autoscoperm.slicer.org/vtk-schema-1.0.json", + "version": 1.0, + "focal-point": [-7.9999999999999964, -245.50000000000006, -186.65000000000006], + "camera-position": [104.71926635196253, -255.22259800818924, -179.66771669788898], + "view-up": [0.0, 1.0, 0.0], + "view-angle": 30.0, + "image-width": 1760, + "image-height": 1760, +} +``` ## MayaCam 2.0 ### Overview @@ -17,6 +56,15 @@ The camera calibration file is a txt and contains all of the information to load ### Template +```{note} +The rotation is applied to the camera after the translation. Therefore, the final camera position is calculated by the following equation: + +``` + +```{math} +camera\_position = rotation * -translation +``` + ``` image size height,width @@ -28,13 +76,13 @@ fx,0,cx rotation r00,r01,r02 -r10,r11,r12 -r20,r21,r22 +-r10,-r11,-r12 +-r20,-r21,-r22 translation -t0 -t1 -t2 +-x +y +-z ``` ### Example @@ -70,10 +118,10 @@ The camera calibration file is a csv and contains all of the information to load | Line Number | Description | | --- | --- | | 1 | The camera location in world space. | -| 2 | Rotations around the local x, y, and z axes of the camera. The order of rotation is z, y, x. | +| 2 | Rotations around the local x, y, and z axes of the camera. The order of rotation is z, y, x (Roll, Pitch, Yaw).| | 3 | The position of the film plane relative to the camera. Given in the camera's local space. | | 4 | u0, v0, z | -| 5 | scale, height, width | +| 5 | scale, height, width. Scale is ignored and is computed from distance and z. | ### Example @@ -85,3 +133,140 @@ The camera calibration file is a csv and contains all of the information to load 840.2,839.2,-6325.8 0.1,1760,1760 ``` + +## Backend Calculations + +### Overview + +This section describes the calculations that are performed by the backend to convert the camera calibration file into an internal Camera object. + +### Image Plane + + +The image plane is the location of the DRR image in world space. The center of the image plane is calculated by the following equation: + +```{note} +The below formula applies to VTKCam 1.0 and MayaCam 2.0. For MayaCam 1.0, `c_x`` and `c_y` are replaced by `u0` and `v0` respectively. The value for `z` is also not calculated and is instead pulled from the camera calibration file. + +`DRR_X` and `DRR_Y` are the width and height of the DRR image respectively. + +The translation matrix is represented by `t` and the rotation matrix is represented by `R`. +``` + +The constant `-0.5` is used so that the `z` value is the average of the `f_x` and `f_y` values, and it is negated to be consistent with the MayaCam 1.0 file format. + +```{math} +z = -0.5 * (f_x + f_y) +``` + +```{math} +distance = \sqrt{t_x^2 + t_y^2 + t_z^2} +``` + +The constant `-1.5` is used so that the image plane is placed across the origin from the camera. + +```{math} +scale = \frac{-1.5 * distance}{z} +``` + +```{math} +image\_plane\_translation[0] = scale * (\frac{DRR_X}{2} - c_x) +``` + +```{math} +image\_plane\_translation[1] = scale * (\frac{DRR_Y}{2} - c_y) +``` + +```{math} +image\_plane\_translation[2] = scale * z +``` + +```{math} +image\_plane\_center = R * image\_plane\_translation + t +``` + +### Viewport + +The viewport defines the position of the 2D viewer. The viewport is calculated by the following equation: + +```{note} +The below formula applies to VTKCam 1.0 and MayaCam 2.0. For MayaCam 1.0, c_x and c_y are replaced by u0 and v0 respectively. While f_x and f_y are replaced by z. + +DRR_X and DRR_Y are the width and height of the DRR image respectively. +``` + +```{math} +viewport[0] = -(2 * c_x - DRR_X) / f_x +``` + +```{math} +viewport[1] = -(2 * c_y - DRR_Y) / f_y +``` + +```{math} +viewport[2] = 2 * DRR_X / f_x +``` + +```{math} +viewport[3] = 2 * DRR_Y / f_y +``` + +### VTKCam 1.0 Unique Calculations + +The VTKCam 1.0 file format calculates the camera orientation and the focal length from the camera position, focal point, and view angle. + +#### Camera Orientation + +```{note} +The translation matrix is assumed to be the camera position. +``` + +First, a vector is calculated from the camera position to the focal point. + +```{math} +f = \text{focal_point} - \text{camera_position} + +\hat{f} = \frac{f}{\lVert f \rVert} +``` + +Next, a side vector is calculated from the cross product of the focal vector and the up vector. The up vector is assumed to be (0, 1, 0). + +```{math} +s = \hat{f} \times \begin{bmatrix} 0 \\ 1 \\ 0 \end{bmatrix} + +\hat{s} = \frac{s}{\lVert s \rVert} +``` + +Finally, the up vector is calculated from the cross product of the side vector and the focal vector. + +```{math} +\text{up} = \hat{s} \times \hat{f} +``` + +The rotation matrix is then calculated from the side, up, and focal vectors. + +```{math} +\text{rotation_matrix} = \begin{bmatrix} +s_x & s_y & s_z \\ +up_x & up_y & up_z \\ +-f_X & -f_y & -f_z \\ +\end{bmatrix} +``` + +#### Focal Length + +```{note} +The principal point (c_x, c_y) is assumed to be half of the image size in the x and y directions respectively. + +The view angle is assumed to be in radians (This is converted from degrees to radians in the backend.). +``` + +The focal length is calculated from the view angle and the image size. + +```{math} +f_x = \frac{image\_size_x}{2 \cdot \tan(\frac{view\_angle}{2})} +``` + +```{math} +f_y = \frac{image\_size_y}{2 \cdot \tan(\frac{view\_angle}{2})} +``` \ No newline at end of file diff --git a/SuperBuild.cmake b/SuperBuild.cmake index d883dec3..7c66bf49 100644 --- a/SuperBuild.cmake +++ b/SuperBuild.cmake @@ -2,6 +2,7 @@ set(Autoscoper_DEPENDENCIES GLEW TIFF + JsonCpp ) if(Autoscoper_RENDERING_BACKEND STREQUAL "OpenCL") if(Autoscoper_OPENCL_USE_ICD_LOADER) diff --git a/Superbuild/External_JsonCpp.cmake b/Superbuild/External_JsonCpp.cmake new file mode 100644 index 00000000..4531a888 --- /dev/null +++ b/Superbuild/External_JsonCpp.cmake @@ -0,0 +1,75 @@ + +set(proj JsonCpp) + +set(${proj}_DEPENDENCIES "") + +# Include dependent projects if any +ExternalProject_Include_Dependencies(${proj} PROJECT_VAR proj DEPENDS_VAR ${proj}_DEPENDENCIES) + +if(Autoscoper_USE_SYSTEM_${proj}) + message(FATAL_ERROR "Enabling Autoscoper_USE_SYSTEM_${proj} is not supported !") +endif() + +# Sanity checks +if(DEFINED JsonCpp_INCLUDE_DIR AND NOT EXISTS ${JsonCpp_INCLUDE_DIR}) + message(FATAL_ERROR "JsonCpp_INCLUDE_DIR variable is defined but corresponds to nonexistent directory") +endif() +if(DEFINED JsonCpp_LIBRARY AND NOT EXISTS ${JsonCpp_LIBRARY}) + message(FATAL_ERROR "JsonCpp_LIBRARY variable is defined but corresponds to nonexistent file") +endif() + +if((NOT DEFINED JsonCpp_INCLUDE_DIR + OR NOT DEFINED JsonCpp_LIBRARY) AND NOT Autoscoper_USE_SYSTEM_${proj}) + + set(EP_SOURCE_DIR ${CMAKE_BINARY_DIR}/${proj}) + set(EP_BINARY_DIR ${CMAKE_BINARY_DIR}/${proj}-build) + set(EP_INSTALL_DIR ${CMAKE_BINARY_DIR}/${proj}-install) + + set(EXTERNAL_PROJECT_OPTIONAL_CMAKE_CACHE_ARGS) + + ExternalProject_Add(${proj} + ${${proj}_EP_ARGS} + GIT_REPOSITORY https://github.com/open-source-parsers/jsoncpp.git + GIT_TAG 5defb4ed1a4293b8e2bf641e16b156fb9de498cc # 1.9.5 + SOURCE_DIR ${EP_SOURCE_DIR} + BINARY_DIR ${EP_BINARY_DIR} + INSTALL_DIR ${EP_INSTALL_DIR} + CMAKE_CACHE_ARGS + # Compiler settings + -DCMAKE_C_COMPILER:FILEPATH=${CMAKE_C_COMPILER} + -DCMAKE_CXX_COMPILER:FILEPATH=${CMAKE_CXX_COMPILER} + -DCMAKE_CXX_EXTENSIONS:BOOL=${CMAKE_CXX_EXTENSIONS} + -DCMAKE_CXX_STANDARD:STRING=${CMAKE_CXX_STANDARD} + -DCMAKE_CXX_STANDARD_REQUIRED:BOOL=${CMAKE_CXX_STANDARD_REQUIRED} + # Options + -DJSONCPP_WITH_TESTS:BOOL=OFF + -DJSONCPP_WITH_POST_BUILD_UNITTEST:BOOL=OFF + -DJSONCPP_WITH_WARNING_AS_ERROR:BOOL=OFF + -DJSONCPP_WITH_PKGCONFIG_SUPPORT:BOOL=OFF + -DJSONCPP_WITH_CMAKE_PACKAGE:BOOL=ON + -DBUILD_SHARED_LIBS:BOOL=OFF + -DBUILD_STATIC_LIBS:BOOL=ON + # Install directories + -DCMAKE_INSTALL_PREFIX:PATH= + -DCMAKE_INSTALL_BINDIR:STRING=${Autoscoper_BIN_DIR} + -DCMAKE_INSTALL_LIBDIR:STRING=${Autoscoper_LIB_DIR} + ${EXTERNAL_PROJECT_OPTIONAL_CMAKE_CACHE_ARGS} + DEPENDS + ${${proj}_DEPENDENCIES} + ) + set(${proj}_INCLUDE_DIR ${EP_INSTALL_DIR}/include) + set(${proj}_LIBRARY ${EP_INSTALL_DIR}/${Autoscoper_LIB_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}jsoncpp${CMAKE_STATIC_LIBRARY_SUFFIX}) +else() + ExternalProject_Add_Empty(${proj} DEPENDS ${${proj}_DEPENDENCIES}) +endif() + +# For sake of consistency with how Slicer integrates JsonCpp, we ignore the "jsoncppConfig.cmake" file provided by jsoncpp and +# instead set JsonCpp_INCLUDE_DIR and JsonCpp_LIBRARY expected by the "FindJsonCpp" CMake module. +mark_as_superbuild( + VARS + ${proj}_INCLUDE_DIR:PATH + ${proj}_LIBRARY:FILEPATH +) + +ExternalProject_Message(${proj} "${proj}_INCLUDE_DIR:${${proj}_INCLUDE_DIR}") +ExternalProject_Message(${proj} "${proj}_LIBRARY:${${proj}_LIBRARY}") diff --git a/libautoscoper/CMakeLists.txt b/libautoscoper/CMakeLists.txt index f822bc4b..5cf231c7 100644 --- a/libautoscoper/CMakeLists.txt +++ b/libautoscoper/CMakeLists.txt @@ -105,9 +105,14 @@ find_package(TIFF REQUIRED MODULE) target_link_libraries(libautoscoper PUBLIC TIFF::TIFF) target_compile_definitions(libautoscoper PUBLIC -DUSE_LIBTIFF) +find_package(JsonCpp REQUIRED) +target_link_libraries(libautoscoper PRIVATE ${JsonCpp_LIBRARY}) +target_include_directories(libautoscoper PRIVATE ${JsonCpp_INCLUDE_DIR}) + target_include_directories(libautoscoper PUBLIC ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/src + ../math ) install(TARGETS libautoscoper diff --git a/libautoscoper/src/Camera.cpp b/libautoscoper/src/Camera.cpp index f3f26b32..9184c757 100644 --- a/libautoscoper/src/Camera.cpp +++ b/libautoscoper/src/Camera.cpp @@ -44,10 +44,14 @@ #define M_PI 3.14159265358979323846 #endif +#define VTK_CAMERA_SCHEMA_URL "https://autoscoperm.slicer.org/vtkCamera-schema-1.0.json" + +#include #include #include #include #include +#include #include "Camera.hpp" @@ -100,31 +104,60 @@ namespace xromm "See https://autoscoper.readthedocs.io/en/latest/file-specifications/camera-calibration.html#mayacam-" + version + "-0"; } -Camera::Camera(const std::string& mayacam) : mayacam_(mayacam) -{ - // Load the mayacam.csv file into an array of doubles + std::string vtkCamReadingError(const std::string& version, int line, const std::string& filename, const std::string& message) { + return std::string("Invalid VTKCam ") + version + ".0 file. " + message + "\n" + "See line " + std::to_string(line) + " in " + filename + ".\n" + "\n" + "Please check the VTKCam " + version + ".0 specification.\n" + "See https://autoscoper.readthedocs.io/en/latest/file-specifications/camera-calibration.html#vtkcam-" + version + "-0"; + } - std::fstream file(mayacam.c_str(), std::ios::in); - if (file.is_open() == false) { - throw std::runtime_error("File not found: " + mayacam); + bool parseArray(std::string& value, double* a, int n) { + value.erase(std::remove(value.begin(), value.end(), '['), value.end()); + value.erase(std::remove(value.begin(), value.end(), ']'), value.end()); + value.erase(std::remove(value.begin(), value.end(), ','), value.end()); + std::istringstream value_stream(value); + for (int i = 0; i < n; ++i) { + if (!(value_stream >> a[i])) { + return false; + } } - - std::string csv_line; - safeGetline(file, csv_line); - file.close(); - if (csv_line.compare("image size") == 0) - { - loadMayaCam2(mayacam); - } - else - { - loadMayaCam1(mayacam); + return true; } -} + Camera::Camera(const std::string& mayacam) : mayacam_(mayacam) + { + // Check the file extension + std::string::size_type ext_pos = mayacam_.find_last_of('.'); + if (ext_pos == std::string::npos) { + throw std::runtime_error("Invalid MayaCam file"); + } + std::string ext = mayacam_.substr(ext_pos + 1); + // if its a yaml file load it as a vtk camera + if (ext.compare("json") == 0) { + loadVTKCamera(mayacam_); + } + else { + std::fstream file(mayacam.c_str(), std::ios::in); + if (file.is_open() == false) { + throw std::runtime_error("File not found: " + mayacam); + } + std::string csv_line; + safeGetline(file, csv_line); + file.close(); + if (csv_line.compare("image size") == 0) + { + loadMayaCam2(mayacam); + } + else + { + loadMayaCam1(mayacam); + } + } + } -void Camera::loadMayaCam1(const std::string& mayacam) + void Camera::loadMayaCam1(const std::string& mayacam) { std::cout << "Reading MayaCam 1.0 file: " << mayacam << std::endl; @@ -200,67 +233,16 @@ void Camera::loadMayaCam1(const std::string& mayacam) coord_frame_ = CoordFrame::from_xyzypr(xyzypr); // Calculate the viewport - viewport_[0] = (2.0f*u0 - size_[0]) / z; - viewport_[1] = (2.0f*v0 - size_[1]) / z; - viewport_[2] = -2.0f*size_[0] / z; - viewport_[3] = -2.0f*size_[1] / z; - - // Choose the scaling factor such that the image plane will be on the - // other side of the origin from the camera. The values in the mayacam - // file are discarded. - double distance = sqrt(translation[0] * translation[0] + - translation[1] * translation[1] + - translation[2] * translation[2]); - double scale = -1.5*distance / z; - - image_plane_trans[0] = scale*(size_[0] / 2.0 - u0); - image_plane_trans[1] = scale*(size_[1] / 2.0 - v0); - image_plane_trans[2] = scale*z; - - // Calculate the vertices at the corner of the image plane. - double image_plane_center[3]; - coord_frame_.point_to_world_space(image_plane_trans, image_plane_center); - - double half_width = scale*size_[0] / 2.0; - double half_height = scale*size_[1] / 2.0; - - double right[3] = { coord_frame_.rotation()[0], - coord_frame_.rotation()[1], - coord_frame_.rotation()[2] }; - double up[3] = { coord_frame_.rotation()[3], - coord_frame_.rotation()[4], - coord_frame_.rotation()[5] }; + if (z < 0) { + calculateViewport(u0, v0, -z, -z); + } else { + calculateViewport(u0, v0, z, z); + } - image_plane_[0] = image_plane_center[0] - half_width*right[0] + - half_height*up[0]; - image_plane_[1] = image_plane_center[1] - half_width*right[1] + - half_height*up[1]; - image_plane_[2] = image_plane_center[2] - half_width*right[2] + - half_height*up[2]; - - image_plane_[3] = image_plane_center[0] - half_width*right[0] - - half_height*up[0]; - image_plane_[4] = image_plane_center[1] - half_width*right[1] - - half_height*up[1]; - image_plane_[5] = image_plane_center[2] - half_width*right[2] - - half_height*up[2]; - - image_plane_[6] = image_plane_center[0] + half_width*right[0] - - half_height*up[0]; - image_plane_[7] = image_plane_center[1] + half_width*right[1] - - half_height*up[1]; - image_plane_[8] = image_plane_center[2] + half_width*right[2] - - half_height*up[2]; - - image_plane_[9] = image_plane_center[0] + half_width*right[0] + - half_height*up[0]; - image_plane_[10] = image_plane_center[1] + half_width*right[1] + - half_height*up[1]; - image_plane_[11] = image_plane_center[2] + half_width*right[2] + - half_height*up[2]; + // Calculate the Image Plane + calculateImagePlane(u0, v0, z); } - void Camera::loadMayaCam2(const std::string& mayacam) { std::cout << "Reading MayaCam 2.0 file: " << mayacam << std::endl; @@ -277,7 +259,6 @@ void Camera::loadMayaCam1(const std::string& mayacam) std::fstream file(mayacam.c_str(), std::ios::in); - double csv_vals[5][3]; std::string csv_line, csv_val; for (int i = 0; i < 17 && safeGetline(file, csv_line); ++i) { std::istringstream csv_line_stream(csv_line); @@ -398,32 +379,93 @@ void Camera::loadMayaCam1(const std::string& mayacam) coord_frame_ = CoordFrame(&rotation[0][0], translation); // Calculate the viewport - viewport_[0] = -(2.0f*K[2][0] - size_[0]) / K[0][0]; - viewport_[1] = -(2.0f*K[2][1] - size_[1]) / K[1][1]; - viewport_[2] = 2.0f*size_[0] / K[0][0]; - viewport_[3] = 2.0f*size_[1] / K[1][1]; + calculateViewport(K[2][0], K[2][1], K[0][0], K[1][1]); + + // Calculate the image plane + double z = -0.5* (K[0][0] + K[1][1]); // Average focal length, negated to be consistent with MayaCam 1.0 + calculateImagePlane(K[2][0], K[2][1], z); + } + + void Camera::loadVTKCamera(const std::string& filename) { + std::cout << "Loading VTKCam file: " << filename << std::endl; + std::fstream file(filename.c_str(), std::ios::in); + if (!file.is_open()) { + throw std::runtime_error("Error opening VTKCam file: " + filename); + } + + Json::Value calibrationFile; + file >> calibrationFile; + file.close(); + + // Check the schema + if (calibrationFile["@schema"].asString() != VTK_CAMAERA_SCHEMA_URL) { + throw std::runtime_error(vtkCamReadingError("1", -1, filename, "Unsupported schema. " + calibrationFile["@schema"].asString() + " is not " + VTK_CAMERA_SCHEMA_URL)); + } + + // Check the version number + if (calibrationFile["version"].asDouble() != 1.0) { + throw std::runtime_error(vtkCamReadingError("1", -1, filename, "Unsupported version number. "+ calibrationFile["version"].asString() + " is not 1.0")); + } + + + // Set the size + size_[0] = calibrationFile["image-width"].asDouble(); + size_[1] = calibrationFile["image-height"].asDouble(); + + Vec3d cam_pos(calibrationFile["camera-position"][0].asDouble(), calibrationFile["camera-position"][1].asDouble(), calibrationFile["camera-position"][2].asDouble()); + Vec3d focal(calibrationFile["focal-point"][0].asDouble(), calibrationFile["focal-point"][1].asDouble(), calibrationFile["focal-point"][2].asDouble()); + Vec3d up(calibrationFile["view-up"][0].asDouble(), calibrationFile["view-up"][1].asDouble(), calibrationFile["view-up"][2].asDouble()); + double rot[9] = { 0.0 }; + calculateLookAtMatrix(cam_pos, focal, up, rot); + coord_frame_ = CoordFrame(rot, cam_pos); + // Calculate the focal length + double focal_lengths[2] = { 0.0 }; + calculateFocalLength(calibrationFile["view-angle"].asDouble(), focal_lengths); - // Choose the scaling factor such that the image plane will be on the - // other side of the origin from the camera. The values in the mayacam - // file are discarded. - double z = - 0.5* (K[0][0] + K[1][1]); - double distance = sqrt(translation[0] * translation[0] + - translation[1] * translation[1] + - translation[2] * translation[2]); - double scale = -1.5*distance / z; + // Calculate the principal point + double cx = size_[0] / 2.0; + double cy = size_[1] / 2.0; + + // Calculate the viewport + calculateViewport(cx, cy , focal_lengths[0], focal_lengths[1]); + + // Calculate the image plane + double z = -0.5* (focal_lengths[0] + focal_lengths[1]); + calculateImagePlane(cx, cy, z); + } + + void Camera::calculateViewport(const double& cx, const double& cy, const double& fx, const double& fy) { + // Calculate the viewport + + // Validate that neither fx nor fy are zero + if (fx == 0 || fy == 0) { + throw std::runtime_error("Invalid camera parameters (fx or fy is zero)"); + } + viewport_[0] = -(2.0f*cx - size_[0]) / fx; + viewport_[1] = -(2.0f*cy - size_[1]) / fy; + viewport_[2] = 2.0f* size_[0] / fx; + viewport_[3] = 2.0f* size_[1] / fy; + } + + void Camera::calculateImagePlane(const double& cx, const double& cy, const double& z) { + // Pick a scale factor that places the image plane on the other side of the origin from the camera. + double distance = sqrt(coord_frame_.translation()[0] * coord_frame_.translation()[0] + + coord_frame_.translation()[1] * coord_frame_.translation()[1] + + coord_frame_.translation()[2] * coord_frame_.translation()[2]); + double scale = -1.5 * distance / z; double image_plane_trans[3]; - image_plane_trans[0] = scale*(size_[0] / 2.0 - K[2][0]); - image_plane_trans[1] = scale*(size_[1] / 2.0 - K[2][1]); - image_plane_trans[2] = scale*z; + image_plane_trans[0] = scale * (size_[0] / 2.0 - cx); + image_plane_trans[1] = scale * (size_[1] / 2.0 - cy); + image_plane_trans[2] = scale * z; // Calculate the vertices at the corner of the image plane. double image_plane_center[3]; coord_frame_.point_to_world_space(image_plane_trans, image_plane_center); - double half_width = scale*size_[0] / 2.0; - double half_height = scale*size_[1] / 2.0; + double half_width = scale * size_[0] / 2.0; + double half_height = scale * size_[1] / 2.0; double right[3] = { coord_frame_.rotation()[0], coord_frame_.rotation()[1], @@ -432,33 +474,59 @@ void Camera::loadMayaCam1(const std::string& mayacam) coord_frame_.rotation()[4], coord_frame_.rotation()[5] }; - image_plane_[0] = image_plane_center[0] - half_width*right[0] + - half_height*up[0]; - image_plane_[1] = image_plane_center[1] - half_width*right[1] + - half_height*up[1]; - image_plane_[2] = image_plane_center[2] - half_width*right[2] + - half_height*up[2]; - - image_plane_[3] = image_plane_center[0] - half_width*right[0] - - half_height*up[0]; - image_plane_[4] = image_plane_center[1] - half_width*right[1] - - half_height*up[1]; - image_plane_[5] = image_plane_center[2] - half_width*right[2] - - half_height*up[2]; - - image_plane_[6] = image_plane_center[0] + half_width*right[0] - - half_height*up[0]; - image_plane_[7] = image_plane_center[1] + half_width*right[1] - - half_height*up[1]; - image_plane_[8] = image_plane_center[2] + half_width*right[2] - - half_height*up[2]; - - image_plane_[9] = image_plane_center[0] + half_width*right[0] + - half_height*up[0]; - image_plane_[10] = image_plane_center[1] + half_width*right[1] + - half_height*up[1]; - image_plane_[11] = image_plane_center[2] + half_width*right[2] + - half_height*up[2]; + image_plane_[0] = image_plane_center[0] - half_width * right[0] + + half_height * up[0]; + image_plane_[1] = image_plane_center[1] - half_width * right[1] + + half_height * up[1]; + image_plane_[2] = image_plane_center[2] - half_width * right[2] + + half_height * up[2]; + + image_plane_[3] = image_plane_center[0] - half_width * right[0] - + half_height * up[0]; + image_plane_[4] = image_plane_center[1] - half_width * right[1] - + half_height * up[1]; + image_plane_[5] = image_plane_center[2] - half_width * right[2] - + half_height * up[2]; + + image_plane_[6] = image_plane_center[0] + half_width * right[0] - + half_height * up[0]; + image_plane_[7] = image_plane_center[1] + half_width * right[1] - + half_height * up[1]; + image_plane_[8] = image_plane_center[2] + half_width * right[2] - + half_height * up[2]; + + image_plane_[9] = image_plane_center[0] + half_width * right[0] + + half_height * up[0]; + image_plane_[10] = image_plane_center[1] + half_width * right[1] + + half_height * up[1]; + image_plane_[11] = image_plane_center[2] + half_width * right[2] + + half_height * up[2]; + } + + void Camera::calculateFocalLength(const double& view_angle, double focal_lengths[2]) { + // Convert from deg to rad + double angle_rad = view_angle * (M_PI / 180); + + focal_lengths[0] = size_[0] / (2 * std::tan(angle_rad / 2)); + focal_lengths[1] = size_[1] / (2 * std::tan(angle_rad / 2)); + } + + void Camera::calculateLookAtMatrix(const Vec3d& eye, const Vec3d& center, const Vec3d& up, double matrix[9]) { + // Implementation based off of: + // https://www.khronos.org/opengl/wiki/GluLookAt_code + Vec3d forward = unit(center - eye); + // This vector points to the right-hand side of the camera's orientation. + Vec3d side = unit(cross(forward, up)); + Vec3d perpendicularUp = cross(side, forward); + matrix[0] = side.x; + matrix[1] = side.y; + matrix[2] = side.z; + matrix[3] = perpendicularUp.x; + matrix[4] = perpendicularUp.y; + matrix[5] = perpendicularUp.z; + matrix[6] = -forward.x; + matrix[7] = -forward.y; + matrix[8] = -forward.z; } } // namespace XROMM diff --git a/libautoscoper/src/Camera.hpp b/libautoscoper/src/Camera.hpp index 50b1e113..e662f519 100644 --- a/libautoscoper/src/Camera.hpp +++ b/libautoscoper/src/Camera.hpp @@ -44,6 +44,9 @@ #include +#include +#include + #include "CoordFrame.hpp" namespace xromm @@ -89,6 +92,14 @@ class Camera void loadMayaCam2(const std::string& mayacam); + void loadVTKCamera(const std::string& filename); + + // helper functions + void calculateViewport(const double &cx, const double &cy, const double &fx, const double &fy); + void calculateImagePlane(const double &cx, const double &cy, const double &z); + void calculateFocalLength(const double& view_angle, double focal_lengths[2]); + void calculateLookAtMatrix(const Vec3d& eye, const Vec3d& center, const Vec3d& up, double matrix[9]); + }; } // namespace xromm diff --git a/libautoscoper/src/gpu/opencl/OpenCL.cpp b/libautoscoper/src/gpu/opencl/OpenCL.cpp index 4023c6a2..f3a7d2db 100644 --- a/libautoscoper/src/gpu/opencl/OpenCL.cpp +++ b/libautoscoper/src/gpu/opencl/OpenCL.cpp @@ -993,7 +993,8 @@ void Buffer::copy(const Buffer* dst, size_t size) const { if (size == 0) size = size_; if (size > dst->size_) - ERROR("Destination buffer does not have enough room!"); + ERROR("Destination buffer does not have enough room! (" + << size << " > " << dst->size_ << ")"); err_ = clEnqueueCopyBuffer( queue_, buffer_, dst->buffer_, 0, 0, size, 0, NULL, NULL); CHECK_CL diff --git a/libautoscoper/src/schema/vtkCamera-schema-v1.0.json b/libautoscoper/src/schema/vtkCamera-schema-v1.0.json new file mode 100644 index 00000000..d0300c8f --- /dev/null +++ b/libautoscoper/src/schema/vtkCamera-schema-v1.0.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://autoscoperm.slicer.org/vtk-schema-1.0.json", + "title": "VTK Camera", + "description": "Parameters for recreating a VTK Camera Object", + "type": "object", + "properties": { + "version": { + "description": "The version of the file", + "type": "number" + }, + "focal-point": { + "description": "The point the camera is looking at", + "type": "array", + "items" : { + "type": "number" + }, + "minItems": 3, + "maxItems": 3 + }, + "camera-position": { + "description": "The point the camera is at", + "type": "array", + "items" : { + "type": "number" + }, + "minItems": 3, + "maxItems": 3 + }, + "view-up": { + "description": "The up vector of the camera", + "type": "array", + "items" : { + "type": "number" + }, + "minItems": 3, + "maxItems": 3 + }, + "view-angle" : { + "description": "The viewing angle", + "type": "number" + }, + "image-width": { + "description": "The width of the DRR image", + "type": "integer" + }, + "image-height": { + "description": "The height of the DRR image", + "type": "integer" + } + }, + "required": [ "version","focal-point","camera-position","view-up","view-angle","image-width","image-height" ] +} \ No newline at end of file