In January 2021 we will introduce a 10 GB quota for project repositories. Higher limits for individual projects will be available on request. Please see https://doku.lrz.de/display/PUBLIC/GitLab for more information.

Commit 1a279437 authored by David Frank's avatar David Frank Committed by Tobias Lasser

use inverse projection matrix for ray generation, add benchmarks for ray generation

parent e9e9a882
Pipeline #227802 passed with stages
in 34 minutes and 8 seconds
.idea .idea
.vscode .vscode
cmake-build-*
cmake-build-debug cmake-build-debug
cmake-build-release cmake-build-release
build/ build/
...@@ -119,20 +119,21 @@ if(NOT ELSA_MASTER_PROJECT) ...@@ -119,20 +119,21 @@ if(NOT ELSA_MASTER_PROJECT)
set(ELSA_TESTING OFF) set(ELSA_TESTING OFF)
endif(NOT ELSA_MASTER_PROJECT) endif(NOT ELSA_MASTER_PROJECT)
if(ELSA_TESTING) if(ELSA_TESTING OR ELSA_BENCHMARKS)
message(STATUS "elsa testing is enabled")
enable_testing() enable_testing()
add_subdirectory(thirdparty/Catch2) add_subdirectory(thirdparty/Catch2)
# add the CMake modules for automatic test discovery
set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/thirdparty/Catch2/contrib" ${CMAKE_MODULE_PATH})
if(ELSA_TESTING)
message(STATUS "elsa testing is enabled")
add_custom_target(tests add_custom_target(tests
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Build and run all the tests.") COMMENT "Build and run all the tests.")
# add the CMake modules for automatic test discovery
set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/thirdparty/Catch2/contrib" ${CMAKE_MODULE_PATH})
if(ELSA_COVERAGE) if(ELSA_COVERAGE)
message(STATUS "elsa test coverage is enabled") message(STATUS "elsa test coverage is enabled")
...@@ -144,10 +145,16 @@ if(ELSA_TESTING) ...@@ -144,10 +145,16 @@ if(ELSA_TESTING)
else(ELSA_COVERAGE) else(ELSA_COVERAGE)
message(STATUS "elsa test coverage is disabled") message(STATUS "elsa test coverage is disabled")
endif(ELSA_COVERAGE) endif(ELSA_COVERAGE)
endif(ELSA_TESTING)
if (ELSA_BENCHMARKS)
message(STATUS "elsa benchmarks are enabled")
add_subdirectory(benchmarks)
endif(ELSA_BENCHMARKS)
else(ELSA_TESTING) else(ELSA_TESTING OR ELSA_BENCHMARKS)
message(STATUS " elsa testing is disabled") message(STATUS " elsa testing is disabled")
endif(ELSA_TESTING) endif(ELSA_TESTING OR ELSA_BENCHMARKS)
# ------------ add code/docs ----------- # ------------ add code/docs -----------
......
cmake_minimum_required(VERSION 3.9)
# enable ctest and Catch test discovery
include(CTest)
include(Catch)
macro(ELSA_BENCHMARK NAME)
# Add source to all sources, so we can create a common target to build and run the benchmarks
SET(BENCHMARK_SOURCES ${BENCHMARK_SOURCES} bench_${NAME}.cpp)
# create the test executable
add_executable(bench_${NAME} EXCLUDE_FROM_ALL bench_${NAME}.cpp bench_main.cpp)
# add catch and the corresponding elsa library
target_link_libraries(bench_${NAME} PRIVATE Catch2::Catch2 elsa::all)
# enable C++17
target_compile_features(bench_${NAME} PUBLIC cxx_std_17)
# include helpers
target_include_directories(bench_${NAME} PRIVATE ${CMAKE_SOURCE_DIR}/elsa/test_routines/)
# discover test for CMake and CTest
catch_discover_tests(bench_${NAME} TEST_SPEC "" )
endmacro(ELSA_BENCHMARK)
ELSA_BENCHMARK(RayGenerationBench)
ELSA_BENCHMARK(Projectors)
# Add a single executable for all benchmarks, as CTest removes a lot of the output
add_executable(bench_all EXCLUDE_FROM_ALL bench_main.cpp ${BENCHMARK_SOURCES})
# add catch and the corresponding elsa library
target_link_libraries(bench_all PRIVATE Catch2::Catch2 elsa::all)
# enable C++17
target_compile_features(bench_all PUBLIC cxx_std_17)
# include helpers
target_include_directories(bench_all PRIVATE ${CMAKE_SOURCE_DIR}/elsa/test_routines/)
# Add the custom target to run all the benchmarks
add_custom_target(benchmarks
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/bench_all
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
USES_TERMINAL
COMMENT "Run benchmarks")
add_dependencies(benchmarks bench_all)
\ No newline at end of file
/**
* \file test_RayGenerationBench.cpp
*
* \brief Benchmarks for projectors
*
* \author David Frank
*/
#define CATCH_CONFIG_ENABLE_BENCHMARKING
#include <catch2/catch.hpp>
#include "Logger.h"
#include "elsaDefines.h"
#include "PhantomGenerator.h"
#include "CircleTrajectoryGenerator.h"
#include "SiddonsMethod.h"
#include "JosephsMethod.h"
using namespace elsa;
using DataContainer_t = DataContainer<real_t>;
// Some minor fixtures
template <typename Projector>
void runProjector2D(index_t coeffsPerDim)
{
// generate 2d phantom
IndexVector_t size(2);
size << coeffsPerDim, coeffsPerDim;
auto phantom = DataContainer_t(DataDescriptor(size));
phantom = 0;
// generate circular trajectory
index_t noAngles{180}, arc{360};
auto [geometry, sinoDescriptor] = CircleTrajectoryGenerator::createTrajectory(
noAngles, phantom.getDataDescriptor(), arc, 20, 20);
// setup operator for 2d X-ray transform
Projector projector(phantom.getDataDescriptor(), *sinoDescriptor, geometry);
DataContainer_t sinogram(*sinoDescriptor);
BENCHMARK("Forward projection")
{
sinogram = projector.apply(phantom);
return sinogram;
};
BENCHMARK("Backward projection") { return projector.applyAdjoint(sinogram); };
}
template <typename Projector>
void runProjector3D(index_t coeffsPerDim)
{
// Turn logger off
Logger::setLevel(Logger::LogLevel::OFF);
// generate 2d phantom
IndexVector_t size(3);
size << coeffsPerDim, coeffsPerDim, coeffsPerDim;
auto phantom = DataContainer_t(DataDescriptor(size));
phantom = 0;
// generate circular trajectory
index_t noAngles{180}, arc{360};
auto [geometry, sinoDescriptor] = CircleTrajectoryGenerator::createTrajectory(
noAngles, phantom.getDataDescriptor(), arc, 20, 20);
// setup operator for 2d X-ray transform
Projector projector(phantom.getDataDescriptor(), *sinoDescriptor, geometry);
DataContainer_t sinogram(*sinoDescriptor);
BENCHMARK("Forward projection")
{
sinogram = projector.apply(phantom);
return sinogram;
};
BENCHMARK("Backward projection") { return projector.applyAdjoint(sinogram); };
}
TEST_CASE("Testing Siddon's projector in 2D")
{
// Turn logger off
Logger::setLevel(Logger::LogLevel::OFF);
using Siddon = SiddonsMethod<real_t>;
GIVEN("A 8x8 Problem:") { runProjector2D<Siddon>(8); }
GIVEN("A 16x16 Problem:") { runProjector2D<Siddon>(16); }
GIVEN("A 32x32 Problem:") { runProjector2D<Siddon>(32); }
GIVEN("A 64x64 Problem:") { runProjector2D<Siddon>(64); }
}
TEST_CASE("Testing Siddon's projector in 3D")
{
// Turn logger off
Logger::setLevel(Logger::LogLevel::OFF);
using Siddon = SiddonsMethod<real_t>;
GIVEN("A 8x8x8 Problem:") { runProjector3D<Siddon>(8); }
GIVEN("A 16x16x16 Problem:") { runProjector3D<Siddon>(16); }
GIVEN("A 32x32x32 Problem:") { runProjector3D<Siddon>(32); }
}
TEST_CASE("Testing Joseph's projector in 2D")
{
// Turn logger off
Logger::setLevel(Logger::LogLevel::OFF);
using Joseph = JosephsMethod<real_t>;
GIVEN("A 8x8 Problem:") { runProjector2D<Joseph>(8); }
GIVEN("A 16x16 Problem:") { runProjector2D<Joseph>(16); }
GIVEN("A 32x32 Problem:") { runProjector2D<Joseph>(32); }
GIVEN("A 64x64 Problem:") { runProjector2D<Joseph>(64); }
}
TEST_CASE("Testing Joseph's projector in 3D")
{
// Turn logger off
Logger::setLevel(Logger::LogLevel::OFF);
using Joseph = JosephsMethod<real_t>;
GIVEN("A 8x8x8 Problem:") { runProjector3D<Joseph>(8); }
GIVEN("A 16x16x16 Problem:") { runProjector3D<Joseph>(16); }
GIVEN("A 32x32x32 Problem:") { runProjector3D<Joseph>(32); }
}
\ No newline at end of file
/**
* \file test_RayGenerationBench.cpp
*
* \brief Benchmarks for ray generation
*
* \author David Frank
*/
#define CATCH_CONFIG_ENABLE_BENCHMARKING
#include <catch2/catch.hpp>
#include "Geometry.h"
#include <string>
#include <cstdlib>
using namespace elsa;
static const index_t dimension = 2;
void iterate2D(const Geometry& geo)
{
for (real_t detPixel : std::initializer_list<real_t>{0.5, 2.5, 4.5}) {
RealVector_t pixel(1);
pixel << detPixel;
BENCHMARK("Ray for detector at pixel " + std::to_string(detPixel))
{
return geo.computeRayTo(pixel);
};
}
}
void iterate3D(const Geometry& geo)
{
for (real_t detPixel1 : std::initializer_list<real_t>{0.5, 2.5, 4.5}) {
for (real_t detPixel2 : std::initializer_list<real_t>{0.5, 2.5, 4.5}) {
RealVector_t pixel(2);
pixel << detPixel1, detPixel2;
BENCHMARK("Ray for detector at pixel " + std::to_string(detPixel1) + "/"
+ std::to_string(detPixel2))
{
return geo.computeRayTo(pixel);
};
}
}
}
TEST_CASE("Ray generation for 2D")
{
IndexVector_t volCoeff(2);
volCoeff << 5, 5;
DataDescriptor ddVol(volCoeff);
IndexVector_t detCoeff(1);
detCoeff << 5;
DataDescriptor ddDet(detCoeff);
real_t s2c = 10;
real_t c2d = 4;
GIVEN("Geometry without offset and rotation")
{
Geometry g(s2c, c2d, 0, ddVol, ddDet);
// test outer + central pixels
iterate2D(g);
}
GIVEN("Geometry with offset but no rotation")
{
real_t offset = 2;
Geometry g(s2c, c2d, 0, ddVol, ddDet, offset);
// test outer + central pixels
iterate2D(g);
}
GIVEN("Geometry at 90°, but no offset")
{
real_t angle = pi_t / 2; // 90 degrees
Geometry g(s2c, c2d, angle, ddVol, ddDet);
// test outer + central pixels
iterate2D(g);
}
GIVEN("Geometry at 45° with offset")
{
real_t angle = pi_t / 4; // 45 degrees
real_t cx = -1;
real_t cy = 2;
Geometry g(s2c, c2d, angle, ddVol, ddDet, 0, cx, cy);
// test outer + central pixels
iterate2D(g);
}
}
TEST_CASE("Ray generation for 3D")
{
IndexVector_t volCoeff(3);
volCoeff << 5, 5, 5;
DataDescriptor ddVol(volCoeff);
IndexVector_t detCoeff(2);
detCoeff << 5, 5;
DataDescriptor ddDet(detCoeff);
real_t s2c = 10;
real_t c2d = 4;
GIVEN("Geometry without offset and rotation")
{
Geometry g(s2c, c2d, ddVol, ddDet, 0);
// test outer + central pixels
iterate3D(g);
}
GIVEN("Geometry with offset but no rotation")
{
real_t px = -1;
real_t py = 3;
Geometry g(s2c, c2d, ddVol, ddDet, 0, 0, 0, px, py);
// test outer + central pixels
iterate3D(g);
}
GIVEN("Geometry at 90°, but no offset")
{
real_t angle = pi_t / 2;
Geometry g(s2c, c2d, ddVol, ddDet, angle);
// test outer + central pixels
iterate3D(g);
}
GIVEN("Geometry at 45°/22.5 with offset")
{
real_t angle1 = pi_t / 4;
real_t angle2 = pi_t / 2;
RealVector_t offset(3);
offset << 1, -2, -1;
Geometry g(s2c, c2d, ddVol, ddDet, angle1, angle2, 0, 0, 0, offset[0], offset[1],
offset[2]);
// test outer + central pixels
iterate3D(g);
}
GIVEN("Geometry at 45/22.5/12.25 with offset")
{
real_t angle1 = pi_t / 4;
real_t angle2 = pi_t / 2;
real_t angle3 = pi_t / 8;
RealMatrix_t rot1(3, 3);
rot1 << std::cos(angle1), 0, std::sin(angle1), 0, 1, 0, -std::sin(angle1), 0,
std::cos(angle1);
RealMatrix_t rot2(3, 3);
rot2 << std::cos(angle2), -std::sin(angle2), 0, std::sin(angle2), std::cos(angle2), 0, 0, 0,
1;
RealMatrix_t rot3(3, 3);
rot3 << std::cos(angle3), 0, std::sin(angle3), 0, 1, 0, -std::sin(angle3), 0,
std::cos(angle3);
RealMatrix_t R = rot1 * rot2 * rot3;
Geometry g(s2c, c2d, ddVol, ddDet, R);
// test outer + central pixels
iterate3D(g);
}
}
\ No newline at end of file
#define CATCH_CONFIG_RUNNER
#define CATCH_CONFIG_ENABLE_BENCHMARKING
#include <catch2/catch.hpp>
int main(int argc, char* argv[])
{
Catch::Session session; // There must be exactly one instance
// writing to session.configData() here sets defaults
// this is the preferred way to set them
// Get binary name
std::string executable_path = argv[0];
// Find name or executable (without filesystem as GCC 7.4 doesn't support it)
// find last of / or \ (depending on linux or windows systems)
auto found = executable_path.find_last_of("/\\");
std::string filename = executable_path.substr(found + 1);
// set reporter and filename to match binary
session.configData().reporterName = "console";
// session.configData().outputFilename = filename + ".xml";
int returnCode = session.applyCommandLine(argc, argv);
if (returnCode != 0) // Indicates a command line error
return returnCode;
// writing to session.configData() or session.Config() here
// overrides command line args
// only do this if you know you need to
int numFailed = session.run();
// numFailed is clamped to 255 as some unices only use the lower 8 bits.
// This clamping has already been applied, so just return it here
// You can also do any post run clean-up here
return numFailed;
}
...@@ -13,7 +13,7 @@ macro(ELSA_TEST NAME) ...@@ -13,7 +13,7 @@ macro(ELSA_TEST NAME)
# create the test executable # create the test executable
add_executable(test_${NAME} EXCLUDE_FROM_ALL test_${NAME}.cpp test_main.cpp) add_executable(test_${NAME} EXCLUDE_FROM_ALL test_${NAME}.cpp test_main.cpp)
# add catch and the corresponding elsa library # add catch and the corresponding elsa library
target_link_libraries(test_${NAME} PRIVATE Catch2::Catch2 ${ELSA_MODULE_TARGET_NAME}) target_link_libraries(test_${NAME} PRIVATE Catch2::Catch2 ${ELSA_MODULE_TARGET_NAME} elsa::test_routines)
# enable C++17 # enable C++17
target_compile_features(test_${NAME} PUBLIC cxx_std_17) target_compile_features(test_${NAME} PUBLIC cxx_std_17)
# include helpers # include helpers
...@@ -50,6 +50,7 @@ if(ELSA_BUILD_CUDA_PROJECTORS) ...@@ -50,6 +50,7 @@ if(ELSA_BUILD_CUDA_PROJECTORS)
add_subdirectory(projectors_cuda) add_subdirectory(projectors_cuda)
endif(ELSA_BUILD_CUDA_PROJECTORS) endif(ELSA_BUILD_CUDA_PROJECTORS)
add_subdirectory(generators) add_subdirectory(generators)
add_subdirectory(test_routines)
# library to build and add all registered components of the library # library to build and add all registered components of the library
......
...@@ -148,14 +148,9 @@ namespace elsa ...@@ -148,14 +148,9 @@ namespace elsa
RealVector_t homP(_objectDimension); RealVector_t homP(_objectDimension);
homP << p, 1; homP << p, 1;
// solve for ray direction // multiplication of inverse projection matrix and homogeneous detector coordinate
RealVector_t rd = (_P.block(0, 0, _objectDimension, _objectDimension)) auto rd = (_Pinv * homP).normalized();
.colPivHouseholderQr() return std::make_pair(ro, rd.head(_objectDimension));
.solve(homP)
.normalized();
rd.normalize();
return std::make_pair(ro, rd);
} }
const RealMatrix_t& Geometry::getProjectionMatrix() const { return _P; } const RealMatrix_t& Geometry::getProjectionMatrix() const { return _P; }
......
This diff is collapsed.
...@@ -129,8 +129,9 @@ SCENARIO("Testing 2D geometries") ...@@ -129,8 +129,9 @@ SCENARIO("Testing 2D geometries")
auto c = g.getCameraCenter(); auto c = g.getCameraCenter();
REQUIRE((ro - c).sum() == Approx(0)); REQUIRE((ro - c).sum() == Approx(0));
real_t factor = real_t factor = (std::abs(rd[0]) >= Approx(0.001))
(std::abs(rd[0]) > 0) ? ((pixel[0] - ro[0] - px) / rd[0]) : (s2c + c2d); ? ((pixel[0] - ro[0] - px) / rd[0])
: (s2c + c2d);
real_t detCoordY = ro[1] + factor * rd[1]; real_t detCoordY = ro[1] + factor * rd[1];
REQUIRE(detCoordY == Approx(ddVol.getLocationOfOrigin()[1] + c2d)); REQUIRE(detCoordY == Approx(ddVol.getLocationOfOrigin()[1] + c2d));
} }
......
cmake_minimum_required(VERSION 3.9) cmake_minimum_required(VERSION 3.9)
# set the name of the module
set(ELSA_MODULE_NAME test_routines)
set(ELSA_MODULE_TARGET_NAME elsa_test_routines)
set(ELSA_MODULE_EXPORT_TARGET elsa_${ELSA_MODULE_NAME}Targets)
# list all the headers of the module
set(MODULE_HEADERS
testHelpers.h)
# list all the code files of the module
set(MODULE_SOURCES
)
# build the module library
add_library(${ELSA_MODULE_TARGET_NAME} ${MODULE_HEADERS} ${MODULE_SOURCES})
add_library(elsa::${ELSA_MODULE_NAME} ALIAS ${ELSA_MODULE_TARGET_NAME})
target_link_libraries(${ELSA_MODULE_TARGET_NAME} PUBLIC elsa_core elsa_logging)
target_include_directories(${ELSA_MODULE_TARGET_NAME}
PUBLIC
$<INSTALL_INTERFACE:include/elsa/${ELSA_MODULE_NAME}>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
)
# require C++17
target_compile_features(${ELSA_MODULE_TARGET_NAME} PUBLIC cxx_std_17)
# set -fPIC
set_target_properties(${ELSA_MODULE_TARGET_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON)
# This is only used privately, so don't bother exporting it and installing it
\ No newline at end of file
...@@ -5,26 +5,38 @@ ...@@ -5,26 +5,38 @@
#include <random> #include <random>
#include "elsaDefines.h" #include "elsaDefines.h"
/** #include <iomanip>
#include <limits>
#include <cassert>
namespace elsa
{
/**
* \brief Epsilon value for our test suit
*/
static constexpr elsa::real_t epsilon = static_cast<elsa::real_t>(0.0001);
/**
* \brief comparing two number types for approximate equality for complex and regular number * \brief comparing two number types for approximate equality for complex and regular number
* *
* \tparam T - arithmetic data type * \tparam T - arithmetic data type
* \return true if same number * \return true if same number
* *
* Use example in test case: REQUIRE(checkSameNumbers(a, b)); * Use example in test case: REQUIRE(checkSameNumbers(a, b));
* The CHECK(...) assertion in the function ensures that the values are reported when the test fails * The CHECK(...) assertion in the function ensures that the values are reported when the test
* fails
*/ */
template <typename T> template <typename T>
bool checkSameNumbers(T left, T right, int epsilonFactor = 1) bool checkSameNumbers(T left, T right, int epsilonFactor = 1)
{ {
using numericalBaseType = elsa::GetFloatingPointType_t<T>; using numericalBaseType = elsa::GetFloatingPointType_t<T>;
numericalBaseType eps = std::numeric_limits<numericalBaseType>::epsilon() numericalBaseType eps = std::numeric_limits<numericalBaseType>::epsilon()
* static_cast<numericalBaseType>(epsilonFactor) * static_cast<numericalBaseType>(epsilonFactor)
* static_cast<numericalBaseType>(100); * static_cast<numericalBaseType>(100);
if constexpr (std::is_same_v<T, if constexpr (std::is_same_v<