diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index d5e65c32fb..25ffd88540 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -330,7 +330,7 @@ jobs: EXTRA_BUILD_FLAGS="${EXTRA_BUILD_FLAGS} -DBUILD_SWIG_PYTHON=ON -DPython_EXECUTABLE=`which python`" EXTRA_BUILD_FLAGS="${EXTRA_BUILD_FLAGS} -DCMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX} -DCMAKE_BUILD_TYPE=${BUILD_TYPE}" EXTRA_BUILD_FLAGS="${EXTRA_BUILD_FLAGS} -DDOWNLOAD_ZENODO_TEST_DATA=ON" - EXTRA_BUILD_FLAGS="${EXTRA_BUILD_FLAGS} -DDISABLE_STIR_LOCAL=OFF -DSTIR_LOCAL=${GITHUB_WORKSPACE}/examples/C++/using_STIR_LOCAL" + EXTRA_BUILD_FLAGS="${EXTRA_BUILD_FLAGS} -DDISABLE_PETSIRD:BOOL=ON -DDISABLE_STIR_LOCAL=OFF -DSTIR_LOCAL=${GITHUB_WORKSPACE}/examples/C++/using_STIR_LOCAL" echo "cmake flags $BUILD_FLAGS $EXTRA_BUILD_FLAGS" mkdir build cd build diff --git a/.gitmodules b/.gitmodules index 5d1ac04565..ec9680161d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "external_helpers/fmt"] path = external_helpers/fmt url = https://github.com/fmtlib/fmt.git +[submodule "PETSIRD"] + path = PETSIRD + url = https://github.com/ETSInitiative/PETSIRD.git diff --git a/.travis.yml b/.travis.yml index 3d6b75b578..9ad59d8f03 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,7 +35,7 @@ matrix: packages: - *addons_apt_packages - [g++-6] - env: EXTRA_BUILD_FLAGS="-DDISABLE_CERN_ROOT=1 -DSTIR_OPENMP:BOOL=OFF" CC=gcc-6 CXX=g++-6 + env: EXTRA_BUILD_FLAGS="-DDISABLE_CERN_ROOT=1 -DSTIR_OPENMP:BOOL=OFF -DDISABLE_PETSIRD:BOOL=ON" CC=gcc-6 CXX=g++-6 - os: linux python: 3 addons: @@ -43,7 +43,7 @@ matrix: packages: - *addons_apt_packages - [g++-5, libinsighttoolkit4-dev] - env: EXTRA_BUILD_FLAGS="-DDISABLE_CERN_ROOT=1 -DSTIR_OPENMP:BOOL=OFF -DDISABLE_HDF5=ON" CC=gcc-5 CXX=g++-5 + env: EXTRA_BUILD_FLAGS="-DDISABLE_CERN_ROOT=1 -DSTIR_OPENMP:BOOL=OFF -DDISABLE_HDF5=ON -DDISABLE_PETSIRD:BOOL=ON" CC=gcc-5 CXX=g++-5 - os: linux python: 3 addons: @@ -51,7 +51,7 @@ matrix: packages: - *addons_apt_packages - [g++-7, libinsighttoolkit4-dev] - env: EXTRA_BUILD_FLAGS="-DDISABLE_CERN_ROOT=0 -DSTIR_OPENMP:BOOL=ON" CC=gcc-7 CXX=g++-7 + env: EXTRA_BUILD_FLAGS="-DDISABLE_CERN_ROOT=0 -DSTIR_OPENMP:BOOL=ON -DDISABLE_PETSIRD:BOOL=ON" CC=gcc-7 CXX=g++-7 - os: linux dist: focal python: 3 @@ -60,7 +60,7 @@ matrix: packages: - *addons_apt_packages - [g++-9, libinsighttoolkit4-dev, nlohmann-json3-dev] - env: EXTRA_BUILD_FLAGS="-DDISABLE_CERN_ROOT=0 -DSTIR_OPENMP:BOOL=ON" CC=gcc-9 CXX=g++-9 + env: EXTRA_BUILD_FLAGS="-DDISABLE_CERN_ROOT=0 -DSTIR_OPENMP:BOOL=ON -DDISABLE_PETSIRD:BOOL=ON" CC=gcc-9 CXX=g++-9 - os: linux dist: focal python: 3 @@ -69,7 +69,7 @@ matrix: packages: - *addons_apt_packages - [g++-10, nlohmann-json3-dev] - env: EXTRA_BUILD_FLAGS="-DDISABLE_CERN_ROOT=1 -DSTIR_OPENMP:BOOL=ON" CC=gcc-10 CXX=g++-10 + env: EXTRA_BUILD_FLAGS="-DDISABLE_CERN_ROOT=1 -DSTIR_OPENMP:BOOL=ON -DDISABLE_PETSIRD:BOOL=ON" CC=gcc-10 CXX=g++-10 - os: linux # note: can't get it to install on focal due to a package problem dist: bionic @@ -79,14 +79,14 @@ matrix: packages: - *addons_apt_packages - [clang, libomp-dev] - env: EXTRA_BUILD_FLAGS="-DDISABLE_CERN_ROOT=1 -DSTIR_OPENMP:BOOL=ON" CC=clang CXX=clang++ + env: EXTRA_BUILD_FLAGS="-DDISABLE_CERN_ROOT=1 -DSTIR_OPENMP:BOOL=ON -DDISABLE_PETSIRD:BOOL=ON" CC=clang CXX=clang++ #### osx # note: cannot use OpenMP on OSX yet, see https://github.com/UCL/STIR/issues/117 - os: osx osx_image: xcode12.2 python: 3 - env: EXTRA_BUILD_FLAGS="-DDISABLE_CERN_ROOT=1 -DSTIR_OPENMP:BOOL=OFF" CC=gcc CXX=g++ + env: EXTRA_BUILD_FLAGS="-DDISABLE_CERN_ROOT=1 -DSTIR_OPENMP:BOOL=OFF -DDISABLE_PETSIRD:BOOL=ON" CC=gcc CXX=g++ # Disable as ROOT is currently failing via brew (as it wants to build it, and which causes a timeout) #- os: osx # python: 3 @@ -94,7 +94,7 @@ matrix: - os: osx osx_image: xcode11.3 python: 3 - env: EXTRA_BUILD_FLAGS="-DDISABLE_CERN_ROOT=1 -DSTIR_OPENMP:BOOL=OFF -DSTIR_ENABLE_EXPERIMENTAL:BOOL=ON" CC=clang CXX=clang++ + env: EXTRA_BUILD_FLAGS="-DDISABLE_CERN_ROOT=1 -DSTIR_OPENMP:BOOL=OFF -DSTIR_ENABLE_EXPERIMENTAL:BOOL=ON -DDISABLE_PETSIRD:BOOL=ON" CC=clang CXX=clang++ # osx, OpenMP using llvm clang # disable due to LLVM clang 12.0.1 problem @@ -105,7 +105,7 @@ matrix: env: global: - - BUILD_FLAGS="-DBUILD_SWIG_PYTHON:BOOL=On -DSTIR_MPI:BOOL=Off -DCMAKE_BUILD_TYPE=Release" + - BUILD_FLAGS="-DBUILD_SWIG_PYTHON:BOOL=On -DSTIR_MPI:BOOL=Off -DDISABLE_PETSIRD:BOOL=ON -DCMAKE_BUILD_TYPE=Release" # No need for sudo sudo: false diff --git a/CMakeLists.txt b/CMakeLists.txt index 73d5c2c3e2..a8c76ab913 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,32 +92,33 @@ find_package( Boost 1.36.0 REQUIRED ) #### optional external libraries. # Listed here such that we know if we should compile extra utilities -option(DISABLE_LLN_MATRIX "disable use of LLN library" OFF) -option(DISABLE_ITK "disable use of ITK library" OFF) -option(DISABLE_HDF5 "disable use of HDF5 libraries" OFF) -option(DISABLE_STIR_LOCAL "disable use of LOCAL extensions to STIR" OFF) -option(DISABLE_CERN_ROOT "disable use of Cern ROOT libraries" OFF) -option(DISABLE_NLOHMANN_JSON "disable use of nlohmann JSON libraries" OFF) -option(STIR_ENABLE_EXPERIMENTAL "disable use of STIR experimental code" OFF) # disable by default -option(DISABLE_NiftyPET_PROJECTOR "disable use of NiftyPET projector" OFF) -option(DISABLE_Parallelproj_PROJECTOR "disable use of Parallelproj projector" OFF) +option(DISABLE_LLN_MATRIX "disable use of LLN library" ON) +option(DISABLE_ITK "disable use of ITK library" ON) +option(DISABLE_HDF5 "disable use of HDF5 libraries" ON) +option(DISABLE_STIR_LOCAL "disable use of LOCAL extensions to STIR" ON) +option(DISABLE_CERN_ROOT "disable use of Cern ROOT libraries" ON) +option(DISABLE_NLOHMANN_JSON "disable use of nlohmann JSON libraries" ON) +option(STIR_ENABLE_EXPERIMENTAL "disable use of STIR experimental code" ON) # disable by default +option(DISABLE_NiftyPET_PROJECTOR "disable use of NiftyPET projector" ON) +option(DISABLE_Parallelproj_PROJECTOR "disable use of Parallelproj projector" ON) OPTION(DOWNLOAD_ZENODO_TEST_DATA "download zenodo data for tests" OFF) -option(DISABLE_UPENN "disable use of UPENN filetypes" OFF) +option(DISABLE_UPENN "disable use of UPENN filetypes" ON) +option(DISABLE_PETSIRD "disable use of PETSIRD filetypes" OFF) find_package(Git QUIET) -if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git") -# Update submodules as needed - option(GIT_SUBMODULE "Check submodules during build" ON) - if(GIT_SUBMODULE) - message(STATUS "Submodule update") - execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - RESULT_VARIABLE GIT_SUBMOD_RESULT) - if(NOT GIT_SUBMOD_RESULT EQUAL "0") - message(FATAL_ERROR "git submodule update --init --recursive failed with ${GIT_SUBMOD_RESULT}, please checkout submodules") - endif() - endif() -endif() +# if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git") +# # Update submodules as needed +# option(GIT_SUBMODULE "Check submodules during build" ON) +# if(GIT_SUBMODULE) +# message(STATUS "Submodule update") +# execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive +# WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +# RESULT_VARIABLE GIT_SUBMOD_RESULT) +# if(NOT GIT_SUBMOD_RESULT EQUAL "0") +# message(FATAL_ERROR "git submodule update --init --recursive failed with ${GIT_SUBMOD_RESULT}, please checkout submodules") +# endif() +# endif() +# endif() if(NOT EXISTS "${PROJECT_SOURCE_DIR}/external_helpers/fmt/CMakeLists.txt") message(FATAL_ERROR "The {fmt} submodule was not downloaded! GIT_SUBMODULE was turned off or failed. Please update submodules and try again.") @@ -259,6 +260,58 @@ else() message(STATUS "Parallelproj projector support disabled or not available.") endif() +if(NOT DISABLE_PETSIRD) + if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git") + # Initialize PETSIRD submodule if not present + if(NOT EXISTS "${PROJECT_SOURCE_DIR}/PETSIRD/CMakeLists.txt") + message(STATUS "Initializing PETSIRD submodule...") + execute_process( + COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive PETSIRD + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + RESULT_VARIABLE GIT_SUBMOD_RESULT + ) + if(NOT GIT_SUBMOD_RESULT EQUAL 0) + message(FATAL_ERROR "Failed to initialize PETSIRD submodule") + endif() + endif() + endif() + + # Check if PETSIRD directory exists + if(EXISTS "${PROJECT_SOURCE_DIR}/PETSIRD/cpp/CMakeLists.txt") + set(PETSIRD_CPP_DIR ${PROJECT_SOURCE_DIR}/PETSIRD/cpp) + set(PETSIRD_GENERATED_DIR ${PETSIRD_CPP_DIR}/generated/petsird) + + find_program(YARDL_EXECUTABLE yardl) + if(NOT YARDL_EXECUTABLE) + message(FATAL_ERROR "yardl not found but required for PETSIRD") + endif() + + if(NOT EXISTS "${PETSIRD_GENERATED_DIR}/CMakeLists.txt") + message(STATUS "Running yardl for PETSIRD...") + execute_process( + COMMAND just build + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/PETSIRD + ) + if(NOT YARDL_RESULT EQUAL 0) + message(FATAL_ERROR "yardl generation failed") + endif() + endif() + set(HAVE_PETSIRD TRUE) + set(PETSIRD_dir ${PROJECT_SOURCE_DIR}/PETSIRD/cpp) + add_subdirectory(${PETSIRD_dir} PETSIRD) + # Make PETSIRD headers globally available + include_directories(${PROJECT_SOURCE_DIR}/PETSIRD/cpp/generated) + include_directories(${PROJECT_SOURCE_DIR}/PETSIRD/cpp/helpers/include) + install(TARGETS petsird_generated + EXPORT STIRTargets + DESTINATION lib) + message(STATUS "PETSIRD support enabled") + else() + message(FATAL_ERROR "PETSIRD directory not found") + endif() + +endif() + #### enable support for ctest ENABLE_TESTING() diff --git a/PETSIRD b/PETSIRD new file mode 160000 index 0000000000..640644a687 --- /dev/null +++ b/PETSIRD @@ -0,0 +1 @@ +Subproject commit 640644a687924147c798792984af727bcb97d2f1 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0020d27d12..7496c50bb5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -254,6 +254,8 @@ endif() # Warning: dependencies for object libraries are not transitive! add_library(stir_registries OBJECT ${STIR_REGISTRIES}) target_link_libraries(stir_registries PRIVATE fmt) +#target_link_libraries(stir_registries PRIVATE petsird) + # TODO, really should use stir_libs.cmake target_include_directories(stir_registries PRIVATE ${STIR_INCLUDE_DIR}) target_include_directories(stir_registries PRIVATE ${Boost_INCLUDE_DIR}) diff --git a/src/IO/CMakeLists.txt b/src/IO/CMakeLists.txt index c40da974a5..987330d679 100644 --- a/src/IO/CMakeLists.txt +++ b/src/IO/CMakeLists.txt @@ -79,10 +79,17 @@ if (HAVE_HDF5) ) endif() +if (HAVE_PETSIRD) +list(APPEND ${dir_LIB_SOURCES} + PETSIRDCListmodeInputFileFormat.cxx +) +endif() + endif() # MINI_STIR include(stir_lib_target) + target_link_libraries(IO PRIVATE fmt) if (LLN_FOUND) @@ -136,3 +143,10 @@ if (NOT MINI_STIR) target_link_libraries(IO PUBLIC modelling_buildblock ) target_link_libraries(IO PUBLIC listmode_buildblock) endif() + +if(HAVE_PETSIRD) + target_link_libraries(IO PUBLIC petsird_generated) +endif() + + + diff --git a/src/IO/IO_registries.cxx b/src/IO/IO_registries.cxx index 242a42113c..814b7e875b 100644 --- a/src/IO/IO_registries.cxx +++ b/src/IO/IO_registries.cxx @@ -66,6 +66,10 @@ # include "stir/IO/InputStreamFromROOTFileForECATPET.h" # endif +# ifdef HAVE_PETSIRD +# include "stir/IO/PETSIRDCListmodeInputFileFormat.h" +# endif + # ifdef HAVE_UPENN # include "stir/IO/PENNListmodeInputFileFormat.h" # include "stir/IO/InputStreamWithRecordsFromUPENNbin.h" @@ -156,5 +160,9 @@ static InputStreamWithRecordsFromUPENNtxt::RegisterIt dummy686062; // static RegisterInputFileFormat idummy1(2); # endif +# ifdef HAVE_PETSIRD +static RegisterInputFileFormat LMdummyPETSIRD(10); +# endif + #endif // MINI_STIR END_NAMESPACE_STIR diff --git a/src/IO/PETSIRDCListmodeInputFileFormat.cxx b/src/IO/PETSIRDCListmodeInputFileFormat.cxx new file mode 100644 index 0000000000..cd187be14f --- /dev/null +++ b/src/IO/PETSIRDCListmodeInputFileFormat.cxx @@ -0,0 +1,85 @@ +/* PETSIRDCListmodeInputFileFormat.h + + Class defining input file format for coincidence listmode data for PETSIRD. + + Copyright 2025, UMCG + Copyright 2025 National Physical Laboratory + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +/*! + + \file + \ingroup listmode + \brief Declaration of class stir::PETSIRDCListmodeInputFileFormat + + \author Nikos Efthimiou + \author Daniel Deidda + +*/ + +#include "stir/IO/PETSIRDCListmodeInputFileFormat.h" +#include "petsird/binary/protocols.h" +#include "petsird/hdf5/protocols.h" +// #include "../../PETSIRD/cpp/generated/types.h" +// #include "../../PETSIRD/cpp/helpers/include/petsird_helpers.h" + +START_NAMESPACE_STIR + +bool +PETSIRDCListmodeInputFileFormat::can_read(const FileSignature& signature, const std::string& filename) +{ + + std::array hdf5_signature = { 'H', 'D', 'F', '5' }; + std::array binary_signature = { 'y', 'a', 'r', 'd' }; + + std::ifstream file(filename, std::ios::binary); + if (!file.is_open()) + { + std::cerr << "Cannot open file: " << filename << std::endl; + return false; + } + + std::array signature_{}; + file.read(signature_.data(), signature_.size()); + + if (signature_ == hdf5_signature) + { + use_hdf5 = true; + return use_hdf5; + } + + if (signature_ == binary_signature) + { + use_hdf5 = false; + return true; + } + + // petsird::hdf5::PETSIRDReader* petsird_reader = new petsird::hdf5::PETSIRDReader(filename); + + // if (is_null_ptr(petsird_reader)) + // { + + // petsird::binary::PETSIRDReader* petsird_reader = new petsird::binary::PETSIRDReader(filename); + // if (is_null_ptr(petsird_reader)) + // { + // return false; + // } + // return true; + // } + + return false; +} + +END_NAMESPACE_STIR diff --git a/src/buildblock/CMakeLists.txt b/src/buildblock/CMakeLists.txt index 8c9534f1b2..58b135ee55 100644 --- a/src/buildblock/CMakeLists.txt +++ b/src/buildblock/CMakeLists.txt @@ -107,7 +107,12 @@ if (NOT MINI_STIR) list(APPEND ${dir_LIB_SOURCES} ProjDataGEHDF5.cxx ) -endif() + endif() + if (HAVE_PETSIRD) + list(APPEND ${dir_LIB_SOURCES} + PETSIRDInfo.cxx + ) + endif() endif() # MINI_STIR @@ -153,3 +158,7 @@ endif() if (STIR_OPENMP) target_link_libraries(buildblock PUBLIC ${OpenMP_EXE_LINKER_FLAGS}) endif() + +if(HAVE_PETSIRD) + target_link_libraries(buildblock PUBLIC petsird_generated) +endif() \ No newline at end of file diff --git a/src/buildblock/PETSIRDInfo.cxx b/src/buildblock/PETSIRDInfo.cxx new file mode 100644 index 0000000000..23e068e265 --- /dev/null +++ b/src/buildblock/PETSIRDInfo.cxx @@ -0,0 +1,834 @@ +/* CListModeDataPETSIRD.cxx + +Coincidence LM Data Class for PETSIRD: Implementation + + Copyright 2025, UMCG + Copyright 2025, MGH / HST A. Martinos Center for Biomedical Imaging + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +/*! + +\file +\ingroup listmode +\brief implementation of class stir::CListModeDataPETSIRD + +\author Nikos Efthimiou +*/ + +#include "stir/PETSIRDInfo.h" +#include "stir/Succeeded.h" +#include +#include "stir/info.h" +#include "stir/warning.h" +#include "stir/error.h" + +#include "petsird_helpers.h" +#include "petsird_helpers/create.h" +#include "petsird_helpers/geometry.h" + +#include "petsird/binary/protocols.h" +#include "petsird/hdf5/protocols.h" + +START_NAMESPACE_STIR + +/*! + \namespace matrix + \brief Lightweight 3x3 matrix and 3D vector helpers used during PETSIRD geometry analysis. + + Provides utilities to: + - transpose a 3x3 matrix, + - subtract two 3x3 matrices, + - extract a rotation axis vector from the skew-symmetric part of a matrix. + + \details + - Mat3: std::array,3> for compact fixed-size storage. + - Vec3: std::array for simple 3D vectors. + - getAxisFromSkew(): + Given S = R - R^T (skew-symmetric part of a rotation matrix R), + returns the axis proportional to: + (S_z,y - S_y,z)/2, (S_x,z - S_z,x)/2, (S_y,x - S_x,y)/2. + For a pure rotation, S encodes the axis direction. + - These helpers assume small numerical noise; thresholds are handled by callers. + - No external dependencies; intended for quick geometric inference (e.g., rotation axis detection). +*/ +namespace matrix +{ + +using Mat3 = std::array, 3>; +using Vec3 = std::array; + +/*! + \brief Transpose a 3x3 matrix. + \param mat Input matrix. + \return Transposed matrix. +*/ +inline Mat3 +transpose(const Mat3& mat) +{ + std::array, 3> result{}; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + result[j][i] = mat[i][j]; + return result; +} + +/*! + \brief Subtract two 3x3 matrices (A - B). + \param A Left-hand matrix. + \param B Right-hand matrix. + \return Result of A - B. +*/ +inline Mat3 +subtract(const Mat3& A, const Mat3& B) +{ + std::array, 3> result{}; + for (size_t i = 0; i < 3; ++i) + for (size_t j = 0; j < 3; ++j) + result[i][j] = A[i][j] - B[i][j]; + return result; +} + +/*! + \brief Extract rotation axis from the skew-symmetric matrix S = R - R^T. + \param S Skew-symmetric matrix. + \return Axis vector proportional to the rotation axis. +*/ +inline Vec3 +getAxisFromSkew(const Mat3& S) +{ + return { + 0.5f * (S[2][1] - S[1][2]), // x + 0.5f * (S[0][2] - S[2][0]), // y + 0.5f * (S[1][0] - S[0][1]) // z + }; +} + +} // namespace matrix + +/*! + \namespace vector_utils + \brief Helpers for spacing analysis and axis inference from coordinate sets. + + \details + - \ref vector_utils::get_spacing_uniform computes successive spacings between + sorted unique values and tests if they are uniform within a tolerance. + - \ref vector_utils::getLargestVector returns the largest of three coordinate + sets and logs which axis is inferred as “axial”. +*/ +namespace vector_utils +{ +/*! + \brief Compute spacings between sorted unique values and test uniformity. + \param spacing Output vector. Appends |x[i] - x[i-1]| for i=1..N-1, where x is the sorted version of \a unsorted_block_poss. + \param unsorted_block_poss Set of unique positions (e.g., angles or translations). + \param epsilon Tolerance for uniformity check (default 1e-4). + \return True if all spacings differ from the first spacing by <= \a epsilon, false otherwise. + + \details + - If \a spacing ends up empty (e.g., input size < 2), returns true (trivially uniform). +*/ +inline bool +get_spacing_uniform(std::vector& spacing, const std::set& unsorted_block_poss, double epsilon = 1e-4) +{ + std::vector sorted_z(unsorted_block_poss.begin(), unsorted_block_poss.end()); + for (size_t i = 1; i < sorted_z.size(); ++i) + { + spacing.push_back(std::abs(sorted_z[i] - sorted_z[i - 1])); + } + + return std::all_of(spacing.begin(), spacing.end(), [&](float s) { return std::abs(s - spacing.front()) <= epsilon; }); +} + +/*! + \brief Return the largest of three sets and report inferred axial direction. + \param x Values along X. + \param y Values along Y. + \param z Values along Z. + \return Const reference to the largest set among \a x, \a y, \a z. + + \details + Logs the index of the inferred axial direction: 0 (x), 1 (y), or 2 (z). +*/ +const std::set& +getLargestVector(const std::set& x, const std::set& y, const std::set& z) +{ + const std::set* largest = &x; + int axis = 0; + if (y.size() > largest->size()) + { + largest = &y; + axis = 1; + } + else if (z.size() > largest->size()) + { + largest = &z; + axis = 2; + } + + info(fmt::format("I believe the axial direction is the {}.", axis)); + return *largest; +} + +/*! + \brief Collect unique values from a 1D vector. + \param values Output set for unique values. + \param input Input vector. +*/ +void +find_unique_values_1D(std::set& values, const std::vector& input) +{ + for (float val : input) + { + // std::cout << val << std::endl; + values.insert(val); + } +} + +/*! + \brief Collect unique values from a 2D vector (matrix). + \param values Output set for unique values. + \param input Input 2D vector [rows][cols]. +*/ +void +find_unique_values_2D(std::set& values, const std::vector>& input) +{ + for (size_t row = 0; row < input.size(); ++row) + for (size_t col = 0; col < input[row].size(); ++col) + values.insert(input[row][col]); +} + +} // namespace vector_utils + +bool +almostEqual(double a, double b, double tol = 1e-6) +{ + return std::fabs(a - b) <= tol; +} + +// Detect groupSize along dim2 (y) and loops along dim3 (z) +// Returns true on success, false if pattern doesn't match the assumed structure. +bool +inferGroupSizes_dim2_dim3(const std::vector>& pts, + std::size_t& groupSize_dim2, + std::size_t& groupSize_dim3, + float tol = 1e-5f) +{ + const std::size_t n = pts.size(); + groupSize_dim2 = groupSize_dim3 = 0; + + if (n == 0) + return false; + + if (n == 1) + { + groupSize_dim2 = 1; + groupSize_dim3 = 1; + return true; + } + + // STIR CartesianCoordinate3D is (z, y, x) + const float x0 = pts[0].x(); + const float z0 = pts[0].z(); + + // 1) Find how many initial points keep x and z the same + std::size_t runLen = 1; + while (runLen < n && almostEqual(pts[runLen].x(), x0, tol) && almostEqual(pts[runLen].z(), z0, tol)) + { + ++runLen; + } + + groupSize_dim2 = runLen; + + // Must tile the full array + if (groupSize_dim2 == 0 || n % groupSize_dim2 != 0) + return false; + + groupSize_dim3 = n / groupSize_dim2; + + // 2) Check each block of size groupSize_dim2 has constant x,z + for (std::size_t b = 0; b < groupSize_dim3; ++b) + { + std::size_t start = b * groupSize_dim2; + float xb = pts[start].x(); + float zb = pts[start].z(); + + for (std::size_t i = 1; i < groupSize_dim2; ++i) + { + const auto& p = pts[start + i]; + if (!almostEqual(p.x(), xb, tol) || !almostEqual(p.z(), zb, tol)) + { + return false; // pattern breaks inside a block + } + } + } + + // 3) Optionally check that z actually changes between blocks + for (std::size_t b = 1; b < groupSize_dim3; ++b) + { + float z_prev = pts[(b - 1) * groupSize_dim2].z(); + float z_curr = pts[b * groupSize_dim2].z(); + if (almostEqual(z_prev, z_curr, tol)) + { + return false; // outer loop didn't move in z + } + } + + return true; +} + +/*! + \brief Infer scanner blocks and rotation axis from PETSIRD replicated modules. + \param unique_dim1_values Output set of unique translations along X. + \param unique_dim2_values Output set of unique translations along Y. + \param unique_dim3_values Output set of unique translations along Z. + \param replicated_module_list PETSIRD replicated detector modules. + \return Index of rotation axis (0=x, 1=y, 2=z) or -1 if not found. + + \details + - Extracts translation components into the provided sets. + - Uses skew-symmetric part of rotation matrices to infer axis direction. + - Emits warnings for mixed-axis rotations or inconsistencies. +*/ +int +PETSIRDInfo::figure_out_scanner_blocks_and_rotation_axis(std::set& unique_dim1_values, + std::set& unique_dim2_values, + std::set& unique_dim3_values) +{ + auto insertTranslations = [&](const petsird::RigidTransformation& trans) { + unique_dim1_values.insert(trans.matrix.at(0, 3)); + unique_dim2_values.insert(trans.matrix.at(1, 3)); + unique_dim3_values.insert(trans.matrix.at(2, 3)); + }; + + auto extractRotationMatrix = [](const petsird::RigidTransformation& trans) -> matrix::Mat3 { + matrix::Mat3 R; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + R[i][j] = trans.matrix.at(i, j); + return R; + // skew = matrix::subtract(R, matrix::transpose(R)); + // auto rot = matrix::getAxisFromSkew(skew); + }; + + std::array, 3> skew; + int detected_axis = -1; + + std::vector replicated_module_list + = petsird_scanner_info_sptr->scanner_geometry.replicated_modules; + for (const auto& module : replicated_module_list) + for (const auto& mod_trans : module.transforms) + { + insertTranslations(mod_trans); + matrix::Mat3 R = extractRotationMatrix(mod_trans); + skew = matrix::subtract(R, matrix::transpose(R)); + auto axis_vec = matrix::getAxisFromSkew(skew); + + int current_axis = -1; + for (int i = 0; i < 3; ++i) + { + if (std::abs(axis_vec[i]) > 1e-6f) + { + if (current_axis != -1) + { + warning("Rotation involves multiple axis components. Possibly non-pure rotation."); + current_axis = -2; // Sentinel for mixed axes + return -1; + } + current_axis = i; + } + } + + if (current_axis >= 0) + { + if (detected_axis == -1) + detected_axis = current_axis; + else if (detected_axis != current_axis) + warning("Inconsistent rotation axis detected between modules."); + } + } + info(fmt::format("Rotation axis of blocks inferred as axis index {}", detected_axis)); + return detected_axis; +} + +/*! + \brief Determine block element translations and radius. + \param unique_dim1_values Output set of element translations along X. + \param unique_dim2_values Output set of element translations along Y. + \param unique_dim3_values Output set of element translations along Z. + \param radius Output inferred radius (positive component orthogonal to rotation axis). + \param radius_index Output index of the axis containing the radius component. + \param rotation_axis Known rotation axis (0=x, 1=y, 2=z). + \param replicated_module_list PETSIRD replicated detector modules. + + \details + - Scans element-level transforms to infer radius and collect translations. + - Emits warnings if mixed radii are detected. +*/ +void +PETSIRDInfo::figure_out_block_element_transformations(std::set& unique_dim1_values, + std::set& unique_dim2_values, + std::set& unique_dim3_values, + float& radius, + int& radius_index, + const int rotation_axis) +{ + + auto insert_translation = [&](const petsird::RigidTransformation& trans) { + unique_dim1_values.insert(trans.matrix.at(0, 3)); + unique_dim2_values.insert(trans.matrix.at(1, 3)); + unique_dim3_values.insert(trans.matrix.at(2, 3)); + }; + + auto detect_radius = [&](const petsird::RigidTransformation& trans) -> bool { + for (int i = 0; i < 3; ++i) + { + if (i == rotation_axis) + continue; + float candidate = trans.matrix.at(i, 3); + if (candidate > 0.0f) + { + radius = candidate; + radius_index = i; + return true; + } + } + return false; + }; + + std::vector replicated_module_list + = petsird_scanner_info_sptr->scanner_geometry.replicated_modules; + for (const auto& module : replicated_module_list) + { + for (const auto& el_trans : module.object.detecting_elements.transforms) + { + if (radius == 0.0f) + { + if (!detect_radius(el_trans)) + { + error("Unable to determine radius from translation components."); + continue; + } + } + else + { + float current = el_trans.matrix.at(radius_index, 3); + if (std::abs(current - radius) > 1e-4f) + warning("Mixed radii detected. Consider checking for misaligned modules."); + } + + insert_translation(el_trans); + // std::cout << el_trans.matrix << std::endl; + } + } +} + +void +PETSIRDInfo::figure_out_block_angles(std::set& unique_angle_modules, const int rot_axis) +{ + std::vector replicated_module_list + = petsird_scanner_info_sptr->scanner_geometry.replicated_modules; + for (const auto& module : replicated_module_list) + for (const auto& transform : module.transforms) + { + if (rot_axis == 0) + unique_angle_modules.insert( + std::fabs(int(1000.F * std::atan2(transform.matrix.at(1, 0), transform.matrix.at(2, 0))) / 1000.F)); + else if (rot_axis == 1) + unique_angle_modules.insert( + std::fabs(int(1000.F * std::atan2(transform.matrix.at(2, 0), transform.matrix.at(0, 0))) / 1000.F)); + else if (rot_axis == 2) + unique_angle_modules.insert( + std::fabs(int(1000.F * std::atan2(transform.matrix.at(1, 0), transform.matrix.at(0, 0))) / 1000.F)); + } +} + +PETSIRDInfo::PETSIRDInfo(shared_ptr petsird_info_sptr) + : petsird_scanner_info_sptr(petsird_info_sptr) +{ + + if (!petsird_scanner_info_sptr) + error("PETSIRDInfo: Null PETSIRD ScannerInformation pointer provided."); + + //! TODO: Determine the DOI based on material + float average_doi = 0.0; + if (petsird_scanner_info_sptr->bulk_materials.size() > 0) + { + const std::string& material = petsird_scanner_info_sptr->bulk_materials[0].name; + if (material.size() > 0) + average_doi = (material == "BGO") ? 5.0f : (material == "LSO" || material == "LYSO") ? 7.0f : 0.0f; + } + + const petsird::TypeOfModule type_of_module = petsird_scanner_info_sptr->scanner_geometry.replicated_modules.size() - 1; + + if (type_of_module > 0) + { + error("Multiple types of PETSIRD modules are not supported. Abord."); + } + + const auto& tof_bin_edges = petsird_scanner_info_sptr->tof_bin_edges[type_of_module][type_of_module]; + info(fmt::format("Num. of TOF bins in PETSIRD {}", tof_bin_edges.NumberOfBins())); + + std::set unique_tof_values; + vector_utils::find_unique_values_2D(unique_tof_values, petsird_scanner_info_sptr->tof_resolution); + //! TODO: Supports only single type of module + numberOfModules = petsird_scanner_info_sptr->scanner_geometry.replicated_modules[0].NumberOfObjects(); + //! TODO: Supports only single type of module + numberOfElementsIndices + = petsird_scanner_info_sptr->scanner_geometry.replicated_modules[0].object.detecting_elements.NumberOfObjects(); + std::set unique_dim1_values, unique_dim2_values, unique_dim3_values; + std::set unique_tof_resolutions; + + const int rotation_axis + = figure_out_scanner_blocks_and_rotation_axis(unique_dim1_values, unique_dim2_values, unique_dim3_values); + if (rotation_axis == -1) + is_cylindrical = false; + + const std::set& main_axis = vector_utils::getLargestVector(unique_dim1_values, unique_dim2_values, unique_dim3_values); + + int num_transaxial_blocks = numberOfModules / main_axis.size(); + info(fmt::format("I deduce that the scanner has {} transaxial number of blocks", num_transaxial_blocks)); + + float radius = 0; + int radius_indx = -1; + + std::set unique_elements_dim1_values, unique_elements_dim2_values, unique_elements_dim3_values; + figure_out_block_element_transformations( + unique_elements_dim1_values, unique_elements_dim2_values, unique_elements_dim3_values, radius, radius_indx, rotation_axis); + + std::set unique_angle_modules; + figure_out_block_angles(unique_angle_modules, rotation_axis); + + std::vector block_angular_spacing; + if (!vector_utils::get_spacing_uniform( + block_angular_spacing, unique_angle_modules, 1e-2)) /// epsilon * 10000) // relax epsilon here + is_cylindrical = false; + + std::size_t group2 = 0, group3 = 0; + { + std::vector> pet_sird_positions; + + for (uint32_t module = 0; module < 1; module++) + { + for (uint32_t elem = 0; elem < numberOfElementsIndices; elem++) + { + petsird::ExpandedDetectionBin expanded_detection_bin{ module, elem, 1 }; + auto box_shape = petsird_helpers::geometry::get_detecting_box( + *petsird_scanner_info_sptr, type_of_module, expanded_detection_bin); + CartesianCoordinate3D mean_coord; + for (auto& corner : box_shape.corners) + { // if STIR (z,y,x) -> PETSIRD (-y, -x, z) pheraps the order below needs to be changed + mean_coord.x() = +corner.c[0] / box_shape.corners.size(); + mean_coord.y() = +corner.c[1] / box_shape.corners.size(); + mean_coord.z() = +corner.c[2] / box_shape.corners.size(); + } + // mean_coord.z() += this_scanner_sptr->get_axial_crystal_spacing() / + // save mean pos into map + pet_sird_positions.push_back(mean_coord); + } + } + + if (inferGroupSizes_dim2_dim3(pet_sird_positions, group2, group3)) + { + std::cout << "groupSize_dim2 = " << group2 << "\n"; + + std::cout << "groupSize_dim3 = " << group3 << "\n"; + } + else + { + std::cout << "No (dim2, dim3) loop structure detected.\n"; + group2 = 1; + group3 = 1; + } + } + + std::vector element_horizontal_spacing, element_vertical_spacing; + std::set unique_elements_horizontal_values, unique_elements_vertical_values; + if (radius_indx == 0) + { + if (!vector_utils::get_spacing_uniform(element_horizontal_spacing, unique_elements_dim3_values)) + is_cylindrical = false; + if (!vector_utils::get_spacing_uniform(element_vertical_spacing, unique_elements_dim2_values)) + is_cylindrical = false; + unique_elements_horizontal_values = unique_elements_dim3_values; + unique_elements_vertical_values = unique_elements_dim2_values; + } + else + { + //! TODO: Multiple radii handling + error("Multiple radii handling not implemented yet."); + } + + { + info("Printing TOF bin edges for validation (please make sure that the STIR TOF bin edges match the PETSIRD TOF bin edges):"); + for (size_t i = 0; i < tof_bin_edges.NumberOfBins(); ++i) + { + std::cout << "PETSIRD TOF bin edge " << i << ": " << tof_bin_edges.edges[i] << "\n"; + } + } + + blocks_per_bucket_transaxial = group2 > 1 ? unique_elements_vertical_values.size() / group2 : group2; + std::cout << "blocks per bucket in transaxial direction = " << blocks_per_bucket_transaxial << "\n"; + blocks_per_bucket_axial = group3 > 1 ? unique_elements_horizontal_values.size() / (numberOfElementsIndices / group3) : group3; + std::cout << "blocks per bucket in axial direction = " << blocks_per_bucket_axial << "\n"; + num_axial_crystals_per_block = unique_elements_horizontal_values.size() / blocks_per_bucket_axial; + num_trans_crystals_per_block = unique_elements_vertical_values.size() / blocks_per_bucket_transaxial; + + std::vector block_axial_spacing; + vector_utils::get_spacing_uniform(block_axial_spacing, main_axis); + if (block_axial_spacing.size() < 1) + { + // std::set::iterator it = unique_elements_horizontal_values.begin(); + // std::advance(it,0); + float begin = *std::next(unique_elements_horizontal_values.begin(), 0); + // std::advance(it,unique_elements_horizontal_values.size()-1); + float end = *std::next(unique_elements_horizontal_values.begin(), unique_elements_horizontal_values.size() - 1); + block_axial_spacing.push_back(std::abs(end - begin)); + } + + info(fmt::format("I counted {} axial blocks with spacing {}", unique_dim3_values.size(), block_axial_spacing[0])); + // Check if the cyrcle area is less than 5% different from the polygon + float expected_circle_area = float(M_PI * radius * radius); + float polygon_area + = float(0.5f * unique_angle_modules.size() * radius * radius * std::sin(2.f * float(M_PI) / unique_angle_modules.size())); + info(fmt::format("Circle area: {}, Polygon area: {}, pct {}", + expected_circle_area, + polygon_area, + std::abs(expected_circle_area - polygon_area) / expected_circle_area)); + + if (std::abs(expected_circle_area - polygon_area) / expected_circle_area < 0.000000001f) + { + info("the cylindical area is more then 95% matching the polygon area. We will predsume a cylindrical configuration."); + stir_scanner_sptr.reset(new Scanner(Scanner::User_defined_scanner, + std::string("PETSIRD_defined_scanner"), + /* num dets per ring */ + (num_transaxial_blocks * unique_elements_vertical_values.size()), + unique_dim3_values.size() * unique_elements_horizontal_values.size() /* num of rings */, + /* number of non arccor bins */ + (num_transaxial_blocks * unique_elements_vertical_values.size()) / 2, + /* number of maximum arccor bins */ + (num_transaxial_blocks * unique_elements_vertical_values.size()) / 2, + /* inner ring radius */ + radius, + /* doi */ average_doi, // average_doi, + /* ring spacing */ + element_horizontal_spacing[0], //* 10.f, + // bin_size_v + element_vertical_spacing[0], // * 10.f, + /*intrinsic_tilt_v*/ + 0.f, + /*num_axial_blocks_per_bucket_v */ + blocks_per_bucket_axial, + /*num_transaxial_blocks_per_bucket_v*/ + blocks_per_bucket_transaxial, + /*num_axial_crystals_per_block_v*/ + num_axial_crystals_per_block, + /*num_transaxial_crystals_per_block_v*/ + num_trans_crystals_per_block, + /*num_axial_crystals_per_singles_unit_v*/ + unique_elements_horizontal_values.size() / blocks_per_bucket_axial, + /*num_transaxial_crystals_per_singles_unit_v*/ + unique_elements_vertical_values.size() / blocks_per_bucket_transaxial, + /*num_detector_layers_v*/ + 1, // num_detector_layers_v + petsird_scanner_info_sptr->energy_resolution_at_511.front(), // energy_resolution_v + 511, // reference_energy_v + tof_bin_edges.NumberOfBins(), + (tof_bin_edges.edges[1] - tof_bin_edges.edges[0]) / speed_of_light_in_mm_per_ps_div2, + *unique_tof_values.begin() * 10 // non-TOF + )); + is_cylindrical = true; + } + else + { + info("the cylindical area is less then 95% matching the polygon area. We will predsume a non-cylindrical configuration."); + stir_scanner_sptr.reset( + new Scanner(Scanner::User_defined_scanner, + std::string("PETSIRD_defined_scanner"), + /* num dets per ring */ + (num_transaxial_blocks * unique_elements_vertical_values.size()), + unique_dim3_values.size() * unique_elements_horizontal_values.size() /* num of rings */, + /* number of non arccor bins */ + (num_transaxial_blocks * unique_elements_vertical_values.size()) / 2, + /* number of maximum arccor bins */ + (num_transaxial_blocks * unique_elements_vertical_values.size()) / 2, + /* inner ring radius */ + radius, + /* doi */ average_doi, + /* ring spacing */ + element_horizontal_spacing[0], //* 10.f, + // bin_size_v + element_vertical_spacing[0], // * 10.f, + /*intrinsic_tilt_v*/ + 0.f, + /*num_axial_blocks_per_bucket_v */ + 1, + /*num_transaxial_blocks_per_bucket_v*/ + 1, + /*num_axial_crystals_per_block_v*/ + blocks_per_bucket_axial * num_axial_crystals_per_block, + /*num_transaxial_crystals_per_block_v*/ + blocks_per_bucket_transaxial * num_trans_crystals_per_block, + /*num_axial_crystals_per_singles_unit_v*/ + unique_elements_horizontal_values.size() / blocks_per_bucket_axial, + /*num_transaxial_crystals_per_singles_unit_v*/ + unique_elements_vertical_values.size() / blocks_per_bucket_transaxial, + /*num_detector_layers_v*/ + 1, // num_detector_layers_v + petsird_scanner_info_sptr->energy_resolution_at_511.front(), // energy_resolution_v + 511, // reference_energy_v + 1, + 0.F, + 0.F, // non-TOF + "BlocksOnCylindrical", // scanner_geometry_v + (*std::next(unique_elements_horizontal_values.begin()) + - *unique_elements_horizontal_values.begin()), // axial_crystal_spacing_v + (*std::next(unique_elements_vertical_values.begin()) + - *unique_elements_vertical_values.begin()), // transaxial_crystal_spacing_v + (*std::next(unique_elements_horizontal_values.begin()) - *unique_elements_horizontal_values.begin()) + * num_axial_crystals_per_block * blocks_per_bucket_axial, // axial_block_spacing_v + (*std::next(unique_elements_vertical_values.begin()) - *unique_elements_vertical_values.begin()) + * num_trans_crystals_per_block * blocks_per_bucket_transaxial, // transaxial_block_spacing_v + "" // crystal_map_file_name_v + )); + is_cylindrical = false; + } + + /// Now let's create the PETISIRD - STIR geometry mapping + petsird_to_stir = std::make_shared(); + petsird_map_sptr = std::make_shared(); + + enum class InnerLoopDim + { + Axial, + Tangential, + Radial + }; + InnerLoopDim inner_dim = InnerLoopDim::Tangential; // determined from your groupSize analysis + + // PRECOMPUTED from previous step: + std::size_t groupSize + = blocks_per_bucket_transaxial == 1 ? 0 : num_trans_crystals_per_block; // e.g. 5, or 1 if purely monotonic + // extern InnerLoopDim inner_dim; // Axial / Tangential / Radial + + const int num_ax = blocks_per_bucket_axial * num_axial_crystals_per_block; + const int num_tang = blocks_per_bucket_transaxial * num_trans_crystals_per_block; + + for (uint32_t module = 0; module < numberOfModules; module++) + for (uint32_t elem = 0; elem < numberOfElementsIndices; elem++) + // for (uint32_t ener = 0; ener < num_event_energy_bins; ener++) //energy not supported yet + { + // ---- 1) Decompose elem into: tile, in-tile indices ---- + const uint32_t tileSize = groupSize * groupSize; // elems per tile + const uint32_t tiles_per_bucket = blocks_per_bucket_axial * blocks_per_bucket_transaxial; + + const uint32_t tile = (groupSize > 0 ? elem / tileSize : 0); // which tile + const uint32_t inTile = (groupSize > 0 ? elem % tileSize : elem); // index inside tile + + const uint32_t i0 = inTile % groupSize; // fast inside tile (local "x") + const uint32_t i1 = inTile / groupSize; // slow inside tile (local "y") + + int ax_pos = 0; + int tang_pos = 0; + int rad_pos = 0; // ignored for now + + // ---- 2) Decode which block (tile) we are in along axial/tangential ---- + switch (inner_dim) + { + case InnerLoopDim::Tangential: { + // Here we assume: + // - i0 runs tangential inside a block + // - i1 runs axial inside a block + // + // tiles are laid out as: + // tangential: blocks_per_bucket_transaxial tiles + // axial: blocks_per_bucket_axial tiles + + const uint32_t tang_block = tile % blocks_per_bucket_transaxial; + const uint32_t axial_block = tile / blocks_per_bucket_transaxial; + + tang_pos = static_cast(tang_block * groupSize + i0); + ax_pos = static_cast(axial_block * groupSize + i1); + break; + } + + case InnerLoopDim::Axial: { + // Here we assume: + // - i0 runs axial inside a block + // - i1 runs tangential inside a block + // + // tiles are laid out as: + // axial: blocks_per_bucket_axial tiles + // tangential: blocks_per_bucket_transaxial tiles + + const uint32_t axial_block = tile % blocks_per_bucket_axial; + const uint32_t tang_block = tile / blocks_per_bucket_axial; + + ax_pos = static_cast(axial_block * groupSize + i0); + tang_pos = static_cast(tang_block * groupSize + i1); + break; + } + case InnerLoopDim::Radial: { + error("Radial inner loop not supported yet."); + break; + } + } + + DetectionPosition<> detpos( + tang_pos + module * (num_trans_crystals_per_block * blocks_per_bucket_transaxial), ax_pos, rad_pos); + + petsird::ExpandedDetectionBin expanded_detection_bin{ module, elem, 0 }; + + if (stir_scanner_sptr->get_scanner_geometry() == "Generic") + { + auto box_shape = petsird_helpers::geometry::get_detecting_box( + *petsird_scanner_info_sptr, type_of_module, expanded_detection_bin); + CartesianCoordinate3D mean_coord(0.f, 0.f, 0.f); + + for (auto& corner : box_shape.corners) + { + mean_coord.x() += corner.c[0] / box_shape.corners.size(); + mean_coord.y() += corner.c[1] / box_shape.corners.size(); + mean_coord.z() += corner.c[2] / box_shape.corners.size(); + } + + (*petsird_map_sptr)[detpos] = mean_coord; + + std::cout << detpos.radial_coord() << ", " << detpos.axial_coord() << ", " << detpos.tangential_coord() << ", " + << mean_coord.x() << ", " << mean_coord.y() << ", " << mean_coord.z() << "\n"; + } + else if (stir_scanner_sptr->get_scanner_geometry() == "BlocksOnCylindrical" || is_cylindrical) + { + (*petsird_to_stir)[expanded_detection_bin] = detpos; + } + + // auto detectionBin = petsird_helpers::make_detection_bin( + // *scanner_info, + // type_of_module, + // expanded_detection_bin); + + // petsird_map[detectionBin] = mean_coord; + + // Save to shared_ptr map + } + + // // this->map.reset(new DetectorCoordinateMapLightPETSIRD(petsird_map)); + // this->map.reset(new DetectorCoordinateMap(petsird_map)); + // // this_scanner_sptr->get_detector_map_sptr()->set_detector_coordinate_map_light_sptr( + // // std::make_shared(petsird_map)); + // this->map->write_detectormap_to_file("petsird_detector_map_from_scanner_definition.txt"); + // this_scanner_sptr->set_detector_map(petsird_map); + // this_scanner_sptr->set_up(); +} + +END_NAMESPACE_STIR \ No newline at end of file diff --git a/src/buildblock/ProjDataInfo.cxx b/src/buildblock/ProjDataInfo.cxx index b72804f35b..be7e90679b 100644 --- a/src/buildblock/ProjDataInfo.cxx +++ b/src/buildblock/ProjDataInfo.cxx @@ -169,94 +169,82 @@ ProjDataInfo::set_max_tangential_pos_num(const int max_tang_poss) max_tangential_pos_num = max_tang_poss; } -//! \todo N.E: This function is very ugly and unnessesary complicated. Could be much better. void ProjDataInfo::set_tof_mash_factor(const int new_num) { - if (scanner_ptr->is_tof_ready() && new_num > 0) - { - tof_mash_factor = new_num; - if (tof_mash_factor > scanner_ptr->get_max_num_timing_poss()) - error("ProjDataInfo::set_tof_mash_factor: TOF mashing factor (" + std::to_string(tof_mash_factor) - + +") must be smaller than or equal to the scanner's number of max timing bins (" - + std::to_string(scanner_ptr->get_max_num_timing_poss()) + ")."); - -#if 0 - // KT: code disabled as buggy but currently not needed - tof_increament_in_mm = tof_delta_time_to_mm(scanner_ptr->get_size_of_timing_pos()); - min_unmashed_tof_pos_num = - (scanner_ptr->get_max_num_timing_poss())/2; - max_unmashed_tof_pos_num = min_unmashed_tof_pos_num + (scanner_ptr->get_max_num_timing_poss()) -1; - - // Upper and lower boundaries of the timing poss; - tof_bin_unmashed_boundaries_mm.grow(min_unmashed_tof_pos_num, max_unmashed_tof_pos_num); - tof_bin_unmashed_boundaries_ps.grow(min_unmashed_tof_pos_num, max_unmashed_tof_pos_num); - - // Silently intialise the unmashed TOF bins. - for (int k = min_unmashed_tof_pos_num; k <= max_unmashed_tof_pos_num; ++k ) - { - Bin bin; - bin.timing_pos_num() = k; - // if we ever re-enable this code, there is a BUG here: - // get_k relies on num_tof_bins, so this should have been set to the unmashed value from the scanner - float cur_low = get_k(bin) - get_sampling_in_k(bin)/2.f; - float cur_high = get_k(bin) + get_sampling_in_k(bin)/2.f; - - tof_bin_unmashed_boundaries_mm[k].low_lim = cur_low; - tof_bin_unmashed_boundaries_mm[k].high_lim = cur_high; - tof_bin_unmashed_boundaries_ps[k].low_lim = static_cast(mm_to_tof_delta_time(tof_bin_unmashed_boundaries_mm[k].low_lim)); - tof_bin_unmashed_boundaries_ps[k].high_lim = static_cast(mm_to_tof_delta_time(tof_bin_unmashed_boundaries_mm[k].high_lim)); + const bool tof_ready = scanner_ptr->is_tof_ready(); + + // Non-TOF mode (either scanner not TOF-ready or invalid mash factor) + if (!tof_ready || new_num <= 0) + { + num_tof_bins = 1; + tof_mash_factor = 0; + min_tof_pos_num = 0; + max_tof_pos_num = 0; + // we assume TOF mashing factor = 0 means non-TOF and the projector + // won't use any boundary conditions + return; + } + + const int max_timing_poss = scanner_ptr->get_max_num_timing_poss(); + + if (new_num > max_timing_poss) + { + error("ProjDataInfo::set_tof_mash_factor: TOF mashing factor (" + + std::to_string(new_num) + + ") must be smaller than or equal to the scanner's number of " + "max timing bins (" + + std::to_string(max_timing_poss) + ")."); + } + + tof_mash_factor = new_num; + // Initialise mashed TOF bins +tof_increament_in_mm = + tof_delta_time_to_mm(tof_mash_factor * scanner_ptr->get_size_of_timing_pos()); + +const int num_mashed_bins = max_timing_poss / tof_mash_factor; +num_tof_bins = num_mashed_bins; + +// Compute min/max TOF position numbers without enforcing odd count +// +// We choose a symmetric convention: +// - If num_tof_bins is odd: bins go from -N/2 ... +N/2 +// - If num_tof_bins is even: bins go from -(N/2) ... +(N/2 - 1) +// This preserves the old behavior for odd counts and gives clean indexing for even counts. +min_tof_pos_num = -num_tof_bins / 2; +max_tof_pos_num = min_tof_pos_num + num_tof_bins - 1; - } -#endif - // Now, initialise the mashed TOF bins. - tof_increament_in_mm = tof_delta_time_to_mm(tof_mash_factor * scanner_ptr->get_size_of_timing_pos()); +// Upper and lower boundaries of the timing positions +tof_bin_boundaries_mm.grow(min_tof_pos_num, max_tof_pos_num); +tof_bin_boundaries_ps.grow(min_tof_pos_num, max_tof_pos_num); - // TODO cope with even numbers! - min_tof_pos_num = -(scanner_ptr->get_max_num_timing_poss() / tof_mash_factor) / 2; - max_tof_pos_num = min_tof_pos_num + (scanner_ptr->get_max_num_timing_poss() / tof_mash_factor) - 1; +for (int k = min_tof_pos_num; k <= max_tof_pos_num; ++k) +{ + Bin bin; + bin.timing_pos_num() = k; - num_tof_bins = max_tof_pos_num - min_tof_pos_num + 1; + const float sampling = get_sampling_in_k(bin); + const float center = get_k(bin); - // Ensure that we have a central tof bin. - if (num_tof_bins % 2 == 0) - error("ProjDataInfo: Number of TOF bins should be an odd number. Abort."); + const float cur_low = center - sampling / 2.f; + const float cur_high = center + sampling / 2.f; - // Upper and lower boundaries of the timing poss; - tof_bin_boundaries_mm.grow(min_tof_pos_num, max_tof_pos_num); + tof_bin_boundaries_mm[k].low_lim = cur_low; + tof_bin_boundaries_mm[k].high_lim = cur_high; - tof_bin_boundaries_ps.grow(min_tof_pos_num, max_tof_pos_num); + tof_bin_boundaries_ps[k].low_lim = + static_cast(mm_to_tof_delta_time(cur_low)); + tof_bin_boundaries_ps[k].high_lim = + static_cast(mm_to_tof_delta_time(cur_high)); - for (int k = min_tof_pos_num; k <= max_tof_pos_num; ++k) - { - Bin bin; - bin.timing_pos_num() = k; - - float cur_low = get_k(bin) - get_sampling_in_k(bin) / 2.f; - float cur_high = get_k(bin) + get_sampling_in_k(bin) / 2.f; - - tof_bin_boundaries_mm[k].low_lim = cur_low; - tof_bin_boundaries_mm[k].high_lim = cur_high; - tof_bin_boundaries_ps[k].low_lim = static_cast(mm_to_tof_delta_time(tof_bin_boundaries_mm[k].low_lim)); - tof_bin_boundaries_ps[k].high_lim = static_cast(mm_to_tof_delta_time(tof_bin_boundaries_mm[k].high_lim)); - // I could imagine a better printing. - info(format("Tbin {}: {} - {} mm ({} - {} ps) = {}", - k, - tof_bin_boundaries_mm[k].low_lim, - tof_bin_boundaries_mm[k].high_lim, - tof_bin_boundaries_ps[k].low_lim, - tof_bin_boundaries_ps[k].high_lim, - get_sampling_in_k(bin))); - } - } - else if ((scanner_ptr->is_tof_ready() && new_num <= 0) - || !scanner_ptr->is_tof_ready()) // Case new_num <=, will produce non-TOF data for a TOF compatible scanner - { - num_tof_bins = 1; - tof_mash_factor = 0; - min_tof_pos_num = 0; - max_tof_pos_num = 0; - // we assume TOF mashing factor = 0 means non-TOF and the projecter won't use any boundary conditions - } + info(format("Tbin {}: {} - {} mm ({} - {} ps) = {}", + k, + tof_bin_boundaries_mm[k].low_lim, + tof_bin_boundaries_mm[k].high_lim, + tof_bin_boundaries_ps[k].low_lim, + tof_bin_boundaries_ps[k].high_lim, + sampling)); +} } std::vector diff --git a/src/cmake/STIRConfig.cmake.in b/src/cmake/STIRConfig.cmake.in index baff57b8c1..3ae838e78b 100644 --- a/src/cmake/STIRConfig.cmake.in +++ b/src/cmake/STIRConfig.cmake.in @@ -160,6 +160,10 @@ if(@STIR_WITH_Parallelproj_PROJECTOR@) set(STIR_WITH_Parallelproj_PROJECTOR TRUE) endif() +if(@HAVE_PETSIRD@) + set(HAVE_PETSIRD TRUE) +endif() + SET(STIR_WITH_EXPERIMENTAL @STIR_ENABLE_EXPERIMENTAL@) if(STIR_WITH_EXPERIMENTAL) if(${CMAKE_VERSION} VERSION_LESS "3.12.0") diff --git a/src/cmake/STIRConfig.h.in b/src/cmake/STIRConfig.h.in index efc241dae9..5055d63a31 100644 --- a/src/cmake/STIRConfig.h.in +++ b/src/cmake/STIRConfig.h.in @@ -90,6 +90,8 @@ namespace stir { #cmakedefine HAVE_SYSTEM_GETOPT +#cmakedefine HAVE_PETSIRD + #cmakedefine STIR_DEFAULT_PROJECTOR_AS_V2 #ifndef STIR_DEFAULT_PROJECTOR_AS_V2 #define USE_PMRT diff --git a/src/include/stir/IO/InputFileFormat.h b/src/include/stir/IO/InputFileFormat.h index 03bcb6a74a..3a2b6bf7a9 100644 --- a/src/include/stir/IO/InputFileFormat.h +++ b/src/include/stir/IO/InputFileFormat.h @@ -46,7 +46,7 @@ class InputFileFormat { return this->actual_can_read(signature, input); } - virtual bool can_read(const FileSignature& signature, const std::string& filename) const + virtual bool can_read(const FileSignature& signature, const std::string& filename) { std::ifstream input; open_read_binary(input, filename); diff --git a/src/include/stir/IO/InputStreamFromPETSIRD.h b/src/include/stir/IO/InputStreamFromPETSIRD.h new file mode 100644 index 0000000000..9040e2b349 --- /dev/null +++ b/src/include/stir/IO/InputStreamFromPETSIRD.h @@ -0,0 +1,85 @@ +// /*! +// \file +// \ingroup IO +// \brief Declaration of class stir::InputStreamFromROOTFile + +// \author Nikos Efthimiou +// \author Harry Tsoumpas +// \author Kris Thielemans +// \author Robert Twyman +// */ +// /* +// * Copyright (C) 2015, 2016 University of Leeds +// Copyright (C) 2016, 2021, 2020, 2021 UCL +// Copyright (C) 2018 University of Hull +// This file is part of STIR. + +// SPDX-License-Identifier: Apache-2.0 + +// See STIR/LICENSE.txt for details +// */ + +// #ifndef __stir_IO_InputStreamFromPETSIRD_H__ +// #define __stir_IO_InputStreamFromPETSIRD_H__ + +// #include "stir/shared_ptr.h" +// #include "stir/Succeeded.h" +// #include "stir/listmode/CListRecordPETSIRD.h" +// #include "stir/RegisteredObject.h" +// #include "stir/error.h" + +// #include "../../PETSIRD/cpp/generated/binary/protocols.h" +// #include "stir/IO/InputStreamWithRecords.h" +// #include "../../PETSIRD/cpp/generated/hdf5/protocols.h" + +// START_NAMESPACE_STIR + +// class InputStreamWithRecordsFromPETSIRD : public InputStreamWithRecords +// { +// public: +// typedef std::vector::size_type SavedPosition; + +// //! Default constructor +// InputStreamFromPETSIRD(std::string filename); + +// ~InputStreamFromPETSIRD() override +// {} +// //! \details Returns the next record in the ROOT file. +// //! The code is adapted from Sadek A. Nehmeh and CR Schmidtlein, +// //! downloaded from here +// virtual Succeeded get_next_record(CListReco*/rdPETSIRD& record) = 0; +// //! Go to the first event. +// inline Succeeded reset(); +// //! Must be called before calling for the first event. +// virtual Succeeded set_up(const std::string& header_path); +// //! Save current position in a vector +// inline SavedPosition save_get_position(); +// //! Set current position +// inline Succeeded set_get_position(const SavedPosition&); +// //! Get the vector with the saved positions +// inline std::vector get_saved_get_positions() const; +// //! Set a vector with saved positions +// inline void set_saved_get_positions(const std::vector&); +// //! Returns the total number of events +// inline unsigned long int get_total_number_of_events() const; + +// inline std::string get_PETSIRD_filename() const; + +// protected: + +// //! The starting position. +// unsigned long int starting_stream_position; +// //! The total number of entries +// unsigned long int nentries; +// //! Current get position +// unsigned long int current_position; +// //! A vector with saved position indices. +// std::vector saved_get_positions; +// //! The name of the ROOT chain to be read +// }; + +// END_NAMESPACE_STIR + +// #include "stir/IO/InputStreamFromROOTFile.inl" + +// #endif diff --git a/src/include/stir/IO/PETSIRDCListmodeInputFileFormat.h b/src/include/stir/IO/PETSIRDCListmodeInputFileFormat.h new file mode 100644 index 0000000000..ff291dbaa0 --- /dev/null +++ b/src/include/stir/IO/PETSIRDCListmodeInputFileFormat.h @@ -0,0 +1,78 @@ +/* PETSIRDCListmodeInputFileFormat.h + + Class defining input file format for coincidence listmode data for PETSIRD. + + Copyright 2025, UMCG + Copyright 2025 National Physical Laboratory + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +/*! + + \file + \ingroup listmode + \brief Declaration of class stir::PETSIRDCListmodeInputFileFormat + + \author Nikos Efthimiou + \author Daniel Deidda + +*/ + +#ifndef __stir_IO_PETSIRDCListmodeInputFileFormat_H__ +#define __stir_IO_PETSIRDCListmodeInputFileFormat_H__ + +// #include "boost/algorithm/string.hpp" + +#include "stir/IO/InputFileFormat.h" +#include "stir/error.h" + +#include "stir/listmode/CListModeDataPETSIRD.h" + +START_NAMESPACE_STIR + +/*! \brief Class for reading PETSIRD coincidence listmode data. + + The first 32 bytes of the binary file are interpreted as file signature and matched against the strings "MUPET CListModeData\0", +"PETSIRD". If either is successfull, the class claims it can read the file format. The +rest of the file is read as records, e.g. CListRecordPETSIRD. +*/ + +class PETSIRDCListmodeInputFileFormat : public InputFileFormat +{ +public: + const std::string get_name() const override { return "PETSIRD"; } + + //! Checks in binary data file for correct signature. + bool can_read(const FileSignature& signature, const std::string& filename) override; + +protected: + bool actual_can_read(const FileSignature& signature, std::istream& input) const override { return false; } + + bool use_hdf5 = false; + +public: + unique_ptr read_from_file(std::istream& input) const override + { + error("read_from_file for ROOT listmode data with istream not implemented %s:%s. Sorry", __FILE__, __LINE__); + return unique_ptr(); + } + + unique_ptr read_from_file(const std::string& filename) const override + { + info("PETSIRDCListmodeInputFileFormat: read_from_file(" + std::string(filename) + ")"); + return unique_ptr(new CListModeDataPETSIRD(filename, use_hdf5)); + } +}; +END_NAMESPACE_STIR +#endif diff --git a/src/include/stir/IO/SAFIRCListmodeInputFileFormat.h b/src/include/stir/IO/SAFIRCListmodeInputFileFormat.h index 3042d4dd55..ee72a67106 100644 --- a/src/include/stir/IO/SAFIRCListmodeInputFileFormat.h +++ b/src/include/stir/IO/SAFIRCListmodeInputFileFormat.h @@ -91,7 +91,7 @@ class SAFIRCListmodeInputFileFormat : public InputFileFormat, publ //! Checks in binary data file for correct signature (can be either "SAFIR CListModeData", "NeuroLF CListModeData" or "MUPET //! CListModeData"). - bool can_read(const FileSignature& signature, const std::string& filename) const override + bool can_read(const FileSignature& signature, const std::string& filename) override { // Looking for the right key in the parameter file std::ifstream par_file(filename.c_str()); @@ -140,10 +140,10 @@ class SAFIRCListmodeInputFileFormat : public InputFileFormat, publ std::unique_ptr read_from_file(const std::string& filename) const override { - info("SAFIRCListmodeInputFileFormat: read_from_file(" + std::string(filename) + ")"); - actual_do_parsing(filename); - return std::unique_ptr(new CListModeDataSAFIR>( - listmode_filename, crystal_map_filename, template_proj_data_filename, lor_randomization_sigma)); + // info("SAFIRCListmodeInputFileFormat: read_from_file(" + std::string(filename) + ")"); + // actual_do_parsing(filename); + // return std::unique_ptr(new CListModeDataSAFIR>( + // listmode_filename, crystal_map_filename, template_proj_data_filename, lor_randomization_sigma)); } protected: diff --git a/src/include/stir/PETSIRDInfo.h b/src/include/stir/PETSIRDInfo.h new file mode 100644 index 0000000000..b3af2bc977 --- /dev/null +++ b/src/include/stir/PETSIRDInfo.h @@ -0,0 +1,154 @@ +/* CListModeDataPETSIRD.h + +Coincidence LM Data Class for PETSIRD + + Copyright 2025, UMCG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +/*! + +\file +\ingroup listmode +\brief Place that converts PETSIRD geometry to STIR geometry + +\author Nikos Efthimiou +*/ + +#include "petsird/protocols.h" +#include "stir/Scanner.h" +#include "stir/DetectorCoordinateMap.h" +#include + +START_NAMESPACE_STIR + +/*! + \brief Comparator for ordering petsird::ExpandedDetectionBin in std::map. + + \details + Orders by module_index, then element_index, then energy_index. +*/ +struct ExpandedDetectionBinLess +{ + + bool operator()(const petsird::ExpandedDetectionBin& a, const petsird::ExpandedDetectionBin& b) const + { + // Adjust field names if needed (I assume: module, element, energy_bin) + if (a.module_index < b.module_index) + return true; + if (a.module_index > b.module_index) + return false; + + if (a.element_index < b.element_index) + return true; + if (a.element_index > b.element_index) + return false; + return a.energy_index < b.energy_index; + } +}; + +/*! + \brief Mapping type from PETSIRD ExpandedDetectionBin to STIR DetectionPosition. +*/ +using PETSIRDToSTIRDetectorIndexMap = std::map, ExpandedDetectionBinLess>; + +/*! + \brief Class to hold PETSIRD-related information for STIR and do any necessary conversions. +*/ +class PETSIRDInfo +{ +public: + explicit PETSIRDInfo(shared_ptr); + + // void initialize(); + + inline std::shared_ptr get_scanner_sptr() const + { + return stir_scanner_sptr; + } + + inline shared_ptr get_petsird_to_stir_map() const + { + return petsird_to_stir; + } + + inline shared_ptr get_petsird_map_sptr() const + { + return petsird_map_sptr; + } + + bool is_cylindrical_configuration() { return is_cylindrical; }; + +private: + /*! + \brief Infer scanner blocks and rotation axis from PETSIRD replicated modules. + \param unique_dim1_values Output set of unique translations along X. + \param unique_dim2_values Output set of unique translations along Y. + \param unique_dim3_values Output set of unique translations along Z. + \param replicated_module_list PETSIRD replicated detector modules. + \return Index of rotation axis (0=x, 1=y, 2=z) or -1 if not found. + */ + int figure_out_scanner_blocks_and_rotation_axis(std::set& unique_dim1_values, + std::set& unique_dim2_values, + std::set& unique_dim3_values); + /*! + \brief Compute unique module rotation angles around the given axis. + \param unique_angle_modules Output set of unique angles (radians). + \param rot_axis Rotation axis index (0=x, 1=y, 2=z). + */ + void figure_out_block_angles(std::set& unique_angle_modules, const int rot_axis); + /*! + \brief Determine block element translations and radius. + \param unique_dim1_values Output set of element translations along X. + \param unique_dim2_values Output set of element translations along Y. + \param unique_dim3_values Output set of element translations along Z. + \param radius Output inferred radius. + \param radius_index Output axis index containing the radius component. + \param rotation_axis Known rotation axis (0=x, 1=y, 2=z). + \param replicated_module_list PETSIRD replicated detector modules. +*/ + void figure_out_block_element_transformations(std::set& unique_dim1_values, + std::set& unique_dim2_values, + std::set& unique_dim3_values, + float& radius, + int& radius_index, + const int rotation_axis); + + //! Scanner information as provided by PETSIRD. + shared_ptr petsird_scanner_info_sptr; + //! Active scanner instance. + shared_ptr stir_scanner_sptr; + + //! Number of replicated modules. + uint32_t numberOfModules; + //! Number of element indices per module. + uint32_t numberOfElementsIndices; + //! Transaxial blocks per bucket (scanner metadata). + uint32_t blocks_per_bucket_transaxial; + //! Axial blocks per bucket (scanner metadata). + uint32_t blocks_per_bucket_axial; + //! Number of axial crystals per block. + uint32_t num_axial_crystals_per_block; + //! Number of transaxial crystals per block. + uint32_t num_trans_crystals_per_block; + + bool is_cylindrical = true; + //! Mapping from PETSIRD expanded bins to STIR detection positions. + shared_ptr petsird_to_stir; + //! Mapping from STIR detection positions to PETSIRD coordinates. + shared_ptr petsird_map_sptr; +}; + +END_NAMESPACE_STIR \ No newline at end of file diff --git a/src/include/stir/ProjDataInfoCylindricalNoArcCorr.inl b/src/include/stir/ProjDataInfoCylindricalNoArcCorr.inl index 9f3678de3f..ff37ac0ee2 100644 --- a/src/include/stir/ProjDataInfoCylindricalNoArcCorr.inl +++ b/src/include/stir/ProjDataInfoCylindricalNoArcCorr.inl @@ -124,7 +124,7 @@ ProjDataInfoCylindricalNoArcCorr::get_bin_for_det_pair( } else { - bin.timing_pos_num() = -timing_pos_num; + bin.timing_pos_num() = -timing_pos_num - (get_num_tof_poss() % 2 == 0); return get_segment_axial_pos_num_for_ring_pair(bin.segment_num(), bin.axial_pos_num(), ring_num2, ring_num1); } } diff --git a/src/include/stir/Scanner.h b/src/include/stir/Scanner.h index 5b2210c694..3ddd69be35 100644 --- a/src/include/stir/Scanner.h +++ b/src/include/stir/Scanner.h @@ -574,6 +574,8 @@ class Scanner shared_ptr get_detector_map_sptr() const { return detector_map_sptr; } + void set_detector_map(const DetectorCoordinateMap::det_pos_to_coord_type& coord_map); + private: bool _already_setup; Type type; @@ -628,7 +630,6 @@ class Scanner std::string crystal_map_file_name; shared_ptr detector_map_sptr; /*! effective detection positions including average DOI */ - void set_detector_map(const DetectorCoordinateMap::det_pos_to_coord_type& coord_map); void initialise_max_FOV_radius(); // function to create the maps diff --git a/src/include/stir/listmode/CListModeDataBasedOnCoordinateMap.h b/src/include/stir/listmode/CListModeDataBasedOnCoordinateMap.h new file mode 100644 index 0000000000..05cc87e845 --- /dev/null +++ b/src/include/stir/listmode/CListModeDataBasedOnCoordinateMap.h @@ -0,0 +1,72 @@ +/* CListModeDataSAFIR.h + + Coincidence LM Data Class for SAFIR: Header File + Jannis Fischer + + Copyright 2015 ETH Zurich, Institute of Particle Physics + Copyright 2020 Positrigo AG, Zurich + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +/*! + + \file + \ingroup listmode + \brief Declaration of class stir::CListModeDataSAFIR + + \author Jannis Fischer +*/ + +#ifndef __stir_listmode_CListModeDataBasedOnCoordinateMap_H__ +#define __stir_listmode_CListModeDataBasedOnCoordinateMap_H__ + +#include +#include + +#include "stir/listmode/CListModeData.h" +#include "stir/ProjData.h" +#include "stir/ProjDataInfo.h" +#include "stir/listmode/CListRecord.h" +#include "stir/IO/InputStreamWithRecords.h" +#include "stir/shared_ptr.h" + +// #include "stir/listmode/CListRecordSAFIR.h" + + +START_NAMESPACE_STIR + +class CListModeDataBasedOnCoordinateMap : public CListModeData +{ +public: + std::string get_name() const override; + + // virtual shared_ptr> get_current_lm_file() = 0; + + SavedPosition save_get_position() override = 0; + + Succeeded set_get_position(const SavedPosition& pos) override = 0; + +protected: + std::string listmode_filename; + + mutable std::vector saved_get_positions; + virtual Succeeded open_lm_file() const = 0; + + shared_ptr map; +}; + +END_NAMESPACE_STIR + +#endif diff --git a/src/include/stir/listmode/CListModeDataPETSIRD.h b/src/include/stir/listmode/CListModeDataPETSIRD.h new file mode 100644 index 0000000000..7e10311754 --- /dev/null +++ b/src/include/stir/listmode/CListModeDataPETSIRD.h @@ -0,0 +1,134 @@ +/* CListModeDataPETSIRD.h + +Coincidence LM Data Class for PETSIRD + + Copyright 2025, UMCG + Copyright 2025, MGH / HST A. Martinos Center for Biomedical Imaging + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +/*! + +\file +\ingroup listmode +\brief Declaration of class stir::CListModeDataPETSIRD + +\author Daniel Deidda +\author Nikos Efthimiou +*/ + +#ifndef __stir_listmode_CListModeDataPETSIRD_H__ +#define __stir_listmode_CListModeDataPETSIRD_H__ + +#include "stir/listmode/CListModeDataBasedOnCoordinateMap.h" +#include "stir/ProjData.h" +#include "stir/listmode/CListRecord.h" +#include "stir/shared_ptr.h" + +#include "stir/PETSIRDInfo.h" + +START_NAMESPACE_STIR + +/*! + \class CListModeDataPETSIRD + \brief Reader for PETSIRD listmode data supporting variable geometry. + \ingroup listmode + + \par Overview + - Supports HDF5 and binary PETSIRD formats. + - Infers scanner geometry: + - Cylindrical → creates cylindrical scanner. + - Block-based → creates block-based scanner. + - Otherwise → creates generic scanner using crystal positions. + - Builds a DetectorCoordinateMap when needed and stores to disk. + \par + + Infering the scanner geometry makes a lot of assumptions about what PET is. + In particular, it assumes: + \li A PET scanner is made of rings of detectors + \li The largest axis is the axial one. + \li So far we support only a single layer. This is partly hard-coded for simplicity. (look in the code for relevant TODOs and + comments.) + \li Some of the hardcoded assumptions are in CListRecordPETSIRD as well. + + \note Exact PETSIRD format specification is defined in the PETSIRD project documentation. (NOTE: It looks like GATE.) + \note Initially, I wanted to: + - Is close to a cylindrical geometry ? + - then yes use a cylindrical scanner that is simpler. + - Else, is it made of blocks arranged on a cylinder. + + However, now I do the following: + - Is close to cylindriacl geometry? + - yes use cylindrical scanner + - Check if blocks-on-cylinder configuration, are a good match. + - yes use blocks-on-cylinder scanner + - else use generic scanner and export the map to the disk. + + If listmode reconstruciton is done, the map is regenerated on-the-fly. + +*/ +class CListModeDataPETSIRD : public CListModeDataBasedOnCoordinateMap +{ +public: + /*! + \brief Construct reader. + \param listmode_filename Path to PETSIRD listmode file. + \param use_hdf5 If true, use HDF5 reader; otherwise use binary reader. + */ + CListModeDataPETSIRD(const std::string& listmode_filename, bool use_hdf5); + + virtual shared_ptr get_empty_record_sptr() const override; + + Succeeded get_next_record(CListRecord& record_of_general_type) const override; + + SavedPosition save_get_position() override { return static_cast(curr_event_in_event_block); } + + Succeeded set_get_position(const SavedPosition& pos) override { return Succeeded::yes; } + + virtual bool has_delayeds() const override { return m_has_delayeds; } + + Succeeded reset() override { return Succeeded::yes; } + +protected: + virtual Succeeded open_lm_file() const override; + + mutable shared_ptr current_lm_data_ptr; + +private: + //! Whether to use the HDF5 reader. + const bool use_hdf5; + + mutable unsigned long int curr_event_in_event_block = 0; + + mutable petsird::TimeBlock curr_time_block; + + mutable petsird::EventTimeBlock curr_event_block; + + //! Active module pair (prompt/delayed, or two modules for coincidences). + //! \todo: This hard-codes a single/matterial layer detector assumption. + petsird::TypeOfModulePair type_of_module_pair{ 0, 0 }; + + //! Current event prompt flag. + mutable bool curr_is_prompt = true; + //! Whether delayed events are present. + mutable bool m_has_delayeds; + + shared_ptr scanner_info; + + shared_ptr petsird_info_sptr; +}; + +END_NAMESPACE_STIR +#endif // CLISTMODEDATAPETSIRD_H diff --git a/src/include/stir/listmode/CListModeDataSAFIR.h b/src/include/stir/listmode/CListModeDataSAFIR.h index 66ca4344e7..d8d400daf7 100644 --- a/src/include/stir/listmode/CListModeDataSAFIR.h +++ b/src/include/stir/listmode/CListModeDataSAFIR.h @@ -1,52 +1,46 @@ /* CListModeDataSAFIR.h - Coincidence LM Data Class for SAFIR: Header File - Jannis Fischer +Coincidence LM Data Class for SAFIR: Header File +Jannis Fischer - Copyright 2015 ETH Zurich, Institute of Particle Physics - Copyright 2020 Positrigo AG, Zurich + Copyright 2015 ETH Zurich, Institute of Particle Physics + Copyright 2020 Positrigo AG, Zurich - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ /*! - \file - \ingroup listmode - \brief Declaration of class stir::CListModeDataSAFIR +\file +\ingroup listmode +\brief Declaration of class stir::CListModeDataSAFIR - \author Jannis Fischer +\author Jannis Fischer */ #ifndef __stir_listmode_CListModeDataSAFIR_H__ #define __stir_listmode_CListModeDataSAFIR_H__ -#include #include -#include -#include -#include "stir/listmode/CListModeData.h" +#include "stir/listmode/CListModeDataBasedOnCoordinateMap.h" #include "stir/ProjData.h" #include "stir/ProjDataInfo.h" #include "stir/listmode/CListRecord.h" #include "stir/IO/InputStreamWithRecords.h" #include "stir/shared_ptr.h" -#include "stir/listmode/CListRecordSAFIR.h" -#include "stir/DetectorCoordinateMap.h" - START_NAMESPACE_STIR /*! @@ -57,47 +51,31 @@ START_NAMESPACE_STIR coordinates. */ template -class CListModeDataSAFIR : public CListModeData +class CListModeDataSAFIR : public CListModeDataBasedOnCoordinateMap { public: - /*! Constructor - \par - Takes as arguments the filenames of the coicidence listmode file, the crystal map (text) file, and the template projection data - file - */ CListModeDataSAFIR(const std::string& listmode_filename, const std::string& crystal_map_filename, const std::string& template_proj_data_filename, const double lor_randomization_sigma = 0.0); + CListModeDataSAFIR(const std::string& listmode_filename, const shared_ptr& proj_data_info_sptr); - std::string get_name() const override; - shared_ptr get_empty_record_sptr() const override; - Succeeded get_next_record(CListRecord& record_of_general_type) const override; + shared_ptr get_empty_record_sptr() const override; + Succeeded get_next_record(CListRecordT& record_of_general_type) const override; + + bool has_delayeds() const override { return false; } + Succeeded reset() override; - /*! - This function should save the position in input file. This is not implemented but disabled. - Returns 0 in the moement. - \todo Maybe provide real implementation? - */ SavedPosition save_get_position() override { return static_cast(current_lm_data_ptr->save_get_position()); } - Succeeded set_get_position(const SavedPosition& pos) override { return current_lm_data_ptr->set_get_position(pos); } - /*! - Returns just false in the moment. - \todo Implement this properly to check for delayed events in LM files. - */ - bool has_delayeds() const override { return false; } + Succeeded set_get_position(const SavedPosition& pos) override { return current_lm_data_ptr->set_get_position(pos); } -private: - std::string listmode_filename; +protected: + virtual Succeeded open_lm_file() const override; mutable shared_ptr> current_lm_data_ptr; - mutable std::vector saved_get_positions; - Succeeded open_lm_file() const; - shared_ptr map; }; END_NAMESPACE_STIR - -#endif +#endif // CLISTMODEDATASAFIR_H diff --git a/src/include/stir/listmode/CListRecordPETSIRD.h b/src/include/stir/listmode/CListRecordPETSIRD.h new file mode 100644 index 0000000000..e96c3471be --- /dev/null +++ b/src/include/stir/listmode/CListRecordPETSIRD.h @@ -0,0 +1,156 @@ +/* CListRecordPETSIRD.h + +Coincidence Event Class for PETSIRD: Header File + + Copyright 2025, UMCG + Copyright 2025 National Physical Laboratory + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +/*! + +\file +\ingroup listmode +\brief Declaration of class stir::CListEventPETSIRD and stir::CListRecordPETSIRD with supporting classes + +\author Nikos Efthimiou +\author Daniel Deidda +*/ + +#ifndef __stir_listmode_CListRecordPETSIRD_H__ +#define __stir_listmode_CListRecordPETSIRD_H__ + +#include "stir/listmode/CListRecord.h" +#include "stir/DetectionPositionPair.h" +#include "stir/Succeeded.h" +#include "stir/ByteOrderDefine.h" +#include "boost/cstdint.hpp" + +#include "stir/DetectorCoordinateMap.h" + +START_NAMESPACE_STIR + +/*! +Provides interface of the record class to STIR by implementing get_LOR(). It uses an optional map from detector indices to +coordinates to specify LORAs2Points from given detection pair indices. + +\ingroup listmode +*/ + +class CListEventPETSIRD : public CListEvent +{ +public: + inline CListEventPETSIRD() {} + + //! Returns LOR corresponding to the given event. + inline LORAs2Points get_LOR() const override; + + //! Override the default implementation + inline void get_bin(Bin& bin, const ProjDataInfo& proj_data_info) const override; + + inline void set_map_sptr(shared_ptr new_map_sptr) { map_sptr = new_map_sptr; } + + inline void set_petsird_to_stir_map(shared_ptr new_map) { petsird_to_stir = new_map; } + + inline bool is_valid_template(const ProjDataInfo&) const override { return true; } + + inline bool is_prompt() const override { return m_prompt; } + + inline Succeeded set_prompt(const bool prompt) override + { + m_prompt = prompt; + return Succeeded::yes; + } + + inline void set_expanded_detection_bins(const petsird_helpers::ExpandedDetectionBin& det0, + const petsird_helpers::ExpandedDetectionBin& det1, + const uint32_t tof_idx) + { + exp_det_0 = det0; + exp_det_1 = det1; + m_tof_bin = tof_idx; + } + + inline void set_tof_bin(const uint32_t value) { m_tof_bin = value; } + + inline stir::DetectionPosition<> + get_stir_det_pos_from_PETSIRD_id(const petsird_helpers::ExpandedDetectionBin& exp_det_bin) const; + +private: + shared_ptr map_sptr = nullptr; + shared_ptr petsird_to_stir = nullptr; + + bool m_prompt; + petsird_helpers::ExpandedDetectionBin exp_det_0, exp_det_1; + uint32_t m_tof_bin; +}; + +class CListTimePETSIRD : public ListTime +{ +public: + inline unsigned long get_time_in_millisecs() const { return static_cast(time); } + inline Succeeded set_time_in_millisecs(const unsigned long time_in_millisecs) + { + time = time_in_millisecs; + return Succeeded::yes; + } + inline bool is_time() const { return true; } + uint32_t time; +}; + +class CListRecordPETSIRD : public CListRecord +{ +public: + CListRecordPETSIRD() + { + } + + // ~CListRecordPETSIRD() override {} + + bool is_time() const override { return true; /*time_data.is_time();*/ } + + bool is_event() const override { return true; } + + CListEventPETSIRD& event() override { return event_data; } + const CListEventPETSIRD& event() const override { /*return event_data;*/ } + + CListTimePETSIRD& time() override { return time_data; } + const CListTimePETSIRD& time() const override { return time_data; } + + bool operator==(const CListRecordPETSIRD& e2) const + { + // return dynamic_cast(&e2) != 0 && raw == static_cast(e2).r; + } + + virtual Succeeded init_from_data(const petsird_helpers::ExpandedDetectionBin& det0, + const petsird_helpers::ExpandedDetectionBin& det1, + const uint32_t tof_idx, + const bool is_prompt = true) + { + event_data.set_expanded_detection_bins(det0, det1, tof_idx); + event_data.set_prompt(is_prompt); + return Succeeded::yes; + } + +private: + CListEventPETSIRD event_data; + CListTimePETSIRD time_data; +}; + +END_NAMESPACE_STIR + +#include "CListRecordPETSIRD.inl" + +#endif diff --git a/src/include/stir/listmode/CListRecordPETSIRD.inl b/src/include/stir/listmode/CListRecordPETSIRD.inl new file mode 100644 index 0000000000..cfa43e68b8 --- /dev/null +++ b/src/include/stir/listmode/CListRecordPETSIRD.inl @@ -0,0 +1,141 @@ +/* CListRecordPETSIRD.inl + Coincidence Event Class for PETSIRD: Inline File + + Copyright 2025, UMCG + Copyright 2025 National Physical Laboratory + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +/*! + + \file + \ingroup listmode + \brief Inline implementation of class stir::CListEventPETSIRD and stir::CListRecordPETSIRD with supporting classes + + \author Nikos Efthimiou + \author Daniel Deidda +*/ + +#include "stir/LORCoordinates.h" +#include "stir/listmode/CListRecord.h" +#include "stir/ProjDataInfo.h" +#include "stir/Bin.h" +#include "stir/CartesianCoordinate3D.h" +#include "stir/error.h" +#include "stir/ProjDataInfoCylindricalNoArcCorr.h" +#include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" +#include "stir/ProjDataInfoGenericNoArcCorr.h" + +START_NAMESPACE_STIR + +stir::DetectionPosition<> +CListEventPETSIRD::get_stir_det_pos_from_PETSIRD_id(const petsird::ExpandedDetectionBin& exp_det_bin) const +{ + // const-friendly lookup + auto it = petsird_to_stir->find(exp_det_bin); + if (it == petsird_to_stir->end()) + { + // handle missing key however STIR usually does: + // - throw + // - or call error(...) + // - or return a default DetectionPosition + error("get_stir_det_pos_from_PETSIRD_id: PETSIRD id not found in petsird_to_stir map", + exp_det_bin.module_index, + exp_det_bin.element_index, + exp_det_bin.energy_index); + } + + return it->second; // copy of DetectionPosition<> +} + +LORAs2Points +CListEventPETSIRD::get_LOR() const +{ + LORAs2Points lor; + DetectionPositionPair<> det_pos_pair; + + det_pos_pair.pos1() = get_stir_det_pos_from_PETSIRD_id(exp_det_0); + det_pos_pair.pos2() = get_stir_det_pos_from_PETSIRD_id(exp_det_1); + + // lor.p1() = map_to_use().get_coordinate_for_index(det_pos_pair.pos1()); + // lor.p2() = map_to_use().get_coordinate_for_index(det_pos_pair.pos2()); + + // std::cout << "lor_p1 " << lor.p1().x() << " " << lor.p1().y() << " " << lor.p1().z() << std::endl; + // std::cout << "lor_p2 " < det_pos_pair; + + if (proj_data_info.get_scanner_sptr()->get_scanner_geometry() == "Cylindrical") + { + if (!petsird_to_stir) + { + error("CListEventPETSIRD::get_bin: petsird_to_stir map not set. Probably your ProjDataInfo point to a Generic Scanner. \n The Scanner in the Listmode data and the one in the ProjDataInfo must match."); + } + + det_pos_pair.pos1() = get_stir_det_pos_from_PETSIRD_id(exp_det_1); + det_pos_pair.pos2() = get_stir_det_pos_from_PETSIRD_id(exp_det_0); + det_pos_pair.timing_pos() = static_cast(m_tof_bin) + (proj_data_info.get_min_tof_pos_num() ); + dynamic_cast(proj_data_info).get_bin_for_det_pos_pair(bin, det_pos_pair); + } + else if (proj_data_info.get_scanner_sptr()->get_scanner_geometry() == "Generic") + { + // if (!map_sptr) + // { + // std::cerr << "Error: No detector map set in CListEventPETSIRD::get_bin()" << std::endl; + // // this->get_data().get_detection_position_pair(det_pos_pair); + // } + // else{ + // DetectionPositionPair<> det_pos_pair; + // det_pos_pair.pos1() = get_stir_det_pos_from_PETSIRD_id(exp_det_0); + // det_pos_pair.pos2() = get_stir_det_pos_from_PETSIRD_id(exp_det_1); + // // std::cout<< exp_det_0.module_index << ", " << exp_det_0.element_index << " ---- " << exp_det_1.module_index + // << ", " << exp_det_1.element_index << std::endl; const stir::CartesianCoordinate3D c1 = + // map_sptr->get_coordinate_for_index(det_pos_pair.pos1()); const stir::CartesianCoordinate3D c2 = + // map_sptr->get_coordinate_for_index(det_pos_pair.pos2()); + + // // std::cout << "CListEventPETSIRD::get_bin(): det_pos1: " << det_pos_pair.pos1().tangential_coord() << ", " + // // << det_pos_pair.pos1().axial_coord() << ", " << det_pos_pair.pos1().radial_coord() << std::endl; + // // std::cout << "CListEventPETSIRD::get_bin(): det_pos2: " << det_pos_pair.pos2().tangential_coord() << ", " + // // << det_pos_pair.pos2().axial_coord() << ", " << det_pos_pair.pos2().radial_coord() << std::endl; + // // std::cout << "CListEventPETSIRD::get_bin(): c1: " << c1.x() << ", " << c1.y() << ", " << c1.z() << + // std::endl; + // // std::cout << "CListEventPETSIRD::get_bin(): c2: " << c2.x() << ", " << c2.y() << ", " << c2.z() << + // std::endl; const LORAs2Points lor(c1, c2); bin = proj_data_info.get_bin(lor); + // } + } + else if (proj_data_info.get_scanner_sptr()->get_scanner_geometry() == "BlocksOnCylindrical") + { + if (!petsird_to_stir) + { + error("CListEventPETSIRD::get_bin: petsird_to_stir map not set. Probably your ProjDataInfo point to a Generic Scanner. \n The Scanner in the Listmode data and the one in the ProjDataInfo must match."); + } + det_pos_pair.pos1() = (*petsird_to_stir)[exp_det_1]; + det_pos_pair.pos2() = (*petsird_to_stir)[exp_det_0]; + dynamic_cast(proj_data_info).get_bin_for_det_pos_pair(bin, det_pos_pair); + } + else + { + error("CListEventPETSIRD::get_bin: How did I get with an unsupported scanner geometry ? -", + proj_data_info.get_scanner_sptr()->get_scanner_geometry()); + } +} + +END_NAMESPACE_STIR diff --git a/src/include/stir/recon_buildblock/BinNormalisationFromPETSIRD.h b/src/include/stir/recon_buildblock/BinNormalisationFromPETSIRD.h new file mode 100644 index 0000000000..5b594f2dfd --- /dev/null +++ b/src/include/stir/recon_buildblock/BinNormalisationFromPETSIRD.h @@ -0,0 +1,67 @@ +// // +// // +// /*! +// \file +// \ingroup normalisation + +// \brief Declaration of class stir::BinNormalisationFromPETSIRD + +// \author Nikos Efthimiou +// */ +// /* +// Copyright (C) 2025, UMCG +// This file is part of STIR. + +// SPDX-License-Identifier: Apache-2.0 + +// See STIR/LICENSE.txt for details +// */ + + +// #ifndef __stir_recon_buildblock_BinNormalisationFromPETSIRD_H__ +// #define __stir_recon_buildblock_BinNormalisationFromPETSIRD_H__ + +// #include "stir/recon_buildblock/BinNormalisation.h" +// #include "stir/recon_buildblock/BinNormalisationWithCalibration.h" +// #include "stir/RegisteredParsingObject.h" +// #include "stir/ProjData.h" +// #include "stir/shared_ptr.h" + +// using std::string; + +// START_NAMESPACE_STIR + +// class BinNormalisationFromPETSIRD : public RegisteredParsingObject +// { +// private: +// using base_type = BinNormalisation; + +// public: +// //! Name which will be used when parsing a BinNormalisation object +// static const char* const registered_name; + +// BinNormalisationFromPETSIRD(); + +// BinNormalisationFromPETSIRD(const std::string& filename); + +// Succeeded set_up(const shared_ptr& exam_info_sptr, const shared_ptr&) override; + + +// private: + +// void set_defaults() override; + +// void initialise_keymap() override; + +// bool post_processing() override; + +// void read_norm_data(const string& filename); + +// string normalisation_filename; + +// }; + + +// END_NAMESPACE_STIR + +// #endif diff --git a/src/listmode_buildblock/CListModeDataBasedOnCoordinateMap.cxx b/src/listmode_buildblock/CListModeDataBasedOnCoordinateMap.cxx new file mode 100644 index 0000000000..fda23351d5 --- /dev/null +++ b/src/listmode_buildblock/CListModeDataBasedOnCoordinateMap.cxx @@ -0,0 +1,50 @@ +/* CListModeDataSAFIR.cxx + +Coincidence LM Data Class for SAFIR: Implementation + + Copyright 2015 ETH Zurich, Institute of Particle Physics + Copyright 2020 Positrigo AG, Zurich + Copyright 2021 University College London + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +/*! + + \file + \ingroup listmode + \brief implementation of class stir::CListModeDataSAFIR + + \author Jannis Fischer + \author Kris Thielemans + \author Markus Jehl +*/ +#include +#include +#include "stir/Succeeded.h" + +#include "stir/listmode/CListModeDataBasedOnCoordinateMap.h" + +using std::ios; +using std::fstream; +using std::ifstream; +using std::istream; + +START_NAMESPACE_STIR; + +std::string +CListModeDataBasedOnCoordinateMap::get_name() const +{ + return listmode_filename; +} + +END_NAMESPACE_STIR diff --git a/src/listmode_buildblock/CListModeDataPETSIRD.cxx b/src/listmode_buildblock/CListModeDataPETSIRD.cxx new file mode 100644 index 0000000000..4fe420d463 --- /dev/null +++ b/src/listmode_buildblock/CListModeDataPETSIRD.cxx @@ -0,0 +1,203 @@ +/* CListModeDataPETSIRD.cxx + +Coincidence LM Data Class for PETSIRD: Implementation + + Copyright 2025, UMCG + Copyright 2025, MGH / HST A. Martinos Center for Biomedical Imaging + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +/*! + +\file +\ingroup listmode +\brief implementation of class stir::CListModeDataPETSIRD + +\author Daniel Deidda +\author Nikos Efthimiou +*/ + +#include "stir/Succeeded.h" +#include "stir/info.h" +#include "stir/error.h" + +#include "petsird_helpers.h" +#include "petsird_helpers/create.h" +#include "petsird_helpers/geometry.h" + +#include "petsird/binary/protocols.h" +#include "petsird/hdf5/protocols.h" + +#include "stir/listmode/CListModeDataPETSIRD.h" +#include "stir/listmode/CListRecordPETSIRD.h" +#include + +START_NAMESPACE_STIR + +CListModeDataPETSIRD::CListModeDataPETSIRD(const std::string& listmode_filename, bool use_hdf5) + : use_hdf5(use_hdf5) +{ + CListModeDataBasedOnCoordinateMap::listmode_filename = listmode_filename; + + petsird::Header header; + if (use_hdf5) + current_lm_data_ptr.reset(new petsird::hdf5::PETSIRDReader(listmode_filename)); + else + current_lm_data_ptr.reset(new petsird::binary::PETSIRDReader(listmode_filename)); + + m_has_delayeds = false; + + current_lm_data_ptr->ReadHeader(header); + scanner_info = std::make_shared(header.scanner); + // std::vector replicated_module_list = scanner_info->scanner_geometry.replicated_modules; + + // Get the first TimeBlock + // if ( + current_lm_data_ptr->ReadTimeBlocks(curr_time_block); + // ) + // error("CListModeDataPETSIRD: Could not read the first TimeBlock. Abord."); + + if (std::holds_alternative(curr_time_block)) + curr_event_block = std::get(curr_time_block); + else + error("CListModeDataPETSIRD: holds_alternative not true. Abord."); + + petsird_info_sptr = std::make_shared(scanner_info); + auto stir_scanner_sptr = petsird_info_sptr->get_scanner_sptr(); + + int tof_mash_factor = 1; + this->set_proj_data_info_sptr(std::const_pointer_cast( + ProjDataInfo::construct_proj_data_info(petsird_info_sptr->get_scanner_sptr(), + 1, + petsird_info_sptr->get_scanner_sptr()->get_num_rings() - 1, + petsird_info_sptr->get_scanner_sptr()->get_num_detectors_per_ring() / 2, + petsird_info_sptr->get_scanner_sptr()->get_max_num_non_arccorrected_bins(), + /* arc_correction*/ false, + tof_mash_factor) + ->create_shared_clone())); + + shared_ptr _exam_info_sptr(new ExamInfo); + // Only PET scanners supported + _exam_info_sptr->imaging_modality = ImagingModality::PT; + _exam_info_sptr->originating_system = std::string("PETSIRD_defined_scanner"); + // _exam_info_sptr->set_low_energy_thres(scanner_i); + // _exam_info_sptr->set_high_energy_thres(this->root_file_sptr->get_up_energy_thres()); + + this->exam_info_sptr = _exam_info_sptr; + + // N.E.: In my experience the first time block is always empty. + // So I use this unncessessary call to skip to the next. + if (this->open_lm_file() == Succeeded::no) + { + error("CListModeDataPETSIRD: Could not open listmode file " + listmode_filename + "\n"); + } +} + +Succeeded +CListModeDataPETSIRD::open_lm_file() const +{ + // current_lm_data_ptr.reset(new petsird::hdf5::PETSIRDReader(listmode_filename)); + if (!current_lm_data_ptr->ReadTimeBlocks(curr_time_block)) + return Succeeded::no; + curr_event_block = std::get(curr_time_block); + return Succeeded::yes; +} + +shared_ptr +CListModeDataPETSIRD::get_empty_record_sptr() const +{ + shared_ptr sptr( + new CListRecordPETSIRD()); //, this->map, petsird_to_stir)); + if (petsird_info_sptr->get_scanner_sptr()->get_scanner_geometry() == "Generic") + { + std::dynamic_pointer_cast(sptr)->event().set_map_sptr(this->map); + } + else if (petsird_info_sptr->get_scanner_sptr()->get_scanner_geometry() == "BlocksOnCylindrical" + || petsird_info_sptr->get_scanner_sptr()->get_scanner_geometry() == "Cylindrical") + { + std::dynamic_pointer_cast(sptr)->event().set_petsird_to_stir_map( + petsird_info_sptr->get_petsird_to_stir_map()); + } + + // std::dynamic_pointer_cast(sptr)->event().set_scanner_sptr( + // this->get_proj_data_info_sptr()->get_scanner_sptr()); + // std::dynamic_pointer_cast(sptr)->event().set_PETSIRD_ranges(numberOfModules, numberOfElementsIndices); + // std::dynamic_pointer_cast(sptr)->event_PETSIRD().set_map_sptr(map); + + return sptr; +} + +Succeeded +CListModeDataPETSIRD::get_next_record(CListRecord& record_of_general_type) const +{ + + auto& record = dynamic_cast(record_of_general_type); + const auto& prompt_list = curr_event_block.prompt_events.at(0).at(0); // TODO: support mulitple pairs of modules. + const auto& delayed_list = m_has_delayeds ? curr_event_block.delayed_events.at(0).at(0) : prompt_list; + + const auto& event_list = curr_is_prompt ? prompt_list : delayed_list; + + if (event_list.size() == 0) + return Succeeded::no; + + auto event = event_list.at(curr_event_in_event_block); + + if (record.init_from_data( + petsird_helpers::expand_detection_bin(*scanner_info, + 0, // TODO type_of_module, currently we only support single module types. + event.detection_bins[0]), + petsird_helpers::expand_detection_bin(*scanner_info, + 0, // TODO type_of_module, currently we only support single module types. + event.detection_bins[1]), event.tof_idx, + curr_is_prompt) + == Succeeded::no + || record_of_general_type.time().set_time_in_millisecs(curr_event_block.time_interval.start) == Succeeded::no) + { + return Succeeded::no; + } + + ++curr_event_in_event_block; + + if (curr_event_in_event_block < event_list.size()) + { + return Succeeded::yes; + } + + // - Once we hit the size of the vector + curr_event_in_event_block = 0; + + if (!m_has_delayeds || curr_is_prompt) + { + if (m_has_delayeds) + { + curr_is_prompt = false; + } + else + { + if (!current_lm_data_ptr->ReadTimeBlocks(curr_time_block)) + return Succeeded::no; + curr_event_block = std::get(curr_time_block); + } + } + else + { + curr_is_prompt = true; + if (!current_lm_data_ptr->ReadTimeBlocks(curr_time_block)) + return Succeeded::no; + curr_event_block = std::get(curr_time_block); + } + + return Succeeded::yes; +} + +END_NAMESPACE_STIR diff --git a/src/listmode_buildblock/CListModeDataSAFIR.cxx b/src/listmode_buildblock/CListModeDataSAFIR.cxx index 96aa9a3cb3..e584b6fdd6 100644 --- a/src/listmode_buildblock/CListModeDataSAFIR.cxx +++ b/src/listmode_buildblock/CListModeDataSAFIR.cxx @@ -2,42 +2,41 @@ Coincidence LM Data Class for SAFIR: Implementation - Copyright 2015 ETH Zurich, Institute of Particle Physics - Copyright 2020 Positrigo AG, Zurich - Copyright 2021 University College London + Copyright 2015 ETH Zurich, Institute of Particle Physics + Copyright 2020 Positrigo AG, Zurich + Copyright 2021 University College London - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ /*! - \file - \ingroup listmode - \brief implementation of class stir::CListModeDataSAFIR +\file +\ingroup listmode +\brief implementation of class stir::CListModeDataSAFIR - \author Jannis Fischer - \author Kris Thielemans - \author Markus Jehl +\author Jannis Fischer +\author Kris Thielemans +\author Markus Jehl */ #include #include -#include #include "stir/ExamInfo.h" #include "stir/Succeeded.h" #include "stir/info.h" #include "stir/error.h" -//#include "boost/static_assert.hpp" +// #include "boost/static_assert.hpp" #include "stir/listmode/CListModeDataSAFIR.h" #include "stir/listmode/CListRecordSAFIR.h" @@ -54,11 +53,11 @@ CListModeDataSAFIR::CListModeDataSAFIR(const std::string& listmode const std::string& crystal_map_filename, const std::string& template_proj_data_filename, const double lor_randomization_sigma) - : listmode_filename(listmode_filename) { + CListModeDataBasedOnCoordinateMap::listmode_filename = listmode_filename; if (!crystal_map_filename.empty()) { - map = MAKE_SHARED(crystal_map_filename, lor_randomization_sigma); + this->map = MAKE_SHARED(crystal_map_filename, lor_randomization_sigma); } else { @@ -73,7 +72,7 @@ CListModeDataSAFIR::CListModeDataSAFIR(const std::string& listmode shared_ptr template_proj_data_sptr = ProjData::read_from_file(template_proj_data_filename); this->set_proj_data_info_sptr(template_proj_data_sptr->get_proj_data_info_sptr()->create_shared_clone()); - if (open_lm_file() == Succeeded::no) + if (this->open_lm_file() == Succeeded::no) { error("CListModeDataSAFIR: Could not open listmode file " + listmode_filename + "\n"); } @@ -82,8 +81,8 @@ CListModeDataSAFIR::CListModeDataSAFIR(const std::string& listmode template CListModeDataSAFIR::CListModeDataSAFIR(const std::string& listmode_filename, const shared_ptr& proj_data_info_sptr) - : listmode_filename(listmode_filename) { + CListModeDataBasedOnCoordinateMap::listmode_filename = listmode_filename; shared_ptr _exam_info_sptr(new ExamInfo); _exam_info_sptr->imaging_modality = ImagingModality::PT; this->exam_info_sptr = _exam_info_sptr; @@ -96,14 +95,26 @@ CListModeDataSAFIR::CListModeDataSAFIR(const std::string& listmode } template -std::string -CListModeDataSAFIR::get_name() const +Succeeded +CListModeDataSAFIR::open_lm_file() const { - return listmode_filename; + shared_ptr stream_ptr(new fstream(this->listmode_filename.c_str(), ios::in | ios::binary)); + if (!(*stream_ptr)) + { + return Succeeded::no; + } + info("CListModeDataSAFIR: opening file \"" + this->listmode_filename + "\"", 2); + stream_ptr->seekg((std::streamoff)32); + this->current_lm_data_ptr.reset( + new InputStreamWithRecords(stream_ptr, + sizeof(CListTimeDataSAFIR), + sizeof(CListTimeDataSAFIR), + ByteOrder::little_endian != ByteOrder::get_native_order())); + return Succeeded::yes; } template -shared_ptr +shared_ptr CListModeDataSAFIR::get_empty_record_sptr() const { shared_ptr sptr(new CListRecordT); @@ -114,7 +125,7 @@ CListModeDataSAFIR::get_empty_record_sptr() const template Succeeded -CListModeDataSAFIR::get_next_record(CListRecord& record_of_general_type) const +CListModeDataSAFIR::get_next_record(CListRecordT& record_of_general_type) const { CListRecordT& record = static_cast(record_of_general_type); Succeeded status = current_lm_data_ptr->get_next_record(record); @@ -129,26 +140,7 @@ CListModeDataSAFIR::reset() return current_lm_data_ptr->reset(); } -template -Succeeded -CListModeDataSAFIR::open_lm_file() const -{ - shared_ptr stream_ptr(new fstream(listmode_filename.c_str(), ios::in | ios::binary)); - if (!(*stream_ptr)) - { - return Succeeded::no; - } - info("CListModeDataSAFIR: opening file \"" + listmode_filename + "\"", 2); - stream_ptr->seekg((std::streamoff)32); - current_lm_data_ptr.reset( - new InputStreamWithRecords(stream_ptr, - sizeof(CListTimeDataSAFIR), - sizeof(CListTimeDataSAFIR), - ByteOrder::little_endian != ByteOrder::get_native_order())); - return Succeeded::yes; -} - -template class CListModeDataSAFIR>; -template class CListModeDataSAFIR>; +// template class CListModeDataSAFIR>; +// template class CListModeDataSAFIR>; END_NAMESPACE_STIR diff --git a/src/listmode_buildblock/CMakeLists.txt b/src/listmode_buildblock/CMakeLists.txt index 9ac6b1e752..e5d3dc2b7a 100644 --- a/src/listmode_buildblock/CMakeLists.txt +++ b/src/listmode_buildblock/CMakeLists.txt @@ -14,7 +14,8 @@ set(${dir_LIB_SOURCES} LmToProjDataWithRandomRejection.cxx CListModeDataECAT8_32bit.cxx CListRecordECAT8_32bit.cxx - CListModeDataSAFIR.cxx + CListModeDataBasedOnCoordinateMap.cxx + CListModeDataSAFIR.cxx ) if (HAVE_HDF5) @@ -24,6 +25,12 @@ list(APPEND ${dir_LIB_SOURCES} ) endif() +if (HAVE_PETSIRD) +list(APPEND ${dir_LIB_SOURCES} + CListModeDataPETSIRD.cxx +) +endif() + if (HAVE_ECAT) list(APPEND ${dir_LIB_SOURCES} CListModeDataECAT.cxx @@ -63,3 +70,7 @@ if (HAVE_HDF5) # for GEHDF5, TODO remove once IO dependency added or GEHDF5Wrapper no longer includes H5Cpp.h target_include_directories(listmode_buildblock PRIVATE ${HDF5_INCLUDE_DIRS}) endif() + +if (HAVE_PETSIRD) +target_link_libraries(listmode_buildblock PUBLIC petsird_generated) +endif() diff --git a/src/listmode_buildblock/LmToProjData.cxx b/src/listmode_buildblock/LmToProjData.cxx index 87e62b8ad5..37490f3699 100644 --- a/src/listmode_buildblock/LmToProjData.cxx +++ b/src/listmode_buildblock/LmToProjData.cxx @@ -38,7 +38,7 @@ USE_SegmentByView // (Note: can currently NOT be disabled) #define USE_SegmentByView -//#define FRAME_BASED_DT_CORR +// #define FRAME_BASED_DT_CORR // set elem_type to what you want to use for the sinogram elements // we need a signed type, as randoms can be subtracted. However, signed char could do. diff --git a/src/recon_buildblock/BinNormalisationFromPETSIRD.cxx b/src/recon_buildblock/BinNormalisationFromPETSIRD.cxx new file mode 100644 index 0000000000..e6799a6d54 --- /dev/null +++ b/src/recon_buildblock/BinNormalisationFromPETSIRD.cxx @@ -0,0 +1,74 @@ +// // +// // +// /* +// Copyright (C) 2025, UMCG +// This file is part of STIR. + +// SPDX-License-Identifier: Apache-2.0 + +// See STIR/LICENSE.txt for details +// */ +// /*! +// \file +// \ingroup normalisation + +// \brief Implementation for class stir::BinNormalisationFromPETSIRD + +// \author Nikos Efthimiou +// */ + +// #include "stir/recon_buildblock/BinNormalisationFromPETSIRD.h" + +// START_NAMESPACE_STIR + +// const char* const BinNormalisationFromPETSIRD::registered_name = "From PETSIRD"; + +// void +// BinNormalisationFromPETSIRD::set_defaults() +// { +// base_type::set_defaults(); +// normalisation_filename = ""; +// } + +// void +// BinNormalisationFromPETSIRD::initialise_keymap() +// { +// base_type::initialise_keymap(); +// parser.add_start_key("Bin Normalisation From PETSIRD"); +// parser.add_key("normalisation_filename", &normalisation_filename); +// parser.add_stop_key("End Bin Normalisation From PETSIRD"); +// } + +// bool +// BinNormalisationFromPETSIRD::post_processing() +// { +// if (base_type::post_processing()) +// return true; +// read_norm_data(normalisation_filename); +// } + +// BinNormalisationFromPETSIRD::BinNormalisationFromPETSIRD() +// { +// set_defaults(); +// } + +// BinNormalisationFromPETSIRD::BinNormalisationFromPETSIRD(const std::string& filename) +// { +// read_norm_data(filename); +// } + +// Succeeded +// BinNormalisationFromPETSIRD::set_up(const shared_ptr& exam_info_sptr, +// const shared_ptr& proj_data_info_ptr_v) +// { +// base_type::set_up(exam_info_sptr, proj_data_info_ptr_v); +// } + +// void +// BinNormalisationFromPETSIRD::read_norm_data(const string& filename) +// { +// normalisation_filename = filename; +// } + + +// END_NAMESPACE_STIR \ No newline at end of file diff --git a/src/recon_buildblock/CMakeLists.txt b/src/recon_buildblock/CMakeLists.txt index 77ba2d0432..3df0af5191 100644 --- a/src/recon_buildblock/CMakeLists.txt +++ b/src/recon_buildblock/CMakeLists.txt @@ -104,6 +104,12 @@ list(APPEND ${dir_LIB_SOURCES} BinNormalisationFromECAT8.cxx ) +# if (HAVE_PETSIRD) +# list(APPEND ${dir_LIB_SOURCES} +# BinNormalisationFromPETSIRD.cxx +# ) +# endif() + if (HAVE_HDF5) list(APPEND ${dir_LIB_SOURCES} BinNormalisationFromGEHDF5.cxx @@ -182,3 +188,15 @@ endif() if (STIR_WITH_CUDA) target_link_libraries(recon_buildblock PRIVATE CUDA::cudart) endif() + +# if (HAVE_PETSIRD) +# target_include_directories(recon_buildblock +# PUBLIC +# $ +# $ +# ) +# target_link_libraries(recon_buildblock +# PUBLIC +# petsird +# ) +# endif() \ No newline at end of file diff --git a/src/recon_buildblock/find_basic_vs_nums_in_subset.cxx b/src/recon_buildblock/find_basic_vs_nums_in_subset.cxx index 5364d4fc9b..240059b56f 100644 --- a/src/recon_buildblock/find_basic_vs_nums_in_subset.cxx +++ b/src/recon_buildblock/find_basic_vs_nums_in_subset.cxx @@ -39,12 +39,14 @@ find_basic_vs_nums_in_subset(const ProjDataInfo& proj_data_info, std::vector vs_nums_to_process; for (int segment_num = min_segment_num; segment_num <= max_segment_num; segment_num++) { - for (int timing_pos_num = -proj_data_info.get_min_tof_pos_num(); timing_pos_num <= proj_data_info.get_max_tof_pos_num(); + for (int timing_pos_num = proj_data_info.get_min_tof_pos_num(); timing_pos_num <= proj_data_info.get_max_tof_pos_num(); ++timing_pos_num) { for (int view = proj_data_info.get_min_view_num() + subset_num; view <= proj_data_info.get_max_view_num(); view += num_subsets) { + // std::cout << "view: " << view << std::endl; + // std::cout << "timing_pos_num: " << timing_pos_num << std::endl; const ViewSegmentNumbers view_segment_num(view, segment_num); if (!symmetries.is_basic(view_segment_num))