11.08., 9:00 - 11:00: Due to updates GitLab will be unavailable for some minutes between 09:00 and 11:00.

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; }
......
This diff is collapsed.
......@@ -129,8 +129,9 @@ SCENARIO("Testing 2D geometries")
auto c = g.getCameraCenter();
REQUIRE((ro - c).sum() == Approx(0));
real_t factor =
(std::abs(rd[0]) > 0) ? ((pixel[0] - ro[0] - px) / rd[0]) : (s2c + c2d);
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));
}
......
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 @@
#include <random>
#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
*
* \tparam T - arithmetic data type
* \return true if same number
*
* 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>
bool checkSameNumbers(T left, T right, int epsilonFactor = 1)
{
template <typename T>
bool checkSameNumbers(T left, T right, int epsilonFactor = 1)
{
using numericalBaseType = elsa::GetFloatingPointType_t<T>;
numericalBaseType eps = std::numeric_limits<numericalBaseType>::epsilon()
* static_cast<numericalBaseType>(epsilonFactor)
* static_cast<numericalBaseType>(100);
if constexpr (std::is_same_v<T,
std::complex<float>> || std::is_same_v<T, std::complex<double>>) {
if constexpr (std::is_same_v<
T, std::complex<float>> || std::is_same_v<T, std::complex<double>>) {
CHECK(Approx(left.real()).epsilon(eps) == right.real());
CHECK(Approx(left.imag()).epsilon(eps) == right.imag());
return Approx(left.real()).epsilon(eps) == right.real()
......@@ -33,22 +45,22 @@ bool checkSameNumbers(T left, T right, int epsilonFactor = 1)
CHECK(Approx(left).epsilon(eps) == right);
return Approx(left).epsilon(eps) == right;
}
}
}
/**
* \brief Generates a random Eigen matrix for different data_t types with integer values limited to
* a certain range
/**
* \brief Generates a random Eigen matrix for different data_t types with integer values limited
* to a certain range
*
* \param[in] size the number of elements in the vector like matrix
*
* \tparam data_t the numerical type to use
*
* The integer range is chosen to be small, to allow multiplication with the values without running
* into overflow issues.
* The integer range is chosen to be small, to allow multiplication with the values without
* running into overflow issues.
*/
template <typename data_t>
auto generateRandomMatrix(elsa::index_t size)
{
template <typename data_t>
auto generateRandomMatrix(elsa::index_t size)
{
Eigen::Matrix<data_t, Eigen::Dynamic, 1> randVec(size);
if constexpr (std::is_integral_v<data_t>) {
......@@ -68,4 +80,32 @@ auto generateRandomMatrix(elsa::index_t size)
}
return randVec;
}
}
/**
* \brief Compares two DataContainers using their norm. Computes \f$ \sqrt{\| x - y \|_{2}^2}
* \f$ and compares it to \f$ prec * \sqrt{min(\| x \|_{2}^2, \| y \|_{2}^2)} \f$. If the first
* is smaller or equal to the second, we can assume the vectors are approximate equal
*
* @tparam data_t Value type of DataContainers
* @param x First DataContainer
* @param y Second DataContainer
* @param prec Precision to compare, the smaller the closer both have to be
* @return true if the norms of the containers is approximate equal
*/
template <typename data_t>
bool isApprox(const elsa::DataContainer<data_t>& x, const elsa::DataContainer<data_t>& y,
real_t prec = Eigen::NumTraits<real_t>::dummy_precision())
{
// Check size is the same, don't throw an expection, as it is a programming error to pass
// containers with different size
assert(x.getSize() == y.getSize());
DataContainer<data_t> z = x;
z -= y;
data_t lhs = std::sqrt(z.squaredL2Norm());
data_t rhs = prec * std::sqrt(std::min(x.squaredL2Norm(), y.squaredL2Norm()));
return lhs <= rhs;
}
} // namespace elsa
......@@ -22,6 +22,7 @@ echo
# perform clang-format on all cpp-files
find elsa/ -name '*.h' -or -name '*.hpp' -or -name '*.cpp' -or -name '*.cu' -or -name '*.cuh' | xargs clang-format-8 -i -style=file $1
find benchmarks/ -name '*.h' -or -name '*.hpp' -or -name '*.cpp' -or -name '*.cu' -or -name '*.cuh' | xargs clang-format-8 -i -style=file $1
# check if something was modified
notcorrectlist=`git status --porcelain | grep '^ M' | cut -c4-`
......@@ -36,6 +37,7 @@ else
echo "Please run"
echo
echo "find elsa/ -name '*.h' -or -name '*.hpp' -or -name '*.cpp' -or -name '*.cu' -or -name '*.cuh' | xargs clang-format-8 -i -style=file $1"
echo "find benchmarks/ -name '*.h' -or -name '*.hpp' -or -name '*.cpp' -or -name '*.cu' -or -name '*.cuh' | xargs clang-format -i -style=file $1"
echo
echo "to solve the issue."
# cleanup changes in git
......
......@@ -5,7 +5,7 @@ exit_flag=false
# for compilation database
mkdir -p build
cd build
cmake .. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DELSA_CUDA_VECTOR=ON
cmake .. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DELSA_CUDA_VECTOR=ON -DELSA_BENCHMARKS=ON
cd ..
target_branch="master"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment