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
.vscode
cmake-build-*
cmake-build-debug
cmake-build-release
build/
......@@ -119,20 +119,21 @@ if(NOT ELSA_MASTER_PROJECT)
set(ELSA_TESTING OFF)
endif(NOT ELSA_MASTER_PROJECT)
if(ELSA_TESTING)
message(STATUS "elsa testing is enabled")
if(ELSA_TESTING OR ELSA_BENCHMARKS)
enable_testing()
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
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
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)
message(STATUS "elsa test coverage is enabled")
......@@ -144,10 +145,16 @@ if(ELSA_TESTING)
else(ELSA_COVERAGE)
message(STATUS "elsa test coverage is disabled")
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")
endif(ELSA_TESTING)
endif(ELSA_TESTING OR ELSA_BENCHMARKS)
# ------------ 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)
# create the test executable
add_executable(test_${NAME} EXCLUDE_FROM_ALL test_${NAME}.cpp test_main.cpp)
# 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
target_compile_features(test_${NAME} PUBLIC cxx_std_17)
# include helpers
......@@ -50,6 +50,7 @@ if(ELSA_BUILD_CUDA_PROJECTORS)
add_subdirectory(projectors_cuda)
endif(ELSA_BUILD_CUDA_PROJECTORS)
add_subdirectory(generators)
add_subdirectory(test_routines)
# library to build and add all registered components of the library
......
......@@ -148,14 +148,9 @@ namespace elsa
RealVector_t homP(_objectDimension);
homP << p, 1;
// solve for ray direction
RealVector_t rd = (_P.block(0, 0, _objectDimension, _objectDimension))
.colPivHouseholderQr()
.solve(homP)
.normalized();
rd.normalize();
return std::make_pair(ro, rd);
// 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; }
......
......@@ -10,11 +10,16 @@
#include <catch2/catch.hpp>
#include "BinaryMethod.h"
#include "Logger.h"
#include "testHelpers.h"
using namespace elsa;
SCENARIO("Testing BinaryMethod with only one ray")
{
// Turn logger of
Logger::setLevel(Logger::LogLevel::OFF);
IndexVector_t sizeDomain(2);
sizeDomain << 5, 5;
......@@ -142,7 +147,7 @@ SCENARIO("Testing BinaryMethod with only one ray")
op.applyAdjoint(dataRange, AtAx);
auto cmp = RealVector_t(sizeDomain.prod());
cmp << 9, 9, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 9;
cmp << 9, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 9, 9;
DataContainer tmpCmp(domain, cmp);
REQUIRE(tmpCmp == AtAx);
......@@ -168,7 +173,7 @@ SCENARIO("Testing BinaryMethod with only one ray")
// std::cout << AtAx.getData().format(CommaInitFmt) << "\n\n";
auto cmp = RealVector_t(sizeDomain.prod());
cmp << 9, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 9, 9;
cmp << 9, 9, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 9, 9, 0, 0, 0, 0, 9;
DataContainer tmpCmp(domain, cmp);
REQUIRE(tmpCmp == AtAx);
......@@ -179,6 +184,9 @@ SCENARIO("Testing BinaryMethod with only one ray")
SCENARIO("Testing BinaryMethod with only 1 rays for 4 angles")
{
// Turn logger of
Logger::setLevel(Logger::LogLevel::OFF);
IndexVector_t sizeDomain(2);
sizeDomain << 5, 5;
......@@ -251,6 +259,9 @@ SCENARIO("Testing BinaryMethod with only 1 rays for 4 angles")
SCENARIO("Testing BinaryMethod")
{
// Turn logger of
Logger::setLevel(Logger::LogLevel::OFF);
IndexVector_t sizeDomain(2);
sizeDomain << 5, 5;
......@@ -422,6 +433,9 @@ SCENARIO("Testing BinaryMethod")
SCENARIO("Calls to functions of super class")
{
// Turn logger of
Logger::setLevel(Logger::LogLevel::OFF);
GIVEN("A projector")
{
IndexVector_t volumeDims(2), sinoDims(2);
......@@ -471,6 +485,9 @@ SCENARIO("Calls to functions of super class")
SCENARIO("Output DataContainer is not zero initialized")
{
// Turn logger of
Logger::setLevel(Logger::LogLevel::OFF);
GIVEN("A 2D setting")
{
IndexVector_t volumeDims(2), sinoDims(2);
......@@ -574,6 +591,9 @@ SCENARIO("Output DataContainer is not zero initialized")
SCENARIO("Rays not intersecting the bounding box are present")
{
// Turn logger of
Logger::setLevel(Logger::LogLevel::OFF);
GIVEN("A 2D setting")
{
IndexVector_t volumeDims(2), sinoDims(2);
......@@ -762,6 +782,9 @@ SCENARIO("Rays not intersecting the bounding box are present")
SCENARIO("Axis-aligned rays are present")
{
// Turn logger of
Logger::setLevel(Logger::LogLevel::OFF);
GIVEN("A 2D setting with a single ray")
{
IndexVector_t volumeDims(2), sinoDims(2);
......@@ -1227,6 +1250,9 @@ SCENARIO("Axis-aligned rays are present")
SCENARIO("Projection under an angle")
{
// Turn logger of
Logger::setLevel(Logger::LogLevel::OFF);
GIVEN("A 2D setting with a single ray")
{
IndexVector_t volumeDims(2), sinoDims(2);
......@@ -1252,61 +1278,49 @@ SCENARIO("Projection under an angle")
THEN("Ray intersects the correct pixels")
{
/* Our volume: with left top being (0, 0) and bottom right (3,3)
0, 0, 1, 1,
0, 1, 1, 0,
0, 1, 0, 0,
1, 1, 0, 0
*/
volume = 1;
IndexVector_t coord(2);
coord << 3, 0;
volume[volumeDescriptor.getIndexFromCoordinate(coord)] = 0;
coord << 2, 0;
volume[volumeDescriptor.getIndexFromCoordinate(coord)] = 0;
coord << 2, 1;
volume[volumeDescriptor.getIndexFromCoordinate(coord)] = 0;
// volume(2,2) hit because of numerical errors during traversal
coord << 2, 2;
volume[volumeDescriptor.getIndexFromCoordinate(coord)] = 0;
coord << 1, 2;
volume[volumeDescriptor.getIndexFromCoordinate(coord)] = 0;
coord << 1, 3;
volume[volumeDescriptor.getIndexFromCoordinate(coord)] = 0;
coord << 0, 3;
volume[volumeDescriptor.getIndexFromCoordinate(coord)] = 0;
// volume(2,2 is possibly also hit due to numerical errors)
volume(3, 0) = 0;
volume(2, 0) = 0;
volume(2, 1) = 0;
volume(1, 1) = 0;
volume(1, 2) = 0;
volume(1, 3) = 0;
volume(0, 3) = 0;