diff --git a/elsa/core/CMakeLists.txt b/elsa/core/CMakeLists.txt index 02bda8db47251ef44e1436f296ea1593826129db..f851ca6fad6236d18c50504fc5bbe9d013d43310 100644 --- a/elsa/core/CMakeLists.txt +++ b/elsa/core/CMakeLists.txt @@ -13,6 +13,8 @@ set(MODULE_HEADERS Descriptors/DataDescriptor.h Descriptors/DescriptorUtils.h Descriptors/VolumeDescriptor.h + Descriptors/DetectorDescriptor.h + Descriptors/PlanarDetectorDescriptor.h Descriptors/BlockDescriptor.h Descriptors/IdenticalBlocksDescriptor.h Descriptors/PartitionDescriptor.h @@ -32,9 +34,11 @@ set(MODULE_HEADERS set(MODULE_SOURCES Descriptors/DataDescriptor.cpp Descriptors/VolumeDescriptor.cpp + Descriptors/PlanarDetectorDescriptor.cpp Descriptors/RandomBlocksDescriptor.cpp Descriptors/DescriptorUtils.cpp Descriptors/IdenticalBlocksDescriptor.cpp + Descriptors/DetectorDescriptor.cpp Descriptors/PartitionDescriptor.cpp DataContainer.cpp Handlers/DataHandlerCPU.cpp diff --git a/elsa/core/Descriptors/DetectorDescriptor.cpp b/elsa/core/Descriptors/DetectorDescriptor.cpp new file mode 100644 index 0000000000000000000000000000000000000000..84b86fedf4d52ca9bcfa30cdcd2dd10547b44a67 --- /dev/null +++ b/elsa/core/Descriptors/DetectorDescriptor.cpp @@ -0,0 +1,78 @@ +#include "DetectorDescriptor.h" +#include + +namespace elsa +{ + DetectorDescriptor::DetectorDescriptor(const IndexVector_t& numOfCoeffsPerDim, + const std::vector& geometryList) + : DataDescriptor(numOfCoeffsPerDim), _geometry(geometryList) + { + // TODO Clarify: What about empty geometryList? Do we want to support it, or throw an + // execption? + } + + DetectorDescriptor::DetectorDescriptor(const IndexVector_t& numOfCoeffsPerDim, + const RealVector_t& spacingPerDim, + const std::vector& geometryList) + : DataDescriptor(numOfCoeffsPerDim, spacingPerDim), _geometry(geometryList) + { + } + + DetectorDescriptor::Ray + DetectorDescriptor::computeRayFromDetectorCoord(const index_t detectorIndex) const + { + + // Return empty, if access out of bounds + assert(detectorIndex < getNumberOfCoefficients() + && "PlanarDetectorDescriptor::computeRayToDetector(index_t): Assumption " + "detectorIndex smaller than number of coeffs, broken"); + + auto coord = getCoordinateFromIndex(detectorIndex); + return computeRayFromDetectorCoord(coord); + } + + DetectorDescriptor::Ray + DetectorDescriptor::computeRayFromDetectorCoord(const IndexVector_t coord) const + { + // Assume all of the coordinates are inside of the volume + // auto tmp = (coord.array() < getNumberOfCoefficientsPerDimension().array()); + // assert(tmp.all() + // && "DetectorDescriptor::computeRayToDetector(IndexVector_t): Assumption coord " + // "in bound wrong"); + + auto dim = getNumberOfDimensions(); + + // Cast to real_t and shift to center of pixel + auto detectorCoord = coord.head(dim - 1).template cast().array() + 0.5; + + // Last dimension is always the pose index + auto poseIndex = coord[dim - 1]; + + return computeRayFromDetectorCoord(detectorCoord, poseIndex); + } + + index_t DetectorDescriptor::getNumberOfGeometryPoses() const { return _geometry.size(); } + + std::optional DetectorDescriptor::getGeometryAt(const index_t index) const + { + if (_geometry.size() <= std::make_unsigned_t(index)) + return {}; + + return _geometry[index]; + } + + bool DetectorDescriptor::isEqual(const DataDescriptor& other) const + { + if (!DataDescriptor::isEqual(other)) + return false; + + // static cast as type checked in base comparison + auto otherBlock = static_cast(&other); + + if (getNumberOfGeometryPoses() != otherBlock->getNumberOfGeometryPoses()) + return false; + + return std::equal(std::cbegin(_geometry), std::cend(_geometry), + std::cbegin(otherBlock->_geometry)); + } +} // namespace elsa diff --git a/elsa/core/Descriptors/DetectorDescriptor.h b/elsa/core/Descriptors/DetectorDescriptor.h new file mode 100644 index 0000000000000000000000000000000000000000..c9d893ea77a08cc52fedbcbd921b8feb433c98c8 --- /dev/null +++ b/elsa/core/Descriptors/DetectorDescriptor.h @@ -0,0 +1,87 @@ +#pragma once + +#include "elsaDefines.h" +#include "DataDescriptor.h" +#include "Geometry.h" + +#include +#include "Eigen/Geometry" + +namespace elsa +{ + /** + * @brief Class representing metadata for lineraized n-dimensional signal stored in memory. It + * is a base class for different type signals caputred by some kind of detectors (i.e. a planar + * detector, curved or some other shaped detector). Is additionally stored information about the + * different poses of a trajectory. + */ + class DetectorDescriptor : public DataDescriptor + { + public: + using Ray = Eigen::ParametrizedLine; + + public: + /// There is not default signal + DetectorDescriptor() = delete; + + /// Default destructor + ~DetectorDescriptor() override = default; + + /** + * @brief Construct a DetectorDescriptor with a number of coefficients for each dimension + * and a list of geometry poses + */ + DetectorDescriptor(const IndexVector_t& numOfCoeffsPerDim, + const std::vector& geometryList); + /** + * @brief Construct a DetectorDescriptor with a number of coefficients and spacing for each + * dimension and a list of geometry poses + */ + DetectorDescriptor(const IndexVector_t& numOfCoeffsPerDim, + const RealVector_t& spacingPerDim, + const std::vector& geometryList); + + /** + * @brief Overload of computeRayToDetector with a single detectorIndex. Compute the pose and + * coord index using getCoordinateFromIndex and call overload + */ + Ray computeRayFromDetectorCoord(const index_t detectorIndex) const; + + /** + * @brief Overload of computeRayToDetector with a single coord vector. This vector encodes + * the pose index and the detector coordinate. So for a 1D detector, this will be 2D and the + * second dimension, will reference the pose index + */ + Ray computeRayFromDetectorCoord(const IndexVector_t coord) const; + + /** + * @brief Compute a ray from the source from a pose to the given detector coordinate + * + * @param[in] detectorCoord Vector of size dim - 1, specifying the coordinate the ray should + * hit + * @param[in] poseIndex index into geometryList array, which pose to use for ray computation + * + */ + virtual Ray computeRayFromDetectorCoord(const RealVector_t& detectorCoord, + const index_t poseIndex) const = 0; + + /** + * @brief Compute a ray from the source to trougth a pixel/voxel + */ + virtual RealVector_t computeDetectorCoordFromRay(const Ray& ray, + const index_t poseIndex) const = 0; + + /// Get the number of poses used in the geometry + index_t getNumberOfGeometryPoses() const; + + /// Get the i-th geometry in the trajectory. + std::optional getGeometryAt(const index_t index) const; + + protected: + /// implement the polymorphic comparison operation + bool isEqual(const DataDescriptor& other) const override; + + /// List of geometry poses + std::vector _geometry; + }; +} // namespace elsa diff --git a/elsa/core/Descriptors/PlanarDetectorDescriptor.cpp b/elsa/core/Descriptors/PlanarDetectorDescriptor.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b26619c1044a8e24a24058d86551a952dfd2d4ff --- /dev/null +++ b/elsa/core/Descriptors/PlanarDetectorDescriptor.cpp @@ -0,0 +1,85 @@ +#include "PlanarDetectorDescriptor.h" +#include + +namespace elsa +{ + PlanarDetectorDescriptor::PlanarDetectorDescriptor(const IndexVector_t& numOfCoeffsPerDim, + const std::vector& geometryList) + : DetectorDescriptor(numOfCoeffsPerDim, geometryList) + { + } + PlanarDetectorDescriptor::PlanarDetectorDescriptor(const IndexVector_t& numOfCoeffsPerDim, + const RealVector_t& spacingPerDim, + const std::vector& geometryList) + : DetectorDescriptor(numOfCoeffsPerDim, spacingPerDim, geometryList) + { + } + + DetectorDescriptor::Ray + PlanarDetectorDescriptor::computeRayFromDetectorCoord(const RealVector_t& detectorCoord, + const index_t poseIndex) const + { + // Assert that for all dimension of detectorCoord is in bounds and poseIndex can + // be index in the _geometry. If not the calculation will not be correct, but + // as this is the hot path, I don't want execptions and unpacking everything + // We'll just have to ensure, that we don't mess up in our hot path! :-) + assert((detectorCoord.block(0, 0, getNumberOfDimensions() - 1, 0).array() + < getNumberOfCoefficientsPerDimension() + .block(0, 0, getNumberOfDimensions() - 1, 0) + .template cast() + .array()) + .all() + && "PlanarDetectorDescriptor::computeRayToDetector: Assumption detectorCoord in " + "bounds, wrong"); + assert(std::make_unsigned_t(poseIndex) < _geometry.size() + && "PlanarDetectorDescriptor::computeRayToDetector: Assumption poseIndex smaller " + "than number of poses, wrong"); + + auto dim = getNumberOfDimensions(); + + // get the pose of trajectory + auto geometry = _geometry[std::make_unsigned_t(poseIndex)]; + + auto projInvMatrix = geometry.getInverseProjectionMatrix(); + + // homogeneous coordinates [p;1], with p in detector space + RealVector_t homogeneousPixelCoord(dim); + homogeneousPixelCoord << detectorCoord, 1; + + // Camera center is always the ray origin + auto ro = geometry.getCameraCenter(); + + auto rd = (projInvMatrix * homogeneousPixelCoord) // Matrix-Vector multiplication + .head(dim) // Transform to non-homogeneous + .normalized(); // normalize vector + + return Ray(ro, rd); + } + + RealVector_t + PlanarDetectorDescriptor::computeDetectorCoordFromRay(const Ray& ray, + const index_t poseIndex) const + { + auto dim = getNumberOfDimensions(); + auto geometry = _geometry[(poseIndex)]; + + auto projMatrix = geometry.getProjectionMatrix(); + + // Only take the square matrix part + auto pixel = (projMatrix.block(0, 0, dim, dim) * ray.direction()).head(dim - 1); + + return pixel; + } + + bool PlanarDetectorDescriptor::isEqual(const DataDescriptor& other) const + { + // PlanarDetectorDescriptor has no data, so just deligate it to base class + return DetectorDescriptor::isEqual(other); + } + + PlanarDetectorDescriptor* PlanarDetectorDescriptor::cloneImpl() const + { + return new PlanarDetectorDescriptor(getNumberOfCoefficientsPerDimension(), + getSpacingPerDimension(), _geometry); + } +} // namespace elsa diff --git a/elsa/core/Descriptors/PlanarDetectorDescriptor.h b/elsa/core/Descriptors/PlanarDetectorDescriptor.h new file mode 100644 index 0000000000000000000000000000000000000000..7a66b22944c2989624d4e0df6ac841422659685c --- /dev/null +++ b/elsa/core/Descriptors/PlanarDetectorDescriptor.h @@ -0,0 +1,52 @@ +#pragma once + +#include "DetectorDescriptor.h" + +namespace elsa +{ + /** + * @brief Class representing metadata for lineraized n-dimensional signal stored in memory. It + * specifically describes signals, which were captured by a planar detector and stores + * additional information such as different poses + * + * @author David Frank - initial code + */ + class PlanarDetectorDescriptor : public DetectorDescriptor + { + using DetectorDescriptor::Ray; + + public: + PlanarDetectorDescriptor() = delete; + + ~PlanarDetectorDescriptor() = default; + + /** + * @brief Construct a PlanatDetectorDescriptor with given number of coefficients and spacing + * per dimension and a list of geometry poses in the trajectory + */ + PlanarDetectorDescriptor(const IndexVector_t& numOfCoeffsPerDim, + const RealVector_t& spacingPerDim, + const std::vector& geometryList); + + /** + * @brief Construct a PlanatDetectorDescriptor with given number of coefficients + * per dimension and a list of geometry poses in the trajectory + */ + PlanarDetectorDescriptor(const IndexVector_t& numOfCoeffsPerDim, + const std::vector& geometryList); + + using DetectorDescriptor::computeRayFromDetectorCoord; + + /// Override function to compute rays for a planar detector + Ray computeRayFromDetectorCoord(const RealVector_t& detectorCoord, + const index_t poseIndex) const override; + + RealVector_t computeDetectorCoordFromRay(const Ray& ray, + const index_t poseIndex) const override; + + private: + PlanarDetectorDescriptor* cloneImpl() const override; + + bool isEqual(const DataDescriptor& other) const override; + }; +} // namespace elsa diff --git a/elsa/core/Geometry.cpp b/elsa/core/Geometry.cpp index 7faa103c7c60d8a5322c453b380aa396ae4cceba..7a38408e45c734e96b50c08d010bcbc1f353f708 100644 --- a/elsa/core/Geometry.cpp +++ b/elsa/core/Geometry.cpp @@ -147,20 +147,6 @@ namespace elsa buildMatrices(); } - std::pair Geometry::computeRayTo(const RealVector_t& p) const - { - // the ray origin is always the camera center - RealVector_t ro = _C; - - // homogeneous coordinates [p;1] - p is in detector space - RealVector_t homP(_objectDimension); - homP << p, 1; - - // multiplication of inverse projection matrix and homogeneous detector coordinate - auto rd = (_Pinv * homP).normalized(); - return std::make_pair(ro, rd.head(_objectDimension)); - } - const RealMatrix_t& Geometry::getProjectionMatrix() const { return _P; } const RealMatrix_t& Geometry::getInverseProjectionMatrix() const { return _Pinv; } diff --git a/elsa/core/Geometry.h b/elsa/core/Geometry.h index afe4f09506bcc20b5d7da4e6d49a2eeb343805c3..dd65d4059049370956906869bbf0511ec61aec83 100644 --- a/elsa/core/Geometry.h +++ b/elsa/core/Geometry.h @@ -109,18 +109,6 @@ namespace elsa real_t centerOfRotationOffsetY = static_cast(0.0), real_t centerOfRotationOffsetZ = static_cast(0.0)); - /** - * @brief Compute a ray (ray origin and ray direction) that hits the specified point p on - * the detector - * - * @param[in] p point p on detector - * - * @returns pair of , where ro = ray origin and rd = ray direction - * - * Computation are done using the projection matrix. - */ - std::pair computeRayTo(const RealVector_t& p) const; - /** * @brief Return the projection matrix * diff --git a/elsa/core/StrongTypes.h b/elsa/core/StrongTypes.h index 98e9e0c888d392a07588848cd5892367e522cb86..71f91536f48f9f979c333c0f4fd8dacbcf45569a 100644 --- a/elsa/core/StrongTypes.h +++ b/elsa/core/StrongTypes.h @@ -248,9 +248,9 @@ namespace elsa } /// Access to gamma - constexpr Radian gamma() const { return operator[](0); } + constexpr Radian gamma() const { return operator[](0u); } /// Access to beta - constexpr Radian beta() const { return operator[](1); } + constexpr Radian beta() const { return operator[](1u); } /// Access to alpha constexpr Radian alpha() const { return operator[](2); } }; @@ -689,4 +689,4 @@ namespace std struct tuple_element { using type = decltype(std::declval().get()); }; -} // namespace std \ No newline at end of file +} // namespace std diff --git a/elsa/core/tests/CMakeLists.txt b/elsa/core/tests/CMakeLists.txt index 600753313ecf89f33995eff326f411297f0acfe4..2ebbf1b4ab29ee47feefc5e04a51f0c42dbbe6f1 100644 --- a/elsa/core/tests/CMakeLists.txt +++ b/elsa/core/tests/CMakeLists.txt @@ -17,6 +17,7 @@ ELSA_TEST(LinearOperator) ELSA_TEST(ExpressionTemplates) ELSA_TEST(Geometry) ELSA_TEST(StrongTypes) +ELSA_TEST(PlanarDetectorDescriptor) if(ELSA_BENCHMARKS) ELSA_TEST(BenchmarkExpressionTemplates) diff --git a/elsa/core/tests/test_Geometry.cpp b/elsa/core/tests/test_Geometry.cpp index 9650d893c99b75f4e818443390d8db294af949c9..9008184372beffe42b0fbb9b70602513923848c8 100644 --- a/elsa/core/tests/test_Geometry.cpp +++ b/elsa/core/tests/test_Geometry.cpp @@ -64,24 +64,6 @@ SCENARIO("Testing 2D geometries") REQUIRE((c[0] - o[0]) == Approx(0)); REQUIRE((c[1] - o[1] + s2c) == Approx(0)); } - - THEN("rays make sense") - { - // test outer + central pixels - for (real_t detPixel : std::initializer_list{0.5, 2.5, 4.5}) { - RealVector_t pixel(1); - pixel << detPixel; - auto [ro, rd] = g.computeRayTo(pixel); - - auto c = g.getCameraCenter(); - REQUIRE((ro - c).sum() == Approx(0)); - - real_t factor = - (std::abs(rd[0]) > 0) ? ((pixel[0] - ro[0]) / rd[0]) : (s2c + c2d); - real_t detCoordY = ro[1] + factor * rd[1]; - REQUIRE(detCoordY == Approx(ddVol.getLocationOfOrigin()[1] + c2d)); - } - } } WHEN("testing geometry without rotation but with principal point offset") @@ -123,25 +105,6 @@ SCENARIO("Testing 2D geometries") REQUIRE((c[0] - o[0]) == Approx(0)); REQUIRE((c[1] - o[1] + s2c) == Approx(0)); } - - THEN("rays make sense") - { - // test outer + central pixels - for (real_t detPixel : std::initializer_list{0.5, 2.5, 4.5}) { - RealVector_t pixel(1); - pixel << detPixel; - auto [ro, rd] = g.computeRayTo(pixel); - - auto c = g.getCameraCenter(); - REQUIRE((ro - c).sum() == Approx(0)); - - real_t factor = (std::abs(rd[0]) >= Approx(0.001)) - ? ((pixel[0] - ro[0] - px) / rd[0]) - : (s2c + c2d); - real_t detCoordY = ro[1] + factor * rd[1]; - REQUIRE(detCoordY == Approx(ddVol.getLocationOfOrigin()[1] + c2d)); - } - } } WHEN("testing geometry with 90 degree rotation and no offsets") @@ -181,24 +144,6 @@ SCENARIO("Testing 2D geometries") REQUIRE((c[0] - o[0] + s2c) == Approx(0)); REQUIRE((c[1] - o[1]) == Approx(0).margin(0.000001)); } - - THEN("rays make sense") - { - // test outer + central pixels - for (real_t detPixel : std::initializer_list{0.5, 2.5, 4.5}) { - RealVector_t pixel(1); - pixel << detPixel; - auto [ro, rd] = g.computeRayTo(pixel); - - auto c = g.getCameraCenter(); - REQUIRE((ro - c).sum() == Approx(0)); - - real_t factor = - (std::abs(rd[1]) > 0.0000001) ? ((ro[1] - pixel[0]) / rd[1]) : (s2c + c2d); - real_t detCoordX = ro[0] + factor * rd[0]; - REQUIRE(detCoordX == Approx(ddVol.getLocationOfOrigin()[0] + c2d)); - } - } } WHEN("testing geometry with 45 degree rotation and offset center of rotation") @@ -249,30 +194,6 @@ SCENARIO("Testing 2D geometries") REQUIRE((c[0] - newX) == Approx(0).margin(0.000001)); REQUIRE((c[1] - newY) == Approx(0).margin(0.000001)); } - - THEN("rays make sense") - { - // test outer + central pixels - for (real_t detPixel : std::initializer_list{0.5, 2.5, 4.5}) { - RealVector_t pixel(1); - pixel << detPixel; - auto [ro, rd] = g.computeRayTo(pixel); - - auto c = g.getCameraCenter(); - REQUIRE((ro - c).sum() == Approx(0.0)); - - auto o = ddVol.getLocationOfOrigin(); - RealVector_t detCoordWorld(2); - detCoordWorld << detPixel - o[0], c2d; - RealVector_t rotD = g.getRotationMatrix().transpose() * detCoordWorld; - rotD[0] += o[0] + cx; - rotD[1] += o[1] + cy; - - real_t factor = (rotD[0] - ro[0]) / rd[0]; // rd[0] won't be 0 here! - real_t detCoord = ro[1] + factor * rd[1]; - REQUIRE(detCoord == Approx(rotD[1])); - } - } } } } @@ -331,37 +252,6 @@ SCENARIO("Testing 3D geometries") REQUIRE((c[1] - o[1]) == Approx(0)); REQUIRE((c[2] - o[2] + s2c) == Approx(0)); } - - THEN("rays make sense") - { - // test outer + central pixels - for (real_t detPixel1 : std::initializer_list{0.5, 2.5, 4.5}) { - for (real_t detPixel2 : std::initializer_list{0.5, 2.5, 4.5}) { - RealVector_t pixel(2); - pixel << detPixel1, detPixel2; - auto [ro, rd] = g.computeRayTo(pixel); - - auto c = g.getCameraCenter(); - REQUIRE((ro - c).sum() == Approx(0)); - - auto o = ddVol.getLocationOfOrigin(); - RealVector_t detCoordWorld(3); - detCoordWorld << detPixel1 - o[0], detPixel2 - o[1], c2d; - RealVector_t rotD = g.getRotationMatrix().transpose() * detCoordWorld + o; - - real_t factor = 0; - if (std::abs(rd[0]) > 0) - factor = (rotD[0] - ro[0]) / rd[0]; - else if (std::abs(rd[1]) > 0) - factor = (rotD[1] - ro[1]) / rd[1]; - else if (std::abs(rd[2]) > 0) - factor = (rotD[2] - ro[2] / rd[2]); - REQUIRE((ro[0] + factor * rd[0]) == Approx(rotD[0])); - REQUIRE((ro[1] + factor * rd[1]) == Approx(rotD[1])); - REQUIRE((ro[2] + factor * rd[2]) == Approx(rotD[2])); - } - } - } } WHEN("testing geometry without rotation but with principal point offsets") @@ -406,37 +296,6 @@ SCENARIO("Testing 3D geometries") REQUIRE((c[1] - o[1]) == Approx(0)); REQUIRE((c[2] - o[2] + s2c) == Approx(0)); } - - THEN("rays make sense") - { - // test outer + central pixels - for (real_t detPixel1 : std::initializer_list{0.5, 2.5, 4.5}) { - for (real_t detPixel2 : std::initializer_list{0.5, 2.5, 4.5}) { - RealVector_t pixel(2); - pixel << detPixel1, detPixel2; - auto [ro, rd] = g.computeRayTo(pixel); - - auto c = g.getCameraCenter(); - REQUIRE((ro - c).sum() == Approx(0)); - - auto o = ddVol.getLocationOfOrigin(); - RealVector_t detCoordWorld(3); - detCoordWorld << detPixel1 - o[0] - px, detPixel2 - o[1] - py, c2d; - RealVector_t rotD = g.getRotationMatrix().transpose() * detCoordWorld + o; - - real_t factor = 0; - if (std::abs(rd[0]) > 0) - factor = (rotD[0] - ro[0]) / rd[0]; - else if (std::abs(rd[1]) > 0) - factor = (rotD[1] - ro[1]) / rd[1]; - else if (std::abs(rd[2]) > 0) - factor = (rotD[2] - ro[2] / rd[2]); - REQUIRE((ro[0] + factor * rd[0]) == Approx(rotD[0])); - REQUIRE((ro[1] + factor * rd[1]) == Approx(rotD[1])); - REQUIRE((ro[2] + factor * rd[2]) == Approx(rotD[2])); - } - } - } } WHEN("testing geometry with 90 degree rotation and no offsets") @@ -481,37 +340,6 @@ SCENARIO("Testing 3D geometries") REQUIRE((c[1] - o[1]) == Approx(0).margin(0.000001)); REQUIRE((c[2] - o[2]) == Approx(0).margin(0.000001)); } - - THEN("rays make sense") - { - // test outer + central pixels - for (real_t detPixel1 : std::initializer_list{0.5, 2.5, 4.5}) { - for (real_t detPixel2 : std::initializer_list{0.5, 2.5, 4.5}) { - RealVector_t pixel(2); - pixel << detPixel1, detPixel2; - auto [ro, rd] = g.computeRayTo(pixel); - - auto c = g.getCameraCenter(); - REQUIRE((ro - c).sum() == Approx(0)); - - auto o = ddVol.getLocationOfOrigin(); - RealVector_t detCoordWorld(3); - detCoordWorld << detPixel1 - o[0], detPixel2 - o[1], c2d; - RealVector_t rotD = g.getRotationMatrix().transpose() * detCoordWorld + o; - - real_t factor = 0; - if (std::abs(rd[0]) > 0) - factor = (rotD[0] - ro[0]) / rd[0]; - else if (std::abs(rd[1]) > 0) - factor = (rotD[1] - ro[1]) / rd[1]; - else if (std::abs(rd[2]) > 0) - factor = (rotD[2] - ro[2] / rd[2]); - REQUIRE((ro[0] + factor * rd[0]) == Approx(rotD[0])); - REQUIRE((ro[1] + factor * rd[1]) == Approx(rotD[1])); - REQUIRE((ro[2] + factor * rd[2]) == Approx(rotD[2])); - } - } - } } WHEN("testing geometry with 45/22.5 degree rotation and offset center of rotation") @@ -567,38 +395,6 @@ SCENARIO("Testing 3D geometries") REQUIRE((rotSrc - c).sum() == Approx(0).margin(0.000001)); } - - THEN("rays make sense") - { - // test outer + central pixels - for (real_t detPixel1 : std::initializer_list{0.5, 2.5, 4.5}) { - for (real_t detPixel2 : std::initializer_list{0.5, 2.5, 4.5}) { - RealVector_t pixel(2); - pixel << detPixel1, detPixel2; - auto [ro, rd] = g.computeRayTo(pixel); - - auto c = g.getCameraCenter(); - REQUIRE((ro - c).sum() == Approx(0)); - - auto o = ddVol.getLocationOfOrigin(); - RealVector_t detCoordWorld(3); - detCoordWorld << detPixel1 - o[0], detPixel2 - o[1], c2d; - RealVector_t rotD = - g.getRotationMatrix().transpose() * detCoordWorld + o + offset; - - real_t factor = 0; - if (std::abs(rd[0]) > 0.000001) - factor = (rotD[0] - ro[0]) / rd[0]; - else if (std::abs(rd[1]) > 0.000001) - factor = (rotD[1] - ro[1]) / rd[1]; - else if (std::abs(rd[2]) > 0.000001) - factor = (rotD[2] - ro[2] / rd[2]); - REQUIRE((ro[0] + factor * rd[0]) == Approx(rotD[0])); - REQUIRE((ro[1] + factor * rd[1]) == Approx(rotD[1])); - REQUIRE((ro[2] + factor * rd[2]) == Approx(rotD[2])); - } - } - } } WHEN("testing geometry with 45/22.5/12.25 degree rotation as a rotation matrix") @@ -654,37 +450,6 @@ SCENARIO("Testing 3D geometries") REQUIRE((rotSrc - c).sum() == Approx(0).margin(0.00001)); } - - THEN("rays make sense") - { - // test outer + central pixels - for (real_t detPixel1 : std::initializer_list{0.5, 2.5, 4.5}) { - for (real_t detPixel2 : std::initializer_list{0.5, 2.5, 4.5}) { - RealVector_t pixel(2); - pixel << detPixel1, detPixel2; - auto [ro, rd] = g.computeRayTo(pixel); - - auto c = g.getCameraCenter(); - REQUIRE((ro - c).sum() == Approx(0)); - - auto o = ddVol.getLocationOfOrigin(); - RealVector_t detCoordWorld(3); - detCoordWorld << detPixel1 - o[0], detPixel2 - o[1], c2d; - RealVector_t rotD = g.getRotationMatrix().transpose() * detCoordWorld + o; - - real_t factor = 0; - if (std::abs(rd[0]) > 0.000001) - factor = (rotD[0] - ro[0]) / rd[0]; - else if (std::abs(rd[1]) > 0.000001) - factor = (rotD[1] - ro[1]) / rd[1]; - else if (std::abs(rd[2]) > 0.000001) - factor = (rotD[2] - ro[2] / rd[2]); - REQUIRE((ro[0] + factor * rd[0]) == Approx(rotD[0])); - REQUIRE((ro[1] + factor * rd[1]) == Approx(rotD[1])); - REQUIRE((ro[2] + factor * rd[2]) == Approx(rotD[2])); - } - } - } } } -} \ No newline at end of file +} diff --git a/elsa/core/tests/test_PlanarDetectorDescriptor.cpp b/elsa/core/tests/test_PlanarDetectorDescriptor.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6246b2f4ddd7fe4c7ab52a98d1300933e907e637 --- /dev/null +++ b/elsa/core/tests/test_PlanarDetectorDescriptor.cpp @@ -0,0 +1,249 @@ +/** + * @file test_PlanarDetectorDescriptor.cpp + * + * @brief Test for PlanarDetectorDescriptor + * + * @author David Frank - initial code + */ + +#include + +#include "PlanarDetectorDescriptor.h" +#include "VolumeDescriptor.h" + +#include + +using namespace elsa; +using namespace elsa::geometry; + +using Ray = DetectorDescriptor::Ray; + +SCENARIO("Testing 2D PlanarDetectorDescriptor") +{ + GIVEN("Given a 5x5 Volume and a single 5 wide detector pose") + { + IndexVector_t volSize(2); + volSize << 5, 5; + VolumeDescriptor ddVol(volSize); + + IndexVector_t sinoSize(2); + sinoSize << 5, 1; + + real_t s2c = 10; + real_t c2d = 4; + + Geometry g(SourceToCenterOfRotation{s2c}, CenterOfRotationToDetector{c2d}, Radian{0}, + VolumeData2D{Size2D{volSize}}, SinogramData2D{Size2D{sinoSize}}); + + PlanarDetectorDescriptor desc(sinoSize, {g}); + + WHEN("Retreiving the single geometry pose") + { + auto geom = desc.getGeometryAt(0); + + CHECK(desc.getNumberOfGeometryPoses() == 1); + + THEN("Geometry is equal") { CHECK((geom) == g); } + } + + WHEN("Generating rays for detecor pixels 0, 2 and 4") + { + for (real_t detPixel : std::initializer_list{0, 2, 4}) { + RealVector_t pixel(1); + pixel << detPixel + 0.5f; + + // Check that ray for IndexVector_t is equal to previous one + auto ray = desc.computeRayFromDetectorCoord(pixel, 0); + + // Create variables, which make typing quicker + auto ro = ray.origin(); + auto rd = ray.direction(); + + // Check that ray origin is camera center + auto c = g.getCameraCenter(); + CHECK((ro - c).sum() == Approx(0)); + + // compute intersection manually + real_t t = Approx(rd[0]) == 0 ? (s2c + c2d) : ((pixel[0] - ro[0]) / rd[0]); + + auto detCoordY = ro[1] + t * rd[1]; + + CHECK(detCoordY == Approx(ddVol.getLocationOfOrigin()[1] + c2d)); + } + } + WHEN("Computing detector coord from ray") + { + auto ro = g.getCameraCenter(); + auto rd = RealVector_t(2); + + THEN("The detector coord is correct") + { + rd << -0.141421f, 0.989949f; + rd.normalize(); + + auto detCoord = desc.computeDetectorCoordFromRay(Ray(ro, rd), 0); + CHECK(detCoord[0] == Approx(0.5).margin(0.05)); + } + THEN("The detector coord is correct asd") + { + rd << 0.f, 1.f; + rd.normalize(); + + auto detCoord = desc.computeDetectorCoordFromRay(Ray(ro, rd), 0); + CHECK(detCoord[0] == Approx(2.5)); + } + + THEN("The detector coord is correct asd") + { + rd << 0.141421f, 0.989949f; + rd.normalize(); + + auto detCoord = desc.computeDetectorCoordFromRay(Ray(ro, rd), 0); + CHECK(detCoord[0] == Approx(4.5).margin(0.05)); + } + } + + GIVEN("Given a 5x5 Volume and a multiple 5 wide detector pose") + { + IndexVector_t volSize(2); + volSize << 5, 5; + VolumeDescriptor ddVol(volSize); + + IndexVector_t sinoSize(2); + sinoSize << 5, 4; + + real_t s2c = 10; + real_t c2d = 4; + + Geometry g1(SourceToCenterOfRotation{s2c}, CenterOfRotationToDetector{c2d}, Degree{0}, + VolumeData2D{Size2D{volSize}}, SinogramData2D{Size2D{sinoSize}}); + Geometry g2(SourceToCenterOfRotation{s2c}, CenterOfRotationToDetector{c2d}, Degree{90}, + VolumeData2D{Size2D{volSize}}, SinogramData2D{Size2D{sinoSize}}); + Geometry g3(SourceToCenterOfRotation{s2c}, CenterOfRotationToDetector{c2d}, Degree{180}, + VolumeData2D{Size2D{volSize}}, SinogramData2D{Size2D{sinoSize}}); + Geometry g4(SourceToCenterOfRotation{s2c}, CenterOfRotationToDetector{c2d}, Degree{270}, + VolumeData2D{Size2D{volSize}}, SinogramData2D{Size2D{sinoSize}}); + + PlanarDetectorDescriptor desc(sinoSize, {g1, g2, g3, g4}); + + WHEN("Retreiving the single geometry pose") + { + auto geom = desc.getGeometryAt(0); + + CHECK(desc.getNumberOfGeometryPoses() == 4); + + THEN("Geometry is equal") { CHECK((desc.getGeometryAt(0)) == g1); } + THEN("Geometry is equal") { CHECK((desc.getGeometryAt(1)) == g2); } + THEN("Geometry is equal") { CHECK((desc.getGeometryAt(2)) == g3); } + THEN("Geometry is equal") { CHECK((desc.getGeometryAt(3)) == g4); } + } + + WHEN("Check for multiple poses, that all the overloads compute the same rays") + { + for (index_t pose : {0, 1, 2, 3}) { + + for (index_t detPixel : {0, 2, 4}) { + IndexVector_t pixel(2); + pixel << detPixel, pose; + + RealVector_t pixelReal(1); + pixelReal << static_cast(detPixel) + 0.5f; + + auto ray1 = + desc.computeRayFromDetectorCoord(desc.getIndexFromCoordinate(pixel)); + auto ray2 = desc.computeRayFromDetectorCoord(pixel); + auto ray3 = desc.computeRayFromDetectorCoord(pixelReal, pose); + + auto ro1 = ray1.origin(); + auto rd1 = ray1.direction(); + + auto ro2 = ray2.origin(); + auto rd2 = ray2.direction(); + + auto ro3 = ray3.origin(); + auto rd3 = ray3.direction(); + + CHECK(ro1 == ro2); + CHECK(ro1 == ro3); + + CHECK(rd1 == rd2); + CHECK(rd1 == rd3); + + // Shouldn't be necessary, but whatever + CHECK(ro2 == ro3); + CHECK(rd2 == rd3); + } + } + } + } + } +} +SCENARIO("Testing 3D PlanarDetectorDescriptor") +{ + GIVEN("Given a 5x5x5 Volume and a single 5x5 wide detector pose") + { + IndexVector_t volSize(3); + volSize << 5, 5, 5; + VolumeDescriptor ddVol(volSize); + + IndexVector_t sinoSize(3); + sinoSize << 5, 5, 1; + + real_t s2c = 10; + real_t c2d = 4; + + Geometry g(SourceToCenterOfRotation{s2c}, CenterOfRotationToDetector{c2d}, + VolumeData3D{Size3D{volSize}}, SinogramData3D{Size3D{sinoSize}}, + RotationAngles3D{Gamma{0}}); + + PlanarDetectorDescriptor desc(sinoSize, {g}); + + WHEN("Retreiving the single geometry pose") + { + auto geom = desc.getGeometryAt(0); + + CHECK(desc.getNumberOfGeometryPoses() == 1); + + THEN("Geometry is equal") { CHECK((geom) == g); } + } + + WHEN("Generating rays for detecor pixels 0, 2 and 4 for each dim") + { + for (index_t detPixel1 : {0, 2, 4}) { + for (index_t detPixel2 : {0, 2, 4}) { + RealVector_t pixel(2); + pixel << static_cast(detPixel1) + 0.5f, + static_cast(detPixel2) + 0.5f; + + // Check that ray for IndexVector_t is equal to previous one + auto ray = desc.computeRayFromDetectorCoord(pixel, 0); + + // Create variables, which make typing quicker + auto ro = ray.origin(); + auto rd = ray.direction(); + + // Check that ray origin is camera center + auto c = g.getCameraCenter(); + CHECK((ro - c).sum() == Approx(0)); + + auto o = ddVol.getLocationOfOrigin(); + RealVector_t detCoordWorld(3); + detCoordWorld << pixel[0] - o[0], pixel[1] - o[1], c2d; + RealVector_t rotD = g.getRotationMatrix().transpose() * detCoordWorld + o; + + real_t factor = 0; + if (std::abs(rd[0]) > 0) + factor = (rotD[0] - ro[0]) / rd[0]; + else if (std::abs(rd[1]) > 0) + factor = (rotD[1] - ro[1]) / rd[1]; + else if (std::abs(rd[2]) > 0) + factor = (rotD[2] - ro[2] / rd[2]); + + CHECK((ro[0] + factor * rd[0]) == Approx(rotD[0])); + CHECK((ro[1] + factor * rd[1]) == Approx(rotD[1])); + CHECK((ro[2] + factor * rd[2]) == Approx(rotD[2])); + } + } + } + } +} diff --git a/elsa/generators/CircleTrajectoryGenerator.cpp b/elsa/generators/CircleTrajectoryGenerator.cpp index 1c76c1176f915ea59b4e7b8f6f5b8517b78f2a3d..b39f62bfcbd05d72a77890134276fbf381c76721 100644 --- a/elsa/generators/CircleTrajectoryGenerator.cpp +++ b/elsa/generators/CircleTrajectoryGenerator.cpp @@ -1,16 +1,15 @@ #include "CircleTrajectoryGenerator.h" #include "Logger.h" #include "VolumeDescriptor.h" +#include "PlanarDetectorDescriptor.h" #include namespace elsa { - std::pair, std::unique_ptr> - CircleTrajectoryGenerator::createTrajectory(index_t numberOfPoses, - const DataDescriptor& volumeDescriptor, - index_t arcDegrees, real_t sourceToCenter, - real_t centerToDetector) + std::unique_ptr CircleTrajectoryGenerator::createTrajectory( + index_t numberOfPoses, const DataDescriptor& volumeDescriptor, index_t arcDegrees, + real_t sourceToCenter, real_t centerToDetector) { // pull in geometry namespace, to reduce cluttering using namespace geometry; @@ -37,8 +36,6 @@ namespace elsa volumeDescriptor.getSpacingPerDimension()[1], 1; } - VolumeDescriptor sinoDescriptor(coeffs, spacing); - std::vector geometryList; real_t angleIncrement = static_cast(1.0f) * static_cast(arcDegrees) @@ -64,7 +61,7 @@ namespace elsa } } - return std::make_pair(geometryList, sinoDescriptor.clone()); + return std::make_unique(coeffs, spacing, geometryList); } } // namespace elsa diff --git a/elsa/generators/CircleTrajectoryGenerator.h b/elsa/generators/CircleTrajectoryGenerator.h index 8e13919ac7951166ab91a7b9d060d24a93b1fb36..443346992f2452c9b3754c1eb35146a5c0a97299 100644 --- a/elsa/generators/CircleTrajectoryGenerator.h +++ b/elsa/generators/CircleTrajectoryGenerator.h @@ -2,6 +2,8 @@ #include "Geometry.h" +#include "DetectorDescriptor.h" + #include #include @@ -35,8 +37,11 @@ namespace elsa * For example: 3 poses over a 180 arc will yield: 0, 90, 180 degrees. * * Please note: the sinogram size/spacing will match the volume size/spacing. + * + * TODO: Make it possible to return either PlanarDetectorDescriptor, or + * CurvedDetectorDescriptor */ - static std::pair, std::unique_ptr> + static std::unique_ptr createTrajectory(index_t numberOfPoses, const DataDescriptor& volumeDescriptor, index_t arcDegrees, real_t sourceToCenter, real_t centerToDetector); }; diff --git a/elsa/generators/tests/test_CircleTrajectoryGenerator.cpp b/elsa/generators/tests/test_CircleTrajectoryGenerator.cpp index cee3193d2e83b98b8ef5c4adc7c4431f6dba0046..b2dbc0421e2bba4e4772f4197cf301f5e718fbda 100644 --- a/elsa/generators/tests/test_CircleTrajectoryGenerator.cpp +++ b/elsa/generators/tests/test_CircleTrajectoryGenerator.cpp @@ -35,7 +35,7 @@ SCENARIO("Create a Circular Trajectory") auto diffCenterSource = static_cast(s * 100); auto diffCenterDetector = static_cast(s); - auto [geomList, sdesc] = CircleTrajectoryGenerator::createTrajectory( + auto sdesc = CircleTrajectoryGenerator::createTrajectory( numberOfAngles, desc, halfCircular, diffCenterSource, diffCenterDetector); THEN("Every geomList in our list has the same camera center and the same projection " @@ -54,15 +54,17 @@ SCENARIO("Create a Circular Trajectory") SinogramData2D{sdesc->getSpacingPerDimension(), sdesc->getLocationOfOrigin()}); - REQUIRE((tmpGeom.getCameraCenter() - geomList[i].getCameraCenter()).norm() + auto geom = sdesc->getGeometryAt(i); + CHECK(geom); + + REQUIRE((tmpGeom.getCameraCenter() - geom->getCameraCenter()).norm() == Approx(0)); + REQUIRE((tmpGeom.getProjectionMatrix() - geom->getProjectionMatrix()).norm() + == Approx(0).margin(0.0000001)); REQUIRE( - (tmpGeom.getProjectionMatrix() - geomList[i].getProjectionMatrix()).norm() + (tmpGeom.getInverseProjectionMatrix() - geom->getInverseProjectionMatrix()) + .norm() == Approx(0).margin(0.0000001)); - REQUIRE((tmpGeom.getInverseProjectionMatrix() - - geomList[i].getInverseProjectionMatrix()) - .norm() - == Approx(0).margin(0.0000001)); } } } @@ -73,7 +75,7 @@ SCENARIO("Create a Circular Trajectory") auto diffCenterSource = static_cast(s * 100); auto diffCenterDetector = static_cast(s); - auto [geomList, sdesc] = CircleTrajectoryGenerator::createTrajectory( + auto sdesc = CircleTrajectoryGenerator::createTrajectory( numberOfAngles, desc, halfCircular, diffCenterSource, diffCenterDetector); THEN("Every geomList in our list has the same camera center and the same projection " @@ -93,15 +95,17 @@ SCENARIO("Create a Circular Trajectory") SinogramData2D{sdesc->getSpacingPerDimension(), sdesc->getLocationOfOrigin()}); - REQUIRE((tmpGeom.getCameraCenter() - geomList[i].getCameraCenter()).norm() + auto geom = sdesc->getGeometryAt(i); + CHECK(geom); + + REQUIRE((tmpGeom.getCameraCenter() - geom->getCameraCenter()).norm() == Approx(0)); + REQUIRE((tmpGeom.getProjectionMatrix() - geom->getProjectionMatrix()).norm() + == Approx(0).margin(0.0000001)); REQUIRE( - (tmpGeom.getProjectionMatrix() - geomList[i].getProjectionMatrix()).norm() + (tmpGeom.getInverseProjectionMatrix() - geom->getInverseProjectionMatrix()) + .norm() == Approx(0).margin(0.0000001)); - REQUIRE((tmpGeom.getInverseProjectionMatrix() - - geomList[i].getInverseProjectionMatrix()) - .norm() - == Approx(0).margin(0.0000001)); } } } @@ -120,7 +124,7 @@ SCENARIO("Create a Circular Trajectory") auto diffCenterSource = static_cast(s * 100); auto diffCenterDetector = static_cast(s); - auto [geomList, sdesc] = CircleTrajectoryGenerator::createTrajectory( + auto sdesc = CircleTrajectoryGenerator::createTrajectory( numberOfAngles, desc, halfCircular, diffCenterSource, diffCenterDetector); THEN("Every geomList in our list has the same camera center and the same projection " @@ -133,7 +137,6 @@ SCENARIO("Create a Circular Trajectory") / static_cast(numberOfAngles - 1); for (std::size_t i = 0; i < static_cast(numberOfAngles); ++i) { real_t angle = static_cast(i) * angleInc * pi_t / 180.0f; - // Geometry tmpGeom(sourceToCenter, centerToDetector, desc, *sdesc, angle); Geometry tmpGeom(SourceToCenterOfRotation{sourceToCenter}, CenterOfRotationToDetector{centerToDetector}, @@ -142,26 +145,28 @@ SCENARIO("Create a Circular Trajectory") sdesc->getLocationOfOrigin()}, RotationAngles3D{Gamma{angle}}); - REQUIRE((tmpGeom.getCameraCenter() - geomList[i].getCameraCenter()).norm() + auto geom = sdesc->getGeometryAt(i); + CHECK(geom); + + REQUIRE((tmpGeom.getCameraCenter() - geom->getCameraCenter()).norm() == Approx(0)); + REQUIRE((tmpGeom.getProjectionMatrix() - geom->getProjectionMatrix()).norm() + == Approx(0).margin(0.0000001)); REQUIRE( - (tmpGeom.getProjectionMatrix() - geomList[i].getProjectionMatrix()).norm() + (tmpGeom.getInverseProjectionMatrix() - geom->getInverseProjectionMatrix()) + .norm() == Approx(0).margin(0.0000001)); - REQUIRE((tmpGeom.getInverseProjectionMatrix() - - geomList[i].getInverseProjectionMatrix()) - .norm() - == Approx(0).margin(0.0000001)); } } } WHEN("We create a full circular trajectory for this scenario") { - index_t halfCircular = 359; - auto diffCenterSource = static_cast(s * 100); - auto diffCenterDetector = static_cast(s); + const index_t halfCircular = 359; + const auto diffCenterSource = static_cast(s * 100); + const auto diffCenterDetector = static_cast(s); - auto [geomList, sdesc] = CircleTrajectoryGenerator::createTrajectory( + auto sdesc = CircleTrajectoryGenerator::createTrajectory( numberOfAngles, desc, halfCircular, diffCenterSource, diffCenterDetector); THEN("Every geomList in our list has the same camera center and the same projection " @@ -182,15 +187,17 @@ SCENARIO("Create a Circular Trajectory") sdesc->getLocationOfOrigin()}, RotationAngles3D{Gamma{angle}}); - REQUIRE((tmpGeom.getCameraCenter() - geomList[i].getCameraCenter()).norm() + auto geom = sdesc->getGeometryAt(i); + CHECK(geom); + + REQUIRE((tmpGeom.getCameraCenter() - geom->getCameraCenter()).norm() == Approx(0)); + REQUIRE((tmpGeom.getProjectionMatrix() - geom->getProjectionMatrix()).norm() + == Approx(0).margin(0.0000001)); REQUIRE( - (tmpGeom.getProjectionMatrix() - geomList[i].getProjectionMatrix()).norm() + (tmpGeom.getInverseProjectionMatrix() - geom->getInverseProjectionMatrix()) + .norm() == Approx(0).margin(0.0000001)); - REQUIRE((tmpGeom.getInverseProjectionMatrix() - - geomList[i].getInverseProjectionMatrix()) - .norm() - == Approx(0).margin(0.0000001)); } } } diff --git a/elsa/projectors/BinaryMethod.cpp b/elsa/projectors/BinaryMethod.cpp index 2ad7fb4159a128f032f92c3475bd87c021fdd6ec..281a9ae14074e016153ceb6a4b3f76ffd08095eb 100644 --- a/elsa/projectors/BinaryMethod.cpp +++ b/elsa/projectors/BinaryMethod.cpp @@ -8,12 +8,12 @@ namespace elsa { template - BinaryMethod::BinaryMethod(const DataDescriptor& domainDescriptor, - const DataDescriptor& rangeDescriptor, - const std::vector& geometryList) + BinaryMethod::BinaryMethod(const VolumeDescriptor& domainDescriptor, + const DetectorDescriptor& rangeDescriptor) : LinearOperator(domainDescriptor, rangeDescriptor), _boundingBox{domainDescriptor.getNumberOfCoefficientsPerDimension()}, - _geometryList{geometryList} + _detectorDescriptor(static_cast(*_rangeDescriptor)), + _volumeDescriptor(static_cast(*_domainDescriptor)) { // sanity checks auto dim = _domainDescriptor->getNumberOfDimensions(); @@ -23,8 +23,8 @@ namespace elsa if (dim != _rangeDescriptor->getNumberOfDimensions()) throw std::invalid_argument("BinaryMethod: domain and range dimension need to match"); - if (_geometryList.empty()) - throw std::invalid_argument("BinaryMethod: geometry list was empty"); + if (_detectorDescriptor.getNumberOfGeometryPoses() == 0) + throw std::invalid_argument("BinaryMethod: rangeDescriptor without any geometry"); } template @@ -46,7 +46,7 @@ namespace elsa template BinaryMethod* BinaryMethod::cloneImpl() const { - return new BinaryMethod(*_domainDescriptor, *_rangeDescriptor, _geometryList); + return new BinaryMethod(_volumeDescriptor, _detectorDescriptor); } template @@ -59,9 +59,6 @@ namespace elsa if (!otherBM) return false; - if (_geometryList != otherBM->_geometryList) - return false; - return true; } @@ -76,15 +73,13 @@ namespace elsa result = 0; // initialize volume to 0, because we are not going to hit every voxel! } - const auto rangeDim = _rangeDescriptor->getNumberOfDimensions(); - // --> loop either over every voxel that should be updated or every detector // cell that should be calculated #pragma omp parallel for for (index_t rangeIndex = 0; rangeIndex < maxIterations; ++rangeIndex) { - // --> get the current ray to the detector center - auto ray = computeRayToDetector(rangeIndex, rangeDim); + // --> get the current ray to the detector center (from reference to DetectorDescriptor) + auto ray = _detectorDescriptor.computeRayFromDetectorCoord(rangeIndex); // --> setup traversal algorithm TraverseAABB traverse(_boundingBox, ray); @@ -109,21 +104,6 @@ namespace elsa } // end for } - template - typename BinaryMethod::Ray - BinaryMethod::computeRayToDetector(index_t detectorIndex, index_t dimension) const - { - auto detectorCoord = _rangeDescriptor->getCoordinateFromIndex(detectorIndex); - - // center of detector pixel is 0.5 units away from the corresponding detector coordinates - auto geometry = - _geometryList.at(std::make_unsigned_t(detectorCoord(dimension - 1))); - auto [ro, rd] = geometry.computeRayTo( - detectorCoord.block(0, 0, dimension - 1, 1).template cast().array() + 0.5); - - return Ray(ro, rd); - } - // ------------------------------------------ // explicit template instantiation template class BinaryMethod; diff --git a/elsa/projectors/BinaryMethod.h b/elsa/projectors/BinaryMethod.h index b7ff08e288fce830616d13c79ae9673dd7568bef..d09a42ef08f7f65ae6a77248591a230834a62cbf 100644 --- a/elsa/projectors/BinaryMethod.h +++ b/elsa/projectors/BinaryMethod.h @@ -4,6 +4,9 @@ #include "Geometry.h" #include "BoundingBox.h" +#include "VolumeDescriptor.h" +#include "DetectorDescriptor.h" + #include #include @@ -43,13 +46,15 @@ namespace elsa * * \param[in] domainDescriptor describing the domain of the operator (the volume) * \param[in] rangeDescriptor describing the range of the operator (the sinogram) - * \param[in] geometryList vector containing the geometries for the acquisition poses * * The domain is expected to be 2 or 3 dimensional (volSizeX, volSizeY, [volSizeZ]), * the range is expected to be matching the domain (detSizeX, [detSizeY], acqPoses). */ - BinaryMethod(const DataDescriptor& domainDescriptor, const DataDescriptor& rangeDescriptor, - const std::vector& geometryList); + // BinaryMethod(const DataDescriptor& domainDescriptor, const DataDescriptor& + // rangeDescriptor, const std::vector& geometryList); + + BinaryMethod(const VolumeDescriptor& domainDescriptor, + const DetectorDescriptor& rangeDescriptor); /// default destructor ~BinaryMethod() override = default; @@ -75,8 +80,11 @@ namespace elsa /// the bounding box of the volume BoundingBox _boundingBox; - /// the geometry list - std::vector _geometryList; + /// Reference to DetectorDescriptor stored in LinearOperator + DetectorDescriptor& _detectorDescriptor; + + /// Reference to VolumeDescriptor stored in LinearOperator + VolumeDescriptor& _volumeDescriptor; /// the traversal routine (for both apply/applyAdjoint) template @@ -86,16 +94,6 @@ namespace elsa /// convenience typedef for ray using Ray = Eigen::ParametrizedLine; - /** - * \brief computes the ray to the middle of the detector element - * - * \param[in] detectorIndex the index of the detector element - * \param[in] dimension the dimension of the detector (1 or 2) - * - * \returns the ray - */ - Ray computeRayToDetector(index_t detectorIndex, index_t dimension) const; - /// lift from base class using LinearOperator::_domainDescriptor; using LinearOperator::_rangeDescriptor; diff --git a/elsa/projectors/JosephsMethod.cpp b/elsa/projectors/JosephsMethod.cpp index afda5fc444dc8028d7f617e58920c33a23dceaf2..574d65607967c99b0e3896cfe0b7137f64538175 100644 --- a/elsa/projectors/JosephsMethod.cpp +++ b/elsa/projectors/JosephsMethod.cpp @@ -8,13 +8,13 @@ namespace elsa { template - JosephsMethod::JosephsMethod(const DataDescriptor& domainDescriptor, - const DataDescriptor& rangeDescriptor, - const std::vector& geometryList, + JosephsMethod::JosephsMethod(const VolumeDescriptor& domainDescriptor, + const DetectorDescriptor& rangeDescriptor, Interpolation interpolation) : LinearOperator(domainDescriptor, rangeDescriptor), _boundingBox{domainDescriptor.getNumberOfCoefficientsPerDimension()}, - _geometryList{geometryList}, + _detectorDescriptor(static_cast(*_rangeDescriptor)), + _volumeDescriptor(static_cast(*_domainDescriptor)), _interpolation{interpolation} { auto dim = _domainDescriptor->getNumberOfDimensions(); @@ -26,7 +26,7 @@ namespace elsa throw std::invalid_argument("JosephsMethod: domain and range dimension need to match"); } - if (_geometryList.empty()) { + if (_detectorDescriptor.getNumberOfGeometryPoses() == 0) { throw std::invalid_argument("JosephsMethod: geometry list was empty"); } } @@ -50,7 +50,7 @@ namespace elsa template JosephsMethod* JosephsMethod::cloneImpl() const { - return new JosephsMethod(*_domainDescriptor, *_rangeDescriptor, _geometryList); + return new JosephsMethod(_volumeDescriptor, _detectorDescriptor, _interpolation); } template @@ -63,9 +63,6 @@ namespace elsa if (!otherJM) return false; - if (_geometryList != otherJM->_geometryList || _interpolation != otherJM->_interpolation) - return false; - return true; } @@ -83,7 +80,7 @@ namespace elsa // iterate over all rays #pragma omp parallel for for (index_t ir = 0; ir < sizeOfRange; ir++) { - Ray ray = computeRayToDetector(ir, rangeDim); + const auto ray = _detectorDescriptor.computeRayFromDetectorCoord(ir); // --> setup traversal algorithm TraverseAABBJosephsMethod traverse(_boundingBox, ray); @@ -136,21 +133,6 @@ namespace elsa } } - template - typename JosephsMethod::Ray - JosephsMethod::computeRayToDetector(index_t detectorIndex, index_t dimension) const - { - auto detectorCoord = _rangeDescriptor->getCoordinateFromIndex(detectorIndex); - - // center of detector pixel is 0.5 units away from the corresponding detector coordinates - auto geometry = - _geometryList.at(std::make_unsigned_t(detectorCoord(dimension - 1))); - auto [ro, rd] = geometry.computeRayTo( - detectorCoord.block(0, 0, dimension - 1, 1).template cast().array() + 0.5); - - return Ray(ro, rd); - } - template template void JosephsMethod::linear(const DataContainer& vector, diff --git a/elsa/projectors/JosephsMethod.h b/elsa/projectors/JosephsMethod.h index 3bd227f97ab9581b4f44a8f2d5cf944a9b2f8436..835d3172ca0a408ebd3f6715917ede105a87fce8 100644 --- a/elsa/projectors/JosephsMethod.h +++ b/elsa/projectors/JosephsMethod.h @@ -3,6 +3,8 @@ #include "LinearOperator.h" #include "Geometry.h" #include "BoundingBox.h" +#include "VolumeDescriptor.h" +#include "DetectorDescriptor.h" #include #include @@ -48,14 +50,13 @@ namespace elsa * * \param[in] domainDescriptor describing the domain of the operator (the volume) * \param[in] rangeDescriptor describing the range of the operator (the sinogram) - * \param[in] geometryList vector containing the geometries for the acquisition poses * \param[in] interpolation enum specifying the interpolation mode * * The domain is expected to be 2 or 3 dimensional (volSizeX, volSizeY, [volSizeZ]), * the range is expected to be matching the domain (detSizeX, [detSizeY], acqPoses). */ - JosephsMethod(const DataDescriptor& domainDescriptor, const DataDescriptor& rangeDescriptor, - const std::vector& geometryList, + JosephsMethod(const VolumeDescriptor& domainDescriptor, + const DetectorDescriptor& rangeDescriptor, Interpolation interpolation = Interpolation::LINEAR); /// default destructor @@ -82,8 +83,11 @@ namespace elsa /// the bounding box of the volume BoundingBox _boundingBox; - /// the geometry list - std::vector _geometryList; + /// Reference to DetectorDescriptor stored in LinearOperator + DetectorDescriptor& _detectorDescriptor; + + /// Reference to VolumeDescriptor stored in LinearOperator + VolumeDescriptor& _volumeDescriptor; /// the interpolation mode Interpolation _interpolation; @@ -96,16 +100,6 @@ namespace elsa /// convenience typedef for ray using Ray = Eigen::ParametrizedLine; - /** - * \brief computes the ray to the middle of the detector element - * - * \param[in] detectorIndex the index of the detector element - * \param[in] dimension the dimension of the detector (1 or 2) - * - * \returns the ray - */ - Ray computeRayToDetector(index_t detectorIndex, index_t dimension) const; - /** * \brief Linear interpolation, works in any dimension * diff --git a/elsa/projectors/SiddonsMethod.cpp b/elsa/projectors/SiddonsMethod.cpp index 5e08950fda306b39ced3afdf356a1cc99f05597c..2f879ce953e9ce81bf435388dc733c9f89384a84 100644 --- a/elsa/projectors/SiddonsMethod.cpp +++ b/elsa/projectors/SiddonsMethod.cpp @@ -8,12 +8,12 @@ namespace elsa { template - SiddonsMethod::SiddonsMethod(const DataDescriptor& domainDescriptor, - const DataDescriptor& rangeDescriptor, - const std::vector& geometryList) + SiddonsMethod::SiddonsMethod(const VolumeDescriptor& domainDescriptor, + const DetectorDescriptor& rangeDescriptor) : LinearOperator(domainDescriptor, rangeDescriptor), _boundingBox(domainDescriptor.getNumberOfCoefficientsPerDimension()), - _geometryList{geometryList} + _detectorDescriptor(static_cast(*_rangeDescriptor)), + _volumeDescriptor(static_cast(*_domainDescriptor)) { auto dim = _domainDescriptor->getNumberOfDimensions(); if (dim != _rangeDescriptor->getNumberOfDimensions()) { @@ -24,7 +24,7 @@ namespace elsa throw std::logic_error("SiddonsMethod: only supporting 2d/3d operations"); } - if (_geometryList.empty()) { + if (_detectorDescriptor.getNumberOfGeometryPoses() == 0) { throw std::logic_error("SiddonsMethod: geometry list was empty"); } } @@ -48,7 +48,7 @@ namespace elsa template SiddonsMethod* SiddonsMethod::cloneImpl() const { - return new SiddonsMethod(*_domainDescriptor, *_rangeDescriptor, _geometryList); + return new SiddonsMethod(_volumeDescriptor, _detectorDescriptor); } template @@ -61,9 +61,6 @@ namespace elsa if (!otherSM) return false; - if (_geometryList != otherSM->_geometryList) - return false; - return true; } @@ -78,14 +75,12 @@ namespace elsa result = 0; // initialize volume to 0, because we are not going to hit every voxel! } - const auto rangeDim = _rangeDescriptor->getNumberOfDimensions(); - // --> loop either over every voxel that should updated or every detector // cell that should be calculated #pragma omp parallel for for (index_t rangeIndex = 0; rangeIndex < maxIterations; ++rangeIndex) { // --> get the current ray to the detector center - auto ray = computeRayToDetector(rangeIndex, rangeDim); + const auto ray = _detectorDescriptor.computeRayFromDetectorCoord(rangeIndex); // --> setup traversal algorithm TraverseAABB traverse(_boundingBox, ray); @@ -113,21 +108,6 @@ namespace elsa } } - template - typename SiddonsMethod::Ray - SiddonsMethod::computeRayToDetector(index_t detectorIndex, index_t dimension) const - { - auto detectorCoord = _rangeDescriptor->getCoordinateFromIndex(detectorIndex); - - // center of detector pixel is 0.5 units away from the corresponding detector coordinates - auto geometry = - _geometryList.at(std::make_unsigned_t(detectorCoord(dimension - 1))); - auto [ro, rd] = geometry.computeRayTo( - detectorCoord.block(0, 0, dimension - 1, 1).template cast().array() + 0.5); - - return Ray(ro, rd); - } - // ------------------------------------------ // explicit template instantiation template class SiddonsMethod; diff --git a/elsa/projectors/SiddonsMethod.h b/elsa/projectors/SiddonsMethod.h index 77deed5ec6b4bf79e3813970fe93c9172c61f6af..fced8db089f7d011c73b49a90963e4861640d4be 100644 --- a/elsa/projectors/SiddonsMethod.h +++ b/elsa/projectors/SiddonsMethod.h @@ -3,6 +3,8 @@ #include "LinearOperator.h" #include "Geometry.h" #include "BoundingBox.h" +#include "VolumeDescriptor.h" +#include "DetectorDescriptor.h" #include #include @@ -39,13 +41,12 @@ namespace elsa * * \param[in] domainDescriptor describing the domain of the operator (the volume) * \param[in] rangeDescriptor describing the range of the operator (the sinogram) - * \param[in] geometryList vector containing the geometries for the acquisition poses * * The domain is expected to be 2 or 3 dimensional (volSizeX, volSizeY, [volSizeZ]), * the range is expected to be matching the domain (detSizeX, [detSizeY], acqPoses). */ - SiddonsMethod(const DataDescriptor& domainDescriptor, const DataDescriptor& rangeDescriptor, - const std::vector& geometryList); + SiddonsMethod(const VolumeDescriptor& domainDescriptor, + const DetectorDescriptor& rangeDescriptor); /// default destructor ~SiddonsMethod() override = default; @@ -71,8 +72,11 @@ namespace elsa /// the bounding box of the volume BoundingBox _boundingBox; - /// the geometry list - std::vector _geometryList; + /// Reference to DetectorDescriptor stored in LinearOperator + DetectorDescriptor& _detectorDescriptor; + + /// Reference to VolumeDescriptor stored in LinearOperator + VolumeDescriptor& _volumeDescriptor; /// the traversal routine (for both apply/applyAdjoint) template @@ -82,16 +86,6 @@ namespace elsa /// convenience typedef for ray using Ray = Eigen::ParametrizedLine; - /** - * \brief computes the ray to the middle of the detector element - * - * \param[in] detectorIndex the index of the detector element - * \param[in] dimension the dimension of the detector (1 or 2) - * - * \returns the ray - */ - Ray computeRayToDetector(index_t detectorIndex, index_t dimension) const; - /// lift from base class using LinearOperator::_domainDescriptor; using LinearOperator::_rangeDescriptor; diff --git a/elsa/projectors/tests/test_BinaryMethod.cpp b/elsa/projectors/tests/test_BinaryMethod.cpp index 86ccf2c20acb70b3a3bee1f78909986b7eadcdbc..1a571b9c302b1feacde12cc253f85b43a136e148 100644 --- a/elsa/projectors/tests/test_BinaryMethod.cpp +++ b/elsa/projectors/tests/test_BinaryMethod.cpp @@ -13,6 +13,9 @@ #include "Logger.h" #include "testHelpers.h" #include "VolumeDescriptor.h" +#include "PlanarDetectorDescriptor.h" + +#include "testHelpers.h" using namespace elsa; using namespace elsa::geometry; @@ -29,7 +32,7 @@ SCENARIO("Testing BinaryMethod with only one ray") sizeRange << 1, 1; auto domain = VolumeDescriptor(sizeDomain); - auto range = VolumeDescriptor(sizeRange); + // auto range = VolumeDescriptor(sizeRange); auto stc = SourceToCenterOfRotation{100}; auto ctr = CenterOfRotationToDetector{5}; @@ -46,14 +49,15 @@ SCENARIO("Testing BinaryMethod with only one ray") auto dataDomain = DataContainer(domain); dataDomain = 1; - auto dataRange = DataContainer(range); - dataRange = 0; - WHEN("We have a single ray with 0 degrees") { geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData)); - auto op = BinaryMethod(domain, range, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(domain, range); + + auto dataRange = DataContainer(range); + dataRange = 0; THEN("A^t A x should be close to the original data") { @@ -77,7 +81,11 @@ SCENARIO("Testing BinaryMethod with only one ray") { geom.emplace_back(stc, ctr, Degree{180}, std::move(volData), std::move(sinoData)); - auto op = BinaryMethod(domain, range, geom); + // auto op = BinaryMethod(domain, range, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(domain, range); + auto dataRange = DataContainer(range); + dataRange = 0; THEN("A^t A x should be close to the original data") { @@ -101,7 +109,11 @@ SCENARIO("Testing BinaryMethod with only one ray") { geom.emplace_back(stc, ctr, Degree{90}, std::move(volData), std::move(sinoData)); - auto op = BinaryMethod(domain, range, geom); + // auto op = BinaryMethod(domain, range, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(domain, range); + auto dataRange = DataContainer(range); + dataRange = 0; THEN("A^t A x should be close to the original data") { @@ -125,7 +137,11 @@ SCENARIO("Testing BinaryMethod with only one ray") { geom.emplace_back(stc, ctr, Degree{270}, std::move(volData), std::move(sinoData)); - auto op = BinaryMethod(domain, range, geom); + // auto op = BinaryMethod(domain, range, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(domain, range); + auto dataRange = DataContainer(range); + dataRange = 0; THEN("A^t A x should be close to the original data") { @@ -149,7 +165,11 @@ SCENARIO("Testing BinaryMethod with only one ray") { geom.emplace_back(stc, ctr, Degree{45}, std::move(volData), std::move(sinoData)); - auto op = BinaryMethod(domain, range, geom); + // auto op = BinaryMethod(domain, range, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(domain, range); + auto dataRange = DataContainer(range); + dataRange = 0; THEN("A^t A x should be close to the original data") { @@ -170,7 +190,12 @@ SCENARIO("Testing BinaryMethod with only one ray") { geom.emplace_back(stc, ctr, Degree{225}, std::move(volData), std::move(sinoData)); - auto op = BinaryMethod(domain, range, geom); + // auto op = BinaryMethod(domain, range, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(domain, range); + + auto dataRange = DataContainer(range); + dataRange = 0; // This test case is a little awkward, but the Problem is inside of Geometry, with tiny // rounding erros this will not give exactly a ray with direction of (1/1), rather @@ -207,7 +232,7 @@ SCENARIO("Testing BinaryMethod with only 1 rays for 4 angles") sizeRange << 1, 4; auto domain = VolumeDescriptor(sizeDomain); - auto range = VolumeDescriptor(sizeRange); + // auto range = VolumeDescriptor(sizeRange); auto stc = SourceToCenterOfRotation{100}; auto ctr = CenterOfRotationToDetector{5}; @@ -224,9 +249,6 @@ SCENARIO("Testing BinaryMethod with only 1 rays for 4 angles") auto dataDomain = DataContainer(domain); dataDomain = 1; - auto dataRange = DataContainer(range); - dataRange = 0; - WHEN("We have a single ray with 0, 90, 180, 270 degrees") { geom.emplace_back(stc, ctr, Degree{0}, VolumeData2D{Size2D{sizeDomain}}, @@ -238,7 +260,12 @@ SCENARIO("Testing BinaryMethod with only 1 rays for 4 angles") geom.emplace_back(stc, ctr, Degree{270}, VolumeData2D{Size2D{sizeDomain}}, SinogramData2D{Size2D{sizeRange}}); - auto op = BinaryMethod(domain, range, geom); + // auto op = BinaryMethod(domain, range, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(domain, range); + + auto dataRange = DataContainer(range); + dataRange = 0; THEN("A^t A x should be close to the original data") { @@ -292,7 +319,7 @@ SCENARIO("Testing BinaryMethod") sizeRange << 5, 1; auto domain = VolumeDescriptor(sizeDomain); - auto range = VolumeDescriptor(sizeRange); + // auto range = VolumeDescriptor(sizeRange); auto stc = SourceToCenterOfRotation{100}; auto ctr = CenterOfRotationToDetector{5}; @@ -307,7 +334,9 @@ SCENARIO("Testing BinaryMethod") std::vector geom; geom.emplace_back(stc, ctr, Degree{0}, std::move(volData), std::move(sinoData)); - auto op = BinaryMethod(domain, range, geom); + // auto op = BinaryMethod(domain, range, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(domain, range); // THEN("It is not spd") // { @@ -346,7 +375,7 @@ SCENARIO("Testing BinaryMethod") RealVector_t res = RealVector_t::Constant(sizeRange.prod(), 1, 5); DataContainer tmpRes(range, res); - REQUIRE(tmpRes == dataRange); + REQUIRE(isApprox(tmpRes, dataRange)); op.applyAdjoint(dataRange, AtAx); @@ -364,7 +393,9 @@ SCENARIO("Testing BinaryMethod") std::vector geom; geom.emplace_back(stc, ctr, Degree{180}, std::move(volData), std::move(sinoData)); - auto op = BinaryMethod(domain, range, geom); + // auto op = BinaryMethod(domain, range, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(domain, range); THEN("A^t A x should be close to the original data") { @@ -380,7 +411,7 @@ SCENARIO("Testing BinaryMethod") RealVector_t res = RealVector_t::Constant(sizeRange.prod(), 1, 5); DataContainer tmpRes(range, res); - REQUIRE(tmpRes == dataRange); + REQUIRE(isApprox(tmpRes, dataRange)); op.applyAdjoint(dataRange, AtAx); @@ -397,7 +428,9 @@ SCENARIO("Testing BinaryMethod") std::vector geom; geom.emplace_back(stc, ctr, Degree{90}, std::move(volData), std::move(sinoData)); - auto op = BinaryMethod(domain, range, geom); + // auto op = BinaryMethod(domain, range, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(domain, range); THEN("A^t A x should be close to the original data") { @@ -413,7 +446,7 @@ SCENARIO("Testing BinaryMethod") RealVector_t res = RealVector_t::Constant(sizeRange.prod(), 1, 5); DataContainer tmpRes(range, res); - REQUIRE(tmpRes == dataRange); + REQUIRE(isApprox(tmpRes, dataRange)); op.applyAdjoint(dataRange, AtAx); @@ -430,7 +463,9 @@ SCENARIO("Testing BinaryMethod") std::vector geom; geom.emplace_back(stc, ctr, Degree{270}, std::move(volData), std::move(sinoData)); - auto op = BinaryMethod(domain, range, geom); + // auto op = BinaryMethod(domain, range, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(domain, range); THEN("A^t A x should be close to the original data") { @@ -446,7 +481,7 @@ SCENARIO("Testing BinaryMethod") RealVector_t res = RealVector_t::Constant(sizeRange.prod(), 1, 5); DataContainer tmpRes(range, res); - REQUIRE(tmpRes == dataRange); + REQUIRE(isApprox(tmpRes, dataRange)); op.applyAdjoint(dataRange, AtAx); @@ -466,14 +501,14 @@ SCENARIO("Calls to functions of super class") GIVEN("A projector") { - IndexVector_t volumeDims(2), sinoDims(2); + IndexVector_t volumeDims(2), sizeRange(2); const index_t volSize = 50; const index_t detectorSize = 50; const index_t numImgs = 50; volumeDims << volSize, volSize; - sinoDims << detectorSize, numImgs; + sizeRange << detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); + VolumeDescriptor sinoDescriptor(sizeRange); RealVector_t randomStuff(volumeDescriptor.getNumberOfCoefficients()); randomStuff.setRandom(); @@ -487,9 +522,11 @@ SCENARIO("Calls to functions of super class") for (index_t i = 0; i < numImgs; i++) { auto angle = static_cast(i * 2) * pi_t / 50; geom.emplace_back(stc, ctr, Radian{angle}, VolumeData2D{Size2D{volumeDims}}, - SinogramData2D{Size2D{sinoDims}}); + SinogramData2D{Size2D{sizeRange}}); } - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); WHEN("Projector is cloned") { @@ -522,14 +559,14 @@ SCENARIO("Output DataContainer is not zero initialized") GIVEN("A 2D setting") { - IndexVector_t volumeDims(2), sinoDims(2); + IndexVector_t volumeDims(2), sizeRange(2); const index_t volSize = 5; const index_t detectorSize = 1; const index_t numImgs = 1; volumeDims << volSize, volSize; - sinoDims << detectorSize, numImgs; + sizeRange << detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); + VolumeDescriptor sinoDescriptor(sizeRange); DataContainer volume(volumeDescriptor); DataContainer sino(sinoDescriptor); @@ -537,12 +574,14 @@ SCENARIO("Output DataContainer is not zero initialized") auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; auto volData = VolumeData2D{Size2D{volumeDims}}; - auto sinoData = SinogramData2D{Size2D{sinoDims}}; + auto sinoData = SinogramData2D{Size2D{sizeRange}}; std::vector geom; geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData)); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); WHEN("Sinogram conatainer is not zero initialized and we project through an empty volume") { @@ -577,19 +616,19 @@ SCENARIO("Output DataContainer is not zero initialized") GIVEN("A 3D setting") { - IndexVector_t volumeDims(3), sinoDims(3); + IndexVector_t volumeDims(3), sizeRange(3); const index_t volSize = 3; const index_t detectorSize = 1; const index_t numImgs = 1; volumeDims << volSize, volSize, volSize; - sinoDims << detectorSize, detectorSize, numImgs; + sizeRange << detectorSize, detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); + VolumeDescriptor sinoDescriptor(sizeRange); auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; auto volData = VolumeData3D{Size3D{volumeDims}}; - auto sinoData = SinogramData3D{Size3D{sinoDims}}; + auto sinoData = SinogramData3D{Size3D{sizeRange}}; DataContainer volume(volumeDescriptor); DataContainer sino(sinoDescriptor); @@ -598,7 +637,9 @@ SCENARIO("Output DataContainer is not zero initialized") geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), RotationAngles3D{Gamma{0}}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); WHEN("Sinogram conatainer is not zero initialized and we project through an empty volume") { @@ -639,19 +680,19 @@ SCENARIO("Rays not intersecting the bounding box are present") GIVEN("A 2D setting") { - IndexVector_t volumeDims(2), sinoDims(2); + IndexVector_t volumeDims(2), sizeRange(2); const index_t volSize = 5; const index_t detectorSize = 1; const index_t numImgs = 1; volumeDims << volSize, volSize; - sinoDims << detectorSize, numImgs; + sizeRange << detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); + VolumeDescriptor sinoDescriptor(sizeRange); auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; auto volData = VolumeData2D{Size2D{volumeDims}}; - auto sinoData = SinogramData2D{Size2D{sinoDims}}; + auto sinoData = SinogramData2D{Size2D{sizeRange}}; DataContainer volume(volumeDescriptor); DataContainer sino(sinoDescriptor); @@ -665,7 +706,9 @@ SCENARIO("Rays not intersecting the bounding box are present") geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData), PrincipalPointOffset{}, RotationOffset2D{-volSize, 0}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("Result of forward projection is zero") { @@ -692,7 +735,9 @@ SCENARIO("Rays not intersecting the bounding box are present") geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData), PrincipalPointOffset{}, RotationOffset2D{volSize, 0}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("Result of forward projection is zero") { @@ -718,7 +763,9 @@ SCENARIO("Rays not intersecting the bounding box are present") geom.emplace_back(stc, ctr, Radian{pi_t / 2}, std::move(volData), std::move(sinoData), PrincipalPointOffset{}, RotationOffset2D{0, -volSize}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("Result of forward projection is zero") { @@ -745,7 +792,9 @@ SCENARIO("Rays not intersecting the bounding box are present") geom.emplace_back(stc, ctr, Radian{pi_t / 2}, std::move(volData), std::move(sinoData), PrincipalPointOffset{}, RotationOffset2D{0, volSize}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("Result of forward projection is zero") { @@ -769,14 +818,14 @@ SCENARIO("Rays not intersecting the bounding box are present") GIVEN("A 3D setting") { - IndexVector_t volumeDims(3), sinoDims(3); + IndexVector_t volumeDims(3), sizeRange(3); const index_t volSize = 5; const index_t detectorSize = 1; const index_t numImgs = 1; volumeDims << volSize, volSize, volSize; - sinoDims << detectorSize, detectorSize, numImgs; + sizeRange << detectorSize, detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); + VolumeDescriptor sinoDescriptor(sizeRange); DataContainer volume(volumeDescriptor); DataContainer sino(sinoDescriptor); @@ -786,7 +835,7 @@ SCENARIO("Rays not intersecting the bounding box are present") auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; auto volData = VolumeData3D{Size3D{volumeDims}}; - auto sinoData = SinogramData3D{Size3D{sinoDims}}; + auto sinoData = SinogramData3D{Size3D{sizeRange}}; std::vector geom; @@ -810,7 +859,9 @@ SCENARIO("Rays not intersecting the bounding box are present") PrincipalPointOffset2D{0, 0}, RotationOffset3D{-offsetx[i], -offsety[i], -offsetz[i]}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("Result of forward projection is zero") { @@ -841,14 +892,14 @@ SCENARIO("Axis-aligned rays are present") GIVEN("A 2D setting with a single ray") { - IndexVector_t volumeDims(2), sinoDims(2); + IndexVector_t volumeDims(2), sizeRange(2); const index_t volSize = 5; const index_t detectorSize = 1; const index_t numImgs = 1; volumeDims << volSize, volSize; - sinoDims << detectorSize, numImgs; + sizeRange << detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); + VolumeDescriptor sinoDescriptor(sizeRange); DataContainer volume(volumeDescriptor); DataContainer sino(sinoDescriptor); @@ -856,7 +907,7 @@ SCENARIO("Axis-aligned rays are present") auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; auto volData = VolumeData2D{Size2D{volumeDims}}; - auto sinoData = SinogramData2D{Size2D{sinoDims}}; + auto sinoData = SinogramData2D{Size2D{sizeRange}}; std::vector geom; @@ -875,7 +926,9 @@ SCENARIO("Axis-aligned rays are present") { geom.emplace_back(stc, ctr, Radian{angles[i]}, std::move(volData), std::move(sinoData)); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("The result of projecting through a pixel is exactly the pixel value") { for (index_t j = 0; j < volSize; j++) { @@ -912,7 +965,9 @@ SCENARIO("Axis-aligned rays are present") std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{-0.5, 0}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("The result of projecting through a pixel is the value of the pixel with the " "higher index") { @@ -946,7 +1001,9 @@ SCENARIO("Axis-aligned rays are present") std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{volSize * 0.5, 0}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("The result of projecting is zero") { @@ -975,7 +1032,9 @@ SCENARIO("Axis-aligned rays are present") std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{-volSize / 2.0, 0}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("The result of projecting through a pixel is exactly the pixel's value") { for (index_t j = 0; j < volSize; j++) { @@ -998,14 +1057,14 @@ SCENARIO("Axis-aligned rays are present") GIVEN("A 3D setting with a single ray") { - IndexVector_t volumeDims(3), sinoDims(3); + IndexVector_t volumeDims(3), sizeRange(3); const index_t volSize = 3; const index_t detectorSize = 1; const index_t numImgs = 1; volumeDims << volSize, volSize, volSize; - sinoDims << detectorSize, detectorSize, numImgs; + sizeRange << detectorSize, detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); + VolumeDescriptor sinoDescriptor(sizeRange); DataContainer volume(volumeDescriptor); DataContainer sino(sinoDescriptor); @@ -1013,7 +1072,7 @@ SCENARIO("Axis-aligned rays are present") auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; auto volData = VolumeData3D{Size3D{volumeDims}}; - auto sinoData = SinogramData3D{Size3D{sinoDims}}; + auto sinoData = SinogramData3D{Size3D{sizeRange}}; std::vector geom; @@ -1050,7 +1109,9 @@ SCENARIO("Axis-aligned rays are present") geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("The result of projecting through a voxel is exactly the voxel value") { for (index_t j = 0; j < volSize; j++) { @@ -1137,7 +1198,9 @@ SCENARIO("Axis-aligned rays are present") PrincipalPointOffset2D{0, 0}, RotationOffset3D{-offsetx[i], -offsety[i], 0}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("The result of projecting through a voxel is exactly the voxel's value") { for (index_t j = 0; j < volSize; j++) { @@ -1182,7 +1245,9 @@ SCENARIO("Axis-aligned rays are present") PrincipalPointOffset2D{0, 0}, RotationOffset3D{-offsetx[i], -offsety[i], 0}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("The result of projecting is zero") { volume = 1; @@ -1205,14 +1270,14 @@ SCENARIO("Axis-aligned rays are present") GIVEN("A 2D setting with multiple projection angles") { - IndexVector_t volumeDims(2), sinoDims(2); + IndexVector_t volumeDims(2), sizeRange(2); const index_t volSize = 5; const index_t detectorSize = 1; const index_t numImgs = 4; volumeDims << volSize, volSize; - sinoDims << detectorSize, numImgs; + sizeRange << detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); + VolumeDescriptor sinoDescriptor(sizeRange); DataContainer volume(volumeDescriptor); DataContainer sino(sinoDescriptor); @@ -1224,15 +1289,17 @@ SCENARIO("Axis-aligned rays are present") WHEN("Both x- and y-axis-aligned rays are present") { geom.emplace_back(stc, ctr, Degree{0}, VolumeData2D{Size2D{volumeDims}}, - SinogramData2D{Size2D{sinoDims}}); + SinogramData2D{Size2D{sizeRange}}); geom.emplace_back(stc, ctr, Degree{90}, VolumeData2D{Size2D{volumeDims}}, - SinogramData2D{Size2D{sinoDims}}); + SinogramData2D{Size2D{sizeRange}}); geom.emplace_back(stc, ctr, Degree{180}, VolumeData2D{Size2D{volumeDims}}, - SinogramData2D{Size2D{sinoDims}}); + SinogramData2D{Size2D{sizeRange}}); geom.emplace_back(stc, ctr, Degree{270}, VolumeData2D{Size2D{volumeDims}}, - SinogramData2D{Size2D{sinoDims}}); + SinogramData2D{Size2D{sizeRange}}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("Values are accumulated correctly along each ray's path") { @@ -1269,14 +1336,14 @@ SCENARIO("Axis-aligned rays are present") GIVEN("A 3D setting with multiple projection angles") { - IndexVector_t volumeDims(3), sinoDims(3); + IndexVector_t volumeDims(3), sizeRange(3); const index_t volSize = 3; const index_t detectorSize = 1; const index_t numImgs = 6; volumeDims << volSize, volSize, volSize; - sinoDims << detectorSize, detectorSize, numImgs; + sizeRange << detectorSize, detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); + VolumeDescriptor sinoDescriptor(sizeRange); DataContainer volume(volumeDescriptor); DataContainer sino(sinoDescriptor); @@ -1292,10 +1359,12 @@ SCENARIO("Axis-aligned rays are present") for (index_t i = 0; i < numImgs; i++) geom.emplace_back(stc, ctr, VolumeData3D{Size3D{volumeDims}}, - SinogramData3D{Size3D{sinoDims}}, + SinogramData3D{Size3D{sizeRange}}, RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("Values are accumulated correctly along each ray's path") { @@ -1343,14 +1412,14 @@ SCENARIO("Projection under an angle") GIVEN("A 2D setting with a single ray") { - IndexVector_t volumeDims(2), sinoDims(2); + IndexVector_t volumeDims(2), sizeRange(2); const index_t volSize = 4; const index_t detectorSize = 1; const index_t numImgs = 1; volumeDims << volSize, volSize; - sinoDims << detectorSize, numImgs; + sizeRange << detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); + VolumeDescriptor sinoDescriptor(sizeRange); DataContainer volume(volumeDescriptor); DataContainer sino(sinoDescriptor); @@ -1358,7 +1427,7 @@ SCENARIO("Projection under an angle") auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; auto volData = VolumeData2D{Size2D{volumeDims}}; - auto sinoData = SinogramData2D{Size2D{sinoDims}}; + auto sinoData = SinogramData2D{Size2D{sizeRange}}; std::vector geom; @@ -1367,7 +1436,9 @@ SCENARIO("Projection under an angle") // In this case the ray enters and exits the volume through the borders along the main // direction geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData)); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("Ray intersects the correct pixels") { @@ -1429,7 +1500,9 @@ SCENARIO("Projection under an angle") // through a border not along the main direction geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{std::sqrt(3.f), 0}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("Ray intersects the correct pixels") { @@ -1481,7 +1554,9 @@ SCENARIO("Projection under an angle") // through a border not along the main direction geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{-std::sqrt(3.f), 0}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("Ray intersects the correct pixels") { @@ -1532,7 +1607,9 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{-2 - std::sqrt(3.f) / 2, 0}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("Ray intersects the correct pixels") { @@ -1572,7 +1649,9 @@ SCENARIO("Projection under an angle") // direction geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData), std::move(sinoData)); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("Ray intersects the correct pixels") { @@ -1629,7 +1708,9 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{0, std::sqrt(3.f)}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("Ray intersects the correct pixels") { @@ -1684,7 +1765,9 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{0, -std::sqrt(3.f)}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("Ray intersects the correct pixels") { @@ -1738,7 +1821,9 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{0, -2 - std::sqrt(3.f) / 2}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("Ray intersects the correct pixels") { @@ -1775,21 +1860,21 @@ SCENARIO("Projection under an angle") GIVEN("A 3D setting with a single ray") { - IndexVector_t volumeDims(3), sinoDims(3); + IndexVector_t volumeDims(3), sizeRange(3); const index_t volSize = 3; const index_t detectorSize = 1; const index_t numImgs = 1; volumeDims << volSize, volSize, volSize; - sinoDims << detectorSize, detectorSize, numImgs; + sizeRange << detectorSize, detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); + VolumeDescriptor sinoDescriptor(sizeRange); DataContainer volume(volumeDescriptor); DataContainer sino(sinoDescriptor); auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; auto volData = VolumeData3D{Size3D{volumeDims}}; - auto sinoData = SinogramData3D{Size3D{sinoDims}}; + auto sinoData = SinogramData3D{Size3D{sizeRange}}; std::vector geom; @@ -1800,7 +1885,9 @@ SCENARIO("Projection under an angle") // In this case the ray enters and exits the volume along the main direction geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), RotationAngles3D{Gamma{pi_t / 6}}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("The ray intersects the correct voxels") { @@ -1853,7 +1940,9 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0}, RotationOffset3D{1, 0, 0}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("The ray intersects the correct voxels") { @@ -1903,7 +1992,9 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0}, RotationOffset3D{-1, 0, 0}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("The ray intersects the correct voxels") { @@ -1953,7 +2044,9 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0}, RotationOffset3D{-2, 0, 0}); - BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + // BinaryMethod op(volumeDescriptor, sinoDescriptor, geom); + auto range = PlanarDetectorDescriptor(sizeRange, geom); + auto op = BinaryMethod(volumeDescriptor, range); THEN("The ray intersects the correct voxels") { diff --git a/elsa/projectors/tests/test_JosephsMethod.cpp b/elsa/projectors/tests/test_JosephsMethod.cpp index 11810b27bc91fc6fc548ce2aae503ced0453422b..bbb74d604a56ebf1cd44192c0ddbd0e8567d842f 100644 --- a/elsa/projectors/tests/test_JosephsMethod.cpp +++ b/elsa/projectors/tests/test_JosephsMethod.cpp @@ -5,6 +5,7 @@ #include "Logger.h" #include "testHelpers.h" #include "VolumeDescriptor.h" +#include "PlanarDetectorDescriptor.h" using namespace elsa; using namespace elsa::geometry; @@ -44,7 +45,10 @@ SCENARIO("Testing BinaryVoxelTraversal with only one ray") WHEN("We have a single ray with 0 degrees") { geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData)); - auto op = JosephsMethod(domain, range, geom); + + auto detectorDesc = PlanarDetectorDescriptor(sizeRange, {geom}); + + auto op = JosephsMethod(domain, detectorDesc); THEN("A^t A x should be close to the original data") { @@ -66,7 +70,10 @@ SCENARIO("Testing BinaryVoxelTraversal with only one ray") WHEN("We have a single ray with 180 degrees") { geom.emplace_back(stc, ctr, Degree{180}, std::move(volData), std::move(sinoData)); - auto op = JosephsMethod(domain, range, geom); + + auto detectorDesc = PlanarDetectorDescriptor(sizeRange, {geom}); + + auto op = JosephsMethod(domain, detectorDesc); THEN("A^t A x should be close to the original data") { @@ -88,7 +95,10 @@ SCENARIO("Testing BinaryVoxelTraversal with only one ray") WHEN("We have a single ray with 90 degrees") { geom.emplace_back(stc, ctr, Degree{90}, std::move(volData), std::move(sinoData)); - auto op = JosephsMethod(domain, range, geom); + + auto detectorDesc = PlanarDetectorDescriptor(sizeRange, {geom}); + + auto op = JosephsMethod(domain, detectorDesc); THEN("A^t A x should be close to the original data") { @@ -110,7 +120,10 @@ SCENARIO("Testing BinaryVoxelTraversal with only one ray") WHEN("We have a single ray with 270 degrees") { geom.emplace_back(stc, ctr, Degree{270}, std::move(volData), std::move(sinoData)); - auto op = JosephsMethod(domain, range, geom); + + auto detectorDesc = PlanarDetectorDescriptor(sizeRange, {geom}); + + auto op = JosephsMethod(domain, detectorDesc); THEN("A^t A x should be close to the original data") { @@ -132,7 +145,10 @@ SCENARIO("Testing BinaryVoxelTraversal with only one ray") WHEN("We have a single ray with 45 degrees") { geom.emplace_back(stc, ctr, Degree{45}, std::move(volData), std::move(sinoData)); - auto op = JosephsMethod(domain, range, geom); + + auto detectorDesc = PlanarDetectorDescriptor(sizeRange, {geom}); + + auto op = JosephsMethod(domain, detectorDesc); THEN("A^t A x should be close to the original data") { @@ -152,7 +168,10 @@ SCENARIO("Testing BinaryVoxelTraversal with only one ray") WHEN("We have a single ray with 225 degrees") { geom.emplace_back(stc, ctr, Degree{225}, std::move(volData), std::move(sinoData)); - auto op = JosephsMethod(domain, range, geom); + + auto detectorDesc = PlanarDetectorDescriptor(sizeRange, {geom}); + + auto op = JosephsMethod(domain, detectorDesc); THEN("A^t A x should be close to the original data") { @@ -213,7 +232,9 @@ SCENARIO("Testing JosephsMethod with only 4 ray") SinogramData2D{Size2D{sizeRange}}); geom.emplace_back(stc, ctr, Degree{270}, VolumeData2D{Size2D{sizeDomain}}, SinogramData2D{Size2D{sizeRange}}); - auto op = JosephsMethod(domain, range, geom); + + auto detectorDesc = PlanarDetectorDescriptor(sizeRange, geom); + auto op = JosephsMethod(domain, detectorDesc); THEN("A^t A x should be close to the original data") { @@ -246,11 +267,9 @@ SCENARIO("Calls to functions of super class") volumeDims << volSize, volSize; sinoDims << detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); + DataContainer volume(volumeDescriptor); volume = 0; - DataContainer sino(sinoDescriptor); - sino = 0; auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -262,7 +281,11 @@ SCENARIO("Calls to functions of super class") SinogramData2D{Size2D{sinoDims}}); } - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 0; + + JosephsMethod op(volumeDescriptor, sinoDescriptor); WHEN("Projector is cloned") { @@ -302,9 +325,7 @@ SCENARIO("Output DataContainer is not zero initialized") volumeDims << volSize, volSize; sinoDims << detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -313,8 +334,11 @@ SCENARIO("Output DataContainer is not zero initialized") std::vector geom; geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData)); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, - JosephsMethod<>::Interpolation::LINEAR); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); WHEN("Sinogram conatainer is not zero initialized and we project through an empty volume") { @@ -356,9 +380,7 @@ SCENARIO("Output DataContainer is not zero initialized") volumeDims << volSize, volSize, volSize; sinoDims << detectorSize, detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -369,8 +391,11 @@ SCENARIO("Output DataContainer is not zero initialized") geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), RotationAngles3D{Gamma{0}}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, - JosephsMethod<>::Interpolation::LINEAR); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); + + DataContainer sino(sinoDescriptor); WHEN("Sinogram conatainer is not zero initialized and we project through an empty volume") { @@ -416,11 +441,8 @@ SCENARIO("Rays not intersecting the bounding box are present") volumeDims << volSize, volSize; sinoDims << detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); volume = 1; - sino = 1; auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -434,7 +456,11 @@ SCENARIO("Rays not intersecting the bounding box are present") geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData), PrincipalPointOffset{}, RotationOffset2D{volSize, 0}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 1; + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); THEN("Result of forward projection is zero") @@ -460,7 +486,11 @@ SCENARIO("Rays not intersecting the bounding box are present") geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData), PrincipalPointOffset{}, RotationOffset2D{-volSize, 0}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 1; + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); THEN("Result of forward projection is zero") @@ -485,7 +515,11 @@ SCENARIO("Rays not intersecting the bounding box are present") geom.emplace_back(stc, ctr, Radian{pi_t / 2}, std::move(volData), std::move(sinoData), PrincipalPointOffset{}, RotationOffset2D{0, volSize}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 1; + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); THEN("Result of forward projection is zero") @@ -511,7 +545,11 @@ SCENARIO("Rays not intersecting the bounding box are present") geom.emplace_back(stc, ctr, Radian{pi_t / 2}, std::move(volData), std::move(sinoData), PrincipalPointOffset{}, RotationOffset2D{0, -volSize}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 1; + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); THEN("Result of forward projection is zero") @@ -541,11 +579,8 @@ SCENARIO("Rays not intersecting the bounding box are present") volumeDims << volSize, volSize, volSize; sinoDims << detectorSize, detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); volume = 1; - sino = 1; auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -573,7 +608,11 @@ SCENARIO("Rays not intersecting the bounding box are present") PrincipalPointOffset2D{0, 0}, RotationOffset3D{-offsetx[i], -offsety[i], -offsetz[i]}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 1; + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); THEN("Result of forward projection is zero") @@ -610,9 +649,7 @@ SCENARIO("Axis-aligned rays are present") volumeDims << volSize, volSize; sinoDims << detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -637,7 +674,10 @@ SCENARIO("Axis-aligned rays are present") geom.emplace_back(stc, ctr, Radian{angles[i]}, std::move(volData), std::move(sinoData)); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); THEN("The result of projecting through a pixel is exactly the pixel value") { @@ -678,7 +718,11 @@ SCENARIO("Axis-aligned rays are present") geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, Radian{angles[i]}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{-offsetx[i], -offsety[i]}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); THEN("The result of projecting through a pixel is the interpolated value between " "the two pixels closest to the ray") @@ -711,7 +755,11 @@ SCENARIO("Axis-aligned rays are present") geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, Radian{0}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{volSize * 0.5, 0}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor); THEN("The result of projecting through a pixel is exactly the pixel's value (we mirror " "values at the border for the purpose of interpolation)") @@ -742,7 +790,10 @@ SCENARIO("Axis-aligned rays are present") geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, Radian{0}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{-volSize / 2.0, 0}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor); THEN("The result of projecting through a pixel is exactly the pixel's value (we mirror " "values at the border for the purpose of interpolation)") @@ -776,9 +827,7 @@ SCENARIO("Axis-aligned rays are present") volumeDims << volSize, volSize, volSize; sinoDims << detectorSize, detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -818,8 +867,13 @@ SCENARIO("Axis-aligned rays are present") { geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); + THEN("The result of projecting through a voxel is exactly the voxel value") { for (index_t j = 0; j < volSize; j++) { @@ -875,8 +929,13 @@ SCENARIO("Axis-aligned rays are present") RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}}, PrincipalPointOffset2D{0, 0}, RotationOffset3D{-offsetx[i], -offsety[i], -offsetz[i]}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); + THEN("The result of projecting through a voxel is the interpolated value between " "the four voxels nearest to the ray") { @@ -970,7 +1029,12 @@ SCENARIO("Axis-aligned rays are present") std::move(sinoData), RotationAngles3D{Gamma{0}}, PrincipalPointOffset2D{0, 0}, RotationOffset3D{-offsetx[i], -offsety[i], 0}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor); + THEN("The result of projecting through a voxel is exactly the voxel's value (we " "mirror values at the border for the purpose of interpolation)") { @@ -1024,9 +1088,7 @@ SCENARIO("Axis-aligned rays are present") volumeDims << volSize, volSize, volSize; sinoDims << detectorSize, detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -1045,7 +1107,10 @@ SCENARIO("Axis-aligned rays are present") SinogramData3D{Size3D{sinoDims}}, RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); THEN("Values are accumulated correctly along each ray's path") @@ -1096,9 +1161,7 @@ SCENARIO("Projection under an angle") volumeDims << volSize, volSize; sinoDims << detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -1112,7 +1175,11 @@ SCENARIO("Projection under an angle") // In this case the ray enters and exits the volume through the borders along the main // direction Weighting for all interpolated values should be the same geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData)); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); real_t weight = static_cast(2 / std::sqrt(3.f)); @@ -1168,7 +1235,11 @@ SCENARIO("Projection under an angle") // differently geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{std::sqrt(3.f), 0}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); THEN("Ray intersects the correct pixels") @@ -1222,7 +1293,11 @@ SCENARIO("Projection under an angle") // differently geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{-std::sqrt(3.f), 0}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); THEN("Ray intersects the correct pixels") @@ -1271,7 +1346,11 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{-2 - std::sqrt(3.f) / 2, 0}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); THEN("Ray intersects the correct pixels") @@ -1308,7 +1387,11 @@ SCENARIO("Projection under an angle") // direction Weighting for all interpolated values should be the same geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData), std::move(sinoData)); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); real_t weight = 2 / std::sqrt(3.f); @@ -1366,7 +1449,11 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{0, std::sqrt(3.f)}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); THEN("Ray intersects the correct pixels") @@ -1418,7 +1505,11 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{0, -std::sqrt(3.f)}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); THEN("Ray intersects the correct pixels") @@ -1468,7 +1559,11 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{0, -2 - std::sqrt(3.f) / 2}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); THEN("Ray intersects the correct pixels") @@ -1509,9 +1604,7 @@ SCENARIO("Projection under an angle") volumeDims << volSize, volSize, volSize; sinoDims << detectorSize, detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -1527,7 +1620,11 @@ SCENARIO("Projection under an angle") // In this case the ray enters and exits the volume along the main direction geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), RotationAngles3D{Gamma{pi_t / 6}}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); THEN("The ray intersects the correct voxels") @@ -1571,7 +1668,11 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0}, RotationOffset3D{1, 0, 0}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); THEN("The ray intersects the correct voxels") @@ -1617,7 +1718,11 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0}, RotationOffset3D{-1, 0, 0}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); THEN("The ray intersects the correct voxels") @@ -1663,7 +1768,11 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0}, RotationOffset3D{-2, 0, 0}); - JosephsMethod op(volumeDescriptor, sinoDescriptor, geom, + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + JosephsMethod op(volumeDescriptor, sinoDescriptor, JosephsMethod<>::Interpolation::LINEAR); THEN("The ray intersects the correct voxels") diff --git a/elsa/projectors/tests/test_SiddonsMethod.cpp b/elsa/projectors/tests/test_SiddonsMethod.cpp index c5f5db23ace5f56c9f2d82411a8f960d46ba2439..e00b212eb309033bd3f8ec45ff009e706195e391 100644 --- a/elsa/projectors/tests/test_SiddonsMethod.cpp +++ b/elsa/projectors/tests/test_SiddonsMethod.cpp @@ -5,6 +5,7 @@ #include "Logger.h" #include "testHelpers.h" #include "VolumeDescriptor.h" +#include "PlanarDetectorDescriptor.h" using namespace elsa; using namespace elsa::geometry; @@ -44,7 +45,10 @@ SCENARIO("Testing SiddonsMethod projector with only one ray") WHEN("We have a single ray with 0 degrees") { geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData)); - auto op = SiddonsMethod(domain, range, geom); + + auto detectorDesc = PlanarDetectorDescriptor(sizeRange, {geom}); + + auto op = SiddonsMethod(domain, detectorDesc); THEN("A^t A x should be close to the original data") { @@ -64,7 +68,10 @@ SCENARIO("Testing SiddonsMethod projector with only one ray") WHEN("We have a single ray with 180 degrees") { geom.emplace_back(stc, ctr, Degree{180}, std::move(volData), std::move(sinoData)); - auto op = SiddonsMethod(domain, range, geom); + + auto detectorDesc = PlanarDetectorDescriptor(sizeRange, {geom}); + + auto op = SiddonsMethod(domain, detectorDesc); THEN("A^t A x should be close to the original data") { @@ -84,7 +91,10 @@ SCENARIO("Testing SiddonsMethod projector with only one ray") WHEN("We have a single ray with 90 degrees") { geom.emplace_back(stc, ctr, Degree{90}, std::move(volData), std::move(sinoData)); - auto op = SiddonsMethod(domain, range, geom); + + auto detectorDesc = PlanarDetectorDescriptor(sizeRange, {geom}); + + auto op = SiddonsMethod(domain, detectorDesc); THEN("A^t A x should be close to the original data") { @@ -104,7 +114,10 @@ SCENARIO("Testing SiddonsMethod projector with only one ray") WHEN("We have a single ray with 270 degrees") { geom.emplace_back(stc, ctr, Degree{270}, std::move(volData), std::move(sinoData)); - auto op = SiddonsMethod(domain, range, geom); + + auto detectorDesc = PlanarDetectorDescriptor(sizeRange, {geom}); + + auto op = SiddonsMethod(domain, detectorDesc); THEN("A^t A x should be close to the original data") { @@ -222,12 +235,10 @@ SCENARIO("Calls to functions of super class") volumeDims << volSize, volSize; sinoDims << detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); RealVector_t randomStuff(volumeDescriptor.getNumberOfCoefficients()); randomStuff.setRandom(); DataContainer volume(volumeDescriptor, randomStuff); - DataContainer sino(sinoDescriptor); auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -238,7 +249,9 @@ SCENARIO("Calls to functions of super class") geom.emplace_back(stc, ctr, Radian{angle}, VolumeData2D{Size2D{volumeDims}}, SinogramData2D{Size2D{sinoDims}}); } - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + SiddonsMethod op(volumeDescriptor, sinoDescriptor); WHEN("Projector is cloned") { @@ -277,9 +290,7 @@ SCENARIO("Output DataContainer is not zero initialized") volumeDims << volSize, volSize; sinoDims << detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -289,7 +300,10 @@ SCENARIO("Output DataContainer is not zero initialized") std::vector geom; geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData)); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); WHEN("Sinogram conatainer is not zero initialized and we project through an empty volume") { @@ -329,9 +343,7 @@ SCENARIO("Output DataContainer is not zero initialized") volumeDims << volSize, volSize, volSize; sinoDims << detectorSize, detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -342,7 +354,10 @@ SCENARIO("Output DataContainer is not zero initialized") geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), RotationAngles3D{Gamma{0}}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); WHEN("Sinogram conatainer is not zero initialized and we project through an empty volume") { @@ -388,11 +403,8 @@ SCENARIO("Rays not intersecting the bounding box are present") volumeDims << volSize, volSize; sinoDims << detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); volume = 1; - sino = 1; auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -406,7 +418,11 @@ SCENARIO("Rays not intersecting the bounding box are present") geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData), PrincipalPointOffset{}, RotationOffset2D{-volSize, 0}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 1; + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("Result of forward projection is zero") { @@ -431,7 +447,13 @@ SCENARIO("Rays not intersecting the bounding box are present") geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData), PrincipalPointOffset{}, RotationOffset2D{volSize, 0}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 1; + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); + + // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); THEN("Result of forward projection is zero") { @@ -455,7 +477,13 @@ SCENARIO("Rays not intersecting the bounding box are present") geom.emplace_back(stc, ctr, Radian{pi_t / 2}, std::move(volData), std::move(sinoData), PrincipalPointOffset{}, RotationOffset2D{0, -volSize}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 1; + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); + + // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); THEN("Result of forward projection is zero") { @@ -480,7 +508,13 @@ SCENARIO("Rays not intersecting the bounding box are present") geom.emplace_back(stc, ctr, Radian{pi_t / 2}, std::move(volData), std::move(sinoData), PrincipalPointOffset{}, RotationOffset2D{0, volSize}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 1; + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); + + // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); THEN("Result of forward projection is zero") { @@ -509,11 +543,8 @@ SCENARIO("Rays not intersecting the bounding box are present") volumeDims << volSize, volSize, volSize; sinoDims << detectorSize, detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); volume = 1; - sino = 1; auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -542,7 +573,11 @@ SCENARIO("Rays not intersecting the bounding box are present") PrincipalPointOffset2D{0, 0}, RotationOffset3D{-offsetx[i], -offsety[i], -offsetz[i]}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 1; + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("Result of forward projection is zero") { @@ -578,9 +613,7 @@ SCENARIO("Axis-aligned rays are present") volumeDims << volSize, volSize; sinoDims << detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -604,7 +637,11 @@ SCENARIO("Axis-aligned rays are present") { geom.emplace_back(stc, ctr, Radian{angles[i]}, std::move(volData), std::move(sinoData)); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("The result of projecting through a pixel is exactly the pixel value") { for (index_t j = 0; j < volSize; j++) { @@ -634,7 +671,11 @@ SCENARIO("Axis-aligned rays are present") geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, Radian{0}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{-0.5, 0}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("The result of projecting through a pixel is the value of the pixel with the " "higher index") { @@ -663,7 +704,11 @@ SCENARIO("Axis-aligned rays are present") geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, Radian{0}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{volSize * 0.5, 0}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("The result of projecting is zero") { @@ -723,9 +768,7 @@ SCENARIO("Axis-aligned rays are present") volumeDims << volSize, volSize, volSize; sinoDims << detectorSize, detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -766,7 +809,11 @@ SCENARIO("Axis-aligned rays are present") { geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("The result of projecting through a voxel is exactly the voxel value") { for (index_t j = 0; j < volSize; j++) { @@ -843,7 +890,11 @@ SCENARIO("Axis-aligned rays are present") std::move(sinoData), RotationAngles3D{Gamma{0}}, PrincipalPointOffset2D{0, 0}, RotationOffset3D{-offsetx[i], -offsety[i], 0}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("The result of projecting through a voxel is exactly the voxel's value") { for (index_t j = 0; j < volSize; j++) { @@ -887,7 +938,11 @@ SCENARIO("Axis-aligned rays are present") std::move(sinoData), RotationAngles3D{Gamma{0}}, PrincipalPointOffset2D{0, 0}, RotationOffset3D{-offsetx[i], -offsety[i], 0}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("The result of projecting is zero") { volume = 1; @@ -918,9 +973,7 @@ SCENARIO("Axis-aligned rays are present") volumeDims << volSize, volSize; sinoDims << detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -938,7 +991,10 @@ SCENARIO("Axis-aligned rays are present") geom.emplace_back(stc, ctr, Degree{270}, VolumeData2D{Size2D{volumeDims}}, SinogramData2D{Size2D{sinoDims}}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("Values are accumulated correctly along each ray's path") { @@ -978,9 +1034,7 @@ SCENARIO("Axis-aligned rays are present") volumeDims << volSize, volSize, volSize; sinoDims << detectorSize, detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -997,7 +1051,10 @@ SCENARIO("Axis-aligned rays are present") SinogramData3D{Size3D{sinoDims}}, RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("Values are accumulated correctly along each ray's path") { @@ -1047,9 +1104,7 @@ SCENARIO("Projection under an angle") volumeDims << volSize, volSize; sinoDims << detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -1063,7 +1118,11 @@ SCENARIO("Projection under an angle") // In this case the ray enters and exits the volume through the borders along the main // direction geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData)); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("Ray intersects the correct pixels") { @@ -1120,7 +1179,11 @@ SCENARIO("Projection under an angle") // through a border not along the main direction geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{std::sqrt(3.f), 0}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("Ray intersects the correct pixels") { @@ -1163,7 +1226,11 @@ SCENARIO("Projection under an angle") // through a border not along the main direction geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{-std::sqrt(3.f), 0}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("Ray intersects the correct pixels") { @@ -1205,7 +1272,11 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{-2 - std::sqrt(3.f) / 2, 0}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("Ray intersects the correct pixels") { @@ -1241,7 +1312,11 @@ SCENARIO("Projection under an angle") // direction geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData), std::move(sinoData)); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("Ray intersects the correct pixels") { @@ -1299,7 +1374,11 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{0, std::sqrt(3.f)}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("Ray intersects the correct pixels") { @@ -1344,7 +1423,11 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{0, -std::sqrt(3.f)}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("Ray intersects the correct pixels") { @@ -1389,7 +1472,11 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{0, -2 - std::sqrt(3.f) / 2}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("Ray intersects the correct pixels") { @@ -1429,9 +1516,7 @@ SCENARIO("Projection under an angle") volumeDims << volSize, volSize, volSize; sinoDims << detectorSize, detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -1447,7 +1532,11 @@ SCENARIO("Projection under an angle") // In this case the ray enters and exits the volume along the main direction geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), RotationAngles3D{Gamma{pi_t / 6}}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("The ray intersects the correct voxels") { @@ -1489,7 +1578,11 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0}, RotationOffset3D{1, 0, 0}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("The ray intersects the correct voxels") { @@ -1530,7 +1623,11 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0}, RotationOffset3D{-1, 0, 0}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("The ray intersects the correct voxels") { @@ -1573,7 +1670,11 @@ SCENARIO("Projection under an angle") geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0}, RotationOffset3D{-2, 0, 0}); - SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + // SiddonsMethod op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + SiddonsMethod op(volumeDescriptor, sinoDescriptor); THEN("The ray intersects the correct voxels") { diff --git a/elsa/projectors_cuda/JosephsMethodCUDA.cpp b/elsa/projectors_cuda/JosephsMethodCUDA.cpp index 5cadca6584664c6005587639fcd07e479ad23d98..e5b5f6d85709faeb0bf7ea1da5c4083b00ffd877 100644 --- a/elsa/projectors_cuda/JosephsMethodCUDA.cpp +++ b/elsa/projectors_cuda/JosephsMethodCUDA.cpp @@ -5,13 +5,13 @@ namespace elsa { template - JosephsMethodCUDA::JosephsMethodCUDA(const DataDescriptor& domainDescriptor, - const DataDescriptor& rangeDescriptor, - const std::vector& geometryList, + JosephsMethodCUDA::JosephsMethodCUDA(const VolumeDescriptor& domainDescriptor, + const DetectorDescriptor& rangeDescriptor, bool fast) : LinearOperator(domainDescriptor, rangeDescriptor), _boundingBox{_domainDescriptor->getNumberOfCoefficientsPerDimension()}, - _geometryList{geometryList}, + _detectorDescriptor(static_cast(*_rangeDescriptor)), + _volumeDescriptor(static_cast(*_domainDescriptor)), _fast{fast} { auto dim = static_cast(_domainDescriptor->getNumberOfDimensions()); @@ -24,21 +24,22 @@ namespace elsa throw std::logic_error("JosephsMethodCUDA: only supporting 2d/3d operations"); } - if (geometryList.empty()) { + if (_detectorDescriptor.getNumberOfGeometryPoses() == 0) { throw std::logic_error("JosephsMethodCUDA: geometry list was empty"); } + const index_t numGeometry = _detectorDescriptor.getNumberOfGeometryPoses(); + // allocate device memory and copy ray origins and the inverse of projection matrices to // device - cudaExtent extent = make_cudaExtent(dim * sizeof(real_t), dim, geometryList.size()); + cudaExtent extent = make_cudaExtent(dim * sizeof(real_t), dim, numGeometry); - if (cudaMallocPitch(&_rayOrigins.ptr, &_rayOrigins.pitch, dim * sizeof(real_t), - geometryList.size()) + if (cudaMallocPitch(&_rayOrigins.ptr, &_rayOrigins.pitch, dim * sizeof(real_t), numGeometry) != cudaSuccess) throw std::bad_alloc(); _rayOrigins.xsize = dim; - _rayOrigins.ysize = geometryList.size(); + _rayOrigins.ysize = numGeometry; if (cudaMalloc3D(&_projInvMatrices, extent) != cudaSuccess) throw std::bad_alloc(); @@ -49,8 +50,14 @@ namespace elsa auto projPitch = _projInvMatrices.pitch; auto* rayBasePtr = (int8_t*) _rayOrigins.ptr; auto rayPitch = _rayOrigins.pitch; - for (std::size_t i = 0; i < geometryList.size(); i++) { - RealMatrix_t P = geometryList[i].getInverseProjectionMatrix().block(0, 0, dim, dim); + + for (index_t i = 0; i < numGeometry; i++) { + auto geometry = _detectorDescriptor.getGeometryAt(i); + + if (!geometry) + throw std::logic_error("JosephsMethodCUDA: Access not existing geometry pose"); + + RealMatrix_t P = geometry->getInverseProjectionMatrix().block(0, 0, dim, dim); auto* slice = (int8_t*) _projInvMatrices.ptr + i * projPitch * dim; // transfer inverse of projection matrix @@ -62,7 +69,7 @@ namespace elsa // transfer projection matrix if _fast flag is set if (_fast) { - P = geometryList[i].getProjectionMatrix().block(0, 0, dim, dim); + P = geometry->getProjectionMatrix().block(0, 0, dim, dim); slice = (int8_t*) _projMatrices.ptr + i * projPitch * dim; if (cudaMemcpy2D(slice, projPitch, P.data(), dim * sizeof(real_t), dim * sizeof(real_t), dim, cudaMemcpyHostToDevice) @@ -74,8 +81,8 @@ namespace elsa int8_t* rayPtr = rayBasePtr + i * rayPitch; // get ray origin using direct inverse RealVector_t ro = - -geometryList[i].getInverseProjectionMatrix().block(0, 0, dim, dim) - * geometryList[i].getProjectionMatrix().block(0, static_cast(dim), dim, 1); + -geometry->getInverseProjectionMatrix().block(0, 0, dim, dim) + * geometry->getProjectionMatrix().block(0, static_cast(dim), dim, 1); // transfer ray origin if (cudaMemcpyAsync(rayPtr, ro.data(), dim * sizeof(real_t), cudaMemcpyHostToDevice) != cudaSuccess) @@ -115,9 +122,6 @@ namespace elsa if (!otherJM) return false; - if (_geometryList != otherJM->_geometryList || _fast != otherJM->_fast) - return false; - if (_fast != otherJM->_fast) return false; @@ -384,7 +388,8 @@ namespace elsa JosephsMethodCUDA::JosephsMethodCUDA(const JosephsMethodCUDA& other) : LinearOperator(*other._domainDescriptor, *other._rangeDescriptor), _boundingBox{other._boundingBox}, - _geometryList{other._geometryList}, + _detectorDescriptor(static_cast(*_rangeDescriptor)), + _volumeDescriptor(static_cast(*_domainDescriptor)), _fast{other._fast} { auto dim = static_cast(_domainDescriptor->getNumberOfDimensions()); @@ -398,7 +403,7 @@ namespace elsa throw std::bad_alloc(); _rayOrigins.xsize = dim; - _rayOrigins.ysize = _geometryList.size(); + _rayOrigins.ysize = _detectorDescriptor.getNumberOfGeometryPoses(); if (cudaMalloc3D(&_projInvMatrices, extent) != cudaSuccess) throw std::bad_alloc(); @@ -575,4 +580,4 @@ namespace elsa // explicit template instantiation template class JosephsMethodCUDA; template class JosephsMethodCUDA; -} // namespace elsa \ No newline at end of file +} // namespace elsa diff --git a/elsa/projectors_cuda/JosephsMethodCUDA.h b/elsa/projectors_cuda/JosephsMethodCUDA.h index 8aa85cc6d838257ae729407b18cbc4819ac17677..12ee532e2fb512320fbbc1e22779ae669c17bfa4 100644 --- a/elsa/projectors_cuda/JosephsMethodCUDA.h +++ b/elsa/projectors_cuda/JosephsMethodCUDA.h @@ -8,6 +8,8 @@ #include "LinearOperator.h" #include "Geometry.h" #include "BoundingBox.h" +#include "VolumeDescriptor.h" +#include "DetectorDescriptor.h" #include "TraverseJosephsCUDA.cuh" @@ -56,9 +58,8 @@ namespace elsa * The domain is expected to be 2 or 3 dimensional (volSizeX, volSizeY, [volSizeZ]), * the range is expected to be matching the domain (detSizeX, [detSizeY], acqPoses). */ - JosephsMethodCUDA(const DataDescriptor& domainDescriptor, - const DataDescriptor& rangeDescriptor, - const std::vector& geometryList, bool fast = true); + JosephsMethodCUDA(const VolumeDescriptor& domainDescriptor, + const DetectorDescriptor& rangeDescriptor, bool fast = true); /// destructor ~JosephsMethodCUDA() override; @@ -84,8 +85,11 @@ namespace elsa /// the bounding box of the volume BoundingBox _boundingBox; - /// the geometry list - std::vector _geometryList; + /// Reference to DetectorDescriptor stored in LinearOperator + DetectorDescriptor& _detectorDescriptor; + + /// Reference to VolumeDescriptor stored in LinearOperator + VolumeDescriptor& _volumeDescriptor; /// threads per block used in the kernel execution configuration static const unsigned int THREADS_PER_BLOCK = diff --git a/elsa/projectors_cuda/SiddonsMethodCUDA.cpp b/elsa/projectors_cuda/SiddonsMethodCUDA.cpp index 0f768e9fbb5cb5f823e0b73335991e3dd32c10ae..792b7bb580c5edb73464cc37b62ab11ad1b67a4d 100644 --- a/elsa/projectors_cuda/SiddonsMethodCUDA.cpp +++ b/elsa/projectors_cuda/SiddonsMethodCUDA.cpp @@ -6,12 +6,12 @@ namespace elsa { template - SiddonsMethodCUDA::SiddonsMethodCUDA(const DataDescriptor& domainDescriptor, - const DataDescriptor& rangeDescriptor, - const std::vector& geometryList) + SiddonsMethodCUDA::SiddonsMethodCUDA(const VolumeDescriptor& domainDescriptor, + const DetectorDescriptor& rangeDescriptor) : LinearOperator(domainDescriptor, rangeDescriptor), _boundingBox(_domainDescriptor->getNumberOfCoefficientsPerDimension()), - _geometryList(geometryList) + _detectorDescriptor(static_cast(*_rangeDescriptor)), + _volumeDescriptor(static_cast(*_domainDescriptor)) { auto dim = static_cast(_domainDescriptor->getNumberOfDimensions()); if (dim != static_cast(_rangeDescriptor->getNumberOfDimensions())) { @@ -23,20 +23,21 @@ namespace elsa throw std::logic_error("SiddonsMethodCUDA: only supporting 2d/3d operations"); } - if (geometryList.empty()) { + if (_detectorDescriptor.getNumberOfGeometryPoses() == 0) { throw std::logic_error("SiddonsMethodCUDA: geometry list was empty"); } + const index_t numGeometry = _detectorDescriptor.getNumberOfGeometryPoses(); + // allocate device memory and copy ray origins and the inverse of the significant part of // projection matrices to device - cudaExtent extent = make_cudaExtent(dim * sizeof(real_t), dim, geometryList.size()); + cudaExtent extent = make_cudaExtent(dim * sizeof(real_t), dim, numGeometry); - if (cudaMallocPitch(&_rayOrigins.ptr, &_rayOrigins.pitch, dim * sizeof(real_t), - geometryList.size()) + if (cudaMallocPitch(&_rayOrigins.ptr, &_rayOrigins.pitch, dim * sizeof(real_t), numGeometry) != cudaSuccess) throw std::bad_alloc(); _rayOrigins.xsize = dim; - _rayOrigins.ysize = geometryList.size(); + _rayOrigins.ysize = numGeometry; if (cudaMalloc3D(&_projInvMatrices, extent) != cudaSuccess) throw std::bad_alloc(); @@ -45,8 +46,14 @@ namespace elsa auto projPitch = _projInvMatrices.pitch; auto* rayBasePtr = (int8_t*) _rayOrigins.ptr; auto rayPitch = _rayOrigins.pitch; - for (std::size_t i = 0; i < geometryList.size(); i++) { - RealMatrix_t P = geometryList[i].getInverseProjectionMatrix().block(0, 0, dim, dim); + + for (index_t i = 0; i < numGeometry; i++) { + auto geometry = _detectorDescriptor.getGeometryAt(i); + + if (!geometry) + throw std::logic_error("JosephsMethodCUDA: Access not existing geometry pose"); + + RealMatrix_t P = geometry->getInverseProjectionMatrix().block(0, 0, dim, dim); int8_t* slice = projPtr + i * projPitch * dim; // CUDA also uses a column-major representation, directly transfer matrix // transfer inverse of projection matrix @@ -59,8 +66,7 @@ namespace elsa int8_t* rayPtr = rayBasePtr + i * rayPitch; // get camera center using direct inverse RealVector_t ro = - -P - * geometryList[i].getProjectionMatrix().block(0, static_cast(dim), dim, 1); + -P * geometry->getProjectionMatrix().block(0, static_cast(dim), dim, 1); // transfer ray origin if (cudaMemcpyAsync(rayPtr, ro.data(), dim * sizeof(real_t), cudaMemcpyHostToDevice) != cudaSuccess) @@ -81,7 +87,7 @@ namespace elsa template SiddonsMethodCUDA* SiddonsMethodCUDA::cloneImpl() const { - return new SiddonsMethodCUDA(*_domainDescriptor, *_rangeDescriptor, _geometryList); + return new SiddonsMethodCUDA(_volumeDescriptor, _detectorDescriptor); } template @@ -112,9 +118,6 @@ namespace elsa if (!otherSM) return false; - if (_geometryList != otherSM->_geometryList) - return false; - return true; } @@ -309,4 +312,4 @@ namespace elsa // explicit template instantiation template class SiddonsMethodCUDA; template class SiddonsMethodCUDA; -} // namespace elsa \ No newline at end of file +} // namespace elsa diff --git a/elsa/projectors_cuda/SiddonsMethodCUDA.h b/elsa/projectors_cuda/SiddonsMethodCUDA.h index a8c7e978426bd9f750c3794cfcc872084b0c36ce..cbc662d1c738497daac918c90a7a3daa6ce4978b 100644 --- a/elsa/projectors_cuda/SiddonsMethodCUDA.h +++ b/elsa/projectors_cuda/SiddonsMethodCUDA.h @@ -6,6 +6,8 @@ #include "LinearOperator.h" #include "Geometry.h" #include "BoundingBox.h" +#include "VolumeDescriptor.h" +#include "DetectorDescriptor.h" #include "TraverseSiddonsCUDA.cuh" @@ -47,9 +49,8 @@ namespace elsa * The domain is expected to be 2 or 3 dimensional (volSizeX, volSizeY, [volSizeZ]), * the range is expected to be matching the domain (detSizeX, [detSizeY], acqPoses). */ - SiddonsMethodCUDA(const DataDescriptor& domainDescriptor, - const DataDescriptor& rangeDescriptor, - const std::vector& geometryList); + SiddonsMethodCUDA(const VolumeDescriptor& domainDescriptor, + const DetectorDescriptor& rangeDescriptor); /// destructor ~SiddonsMethodCUDA() override; @@ -75,8 +76,11 @@ namespace elsa /// the bounding box of the volume BoundingBox _boundingBox; - /// the geometry list - std::vector _geometryList; + /// Reference to DetectorDescriptor stored in LinearOperator + DetectorDescriptor& _detectorDescriptor; + + /// Reference to VolumeDescriptor stored in LinearOperator + VolumeDescriptor& _volumeDescriptor; /// threads per block used in the kernel execution configuration static const unsigned int THREADS_PER_BLOCK = diff --git a/elsa/projectors_cuda/tests/test_JosephsMethodCUDA.cpp b/elsa/projectors_cuda/tests/test_JosephsMethodCUDA.cpp index 80a19137bb07ded97257d6730d6d3636b74d0511..33a2bc6df76ef6f87e7750a10a1824dfba9fca32 100644 --- a/elsa/projectors_cuda/tests/test_JosephsMethodCUDA.cpp +++ b/elsa/projectors_cuda/tests/test_JosephsMethodCUDA.cpp @@ -4,31 +4,14 @@ #include "Geometry.h" #include "VolumeDescriptor.h" #include "Logger.h" +#include "PlanarDetectorDescriptor.h" +#include "testHelpers.h" #include using namespace elsa; using namespace elsa::geometry; -/* - * checks whether two DataContainers contain approximately the same data using the same method as - * Eigen - * https://eigen.tuxfamily.org/dox/classEigen_1_1DenseBase.html#ae8443357b808cd393be1b51974213f9c - * - * precision depends on the global elsa::real_t parameter, as the majority of the error is produced - * by the traversal algorithm (which is executed with real_t precision regardless of the - * DataContainer type) - * - */ -template -bool isApprox(const DataContainer& x, const DataContainer& y, - real_t prec = Eigen::NumTraits::dummy_precision()) -{ - DataContainer z = x; - z -= y; - return sqrt(z.squaredL2Norm()) <= prec * sqrt(std::min(x.squaredL2Norm(), y.squaredL2Norm())); -} - /* * this function declaration can be used in conjunction with decltype to deduce the * template parameter of a templated class at compile time @@ -54,11 +37,8 @@ TEMPLATE_TEST_CASE("Scenario: Calls to functions of super class", "", JosephsMet volumeDims << volSize, volSize; sinoDims << detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); volume = 0; - DataContainer sino(sinoDescriptor); - sino = 0; auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -69,8 +49,13 @@ TEMPLATE_TEST_CASE("Scenario: Calls to functions of super class", "", JosephsMet geom.emplace_back(stc, ctr, Radian{angle}, VolumeData2D{Size2D{volumeDims}}, SinogramData2D{Size2D{sinoDims}}); } - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 0; + + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); WHEN("Projector is cloned") { @@ -103,498 +88,499 @@ TEMPLATE_TEST_CASE("Scenario: Calls to functions of super class", "", JosephsMet } } -TEMPLATE_TEST_CASE("Scenario: Output DataContainer is not zero initialized", "", - JosephsMethodCUDA, JosephsMethodCUDA) +TEMPLATE_TEST_CASE("JosephsMethodCUDA 2D setup with a single ray", "", JosephsMethodCUDA, + JosephsMethodCUDA) { // Turn logger of Logger::setLevel(Logger::LogLevel::OFF); using data_t = decltype(return_data_t(std::declval())); - GIVEN("A 2D setting") + + GIVEN("A 5x5 volume and detector of size 1 with 1 angle") { - IndexVector_t volumeDims(2), sinoDims(2); + // Domain setup const index_t volSize = 5; - const index_t detectorSize = 1; - const index_t numImgs = 1; + + IndexVector_t volumeDims(2); volumeDims << volSize, volSize; - sinoDims << detectorSize, numImgs; + VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); + // range setup + const index_t detectorSize = 1; + const index_t numImgs = 1; + + IndexVector_t sinoDims(2); + sinoDims << detectorSize, numImgs; + + // Setup geometry auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; auto volData = VolumeData2D{Size2D{volumeDims}}; auto sinoData = SinogramData2D{Size2D{sinoDims}}; std::vector geom; - geom.emplace_back(stc, ctr, Radian{0}, VolumeData2D{Size2D{volumeDims}}, - SinogramData2D{Size2D{sinoDims}}); - - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); - - WHEN("Sinogram conatainer is not zero initialized and we project through an empty volume") - { - volume = 0; - sino = 1; - - THEN("Result is zero") - { - fast.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(sino == zero); - - sino = 1; - slow.apply(volume, sino); - REQUIRE(sino == zero); - } - } - WHEN("Volume container is not zero initialized and we backproject from an empty sinogram") + GIVEN("A basic geometry setup") { - sino = 0; - volume = 1; - - THEN("Result is zero") - { - fast.applyAdjoint(sino, volume); - DataContainer zero(volumeDescriptor); - zero = 0; - REQUIRE(volume == zero); - - volume = 1; - slow.applyAdjoint(sino, volume); - REQUIRE(volume == zero); - } - } - } - - GIVEN("A 3D setting") - { - IndexVector_t volumeDims(3), sinoDims(3); - const index_t volSize = 3; - const index_t detectorSize = 1; - const index_t numImgs = 1; - volumeDims << volSize, volSize, volSize; - sinoDims << detectorSize, detectorSize, numImgs; - VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); - DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); - - auto stc = SourceToCenterOfRotation{20 * volSize}; - auto ctr = CenterOfRotationToDetector{volSize}; - auto volData = VolumeData3D{Size3D{volumeDims}}; - auto sinoData = SinogramData3D{Size3D{sinoDims}}; - - std::vector geom; - geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), - RotationAngles3D{Gamma{0}}); + geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData)); - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - WHEN("Sinogram conatainer is not zero initialized and we project through an empty volume") - { - volume = 0; - sino = 1; + TestType slow(volumeDescriptor, sinoDescriptor, false); + TestType fast(volumeDescriptor, sinoDescriptor); - THEN("Result is zero") + WHEN("Sinogram conatainer is not zero initialized and we project through an empty " + "volume") { - fast.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(sino == zero); - + volume = 0; sino = 1; - slow.apply(volume, sino); - REQUIRE(sino == zero); - } - } - WHEN("Volume container is not zero initialized and we backproject from an empty sinogram") - { - sino = 0; - volume = 1; - - THEN("Result is zero") - { - fast.applyAdjoint(sino, volume); - DataContainer zero(volumeDescriptor); - zero = 0; - REQUIRE(volume == zero); + THEN("Result is zero") + { + fast.apply(volume, sino); + DataContainer zero(sinoDescriptor); + zero = 0; + REQUIRE(isApprox(sino, zero)); - volume = 1; - slow.applyAdjoint(sino, volume); - REQUIRE(volume == zero); + sino = 1; + slow.apply(volume, sino); + REQUIRE(isApprox(sino, zero)); + } } - } - } -} - -TEMPLATE_TEST_CASE("Scenario: Rays not intersecting the bounding box are present", "", - JosephsMethodCUDA, JosephsMethodCUDA) -{ - // Turn logger of - Logger::setLevel(Logger::LogLevel::OFF); - - using data_t = decltype(return_data_t(std::declval())); - GIVEN("A 2D setting") - { - IndexVector_t volumeDims(2), sinoDims(2); - const index_t volSize = 5; - const index_t detectorSize = 1; - const index_t numImgs = 1; - volumeDims << volSize, volSize; - sinoDims << detectorSize, numImgs; - VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); - DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); - - auto stc = SourceToCenterOfRotation{20 * volSize}; - auto ctr = CenterOfRotationToDetector{volSize}; - auto volData = VolumeData2D{Size2D{volumeDims}}; - auto sinoData = SinogramData2D{Size2D{sinoDims}}; - - volume = 1; - sino = 1; - std::vector geom; - - WHEN("Tracing along a y-axis-aligned ray with a negative x-coordinate of origin") - { - geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData), - PrincipalPointOffset{}, RotationOffset2D{volSize, 0}); - - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); - THEN("Result of forward projection is zero") + WHEN("Volume container is not zero initialized and we backproject from an empty " + "sinogram") { - fast.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(sino == zero); - - slow.apply(volume, sino); - REQUIRE(sino == zero); + sino = 0; + volume = 1; - AND_THEN("Result of backprojection is zero") + THEN("Result is zero") { fast.applyAdjoint(sino, volume); DataContainer zero(volumeDescriptor); zero = 0; - REQUIRE(volume == zero); + REQUIRE(isApprox(volume, zero)); + volume = 1; slow.applyAdjoint(sino, volume); - REQUIRE(volume == zero); + REQUIRE(isApprox(volume, zero)); } } } - WHEN("Tracing along a y-axis-aligned ray with a x-coordinate of origin beyond the bounding " - "box") + GIVEN("Scenario: Rays not intersecting the bounding box are present") { - geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData), - PrincipalPointOffset{}, RotationOffset2D{-volSize, 0}); - - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); - - THEN("Result of forward projection is zero") + WHEN("Tracing along a y-axis-aligned ray with a negative x-coordinate of origin") { - fast.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(sino == zero); + geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData), + PrincipalPointOffset{}, RotationOffset2D{volSize, 0}); - slow.apply(volume, sino); - REQUIRE(sino == zero); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - AND_THEN("Result of backprojection is zero") + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); + + THEN("Result of forward projection is zero") { - fast.applyAdjoint(sino, volume); - DataContainer zero(volumeDescriptor); + fast.apply(volume, sino); + DataContainer zero(sinoDescriptor); zero = 0; - REQUIRE(volume == zero); + REQUIRE(isApprox(sino, zero)); - slow.applyAdjoint(sino, volume); - REQUIRE(volume == zero); - } - } - } + slow.apply(volume, sino); + REQUIRE(isApprox(sino, zero)); - WHEN("Tracing along a x-axis-aligned ray with a negative y-coordinate of origin") - { - geom.emplace_back(stc, ctr, Radian{pi_t / 2}, std::move(volData), std::move(sinoData), - PrincipalPointOffset{}, RotationOffset2D{0, volSize}); + AND_THEN("Result of backprojection is zero") + { + fast.applyAdjoint(sino, volume); + DataContainer zero(volumeDescriptor); + zero = 0; + REQUIRE(isApprox(volume, zero)); - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); + slow.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, zero)); + } + } + } - THEN("Result of forward projection is zero") + WHEN("Tracing along a y-axis-aligned ray with a x-coordinate of origin beyond the " + "bounding " + "box") { - fast.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(sino == zero); + geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData), + PrincipalPointOffset{}, RotationOffset2D{-volSize, 0}); - slow.apply(volume, sino); - REQUIRE(sino == zero); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); - AND_THEN("Result of backprojection is zero") + THEN("Result of forward projection is zero") { - fast.applyAdjoint(sino, volume); - DataContainer zero(volumeDescriptor); + DataContainer zero(sinoDescriptor); zero = 0; - REQUIRE(volume == zero); - slow.applyAdjoint(sino, volume); - REQUIRE(volume == zero); - } - } - } + fast.apply(volume, sino); + REQUIRE(isApprox(sino, zero)); - WHEN("Tracing along a x-axis-aligned ray with a y-coordinate of origin beyond the bounding " - "box") - { - geom.emplace_back(stc, ctr, Radian{pi_t / 2}, std::move(volData), std::move(sinoData), - PrincipalPointOffset{}, RotationOffset2D{0, -volSize}); + slow.apply(volume, sino); + REQUIRE(isApprox(sino, zero)); + + AND_THEN("Result of backprojection is zero") + { + DataContainer zero(volumeDescriptor); + zero = 0; + + fast.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, zero)); - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); + slow.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, zero)); + } + } + } - THEN("Result of forward projection is zero") + WHEN("Tracing along a x-axis-aligned ray with a negative y-coordinate of origin") { - fast.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(sino == zero); + geom.emplace_back(stc, ctr, Radian{pi_t / 2}, std::move(volData), + std::move(sinoData), PrincipalPointOffset{}, + RotationOffset2D{0, volSize}); - slow.apply(volume, sino); - REQUIRE(sino == zero); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - AND_THEN("Result of backprojection is zero") + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); + + THEN("Result of forward projection is zero") { - fast.applyAdjoint(sino, volume); - DataContainer zero(volumeDescriptor); + DataContainer zero(sinoDescriptor); zero = 0; - REQUIRE(volume == zero); - slow.applyAdjoint(sino, volume); - REQUIRE(volume == zero); - } - } - } - } + fast.apply(volume, sino); + REQUIRE(isApprox(sino, zero)); - GIVEN("A 3D setting") - { - IndexVector_t volumeDims(3), sinoDims(3); - const index_t volSize = 5; - const index_t detectorSize = 1; - const index_t numImgs = 1; - volumeDims << volSize, volSize, volSize; - sinoDims << detectorSize, detectorSize, numImgs; - VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); - DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); - volume = 1; - sino = 1; + slow.apply(volume, sino); + REQUIRE(isApprox(sino, zero)); - auto stc = SourceToCenterOfRotation{20 * volSize}; - auto ctr = CenterOfRotationToDetector{volSize}; - auto volData = VolumeData3D{Size3D{volumeDims}}; - auto sinoData = SinogramData3D{Size3D{sinoDims}}; + AND_THEN("Result of backprojection is zero") + { + DataContainer zero(volumeDescriptor); + zero = 0; - std::vector geom; + fast.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, zero)); - constexpr index_t numCases = 9; - std::array alpha = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; - std::array beta = {0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, pi_t / 2, pi_t / 2, pi_t / 2}; - std::array gamma = {0.0, 0.0, 0.0, pi_t / 2, pi_t / 2, - pi_t / 2, pi_t / 2, pi_t / 2, pi_t / 2}; - std::array offsetx = {-volSize, 0.0, -volSize, 0.0, 0.0, - 0.0, -volSize, 0.0, -volSize}; - std::array offsety = {0.0, -volSize, -volSize, -volSize, 0.0, - -volSize, 0.0, 0.0, 0.0}; - std::array offsetz = {0.0, 0.0, 0.0, 0.0, -volSize, - -volSize, 0.0, -volSize, -volSize}; - std::array neg = {"x", "y", "x and y", "y", "z", - "y and z", "x", "z", "x and z"}; - std::array ali = {"z", "z", "z", "x", "x", "x", "y", "y", "y"}; + slow.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, zero)); + } + } + } - for (std::size_t i = 0; i < numCases; i++) { - WHEN("Tracing along a " + ali[i] + "-axis-aligned ray with negative " + neg[i] - + "-coodinate of origin") + WHEN("Tracing along a x-axis-aligned ray with a y-coordinate of origin beyond the " + "bounding " + "box") { - geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), - RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}, Alpha{alpha[i]}}, - PrincipalPointOffset2D{0, 0}, - RotationOffset3D{-offsetx[i], -offsety[i], -offsetz[i]}); + geom.emplace_back(stc, ctr, Radian{pi_t / 2}, std::move(volData), + std::move(sinoData), PrincipalPointOffset{}, + RotationOffset2D{0, -volSize}); - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); THEN("Result of forward projection is zero") { - fast.apply(volume, sino); DataContainer zero(sinoDescriptor); zero = 0; - REQUIRE(sino == zero); + + fast.apply(volume, sino); + REQUIRE(isApprox(sino, zero)); slow.apply(volume, sino); - REQUIRE(sino == zero); + REQUIRE(isApprox(sino, zero)); AND_THEN("Result of backprojection is zero") { - fast.applyAdjoint(sino, volume); DataContainer zero(volumeDescriptor); zero = 0; - REQUIRE(volume == zero); + + fast.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, zero)); slow.applyAdjoint(sino, volume); - REQUIRE(volume == zero); + REQUIRE(isApprox(volume, zero)); } } } } - } -} -TEMPLATE_TEST_CASE("Scenario: Axis-aligned rays are present", "", JosephsMethodCUDA, - JosephsMethodCUDA) -{ - // Turn logger of - Logger::setLevel(Logger::LogLevel::OFF); + // Expected results + Eigen::Matrix backProjections[2]; + backProjections[0].resize(volSize * volSize); + backProjections[1].resize(volSize * volSize); - using data_t = decltype(return_data_t(std::declval())); - GIVEN("A 2D setting with a single ray") - { - IndexVector_t volumeDims(2), sinoDims(2); - const index_t volSize = 5; - const index_t detectorSize = 1; - const index_t numImgs = 1; - volumeDims << volSize, volSize; - sinoDims << detectorSize, numImgs; - VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); + constexpr index_t numCases = 4; + const std::array angles = {0., 90., 180., 270.}; - DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); + // Testing for axis-aligned rays + GIVEN("Given rays along the axes") + { - auto stc = SourceToCenterOfRotation{20 * volSize}; - auto ctr = CenterOfRotationToDetector{volSize}; - auto volData = VolumeData2D{Size2D{volumeDims}}; - auto sinoData = SinogramData2D{Size2D{sinoDims}}; + backProjections[0] << 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 1, 0, 0; + backProjections[1] << 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0; - std::vector geom; + for (std::size_t i = 0; i < numCases; i++) { + WHEN("An axis-aligned ray with an angle of " + std::to_string(angles[i]) + + " degrees passes through the center of a pixel") + { + geom.emplace_back(stc, ctr, Degree{angles[i]}, std::move(volData), + std::move(sinoData)); - const index_t numCases = 4; - const std::array angles = {0.0, pi_t / 2, pi_t, 3 * pi_t / 2}; - Eigen::Matrix backProj[2]; - backProj[0].resize(volSize * volSize); - backProj[1].resize(volSize * volSize); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - backProj[1] << 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0; + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); - backProj[0] << 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0; + THEN("The result of projecting through a pixel is exactly the pixel value") + { + for (index_t j = 0; j < volSize; j++) { + volume = 0; + if (i % 2 == 0) + volume(volSize / 2, j) = 1; + else + volume(j, volSize / 2) = 1; + + fast.apply(volume, sino); + // Using doubles significantly increases interpolation accuracy + // For example: when using floats, points very near the border (less + // than ~1/500th of a pixel away from the border) are rounded to + // actually lie on the border. This then yields more accurate results + // when using floats in some of the axis-aligned test cases, despite the + // lower interpolation accuracy. + // + // => different requirements for floats and doubles, looser + // requirements for doubles + if constexpr (std::is_same_v) + REQUIRE(sino[0] == 1); + else + REQUIRE(sino[0] == Approx(1.0)); + + slow.apply(volume, sino); + if constexpr (std::is_same_v) + REQUIRE(sino[0] == 1); + else + REQUIRE(sino[0] == Approx(1.0)); + } - for (std::size_t i = 0; i < numCases; i++) { - WHEN("An axis-aligned ray with an angle of " + std::to_string(angles[i]) - + " radians passes through the center of a pixel") - { - geom.emplace_back(stc, ctr, Radian{angles[i]}, std::move(volData), - std::move(sinoData)); - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); - THEN("The result of projecting through a pixel is exactly the pixel value") - { - for (index_t j = 0; j < volSize; j++) { - volume = 0; - if (i % 2 == 0) - volume(volSize / 2, j) = 1; - else - volume(j, volSize / 2) = 1; + AND_THEN( + "The backprojection sets the values of all hit pixels to the detector " + "value") + { + fast.applyAdjoint(sino, volume); + REQUIRE( + isApprox(volume, DataContainer(volumeDescriptor, + backProjections[i % 2]))); + + slow.applyAdjoint(sino, volume); + REQUIRE( + isApprox(volume, DataContainer(volumeDescriptor, + backProjections[i % 2]))); + } + } + } + } + } - fast.apply(volume, sino); - /** Using doubles significantly increases interpolation accuracy - * For example: when using floats, points very near the border (less than - * ~1/500th of a pixel away from the border) are rounded to actually lie on - * the border. This then yields more accurate results when using floats in - * some of the axis-aligned test cases, despite the lower interpolation - * accuracy. - * - * => different requirements for floats and doubles, looser requirements - * for doubles - */ - if constexpr (std::is_same_v) - REQUIRE(sino[0] == 1); - else - REQUIRE(sino[0] == Approx(1.0)); + // Move the source far far back, so rays become nearly parallel + stc = SourceToCenterOfRotation{volSize * 2000}; + + GIVEN("Off center rays along axes") + { + std::array offsetx = {-0.25, 0.0, -0.25, 0.0}; + std::array offsety = {0.0, -0.25, 0.0, -0.25}; + + backProjections[0] << 0, 0.25, 0.75, 0, 0, 0, 0.25, 0.75, 0, 0, 0, 0.25, 0.75, 0, 0, 0, + 0.25, 0.75, 0, 0, 0, 0.25, 0.75, 0, 0; + + backProjections[1] << 0, 0, 0, 0, 0, 0.25, 0.25, 0.25, 0.25, 0.25, 0.75, 0.75, 0.75, + 0.75, 0.75, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0; + + for (std::size_t i = 0; i < numCases; i++) { + WHEN("An axis-aligned ray with an angle of " + std::to_string(angles[i]) + + " radians does not pass through the center of a pixel") + { + geom.emplace_back(stc, ctr, Degree{angles[i]}, std::move(volData), + std::move(sinoData), PrincipalPointOffset{0}, + RotationOffset2D{offsetx[i], offsety[i]}); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); + + THEN("The result of projecting through a pixel is the interpolated value " + "between " + "the two pixels closest to the ray") + { + for (index_t j = 0; j < volSize; j++) { + volume = 0; + if (i % 2 == 0) + volume(volSize / 2, j) = 1; + else + volume(j, volSize / 2) = 1; + + fast.apply(volume, sino); + REQUIRE(sino[0] == Approx(0.75)); + + slow.apply(volume, sino); + REQUIRE(sino[0] == Approx(0.75)); + } + + AND_THEN("The slow backprojection yields the exact adjoint, the fast " + "backprojection " + "also yields the exact adjoint for a very distant x-ray source") + { + sino[0] = 1; + slow.applyAdjoint(sino, volume); + REQUIRE( + isApprox(volume, DataContainer(volumeDescriptor, + backProjections[i % 2]))); + + fast.applyAdjoint(sino, volume); + + // Using doubles significantly increases interpolation accuracy + // For example: when using floats, points very near the border (less + // than + // ~1/500th of a pixel away from the border) are rounded to actually lie + // on the border. This then yields more accurate results when using + // floats in some of the axis-aligned test cases, despite the lower + // interpolation accuracy. + // + // => different requirements for floats and doubles, looser + // requirements + // for doubles + if constexpr (std::is_same_v) + REQUIRE(isApprox(volume, + DataContainer(volumeDescriptor, + backProjections[i % 2]))); + else + REQUIRE(isApprox( + volume, + DataContainer(volumeDescriptor, backProjections[i % 2]), + static_cast(0.001))); + } + } + } + } + } + + GIVEN("Rays going running allong right bolume boundary") + { + backProjections[0] << 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 1; + + WHEN("A y-axis-aligned ray runs along the right volume boundary") + { + geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData), + PrincipalPointOffset{0}, RotationOffset2D{volSize * 0.5, 0}); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); + + THEN("The result of projecting through a pixel is exactly the pixel's value (we " + "mirror " + "values at the border for the purpose of interpolation)") + { + for (index_t j = 0; j < volSize; j++) { + volume = 0; + volume(volSize - 1, j) = 1; + + fast.apply(volume, sino); + REQUIRE(sino[0] == 1); slow.apply(volume, sino); - if constexpr (std::is_same_v) - REQUIRE(sino[0] == 1); - else - REQUIRE(sino[0] == Approx(1.0)); + REQUIRE(sino[0] == 1); } - AND_THEN("The backprojection sets the values of all hit pixels to the detector " - "value") + AND_THEN( + "The slow backprojection yields the exact adjoint, the fast backprojection " + "also yields the exact adjoint for a very distant x-ray source") { - fast.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, - DataContainer(volumeDescriptor, backProj[i % 2]))); - + sino[0] = 1; slow.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, - DataContainer(volumeDescriptor, backProj[i % 2]))); + REQUIRE(isApprox( + volume, DataContainer(volumeDescriptor, backProjections[0]))); + + fast.applyAdjoint(sino, volume); + // Using doubles significantly increases interpolation accuracy + // For example: when using floats, points very near the border (less than + // ~1/500th of a pixel away from the border) are rounded to actually lie on + // the border. This then yields more accurate results when using floats in + // some of the axis-aligned test cases, despite the lower interpolation + // accuracy. + // + // => different requirements for floats and doubles, looser requirements + // for doubles + if constexpr (std::is_same_v) + REQUIRE(isApprox( + volume, DataContainer(volumeDescriptor, + (backProjections[0] / 2).eval()))); + else + REQUIRE(isApprox(volume, + DataContainer(volumeDescriptor, + (backProjections[0] / 2).eval()), + static_cast(0.001))); } } } } - std::array offsetx = {-0.25, 0.0, -0.25, 0.0}; - std::array offsety = {0.0, -0.25, 0.0, -0.25}; + GIVEN("Rays going running along left bolume boundary") + { + backProjections[0] << 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, + 0, 0, 0; - backProj[0] << 0, 0.25, 0.75, 0, 0, 0, 0.25, 0.75, 0, 0, 0, 0.25, 0.75, 0, 0, 0, 0.25, 0.75, - 0, 0, 0, 0.25, 0.75, 0, 0; + WHEN("A y-axis-aligned ray runs along the left volume boundary") + { + geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData), + PrincipalPointOffset{0}, RotationOffset2D{-volSize * 0.5, 0}); - backProj[1] << 0, 0, 0, 0, 0, 0.25, 0.25, 0.25, 0.25, 0.25, 0.75, 0.75, 0.75, 0.75, 0.75, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0; + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - for (std::size_t i = 0; i < numCases; i++) { - WHEN("An axis-aligned ray with an angle of " + std::to_string(angles[i]) - + " radians does not pass through the center of a pixel") - { - // x-ray source must be very far from the volume center to make testing of the fast - // backprojection simpler - geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, Radian{angles[i]}, - std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, - RotationOffset2D{offsetx[i], offsety[i]}); - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); - THEN("The result of projecting through a pixel is the interpolated value between " - "the two pixels closest to the ray") + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); + + // TestType fast(volumeDescriptor, sinoDescriptor, geom); + // TestType slow(volumeDescriptor, sinoDescriptor, geom, false); + THEN("The result of projecting through a pixel is exactly the pixel's value (we " + "mirror " + "values at the border for the purpose of interpolation)") { for (index_t j = 0; j < volSize; j++) { volume = 0; - if (i % 2 == 0) - volume(volSize / 2, j) = 1; - else - volume(j, volSize / 2) = 1; + volume(0, j) = 1; fast.apply(volume, sino); - REQUIRE(sino[0] == Approx(0.75)); + REQUIRE(sino[0] == 1); slow.apply(volume, sino); - REQUIRE(sino[0] == Approx(0.75)); + REQUIRE(sino[0] == 1); } AND_THEN( @@ -603,8 +589,8 @@ TEMPLATE_TEST_CASE("Scenario: Axis-aligned rays are present", "", JosephsMethodC { sino[0] = 1; slow.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, - DataContainer(volumeDescriptor, backProj[i % 2]))); + REQUIRE(isApprox( + volume, DataContainer(volumeDescriptor, backProjections[0]))); fast.applyAdjoint(sino, volume); /** Using doubles significantly increases interpolation accuracy @@ -619,486 +605,721 @@ TEMPLATE_TEST_CASE("Scenario: Axis-aligned rays are present", "", JosephsMethodC */ if constexpr (std::is_same_v) REQUIRE(isApprox( - volume, DataContainer(volumeDescriptor, backProj[i % 2]))); + volume, DataContainer(volumeDescriptor, + (backProjections[0] / 2).eval()))); else - REQUIRE(isApprox( - volume, DataContainer(volumeDescriptor, backProj[i % 2]), - static_cast(0.001))); + REQUIRE(isApprox(volume, + DataContainer(volumeDescriptor, + (backProjections[0] / 2).eval()), + static_cast(0.001))); } } } } + } - backProj[0] << 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1; - - WHEN("A y-axis-aligned ray runs along the right volume boundary") - { - geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, Radian{0}, - std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, - RotationOffset2D{volSize * 0.5, 0}); + GIVEN("A 4x4 volume and detector of size 1 with 1 angle at -30 degree") + { + // Domain setup + const index_t volSize = 4; - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); + IndexVector_t volumeDims(2); + volumeDims << volSize, volSize; - THEN("The result of projecting through a pixel is exactly the pixel's value (we mirror " - "values at the border for the purpose of interpolation)") - { - for (index_t j = 0; j < volSize; j++) { - volume = 0; - volume(volSize - 1, j) = 1; + VolumeDescriptor volumeDescriptor(volumeDims); + DataContainer volume(volumeDescriptor); - fast.apply(volume, sino); - REQUIRE(sino[0] == 1); + // range setup + const index_t detectorSize = 1; + const index_t numImgs = 1; - slow.apply(volume, sino); - REQUIRE(sino[0] == 1); - } + IndexVector_t sinoDims(2); + sinoDims << detectorSize, numImgs; - AND_THEN( - "The slow backprojection yields the exact adjoint, the fast backprojection " - "also yields the exact adjoint for a very distant x-ray source") - { - sino[0] = 1; - slow.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, backProj[0]))); + // Setup geometry + const auto stc = SourceToCenterOfRotation{20 * volSize}; + const auto ctr = CenterOfRotationToDetector{volSize}; + auto volData = VolumeData2D{Size2D{volumeDims}}; + auto sinoData = SinogramData2D{Size2D{sinoDims}}; - fast.applyAdjoint(sino, volume); - /** Using doubles significantly increases interpolation accuracy - * For example: when using floats, points very near the border (less than - * ~1/500th of a pixel away from the border) are rounded to actually lie on the - * border. This then yields more accurate results when using floats in some of - * the axis-aligned test cases, despite the lower interpolation accuracy. - * - * => different requirements for floats and doubles, looser requirements for - * doubles - */ - if constexpr (std::is_same_v) - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, - (backProj[0] / 2).eval()))); - else - REQUIRE(isApprox( - volume, - DataContainer(volumeDescriptor, (backProj[0] / 2).eval()), - static_cast(0.001))); - } - } - } + std::vector geom; - backProj[0] << 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0; + const real_t sqrt3r = std::sqrt(static_cast(3)); + const data_t sqrt3d = std::sqrt(static_cast(3)); + const data_t halfd = static_cast(0.5); + const data_t thirdd = static_cast(1.0 / 3); - WHEN("A y-axis-aligned ray runs along the left volume boundary") + GIVEN("An angle of -30 degrees") { - geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, Radian{0}, - std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, - RotationOffset2D{-volSize * 0.5, 0}); - - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); - THEN("The result of projecting through a pixel is exactly the pixel's value (we mirror " - "values at the border for the purpose of interpolation)") + const auto angle = Degree{-30}; + + WHEN("Projections goes through center of volume") { - for (index_t j = 0; j < volSize; j++) { - volume = 0; - volume(0, j) = 1; + geom.emplace_back(stc, ctr, angle, std::move(volData), std::move(sinoData)); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 0; + + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); + + real_t weight = 2 / sqrt3r; + + THEN("Ray intersects the correct pixels") + { + volume = 1; + volume(3, 0) = 0; + volume(1, 1) = 0; + volume(1, 2) = 0; + volume(1, 3) = 0; + + volume(2, 0) = 0; + volume(2, 1) = 0; + volume(2, 2) = 0; + volume(0, 3) = 0; fast.apply(volume, sino); - REQUIRE(sino[0] == 1); + DataContainer zero(sinoDescriptor); + zero = 0; + REQUIRE(isApprox(sino, zero)); slow.apply(volume, sino); - REQUIRE(sino[0] == 1); - } + REQUIRE(isApprox(sino, zero)); - AND_THEN( - "The slow backprojection yields the exact adjoint, the fast backprojection " - "also yields the exact adjoint for a very distant x-ray source") - { - sino[0] = 1; - slow.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, backProj[0]))); + AND_THEN("The correct weighting is applied") + { + volume(3, 0) = 1; + volume(1, 1) = 1; + volume(1, 2) = 1; + volume(1, 3) = 1; - fast.applyAdjoint(sino, volume); - /** Using doubles significantly increases interpolation accuracy - * For example: when using floats, points very near the border (less than - * ~1/500th of a pixel away from the border) are rounded to actually lie on the - * border. This then yields more accurate results when using floats in some of - * the axis-aligned test cases, despite the lower interpolation accuracy. - * - * => different requirements for floats and doubles, looser requirements for - * doubles - */ - if constexpr (std::is_same_v) - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, - (backProj[0] / 2).eval()))); - else - REQUIRE(isApprox( - volume, - DataContainer(volumeDescriptor, (backProj[0] / 2).eval()), - static_cast(0.001))); + fast.apply(volume, sino); + REQUIRE(sino[0] == Approx(2 * weight)); + + slow.apply(volume, sino); + REQUIRE(sino[0] == Approx(2 * weight)); + + sino[0] = 1; + + Eigen::Matrix slowExpected(volSize * volSize); + slowExpected << 0, 0, (3 - sqrt3d) / 2, (sqrt3d - 1) / 2, 0, + (sqrt3d - 1) / (2 * sqrt3d), (sqrt3d + 1) / (2 * sqrt3d), 0, 0, + (sqrt3d + 1) / (2 * sqrt3d), (sqrt3d - 1) / (2 * sqrt3d), 0, + (sqrt3d - 1) / 2, (3 - sqrt3d) / 2, 0, 0; + + slowExpected *= weight; + slow.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, + DataContainer(volumeDescriptor, slowExpected))); + + fast.applyAdjoint(sino, volume); + for (real_t i = 0.5; i < volSize; i += 1) { + for (real_t j = 0.5; j < volSize; j += 1) { + const real_t angle = + std::abs(std::atan((sqrt3r * volSize * 10 + - static_cast(volSize / 2.0) + j) + / (volSize * 10 + + static_cast(volSize / 2.0) - i)) + - pi_t / 3); + const real_t len = volSize * 21 * std::tan(angle); + if (len < 1) { + REQUIRE(volume((index_t) i, (index_t) j) + == Approx(1 - len).epsilon(0.005)); + } else { + REQUIRE(volume((index_t) i, (index_t) j) == 0); + } + } + } + } } } - } - } - GIVEN("A 3D setting with a single ray") - { - IndexVector_t volumeDims(3), sinoDims(3); - const index_t volSize = 3; - const index_t detectorSize = 1; - const index_t numImgs = 1; - volumeDims << volSize, volSize, volSize; - sinoDims << detectorSize, detectorSize, numImgs; - VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); - DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); + WHEN("Ray enters through the right border") + { + // In this case the ray exits through a border along the main ray direction, but + // enters through a border not along the main direction First pixel should be + // weighted differently + geom.emplace_back(stc, ctr, angle, std::move(volData), std::move(sinoData), + PrincipalPointOffset{0}, RotationOffset2D{sqrt3r, 0}); - auto stc = SourceToCenterOfRotation{20 * volSize}; - auto ctr = CenterOfRotationToDetector{volSize}; - auto volData = VolumeData3D{Size3D{volumeDims}}; - auto sinoData = SinogramData3D{Size3D{sinoDims}}; + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 0; - std::vector geom; + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); - const index_t numCases = 6; - std::array beta = {0.0, 0.0, 0.0, 0.0, pi_t / 2, 3 * pi_t / 2}; - std::array gamma = {0.0, pi_t, pi_t / 2, - 3 * pi_t / 2, pi_t / 2, 3 * pi_t / 2}; - std::array al = {"z", "-z", "x", "-x", "y", "-y"}; + // TestType fast(volumeDescriptor, sinoDescriptor, geom); + // TestType slow(volumeDescriptor, sinoDescriptor, geom, false); - Eigen::Matrix backProj[numCases]; - for (auto& backPr : backProj) - backPr.resize(volSize * volSize * volSize); + THEN("Ray intersects the correct pixels") + { + volume = 1; + volume(3, 1) = 0; + volume(3, 2) = 0; + volume(3, 3) = 0; + volume(2, 3) = 0; + volume(2, 2) = 0; - backProj[2] << 0, 0, 0, 0, 0, 0, 0, 0, 0, + fast.apply(volume, sino); + DataContainer zero(sinoDescriptor); + zero = 0; + REQUIRE(isApprox(sino, zero)); - 0, 1, 0, 0, 1, 0, 0, 1, 0, + slow.apply(volume, sino); + REQUIRE(isApprox(sino, zero)); - 0, 0, 0, 0, 0, 0, 0, 0, 0; + AND_THEN("The correct weighting is applied") + { + volume(3, 1) = 1; + volume(2, 2) = 1; + volume(2, 3) = 1; - backProj[1] << 0, 0, 0, 0, 0, 0, 0, 0, 0, + fast.apply(volume, sino); + REQUIRE(sino[0] + == Approx((4 - 2 * sqrt3d) * (sqrt3d - 1) + + (2 / sqrt3d) * (3 - 8 * sqrt3d / 6)) + .epsilon(0.005)); - 0, 0, 0, 1, 1, 1, 0, 0, 0, + slow.apply(volume, sino); + REQUIRE(sino[0] + == Approx((4 - 2 * sqrt3d) * (sqrt3d - 1) + + (2 / sqrt3d) * (3 - 8 * sqrt3d / 6)) + .epsilon(0.005)); - 0, 0, 0, 0, 0, 0, 0, 0, 0; + sino[0] = 1; - backProj[0] << 0, 0, 0, 0, 1, 0, 0, 0, 0, + Eigen::Matrix slowExpected(volSize * volSize); + slowExpected << 0, 0, 0, 0, 0, 0, 0, (4 - 2 * sqrt3d) * (sqrt3d - 1), 0, 0, + (2 / sqrt3d) * (1 + halfd - 5 * sqrt3d / 6), + (4 - 2 * sqrt3d) * (2 - sqrt3d) + + (2 / sqrt3d) * (5 * sqrt3d / 6 - halfd), + 0, 0, (2 / sqrt3d) * (1 + halfd - sqrt3d / 2), + (2 / sqrt3d) * (sqrt3d / 2 - halfd); - 0, 0, 0, 0, 1, 0, 0, 0, 0, + slow.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, + DataContainer(volumeDescriptor, slowExpected))); - 0, 0, 0, 0, 1, 0, 0, 0, 0; + fast.applyAdjoint(sino, volume); + for (real_t i = 0.5; i < volSize; i += 1) { + for (real_t j = 0.5; j < volSize; j += 1) { + const real_t angle = + std::abs(std::atan((40 * sqrt3r - 2 + j) / (42 + sqrt3r - i)) + - pi_t / 3); + const real_t len = 84 * std::tan(angle); + if (len < 1) { + REQUIRE(volume((index_t) i, (index_t) j) + == Approx(1 - len).epsilon(0.01)); + } else { + REQUIRE(volume((index_t) i, (index_t) j) == 0); + } + } + } + } + } + } - for (std::size_t i = 0; i < numCases; i++) { - WHEN("A " + al[i] + "-axis-aligned ray passes through the center of a pixel") + WHEN("Ray exits through the left border") { - geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), - RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}}); + // In this case the ray enters through a border along the main ray direction, but + // exits through a border not along the main direction Last pixel should be weighted + // differently + geom.emplace_back(stc, ctr, angle, std::move(volData), std::move(sinoData), + PrincipalPointOffset{0}, RotationOffset2D{-sqrt3r, 0}); - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); - THEN("The result of projecting through a voxel is exactly the voxel value") + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 0; + + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); + + THEN("Ray intersects the correct pixels") { - for (index_t j = 0; j < volSize; j++) { - volume = 0; - if (i / 2 == 0) - volume(volSize / 2, volSize / 2, j) = 1; - else if (i / 2 == 1) - volume(j, volSize / 2, volSize / 2) = 1; - else if (i / 2 == 2) - volume(volSize / 2, j, volSize / 2) = 1; + volume = 1; + volume(0, 0) = 0; + volume(1, 0) = 0; + volume(0, 1) = 0; + volume(1, 1) = 0; + volume(0, 2) = 0; + + fast.apply(volume, sino); + DataContainer zero(sinoDescriptor); + zero = 0; + REQUIRE(isApprox(sino, zero)); + + slow.apply(volume, sino); + REQUIRE(isApprox(sino, zero)); + + AND_THEN("The correct weighting is applied") + { + volume(1, 0) = 1; + volume(0, 1) = 1; fast.apply(volume, sino); - /** Using doubles significantly increases interpolation accuracy - * For example: when using floats, points very near the border (less than - * ~1/500th of a pixel away from the border) are rounded to actually lie on - * the border. This then yields more accurate results when using floats in - * some of the axis-aligned test cases, despite the lower interpolation - * accuracy. - * - * => different requirements for floats and doubles, looser requirements - * for doubles - */ - if constexpr (std::is_same_v) - REQUIRE(sino[0] == 1); - else - REQUIRE(sino[0] == Approx(1.0)); + REQUIRE(sino[0] + == Approx((sqrt3d - 1) + (5.0 / 3.0 - 1 / sqrt3d) + + (4 - 2 * sqrt3d) * (2 - sqrt3d)) + .epsilon(0.005)); slow.apply(volume, sino); - if constexpr (std::is_same_v) - REQUIRE(sino[0] == 1); - else - REQUIRE(sino[0] == Approx(1.0)); - } + REQUIRE(sino[0] + == Approx((sqrt3d - 1) + (5.0 / 3.0 - 1 / sqrt3d) + + (4 - 2 * sqrt3d) * (2 - sqrt3d)) + .epsilon(0.005)); - AND_THEN("The backprojection sets the values of all hit voxels to the detector " - "value") - { - fast.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, - DataContainer(volumeDescriptor, backProj[i / 2]))); + sino[0] = 1; + + Eigen::Matrix slowExpected(volSize * volSize); + slowExpected << 1 - 1 / sqrt3d, sqrt3d - 1, 0, 0, + (5 * thirdd - 1 / sqrt3d) + (4 - 2 * sqrt3d) * (2 - sqrt3d), + sqrt3d - 5 * thirdd, 0, 0, (sqrt3d - 1) * (4 - 2 * sqrt3d), 0, 0, 0, 0, + 0, 0, 0; slow.applyAdjoint(sino, volume); REQUIRE(isApprox(volume, - DataContainer(volumeDescriptor, backProj[i / 2]))); + DataContainer(volumeDescriptor, slowExpected))); + + fast.applyAdjoint(sino, volume); + + for (real_t i = 0.5; i < volSize; i += 1) { + for (real_t j = 0.5; j < volSize; j += 1) { + const real_t angle = + std::abs(std::atan((40 * sqrt3r - 2 + j) / (42 - sqrt3r - i)) + - pi_t / 3); + const real_t len = 84 * std::tan(angle); + + if (len < 1) { + REQUIRE(volume((index_t) i, (index_t) j) + == Approx(1 - len).margin(0.002)); + } else { + REQUIRE(volume((index_t) i, (index_t) j) == 0); + } + } + } } } } - } - std::array offsetx = {-0.25, -0.25, 0.0, 0.0, 0.0, 0.0}; - std::array offsety = {0.0, 0.0, -0.25, -0.25, 0.0, 0.0}; - std::array offsetz = {0.0, 0.0, 0.0, 0.0, -0.25, -0.25}; + WHEN("Ray only intersects a single pixel") + { + // This is a special case that is handled separately in both forward and + // backprojection + geom.emplace_back(stc, ctr, angle, std::move(volData), std::move(sinoData), + PrincipalPointOffset{0}, RotationOffset2D{-2 - sqrt3r / 2, 0}); - backProj[2] << 0, 0.25, 0, 0, 0.25, 0, 0, 0.25, 0, + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 0; - 0, 0.75, 0, 0, 0.75, 0, 0, 0.75, 0, + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); - 0, 0, 0, 0, 0, 0, 0, 0, 0; + THEN("Ray intersects the correct pixels") + { + volume = 1; + volume(0, 0) = 0; - backProj[1] << 0, 0, 0, 0, 0, 0, 0, 0, 0, + fast.apply(volume, sino); + DataContainer zero(sinoDescriptor); + zero = 0; + REQUIRE(isApprox(sino, zero)); - 0.25, 0.25, 0.25, 0.75, 0.75, 0.75, 0, 0, 0, + slow.apply(volume, sino); + REQUIRE(isApprox(sino, zero)); - 0, 0, 0, 0, 0, 0, 0, 0, 0; + AND_THEN("The correct weighting is applied") + { + volume(0, 0) = 1; - backProj[0] << 0, 0, 0, 0.25, 0.75, 0, 0, 0, 0, + fast.apply(volume, sino); + REQUIRE(sino[0] == Approx(1 / sqrt3d).epsilon(0.005)); - 0, 0, 0, 0.25, 0.75, 0, 0, 0, 0, + slow.apply(volume, sino); + REQUIRE(sino[0] == Approx(1 / sqrt3d).epsilon(0.005)); - 0, 0, 0, 0.25, 0.75, 0, 0, 0, 0; + sino[0] = 1; - for (std::size_t i = 0; i < numCases; i++) { - WHEN("A " + al[i] + "-axis-aligned ray does not pass through the center of a voxel") - { - // x-ray source must be very far from the volume center to make testing of the fast - // backprojection simpler - geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, std::move(volData), - std::move(sinoData), - RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}}, - PrincipalPointOffset2D{0, 0}, - RotationOffset3D{offsetx[i], offsety[i], offsetz[i]}); + Eigen::Matrix slowExpected(volSize * volSize); + slowExpected << 1 / sqrt3d, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0; - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); - THEN("The result of projecting through a voxel is the interpolated value between " - "the four voxels nearest to the ray") - { - for (index_t j = 0; j < volSize; j++) { - volume = 0; - if (i / 2 == 0) - volume(volSize / 2, volSize / 2, j) = 1; - else if (i / 2 == 1) - volume(j, volSize / 2, volSize / 2) = 1; - else if (i / 2 == 2) - volume(volSize / 2, j, volSize / 2) = 1; + slow.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, + DataContainer(volumeDescriptor, slowExpected))); - fast.apply(volume, sino); - /** Using doubles significantly increases interpolation accuracy - * For example: when using floats, points very near the border (less than - * ~1/500th of a pixel away from the border) are rounded to actually lie on - * the border. This then yields more accurate results when using floats in - * some of the axis-aligned test cases, despite the lower interpolation - * accuracy. - * - * => different requirements for floats and doubles, looser requirements - * for doubles - */ - if constexpr (std::is_same_v) - REQUIRE(sino[0] == 0.75); - else - REQUIRE(sino[0] == Approx(0.75)); + fast.applyAdjoint(sino, volume); - slow.apply(volume, sino); - if constexpr (std::is_same_v) - REQUIRE(sino[0] == 0.75); - else - REQUIRE(sino[0] == Approx(0.75)); + for (real_t i = 0.5; i < volSize; i += 1) { + for (real_t j = 0.5; j < volSize; j += 1) { + const real_t angle = std::abs( + std::atan((40 * sqrt3r - 2 + j) / (40 - sqrt3r / 2 - i)) + - pi_t / 3); + const real_t len = 84 * std::tan(angle); + + if (len < 1) { + REQUIRE(volume((index_t) i, (index_t) j) + == Approx(1 - len).margin(0.002)); + } else { + REQUIRE(volume((index_t) i, (index_t) j) == 0); + } + } + } } + } + } + } - AND_THEN("The slow backprojection yields the exact adjoint, the fast " - "backprojection is also exact for a very distant x-ray source") + GIVEN("An angle of -120 degrees") + { + const auto angle = Degree{-120}; + + WHEN("Ray goes through center of volume") + { + // In this case the ray enters and exits the volume through the borders along the + // main direction Weighting for all interpolated values should be the same + geom.emplace_back(stc, ctr, angle, std::move(volData), std::move(sinoData)); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 0; + + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); + + real_t weight = 2 / sqrt3r; + THEN("Ray intersects the correct pixels") + { + sino[0] = 1; + slow.applyAdjoint(sino, volume); + + volume = 1; + volume(0, 0) = 0; + volume(0, 1) = 0; + volume(1, 1) = 0; + volume(1, 2) = 0; + + volume(2, 1) = 0; + volume(2, 2) = 0; + volume(3, 2) = 0; + volume(3, 3) = 0; + + fast.apply(volume, sino); + DataContainer zero(sinoDescriptor); + zero = 0; + REQUIRE(isApprox(sino, zero)); + + slow.apply(volume, sino); + REQUIRE(isApprox(sino, zero)); + + AND_THEN("The correct weighting is applied") { + volume(3, 3) = 1; + volume(0, 1) = 1; + volume(1, 1) = 1; + volume(2, 1) = 1; + + fast.apply(volume, sino); + REQUIRE(sino[0] == Approx(2 * weight)); + + slow.apply(volume, sino); + REQUIRE(sino[0] == Approx(2 * weight)); + sino[0] = 1; - fast.applyAdjoint(sino, volume); - /** Using doubles significantly increases interpolation accuracy - * For example: when using floats, points very near the border (less than - * ~1/500th of a pixel away from the border) are rounded to actually lie on - * the border. This then yields more accurate results when using floats in - * some of the axis-aligned test cases, despite the lower interpolation - * accuracy. - * - * => different requirements for floats and doubles, looser requirements - * for doubles - */ - if constexpr (std::is_same_v) - REQUIRE(isApprox( - volume, DataContainer(volumeDescriptor, backProj[i / 2]))); - else - REQUIRE(isApprox( - volume, DataContainer(volumeDescriptor, backProj[i / 2]), - static_cast(0.005))); + Eigen::Matrix slowExpected(volSize * volSize); + + slowExpected << (sqrt3d - 1) / 2, 0, 0, 0, (3 - sqrt3d) / 2, + (sqrt3d + 1) / (2 * sqrt3d), (sqrt3d - 1) / (2 * sqrt3d), 0, 0, + (sqrt3d - 1) / (2 * sqrt3d), (sqrt3d + 1) / (2 * sqrt3d), + (3 - sqrt3d) / 2, 0, 0, 0, (sqrt3d - 1) / 2; + + slowExpected *= weight; slow.applyAdjoint(sino, volume); REQUIRE(isApprox(volume, - DataContainer(volumeDescriptor, backProj[i / 2]))); + DataContainer(volumeDescriptor, slowExpected))); + + fast.applyAdjoint(sino, volume); + for (real_t i = 0.5; i < volSize; i += 1) { + for (real_t j = 0.5; j < volSize; j += 1) { + const real_t angle = std::abs( + std::atan((sqrt3r * 40 + 2 - i) / (42 - j)) - pi_t / 3); + const real_t len = volSize * 21 * std::tan(angle); + if (len < 1) { + REQUIRE(volume((index_t) i, (index_t) j) + == Approx(1 - len).epsilon(0.005)); + } else { + REQUIRE(volume((index_t) i, (index_t) j) == 0); + } + } + } } } } - } - offsetx[0] = -volSize / 2.0; - offsetx[1] = volSize / 2.0; - offsetx[2] = 0.0; - offsetx[3] = 0.0; - offsetx[4] = volSize / 2.0; - offsetx[5] = -volSize / 2.0; + WHEN("Ray enters through the top border") + { + // In this case the ray exits through a border along the main ray direction, but + // enters through a border not along the main direction First pixel should be + // weighted differently + geom.emplace_back(stc, ctr, angle, std::move(volData), std::move(sinoData), + PrincipalPointOffset{0}, RotationOffset2D{0, std::sqrt(3.f)}); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 0; + + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); + + THEN("Ray intersects the correct pixels") + { + volume = 1; + volume(0, 2) = 0; + volume(0, 3) = 0; + volume(1, 2) = 0; + volume(1, 3) = 0; + volume(2, 3) = 0; - offsety[0] = 0.0; - offsety[1] = 0.0; - offsety[2] = -volSize / 2.0; - offsety[3] = volSize / 2.0; - offsety[4] = volSize / 2.0; - offsety[5] = -volSize / 2.0; + fast.apply(volume, sino); + DataContainer zero(sinoDescriptor); + zero = 0; + REQUIRE(isApprox(sino, zero)); - backProj[0] << 0, 0, 0, 1, 0, 0, 0, 0, 0, + slow.apply(volume, sino); + REQUIRE(isApprox(sino, zero)); - 0, 0, 0, 1, 0, 0, 0, 0, 0, + AND_THEN("The correct weighting is applied") + { + volume(2, 3) = 1; + volume(1, 2) = 1; + volume(1, 3) = 1; - 0, 0, 0, 1, 0, 0, 0, 0, 0; + fast.apply(volume, sino); + REQUIRE(sino[0] == Approx((4 - 2 * sqrt3d) + (2 / sqrt3d))); - backProj[1] << 0, 0, 0, 0, 0, 1, 0, 0, 0, + slow.apply(volume, sino); + REQUIRE(sino[0] == Approx((4 - 2 * sqrt3d) + (2 / sqrt3d))); - 0, 0, 0, 0, 0, 1, 0, 0, 0, + sino[0] = 1; - 0, 0, 0, 0, 0, 1, 0, 0, 0; + Eigen::Matrix slowExpected(volSize * volSize); - backProj[2] << 0, 1, 0, 0, 0, 0, 0, 0, 0, + slowExpected << 0, 0, 0, 0, 0, 0, 0, 0, + (2 / sqrt3d) * (1 + halfd - sqrt3d / 2), + (2 / sqrt3d) * (1 + halfd - 5 * sqrt3d / 6), 0, 0, + (2 / sqrt3d) * (sqrt3d / 2 - halfd), + (4 - 2 * sqrt3d) * (2 - sqrt3d) + + (2 / sqrt3d) * (5 * sqrt3d / 6 - halfd), + (4 - 2 * sqrt3d) * (sqrt3d - 1), 0; - 0, 1, 0, 0, 0, 0, 0, 0, 0, + slow.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, + DataContainer(volumeDescriptor, slowExpected))); - 0, 1, 0, 0, 0, 0, 0, 0, 0; + fast.applyAdjoint(sino, volume); + for (real_t i = 0.5; i < volSize; i += 1) { + for (real_t j = 0.5; j < volSize; j += 1) { + const real_t angle = + std::abs(std::atan((sqrt3r * 40 + 2 - i) / (42 + sqrt3r - j)) + - pi_t / 3); + const real_t len = volSize * 21 * std::tan(angle); + if (len < 1) { + REQUIRE(volume((index_t) i, (index_t) j) + == Approx(1 - len).epsilon(0.01)); + } else { + REQUIRE(volume((index_t) i, (index_t) j) == 0); + } + } + } + } + } + } - backProj[3] << 0, 0, 0, 0, 0, 0, 0, 1, 0, + WHEN("Ray exits through the bottom border") + { + // In this case the ray enters through a border along the main ray direction, but + // exits through a border not along the main direction Last pixel should be weighted + // differently + geom.emplace_back(stc, ctr, angle, std::move(volData), std::move(sinoData), + PrincipalPointOffset{0}, RotationOffset2D{0, -std::sqrt(3.f)}); - 0, 0, 0, 0, 0, 0, 0, 1, 0, + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 0; - 0, 0, 0, 0, 0, 0, 0, 1, 0; + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); - backProj[4] << 0, 0, 0, 0, 0, 0, 0, 0, 1, + THEN("Ray intersects the correct pixels") + { + volume = 1; + volume(1, 0) = 0; + volume(2, 0) = 0; + volume(3, 0) = 0; + volume(2, 1) = 0; + volume(3, 1) = 0; - 0, 0, 0, 0, 0, 0, 0, 0, 1, + fast.apply(volume, sino); + DataContainer zero(sinoDescriptor); + zero = 0; + REQUIRE(isApprox(sino, zero)); - 0, 0, 0, 0, 0, 0, 0, 0, 1; + slow.apply(volume, sino); + REQUIRE(isApprox(sino, zero)); - backProj[5] << 1, 0, 0, 0, 0, 0, 0, 0, 0, + AND_THEN("The correct weighting is applied") + { + volume(2, 0) = 1; + volume(3, 1) = 1; - 1, 0, 0, 0, 0, 0, 0, 0, 0, + fast.apply(volume, sino); + REQUIRE(sino[0] + == Approx((sqrt3d - 1) + (5.0 / 3.0 - 1 / sqrt3d) + + (4 - 2 * sqrt3d) * (2 - sqrt3d)) + .epsilon(0.005)); - 1, 0, 0, 0, 0, 0, 0, 0, 0; + slow.apply(volume, sino); + REQUIRE(sino[0] + == Approx((sqrt3d - 1) + (5.0 / 3.0 - 1 / sqrt3d) + + (4 - 2 * sqrt3d) * (2 - sqrt3d)) + .epsilon(0.005)); - al[0] = "left border"; - al[1] = "right border"; - al[2] = "top border"; - al[3] = "bottom border"; - al[4] = "top right edge"; - al[5] = "bottom left edge"; + sino[0] = 1; - for (std::size_t i = 0; i < numCases; i++) { - WHEN("A z-axis-aligned ray runs along the " + al[i] + " of the volume") + Eigen::Matrix slowExpected(volSize * volSize); + + slowExpected << 0, (sqrt3d - 1) * (4 - 2 * sqrt3d), + (5 * thirdd - 1 / sqrt3d) + (4 - 2 * sqrt3d) * (2 - sqrt3d), + 1 - 1 / sqrt3d, 0, 0, sqrt3d - 5 * thirdd, sqrt3d - 1, 0, 0, 0, 0, 0, 0, + 0, 0; + + slow.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, + DataContainer(volumeDescriptor, slowExpected))); + + fast.applyAdjoint(sino, volume); + + for (real_t i = 0.5; i < volSize; i += 1) { + for (real_t j = 0.5; j < volSize; j += 1) { + const real_t angle = + std::abs(std::atan((sqrt3r * 40 + 2 - i) / (42 - sqrt3r - j)) + - pi_t / 3); + const real_t len = 84 * std::tan(angle); + + if (len < 1) { + REQUIRE(volume((index_t) i, (index_t) j) + == Approx(1 - len).margin(0.002)); + } else { + REQUIRE(volume((index_t) i, (index_t) j) == 0); + } + } + } + } + } + } + + WHEN("Ray only intersects a single pixel") { - // x-ray source must be very far from the volume center to make testing of the fast - // backprojection simpler - geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, std::move(volData), - std::move(sinoData), RotationAngles3D{Gamma{0}}, - PrincipalPointOffset2D{0, 0}, - RotationOffset3D{offsetx[i], offsety[i], 0}); - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); - THEN("The result of projecting through a voxel is exactly the voxel's value (we " - "mirror values at the border for the purpose of interpolation)") + // This is a special case that is handled separately in both forward and + // backprojection + geom.emplace_back(stc, ctr, angle, std::move(volData), std::move(sinoData), + PrincipalPointOffset{0}, + RotationOffset2D{0, -2 - std::sqrt(3.f) / 2}); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 0; + + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); + + THEN("Ray intersects the correct pixels") { - for (index_t j = 0; j < volSize; j++) { - volume = 0; - switch (i) { - case 0: - volume(0, volSize / 2, j) = 1; - break; - case 1: - volume(volSize - 1, volSize / 2, j) = 1; - break; - case 2: - volume(volSize / 2, 0, j) = 1; - break; - case 3: - volume(volSize / 2, volSize - 1, j) = 1; - break; - case 4: - volume(volSize - 1, volSize - 1, j) = 1; - break; - case 5: - volume(0, 0, j) = 1; - break; - default: - break; - } + volume = 1; + volume(3, 0) = 0; + + fast.apply(volume, sino); + DataContainer zero(sinoDescriptor); + zero = 0; + REQUIRE(isApprox(sino, zero)); + + slow.apply(volume, sino); + REQUIRE(isApprox(sino, zero)); + + AND_THEN("The correct weighting is applied") + { + volume(3, 0) = 1; fast.apply(volume, sino); - REQUIRE(sino[0] == 1); + REQUIRE(sino[0] == Approx(1 / sqrt3d).epsilon(0.005)); slow.apply(volume, sino); - REQUIRE(sino[0] == 1); - } + REQUIRE(sino[0] == Approx(1 / sqrt3d).epsilon(0.005)); - AND_THEN("The slow backprojection yields the exact adjoint, the fast " - "backprojection is also exact for a very distant x-ray source") - { sino[0] = 1; - fast.applyAdjoint(sino, volume); - /** Using doubles significantly increases interpolation accuracy - * For example: when using floats, points very near the border (less than - * ~1/500th of a pixel away from the border) are rounded to actually lie on - * the border. This then yields more accurate results when using floats in - * some of the axis-aligned test cases, despite the lower interpolation - * accuracy. - * - * => different requirements for floats and doubles, looser requirements - * for doubles - */ - if constexpr (std::is_same_v) - REQUIRE(isApprox(volume, DataContainer( - volumeDescriptor, - (backProj[i] / (i > 3 ? 4 : 2)).eval()))); - else - REQUIRE(isApprox( - volume, - DataContainer(volumeDescriptor, - (backProj[i] / (i > 3 ? 4 : 2)).eval()), - static_cast(0.005))); + Eigen::Matrix slowExpected(volSize * volSize); + slowExpected << 0, 0, 0, 1 / sqrt3d, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0; slow.applyAdjoint(sino, volume); - REQUIRE( - isApprox(volume, DataContainer(volumeDescriptor, backProj[i]))); + REQUIRE(isApprox(volume, + DataContainer(volumeDescriptor, slowExpected))); + + fast.applyAdjoint(sino, volume); + + for (real_t i = 0.5; i < volSize; i += 1) { + for (real_t j = 0.5; j < volSize; j += 1) { + const real_t angle = std::abs( + std::atan((sqrt3r * 40 + 2 - i) / (40 - sqrt3r / 2 - j)) + - pi_t / 3); + const real_t len = 84 * std::tan(angle); + + if (len < 1) { + REQUIRE(volume((index_t) i, (index_t) j) + == Approx(1 - len).margin(0.002)); + } else { + REQUIRE(volume((index_t) i, (index_t) j) == 0); + } + } + } } } } } } +} + +TEMPLATE_TEST_CASE("JosephsMethodCUDA 2D setup with a multiple rays", "", JosephsMethodCUDA, + JosephsMethodCUDA) +{ + // Turn logger of + Logger::setLevel(Logger::LogLevel::OFF); + + using data_t = decltype(return_data_t(std::declval())); - GIVEN("A 2D setting with multiple projection angles") + GIVEN("a 5x5 Volume and detector of size 1, with 4 angles") { - IndexVector_t volumeDims(2), sinoDims(2); + // Domain setup const index_t volSize = 5; - const index_t detectorSize = 1; - const index_t numImgs = 4; + + IndexVector_t volumeDims(2); volumeDims << volSize, volSize; - sinoDims << detectorSize, numImgs; + VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); + // range setup + const index_t detectorSize = 1; + const index_t numImgs = 4; + + IndexVector_t sinoDims(2); + sinoDims << detectorSize, numImgs; + + // Setup geometry auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; + auto volData = VolumeData2D{Size2D{volumeDims}}; + auto sinoData = SinogramData2D{Size2D{sinoDims}}; std::vector geom; @@ -1113,8 +1334,12 @@ TEMPLATE_TEST_CASE("Scenario: Axis-aligned rays are present", "", JosephsMethodC geom.emplace_back(stc, ctr, Degree{270}, VolumeData2D{Size2D{volumeDims}}, SinogramData2D{Size2D{sinoDims}}); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); - TestType fast(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 0; + + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); THEN("Values are accumulated correctly along each ray's path") { @@ -1151,874 +1376,782 @@ TEMPLATE_TEST_CASE("Scenario: Axis-aligned rays are present", "", JosephsMethodC } } } +} +TEMPLATE_TEST_CASE("JosephsMethodCUDA 3D setup with a single ray", "", JosephsMethodCUDA, + JosephsMethodCUDA) +{ + // Turn logger of + Logger::setLevel(Logger::LogLevel::OFF); + + using data_t = decltype(return_data_t(std::declval())); - GIVEN("A 3D setting with multiple projection angles") + GIVEN("A 3x3x3 Volume") { - IndexVector_t volumeDims(3), sinoDims(3); + // Domain setup const index_t volSize = 3; - const index_t detectorSize = 1; - const index_t numImgs = 6; + + IndexVector_t volumeDims(3); volumeDims << volSize, volSize, volSize; - sinoDims << detectorSize, detectorSize, numImgs; + VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); - auto stc = SourceToCenterOfRotation{20 * volSize}; - auto ctr = CenterOfRotationToDetector{volSize}; + // range setup + const index_t detectorSize = 1; + const index_t numImgs = 1; + + IndexVector_t sinoDims(3); + sinoDims << detectorSize, detectorSize, numImgs; + + // Setup geometry + const auto stc = SourceToCenterOfRotation{20 * volSize}; + const auto ctr = CenterOfRotationToDetector{volSize}; + auto volData = VolumeData3D{Size3D{volumeDims}}; + auto sinoData = SinogramData3D{Size3D{sinoDims}}; std::vector geom; - WHEN("x-, y and z-axis-aligned rays are present") + GIVEN("Single ray along z-axis") { - real_t beta[numImgs] = {0.0, 0.0, 0.0, 0.0, pi_t / 2, 3 * pi_t / 2}; - real_t gamma[numImgs] = {0.0, pi_t, pi_t / 2, 3 * pi_t / 2, pi_t / 2, 3 * pi_t / 2}; - for (index_t i = 0; i < numImgs; i++) - geom.emplace_back(stc, ctr, VolumeData3D{Size3D{volumeDims}}, - SinogramData3D{Size3D{sinoDims}}, - RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}}); + geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), + RotationAngles3D{Gamma{0}}); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 0; - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); - TestType fast(volumeDescriptor, sinoDescriptor, geom); + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); - THEN("Values are accumulated correctly along each ray's path") + WHEN("Sinogram conatainer is not zero initialized and we project through an empty " + "volume") { volume = 0; + sino = 1; - // set only values along the rays' path to one to make sure interpolation is done - // correctly - for (index_t i = 0; i < volSize; i++) { - volume(i, volSize / 2, volSize / 2) = 1; - volume(volSize / 2, i, volSize / 2) = 1; - volume(volSize / 2, volSize / 2, i) = 1; - } - - slow.apply(volume, sino); - for (index_t i = 0; i < numImgs; i++) - REQUIRE(sino[i] == Approx(3.0)); - - fast.apply(volume, sino); - for (index_t i = 0; i < numImgs; i++) - REQUIRE(sino[i] == Approx(3.0)); - - AND_THEN("Both fast and slow backprojections yield the exact adjoint") + THEN("Result is zero") { - Eigen::Matrix cmp(volSize * volSize * volSize); + fast.apply(volume, sino); + DataContainer zero(sinoDescriptor); + zero = 0; + REQUIRE(isApprox(sino, zero)); - cmp << 0, 0, 0, 0, 6, 0, 0, 0, 0, + sino = 1; + slow.apply(volume, sino); + REQUIRE(isApprox(sino, zero)); + } + } - 0, 6, 0, 6, 18, 6, 0, 6, 0, + WHEN("Volume container is not zero initialized and we backproject from an empty " + "sinogram") + { + sino = 0; + volume = 1; - 0, 0, 0, 0, 6, 0, 0, 0, 0; + THEN("Result is zero") + { + fast.applyAdjoint(sino, volume); + DataContainer zero(volumeDescriptor); + zero = 0; + REQUIRE(isApprox(volume, zero)); + volume = 1; slow.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, cmp))); - - fast.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, cmp))); + REQUIRE(isApprox(volume, zero)); } } } - } -} -TEMPLATE_TEST_CASE("Scenario: Projection under an angle", "", JosephsMethodCUDA, - JosephsMethodCUDA) -{ - // Turn logger of - Logger::setLevel(Logger::LogLevel::OFF); - - using data_t = decltype(return_data_t(std::declval())); - real_t sqrt3r = std::sqrt(static_cast(3)); - data_t sqrt3d = std::sqrt(static_cast(3)); - data_t halfd = static_cast(0.5); - data_t thirdd = static_cast(1.0 / 3); - GIVEN("A 2D setting with a single ray") - { - IndexVector_t volumeDims(2), sinoDims(2); - const index_t volSize = 4; - const index_t detectorSize = 1; - const index_t numImgs = 1; - volumeDims << volSize, volSize; - sinoDims << detectorSize, numImgs; - VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); - DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); - - auto stc = SourceToCenterOfRotation{20 * volSize}; - auto ctr = CenterOfRotationToDetector{volSize}; - auto volData = VolumeData2D{Size2D{volumeDims}}; - auto sinoData = SinogramData2D{Size2D{sinoDims}}; + const index_t numCases = 6; + std::array beta = {0.0, 0.0, 0.0, 0.0, pi_t / 2, 3 * pi_t / 2}; + std::array gamma = {0.0, pi_t, pi_t / 2, + 3 * pi_t / 2, pi_t / 2, 3 * pi_t / 2}; + std::array al = {"z", "-z", "x", "-x", "y", "-y"}; - std::vector geom; + Eigen::Matrix backProjections[numCases]; + for (auto& backPr : backProjections) + backPr.resize(volSize * volSize * volSize); - WHEN("Projecting under an angle of 30 degrees and ray goes through center of volume") - { - // In this case the ray enters and exits the volume through the borders along the main - // direction Weighting for all interpolated values should be the same - geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData)); - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); - - real_t weight = 2 / sqrt3r; - THEN("Ray intersects the correct pixels") + // clang-format off + backProjections[0] << 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0; + + backProjections[1] << 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 1, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0; + + backProjections[2] << 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 1, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0; + // clang-format on + + + for (std::size_t i = 0; i < numCases; i++) { + WHEN("A " + al[i] + "-axis-aligned ray passes through the center of a pixel") { - volume = 1; - volume(3, 0) = 0; - volume(1, 1) = 0; - volume(1, 2) = 0; - volume(1, 3) = 0; - - volume(2, 0) = 0; - volume(2, 1) = 0; - volume(2, 2) = 0; - volume(0, 3) = 0; - - fast.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(sino == zero); + geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), + RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}}); - slow.apply(volume, sino); - REQUIRE(sino == zero); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 0; - AND_THEN("The correct weighting is applied") + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); + + THEN("The result of projecting through a voxel is exactly the voxel value") { - volume(3, 0) = 1; - volume(1, 1) = 1; - volume(1, 2) = 1; - volume(1, 3) = 1; - - fast.apply(volume, sino); - REQUIRE(sino[0] == Approx(2 * weight)); - - slow.apply(volume, sino); - REQUIRE(sino[0] == Approx(2 * weight)); + for (index_t j = 0; j < volSize; j++) { + volume = 0; + if (i / 2 == 0) + volume(volSize / 2, volSize / 2, j) = 1; + else if (i / 2 == 1) + volume(j, volSize / 2, volSize / 2) = 1; + else if (i / 2 == 2) + volume(volSize / 2, j, volSize / 2) = 1; - sino[0] = 1; + fast.apply(volume, sino); + // Using doubles significantly increases interpolation accuracy + // For example: when using floats, points very near the border (less than + // ~1/500th of a pixel away from the border) are rounded to actually lie on + // the border. This then yields more accurate results when using floats in + // some of the axis-aligned test cases, despite the lower interpolation + // accuracy. + // => different requirements for floats and doubles, looser requirements + // for doubles + if constexpr (std::is_same_v) + REQUIRE(sino[0] == 1); + else + REQUIRE(sino[0] == Approx(1.0)); - Eigen::Matrix slowExpected(volSize * volSize); - slowExpected << 0, 0, (3 - sqrt3d) / 2, (sqrt3d - 1) / 2, 0, - (sqrt3d - 1) / (2 * sqrt3d), (sqrt3d + 1) / (2 * sqrt3d), 0, 0, - (sqrt3d + 1) / (2 * sqrt3d), (sqrt3d - 1) / (2 * sqrt3d), 0, - (sqrt3d - 1) / 2, (3 - sqrt3d) / 2, 0, 0; + slow.apply(volume, sino); + if constexpr (std::is_same_v) + REQUIRE(sino[0] == 1); + else + REQUIRE(sino[0] == Approx(1.0)); + } - slowExpected *= weight; - slow.applyAdjoint(sino, volume); - REQUIRE( - isApprox(volume, DataContainer(volumeDescriptor, slowExpected))); + AND_THEN("The backprojection sets the values of all hit voxels to the detector " + "value") + { + fast.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, + DataContainer(volumeDescriptor, backProjections[i / 2]))); - fast.applyAdjoint(sino, volume); - for (real_t i = 0.5; i < volSize; i += 1) { - for (real_t j = 0.5; j < volSize; j += 1) { - const real_t angle = std::abs( - std::atan( - (sqrt3r * volSize * 10 - static_cast(volSize / 2.0) + j) - / (volSize * 10 + static_cast(volSize / 2.0) - i)) - - pi_t / 3); - const real_t len = volSize * 21 * std::tan(angle); - if (len < 1) { - REQUIRE(volume((index_t) i, (index_t) j) - == Approx(1 - len).epsilon(0.005)); - } else { - REQUIRE(volume((index_t) i, (index_t) j) == 0); - } - } + slow.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, + DataContainer(volumeDescriptor, backProjections[i / 2]))); } } } } + + + std::array offsetx = {-0.25, -0.25, 0.0, 0.0, 0.0, 0.0}; + std::array offsety = {0.0, 0.0, -0.25, -0.25, 0.0, 0.0}; + std::array offsetz = {0.0, 0.0, 0.0, 0.0, -0.25, -0.25}; - WHEN("Projecting under an angle of 30 degrees and ray enters through the right border") - { - // In this case the ray exits through a border along the main ray direction, but enters - // through a border not along the main direction First pixel should be weighted - // differently - geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData), - PrincipalPointOffset{0}, RotationOffset2D{sqrt3r, 0}); - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); - - THEN("Ray intersects the correct pixels") + // clang-format off + backProjections[0] << 0, 0, 0, 0.25, 0.75, 0, 0, 0, 0, + 0, 0, 0, 0.25, 0.75, 0, 0, 0, 0, + 0, 0, 0, 0.25, 0.75, 0, 0, 0, 0; + + backProjections[1] << 0 , 0 , 0 , 0 , 0 , 0 , 0, 0, 0, + 0.25, 0.25, 0.25, 0.75, 0.75, 0.75, 0, 0, 0, + 0 , 0 , 0 , 0 , 0 , 0 , 0, 0, 0; + + backProjections[2] << 0, 0.25, 0, 0, 0.25, 0, 0, 0.25, 0, + 0, 0.75, 0, 0, 0.75, 0, 0, 0.75, 0, + 0, 0 , 0, 0, 0 , 0, 0, 0 , 0; + // clang-format on + + + for (std::size_t i = 0; i < numCases; i++) { + WHEN("A " + al[i] + "-axis-aligned ray does not pass through the center of a voxel") { - volume = 1; - volume(3, 1) = 0; - volume(3, 2) = 0; - volume(3, 3) = 0; - volume(2, 3) = 0; - volume(2, 2) = 0; - - fast.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(sino == zero); + // x-ray source must be very far from the volume center to make testing of the fast + // backprojection simpler + geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, std::move(volData), + std::move(sinoData), + RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}}, + PrincipalPointOffset2D{0, 0}, + RotationOffset3D{offsetx[i], offsety[i], offsetz[i]}); - slow.apply(volume, sino); - REQUIRE(sino == zero); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 0; - AND_THEN("The correct weighting is applied") + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); + + THEN("The result of projecting through a voxel is the interpolated value between " + "the four voxels nearest to the ray") { - volume(3, 1) = 1; - volume(2, 2) = 1; - volume(2, 3) = 1; + for (index_t j = 0; j < volSize; j++) { + volume = 0; + if (i / 2 == 0) + volume(volSize / 2, volSize / 2, j) = 1; + else if (i / 2 == 1) + volume(j, volSize / 2, volSize / 2) = 1; + else if (i / 2 == 2) + volume(volSize / 2, j, volSize / 2) = 1; - fast.apply(volume, sino); - REQUIRE(sino[0] - == Approx((4 - 2 * sqrt3d) * (sqrt3d - 1) - + (2 / sqrt3d) * (3 - 8 * sqrt3d / 6)) - .epsilon(0.005)); + fast.apply(volume, sino); - slow.apply(volume, sino); - REQUIRE(sino[0] - == Approx((4 - 2 * sqrt3d) * (sqrt3d - 1) - + (2 / sqrt3d) * (3 - 8 * sqrt3d / 6)) - .epsilon(0.005)); + // Using doubles significantly increases interpolation accuracy + // For example: when using floats, points very near the border (less than + // ~1/500th of a pixel away from the border) are rounded to actually lie on + // the border. This then yields more accurate results when using floats in + // some of the axis-aligned test cases, despite the lower interpolation + // accuracy. + // + // => different requirements for floats and doubles, looser requirements + // for doubles - sino[0] = 1; + if constexpr (std::is_same_v) + REQUIRE(sino[0] == 0.75); + else + REQUIRE(sino[0] == Approx(0.75)); - Eigen::Matrix slowExpected(volSize * volSize); - slowExpected << 0, 0, 0, 0, 0, 0, 0, (4 - 2 * sqrt3d) * (sqrt3d - 1), 0, 0, - (2 / sqrt3d) * (1 + halfd - 5 * sqrt3d / 6), - (4 - 2 * sqrt3d) * (2 - sqrt3d) + (2 / sqrt3d) * (5 * sqrt3d / 6 - halfd), - 0, 0, (2 / sqrt3d) * (1 + halfd - sqrt3d / 2), - (2 / sqrt3d) * (sqrt3d / 2 - halfd); + slow.apply(volume, sino); + if constexpr (std::is_same_v) + REQUIRE(sino[0] == 0.75); + else + REQUIRE(sino[0] == Approx(0.75)); + } - slow.applyAdjoint(sino, volume); - REQUIRE( - isApprox(volume, DataContainer(volumeDescriptor, slowExpected))); + AND_THEN("The slow backprojection yields the exact adjoint, the fast " + "backprojection is also exact for a very distant x-ray source") + { + sino[0] = 1; + fast.applyAdjoint(sino, volume); + // Using doubles significantly increases interpolation accuracy + // For example: when using floats, points very near the border (less than + // ~1/500th of a pixel away from the border) are rounded to actually lie on + // the border. This then yields more accurate results when using floats in + // some of the axis-aligned test cases, despite the lower interpolation + // accuracy. + // + // => different requirements for floats and doubles, looser requirements + // for doubles + if constexpr (std::is_same_v) + REQUIRE(isApprox( + volume, DataContainer(volumeDescriptor, backProjections[i / 2]))); + else + REQUIRE(isApprox( + volume, DataContainer(volumeDescriptor, backProjections[i / 2]), + static_cast(0.005))); - fast.applyAdjoint(sino, volume); - for (real_t i = 0.5; i < volSize; i += 1) { - for (real_t j = 0.5; j < volSize; j += 1) { - const real_t angle = std::abs( - std::atan((40 * sqrt3r - 2 + j) / (42 + sqrt3r - i)) - pi_t / 3); - const real_t len = 84 * std::tan(angle); - if (len < 1) { - REQUIRE(volume((index_t) i, (index_t) j) - == Approx(1 - len).epsilon(0.01)); - } else { - REQUIRE(volume((index_t) i, (index_t) j) == 0); - } - } + slow.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, + DataContainer(volumeDescriptor, backProjections[i / 2]))); } } } } + + + offsetx[0] = -volSize / 2.0; + offsetx[1] = volSize / 2.0; + offsetx[2] = 0.0; + offsetx[3] = 0.0; + offsetx[4] = volSize / 2.0; + offsetx[5] = -volSize / 2.0; - WHEN("Projecting under an angle of 30 degrees and ray exits through the left border") - { - // In this case the ray enters through a border along the main ray direction, but exits - // through a border not along the main direction Last pixel should be weighted - // differently - geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData), - PrincipalPointOffset{0}, RotationOffset2D{-sqrt3r, 0}); - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); - - THEN("Ray intersects the correct pixels") - { - volume = 1; - volume(0, 0) = 0; - volume(1, 0) = 0; - volume(0, 1) = 0; - volume(1, 1) = 0; - volume(0, 2) = 0; - - fast.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(sino == zero); - - slow.apply(volume, sino); - REQUIRE(sino == zero); + offsety[0] = 0.0; + offsety[1] = 0.0; + offsety[2] = -volSize / 2.0; + offsety[3] = volSize / 2.0; + offsety[4] = volSize / 2.0; + offsety[5] = -volSize / 2.0; - AND_THEN("The correct weighting is applied") - { - volume(1, 0) = 1; - volume(0, 1) = 1; + // clang-format off + backProjections[0] << 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, 0; - fast.apply(volume, sino); - REQUIRE(sino[0] - == Approx((sqrt3d - 1) + (5.0 / 3.0 - 1 / sqrt3d) - + (4 - 2 * sqrt3d) * (2 - sqrt3d)) - .epsilon(0.005)); + backProjections[1] << 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, 0; - slow.apply(volume, sino); - REQUIRE(sino[0] - == Approx((sqrt3d - 1) + (5.0 / 3.0 - 1 / sqrt3d) - + (4 - 2 * sqrt3d) * (2 - sqrt3d)) - .epsilon(0.005)); + backProjections[2] << 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0; - sino[0] = 1; + backProjections[3] << 0, 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0; - Eigen::Matrix slowExpected(volSize * volSize); - slowExpected << 1 - 1 / sqrt3d, sqrt3d - 1, 0, 0, - (5 * thirdd - 1 / sqrt3d) + (4 - 2 * sqrt3d) * (2 - sqrt3d), - sqrt3d - 5 * thirdd, 0, 0, (sqrt3d - 1) * (4 - 2 * sqrt3d), 0, 0, 0, 0, 0, - 0, 0; + backProjections[4] << 0, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 1; - slow.applyAdjoint(sino, volume); - REQUIRE( - isApprox(volume, DataContainer(volumeDescriptor, slowExpected))); + backProjections[5] << 1, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0; + // clang-format on - fast.applyAdjoint(sino, volume); + al[0] = "left border"; + al[1] = "right border"; + al[2] = "top border"; + al[3] = "bottom border"; + al[4] = "top right edge"; + al[5] = "bottom left edge"; + + for (std::size_t i = 0; i < numCases; i++) { + WHEN("A z-axis-aligned ray runs along the " + al[i] + " of the volume") + { + // x-ray source must be very far from the volume center to make testing of the fast + // backprojection simpler + geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, std::move(volData), + std::move(sinoData), RotationAngles3D{Gamma{0}}, + PrincipalPointOffset2D{0, 0}, + RotationOffset3D{offsetx[i], offsety[i], 0}); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 0; - for (real_t i = 0.5; i < volSize; i += 1) { - for (real_t j = 0.5; j < volSize; j += 1) { - const real_t angle = std::abs( - std::atan((40 * sqrt3r - 2 + j) / (42 - sqrt3r - i)) - pi_t / 3); - const real_t len = 84 * std::tan(angle); - - if (len < 1) { - REQUIRE(volume((index_t) i, (index_t) j) - == Approx(1 - len).margin(0.002)); - } else { - REQUIRE(volume((index_t) i, (index_t) j) == 0); - } + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); + + THEN("The result of projecting through a voxel is exactly the voxel's value (we " + "mirror values at the border for the purpose of interpolation)") + { + for (index_t j = 0; j < volSize; j++) { + volume = 0; + switch (i) { + case 0: + volume(0, volSize / 2, j) = 1; + break; + case 1: + volume(volSize - 1, volSize / 2, j) = 1; + break; + case 2: + volume(volSize / 2, 0, j) = 1; + break; + case 3: + volume(volSize / 2, volSize - 1, j) = 1; + break; + case 4: + volume(volSize - 1, volSize - 1, j) = 1; + break; + case 5: + volume(0, 0, j) = 1; + break; + default: + break; } - } - } - } - } - - WHEN("Projecting under an angle of 30 degrees and ray only intersects a single pixel") - { - // This is a special case that is handled separately in both forward and backprojection - geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData), - PrincipalPointOffset{0}, RotationOffset2D{-2 - sqrt3r / 2, 0}); - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); - - THEN("Ray intersects the correct pixels") - { - volume = 1; - volume(0, 0) = 0; - fast.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(sino == zero); - - slow.apply(volume, sino); - REQUIRE(sino == zero); - - AND_THEN("The correct weighting is applied") - { - volume(0, 0) = 1; - - fast.apply(volume, sino); - REQUIRE(sino[0] == Approx(1 / sqrt3d).epsilon(0.005)); - - slow.apply(volume, sino); - REQUIRE(sino[0] == Approx(1 / sqrt3d).epsilon(0.005)); - - sino[0] = 1; + fast.apply(volume, sino); + REQUIRE(sino[0] == 1); - Eigen::Matrix slowExpected(volSize * volSize); - slowExpected << 1 / sqrt3d, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0; + slow.apply(volume, sino); + REQUIRE(sino[0] == 1); + } - slow.applyAdjoint(sino, volume); - REQUIRE( - isApprox(volume, DataContainer(volumeDescriptor, slowExpected))); + AND_THEN("The slow backprojection yields the exact adjoint, the fast " + "backprojection is also exact for a very distant x-ray source") + { + sino[0] = 1; + fast.applyAdjoint(sino, volume); - fast.applyAdjoint(sino, volume); + // Using doubles significantly increases interpolation accuracy + // For example: when using floats, points very near the border (less than + // ~1/500th of a pixel away from the border) are rounded to actually lie on + // the border. This then yields more accurate results when using floats in + // some of the axis-aligned test cases, despite the lower interpolation + // accuracy. + // + // => different requirements for floats and doubles, looser requirements + // for doubles + if constexpr (std::is_same_v) + REQUIRE(isApprox(volume, DataContainer( + volumeDescriptor, + (backProjections[i] / (i > 3 ? 4 : 2)).eval()))); + else + REQUIRE(isApprox( + volume, + DataContainer(volumeDescriptor, + (backProjections[i] / (i > 3 ? 4 : 2)).eval()), + static_cast(0.005))); - for (real_t i = 0.5; i < volSize; i += 1) { - for (real_t j = 0.5; j < volSize; j += 1) { - const real_t angle = - std::abs(std::atan((40 * sqrt3r - 2 + j) / (40 - sqrt3r / 2 - i)) - - pi_t / 3); - const real_t len = 84 * std::tan(angle); - - if (len < 1) { - REQUIRE(volume((index_t) i, (index_t) j) - == Approx(1 - len).margin(0.002)); - } else { - REQUIRE(volume((index_t) i, (index_t) j) == 0); - } - } + slow.applyAdjoint(sino, volume); + REQUIRE( + isApprox(volume, DataContainer(volumeDescriptor, backProjections[i]))); } } } - } - - WHEN("Projecting under an angle of 120 degrees and ray goes through center of volume") + } + + const data_t sqrt3d = std::sqrt(static_cast(3)); + const data_t thirdd = static_cast(1.0 / 3); + + Eigen::Matrix backProj(volSize * volSize * volSize); + + GIVEN("A 30 degree rotate around z axis") { - // In this case the ray enters and exits the volume through the borders along the main - // direction Weighting for all interpolated values should be the same - geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData), - std::move(sinoData)); - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); - - real_t weight = 2 / sqrt3r; - THEN("Ray intersects the correct pixels") + auto gamma = Gamma{Degree{30}}; + + WHEN("A ray goes through the center of the volume") { - sino[0] = 1; - slow.applyAdjoint(sino, volume); - - volume = 1; - volume(0, 0) = 0; - volume(0, 1) = 0; - volume(1, 1) = 0; - volume(1, 2) = 0; - - volume(2, 1) = 0; - volume(2, 2) = 0; - volume(3, 2) = 0; - volume(3, 3) = 0; - - fast.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(sino == zero); - - slow.apply(volume, sino); - REQUIRE(sino == zero); - - AND_THEN("The correct weighting is applied") + // In this case the ray enters and exits the volume along the main direction + geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), + RotationAngles3D{gamma}); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + TestType op(volumeDescriptor, sinoDescriptor, false); + + THEN("The ray intersects the correct voxels") { - volume(3, 3) = 1; - volume(0, 1) = 1; - volume(1, 1) = 1; - volume(2, 1) = 1; + volume = 1; + volume(1, 1, 1) = 0; + volume(2, 1, 0) = 0; + volume(1, 1, 0) = 0; + volume(0, 1, 2) = 0; + volume(1, 1, 2) = 0; - fast.apply(volume, sino); - REQUIRE(sino[0] == Approx(2 * weight)); - - slow.apply(volume, sino); - REQUIRE(sino[0] == Approx(2 * weight)); - - sino[0] = 1; + op.apply(volume, sino); + REQUIRE(sino[0] == Approx(0).margin(1e-5)); - Eigen::Matrix slowExpected(volSize * volSize); + AND_THEN("The correct weighting is applied") + { + volume(1, 1, 1) = 1; + volume(2, 1, 0) = 3; + volume(1, 1, 2) = 2; - slowExpected << (sqrt3d - 1) / 2, 0, 0, 0, (3 - sqrt3d) / 2, - (sqrt3d + 1) / (2 * sqrt3d), (sqrt3d - 1) / (2 * sqrt3d), 0, 0, - (sqrt3d - 1) / (2 * sqrt3d), (sqrt3d + 1) / (2 * sqrt3d), (3 - sqrt3d) / 2, - 0, 0, 0, (sqrt3d - 1) / 2; + op.apply(volume, sino); + REQUIRE(sino[0] == Approx(6 / sqrt3d + 2.0 / 3).epsilon(0.001)); - slowExpected *= weight; - slow.applyAdjoint(sino, volume); - REQUIRE( - isApprox(volume, DataContainer(volumeDescriptor, slowExpected))); + sino[0] = 1; + // clang-format off + backProj << 0, 0, 0, 0 , 2 / sqrt3d - 2 * thirdd, 2 * thirdd, 0, 0, 0, + 0, 0, 0, 0 , 2 / sqrt3d , 0 , 0, 0, 0, + 0, 0, 0, 2 * thirdd, 2 / sqrt3d - 2 * thirdd, 0 , 0, 0, 0; + // clang-format on - fast.applyAdjoint(sino, volume); - for (real_t i = 0.5; i < volSize; i += 1) { - for (real_t j = 0.5; j < volSize; j += 1) { - const real_t angle = - std::abs(std::atan((sqrt3r * 40 + 2 - i) / (42 - j)) - pi_t / 3); - const real_t len = volSize * 21 * std::tan(angle); - if (len < 1) { - REQUIRE(volume((index_t) i, (index_t) j) - == Approx(1 - len).epsilon(0.005)); - } else { - REQUIRE(volume((index_t) i, (index_t) j) == 0); - } - } + op.applyAdjoint(sino, volume); + REQUIRE( + isApprox(volume, DataContainer(volumeDescriptor, backProj))); } } } - } - WHEN("Projecting under an angle of 120 degrees and ray enters through the top border") - { - // In this case the ray exits through a border along the main ray direction, but enters - // through a border not along the main direction First pixel should be weighted - // differently - geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData), - std::move(sinoData), PrincipalPointOffset{0}, - RotationOffset2D{0, std::sqrt(3.f)}); - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); - - THEN("Ray intersects the correct pixels") + WHEN("A ray enters through the right border") { - volume = 1; - volume(0, 2) = 0; - volume(0, 3) = 0; - volume(1, 2) = 0; - volume(1, 3) = 0; - volume(2, 3) = 0; + // In this case the ray enters through a border orthogonal to a non-main direction + geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), + RotationAngles3D{gamma}, PrincipalPointOffset2D{0, 0}, + RotationOffset3D{1, 0, 0}); - fast.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(sino == zero); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - slow.apply(volume, sino); - REQUIRE(sino == zero); + TestType op(volumeDescriptor, sinoDescriptor, false); - AND_THEN("The correct weighting is applied") + THEN("The ray intersects the correct voxels") { - volume(2, 3) = 1; - volume(1, 2) = 1; - volume(1, 3) = 1; - - fast.apply(volume, sino); - REQUIRE(sino[0] == Approx((4 - 2 * sqrt3d) + (2 / sqrt3d))); - - slow.apply(volume, sino); - REQUIRE(sino[0] == Approx((4 - 2 * sqrt3d) + (2 / sqrt3d))); + volume = 1; + volume(2, 1, 1) = 0; + volume(2, 1, 0) = 0; + volume(2, 1, 2) = 0; + volume(1, 1, 2) = 0; - sino[0] = 1; + op.apply(volume, sino); + REQUIRE(sino[0] == Approx(0).margin(1e-5)); - Eigen::Matrix slowExpected(volSize * volSize); + AND_THEN("The correct weighting is applied") + { + volume(2, 1, 0) = 4; + volume(1, 1, 2) = 3; + volume(2, 1, 1) = 1; - slowExpected << 0, 0, 0, 0, 0, 0, 0, 0, (2 / sqrt3d) * (1 + halfd - sqrt3d / 2), - (2 / sqrt3d) * (1 + halfd - 5 * sqrt3d / 6), 0, 0, - (2 / sqrt3d) * (sqrt3d / 2 - halfd), - (4 - 2 * sqrt3d) * (2 - sqrt3d) + (2 / sqrt3d) * (5 * sqrt3d / 6 - halfd), - (4 - 2 * sqrt3d) * (sqrt3d - 1), 0; + op.apply(volume, sino); + REQUIRE( + sino[0] + == Approx((sqrt3d + 1) * (1 - 1 / sqrt3d) + 3 - sqrt3d / 2 + 2 / sqrt3d) + .epsilon(0.001)); - slow.applyAdjoint(sino, volume); - REQUIRE( - isApprox(volume, DataContainer(volumeDescriptor, slowExpected))); + sino[0] = 1; + // clang-format off + backProj << 0, 0, 0, 0, 0, ((sqrt3d + 1) / 4) * (1 - 1 / sqrt3d), 0, 0, 0, + 0, 0, 0, 0, 0, 2 / sqrt3d + 1 - sqrt3d / 2, 0, 0, 0, 0, 0, 0, 0, + 2 * thirdd, 2 / sqrt3d - 2 * thirdd, 0, 0, 0; + // clang-format on - fast.applyAdjoint(sino, volume); - for (real_t i = 0.5; i < volSize; i += 1) { - for (real_t j = 0.5; j < volSize; j += 1) { - const real_t angle = std::abs( - std::atan((sqrt3r * 40 + 2 - i) / (42 + sqrt3r - j)) - pi_t / 3); - const real_t len = volSize * 21 * std::tan(angle); - if (len < 1) { - REQUIRE(volume((index_t) i, (index_t) j) - == Approx(1 - len).epsilon(0.01)); - } else { - REQUIRE(volume((index_t) i, (index_t) j) == 0); - } - } + op.applyAdjoint(sino, volume); + REQUIRE( + isApprox(volume, DataContainer(volumeDescriptor, backProj))); } } } - } - WHEN("Projecting under an angle of 120 degrees and ray exits through the bottom border") - { - // In this case the ray enters through a border along the main ray direction, but exits - // through a border not along the main direction Last pixel should be weighted - // differently - geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData), - std::move(sinoData), PrincipalPointOffset{0}, - RotationOffset2D{0, -std::sqrt(3.f)}); - - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); - - THEN("Ray intersects the correct pixels") + WHEN("A ray exits through the left border") { - volume = 1; - volume(1, 0) = 0; - volume(2, 0) = 0; - volume(3, 0) = 0; - volume(2, 1) = 0; - volume(3, 1) = 0; + // In this case the ray exit through a border orthogonal to a non-main direction + geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), + RotationAngles3D{gamma}, PrincipalPointOffset2D{0, 0}, + RotationOffset3D{-1, 0, 0}); - fast.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(sino == zero); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - slow.apply(volume, sino); - REQUIRE(sino == zero); + TestType op(volumeDescriptor, sinoDescriptor, false); - AND_THEN("The correct weighting is applied") + THEN("The ray intersects the correct voxels") { - volume(2, 0) = 1; - volume(3, 1) = 1; - - fast.apply(volume, sino); - REQUIRE(sino[0] - == Approx((sqrt3d - 1) + (5.0 / 3.0 - 1 / sqrt3d) - + (4 - 2 * sqrt3d) * (2 - sqrt3d)) - .epsilon(0.005)); - - slow.apply(volume, sino); - REQUIRE(sino[0] - == Approx((sqrt3d - 1) + (5.0 / 3.0 - 1 / sqrt3d) - + (4 - 2 * sqrt3d) * (2 - sqrt3d)) - .epsilon(0.005)); - - sino[0] = 1; + volume = 1; + volume(0, 1, 0) = 0; + volume(1, 1, 0) = 0; + volume(0, 1, 1) = 0; + volume(0, 1, 2) = 0; - Eigen::Matrix slowExpected(volSize * volSize); + op.apply(volume, sino); + REQUIRE(sino[0] == Approx(0).margin(1e-5)); - slowExpected << 0, (sqrt3d - 1) * (4 - 2 * sqrt3d), - (5 * thirdd - 1 / sqrt3d) + (4 - 2 * sqrt3d) * (2 - sqrt3d), 1 - 1 / sqrt3d, - 0, 0, sqrt3d - 5 * thirdd, sqrt3d - 1, 0, 0, 0, 0, 0, 0, 0, 0; + AND_THEN("The correct weighting is applied") + { + volume(0, 1, 2) = 4; + volume(1, 1, 0) = 3; + volume(0, 1, 1) = 1; - slow.applyAdjoint(sino, volume); - REQUIRE( - isApprox(volume, DataContainer(volumeDescriptor, slowExpected))); + op.apply(volume, sino); + REQUIRE( + sino[0] + == Approx((sqrt3d + 1) * (1 - 1 / sqrt3d) + 3 - sqrt3d / 2 + 2 / sqrt3d) + .epsilon(0.001)); - fast.applyAdjoint(sino, volume); + sino[0] = 1; + // clang-format off + backProj << 0, 0, 0, 2 / sqrt3d - 2 * thirdd, 2 * thirdd, 0, 0, 0, 0, 0, 0, + 0, 2 / sqrt3d + 1 - sqrt3d / 2, 0, 0, 0, 0, 0, 0, 0, 0, + ((sqrt3d + 1) / 4) * (1 - 1 / sqrt3d), 0, 0, 0, 0, 0; + // clang-format on - for (real_t i = 0.5; i < volSize; i += 1) { - for (real_t j = 0.5; j < volSize; j += 1) { - const real_t angle = std::abs( - std::atan((sqrt3r * 40 + 2 - i) / (42 - sqrt3r - j)) - pi_t / 3); - const real_t len = 84 * std::tan(angle); - - if (len < 1) { - REQUIRE(volume((index_t) i, (index_t) j) - == Approx(1 - len).margin(0.002)); - } else { - REQUIRE(volume((index_t) i, (index_t) j) == 0); - } - } + op.applyAdjoint(sino, volume); + REQUIRE( + isApprox(volume, DataContainer(volumeDescriptor, backProj))); } } } - } - WHEN("Projecting under an angle of 120 degrees and ray only intersects a single pixel") - { - // This is a special case that is handled separately in both forward and backprojection - geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData), - std::move(sinoData), PrincipalPointOffset{0}, - RotationOffset2D{0, -2 - std::sqrt(3.f) / 2}); - TestType fast(volumeDescriptor, sinoDescriptor, geom); - TestType slow(volumeDescriptor, sinoDescriptor, geom, false); - - THEN("Ray intersects the correct pixels") + WHEN("A ray with an angle of 30 degrees only intersects a single voxel") { - volume = 1; - volume(3, 0) = 0; + // special case - no interior voxels, entry and exit voxels are the same + geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), + RotationAngles3D{gamma}, PrincipalPointOffset2D{0, 0}, + RotationOffset3D{-2, 0, 0}); - fast.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(sino == zero); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - slow.apply(volume, sino); - REQUIRE(sino == zero); + TestType op(volumeDescriptor, sinoDescriptor, false); - AND_THEN("The correct weighting is applied") + THEN("The ray intersects the correct voxels") { - volume(3, 0) = 1; - - fast.apply(volume, sino); - REQUIRE(sino[0] == Approx(1 / sqrt3d).epsilon(0.005)); + volume = 1; + volume(0, 1, 0) = 0; - slow.apply(volume, sino); - REQUIRE(sino[0] == Approx(1 / sqrt3d).epsilon(0.005)); - - sino[0] = 1; + op.apply(volume, sino); + REQUIRE(sino[0] == Approx(0).margin(1e-5)); - Eigen::Matrix slowExpected(volSize * volSize); - slowExpected << 0, 0, 0, 1 / sqrt3d, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0; + AND_THEN("The correct weighting is applied") + { + volume(0, 1, 0) = 1; - slow.applyAdjoint(sino, volume); - REQUIRE( - isApprox(volume, DataContainer(volumeDescriptor, slowExpected))); + op.apply(volume, sino); + REQUIRE(sino[0] == Approx(sqrt3d - 1)); - fast.applyAdjoint(sino, volume); + sino[0] = 1; + // clang-format off + backProj << 0, 0, 0, sqrt3d - 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0; + // clang-format off - for (real_t i = 0.5; i < volSize; i += 1) { - for (real_t j = 0.5; j < volSize; j += 1) { - const real_t angle = - std::abs(std::atan((sqrt3r * 40 + 2 - i) / (40 - sqrt3r / 2 - j)) - - pi_t / 3); - const real_t len = 84 * std::tan(angle); - - if (len < 1) { - REQUIRE(volume((index_t) i, (index_t) j) - == Approx(1 - len).margin(0.002)); - } else { - REQUIRE(volume((index_t) i, (index_t) j) == 0); - } - } + op.applyAdjoint(sino, volume); + REQUIRE( + isApprox(volume, DataContainer(volumeDescriptor, backProj))); } } } } } - GIVEN("A 3D setting with a single ray") + GIVEN("A 5x5x5 Volume") { - IndexVector_t volumeDims(3), sinoDims(3); - const index_t volSize = 3; - const index_t detectorSize = 1; - const index_t numImgs = 1; + // Domain setup + const index_t volSize = 5; + + IndexVector_t volumeDims(3); volumeDims << volSize, volSize, volSize; - sinoDims << detectorSize, detectorSize, numImgs; + VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); - auto stc = SourceToCenterOfRotation{20 * volSize}; - auto ctr = CenterOfRotationToDetector{volSize}; + // range setup + const index_t detectorSize = 1; + const index_t numImgs = 1; + + IndexVector_t sinoDims(3); + sinoDims << detectorSize, detectorSize, numImgs; + + // Setup geometry + const auto stc = SourceToCenterOfRotation{20 * volSize}; + const auto ctr = CenterOfRotationToDetector{volSize}; auto volData = VolumeData3D{Size3D{volumeDims}}; auto sinoData = SinogramData3D{Size3D{sinoDims}}; std::vector geom; - Eigen::Matrix backProj(volSize * volSize * volSize); - - WHEN("A ray with an angle of 30 degrees goes through the center of the volume") + GIVEN("Rays not intersecting the volume") { - // In this case the ray enters and exits the volume along the main direction - geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), - RotationAngles3D{Gamma{pi_t / 6}}); - TestType op(volumeDescriptor, sinoDescriptor, geom, false); - - THEN("The ray intersects the correct voxels") - { - volume = 1; - volume(1, 1, 1) = 0; - volume(2, 1, 0) = 0; - volume(1, 1, 0) = 0; - volume(0, 1, 2) = 0; - volume(1, 1, 2) = 0; - - op.apply(volume, sino); - REQUIRE(sino[0] == Approx(0).margin(1e-5)); - - AND_THEN("The correct weighting is applied") + constexpr index_t numCases = 9; + std::array alpha = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + std::array beta = {0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, pi_t / 2, pi_t / 2, pi_t / 2}; + std::array gamma = {0.0, 0.0, 0.0, pi_t / 2, pi_t / 2, + pi_t / 2, pi_t / 2, pi_t / 2, pi_t / 2}; + std::array offsetx = {-volSize, 0.0, -volSize, 0.0, 0.0, + 0.0, -volSize, 0.0, -volSize}; + std::array offsety = {0.0, -volSize, -volSize, -volSize, 0.0, + -volSize, 0.0, 0.0, 0.0}; + std::array offsetz = {0.0, 0.0, 0.0, 0.0, -volSize, + -volSize, 0.0, -volSize, -volSize}; + std::array neg = {"x", "y", "x and y", "y", "z", + "y and z", "x", "z", "x and z"}; + std::array ali = {"z", "z", "z", "x", "x", "x", "y", "y", "y"}; + + for (std::size_t i = 0; i < numCases; i++) { + WHEN("Tracing along a " + ali[i] + "-axis-aligned ray with negative " + neg[i] + + "-coodinate of origin") { - volume(1, 1, 1) = 1; - volume(2, 1, 0) = 3; - volume(1, 1, 2) = 2; + geom.emplace_back( + stc, ctr, std::move(volData), std::move(sinoData), + RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}, Alpha{alpha[i]}}, + PrincipalPointOffset2D{0, 0}, + RotationOffset3D{-offsetx[i], -offsety[i], -offsetz[i]}); - op.apply(volume, sino); - REQUIRE(sino[0] == Approx(6 / sqrt3d + 2.0 / 3).epsilon(0.001)); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 0; - sino[0] = 1; - backProj << 0, 0, 0, 0, 2 / sqrt3d - 2 * thirdd, 2 * thirdd, 0, 0, 0, + TestType fast(volumeDescriptor, sinoDescriptor); + TestType slow(volumeDescriptor, sinoDescriptor, false); - 0, 0, 0, 0, 2 / sqrt3d, 0, 0, 0, 0, + THEN("Result of forward projection is zero") + { + fast.apply(volume, sino); + DataContainer zero(sinoDescriptor); + zero = 0; + REQUIRE(isApprox(sino, zero)); + + slow.apply(volume, sino); + REQUIRE(isApprox(sino, zero)); - 0, 0, 0, 2 * thirdd, 2 / sqrt3d - 2 * thirdd, 0, 0, 0, 0; + AND_THEN("Result of backprojection is zero") + { + fast.applyAdjoint(sino, volume); + DataContainer zero(volumeDescriptor); + zero = 0; + REQUIRE(isApprox(volume, zero)); - op.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, backProj))); + slow.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, zero)); + } + } } } } + } +} +TEMPLATE_TEST_CASE("JosephsMethodCUDA 3D setup with a multiple rays", "", JosephsMethodCUDA, + JosephsMethodCUDA) +{ + // Turn logger of + Logger::setLevel(Logger::LogLevel::OFF); - WHEN("A ray with an angle of 30 degrees enters through the right border") - { - // getchar(); - // In this case the ray enters through a border orthogonal to a non-main direction - geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), - RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0}, - RotationOffset3D{1, 0, 0}); - TestType op(volumeDescriptor, sinoDescriptor, geom, false); - - THEN("The ray intersects the correct voxels") - { - volume = 1; - volume(2, 1, 1) = 0; - volume(2, 1, 0) = 0; - volume(2, 1, 2) = 0; - volume(1, 1, 2) = 0; + using data_t = decltype(return_data_t(std::declval())); - op.apply(volume, sino); - REQUIRE(sino[0] == Approx(0).margin(1e-5)); + GIVEN("A 3x3x3 volume with 6 projection angles") + { + // Domain setup + const index_t volSize = 3; - AND_THEN("The correct weighting is applied") - { - volume(2, 1, 0) = 4; - volume(1, 1, 2) = 3; - volume(2, 1, 1) = 1; + IndexVector_t volumeDims(3); + volumeDims << volSize, volSize, volSize; - op.apply(volume, sino); - REQUIRE(sino[0] - == Approx((sqrt3d + 1) * (1 - 1 / sqrt3d) + 3 - sqrt3d / 2 + 2 / sqrt3d) - .epsilon(0.001)); + VolumeDescriptor volumeDescriptor(volumeDims); + DataContainer volume(volumeDescriptor); - sino[0] = 1; - backProj << 0, 0, 0, 0, 0, ((sqrt3d + 1) / 4) * (1 - 1 / sqrt3d), 0, 0, 0, + // range setup + const index_t detectorSize = 1; + const index_t numImgs = 6; - 0, 0, 0, 0, 0, 2 / sqrt3d + 1 - sqrt3d / 2, 0, 0, 0, + IndexVector_t sinoDims(3); + sinoDims << detectorSize, detectorSize, numImgs; - 0, 0, 0, 0, 2 * thirdd, 2 / sqrt3d - 2 * thirdd, 0, 0, 0; + // Setup geometry + const auto stc = SourceToCenterOfRotation{20 * volSize}; + const auto ctr = CenterOfRotationToDetector{volSize}; + auto volData = VolumeData3D{Size3D{volumeDims}}; + auto sinoData = SinogramData3D{Size3D{sinoDims}}; - op.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, backProj))); - } - } - } + std::vector geom; - WHEN("A ray with an angle of 30 degrees exits through the left border") + WHEN("x-, y and z-axis-aligned rays are present") { - // In this case the ray exit through a border orthogonal to a non-main direction - geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), - RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0}, - RotationOffset3D{-1, 0, 0}); - TestType op(volumeDescriptor, sinoDescriptor, geom, false); - - THEN("The ray intersects the correct voxels") - { - volume = 1; - volume(0, 1, 0) = 0; - volume(1, 1, 0) = 0; - volume(0, 1, 1) = 0; - volume(0, 1, 2) = 0; - - op.apply(volume, sino); - REQUIRE(sino[0] == Approx(0).margin(1e-5)); - - AND_THEN("The correct weighting is applied") - { - volume(0, 1, 2) = 4; - volume(1, 1, 0) = 3; - volume(0, 1, 1) = 1; + real_t beta[numImgs] = {0.0, 0.0, 0.0, 0.0, pi_t / 2, 3 * pi_t / 2}; + real_t gamma[numImgs] = {0.0, pi_t, pi_t / 2, 3 * pi_t / 2, pi_t / 2, 3 * pi_t / 2}; - op.apply(volume, sino); - REQUIRE(sino[0] - == Approx((sqrt3d + 1) * (1 - 1 / sqrt3d) + 3 - sqrt3d / 2 + 2 / sqrt3d) - .epsilon(0.001)); + for (index_t i = 0; i < numImgs; i++) + geom.emplace_back(stc, ctr, VolumeData3D{Size3D{volumeDims}}, + SinogramData3D{Size3D{sinoDims}}, + RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}}); - sino[0] = 1; - backProj << 0, 0, 0, 2 / sqrt3d - 2 * thirdd, 2 * thirdd, 0, 0, 0, 0, + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - 0, 0, 0, 2 / sqrt3d + 1 - sqrt3d / 2, 0, 0, 0, 0, 0, + TestType slow(volumeDescriptor, sinoDescriptor, false); + TestType fast(volumeDescriptor, sinoDescriptor); - 0, 0, 0, ((sqrt3d + 1) / 4) * (1 - 1 / sqrt3d), 0, 0, 0, 0, 0; + THEN("Values are accumulated correctly along each ray's path") + { + volume = 0; - op.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, backProj))); + // set only values along the rays' path to one to make sure interpolation is done + // correctly + for (index_t i = 0; i < volSize; i++) { + volume(i, volSize / 2, volSize / 2) = 1; + volume(volSize / 2, i, volSize / 2) = 1; + volume(volSize / 2, volSize / 2, i) = 1; } - } - } - - WHEN("A ray with an angle of 30 degrees only intersects a single voxel") - { - // special case - no interior voxels, entry and exit voxels are the same - geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), - RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0}, - RotationOffset3D{-2, 0, 0}); - TestType op(volumeDescriptor, sinoDescriptor, geom, false); - THEN("The ray intersects the correct voxels") - { - volume = 1; - volume(0, 1, 0) = 0; + slow.apply(volume, sino); + for (index_t i = 0; i < numImgs; i++) + REQUIRE(sino[i] == Approx(3.0)); - op.apply(volume, sino); - REQUIRE(sino[0] == Approx(0).margin(1e-5)); + fast.apply(volume, sino); + for (index_t i = 0; i < numImgs; i++) + REQUIRE(sino[i] == Approx(3.0)); - AND_THEN("The correct weighting is applied") + AND_THEN("Both fast and slow backprojections yield the exact adjoint") { - volume(0, 1, 0) = 1; - - op.apply(volume, sino); - REQUIRE(sino[0] == Approx(sqrt3d - 1)); - - sino[0] = 1; - backProj << 0, 0, 0, sqrt3d - 1, 0, 0, 0, 0, 0, + Eigen::Matrix cmp(volSize * volSize * volSize); - 0, 0, 0, 0, 0, 0, 0, 0, 0, + // clang-format off + cmp << 0, 0, 0, 0, 6, 0, 0, 0, 0, + 0, 6, 0, 6, 18, 6, 0, 6, 0, + 0, 0, 0, 0, 6, 0, 0, 0, 0; + // clang-format on - 0, 0, 0, 0, 0, 0, 0, 0, 0; + slow.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, cmp))); - op.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, backProj))); + fast.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, cmp))); } } } diff --git a/elsa/projectors_cuda/tests/test_SiddonsMethodCUDA.cpp b/elsa/projectors_cuda/tests/test_SiddonsMethodCUDA.cpp index 4e5ad5cba18c2f14fb0b152537d1a4536c14cd5e..2208750c2a1622f3580605ce9c0ffcfece580f8a 100644 --- a/elsa/projectors_cuda/tests/test_SiddonsMethodCUDA.cpp +++ b/elsa/projectors_cuda/tests/test_SiddonsMethodCUDA.cpp @@ -6,6 +6,8 @@ #include "testHelpers.h" #include "Logger.h" #include "VolumeDescriptor.h" +#include "PlanarDetectorDescriptor.h" +#include "testHelpers.h" #include @@ -37,11 +39,8 @@ TEMPLATE_TEST_CASE("Scenario: Calls to functions of super class", "", SiddonsMet volumeDims << volSize, volSize; sinoDims << detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); volume = 1; - DataContainer sino(sinoDescriptor); - sino = 0; auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; @@ -52,7 +51,12 @@ TEMPLATE_TEST_CASE("Scenario: Calls to functions of super class", "", SiddonsMet geom.emplace_back(stc, ctr, Radian{angle}, VolumeData2D{Size2D{volumeDims}}, SinogramData2D{Size2D{sinoDims}}); } - TestType op(volumeDescriptor, sinoDescriptor, geom); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + sino = 0; + + TestType op(volumeDescriptor, sinoDescriptor); WHEN("Projector is cloned") { @@ -75,189 +79,71 @@ TEMPLATE_TEST_CASE("Scenario: Calls to functions of super class", "", SiddonsMet } } -TEMPLATE_TEST_CASE("Scenario: Output DataContainer is not zero initialized", "", - SiddonsMethodCUDA, SiddonsMethodCUDA, SiddonsMethod, - SiddonsMethod) +TEMPLATE_TEST_CASE("SiddonsMethodCUDA 2D setup with a single ray", "", SiddonsMethodCUDA, + SiddonsMethodCUDA, SiddonsMethod, SiddonsMethod) { // Turn logger of Logger::setLevel(Logger::LogLevel::OFF); using data_t = decltype(return_data_t(std::declval())); - GIVEN("A 2D setting") + + GIVEN("A 5x5 volume and detector of size 1, with 1 angle") { - IndexVector_t volumeDims(2), sinoDims(2); + // Domain setup const index_t volSize = 5; - const index_t detectorSize = 1; - const index_t numImgs = 1; - volumeDims << volSize, volSize; - sinoDims << detectorSize, numImgs; - VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); - DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); - - auto stc = SourceToCenterOfRotation{20 * volSize}; - auto ctr = CenterOfRotationToDetector{volSize}; - auto volData = VolumeData2D{Size2D{volumeDims}}; - auto sinoData = SinogramData2D{Size2D{sinoDims}}; - - std::vector geom; - geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData)); - TestType op(volumeDescriptor, sinoDescriptor, geom); - - WHEN("Sinogram conatainer is not zero initialized and we project through an empty volume") - { - volume = 0; - sino = 1; - - THEN("Result is zero") - { - op.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(isApprox(sino, zero, epsilon)); - } - } - - WHEN("Volume container is not zero initialized and we backproject from an empty sinogram") - { - sino = 0; - volume = 1; - THEN("Result is zero") - { - op.applyAdjoint(sino, volume); - DataContainer zero(volumeDescriptor); - zero = 0; - REQUIRE(isApprox(volume, zero, epsilon)); - } - } - } + IndexVector_t volumeDims(2); + volumeDims << volSize, volSize; - GIVEN("A 3D setting") - { - IndexVector_t volumeDims(3), sinoDims(3); - const index_t volSize = 3; - const index_t detectorSize = 1; - const index_t numImgs = 1; - volumeDims << volSize, volSize, volSize; - sinoDims << detectorSize, detectorSize, numImgs; VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); - - auto stc = SourceToCenterOfRotation{20 * volSize}; - auto ctr = CenterOfRotationToDetector{volSize}; - auto volData = VolumeData3D{Size3D{volumeDims}}; - auto sinoData = SinogramData3D{Size3D{sinoDims}}; - - std::vector geom; - geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), - RotationAngles3D{Gamma{0}}); - - TestType op(volumeDescriptor, sinoDescriptor, geom); - - WHEN("Sinogram conatainer is not zero initialized and we project through an empty volume") - { - volume = 0; - sino = 1; - - THEN("Result is zero") - { - op.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(isApprox(sino, zero, epsilon)); - } - } - - WHEN("Volume container is not zero initialized and we backproject from an empty sinogram") - { - sino = 0; - volume = 1; - - THEN("Result is zero") - { - op.applyAdjoint(sino, volume); - DataContainer zero(volumeDescriptor); - zero = 0; - REQUIRE(isApprox(volume, zero, epsilon)); - } - } - } -} - -TEMPLATE_TEST_CASE("Scenario: Rays not intersecting the bounding box are present", "", - SiddonsMethodCUDA, SiddonsMethodCUDA, SiddonsMethod, - SiddonsMethod) -{ - // Turn logger of - Logger::setLevel(Logger::LogLevel::OFF); - using data_t = decltype(return_data_t(std::declval())); - GIVEN("A 2D setting") - { - IndexVector_t volumeDims(2), sinoDims(2); - const index_t volSize = 5; + // range setup const index_t detectorSize = 1; const index_t numImgs = 1; - volumeDims << volSize, volSize; + + IndexVector_t sinoDims(2); sinoDims << detectorSize, numImgs; - VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); - DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); + // Setup geometry auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; auto volData = VolumeData2D{Size2D{volumeDims}}; auto sinoData = SinogramData2D{Size2D{sinoDims}}; - volume = 1; - sino = 1; std::vector geom; - WHEN("Tracing along a y-axis-aligned ray with a negative x-coordinate of origin") + GIVEN("A basic geometry setup") { - geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData), - PrincipalPointOffset{}, RotationOffset2D{volSize, 0}); + geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData)); - TestType op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - THEN("Result of forward projection is zero") + TestType op(volumeDescriptor, sinoDescriptor); + + WHEN("Sinogram conatainer is not zero initialized and we project through an empty " + "volume") { - op.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(isApprox(sino, zero, epsilon)); + volume = 0; + sino = 1; - AND_THEN("Result of backprojection is zero") + THEN("Result is zero") { - op.applyAdjoint(sino, volume); - DataContainer zero(volumeDescriptor); + op.apply(volume, sino); + DataContainer zero(sinoDescriptor); zero = 0; - REQUIRE(isApprox(volume, zero, epsilon)); + REQUIRE(isApprox(sino, zero, epsilon)); } } - } - - WHEN("Tracing along a y-axis-aligned ray with a x-coordinate of origin beyond the bounding " - "box") - { - geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData), - PrincipalPointOffset{}, RotationOffset2D{-volSize, 0}); - TestType op(volumeDescriptor, sinoDescriptor, geom); - - THEN("Result of forward projection is zero") + WHEN("Volume container is not zero initialized and we backproject from an empty " + "sinogram") { - op.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(isApprox(sino, zero, epsilon)); + sino = 0; + volume = 1; - AND_THEN("Result of backprojection is zero") + THEN("Result is zero") { op.applyAdjoint(sino, volume); DataContainer zero(volumeDescriptor); @@ -267,167 +153,164 @@ TEMPLATE_TEST_CASE("Scenario: Rays not intersecting the bounding box are present } } - WHEN("Tracing along a x-axis-aligned ray with a negative y-coordinate of origin") + GIVEN("Scenario: Rays not intersecting the bounding box are present") { - geom.emplace_back(stc, ctr, Radian{pi_t / 2}, std::move(volData), std::move(sinoData), - PrincipalPointOffset{}, RotationOffset2D{0, volSize}); + WHEN("Tracing along a y-axis-aligned ray with a negative x-coordinate of origin") + { + geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData), + PrincipalPointOffset{}, RotationOffset2D{volSize, 0}); - TestType op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - THEN("Result of forward projection is zero") - { - op.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - CHECK(isApprox(sino, zero, epsilon)); + TestType op(volumeDescriptor, sinoDescriptor); - AND_THEN("Result of backprojection is zero") + THEN("Result of forward projection is zero") { - op.applyAdjoint(sino, volume); - DataContainer zero(volumeDescriptor); + DataContainer zero(sinoDescriptor); zero = 0; - CHECK(isApprox(volume, zero, epsilon)); + + op.apply(volume, sino); + REQUIRE(isApprox(sino, zero)); + + AND_THEN("Result of backprojection is zero") + { + DataContainer zero(volumeDescriptor); + zero = 0; + + op.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, zero)); + } } } - } - WHEN("Tracing along a x-axis-aligned ray with a y-coordinate of origin beyond the bounding " - "box") - { - geom.emplace_back(stc, ctr, Radian{pi_t / 2}, std::move(volData), std::move(sinoData), - PrincipalPointOffset{}, RotationOffset2D{0, -volSize}); + WHEN("Tracing along a y-axis-aligned ray with a x-coordinate of origin beyond the " + "bounding box") + { + geom.emplace_back(stc, ctr, Radian{0}, std::move(volData), std::move(sinoData), + PrincipalPointOffset{}, RotationOffset2D{-volSize, 0}); - TestType op(volumeDescriptor, sinoDescriptor, geom); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - THEN("Result of forward projection is zero") - { - op.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(isApprox(sino, zero, epsilon)); + TestType op(volumeDescriptor, sinoDescriptor); - AND_THEN("Result of backprojection is zero") + THEN("Result of forward projection is zero") { - op.applyAdjoint(sino, volume); - DataContainer zero(volumeDescriptor); + DataContainer zero(sinoDescriptor); zero = 0; - REQUIRE(isApprox(volume, zero, epsilon)); - } - } - } - } - GIVEN("A 3D setting") - { - IndexVector_t volumeDims(3), sinoDims(3); - const index_t volSize = 5; - const index_t detectorSize = 1; - const index_t numImgs = 1; - volumeDims << volSize, volSize, volSize; - sinoDims << detectorSize, detectorSize, numImgs; - VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); - DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); - volume = 1; - sino = 1; + op.apply(volume, sino); + REQUIRE(isApprox(sino, zero)); - auto stc = SourceToCenterOfRotation{20 * volSize}; - auto ctr = CenterOfRotationToDetector{volSize}; - auto volData = VolumeData3D{Size3D{volumeDims}}; - auto sinoData = SinogramData3D{Size3D{sinoDims}}; + AND_THEN("Result of backprojection is zero") + { + DataContainer zero(volumeDescriptor); + zero = 0; - std::vector geom; + op.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, zero)); + } + } + } - const index_t numCases = 9; - std::array alpha = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; - std::array beta = {0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, pi_t / 2, pi_t / 2, pi_t / 2}; - std::array gamma = {0.0, 0.0, 0.0, pi_t / 2, pi_t / 2, - pi_t / 2, pi_t / 2, pi_t / 2, pi_t / 2}; - std::array offsetx = {volSize, 0.0, volSize, 0.0, 0.0, - 0.0, volSize, 0.0, volSize}; - std::array offsety = {0.0, volSize, volSize, volSize, 0.0, - volSize, 0.0, 0.0, 0.0}; - std::array offsetz = {0.0, 0.0, 0.0, 0.0, volSize, - volSize, 0.0, volSize, volSize}; - std::array neg = {"x", "y", "x and y", "y", "z", - "y and z", "x", "z", "x and z"}; - std::array ali = {"z", "z", "z", "x", "x", "x", "y", "y", "y"}; - - for (std::size_t i = 0; i < numCases; i++) { - WHEN("Tracing along a " + ali[i] + "-axis-aligned ray with negative " + neg[i] - + "-coodinate of origin") + WHEN("Tracing along a x-axis-aligned ray with a negative y-coordinate of origin") { - geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), - RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}, Alpha{alpha[i]}}, - PrincipalPointOffset2D{0, 0}, - RotationOffset3D{-offsetx[i], -offsety[i], -offsetz[i]}); + geom.emplace_back(stc, ctr, Radian{pi_t / 2}, std::move(volData), + std::move(sinoData), PrincipalPointOffset{}, + RotationOffset2D{0, volSize}); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - TestType op(volumeDescriptor, sinoDescriptor, geom); + TestType op(volumeDescriptor, sinoDescriptor); THEN("Result of forward projection is zero") { - op.apply(volume, sino); DataContainer zero(sinoDescriptor); zero = 0; - REQUIRE(isApprox(sino, zero, epsilon)); + + op.apply(volume, sino); + REQUIRE(isApprox(sino, zero)); AND_THEN("Result of backprojection is zero") { - op.applyAdjoint(sino, volume); DataContainer zero(volumeDescriptor); zero = 0; - REQUIRE(isApprox(volume, zero, epsilon)); + + op.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, zero)); } } } - } - } -} -TEMPLATE_TEST_CASE("Scenario: Axis-aligned rays are present", "", SiddonsMethodCUDA, - SiddonsMethodCUDA, SiddonsMethod, SiddonsMethod) -{ - // Turn logger of - Logger::setLevel(Logger::LogLevel::OFF); + WHEN("Tracing along a x-axis-aligned ray with a y-coordinate of origin beyond the " + "bounding box") + { + geom.emplace_back(stc, ctr, Radian{pi_t / 2}, std::move(volData), + std::move(sinoData), PrincipalPointOffset{}, + RotationOffset2D{0, -volSize}); - using data_t = decltype(return_data_t(std::declval())); - GIVEN("A 2D setting with a single ray") - { - IndexVector_t volumeDims(2), sinoDims(2); - const index_t volSize = 5; - const index_t detectorSize = 1; - const index_t numImgs = 1; - volumeDims << volSize, volSize; - sinoDims << detectorSize, numImgs; - VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); + TestType op(volumeDescriptor, sinoDescriptor); - auto stc = SourceToCenterOfRotation{20 * volSize}; - auto ctr = CenterOfRotationToDetector{volSize}; - auto volData = VolumeData2D{Size2D{volumeDims}}; - auto sinoData = SinogramData2D{Size2D{sinoDims}}; + THEN("Result of forward projection is zero") + { + op.apply(volume, sino); + DataContainer zero(sinoDescriptor); + zero = 0; - std::vector geom; + REQUIRE(isApprox(sino, zero)); - const index_t numCases = 4; - const std::array angles = {0.0, pi_t / 2, pi_t, 3 * pi_t / 2}; - Eigen::Matrix backProj[2]; - backProj[1] << 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0; + AND_THEN("Result of backprojection is zero") + { + op.applyAdjoint(sino, volume); + + DataContainer zero(volumeDescriptor); + zero = 0; - backProj[0] << 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0; + REQUIRE(isApprox(volume, zero)); + } + } + } + } - for (std::size_t i = 0; i < numCases; i++) { + // Expected results + Eigen::Matrix backProjections[2]; + backProjections[0].resize(volSize * volSize); + backProjections[1].resize(volSize * volSize); + + constexpr index_t numCases = 4; + const std::array angles = {0., 90., 180., 270.}; + + // clang-format off + backProjections[0] << 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0; + + backProjections[1] << 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0; + // clang-format on + + for (index_t i = 0; i < numCases; i++) { WHEN("An axis-aligned ray with an angle of " + std::to_string(angles[i]) + " radians passes through the center of a pixel") { - geom.emplace_back(stc, ctr, Radian{angles[i]}, std::move(volData), + geom.emplace_back(stc, ctr, Degree{angles[i]}, std::move(volData), std::move(sinoData)); - TestType op(volumeDescriptor, sinoDescriptor, geom); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + TestType op(volumeDescriptor, sinoDescriptor); + THEN("The result of projecting through a pixel is exactly the pixel value") { for (index_t j = 0; j < volSize; j++) { @@ -446,18 +329,25 @@ TEMPLATE_TEST_CASE("Scenario: Axis-aligned rays are present", "", SiddonsMethodC { op.applyAdjoint(sino, volume); REQUIRE(isApprox(volume, - DataContainer(volumeDescriptor, backProj[i % 2]))); + DataContainer(volumeDescriptor, backProjections[i % + 2]))); } } } } - + + WHEN("A y-axis-aligned ray runs along the left voxel boundary") { geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, Radian{0}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{-0.5, 0}); - TestType op(volumeDescriptor, sinoDescriptor, geom); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + TestType op(volumeDescriptor, sinoDescriptor); + THEN("The result of projecting through a pixel is the value of the pixel with the " "higher index") { @@ -473,7 +363,8 @@ TEMPLATE_TEST_CASE("Scenario: Axis-aligned rays are present", "", SiddonsMethodC { sino[0] = 1; op.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, backProj[0]))); + REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, + backProjections[0]))); } } } @@ -485,7 +376,11 @@ TEMPLATE_TEST_CASE("Scenario: Axis-aligned rays are present", "", SiddonsMethodC geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, Radian{0}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{volSize * 0.5, 0}); - TestType op(volumeDescriptor, sinoDescriptor, geom); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + TestType op(volumeDescriptor, sinoDescriptor); THEN("The result of projecting is zero") { @@ -500,19 +395,31 @@ TEMPLATE_TEST_CASE("Scenario: Axis-aligned rays are present", "", SiddonsMethodC op.applyAdjoint(sino, volume); DataContainer zero(volumeDescriptor); zero = 0; - REQUIRE(volume == zero); + REQUIRE(isApprox(volume, zero)); } } } - - backProj[0] << 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0; + + + // clang-format off + backProjections[0] << 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0; + // clang-format on WHEN("A y-axis-aligned ray runs along the left volume boundary") { geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, Radian{0}, std::move(volData), std::move(sinoData), PrincipalPointOffset{0}, RotationOffset2D{-volSize * 0.5, 0}); - TestType op(volumeDescriptor, sinoDescriptor, geom); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + TestType op(volumeDescriptor, sinoDescriptor); + THEN("The result of projecting through a pixel is exactly the pixel's value") { for (index_t j = 0; j < volSize; j++) { @@ -527,322 +434,558 @@ TEMPLATE_TEST_CASE("Scenario: Axis-aligned rays are present", "", SiddonsMethodC { sino[0] = 1; op.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, backProj[0]))); + REQUIRE(isApprox(volume, + DataContainer(volumeDescriptor, backProjections[0]))); } } } } - - GIVEN("A 3D setting with a single ray") + GIVEN("a 4x4 Volume") { - IndexVector_t volumeDims(3), sinoDims(3); - const index_t volSize = 3; - const index_t detectorSize = 1; - const index_t numImgs = 1; - volumeDims << volSize, volSize, volSize; - sinoDims << detectorSize, detectorSize, numImgs; + // Domain setup + const index_t volSize = 4; + + IndexVector_t volumeDims(2); + volumeDims << volSize, volSize; + VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); + // range setup + const index_t detectorSize = 1; + const index_t numImgs = 1; + + IndexVector_t sinoDims(2); + sinoDims << detectorSize, numImgs; + + // Setup geometry auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; - auto volData = VolumeData3D{Size3D{volumeDims}}; - auto sinoData = SinogramData3D{Size3D{sinoDims}}; + auto volData = VolumeData2D{Size2D{volumeDims}}; + auto sinoData = SinogramData2D{Size2D{sinoDims}}; std::vector geom; - const index_t numCases = 6; - std::array beta = {0.0, 0.0, 0.0, 0.0, pi_t / 2, 3 * pi_t / 2}; - std::array gamma = {0.0, pi_t, pi_t / 2, - 3 * pi_t / 2, pi_t / 2, 3 * pi_t / 2}; - std::array al = {"z", "-z", "x", "-x", "y", "-y"}; - - Eigen::Matrix backProj[numCases]; + real_t sqrt3r = std::sqrt(static_cast(3)); + data_t sqrt3d = std::sqrt(static_cast(3)); - backProj[2] << 0, 0, 0, 0, 0, 0, 0, 0, 0, + GIVEN("An angle of -30 degrees") + { + auto angle = Degree{-30}; + WHEN("A ray goes through center of volume") + { + // In this case the ray enters and exits the volume through the borders along the + // main direction + geom.emplace_back(stc, ctr, angle, std::move(volData), std::move(sinoData)); - 0, 1, 0, 0, 1, 0, 0, 1, 0, + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - 0, 0, 0, 0, 0, 0, 0, 0, 0; + TestType op(volumeDescriptor, sinoDescriptor); - backProj[1] << 0, 0, 0, 0, 0, 0, 0, 0, 0, + THEN("Ray intersects the correct pixels") + { + volume = 1; + volume(3, 0) = 0; + volume(2, 0) = 0; + volume(2, 1) = 0; - 0, 0, 0, 1, 1, 1, 0, 0, 0, + volume(1, 2) = 0; + volume(1, 3) = 0; + volume(0, 3) = 0; + volume(2, 2) = 0; - 0, 0, 0, 0, 0, 0, 0, 0, 0; + DataContainer zero(sinoDescriptor); + zero = 0; - backProj[0] << 0, 0, 0, 0, 1, 0, 0, 0, 0, + op.apply(volume, sino); + REQUIRE(zero[0] == Approx(0).epsilon(epsilon)); - 0, 0, 0, 0, 1, 0, 0, 0, 0, + AND_THEN("The correct weighting is applied") + { + volume(3, 0) = 1; + volume(2, 0) = 2; + volume(2, 1) = 3; - 0, 0, 0, 0, 1, 0, 0, 0, 0; + op.apply(volume, sino); + REQUIRE(sino[0] == Approx(2 * sqrt3d + 2)); - for (std::size_t i = 0; i < numCases; i++) { - WHEN("A " + al[i] + "-axis-aligned ray passes through the center of a pixel") - { - geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), - RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}}); - TestType op(volumeDescriptor, sinoDescriptor, geom); - THEN("The result of projecting through a voxel is exactly the voxel value") - { - for (index_t j = 0; j < volSize; j++) { + // on the other side of the center volume = 0; - if (i / 2 == 0) - volume(volSize / 2, volSize / 2, j) = 1; - else if (i / 2 == 1) - volume(j, volSize / 2, volSize / 2) = 1; - else if (i / 2 == 2) - volume(volSize / 2, j, volSize / 2) = 1; + volume(1, 2) = 3; + volume(1, 3) = 2; + volume(0, 3) = 1; op.apply(volume, sino); - REQUIRE(sino[0] == 1); - } + REQUIRE(sino[0] == Approx(2 * sqrt3d + 2)); + + sino[0] = 1; + + Eigen::Matrix expected(volSize * volSize); + + // clang-format off + expected << 0, 0, 2 - 2 / sqrt3d, 4 / sqrt3d - 2, + 0, 0, 2 / sqrt3d, 0, + 0, 2 / sqrt3d, 0, 0, + 4 / sqrt3d - 2, 2 - 2 / sqrt3d, 0, 0; + // clang-format on - AND_THEN("The backprojection sets the values of all hit voxels to the detector " - "value") - { op.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, - DataContainer(volumeDescriptor, backProj[i / 2]))); + REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, expected), + epsilon)); } } } - } - std::array offsetx; - std::array offsety; - - offsetx[0] = volSize / 2.0; - offsetx[3] = -(volSize / 2.0); - offsetx[1] = 0.0; - offsetx[4] = 0.0; - offsetx[5] = -(volSize / 2.0); - offsetx[2] = (volSize / 2.0); + WHEN("A ray enters through the right border") + { + // In this case the ray exits through a border along the main ray direction, but + // enters through a border not along the main direction First pixel should be + // weighted differently + geom.emplace_back(stc, ctr, angle, std::move(volData), std::move(sinoData), + PrincipalPointOffset{0}, RotationOffset2D{sqrt3r, 0}); - offsety[0] = 0.0; - offsety[3] = 0.0; - offsety[1] = volSize / 2.0; - offsety[4] = -(volSize / 2.0); - offsety[5] = -(volSize / 2.0); - offsety[2] = (volSize / 2.0); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - backProj[0] << 0, 0, 0, 1, 0, 0, 0, 0, 0, + TestType op(volumeDescriptor, sinoDescriptor); - 0, 0, 0, 1, 0, 0, 0, 0, 0, + THEN("Ray intersects the correct pixels") + { + volume = 1; + volume(3, 1) = 0; + volume(3, 2) = 0; + volume(3, 3) = 0; + volume(2, 3) = 0; - 0, 0, 0, 1, 0, 0, 0, 0, 0; - - backProj[1] << 0, 1, 0, 0, 0, 0, 0, 0, 0, + op.apply(volume, sino); + DataContainer zero(sinoDescriptor); + zero = 0; + REQUIRE(isApprox(sino, zero, epsilon)); - 0, 1, 0, 0, 0, 0, 0, 0, 0, + AND_THEN("The correct weighting is applied") + { + volume(3, 1) = 4; + volume(3, 2) = 3; + volume(3, 3) = 2; + volume(2, 3) = 1; - 0, 1, 0, 0, 0, 0, 0, 0, 0; + op.apply(volume, sino); + REQUIRE(sino[0] == Approx(14 - 4 * sqrt3d)); - backProj[2] << 1, 0, 0, 0, 0, 0, 0, 0, 0, + sino[0] = 1; - 1, 0, 0, 0, 0, 0, 0, 0, 0, + Eigen::Matrix expected(volSize * volSize); - 1, 0, 0, 0, 0, 0, 0, 0, 0; + // clang-format off + expected << 0, 0, 0, 0, + 0, 0, 0, 4 - 2 * sqrt3d, + 0, 0, 0, 2 / sqrt3d, + 0, 0, 2 - 2 / sqrt3d, 4 / sqrt3d - 2; + // clang-format on - al[0] = "left border"; - al[1] = "bottom border"; - al[2] = "bottom left border"; - al[3] = "right border"; - al[4] = "top border"; - al[5] = "top right edge"; + op.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, expected), + epsilon)); + } + } + } - for (std::size_t i = 0; i < numCases / 2; i++) { - WHEN("A z-axis-aligned ray runs along the " + al[i] + " of the volume") + WHEN("A ray exits through the left border") { - // x-ray source must be very far from the volume center to make testing of the op - // backprojection simpler - geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, std::move(volData), - std::move(sinoData), RotationAngles3D{Gamma{0}}, - PrincipalPointOffset2D{0, 0}, - RotationOffset3D{-offsetx[i], -offsety[i], 0}); - TestType op(volumeDescriptor, sinoDescriptor, geom); - THEN("The result of projecting through a voxel is exactly the voxel's value") + // In this case the ray enters through a border along the main ray direction, but + // exits through a border not along the main direction + geom.emplace_back(stc, ctr, angle, std::move(volData), std::move(sinoData), + PrincipalPointOffset{0}, RotationOffset2D{-sqrt3r, 0}); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + TestType op(volumeDescriptor, sinoDescriptor); + + THEN("Ray intersects the correct pixels") { - for (index_t j = 0; j < volSize; j++) { - volume = 0; - switch (i) { - case 0: - volume(0, volSize / 2, j) = 1; - break; - case 1: - volume(volSize / 2, 0, j) = 1; - break; - case 2: - volume(0, 0, j) = 1; - break; - default: - break; - } + volume = 1; + volume(0, 0) = 0; + volume(1, 0) = 0; + volume(0, 1) = 0; + volume(0, 2) = 0; + + op.apply(volume, sino); + DataContainer zero(sinoDescriptor); + zero = 0; + REQUIRE(isApprox(sino, zero, epsilon)); + + AND_THEN("The correct weighting is applied") + { + volume(1, 0) = 1; + volume(0, 0) = 2; + volume(0, 1) = 3; + volume(0, 2) = 4; op.apply(volume, sino); - REQUIRE(sino[0] == 1); + REQUIRE(sino[0] == Approx(14 - 4 * sqrt3d)); + + sino[0] = 1; + + Eigen::Matrix expected(volSize * volSize); + + // clang-format off + expected << 4 / sqrt3d - 2, 2 - 2 / sqrt3d, 0, 0, + 2 / sqrt3d, 0, 0, 0, + 4 - 2 * sqrt3d, 0, 0, 0, + 0, 0, 0, 0; + // clang-format on + + op.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, expected), + epsilon)); } + } + } + + WHEN("A ray only intersects a single pixel") + { + geom.emplace_back(stc, ctr, angle, std::move(volData), std::move(sinoData), + PrincipalPointOffset{0}, RotationOffset2D{-2 - sqrt3r / 2, 0}); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + TestType op(volumeDescriptor, sinoDescriptor); + + THEN("Ray intersects the correct pixels") + { + volume = 1; + volume(0, 0) = 0; + + op.apply(volume, sino); + DataContainer zero(sinoDescriptor); + zero = 0; + REQUIRE(isApprox(sino, zero, epsilon)); - AND_THEN("The backprojection yields the exact adjoints") + AND_THEN("The correct weighting is applied") { + volume(0, 0) = 1; + + op.apply(volume, sino); + REQUIRE(sino[0] == Approx(1 / sqrt3d)); + sino[0] = 1; - op.applyAdjoint(sino, volume); - REQUIRE( - isApprox(volume, DataContainer(volumeDescriptor, backProj[i]))); + Eigen::Matrix expected(volSize * volSize); + // clang-format off + expected << 1 / sqrt3d, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0; + // clang-format on + + op.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, expected), + epsilon)); } } } } - for (std::size_t i = numCases / 2; i < numCases; i++) { - WHEN("A z-axis-aligned ray runs along the " + al[i] + " of the volume") + GIVEN("An angle of 120 degrees") + { + auto angle = Degree{-120}; + + WHEN("A ray goes through center of volume") { - // x-ray source must be very far from the volume center to make testing of the op - // backprojection simpler - geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, std::move(volData), - std::move(sinoData), RotationAngles3D{Gamma{0}}, - PrincipalPointOffset2D{0, 0}, - RotationOffset3D{-offsetx[i], -offsety[i], 0}); - TestType op(volumeDescriptor, sinoDescriptor, geom); - THEN("The result of projecting is zero") + // In this case the ray enters and exits the volume through the borders along the + // main direction + geom.emplace_back(stc, ctr, angle, std::move(volData), std::move(sinoData)); + + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + TestType op(volumeDescriptor, sinoDescriptor); + + THEN("Ray intersects the correct pixels") { volume = 1; + volume(0, 0) = 0; + volume(0, 1) = 0; + volume(1, 1) = 0; + volume(2, 2) = 0; + volume(3, 2) = 0; + volume(3, 3) = 0; + // volume(1,2) hit due to numerical error + volume(1, 2) = 0; op.apply(volume, sino); - REQUIRE(sino[0] == 0); + DataContainer zero(sinoDescriptor); + zero = 0; + REQUIRE(zero[0] == Approx(0).epsilon(epsilon)); - AND_THEN("The result of backprojection is also zero") + AND_THEN("The correct weighting is applied") { + volume(0, 0) = 1; + volume(0, 1) = 2; + volume(1, 1) = 3; + + op.apply(volume, sino); + REQUIRE(sino[0] == Approx(2 * sqrt3d + 2)); + + // on the other side of the center + volume = 0; + volume(2, 2) = 3; + volume(3, 2) = 2; + volume(3, 3) = 1; + + op.apply(volume, sino); + REQUIRE(sino[0] == Approx(2 * sqrt3d + 2)); + sino[0] = 1; - op.applyAdjoint(sino, volume); - DataContainer zero(volumeDescriptor); - zero = 0; - REQUIRE(volume == zero); + Eigen::Matrix expected(volSize * volSize); + + // clang-format off + expected << 4 / sqrt3d - 2, 0, 0, 0, + 2 - 2 / sqrt3d, 2 / sqrt3d, 0, 0, + 0, 0, 2 / sqrt3d, 2 - 2 / sqrt3d, + 0, 0, 0, 4 / sqrt3d - 2; + // clang-format on + + op.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, expected), + epsilon)); } } } - } - } - GIVEN("A 2D setting with multiple projection angles") - { - IndexVector_t volumeDims(2), sinoDims(2); - const index_t volSize = 5; - const index_t detectorSize = 1; - const index_t numImgs = 4; - volumeDims << volSize, volSize; - sinoDims << detectorSize, numImgs; - VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); - DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); + WHEN("A ray enters through the top border") + { + // In this case the ray exits through a border along the main ray direction, but + // enters through a border not along the main direction + geom.emplace_back(stc, ctr, angle, std::move(volData), std::move(sinoData), + PrincipalPointOffset{0}, RotationOffset2D{0, std::sqrt(3.f)}); - auto stc = SourceToCenterOfRotation{20 * volSize}; - auto ctr = CenterOfRotationToDetector{volSize}; + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - std::vector geom; + TestType op(volumeDescriptor, sinoDescriptor); - WHEN("Both x- and y-axis-aligned rays are present") - { - geom.emplace_back(stc, ctr, Degree{0}, VolumeData2D{Size2D{volumeDims}}, - SinogramData2D{Size2D{sinoDims}}); - geom.emplace_back(stc, ctr, Degree{90}, VolumeData2D{Size2D{volumeDims}}, - SinogramData2D{Size2D{sinoDims}}); - geom.emplace_back(stc, ctr, Degree{180}, VolumeData2D{Size2D{volumeDims}}, - SinogramData2D{Size2D{sinoDims}}); - geom.emplace_back(stc, ctr, Degree{270}, VolumeData2D{Size2D{volumeDims}}, - SinogramData2D{Size2D{sinoDims}}); + THEN("Ray intersects the correct pixels") + { + volume = 1; + volume(0, 2) = 0; + volume(0, 3) = 0; + volume(1, 3) = 0; + volume(2, 3) = 0; - TestType op(volumeDescriptor, sinoDescriptor, geom); + op.apply(volume, sino); + DataContainer zero(sinoDescriptor); + zero = 0; + REQUIRE(zero[0] == Approx(0).epsilon(epsilon)); + + AND_THEN("The correct weighting is applied") + { + volume(0, 2) = 1; + volume(0, 3) = 2; + volume(1, 3) = 3; + volume(2, 3) = 4; + + op.apply(volume, sino); + REQUIRE(sino[0] == Approx(14 - 4 * sqrt3d)); + + sino[0] = 1; + + Eigen::Matrix expected(volSize * volSize); + + // clang-format off + expected << 0, 0, 0, 0, + 0, 0, 0, 0, + 2 - 2 / sqrt3d, 0, 0, 0, + 4 / sqrt3d - 2, 2 / sqrt3d, 4 - 2 * sqrt3d, 0; + // clang-format on + + op.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, expected), + epsilon)); + } + } + } + + WHEN("A ray exits through the bottom border") - THEN("Values are accumulated correctly along each ray's path") { - volume = 0; + // In this case the ray enters through a border along the main ray direction, but + // exits through a border not along the main direction + geom.emplace_back(stc, ctr, angle, std::move(volData), std::move(sinoData), + PrincipalPointOffset{0}, RotationOffset2D{0, -std::sqrt(3.f)}); - // set only values along the rays' path to one to make sure interpolation is dones - // correctly - for (index_t i = 0; i < volSize; i++) { - volume(i, volSize / 2) = 1; - volume(volSize / 2, i) = 1; + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + TestType op(volumeDescriptor, sinoDescriptor); + + THEN("Ray intersects the correct pixels") + { + volume = 1; + volume(1, 0) = 0; + volume(2, 0) = 0; + volume(3, 0) = 0; + volume(3, 1) = 0; + + op.apply(volume, sino); + DataContainer zero(sinoDescriptor); + zero = 0; + REQUIRE(zero[0] == Approx(0).epsilon(epsilon)); + + AND_THEN("The correct weighting is applied") + { + volume(1, 0) = 4; + volume(2, 0) = 3; + volume(3, 0) = 2; + volume(3, 1) = 1; + + op.apply(volume, sino); + REQUIRE(sino[0] == Approx(14 - 4 * sqrt3d)); + + sino[0] = 1; + + Eigen::Matrix expected(volSize * volSize); + + // clang-format off + expected << 0, 4 - 2 * sqrt3d, 2 / sqrt3d, 4 / sqrt3d - 2, 0, 0, 0, + 2 - 2 / sqrt3d, 0, 0, 0, 0, 0, 0, 0, 0; + // clang-format on + + op.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, expected), + epsilon)); + } } + } - op.apply(volume, sino); - for (index_t i = 0; i < numImgs; i++) - REQUIRE(sino[i] == Approx(5.0)); + WHEN("A ray only intersects a single pixel") + { + // This is a special case that is handled separately in both forward and + // backprojection + geom.emplace_back(stc, ctr, angle, std::move(volData), std::move(sinoData), + PrincipalPointOffset{0}, + RotationOffset2D{0, -2 - std::sqrt(3.f) / 2}); - AND_THEN("Backprojection yields the exact adjoint") + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + TestType op(volumeDescriptor, sinoDescriptor); + + THEN("Ray intersects the correct pixels") { - Eigen::Matrix cmp(volSize * volSize); + volume = 1; + volume(3, 0) = 0; - cmp << 0, 0, 10, 0, 0, 0, 0, 10, 0, 0, 10, 10, 20, 10, 10, 0, 0, 10, 0, 0, 0, 0, - 10, 0, 0; + op.apply(volume, sino); + DataContainer zero(sinoDescriptor); + zero = 0; + REQUIRE(zero[0] == Approx(0).epsilon(epsilon)); - op.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, cmp))); + AND_THEN("The correct weighting is applied") + { + volume(3, 0) = 1; + + op.apply(volume, sino); + REQUIRE(sino[0] == Approx(1 / sqrt3d).epsilon(0.005)); + + sino[0] = 1; + + Eigen::Matrix expected(volSize * volSize); + + // clang-format off + expected << 0, 0, 0, 1 / sqrt3d, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0; + // clang-format on + + op.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, expected), + epsilon)); + } } } } } +} - GIVEN("A 3D setting with multiple projection angles") +TEMPLATE_TEST_CASE("SiddonsMethodCUDA 2D setup with a multiple rays", "", SiddonsMethodCUDA, + SiddonsMethodCUDA, SiddonsMethod, SiddonsMethod) +{ + // Turn logger of + Logger::setLevel(Logger::LogLevel::OFF); + + using data_t = decltype(return_data_t(std::declval())); + + GIVEN("Given a 5x5 volume") { - IndexVector_t volumeDims(3), sinoDims(3); - const index_t volSize = 3; - const index_t detectorSize = 1; - const index_t numImgs = 6; - volumeDims << volSize, volSize, volSize; - sinoDims << detectorSize, detectorSize, numImgs; + // Domain setup + const index_t volSize = 5; + + IndexVector_t volumeDims(2); + volumeDims << volSize, volSize; + VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); + // range setup + const index_t detectorSize = 1; + const index_t numImgs = 4; + + IndexVector_t sinoDims(2); + sinoDims << detectorSize, numImgs; + + // Setup geometry auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; + auto volData = VolumeData2D{Size2D{volumeDims}}; + auto sinoData = SinogramData2D{Size2D{sinoDims}}; std::vector geom; - WHEN("x-, y and z-axis-aligned rays are present") + WHEN("Both x- and y-axis-aligned rays are present") { - real_t beta[numImgs] = {0.0, 0.0, 0.0, 0.0, pi_t / 2, 3 * pi_t / 2}; - real_t gamma[numImgs] = {0.0, pi_t, pi_t / 2, 3 * pi_t / 2, pi_t / 2, 3 * pi_t / 2}; + geom.emplace_back(stc, ctr, Degree{0}, VolumeData2D{Size2D{volumeDims}}, + SinogramData2D{Size2D{sinoDims}}); + geom.emplace_back(stc, ctr, Degree{90}, VolumeData2D{Size2D{volumeDims}}, + SinogramData2D{Size2D{sinoDims}}); + geom.emplace_back(stc, ctr, Degree{180}, VolumeData2D{Size2D{volumeDims}}, + SinogramData2D{Size2D{sinoDims}}); + geom.emplace_back(stc, ctr, Degree{270}, VolumeData2D{Size2D{volumeDims}}, + SinogramData2D{Size2D{sinoDims}}); - for (index_t i = 0; i < numImgs; i++) - geom.emplace_back(stc, ctr, VolumeData3D{Size3D{volumeDims}}, - SinogramData3D{Size3D{sinoDims}}, - RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}}); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - TestType op(volumeDescriptor, sinoDescriptor, geom); + TestType op(volumeDescriptor, sinoDescriptor); THEN("Values are accumulated correctly along each ray's path") { volume = 0; - // set only values along the rays' path to one to make sure interpolation is dones - // correctly + // set only values along the rays' path to one to make sure interpolation is + // dones correctly for (index_t i = 0; i < volSize; i++) { - volume(i, volSize / 2, volSize / 2) = 1; - volume(volSize / 2, i, volSize / 2) = 1; - volume(volSize / 2, volSize / 2, i) = 1; + volume(i, volSize / 2) = 1; + volume(volSize / 2, i) = 1; } op.apply(volume, sino); for (index_t i = 0; i < numImgs; i++) - REQUIRE(sino[i] == Approx(3.0)); + REQUIRE(sino[i] == Approx(5.0)); AND_THEN("Backprojection yields the exact adjoint") { - Eigen::Matrix cmp(volSize * volSize * volSize); - - cmp << 0, 0, 0, 0, 6, 0, 0, 0, 0, - - 0, 6, 0, 6, 18, 6, 0, 6, 0, + Eigen::Matrix cmp(volSize * volSize); - 0, 0, 0, 0, 6, 0, 0, 0, 0; + // clang-format off + cmp << 0, 0, 10, 0, 0, + 0, 0, 10, 0, 0, + 10, 10, 20, 10, 10, + 0, 0, 10, 0, 0, + 0, 0, 10, 0, 0; + // clang-format on op.applyAdjoint(sino, volume); REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, cmp))); @@ -852,7 +995,7 @@ TEMPLATE_TEST_CASE("Scenario: Axis-aligned rays are present", "", SiddonsMethodC } } -TEMPLATE_TEST_CASE("Scenario: Projection under an angle", "", SiddonsMethodCUDA, +TEMPLATE_TEST_CASE("SiddonsMethodCUDA 3D setup with a single ray", "", SiddonsMethodCUDA, SiddonsMethodCUDA, SiddonsMethod, SiddonsMethod) { // Turn logger of @@ -860,409 +1003,475 @@ TEMPLATE_TEST_CASE("Scenario: Projection under an angle", "", SiddonsMethodCUDA< using data_t = decltype(return_data_t(std::declval())); - real_t sqrt3r = std::sqrt(static_cast(3)); - data_t sqrt3d = std::sqrt(static_cast(3)); - - GIVEN("A 2D setting with a single ray") + GIVEN("Given a 3x3x3 volume") { - IndexVector_t volumeDims(2), sinoDims(2); - const index_t volSize = 4; - const index_t detectorSize = 1; - const index_t numImgs = 1; - volumeDims << volSize, volSize; - sinoDims << detectorSize, numImgs; + // Domain setup + const index_t volSize = 3; + + IndexVector_t volumeDims(3); + volumeDims << volSize, volSize, volSize; + VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); + // range setup + const index_t detectorSize = 1; + const index_t numImgs = 1; + + IndexVector_t sinoDims(3); + sinoDims << detectorSize, detectorSize, numImgs; + + // Setup geometry auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; - auto volData = VolumeData2D{Size2D{volumeDims}}; - auto sinoData = SinogramData2D{Size2D{sinoDims}}; + auto volData = VolumeData3D{Size3D{volumeDims}}; + auto sinoData = SinogramData3D{Size3D{sinoDims}}; std::vector geom; - WHEN("Projecting under an angle of 30 degrees and ray goes through center of volume") + GIVEN("A basic 3D setup") { - // In this case the ray enters and exits the volume through the borders along the main - // direction - geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData)); - TestType op(volumeDescriptor, sinoDescriptor, geom); + geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), + RotationAngles3D{Gamma{0}}); - THEN("Ray intersects the correct pixels") - { - volume = 1; - volume(3, 0) = 0; - volume(2, 0) = 0; - volume(2, 1) = 0; + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - volume(1, 2) = 0; - volume(1, 3) = 0; - volume(0, 3) = 0; - volume(2, 2) = 0; + TestType op(volumeDescriptor, sinoDescriptor); - op.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(zero[0] == Approx(0).epsilon(epsilon)); + WHEN("Sinogram conatainer is not zero initialized and we project through an empty " + "volume") + { + volume = 0; + sino = 1; - AND_THEN("The correct weighting is applied") + THEN("Result is zero") { - volume(3, 0) = 1; - volume(2, 0) = 2; - volume(2, 1) = 3; - - op.apply(volume, sino); - REQUIRE(sino[0] == Approx(2 * sqrt3d + 2)); - - // on the other side of the center - volume = 0; - volume(1, 2) = 3; - volume(1, 3) = 2; - volume(0, 3) = 1; + DataContainer zero(sinoDescriptor); + zero = 0; op.apply(volume, sino); - REQUIRE(sino[0] == Approx(2 * sqrt3d + 2)); - - sino[0] = 1; - - Eigen::Matrix expected(volSize * volSize); - expected << 0, 0, 2 - 2 / sqrt3d, 4 / sqrt3d - 2, 0, 0, 2 / sqrt3d, 0, 0, - 2 / sqrt3d, 0, 0, 4 / sqrt3d - 2, 2 - 2 / sqrt3d, 0, 0; - - op.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, expected), - epsilon)); + REQUIRE(isApprox(sino, zero, epsilon)); } } - } - WHEN("Projecting under an angle of 30 degrees and ray enters through the right border") - { - // In this case the ray exits through a border along the main ray direction, but enters - // through a border not along the main direction First pixel should be weighted - // differently - geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData), - PrincipalPointOffset{0}, RotationOffset2D{sqrt3r, 0}); - // through a border not along the main direction - TestType op(volumeDescriptor, sinoDescriptor, geom); - - THEN("Ray intersects the correct pixels") + WHEN("Volume container is not zero initialized and we backproject from an empty " + "sinogram") { + sino = 0; volume = 1; - volume(3, 1) = 0; - volume(3, 2) = 0; - volume(3, 3) = 0; - volume(2, 3) = 0; - op.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(isApprox(sino, zero, epsilon)); - - AND_THEN("The correct weighting is applied") + THEN("Result is zero") { - volume(3, 1) = 4; - volume(3, 2) = 3; - volume(3, 3) = 2; - volume(2, 3) = 1; - - op.apply(volume, sino); - REQUIRE(sino[0] == Approx(14 - 4 * sqrt3d)); - - sino[0] = 1; - - Eigen::Matrix expected(volSize * volSize); - expected << 0, 0, 0, 0, 0, 0, 0, 4 - 2 * sqrt3d, 0, 0, 0, 2 / sqrt3d, 0, 0, - 2 - 2 / sqrt3d, 4 / sqrt3d - 2; + DataContainer zero(volumeDescriptor); + zero = 0; op.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, expected), - epsilon)); + REQUIRE(isApprox(volume, zero, epsilon)); } } } - WHEN("Projecting under an angle of 30 degrees and ray exits through the left border") + GIVEN("A rays along different axes") { - // In this case the ray enters through a border along the main ray direction, but exits - // through a border not along the main direction - geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData), - PrincipalPointOffset{0}, RotationOffset2D{-sqrt3r, 0}); - TestType op(volumeDescriptor, sinoDescriptor, geom); + const index_t numCases = 6; - THEN("Ray intersects the correct pixels") - { - volume = 1; - volume(0, 0) = 0; - volume(1, 0) = 0; - volume(0, 1) = 0; - volume(0, 2) = 0; + using RealArray = std::array; + using StrArray = std::array; - op.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(isApprox(sino, zero, epsilon)); + RealArray beta = {0.0, 0.0, 0.0, 0.0, pi_t / 2, 3 * pi_t / 2}; + RealArray gamma = {0.0, pi_t, pi_t / 2, 3 * pi_t / 2, pi_t / 2, 3 * pi_t / 2}; + StrArray al = {"z", "-z", "x", "-x", "y", "-y"}; + + Eigen::Matrix backProj[numCases]; + + // clang-format off + backProj[0] << 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0; + + backProj[1] << 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 1, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0; + + backProj[2] << 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 1, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0; + // clang-format on - AND_THEN("The correct weighting is applied") + for (index_t i = 0; i < numCases; i++) { + WHEN("A " + al[i] + "-axis-aligned ray passes through the center of a pixel") { - volume(1, 0) = 1; - volume(0, 0) = 2; - volume(0, 1) = 3; - volume(0, 2) = 4; + geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), + RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}}); - op.apply(volume, sino); - REQUIRE(sino[0] == Approx(14 - 4 * sqrt3d)); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - sino[0] = 1; + TestType op(volumeDescriptor, sinoDescriptor); - Eigen::Matrix expected(volSize * volSize); - expected << 4 / sqrt3d - 2, 2 - 2 / sqrt3d, 0, 0, 2 / sqrt3d, 0, 0, 0, - 4 - 2 * sqrt3d, 0, 0, 0, 0, 0, 0, 0; + THEN("The result of projecting through a voxel is exactly the voxel value") + { + for (index_t j = 0; j < volSize; j++) { + volume = 0; + if (i / 2 == 0) + volume(volSize / 2, volSize / 2, j) = 1; + else if (i / 2 == 1) + volume(j, volSize / 2, volSize / 2) = 1; + else if (i / 2 == 2) + volume(volSize / 2, j, volSize / 2) = 1; + + op.apply(volume, sino); + REQUIRE(sino[0] == 1); + } - op.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, expected), - epsilon)); + AND_THEN( + "The backprojection sets the values of all hit voxels to the detector " + "value") + { + op.applyAdjoint(sino, volume); + REQUIRE(isApprox( + volume, DataContainer(volumeDescriptor, backProj[i / 2]))); + } + } } } - } - WHEN("Projecting under an angle of 30 degrees and ray only intersects a single pixel") - { - geom.emplace_back(stc, ctr, Radian{-pi_t / 6}, std::move(volData), std::move(sinoData), - PrincipalPointOffset{0}, RotationOffset2D{-2 - sqrt3r / 2, 0}); - TestType op(volumeDescriptor, sinoDescriptor, geom); + RealArray offsetx; + RealArray offsety; + + offsetx[0] = volSize / 2.0; + offsetx[1] = 0.0; + offsetx[2] = (volSize / 2.0); + offsetx[3] = -(volSize / 2.0); + offsetx[4] = 0.0; + offsetx[5] = -(volSize / 2.0); + + offsety[0] = 0.0; + offsety[1] = volSize / 2.0; + offsety[2] = (volSize / 2.0); + offsety[3] = 0.0; + offsety[4] = -(volSize / 2.0); + offsety[5] = -(volSize / 2.0); + + // clang-format off + backProj[0] << 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, 0; + + backProj[1] << 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0; + + backProj[2] << 1, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0; + // clang-format on + + al[0] = "left border"; + al[1] = "bottom border"; + al[2] = "bottom left border"; + al[3] = "right border"; + al[4] = "top border"; + al[5] = "top right edge"; + + for (index_t i = 0; i < numCases / 2; i++) { + WHEN("A z-axis-aligned ray runs along the " + al[i] + " of the volume") + { + // x-ray source must be very far from the volume center to make testing of the + // op backprojection simpler + geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, + std::move(volData), std::move(sinoData), + RotationAngles3D{Gamma{0}}, PrincipalPointOffset2D{0, 0}, + RotationOffset3D{-offsetx[i], -offsety[i], 0}); - THEN("Ray intersects the correct pixels") - { - volume = 1; - volume(0, 0) = 0; + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - op.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(isApprox(sino, zero, epsilon)); + TestType op(volumeDescriptor, sinoDescriptor); + + THEN("The result of projecting through a voxel is exactly the voxel's value") + { + for (index_t j = 0; j < volSize; j++) { + volume = 0; + switch (i) { + case 0: + volume(0, volSize / 2, j) = 1; + break; + case 1: + volume(volSize / 2, 0, j) = 1; + break; + case 2: + volume(0, 0, j) = 1; + break; + default: + break; + } + + op.apply(volume, sino); + REQUIRE(sino[0] == 1); + } + + AND_THEN("The backprojection yields the exact adjoints") + { + sino[0] = 1; + op.applyAdjoint(sino, volume); + + REQUIRE(isApprox(volume, + DataContainer(volumeDescriptor, backProj[i]))); + } + } + } + } - AND_THEN("The correct weighting is applied") + for (index_t i = numCases / 2; i < numCases; i++) { + WHEN("A z-axis-aligned ray runs along the " + al[i] + " of the volume") { - volume(0, 0) = 1; + // x-ray source must be very far from the volume center to make testing of the + // op backprojection simpler + geom.emplace_back(SourceToCenterOfRotation{volSize * 2000}, ctr, + std::move(volData), std::move(sinoData), + RotationAngles3D{Gamma{0}}, PrincipalPointOffset2D{0, 0}, + RotationOffset3D{-offsetx[i], -offsety[i], 0}); - op.apply(volume, sino); - REQUIRE(sino[0] == Approx(1 / sqrt3d)); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - sino[0] = 1; + TestType op(volumeDescriptor, sinoDescriptor); - Eigen::Matrix expected(volSize * volSize); - expected << 1 / sqrt3d, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0; + THEN("The result of projecting is zero") + { + volume = 1; - op.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, expected), - epsilon)); + op.apply(volume, sino); + REQUIRE(sino[0] == 0); + + AND_THEN("The result of backprojection is also zero") + { + sino[0] = 1; + op.applyAdjoint(sino, volume); + + DataContainer zero(volumeDescriptor); + zero = 0; + REQUIRE(volume == zero); + } + } } } } - WHEN("Projecting under an angle of 120 degrees and ray goes through center of volume") + GIVEN("An angle of 30 degrees") { - // In this case the ray enters and exits the volume through the borders along the main - // direction - geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData), - std::move(sinoData)); - TestType op(volumeDescriptor, sinoDescriptor, geom); + data_t sqrt3d = std::sqrt(static_cast(3)); - THEN("Ray intersects the correct pixels") - { - volume = 1; - volume(0, 0) = 0; - volume(0, 1) = 0; - volume(1, 1) = 0; - volume(2, 2) = 0; - volume(3, 2) = 0; - volume(3, 3) = 0; - // volume(1,2) hit due to numerical error - volume(1, 2) = 0; + Eigen::Matrix backProj; - op.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(zero[0] == Approx(0).epsilon(epsilon)); + auto gamma = Gamma{Degree{30}}; - AND_THEN("The correct weighting is applied") - { - volume(0, 0) = 1; - volume(0, 1) = 2; - volume(1, 1) = 3; + WHEN("A ray goes through the center of the volume") + { + // In this case the ray enters and exits the volume along the main direction + geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), + RotationAngles3D{gamma}); - op.apply(volume, sino); - REQUIRE(sino[0] == Approx(2 * sqrt3d + 2)); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - // on the other side of the center - volume = 0; - volume(2, 2) = 3; - volume(3, 2) = 2; - volume(3, 3) = 1; + TestType op(volumeDescriptor, sinoDescriptor); + + THEN("The ray intersects the correct voxels") + { + volume = 1; + volume(1, 1, 1) = 0; + volume(2, 1, 0) = 0; + volume(1, 1, 0) = 0; + volume(0, 1, 2) = 0; + volume(1, 1, 2) = 0; op.apply(volume, sino); - REQUIRE(sino[0] == Approx(2 * sqrt3d + 2)); + REQUIRE(sino[0] == Approx(0).margin(1e-5)); - sino[0] = 1; + AND_THEN("The correct weighting is applied") + { + volume(1, 1, 1) = 1; + volume(2, 1, 0) = 3; + volume(1, 1, 2) = 2; - Eigen::Matrix expected(volSize * volSize); + op.apply(volume, sino); + REQUIRE(sino[0] == Approx(3 * sqrt3d - 1).epsilon(epsilon)); - expected << 4 / sqrt3d - 2, 0, 0, 0, 2 - 2 / sqrt3d, 2 / sqrt3d, 0, 0, 0, 0, - 2 / sqrt3d, 2 - 2 / sqrt3d, 0, 0, 0, 4 / sqrt3d - 2; + sino[0] = 1; + // clang-format off + backProj << 0, 0, 0, 0, 1 - 1 / sqrt3d, sqrt3d - 1, 0, 0, 0, + 0, 0, 0, 0, 2 / sqrt3d, 0, 0, 0, 0, + 0, 0, 0, sqrt3d - 1, 1 - 1 / sqrt3d, 0, 0, 0, 0; + // clang-format on - op.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, expected), - epsilon)); + op.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, backProj), + epsilon)); + } } } - } - WHEN("Projecting under an angle of 120 degrees and ray enters through the top border") - { - // In this case the ray exits through a border along the main ray direction, but enters - // through a border not along the main direction - geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData), - std::move(sinoData), PrincipalPointOffset{0}, - RotationOffset2D{0, std::sqrt(3.f)}); - TestType op(volumeDescriptor, sinoDescriptor, geom); - - THEN("Ray intersects the correct pixels") + WHEN("A ray with an angle of 30 degrees enters through the right border") { - volume = 1; - volume(0, 2) = 0; - volume(0, 3) = 0; - volume(1, 3) = 0; - volume(2, 3) = 0; + // In this case the ray enters through a border orthogonal to a non-main + // direction + geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), + RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0}, + RotationOffset3D{1, 0, 0}); - op.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(zero[0] == Approx(0).epsilon(epsilon)); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - AND_THEN("The correct weighting is applied") + TestType op(volumeDescriptor, sinoDescriptor); + + THEN("The ray intersects the correct voxels") { - volume(0, 2) = 1; - volume(0, 3) = 2; - volume(1, 3) = 3; - volume(2, 3) = 4; + volume = 1; + volume(2, 1, 1) = 0; + volume(2, 1, 0) = 0; + volume(2, 1, 2) = 0; + volume(1, 1, 2) = 0; op.apply(volume, sino); - REQUIRE(sino[0] == Approx(14 - 4 * sqrt3d)); + REQUIRE(sino[0] == Approx(0).epsilon(epsilon)); - sino[0] = 1; + AND_THEN("The correct weighting is applied") + { + volume(2, 1, 0) = 4; + volume(1, 1, 2) = 3; + volume(2, 1, 1) = 1; - Eigen::Matrix expected(volSize * volSize); + op.apply(volume, sino); + REQUIRE(sino[0] == Approx(1 - 2 / sqrt3d + 3 * sqrt3d)); - expected << 0, 0, 0, 0, 0, 0, 0, 0, 2 - 2 / sqrt3d, 0, 0, 0, 4 / sqrt3d - 2, - 2 / sqrt3d, 4 - 2 * sqrt3d, 0; + sino[0] = 1; - op.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, expected), - epsilon)); + // clang-format off + backProj << 0, 0, 0, 0, 0, 1 - 1 / sqrt3d, 0, 0, 0, + 0, 0, 0, 0, 0, 2 / sqrt3d, 0, 0, 0, + 0, 0, 0, 0, sqrt3d - 1, 1 - 1 / sqrt3d, 0, 0, 0; + // clang-format on + + op.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, backProj), + epsilon)); + } } } - } - WHEN("Projecting under an angle of 120 degrees and ray exits through the bottom border") - { - // In this case the ray enters through a border along the main ray direction, but exits - // through a border not along the main direction - geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData), - std::move(sinoData), PrincipalPointOffset{0}, - RotationOffset2D{0, -std::sqrt(3.f)}); - TestType op(volumeDescriptor, sinoDescriptor, geom); - - THEN("Ray intersects the correct pixels") + WHEN("A ray with an angle of 30 degrees exits through the left border") { - volume = 1; - volume(1, 0) = 0; - volume(2, 0) = 0; - volume(3, 0) = 0; - volume(3, 1) = 0; + // In this case the ray exit through a border orthogonal to a non-main direction + geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), + RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0}, + RotationOffset3D{-1, 0, 0}); - op.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(zero[0] == Approx(0).epsilon(epsilon)); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - AND_THEN("The correct weighting is applied") + TestType op(volumeDescriptor, sinoDescriptor); + + THEN("The ray intersects the correct voxels") { - volume(1, 0) = 4; - volume(2, 0) = 3; - volume(3, 0) = 2; - volume(3, 1) = 1; + volume = 1; + volume(0, 1, 0) = 0; + volume(1, 1, 0) = 0; + volume(0, 1, 1) = 0; + volume(0, 1, 2) = 0; op.apply(volume, sino); - REQUIRE(sino[0] == Approx(14 - 4 * sqrt3d)); + REQUIRE(sino[0] == Approx(0).epsilon(epsilon)); - sino[0] = 1; + AND_THEN("The correct weighting is applied") + { + volume(0, 1, 2) = 4; + volume(1, 1, 0) = 3; + volume(0, 1, 1) = 1; - Eigen::Matrix expected(volSize * volSize); + op.apply(volume, sino); + REQUIRE(sino[0] == Approx(3 * sqrt3d + 1 - 2 / sqrt3d).epsilon(epsilon)); - expected << 0, 4 - 2 * sqrt3d, 2 / sqrt3d, 4 / sqrt3d - 2, 0, 0, 0, - 2 - 2 / sqrt3d, 0, 0, 0, 0, 0, 0, 0, 0; + sino[0] = 1; - op.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, expected), - epsilon)); + // clang-format off + backProj << 0, 0, 0, 1 - 1 / sqrt3d, sqrt3d - 1, 0, 0, 0, 0, + 0, 0, 0, 2 / sqrt3d, 0, 0, 0, 0, 0, + 0, 0, 0, 1 - 1 / sqrt3d, 0, 0, 0, 0, 0; + // clang-format on + + op.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, backProj), + epsilon)); + } } } - } - - WHEN("Projecting under an angle of 120 degrees and ray only intersects a single pixel") - { - // This is a special case that is handled separately in both forward and backprojection - geom.emplace_back(stc, ctr, Radian{-2 * pi_t / 3}, std::move(volData), - std::move(sinoData), PrincipalPointOffset{0}, - RotationOffset2D{0, -2 - std::sqrt(3.f) / 2}); - TestType op(volumeDescriptor, sinoDescriptor, geom); - THEN("Ray intersects the correct pixels") + WHEN("A ray with an angle of 30 degrees only intersects a single voxel") { - volume = 1; - volume(3, 0) = 0; + // special case - no interior voxels, entry and exit voxels are the same + geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), + RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0}, + RotationOffset3D{-2, 0, 0}); - op.apply(volume, sino); - DataContainer zero(sinoDescriptor); - zero = 0; - REQUIRE(zero[0] == Approx(0).epsilon(epsilon)); + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); + + TestType op(volumeDescriptor, sinoDescriptor); - AND_THEN("The correct weighting is applied") + THEN("The ray intersects the correct voxels") { - volume(3, 0) = 1; + volume = 1; + volume(0, 1, 0) = 0; op.apply(volume, sino); - REQUIRE(sino[0] == Approx(1 / sqrt3d).epsilon(0.005)); + REQUIRE(sino[0] == Approx(0).epsilon(epsilon)); - sino[0] = 1; + AND_THEN("The correct weighting is applied") + { + volume(0, 1, 0) = 1; + + op.apply(volume, sino); + REQUIRE(sino[0] == Approx(sqrt3d - 1).epsilon(epsilon)); + + sino[0] = 1; - Eigen::Matrix expected(volSize * volSize); - expected << 0, 0, 0, 1 / sqrt3d, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0; + // clang-format off + backProj << 0, 0, 0, sqrt3d - 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0; + // clang-format on - op.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, expected), - epsilon)); + op.applyAdjoint(sino, volume); + REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, backProj), + epsilon)); + } } } } } - GIVEN("A 3D setting with a single ray") + GIVEN("Given a 5x5x5 volume") { - IndexVector_t volumeDims(3), sinoDims(3); - const index_t volSize = 3; - const index_t detectorSize = 1; - const index_t numImgs = 1; + // Domain setup + const index_t volSize = 5; + + IndexVector_t volumeDims(3); volumeDims << volSize, volSize, volSize; - sinoDims << detectorSize, detectorSize, numImgs; + VolumeDescriptor volumeDescriptor(volumeDims); - VolumeDescriptor sinoDescriptor(sinoDims); DataContainer volume(volumeDescriptor); - DataContainer sino(sinoDescriptor); + // range setup + const index_t detectorSize = 1; + const index_t numImgs = 1; + + IndexVector_t sinoDims(3); + sinoDims << detectorSize, detectorSize, numImgs; + + // Setup geometry auto stc = SourceToCenterOfRotation{20 * volSize}; auto ctr = CenterOfRotationToDetector{volSize}; auto volData = VolumeData3D{Size3D{volumeDims}}; @@ -1270,167 +1479,137 @@ TEMPLATE_TEST_CASE("Scenario: Projection under an angle", "", SiddonsMethodCUDA< std::vector geom; - Eigen::Matrix backProj; - - WHEN("A ray with an angle of 30 degrees goes through the center of the volume") + GIVEN("Tracing rays along axis") { - // In this case the ray enters and exits the volume along the main direction - geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), - RotationAngles3D{Gamma{pi_t / 6}}); - TestType op(volumeDescriptor, sinoDescriptor, geom); + const index_t numCases = 9; + using Array = std::array; + using StrArray = std::array; - THEN("The ray intersects the correct voxels") - { - volume = 1; - volume(1, 1, 1) = 0; - volume(2, 1, 0) = 0; - volume(1, 1, 0) = 0; - volume(0, 1, 2) = 0; - volume(1, 1, 2) = 0; + Array alpha = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + Array beta = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, pi_t / 2, pi_t / 2, pi_t / 2}; + Array gamma = {0.0, 0.0, 0.0, pi_t / 2, pi_t / 2, + pi_t / 2, pi_t / 2, pi_t / 2, pi_t / 2}; - op.apply(volume, sino); - REQUIRE(sino[0] == Approx(0).margin(1e-5)); - - AND_THEN("The correct weighting is applied") - { - volume(1, 1, 1) = 1; - volume(2, 1, 0) = 3; - volume(1, 1, 2) = 2; + Array offsetx = {volSize, 0.0, volSize, 0.0, 0.0, 0.0, volSize, 0.0, volSize}; + Array offsety = {0.0, volSize, volSize, volSize, 0.0, volSize, 0.0, 0.0, 0.0}; + Array offsetz = {0.0, 0.0, 0.0, 0.0, volSize, volSize, 0.0, volSize, volSize}; - op.apply(volume, sino); - REQUIRE(sino[0] == Approx(3 * sqrt3d - 1).epsilon(epsilon)); + StrArray neg = {"x", "y", "x and y", "y", "z", "y and z", "x", "z", "x and z"}; + StrArray ali = {"z", "z", "z", "x", "x", "x", "y", "y", "y"}; - sino[0] = 1; - backProj << 0, 0, 0, 0, 1 - 1 / sqrt3d, sqrt3d - 1, 0, 0, 0, + for (index_t i = 0; i < numCases; i++) { + WHEN("Tracing along a " + ali[i] + "-axis-aligned ray with negative " + neg[i] + + "-coodinate of origin") + { + geom.emplace_back( + stc, ctr, std::move(volData), std::move(sinoData), + RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}, Alpha{alpha[i]}}, + PrincipalPointOffset2D{0, 0}, + RotationOffset3D{-offsetx[i], -offsety[i], -offsetz[i]}); - 0, 0, 0, 0, 2 / sqrt3d, 0, 0, 0, 0, + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - 0, 0, 0, sqrt3d - 1, 1 - 1 / sqrt3d, 0, 0, 0, 0; + TestType op(volumeDescriptor, sinoDescriptor); - op.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, backProj), - epsilon)); + THEN("Result of forward projection is zero") + { + op.apply(volume, sino); + DataContainer zero(sinoDescriptor); + zero = 0; + REQUIRE(isApprox(sino, zero, epsilon)); + + AND_THEN("Result of backprojection is zero") + { + op.applyAdjoint(sino, volume); + DataContainer zero(volumeDescriptor); + zero = 0; + REQUIRE(isApprox(volume, zero, epsilon)); + } + } } } } + } +} - WHEN("A ray with an angle of 30 degrees enters through the right border") - { - // In this case the ray enters through a border orthogonal to a non-main direction - geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), - RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0}, - RotationOffset3D{1, 0, 0}); - TestType op(volumeDescriptor, sinoDescriptor, geom); +TEMPLATE_TEST_CASE("SiddonsMethodCUDA 3D setup with a multiple rays", "", SiddonsMethodCUDA, + SiddonsMethodCUDA, SiddonsMethod, SiddonsMethod) +{ + // Turn logger of + Logger::setLevel(Logger::LogLevel::OFF); - THEN("The ray intersects the correct voxels") - { - volume = 1; - volume(2, 1, 1) = 0; - volume(2, 1, 0) = 0; - volume(2, 1, 2) = 0; - volume(1, 1, 2) = 0; + using data_t = decltype(return_data_t(std::declval())); - op.apply(volume, sino); - REQUIRE(sino[0] == Approx(0).epsilon(epsilon)); + GIVEN("A 3D setting with multiple projection angles") + { + // Domain setup + const index_t volSize = 3; - AND_THEN("The correct weighting is applied") - { - volume(2, 1, 0) = 4; - volume(1, 1, 2) = 3; - volume(2, 1, 1) = 1; + IndexVector_t volumeDims(3); + volumeDims << volSize, volSize, volSize; - op.apply(volume, sino); - REQUIRE(sino[0] == Approx(1 - 2 / sqrt3d + 3 * sqrt3d)); + VolumeDescriptor volumeDescriptor(volumeDims); + DataContainer volume(volumeDescriptor); - sino[0] = 1; - backProj << 0, 0, 0, 0, 0, 1 - 1 / sqrt3d, 0, 0, 0, + // range setup + const index_t detectorSize = 1; + const index_t numImgs = 6; - 0, 0, 0, 0, 0, 2 / sqrt3d, 0, 0, 0, + IndexVector_t sinoDims(3); + sinoDims << detectorSize, detectorSize, numImgs; - 0, 0, 0, 0, sqrt3d - 1, 1 - 1 / sqrt3d, 0, 0, 0; + // Setup geometry + auto stc = SourceToCenterOfRotation{20 * volSize}; + auto ctr = CenterOfRotationToDetector{volSize}; + auto volData = VolumeData3D{Size3D{volumeDims}}; + auto sinoData = SinogramData3D{Size3D{sinoDims}}; - op.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, backProj), - epsilon)); - } - } - } + std::vector geom; - WHEN("A ray with an angle of 30 degrees exits through the left border") + WHEN("x-, y and z-axis-aligned rays are present") { - // In this case the ray exit through a border orthogonal to a non-main direction - geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), - RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0}, - RotationOffset3D{-1, 0, 0}); - TestType op(volumeDescriptor, sinoDescriptor, geom); - - THEN("The ray intersects the correct voxels") - { - volume = 1; - volume(0, 1, 0) = 0; - volume(1, 1, 0) = 0; - volume(0, 1, 1) = 0; - volume(0, 1, 2) = 0; - - op.apply(volume, sino); - REQUIRE(sino[0] == Approx(0).epsilon(epsilon)); - - AND_THEN("The correct weighting is applied") - { - volume(0, 1, 2) = 4; - volume(1, 1, 0) = 3; - volume(0, 1, 1) = 1; + real_t beta[numImgs] = {0.0, 0.0, 0.0, 0.0, pi_t / 2, 3 * pi_t / 2}; + real_t gamma[numImgs] = {0.0, pi_t, pi_t / 2, 3 * pi_t / 2, pi_t / 2, 3 * pi_t / 2}; - op.apply(volume, sino); - REQUIRE(sino[0] == Approx(3 * sqrt3d + 1 - 2 / sqrt3d).epsilon(epsilon)); + for (index_t i = 0; i < numImgs; i++) + geom.emplace_back(stc, ctr, VolumeData3D{Size3D{volumeDims}}, + SinogramData3D{Size3D{sinoDims}}, + RotationAngles3D{Gamma{gamma[i]}, Beta{beta[i]}}); - sino[0] = 1; - backProj << 0, 0, 0, 1 - 1 / sqrt3d, sqrt3d - 1, 0, 0, 0, 0, + PlanarDetectorDescriptor sinoDescriptor(sinoDims, geom); + DataContainer sino(sinoDescriptor); - 0, 0, 0, 2 / sqrt3d, 0, 0, 0, 0, 0, + TestType op(volumeDescriptor, sinoDescriptor); - 0, 0, 0, 1 - 1 / sqrt3d, 0, 0, 0, 0, 0; + THEN("Values are accumulated correctly along each ray's path") + { + volume = 0; - op.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, backProj), - epsilon)); + // set only values along the rays' path to one to make sure interpolation is dones + // correctly + for (index_t i = 0; i < volSize; i++) { + volume(i, volSize / 2, volSize / 2) = 1; + volume(volSize / 2, i, volSize / 2) = 1; + volume(volSize / 2, volSize / 2, i) = 1; } - } - } - - WHEN("A ray with an angle of 30 degrees only intersects a single voxel") - { - // special case - no interior voxels, entry and exit voxels are the same - geom.emplace_back(stc, ctr, std::move(volData), std::move(sinoData), - RotationAngles3D{Gamma{pi_t / 6}}, PrincipalPointOffset2D{0, 0}, - RotationOffset3D{-2, 0, 0}); - TestType op(volumeDescriptor, sinoDescriptor, geom); - - THEN("The ray intersects the correct voxels") - { - volume = 1; - volume(0, 1, 0) = 0; op.apply(volume, sino); - REQUIRE(sino[0] == Approx(0).epsilon(epsilon)); + for (index_t i = 0; i < numImgs; i++) + REQUIRE(sino[i] == Approx(3.0)); - AND_THEN("The correct weighting is applied") + AND_THEN("Backprojection yields the exact adjoint") { - volume(0, 1, 0) = 1; - - op.apply(volume, sino); - REQUIRE(sino[0] == Approx(sqrt3d - 1).epsilon(epsilon)); - - sino[0] = 1; - backProj << 0, 0, 0, sqrt3d - 1, 0, 0, 0, 0, 0, - - 0, 0, 0, 0, 0, 0, 0, 0, 0, + Eigen::Matrix cmp(volSize * volSize * volSize); - 0, 0, 0, 0, 0, 0, 0, 0, 0; + // clang-format off + cmp << 0, 0, 0, 0, 6, 0, 0, 0, 0, + 0, 6, 0, 6, 18, 6, 0, 6, 0, + 0, 0, 0, 0, 6, 0, 0, 0, 0; + // clang-format on op.applyAdjoint(sino, volume); - REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, backProj), - epsilon)); + REQUIRE(isApprox(volume, DataContainer(volumeDescriptor, cmp))); } } } diff --git a/examples/example2d.cpp b/examples/example2d.cpp index 0d8054823eea4311cbb69b4dcdf14876af51df1e..e993fc6c0ca1e4a36790be8aaf1c3d6e1d179933 100644 --- a/examples/example2d.cpp +++ b/examples/example2d.cpp @@ -12,18 +12,23 @@ void example2d() IndexVector_t size(2); size << 128, 128; auto phantom = PhantomGenerator::createModifiedSheppLogan(size); + auto& volumeDescriptor = phantom.getDataDescriptor(); // write the phantom out EDF::write(phantom, "2dphantom.edf"); // generate circular trajectory - index_t noAngles{180}, arc{360}; - auto [geometry, sinoDescriptor] = CircleTrajectoryGenerator::createTrajectory( - noAngles, phantom.getDataDescriptor(), arc, size(0) * 100, size(0)); + index_t numAngles{180}, arc{360}; + auto sinoDescriptor = CircleTrajectoryGenerator::createTrajectory( + numAngles, phantom.getDataDescriptor(), arc, size(0) * 100, size(0)); + + // dynamic_cast to VolumeDescriptor is legal and will not throw, as PhantomGenerator returns a + // VolumeDescriptor // setup operator for 2d X-ray transform Logger::get("Info")->info("Simulating sinogram using Siddon's method"); - SiddonsMethod projector(phantom.getDataDescriptor(), *sinoDescriptor, geometry); + SiddonsMethod projector(dynamic_cast(volumeDescriptor), + *sinoDescriptor); // simulate the sinogram auto sinogram = projector.apply(phantom); diff --git a/examples/example3d.cpp b/examples/example3d.cpp index 7d6a34eaab96c8d2c1285b77b0efa1fcca3111c0..c3dd17f3333c3fdef432837668911b2244d80b00 100644 --- a/examples/example3d.cpp +++ b/examples/example3d.cpp @@ -12,18 +12,23 @@ void example3d() IndexVector_t size(3); size << 128, 128, 128; auto phantom = PhantomGenerator::createModifiedSheppLogan(size); + auto& volumeDescriptor = phantom.getDataDescriptor(); // write the phantom out EDF::write(phantom, "3dphantom.edf"); // generate circular trajectory - index_t noAngles{180}, arc{360}; - auto [geometry, sinoDescriptor] = CircleTrajectoryGenerator::createTrajectory( - noAngles, phantom.getDataDescriptor(), arc, size(0) * 100, size(0)); + index_t numAngles{180}, arc{360}; + auto sinoDescriptor = CircleTrajectoryGenerator::createTrajectory( + numAngles, phantom.getDataDescriptor(), arc, size(0) * 100, size(0)); + + // dynamic_cast to VolumeDescriptor is legal and will not throw, as PhantomGenerator returns a + // VolumeDescriptor // setup operator for 2d X-ray transform Logger::get("Info")->info("Simulating sinogram using Siddon's method"); - JosephsMethodCUDA projector(phantom.getDataDescriptor(), *sinoDescriptor, geometry); + JosephsMethodCUDA projector(dynamic_cast(volumeDescriptor), + *sinoDescriptor); // simulate the sinogram auto sinogram = projector.apply(phantom); diff --git a/examples/speed_test.cpp b/examples/speed_test.cpp index 0fd6bc9be42381b515019fb54485fdfef4f78012..d551612adbe076b78a3c5838e62ad74975a2b077 100644 --- a/examples/speed_test.cpp +++ b/examples/speed_test.cpp @@ -66,22 +66,28 @@ int main() auto& volumeDescriptor = phantom.getDataDescriptor(); // generate circular trajectory with numAngles angles over 360 degrees - auto [geom, sinoDescriptor] = CircleTrajectoryGenerator::createTrajectory( + auto sinoDescriptor = CircleTrajectoryGenerator::createTrajectory( numAngles, volumeDescriptor, 360, 30.0f * size, 2.0f * size); + // dynamic_cast to VolumeDescriptor is legal and will not throw, as PhantomGenerator returns + // a VolumeDescriptor + // setup and run test for fast Joseph's Logger::get("Setup")->info("Fast unmatched Joseph's:\n"); - auto josephsFast = JosephsMethodCUDA(volumeDescriptor, *sinoDescriptor, geom); + auto josephsFast = JosephsMethodCUDA( + dynamic_cast(volumeDescriptor), *sinoDescriptor); testExecutionSpeed(josephsFast, phantom, numIters); // setup and run test for slow Joseph's Logger::get("Setup")->info("Slow matched Joseph's:\n"); - auto josephsSlow = JosephsMethodCUDA(volumeDescriptor, *sinoDescriptor, geom, false); + auto josephsSlow = JosephsMethodCUDA( + dynamic_cast(volumeDescriptor), *sinoDescriptor, false); testExecutionSpeed(josephsSlow, phantom, numIters); // setup and run test for Siddon's Logger::get("Setup")->info("Siddon's:\n"); - auto siddons = SiddonsMethodCUDA(volumeDescriptor, *sinoDescriptor, geom); + auto siddons = SiddonsMethodCUDA(dynamic_cast(volumeDescriptor), + *sinoDescriptor); testExecutionSpeed(siddons, phantom, numIters); } -} \ No newline at end of file +}