From c42611b0680c93ac3447882d4eb267117f06fb82 Mon Sep 17 00:00:00 2001 From: Parisa Khateri Date: Mon, 20 Aug 2018 15:07:06 +0200 Subject: [PATCH 01/81] modification for block geometry list of modified classes: - InterfileHeader - interfile.cxx - Scanner - VoxelsOnCartesianGrid - LORCoordinates - CListRecordSAFIR - DataSymmetriesForBins_PET_CartesianGrid - ProjMatrixByBinUsingRayTracing list of added classes: - GeometryBlocksOnCylindrical - ProjDataInfoBlocksOnCylindrical - ProjDataInfoBlocksOnCylindricalNoArcCorr --- src/IO/InterfileHeader.cxx | 116 +++- src/IO/interfile.cxx | 53 +- src/buildblock/CMakeLists.txt | 3 + .../GeometryBlocksOnCylindrical.cxx | 239 ++++++++ .../ProjDataInfoBlocksOnCylindrical.cxx | 520 +++++++++++++++++ ...ojDataInfoBlocksOnCylindricalNoArcCorr.cxx | 528 ++++++++++++++++++ src/buildblock/Scanner.cxx | 176 +++++- src/buildblock/VoxelsOnCartesianGrid.cxx | 25 + .../stir/GeometryBlocksOnCylindrical.h | 113 ++++ .../stir/GeometryBlocksOnCylindrical.inl | 97 ++++ src/include/stir/IO/InterfileHeader.h | 10 + src/include/stir/LORCoordinates.h | 6 + src/include/stir/LORCoordinates.inl | 36 +- .../stir/ProjDataInfoBlocksOnCylindrical.h | 313 +++++++++++ .../stir/ProjDataInfoBlocksOnCylindrical.inl | 291 ++++++++++ ...ProjDataInfoBlocksOnCylindricalNoArcCorr.h | 313 +++++++++++ ...ojDataInfoBlocksOnCylindricalNoArcCorr.inl | 191 +++++++ src/include/stir/Scanner.h | 76 ++- src/include/stir/Scanner.inl | 67 +++ src/include/stir/listmode/CListRecordSAFIR.h | 8 +- .../stir/listmode/CListRecordSAFIR.inl | 65 +++ ...ataSymmetriesForBins_PET_CartesianGrid.inl | 280 +++++++++- ...ataSymmetriesForBins_PET_CartesianGrid.cxx | 123 ++++ .../ProjMatrixByBinUsingRayTracing.cxx | 95 +++- 24 files changed, 3692 insertions(+), 52 deletions(-) create mode 100644 src/buildblock/GeometryBlocksOnCylindrical.cxx create mode 100644 src/buildblock/ProjDataInfoBlocksOnCylindrical.cxx create mode 100644 src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx create mode 100644 src/include/stir/GeometryBlocksOnCylindrical.h create mode 100644 src/include/stir/GeometryBlocksOnCylindrical.inl create mode 100644 src/include/stir/ProjDataInfoBlocksOnCylindrical.h create mode 100644 src/include/stir/ProjDataInfoBlocksOnCylindrical.inl create mode 100644 src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h create mode 100644 src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl diff --git a/src/IO/InterfileHeader.cxx b/src/IO/InterfileHeader.cxx index 11fd020d3..445351d2d 100644 --- a/src/IO/InterfileHeader.cxx +++ b/src/IO/InterfileHeader.cxx @@ -24,6 +24,7 @@ \author Kris Thielemans \author PARAPET project + \author Parisa Khateri */ #include "stir/IO/InterfileHeader.h" @@ -35,6 +36,7 @@ #include "stir/ProjDataInfoCylindricalNoArcCorr.h" #include #include +#include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" #ifndef STIR_NO_NAMESPACES using std::binary_function; @@ -564,6 +566,32 @@ InterfilePDFSHeader::InterfilePDFSHeader() add_key("Reference energy (in keV)", &reference_energy); + // new keys for block geometry + scanner_orientation = "X"; + add_key("Scanner orientation (X or Y)", + KeyArgument::ASCII, &scanner_orientation); + + scanner_geometry = "None"; + add_key("Scanner geometry (BlocksOnCylindrical/Cylindrical)", + KeyArgument::ASCII, &scanner_geometry); + + axial_distance_between_crystals_in_cm = -1; + add_key("distance between crystals in axial direction (cm)", + &axial_distance_between_crystals_in_cm); + + transaxial_distance_between_crystals_in_cm = -1; + add_key("distance between crystals in transaxial direction (cm)", + &transaxial_distance_between_crystals_in_cm); + + axial_distance_between_blocks_in_cm = -1; + add_key("distance between blocks in axial direction (cm)", + &axial_distance_between_blocks_in_cm); + + transaxial_distance_between_blocks_in_cm = -1; + add_key("distance between blocks in transaxial direction (cm)", + &transaxial_distance_between_blocks_in_cm); + // end of new keys for block geometry + add_key("end scanner parameters", KeyArgument::NONE, &KeyParser::do_nothing); @@ -1057,7 +1085,18 @@ bool InterfilePDFSHeader::post_processing() energy_resolution = guessed_scanner_ptr->get_energy_resolution(); if (reference_energy < 0) reference_energy = guessed_scanner_ptr->get_reference_energy(); - + + // new variables for block geometry + if (axial_distance_between_crystals_in_cm < 0) + axial_distance_between_crystals_in_cm = guessed_scanner_ptr->get_transaxial_crystal_spacing()/10; + if (transaxial_distance_between_crystals_in_cm < 0) + transaxial_distance_between_crystals_in_cm = guessed_scanner_ptr->get_transaxial_crystal_spacing()/10; + if (axial_distance_between_blocks_in_cm < 0) + axial_distance_between_blocks_in_cm = guessed_scanner_ptr->get_axial_block_spacing()/10; + if (transaxial_distance_between_blocks_in_cm < 0) + transaxial_distance_between_blocks_in_cm = guessed_scanner_ptr->get_transaxial_block_spacing()/10; + // end of new variables for block geometry + // consistency check with values of the guessed_scanner_ptr we guessed above if (num_rings != guessed_scanner_ptr->get_num_rings()) @@ -1193,6 +1232,37 @@ bool InterfilePDFSHeader::post_processing() // mismatch_between_header_and_guess = true; } } + + // new variables for block geometry + if (fabs(axial_distance_between_crystals_in_cm + -guessed_scanner_ptr->get_axial_crystal_spacing()/10) > .001) + { + warning("Interfile warning: 'distance between crystals in axial direction (cm)' (%f) is expected to be %f.\n", + axial_distance_between_crystals_in_cm, guessed_scanner_ptr->get_axial_crystal_spacing()/10); + mismatch_between_header_and_guess = true; + } + if (fabs(transaxial_distance_between_crystals_in_cm + -guessed_scanner_ptr->get_transaxial_crystal_spacing()/10) > .001) + { + warning("Interfile warning: 'distance between crystals in transaxial direction (cm)' (%f) is expected to be %f.\n", + transaxial_distance_between_crystals_in_cm, guessed_scanner_ptr->get_transaxial_crystal_spacing()/10); + mismatch_between_header_and_guess = true; + } + if (fabs(axial_distance_between_blocks_in_cm + -guessed_scanner_ptr->get_axial_block_spacing()/10) > .001) + { + warning("Interfile warning: 'distance between crystals in axial direction (cm)' (%f) is expected to be %f.\n", + axial_distance_between_blocks_in_cm, guessed_scanner_ptr->get_axial_block_spacing()/10); + mismatch_between_header_and_guess = true; + } + if (fabs(transaxial_distance_between_blocks_in_cm + -guessed_scanner_ptr->get_transaxial_block_spacing()/10) > .001) + { + warning("Interfile warning: 'distance between crystals in axial direction (cm)' (%f) is expected to be %f.\n", + transaxial_distance_between_blocks_in_cm, guessed_scanner_ptr->get_transaxial_block_spacing()/10); + mismatch_between_header_and_guess = true; + } + // end of new variables for block geometry // end of checks. If they failed, we ignore the guess if (mismatch_between_header_and_guess) @@ -1231,7 +1301,16 @@ bool InterfilePDFSHeader::post_processing() warning("Interfile warning: 'axial crystals per singles unit' invalid.\n"); if (num_transaxial_crystals_per_singles_unit <= 0) warning("Interfile warning: 'transaxial crystals per singles unit' invalid.\n"); - + // new variables for block geometry + if (axial_distance_between_crystals_in_cm <= 0) + warning("Interfile warning: 'distance between crystals in axial direction (cm)' invalid.\n"); + if (transaxial_distance_between_crystals_in_cm <= 0) + warning("Interfile warning: 'distance between crystals in transaxial direction (cm)' invalid.\n"); + if (axial_distance_between_blocks_in_cm <= 0) + warning("Interfile warning: 'distance between blocks in axial direction (cm)' invalid.\n"); + if (transaxial_distance_between_blocks_in_cm <= 0) + warning("Interfile warning: 'distance between blocks in transaxial direction (cm)' invalid.\n"); + // end of new variables for block geometry } // finally, we construct a new scanner object with @@ -1256,7 +1335,14 @@ bool InterfilePDFSHeader::post_processing() num_transaxial_crystals_per_singles_unit, num_detector_layers, energy_resolution, - reference_energy)); + reference_energy, + scanner_orientation, + scanner_geometry, + static_cast(axial_distance_between_crystals_in_cm*10.), + static_cast(transaxial_distance_between_crystals_in_cm*10.), + static_cast(axial_distance_between_blocks_in_cm*10.), + static_cast(transaxial_distance_between_blocks_in_cm*10.) + )); bool is_consistent = scanner_ptr_from_file->check_consistency() == Succeeded::yes; @@ -1274,7 +1360,8 @@ bool InterfilePDFSHeader::post_processing() - + if (scanner_geometry == "Cylindrical") + { if (is_arccorrected) { if (effective_central_bin_size_in_cm <= 0) @@ -1317,6 +1404,27 @@ bool InterfilePDFSHeader::post_processing() data_info_ptr->get_sampling_in_s(Bin(0,0,0,0))/10.); } } + } + else // if block geometry + { + data_info_ptr = + new ProjDataInfoBlocksOnCylindricalNoArcCorr ( + scanner_ptr_from_file, + sorted_num_rings_per_segment, + sorted_min_ring_diff, + sorted_max_ring_diff, + num_views,num_bins); + if (effective_central_bin_size_in_cm>0 && + fabs(effective_central_bin_size_in_cm - + data_info_ptr->get_sampling_in_s(Bin(0,0,0,0))/10.)>.01) + { + warning("Interfile warning: inconsistent effective_central_bin_size_in_cm\n" + "Value in header is %g while I expect %g from the inner ring radius etc\n" + "Ignoring value in header", + effective_central_bin_size_in_cm, + data_info_ptr->get_sampling_in_s(Bin(0,0,0,0))/10.); + } + } //cerr << data_info_ptr->parameter_info() << endl; return false; diff --git a/src/IO/interfile.cxx b/src/IO/interfile.cxx index 08501d1d0..7fa231268 100644 --- a/src/IO/interfile.cxx +++ b/src/IO/interfile.cxx @@ -25,6 +25,7 @@ \author Kris Thielemans \author Sanida Mustafovic \author PARAPET project + \author Parisa Khateri */ // Pretty horrible implementations at the moment... @@ -55,6 +56,8 @@ #include #include #include +#include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" + #ifndef STIR_NO_NAMESPACES using std::cerr; @@ -1012,7 +1015,55 @@ write_basic_interfile_PDFS_header(const string& header_file_name, } // end of cylindrical scanner else { - // TODO something here + // !author Parisa Khateri + const ProjDataInfoBlocksOnCylindrical* proj_data_info_ptr = + dynamic_cast< const ProjDataInfoBlocksOnCylindrical*> (pdfs.get_proj_data_info_ptr()); + + if (proj_data_info_ptr!=NULL) + { + // BlocksOncylindrical scanners + output_header << "minimum ring difference per segment := "; + { + std::vector::const_iterator seg = segment_sequence.begin(); + output_header << "{ " << proj_data_info_ptr->get_min_ring_difference(*seg); + for (seg++; seg != segment_sequence.end(); seg++) + output_header << "," <get_min_ring_difference(*seg); + output_header << "}\n"; + } + + output_header << "maximum ring difference per segment := "; + { + std::vector::const_iterator seg = segment_sequence.begin(); + output_header << "{ " <get_max_ring_difference(*seg); + for (seg++; seg != segment_sequence.end(); seg++) + output_header << "," <get_max_ring_difference(*seg); + output_header << "}\n"; + } + + const Scanner& scanner = *proj_data_info_ptr->get_scanner_ptr(); + if (fabs(proj_data_info_ptr->get_ring_radius()- + scanner.get_effective_ring_radius()) > .1) + warning("write_basic_interfile_PDFS_header: inconsistent effective ring radius:\n" + "\tproj_data_info has %g, scanner has %g.\n" + "\tThis really should not happen and signifies a bug.\n" + "\tYou will have a problem reading this data back in.", + proj_data_info_ptr->get_ring_radius(), + scanner.get_effective_ring_radius()); + if (fabs(proj_data_info_ptr->get_ring_spacing()- + scanner.get_ring_spacing()) > .1) + warning("write_basic_interfile_PDFS_header: inconsistent ring spacing:\n" + "\tproj_data_info has %g, scanner has %g.\n" + "\tThis really should not happen and signifies a bug.\n" + "\tYou will have a problem reading this data back in.", + proj_data_info_ptr->get_ring_spacing(), + scanner.get_ring_spacing()); + + output_header << scanner.parameter_info(); + + output_header << "effective central bin size (cm) := " + << proj_data_info_ptr->get_sampling_in_s(Bin(0,0,0,0))/10. << endl; + + }// end of BlocksOnCylindrical scanner } diff --git a/src/buildblock/CMakeLists.txt b/src/buildblock/CMakeLists.txt index b5761a341..63243571b 100644 --- a/src/buildblock/CMakeLists.txt +++ b/src/buildblock/CMakeLists.txt @@ -74,6 +74,9 @@ set(${dir_LIB_SOURCES} num_threads GeneralisedPoissonNoiseGenerator FilePath + GeometryBlocksOnCylindrical + ProjDataInfoBlocksOnCylindrical + ProjDataInfoBlocksOnCylindricalNoArcCorr ) if (NOT HAVE_SYSTEM_GETOPT) diff --git a/src/buildblock/GeometryBlocksOnCylindrical.cxx b/src/buildblock/GeometryBlocksOnCylindrical.cxx new file mode 100644 index 000000000..4d17ab84e --- /dev/null +++ b/src/buildblock/GeometryBlocksOnCylindrical.cxx @@ -0,0 +1,239 @@ + +/* + +TODO copyright and License + +*/ + +/*! + \file + \ingroup projdata + + \brief Non-inline implementations of stir::GeometryBlocksOnCylindrical + + \author Parisa Khateri + +*/ + +#include "stir/DetectionPosition.h" +#include "stir/CartesianCoordinate3D.h" +#include "stir/Scanner.h" +#include "stir/shared_ptr.h" +#include "stir/GeometryBlocksOnCylindrical.h" +#include +#include +#include "stir/Array.h" +#include "stir/make_array.h" +#include "stir/numerics/MatrixFunction.h" +#include +#include +#include +#include + +START_NAMESPACE_STIR + + +GeometryBlocksOnCylindrical:: +GeometryBlocksOnCylindrical() +{} + +GeometryBlocksOnCylindrical:: +GeometryBlocksOnCylindrical(const shared_ptr &scanner_ptr_v): + scanner_ptr(scanner_ptr_v) +{ + build_crystal_maps(); +} + +bool +GeometryBlocksOnCylindrical:: +compare_det_pos:: +operator() (const stir::DetectionPosition<>& det_pos1, + const stir::DetectionPosition<>& det_pos2) const +{ + if ( det_pos1.tangential_coord()& cart_coord1, + const stir::CartesianCoordinate3D& cart_coord2) const +{ + if (cart_coord1.z()get_num_axial_crystals_per_block(); + int num_transaxial_crystals_per_block = get_scanner_ptr()->get_num_transaxial_crystals_per_block(); + int num_axial_blocks = get_scanner_ptr()->get_num_axial_blocks(); + int num_transaxial_blocks_per_bucket = get_scanner_ptr()->get_num_transaxial_blocks_per_bucket(); + int num_transaxial_buckets = get_scanner_ptr()->get_num_transaxial_blocks()/num_transaxial_blocks_per_bucket; + int num_detectors_per_ring = get_scanner_ptr()->get_num_detectors_per_ring(); + float axial_block_spacing = get_scanner_ptr()->get_axial_block_spacing(); + float transaxial_block_spacing = get_scanner_ptr()->get_transaxial_block_spacing(); + float axial_crystal_spacing = get_scanner_ptr()->get_axial_crystal_spacing(); + float transaxial_crystal_spacing = get_scanner_ptr()->get_transaxial_crystal_spacing(); + std::string scanner_orientation = get_scanner_ptr()->get_scanner_orientation(); + + // check for the scanner orientation + if (scanner_orientation=="Y" || num_transaxial_buckets%4==0 ) + {/*Building starts from a bucket perpendicular to y axis, from its first crystal. + see start_x*/ + + //calculate start_point to build the map. + float start_z = -1*( + ((num_axial_blocks-1)/2.)*axial_block_spacing + + ((num_axial_crystals_per_block-1)/2.)*axial_crystal_spacing + ); + float start_y = -1*get_scanner_ptr()->get_effective_ring_radius(); + float start_x = -1*( + ((num_transaxial_blocks_per_bucket-1)/2.)*transaxial_block_spacing + + ((num_transaxial_crystals_per_block-1)/2.)*transaxial_crystal_spacing + ); //the first crystal in the bucket + stir::CartesianCoordinate3D start_point(start_z, start_y, start_x); + + for (int ax_block_num=0; ax_block_num det_pos(tangential_coord, axial_coord, radial_coord); + + //calculate cartesion coordinate for a given detector + stir::CartesianCoordinate3D transformation_matrix( + ax_block_num*axial_block_spacing + ax_crys_num*axial_crystal_spacing, + 0., + trans_block_num*transaxial_block_spacing + trans_crys_num*transaxial_crystal_spacing); + float alpha = trans_bucket_num*(2*_PI)/num_transaxial_buckets; + + stir::Array<2, float> rotation_matrix = get_rotation_matrix(alpha); + // to match index range of CartesianCoordinate3D, which is 1 to 3 + rotation_matrix.set_min_index(1); + rotation_matrix[1].set_min_index(1); + rotation_matrix[2].set_min_index(1); + rotation_matrix[3].set_min_index(1); + + stir::CartesianCoordinate3D transformed_coord = + start_point + transformation_matrix; + stir::CartesianCoordinate3D cart_coord = + stir::matrix_multiply(rotation_matrix, transformed_coord); + + // rounding cart_coord to 3 and 2 decimal points then filling maps + cart_coord.z() = (round(cart_coord.z()*1000.0))/1000.0; + cart_coord.y() = (round(cart_coord.y()*1000.0))/1000.0; + cart_coord.x() = (round(cart_coord.x()*1000.0))/1000.0; + cartesian_coord_map_given_detection_position_keys[det_pos] = cart_coord; //used to find s, m, phi, theta + detection_position_map_given_cartesian_coord_keys_3_decimal[cart_coord] = det_pos; //used to find bin from listmode data + cart_coord.z() = (round(cart_coord.z()*100.0))/100.0; + cart_coord.y() = (round(cart_coord.y()*100.0))/100.0; + cart_coord.x() = (round(cart_coord.x()*100.0))/100.0; + detection_position_map_given_cartesian_coord_keys_2_decimal[cart_coord] = det_pos; + } + } + + else if (scanner_orientation=="X" ) + {/*Building starts from a bucket perpendicular to x axis, from its first crystal. + see start_y*/ + + //calculate start_point to build the map. + float start_z = -1*( + ((num_axial_blocks-1)/2.)*axial_block_spacing + + ((num_axial_crystals_per_block-1)/2.)*axial_crystal_spacing + ); + float start_x = get_scanner_ptr()->get_effective_ring_radius(); + float start_y = -1*( + ((num_transaxial_blocks_per_bucket-1)/2.)*transaxial_block_spacing + + ((num_transaxial_crystals_per_block-1)/2.)*transaxial_crystal_spacing + ); //the first crystal in the bucket + stir::CartesianCoordinate3D start_point(start_z, start_y, start_x); + + for (int ax_block_num=0; ax_block_num det_pos(tangential_coord, axial_coord, radial_coord); + //calculate cartesion coordinate for a given detector + stir::CartesianCoordinate3D transformation_matrix( + ax_block_num*axial_block_spacing + ax_crys_num*axial_crystal_spacing, + 0., + trans_block_num*transaxial_block_spacing + trans_crys_num*transaxial_crystal_spacing); + + float alpha = (trans_bucket_num - num_transaxial_buckets/4) + *(2*_PI)/num_transaxial_buckets; + + stir::Array<2, float> rotation_matrix = get_rotation_matrix(alpha); + // to match index range of CartesianCoordinate3D, which is 1 to 3 + rotation_matrix.set_min_index(1); + rotation_matrix[1].set_min_index(1); + rotation_matrix[2].set_min_index(1); + rotation_matrix[3].set_min_index(1); + + stir::CartesianCoordinate3D transformed_coord = start_point+transformation_matrix; + stir::CartesianCoordinate3D cart_coord = + stir::matrix_multiply(rotation_matrix, transformed_coord); + + // rounding cart_coord to 3 and 2 decimal points then filling maps + cart_coord.z() = (round(cart_coord.z()*1000.0))/1000.0; + cart_coord.y() = (round(cart_coord.y()*1000.0))/1000.0; + cart_coord.x() = (round(cart_coord.x()*1000.0))/1000.0; + cartesian_coord_map_given_detection_position_keys[det_pos] = cart_coord; //used to find s, m, phi, theta + detection_position_map_given_cartesian_coord_keys_3_decimal[cart_coord] = det_pos; //used to find bin from listmode data + cart_coord.z() = (round(cart_coord.z()*100.0))/100.0; + cart_coord.y() = (round(cart_coord.y()*100.0))/100.0; + cart_coord.x() = (round(cart_coord.x()*100.0))/100.0; + detection_position_map_given_cartesian_coord_keys_2_decimal[cart_coord] = det_pos; + } + } +} + + +END_NAMESPACE_STIR diff --git a/src/buildblock/ProjDataInfoBlocksOnCylindrical.cxx b/src/buildblock/ProjDataInfoBlocksOnCylindrical.cxx new file mode 100644 index 000000000..856a6361d --- /dev/null +++ b/src/buildblock/ProjDataInfoBlocksOnCylindrical.cxx @@ -0,0 +1,520 @@ + +/* + +TODO copyright and License + +*/ + +/*! + \file + \ingroup projdata + + \brief Non-inline implementations of stir::ProjDataInfoBlocksOnCylindrical + + \author Parisa Khateri + +*/ +#include "stir/ProjDataInfoBlocksOnCylindrical.h" +#include "stir/LORCoordinates.h" +#include +#ifdef BOOST_NO_STRINGSTREAM +#include +#else +#include +#endif + +#include "stir/round.h" +#include + +#ifndef STIR_NO_NAMESPACES +using std::min_element; +using std::max_element; +using std::min; +using std::max; +using std::swap; +using std::endl; +using std::string; +using std::pair; +using std::vector; +#endif + +START_NAMESPACE_STIR + +ProjDataInfoBlocksOnCylindrical:: +ProjDataInfoBlocksOnCylindrical() +{} + + +ProjDataInfoBlocksOnCylindrical:: +ProjDataInfoBlocksOnCylindrical(const shared_ptr& scanner_ptr, + const VectorWithOffset& num_axial_pos_per_segment, + const VectorWithOffset& min_ring_diff_v, + const VectorWithOffset& max_ring_diff_v, + const int num_views,const int num_tangential_poss) + :ProjDataInfo(scanner_ptr,num_axial_pos_per_segment, + num_views,num_tangential_poss), + min_ring_diff(min_ring_diff_v), + max_ring_diff(max_ring_diff_v) +{ + + azimuthal_angle_sampling = static_cast(_PI/num_views); + ring_radius.resize(0,0); + ring_radius[0] = get_scanner_ptr()->get_effective_ring_radius(); + ring_spacing= get_scanner_ptr()->get_ring_spacing() ; + + // TODO this info should probably be provided via the constructor, or at + // least by Scanner. + sampling_corresponds_to_physical_rings = + scanner_ptr->get_type() != Scanner::HiDAC; + + + assert(min_ring_diff.get_length() == max_ring_diff.get_length()); + assert(min_ring_diff.get_length() == num_axial_pos_per_segment.get_length()); + + // check min,max ring diff + { + for (int segment_num=get_min_segment_num(); segment_num<=get_max_segment_num(); ++segment_num) + if (min_ring_diff[segment_num]> max_ring_diff[segment_num]) + { + warning("ProjDataInfoBlocksOnCylindrical: min_ring_difference %d is larger than max_ring_difference %d for segment %d. " + "Swapping them around", + min_ring_diff[segment_num], max_ring_diff[segment_num], segment_num); + swap(min_ring_diff[segment_num], max_ring_diff[segment_num]); + } + } + + initialise_ring_diff_arrays(); +} + +/* warning In cylindrical geometry m_offset is calculated based on axial_spacing, + then it is used to caculate ax_pos_num_offset and segment_axial_pos_to_ring1_plus_ring2. + For block geometry, m_offset has been removed and the above mentioned variables are + calculated independant of m_offset.*/ +void +ProjDataInfoBlocksOnCylindrical:: +initialise_ring_diff_arrays() const +{ + + // check min,max ring diff + { + // check is necessary here again because of set_min_ring_difference() + // we do not swap here because that would require the min/max_ring_diff arrays to be mutable as well + for (int segment_num=get_min_segment_num(); segment_num<=get_max_segment_num(); ++segment_num) + if (min_ring_diff[segment_num]> max_ring_diff[segment_num]) + { + error("ProjDataInfoBlocksOnCylindrical: min_ring_difference %d is larger than " + "max_ring_difference %d for segment %d.", + min_ring_diff[segment_num], max_ring_diff[segment_num], segment_num); + } + } + + // initialise ax_pos_num_offset + if (sampling_corresponds_to_physical_rings) + { + const int num_rings = get_scanner_ptr()->get_num_rings(); + ax_pos_num_offset = + VectorWithOffset(get_min_segment_num(),get_max_segment_num()); + + /* ax_pos_num will be determined by looking at ring1+ring2. + This also works for axially compressed data (i.e. span) as + ring1+ring2 is constant for all ring-pairs combined into 1 + segment,ax_pos. + + Ignoring the difficulties of axial compression for a second, it is clear that + for a given bin, there will be 2 rings as follows: + ring2 = get_m(bin)/ring_spacing + ring_diff/2 + (num_rings-1)/2 + ring1 = get_m(bin)/ring_spacing - ring_diff/2 + (num_rings-1)/2 + This follows from the fact that get_m() returns the z position + in millimeter of the middle of the LOR w.r.t. the middle of the scanner. + The (num_rings-1)/2 shifts the origin such that the first ring has + ring_num==0. + + From the above, it follows that + ring1+ring2=2*get_m(bin)/ring_spacing + (num_rings-1) + Finally, we use the formula for get_m to obtain + ring1+ring2=2*ax_pos_num/get_num_axial_poss_per_ring_inc(segment_num) + -2*m_offset[segment_num]/ring_spacing + (num_rings-1) + Solving this for ax_pos_num: + ax_pos_num = (ring1+ring2-(num_rings-1) + + 2*m_offset[segment_num]/ring_spacing + ) * get_num_axial_poss_per_ring_inc(segment_num)/2 + + We could plug m_offset in to obtain + ax_pos_num = (ring1+ring2-(num_rings-1) + ) * get_num_axial_poss_per_ring_inc(segment_num)/2. + + + (get_max_axial_pos_num(segment_num) + + get_min_axial_pos_num(segment_num) )/2. + this formula is easy to understand, but we don't use it as + at some point somebody might change m_offset + and forget to change this code... + (also, the form above would need float division and then rounding) + */ + for (int segment_num=get_min_segment_num(); segment_num<=get_max_segment_num(); ++segment_num) + { + const float ax_pos_num_offset_float = (num_rings-1) - + (get_max_axial_pos_num(segment_num) + + get_min_axial_pos_num(segment_num)) + /get_num_axial_poss_per_ring_inc(segment_num); + ax_pos_num_offset[segment_num] = round(ax_pos_num_offset_float); + // check that it was integer + if (fabs(ax_pos_num_offset[segment_num] - ax_pos_num_offset_float) > 1E-4) + { + error("ProjDataInfoBlocksOnCylindrical: in segment %d, the axial positions\n" + "do not correspond to the usual locations between physical rings.\n" + "This is suspicious and can make things go wrong in STIR, so I abort.\n" + "Check the number of axial positions in this segment.", + segment_num); + } + + if (get_num_axial_poss_per_ring_inc(segment_num)==1) + { + // check that we'll get an integer ax_pos_num, i.e. + // (ring1+ring2 - ax_pos_num_offset) has to be even, for any + // ring1,ring2 in the segment, i.e ring1-ring2 = ring_diff, so + // ring1+ring2 = 2*ring2 + ring_diff + assert(get_min_ring_difference(segment_num) == + get_max_ring_difference(segment_num)); + if ((get_max_ring_difference(segment_num) - + ax_pos_num_offset[segment_num]) % 2 != 0) + warning("ProjDataInfoBlocksOnCylindrical: the number of axial positions in " + "segment %d is such that current conventions will place " + "the LORs shifted with respect to the physical rings.", + segment_num); + } + } + } + // initialise ring_diff_to_segment_num + if (sampling_corresponds_to_physical_rings) + { + const int min_ring_difference = + *min_element(min_ring_diff.begin(), min_ring_diff.end()); + const int max_ring_difference = + *max_element(max_ring_diff.begin(), max_ring_diff.end()); + + // set ring_diff_to_segment_num to appropriate size + // in principle, the max ring difference would be scanner.num_rings-1, but + // in case someone is up to strange things, we take the max of this value + // with the max_ring_difference as given in the file + ring_diff_to_segment_num = + VectorWithOffset(min(min_ring_difference, -(get_scanner_ptr()->get_num_rings()-1)), + max(max_ring_difference, get_scanner_ptr()->get_num_rings()-1)); + // first set all to impossible value + // warning: get_segment_num_for_ring_difference relies on the fact that this value + // is larger than get_max_segment_num() + ring_diff_to_segment_num.fill(get_max_segment_num()+1); + + for(int ring_diff=min_ring_difference; ring_diff <= max_ring_difference; ++ring_diff) + { + int segment_num; + for (segment_num=get_min_segment_num(); segment_num<=get_max_segment_num(); ++segment_num) + { + if (ring_diff >= min_ring_diff[segment_num] && + ring_diff <= max_ring_diff[segment_num]) + { +#if 0 + std::cerr << "ring diff " << ring_diff << " stored in s:" << segment_num << std::endl; +#endif + ring_diff_to_segment_num[ring_diff] = segment_num; + break; + } + } + if (segment_num>get_max_segment_num()) + { + warning("ProjDataInfoBlocksOnCylindrical: ring difference %d does not belong to a segment", + ring_diff); + } + } + } + // initialise segment_axial_pos_to_ring1_plus_ring2 + if (sampling_corresponds_to_physical_rings) + { + segment_axial_pos_to_ring1_plus_ring2 = + VectorWithOffset >(get_min_segment_num(), get_max_segment_num()); + for (int s_num=get_min_segment_num(); s_num<=get_max_segment_num(); ++s_num) + { + const int min_ax_pos_num = get_min_axial_pos_num(s_num); + const int max_ax_pos_num = get_max_axial_pos_num(s_num); + segment_axial_pos_to_ring1_plus_ring2[s_num].grow(min_ax_pos_num, max_ax_pos_num); + for (int ax_pos_num=min_ax_pos_num; ax_pos_num<=max_ax_pos_num; ++ax_pos_num) + { + // see documentation above for formulas + const float ring1_plus_ring2_float = + (2*ax_pos_num - + get_max_axial_pos_num(s_num) + get_min_axial_pos_num(s_num)) + /get_num_axial_poss_per_ring_inc(s_num) + + get_scanner_ptr()->get_num_rings()-1; + + const int ring1_plus_ring2 = + round(ring1_plus_ring2_float); + // check that it was integer + assert(fabs(ring1_plus_ring2 - ring1_plus_ring2_float) < 1E-4) ; + segment_axial_pos_to_ring1_plus_ring2[s_num][ax_pos_num] = ring1_plus_ring2; + } + } + } + + if (sampling_corresponds_to_physical_rings) + allocate_segment_axial_pos_to_ring_pair(); + + ring_diff_arrays_computed = true; +} + +/*! Default implementation checks common variables. Needs to be overloaded. + */ +bool +ProjDataInfoBlocksOnCylindrical:: +blindly_equals(const root_type * const that) const +{ + if (!base_type::blindly_equals(that)) + return false; + + const self_type& proj_data_info = static_cast(*that); + return + this->azimuthal_angle_sampling == proj_data_info.azimuthal_angle_sampling && + this->ring_radius == proj_data_info.ring_radius && + this->sampling_corresponds_to_physical_rings == proj_data_info.sampling_corresponds_to_physical_rings && + this->ring_spacing == proj_data_info.ring_spacing && + this->min_ring_diff == proj_data_info.min_ring_diff && + this->max_ring_diff == proj_data_info.max_ring_diff; +} + +void +ProjDataInfoBlocksOnCylindrical:: +get_ring_pair_for_segment_axial_pos_num(int& ring1, + int& ring2, + const int segment_num, + const int axial_pos_num) const +{ + if (!sampling_corresponds_to_physical_rings) + error("ProjDataInfoBlocksOnCylindrical::get_ring_pair_for_segment_axial_pos_num does not work for this type of sampled data"); + // can do only span=1 at the moment + if (get_min_ring_difference(segment_num) != get_max_ring_difference(segment_num)) + error("ProjDataInfoBlocksOnCylindrical::get_ring_pair_for_segment_axial_pos_num does not work for data with axial compression"); + + if (!ring_diff_arrays_computed) + initialise_ring_diff_arrays(); + + const int ring_diff = get_max_ring_difference(segment_num); + const int ring1_plus_ring2= segment_axial_pos_to_ring1_plus_ring2[segment_num][axial_pos_num]; + + // KT 01/08/2002 swapped rings + ring1 = (ring1_plus_ring2 - ring_diff)/2; + ring2 = (ring1_plus_ring2 + ring_diff)/2; + assert((ring1_plus_ring2 + ring_diff)%2 == 0); + assert((ring1_plus_ring2 - ring_diff)%2 == 0); +} + + +void +ProjDataInfoBlocksOnCylindrical:: +set_azimuthal_angle_sampling(const float angle_v) +{ + azimuthal_angle_sampling = angle_v; +} + +//void +//ProjDataInfoBlocksOnCylindrical:: +//set_axial_sampling(const float samp_v, int segment_num) +//{axial_sampling = samp_v;} + + +void +ProjDataInfoBlocksOnCylindrical:: +set_num_views(const int new_num_views) +{ + const float old_azimuthal_angle_range = + this->get_azimuthal_angle_sampling() * this->get_num_views(); + base_type::set_num_views(new_num_views); + this->azimuthal_angle_sampling = old_azimuthal_angle_range/this->get_num_views(); +} + +void +ProjDataInfoBlocksOnCylindrical:: +set_min_ring_difference( int min_ring_diff_v, int segment_num) +{ + ring_diff_arrays_computed = false; + min_ring_diff[segment_num] = min_ring_diff_v; +} + +void +ProjDataInfoBlocksOnCylindrical:: +set_max_ring_difference( int max_ring_diff_v, int segment_num) +{ + ring_diff_arrays_computed = false; + max_ring_diff[segment_num] = max_ring_diff_v; +} + +void +ProjDataInfoBlocksOnCylindrical:: +set_ring_spacing(float ring_spacing_v) +{ + ring_diff_arrays_computed = false; + ring_spacing = ring_spacing_v; +} + +void +ProjDataInfoBlocksOnCylindrical:: +allocate_segment_axial_pos_to_ring_pair() const +{ + segment_axial_pos_to_ring_pair = + VectorWithOffset > > + (get_min_segment_num(), get_max_segment_num()); + + for (int segment_num = get_min_segment_num(); + segment_num <= get_max_segment_num(); + ++segment_num) + { + segment_axial_pos_to_ring_pair[segment_num].grow(get_min_axial_pos_num(segment_num), + get_max_axial_pos_num(segment_num)); + } +} + +void +ProjDataInfoBlocksOnCylindrical:: +compute_segment_axial_pos_to_ring_pair(const int segment_num, const int axial_pos_num) const +{ + shared_ptr new_el(new RingNumPairs); + segment_axial_pos_to_ring_pair[segment_num][axial_pos_num] = new_el; + + RingNumPairs& table = + *segment_axial_pos_to_ring_pair[segment_num][axial_pos_num]; + table.reserve(get_max_ring_difference(segment_num) - + get_min_ring_difference(segment_num) + 1); + + /* We compute the lookup-table in a fancy way. + We could just as well have a simple loop over all ring pairs and check + if it belongs to this segment/axial_pos. + The current way is a lot faster though. + */ + const int min_ring_diff = get_min_ring_difference(segment_num); + const int max_ring_diff = get_max_ring_difference(segment_num); + const int num_rings = get_scanner_ptr()->get_num_rings(); + + /* ring1_plus_ring2 is the same for any ring pair that contributes to + this particular segment_num, axial_pos_num. + */ + const int ring1_plus_ring2= + segment_axial_pos_to_ring1_plus_ring2[segment_num][axial_pos_num]; + + /* + The ring_difference increments with 2 as the other ring differences do + not give a ring pair with this axial_position. This is because + ring1_plus_ring2%2 == ring_diff%2 + (which easily follows by plugging in ring1+ring2 and ring1-ring2). + The starting ring_diff is determined such that the above condition + is satisfied. You can check it by noting that the + start_ring_diff%2 + == (min_ring_diff + (min_ring_diff+ring1_plus_ring2)%2)%2 + == (2*min_ring_diff+ring1_plus_ring2)%2 + == ring1_plus_ring2%2 + */ + for(int ring_diff = min_ring_diff + (min_ring_diff+ring1_plus_ring2)%2; + ring_diff <= max_ring_diff; + ring_diff+=2 ) + { + const int ring1 = (ring1_plus_ring2 - ring_diff)/2; + const int ring2 = (ring1_plus_ring2 + ring_diff)/2; + if (ring1<0 || ring2 < 0 || ring1>=num_rings || ring2 >= num_rings) + continue; + assert((ring1_plus_ring2 + ring_diff)%2 == 0); + assert((ring1_plus_ring2 - ring_diff)%2 == 0); + table.push_back(pair(ring1, ring2)); + #ifndef NDEBUG + int check_segment_num = 0, check_axial_pos_num = 0; + assert(get_segment_axial_pos_num_for_ring_pair(check_segment_num, + check_axial_pos_num, + ring1, + ring2) == + Succeeded::yes); + assert(check_segment_num == segment_num); + assert(check_axial_pos_num == axial_pos_num); + #endif + } +} + +void +ProjDataInfoBlocksOnCylindrical:: +set_num_axial_poss_per_segment(const VectorWithOffset& num_axial_poss_per_segment) +{ + ProjDataInfo::set_num_axial_poss_per_segment(num_axial_poss_per_segment); + ring_diff_arrays_computed = false; +} + +void +ProjDataInfoBlocksOnCylindrical:: +set_min_axial_pos_num(const int min_ax_pos_num, const int segment_num) +{ + ProjDataInfo::set_min_axial_pos_num(min_ax_pos_num, segment_num); + ring_diff_arrays_computed = false; +} + +void ProjDataInfoBlocksOnCylindrical:: +set_max_axial_pos_num(const int max_ax_pos_num, const int segment_num) +{ + ProjDataInfo::set_max_axial_pos_num(max_ax_pos_num, segment_num); + ring_diff_arrays_computed = false; +} + +void +ProjDataInfoBlocksOnCylindrical:: +reduce_segment_range(const int min_segment_num, const int max_segment_num) +{ + ProjDataInfo::reduce_segment_range(min_segment_num, max_segment_num); + // reduce ring_diff arrays to new valid size + VectorWithOffset new_min_ring_diff(min_segment_num, max_segment_num); + VectorWithOffset new_max_ring_diff(min_segment_num, max_segment_num); + + for (int segment_num = min_segment_num; segment_num<= max_segment_num; ++segment_num) + { + new_min_ring_diff[segment_num] = this->min_ring_diff[segment_num]; + new_max_ring_diff[segment_num] = this->max_ring_diff[segment_num]; + } + + this->min_ring_diff = new_min_ring_diff; + this->max_ring_diff = new_max_ring_diff; + + // make sure other arrays will be updated if/when necessary + this->ring_diff_arrays_computed = false; +} + +//! warning Find lor from cartesian coordinates of detector pair +void +ProjDataInfoBlocksOnCylindrical:: +get_LOR(LORInAxialAndNoArcCorrSinogramCoordinates& lor, + const Bin& bin) const +{ + CartesianCoordinate3D _p1; + CartesianCoordinate3D _p2; + find_cartesian_coordinates_of_detection(_p1, _p2, bin); + + LORAs2Points lor_as_2_points(_p1, _p2); + const double R = get_ring_radius(); + lor_as_2_points.change_representation_for_block(lor, R); +} + +string +ProjDataInfoBlocksOnCylindrical::parameter_info() const +{ + +#ifdef BOOST_NO_STRINGSTREAM + // dangerous for out-of-range, but 'old-style' ostrstream seems to need this + char str[30000]; + ostrstream s(str, 30000); +#else + std::ostringstream s; +#endif + s << ProjDataInfo::parameter_info(); + s << "Azimuthal angle increment (deg): " << get_azimuthal_angle_sampling()*180/_PI << '\n'; + s << "Azimuthal angle extent (deg): " << fabs(get_azimuthal_angle_sampling())*get_num_views()*180/_PI << '\n'; + + s << "ring differences per segment: \n"; + for (int segment_num=get_min_segment_num(); segment_num<=get_max_segment_num(); ++segment_num) + { + s << '(' << min_ring_diff[segment_num] << ',' << max_ring_diff[segment_num] <<')'; + } + s << std::endl; + return s.str(); +} + +END_NAMESPACE_STIR diff --git a/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx b/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx new file mode 100644 index 000000000..f6d45a2f8 --- /dev/null +++ b/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx @@ -0,0 +1,528 @@ + +/* + +TODO copyright and License + +*/ + +/*! + \file + \ingroup projdata + + \brief Non-inline implementations of + stir::ProjDataInfoBlocksOnCylindricalNoArcCorr + + \author Parisa Khateri + +*/ + +#include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" +#include "stir/Bin.h" +#include "stir/LORCoordinates.h" +#include "stir/round.h" +#include "stir/DetectionPosition.h" +#include +#include + +#ifdef BOOST_NO_STRINGSTREAM +#include +#else +#include +#endif + +#ifndef STIR_NO_NAMESPACES +using std::endl; +using std::ends; +using std::string; +using std::pair; +using std::vector; +#endif + +START_NAMESPACE_STIR +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +ProjDataInfoBlocksOnCylindricalNoArcCorr() +{} + +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +ProjDataInfoBlocksOnCylindricalNoArcCorr(const shared_ptr scanner_ptr, + const float ring_radius_v, const float angular_increment_v, + const VectorWithOffset& num_axial_pos_per_segment, + const VectorWithOffset& min_ring_diff_v, + const VectorWithOffset& max_ring_diff_v, + const int num_views,const int num_tangential_poss) +: ProjDataInfoBlocksOnCylindrical(scanner_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + num_views, num_tangential_poss), + ring_radius(ring_radius_v), + angular_increment(angular_increment_v) +{ + uncompressed_view_tangpos_to_det1det2_initialised = false; + det1det2_to_uncompressed_view_tangpos_initialised = false; + crystal_map.reset(new GeometryBlocksOnCylindrical(scanner_ptr)); +} + +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +ProjDataInfoBlocksOnCylindricalNoArcCorr(const shared_ptr scanner_ptr, + const VectorWithOffset& num_axial_pos_per_segment, + const VectorWithOffset& min_ring_diff_v, + const VectorWithOffset& max_ring_diff_v, + const int num_views,const int num_tangential_poss) +: ProjDataInfoBlocksOnCylindrical(scanner_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + num_views, num_tangential_poss) +{ + assert(!is_null_ptr(scanner_ptr)); + ring_radius = scanner_ptr->get_effective_ring_radius(); + angular_increment = static_cast(_PI/scanner_ptr->get_num_detectors_per_ring()); + uncompressed_view_tangpos_to_det1det2_initialised = false; + det1det2_to_uncompressed_view_tangpos_initialised = false; + crystal_map.reset(new GeometryBlocksOnCylindrical(scanner_ptr)); +} + + + +ProjDataInfo* +ProjDataInfoBlocksOnCylindricalNoArcCorr::clone() const +{ + return static_cast(new ProjDataInfoBlocksOnCylindricalNoArcCorr(*this)); +} + +bool +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +operator==(const self_type& that) const +{ + if (!base_type::blindly_equals(&that)) + return false; + return + this->ring_radius == that.ring_radius && + this->angular_increment == that.angular_increment; +} + +bool +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +blindly_equals(const root_type * const that_ptr) const +{ + assert(dynamic_cast(that_ptr) != 0); + return + this->operator==(static_cast(*that_ptr)); +} + +string +ProjDataInfoBlocksOnCylindricalNoArcCorr::parameter_info() const +{ + + #ifdef BOOST_NO_STRINGSTREAM + // dangerous for out-of-range, but 'old-style' ostrstream seems to need this + char str[50000]; + ostrstream s(str, 50000); + #else + std::ostringstream s; + #endif + s << "ProjDataInfoBlocksOnCylindricalNoArcCorr := \n"; + s << ProjDataInfoBlocksOnCylindrical::parameter_info(); + s << "End :=\n"; + return s.str(); +} + +/* + TODO make compile time assert + + Warning: + this code makes use of an implementation dependent feature: + bit shifting negative ints to the right. + -1 >> 1 should be -1 + -2 >> 1 should be -1 + This is ok on SUNs (gcc, but probably SUNs cc as well), Parsytec (gcc), + Pentium (gcc, VC++) and probably every other system which uses + the 2-complement convention. +*/ + +/*! + Go from sinograms to detectors. + + Because sinograms are not arc-corrected, tang_pos_num corresponds + to an angle as well. Before interleaving we have that + \verbatim + det_angle_1 = LOR_angle + bin_angle + det_angle_2 = LOR_angle + (Pi - bin_angle) + \endverbatim + (Hint: understand this first at LOR_angle=0, then realise that + other LOR_angles follow just by rotation) + + Code gets slightly intricate because: + - angles have to be defined modulo 2 Pi (so num_detectors) + - interleaving +*/ + +//! build look-up table for get_view_tangential_pos_num_for_det_num_pair() +//Parisa: changed phi assertion +void +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +initialise_uncompressed_view_tangpos_to_det1det2() const +{ + assert(-1 >> 1 == -1); + assert(-2 >> 1 == -1); + + const int num_detectors = + get_scanner_ptr()->get_num_detectors_per_ring(); + + assert(num_detectors%2 == 0); + // check views range from 0 to Pi + /*! + warning The following assersions are slightly different from cylindrical. + because in the function get_phi(bin), get_lor is called. + */ + assert(fabs(Bin(0,0,0,0).view_num()*get_azimuthal_angle_sampling()) < 1.E-4); + assert(fabs(Bin(0,get_num_views(),0,0).view_num()*get_azimuthal_angle_sampling() - _PI) < 1.E-4); + + const int min_tang_pos_num = -(num_detectors/2)+1; + const int max_tang_pos_num = -(num_detectors/2)+num_detectors; + + if (this->get_min_tangential_pos_num() < min_tang_pos_num || + this->get_max_tangential_pos_num() > max_tang_pos_num) + { + error("The tangential_pos range (%d to %d) for this projection data is too large.\n" + "Maximum supported range is from %d to %d", + this->get_min_tangential_pos_num(), this->get_max_tangential_pos_num(), + min_tang_pos_num, max_tang_pos_num); + } + + uncompressed_view_tangpos_to_det1det2.grow(0,num_detectors/2-1); + for (int v_num=0; v_num<=num_detectors/2-1; ++v_num) + { + uncompressed_view_tangpos_to_det1det2[v_num].grow(min_tang_pos_num, max_tang_pos_num); + + for (int tp_num=min_tang_pos_num; tp_num<=max_tang_pos_num; ++tp_num) + { + /* + adapted from CTI code + Note for implementation: avoid using % with negative numbers + so add num_detectors before doing modulo num_detectors) + */ + uncompressed_view_tangpos_to_det1det2[v_num][tp_num].det1_num = + (v_num + (tp_num >> 1) + num_detectors) % num_detectors; + uncompressed_view_tangpos_to_det1det2[v_num][tp_num].det2_num = + (v_num - ( (tp_num + 1) >> 1 ) + num_detectors/2) % num_detectors; + } + } + uncompressed_view_tangpos_to_det1det2_initialised = true; +} + +void +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +initialise_det1det2_to_uncompressed_view_tangpos() const +{ + assert(-1 >> 1 == -1); + assert(-2 >> 1 == -1); + + const int num_detectors = + get_scanner_ptr()->get_num_detectors_per_ring(); + + if (num_detectors%2 != 0) + { + error("Number of detectors per ring should be even but is %d", num_detectors); + } + if (this->get_min_view_num() != 0) + { + error("Minimum view number should currently be zero to be able to use get_view_tangential_pos_num_for_det_num_pair()"); + } + // check views range from 0 to Pi + + + // check views range from 0 to Pi + //! warning The following assersions are slightly different from cylindrical. See the above function + assert(fabs(Bin(0,0,0,0).view_num()*get_azimuthal_angle_sampling()) < 1.E-4); + assert(fabs(Bin(0,get_max_view_num()+1,0,0).view_num()*get_azimuthal_angle_sampling() - _PI) < 1.E-4); + + + //const int min_tang_pos_num = -(num_detectors/2); + //const int max_tang_pos_num = -(num_detectors/2)+num_detectors; + const int max_num_views = num_detectors/2; + + det1det2_to_uncompressed_view_tangpos.grow(0,num_detectors-1); + for (int det1_num=0; det1_num> 1) + num_detectors) % num_detectors; + + /* Now adjust ranges for view_num, tang_pos_num. + The next lines go only wrong in the singular (and irrelevant) case + det_num1 == det_num2 (when tang_pos_num == num_detectors - tang_pos_num) + + We use the combinations of the following 'symmetries' of + (tang_pos_num, view_num) == (tang_pos_num+2*num_views, view_num + num_views) + == (-tang_pos_num, view_num + num_views) + Using the latter interchanges det_num1 and det_num2, and this leaves + the LOR the same in the 2D case. However, in 3D this interchanges the rings + as well. So, we keep track of this in swap_detectors, and return its final + value. + */ + if (view_num < max_num_views) + { + if (tang_pos_num >= max_num_views) + { + tang_pos_num = num_detectors - tang_pos_num; + swap_detectors = 1; + } + else + { + swap_detectors = 0; + } + } + else + { + view_num -= max_num_views; + if (tang_pos_num >= max_num_views) + { + tang_pos_num -= num_detectors; + swap_detectors = 0; + } + else + { + tang_pos_num *= -1; + swap_detectors = 1; + } + } + + det1det2_to_uncompressed_view_tangpos[det1_num][det2_num].view_num = view_num; + det1det2_to_uncompressed_view_tangpos[det1_num][det2_num].tang_pos_num = tang_pos_num; + det1det2_to_uncompressed_view_tangpos[det1_num][det2_num].swap_detectors = swap_detectors==0; + } + } + det1det2_to_uncompressed_view_tangpos_initialised = true; +} + +unsigned int +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +get_num_det_pos_pairs_for_bin(const Bin& bin) const +{ + return + get_num_ring_pairs_for_segment_axial_pos_num(bin.segment_num(), + bin.axial_pos_num())* + get_view_mashing_factor(); +} + +void +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +get_all_det_pos_pairs_for_bin(vector >& dps, + const Bin& bin) const +{ + this->initialise_uncompressed_view_tangpos_to_det1det2_if_not_done_yet(); + + dps.resize(get_num_det_pos_pairs_for_bin(bin)); + + const ProjDataInfoBlocksOnCylindrical::RingNumPairs& ring_pairs = + get_all_ring_pairs_for_segment_axial_pos_num(bin.segment_num(), + bin.axial_pos_num()); + // not sure how to handle mashing with non-zero view offset... + assert(get_min_view_num()==0); + + unsigned int current_dp_num=0; + for (int uncompressed_view_num=bin.view_num()*get_view_mashing_factor(); + uncompressed_view_num<(bin.view_num()+1)*get_view_mashing_factor(); + ++uncompressed_view_num) + { + const int det1_num = + uncompressed_view_tangpos_to_det1det2[uncompressed_view_num][bin.tangential_pos_num()].det1_num; + const int det2_num = + uncompressed_view_tangpos_to_det1det2[uncompressed_view_num][bin.tangential_pos_num()].det2_num; + for (ProjDataInfoBlocksOnCylindrical::RingNumPairs::const_iterator rings_iter = ring_pairs.begin(); + rings_iter != ring_pairs.end(); + ++rings_iter) + { + assert(current_dp_num < get_num_det_pos_pairs_for_bin(bin)); + dps[current_dp_num].pos1().tangential_coord() = det1_num; + dps[current_dp_num].pos1().axial_coord() = rings_iter->first; + dps[current_dp_num].pos2().tangential_coord() = det2_num; + dps[current_dp_num].pos2().axial_coord() = rings_iter->second; + ++current_dp_num; + } + } + assert(current_dp_num == get_num_det_pos_pairs_for_bin(bin)); +} + +//!warning Use crystal map +Succeeded +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +find_scanner_coordinates_given_cartesian_coordinates(int& det1, int& det2, int& ring1, int& ring2, + const CartesianCoordinate3D& c1, + const CartesianCoordinate3D& c2) const +{ + + DetectionPosition<> det_pos1; + DetectionPosition<> det_pos2; + if (crystal_map->find_detection_position_given_cartesian_coordinate(det_pos1, c1)==Succeeded::no || + crystal_map->find_detection_position_given_cartesian_coordinate(det_pos2, c2)==Succeeded::no) + { + return Succeeded::no; + } + + det1 = det_pos1.tangential_coord(); + det2 = det_pos2.tangential_coord(); + ring1 = det_pos1.axial_coord(); + ring2 = det_pos2.axial_coord(); + + assert(det1 >=0 && det1get_num_detectors_per_ring()); + assert(det2 >=0 && det2get_num_detectors_per_ring()); + + return + (ring1 >=0 && ring1get_num_rings() && + ring2 >=0 && ring2get_num_rings() && + det1!=det2) + ? Succeeded::yes : Succeeded::no; +} + + +void +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +find_cartesian_coordinates_of_detection( + CartesianCoordinate3D& coord_1, + CartesianCoordinate3D& coord_2, + const Bin& bin) const +{ + // find detectors + int det_num_a; + int det_num_b; + int ring_a; + int ring_b; + get_det_pair_for_bin(det_num_a, ring_a, + det_num_b, ring_b, bin); + + // find corresponding cartesian coordinates + find_cartesian_coordinates_given_scanner_coordinates(coord_1,coord_2, + ring_a,ring_b,det_num_a,det_num_b); + return; +} + +//!warning Use crystal map +void +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +find_cartesian_coordinates_given_scanner_coordinates(CartesianCoordinate3D& coord_1, + CartesianCoordinate3D& coord_2, + const int Ring_A,const int Ring_B, + const int det1, const int det2) const +{ + assert(0<=det1); + assert(det1get_num_detectors_per_ring()); + assert(0<=det2); + assert(det2get_num_detectors_per_ring()); + + DetectionPosition<> det_pos1; + DetectionPosition<> det_pos2; + det_pos1.tangential_coord() = det1; + det_pos2.tangential_coord() = det2; + det_pos1.axial_coord() = Ring_A; + det_pos2.axial_coord() = Ring_B; + + if (crystal_map->find_cartesian_coordinate_given_detection_position(coord_1, det_pos1)==Succeeded::yes && + crystal_map->find_cartesian_coordinate_given_detection_position(coord_2, det_pos2)==Succeeded::yes) + { + return; + } + else + { + std::cerr<<"WARNING: couldn't find corresponding cartesian coordinates for given detection positions.\n"; + return; + } +} + + +void +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +find_bin_given_cartesian_coordinates_of_detection(Bin& bin, + const CartesianCoordinate3D& coord_1, + const CartesianCoordinate3D& coord_2) const +{ + int det_num_a; + int det_num_b; + int ring_a; + int ring_b; + + // given two CartesianCoordinates find the intersection + if (find_scanner_coordinates_given_cartesian_coordinates(det_num_a,det_num_b, + ring_a, ring_b, + coord_1, + coord_2) == + Succeeded::no) + { + bin.set_bin_value(-1); + return; + } + + // check rings are in valid range + // this should have been done by find_scanner_coordinates_given_cartesian_coordinates + assert(!(ring_a<0 || + ring_a>=get_scanner_ptr()->get_num_rings() || + ring_b<0 || + ring_b>=get_scanner_ptr()->get_num_rings())); + + if (get_bin_for_det_pair(bin, + det_num_a, ring_a, + det_num_b, ring_b) == Succeeded::no || + bin.tangential_pos_num() < get_min_tangential_pos_num() || + bin.tangential_pos_num() > get_max_tangential_pos_num()) + bin.set_bin_value(-1); +} + +//!warning Use crystal map +Bin +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +get_bin(const LOR& lor) const +{ + Bin bin; + + const LORAs2Points & lor_as_2points = dynamic_cast &>(lor); + + CartesianCoordinate3D _p1 = lor_as_2points.p1(); + CartesianCoordinate3D _p2 = lor_as_2points.p2(); + + DetectionPosition<> det_pos1; + DetectionPosition<> det_pos2; + if (crystal_map->find_detection_position_given_cartesian_coordinate(det_pos1, _p1)==Succeeded::no || + crystal_map->find_detection_position_given_cartesian_coordinate(det_pos2, _p2)==Succeeded::no) + { + bin.set_bin_value(-1); + return bin; + } + + DetectionPositionPair<> det_pos_pair; + det_pos_pair.pos1() = det_pos1; + det_pos_pair.pos2() = det_pos2; + + + if (get_bin_for_det_pos_pair(bin, det_pos_pair) == Succeeded::yes&& + bin.tangential_pos_num() >= get_min_tangential_pos_num() && + bin.tangential_pos_num() <= get_max_tangential_pos_num()) + { + bin.set_bin_value(1); + return bin; + } + else + { + bin.set_bin_value(-1); + return bin; + } + +} + + +END_NAMESPACE_STIR diff --git a/src/buildblock/Scanner.cxx b/src/buildblock/Scanner.cxx index 7022eb8b3..038d1175f 100644 --- a/src/buildblock/Scanner.cxx +++ b/src/buildblock/Scanner.cxx @@ -31,6 +31,7 @@ \author Kris Thielemans \author Claire Labbe \author PARAPET project + \author Parisa Khateri */ #include "stir/Scanner.h" @@ -434,7 +435,13 @@ Scanner::Scanner(Type type_v, const list& list_of_names_v, int num_transaxial_crystals_per_singles_unit_v, int num_detector_layers_v, float energy_resolution_v, - float reference_energy_v) + float reference_energy_v, + const string& scanner_orientation_v, + const string& scanner_geometry_v, + float axial_crystal_spacing_v, + float transaxial_crystal_spacing_v, + float axial_block_spacing_v, + float transaxial_block_spacing_v) { set_params(type_v, list_of_names_v, num_rings_v, max_num_non_arccorrected_bins_v, @@ -449,7 +456,13 @@ Scanner::Scanner(Type type_v, const list& list_of_names_v, num_transaxial_crystals_per_singles_unit_v, num_detector_layers_v, energy_resolution_v, - reference_energy_v); + reference_energy_v, + scanner_orientation_v, + scanner_geometry_v, + axial_crystal_spacing_v, + transaxial_crystal_spacing_v, + axial_block_spacing_v, + transaxial_block_spacing_v); } @@ -466,7 +479,13 @@ Scanner::Scanner(Type type_v, const string& name, int num_transaxial_crystals_per_singles_unit_v, int num_detector_layers_v, float energy_resolution_v, - float reference_energy_v) + float reference_energy_v, + const string& scanner_orientation_v, + const string& scanner_geometry_v, + float axial_crystal_spacing_v, + float transaxial_crystal_spacing_v, + float axial_block_spacing_v, + float transaxial_block_spacing_v) { set_params(type_v, string_list(name), num_rings_v, max_num_non_arccorrected_bins_v, @@ -481,7 +500,13 @@ Scanner::Scanner(Type type_v, const string& name, num_transaxial_crystals_per_singles_unit_v, num_detector_layers_v, energy_resolution_v, - reference_energy_v); + reference_energy_v, + scanner_orientation_v, + scanner_geometry_v, + axial_crystal_spacing_v, + transaxial_crystal_spacing_v, + axial_block_spacing_v, + transaxial_block_spacing_v); } @@ -506,7 +531,13 @@ set_params(Type type_v,const list& list_of_names_v, int num_transaxial_crystals_per_singles_unit_v, int num_detector_layers_v, float energy_resolution_v, - float reference_energy_v) + float reference_energy_v, + const string& scanner_orientation_v, + const string& scanner_geometry_v, + float axial_crystal_spacing_v, + float transaxial_crystal_spacing_v, + float axial_block_spacing_v, + float transaxial_block_spacing_v) { set_params(type_v, list_of_names_v, num_rings_v, max_num_non_arccorrected_bins_v, @@ -521,7 +552,13 @@ set_params(Type type_v,const list& list_of_names_v, num_transaxial_crystals_per_singles_unit_v, num_detector_layers_v, energy_resolution_v, - reference_energy_v); + reference_energy_v, + scanner_orientation_v, + scanner_geometry_v, + axial_crystal_spacing_v, + transaxial_crystal_spacing_v, + axial_block_spacing_v, + transaxial_block_spacing_v); } @@ -542,7 +579,13 @@ set_params(Type type_v,const list& list_of_names_v, int num_transaxial_crystals_per_singles_unit_v, int num_detector_layers_v, float energy_resolution_v, - float reference_energy_v) + float reference_energy_v, + const string& scanner_orientation_v, + const string& scanner_geometry_v, + float axial_crystal_spacing_v, + float transaxial_crystal_spacing_v, + float axial_block_spacing_v, + float transaxial_block_spacing_v) { type = type_v; list_of_names = list_of_names_v; @@ -565,6 +608,13 @@ set_params(Type type_v,const list& list_of_names_v, energy_resolution = energy_resolution_v; reference_energy = reference_energy_v; + + scanner_orientation = scanner_orientation_v; + scanner_geometry = scanner_geometry_v; + axial_crystal_spacing = axial_crystal_spacing_v; + transaxial_crystal_spacing = transaxial_crystal_spacing_v; + axial_block_spacing = axial_block_spacing_v; + transaxial_block_spacing = transaxial_block_spacing_v; } @@ -699,6 +749,37 @@ check_consistency() const } } } + + if (get_scanner_geometry() == "BlocksOnCylindrical") + {//! Check consistency of axial and transaxial spacing for block geometry + if (get_axial_crystal_spacing()*get_num_axial_crystals_per_block() > get_axial_block_spacing()) + { + warning("Scanner %s: inconsistent axial spacing:\n" + "\taxial_crystal_spacing %f muliplied by num_axial_crystals_per_block %d should fit into axial_block_spacing %f", + this->get_name().c_str(), + get_axial_crystal_spacing(), get_num_axial_crystals_per_block(), get_axial_block_spacing()); + return Succeeded::no; + } + if (get_transaxial_crystal_spacing()*get_num_transaxial_crystals_per_block() > get_transaxial_block_spacing()) + { + warning("Scanner %s: inconsistent transaxial spacing:\n" + "\ttransaxial_crystal_spacing %f muliplied by num_transaxial_crystals_per_block %d should fit into transaxial_block_spacing %f", + this->get_name().c_str(), + get_transaxial_crystal_spacing(), get_num_transaxial_crystals_per_block(), get_transaxial_block_spacing()); + return Succeeded::no; + } + + if (get_transaxial_block_spacing()*get_num_transaxial_blocks_per_bucket() + < 2*inner_ring_radius*tan(_PI/get_num_transaxial_blocks()/get_num_transaxial_blocks_per_bucket())) + { + warning("Scanner %s: inconsistent transaxial spacing:\n" + "\ttransaxial_block_spacing %f muliplied by num_transaxial_blocks_per_bucket %d should fit into a polygon that encircles a cylinder with inner_ring_radius %f", + this->get_name().c_str(), + get_transaxial_block_spacing(), get_num_transaxial_blocks_per_bucket(), get_inner_ring_radius()); + return Succeeded::no; + } + + } return Succeeded::yes; } @@ -734,6 +815,10 @@ if (!close_enough(energy_resolution, scanner.energy_resolution) && close_enough(ring_spacing, scanner.ring_spacing) && close_enough(bin_size,scanner.bin_size) && close_enough(intrinsic_tilt,scanner.intrinsic_tilt) && + close_enough(axial_crystal_spacing, scanner.axial_crystal_spacing) && + close_enough(transaxial_crystal_spacing, scanner.transaxial_crystal_spacing) && + close_enough(axial_block_spacing, scanner.axial_block_spacing) && + close_enough(transaxial_block_spacing, scanner.transaxial_block_spacing) && (num_transaxial_blocks_per_bucket == scanner.num_transaxial_blocks_per_bucket) && (num_axial_blocks_per_bucket == scanner.num_axial_blocks_per_bucket) && (num_axial_crystals_per_block == scanner.num_axial_crystals_per_block) && @@ -807,6 +892,27 @@ Scanner::parameter_info() const << "Number of crystals per singles unit in transaxial direction := " << get_num_transaxial_crystals_per_singles_unit() << '\n'; + //block geometry description + if (get_scanner_orientation() != "" && get_scanner_geometry() != "") + { + s << "Scanner orientation (X or Y) := " + < -1 && ReferenceEnergy > -1) - Scanner* scanner_ptr = - new Scanner(type, string_list(name), - num_detectors_per_ring, NoRings, - NoBins, NoBins, - InnerRingRadius, AverageDepthOfInteraction, - RingSpacing, BinSize,intrTilt*float(_PI)/180, - AxialBlocksPerBucket,TransBlocksPerBucket, - AxialCrystalsPerBlock,TransaxialCrystalsPerBlock, - AxialCrstalsPerSinglesUnit, TransaxialCrystalsPerSinglesUnit, - num_detector_layers, - EnergyResolution, - ReferenceEnergy ); - else - Scanner* scanner_ptr = - new Scanner(type, string_list(name), - num_detectors_per_ring, NoRings, - NoBins, NoBins, - InnerRingRadius, AverageDepthOfInteraction, - RingSpacing, BinSize,intrTilt*float(_PI)/180, - AxialBlocksPerBucket,TransBlocksPerBucket, - AxialCrystalsPerBlock,TransaxialCrystalsPerBlock, - AxialCrstalsPerSinglesUnit, TransaxialCrystalsPerSinglesUnit, - num_detector_layers); + scanner_ptr = + new Scanner(type, string_list(name), + num_detectors_per_ring, NoRings, + NoBins, NoBins, + InnerRingRadius, AverageDepthOfInteraction, + RingSpacing, BinSize,intrTilt*float(_PI)/180, + AxialBlocksPerBucket,TransBlocksPerBucket, + AxialCrystalsPerBlock,TransaxialCrystalsPerBlock, + AxialCrstalsPerSinglesUnit, TransaxialCrystalsPerSinglesUnit, + num_detector_layers, + EnergyResolution, + ReferenceEnergy, + ScannerOrientation, + ScannerGeometry, + TransaxialCrystalSpacing, + AxialCrystalSpacing, + AxialBlockSpacing, + TransaxialBlockSpacing); if (scanner_ptr->check_consistency()==Succeeded::yes || !ask("Ask questions again?",true)) diff --git a/src/buildblock/VoxelsOnCartesianGrid.cxx b/src/buildblock/VoxelsOnCartesianGrid.cxx index e9cdbe4bf..28b9cc3dc 100644 --- a/src/buildblock/VoxelsOnCartesianGrid.cxx +++ b/src/buildblock/VoxelsOnCartesianGrid.cxx @@ -24,6 +24,7 @@ \author Sanida Mustafovic \author Kris Thielemans (with help from Alexey Zverovich) \author PARAPET project + \author Parisa Khateri */ @@ -44,6 +45,8 @@ #include #include #include "stir/unique_ptr.h" +#include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" + #ifndef STIR_NO_NAMESPACES using std::ifstream; using std::max; @@ -86,6 +89,28 @@ static void find_sampling_and_z_size( ? proj_data_info_cyl_ptr->get_num_axial_poss(0) : 2*proj_data_info_cyl_ptr->get_num_axial_poss(0) - 1; } + else if (const ProjDataInfoBlocksOnCylindrical* + proj_data_info_blk_ptr = + dynamic_cast(proj_data_info_ptr)) + { + // the case of BlocksOnCylindrical data + + z_sampling = proj_data_info_blk_ptr->get_ring_spacing()/2; + + // for 'span>1' case, we take z_size = number of sinograms in segment 0 + // for 'span==1' case, we take 2*num_rings-1 + + // first check if we have segment 0 + assert(proj_data_info_blk_ptr->get_min_segment_num() <= 0); + assert(proj_data_info_blk_ptr->get_max_segment_num() >= 0); + + if (z_size<0) + z_size = + proj_data_info_blk_ptr->get_max_ring_difference(0) > + proj_data_info_blk_ptr->get_min_ring_difference(0) + ? proj_data_info_blk_ptr->get_num_axial_poss(0) + : 2*proj_data_info_blk_ptr->get_num_axial_poss(0) - 1; + } else { // this is any other weird projection data. We just check sampling of segment 0 diff --git a/src/include/stir/GeometryBlocksOnCylindrical.h b/src/include/stir/GeometryBlocksOnCylindrical.h new file mode 100644 index 000000000..d6817a18a --- /dev/null +++ b/src/include/stir/GeometryBlocksOnCylindrical.h @@ -0,0 +1,113 @@ + +/* + +TODO copyright and License + +*/ + +/*! + \file + \ingroup projdata + + \brief Declaration of class stir::GeometryBlocksOnCylindrical + + \author Parisa Khateri + +*/ +#ifndef __stir_GeometryBlocksOnCylindrical_H__ +#define __stir_GeometryBlocksOnCylindrical_H__ + +#include "stir/DetectionPosition.h" +#include "stir/CartesianCoordinate3D.h" +#include "stir/Scanner.h" +#include "stir/shared_ptr.h" +#include "stir/Array.h" +#include "stir/make_array.h" +#include "stir/numerics/MatrixFunction.h" +#include +#include +#include +#include +#include "stir/Succeeded.h" + +START_NAMESPACE_STIR + +/*! + \ingroup projdata + \brief A helper class to build the crystal map based on scanner info. + + \This class builds two maps between cartesian coordinates (z, y, x) + \ and the corresponding detection position (tangential_num, axial_num, radial_num) for each crystal. + \The crystal map, then, is used in ProjDataInfoBlocksOnCylindrical, ProjDataInfoBlocksOnCylindricalNoArcCorr, and CListRecordSAFIR + + \The center of first ring is the center of coordinates. + \Distances are from center to center of crystals. + +*/ + +class GeometryBlocksOnCylindrical +{ + + +public: + + //! Consstructors + GeometryBlocksOnCylindrical(); + + GeometryBlocksOnCylindrical(const shared_ptr &scanner_ptr_v); + + //! Destructor + ~GeometryBlocksOnCylindrical() {} + + //! comparison operator for DetectionPosition class. needded to be used as a key type in building map + class compare_det_pos{ + public: + bool operator() (const stir::DetectionPosition<>& , const stir::DetectionPosition<>&) const; + }; + + //! comparison operator for CartesianCoordinate3D class. needded to be used as a key type in building map + class compare_cartesian_coord{ + public: + bool operator() (const stir::CartesianCoordinate3D& , const stir::CartesianCoordinate3D&) const; + }; + + //! Get rotation matrix for a given angle around z axis + inline stir::Array<2, float> get_rotation_matrix(float alpha); + + //! Build crystal map in cartesian coordinate + void build_crystal_maps(); + + //! Get cartesian coordinate for a given detection position + inline Succeeded + find_cartesian_coordinate_given_detection_position(CartesianCoordinate3D& , + DetectionPosition<>); + + //! Get cartesian coordinate for a given detection position + inline Succeeded + find_detection_position_given_cartesian_coordinate(DetectionPosition<>&, + CartesianCoordinate3D); + + //! Get scanner pointer + inline const Scanner* get_scanner_ptr() const; + + +private: + //! member variables + shared_ptr scanner_ptr; + std::map, + stir::CartesianCoordinate3D, + stir::GeometryBlocksOnCylindrical::compare_det_pos> cartesian_coord_map_given_detection_position_keys; + std::map, + stir::DetectionPosition<>, + stir::GeometryBlocksOnCylindrical::compare_cartesian_coord> detection_position_map_given_cartesian_coord_keys_3_decimal; + std::map, + stir::DetectionPosition<>, + stir::GeometryBlocksOnCylindrical::compare_cartesian_coord> detection_position_map_given_cartesian_coord_keys_2_decimal; + +}; + +END_NAMESPACE_STIR + +#include "stir/GeometryBlocksOnCylindrical.inl" + +#endif diff --git a/src/include/stir/GeometryBlocksOnCylindrical.inl b/src/include/stir/GeometryBlocksOnCylindrical.inl new file mode 100644 index 000000000..24e5524cb --- /dev/null +++ b/src/include/stir/GeometryBlocksOnCylindrical.inl @@ -0,0 +1,97 @@ + +/* + +TODO copyright and License + +*/ + +/*! + \file + \ingroup projdata + + \brief Implementation of inline functions of class stir::GeometryBlocksOnCylindrical + + \author Parisa Khateri + +*/ + +START_NAMESPACE_STIR + + +stir::Array<2, float> +GeometryBlocksOnCylindrical:: +get_rotation_matrix(float alpha) +{ + return stir::make_array( + stir::make_1d_array(1.F,0.F,0.F), + stir::make_1d_array(0.F, std::cos(alpha), std::sin(alpha)), + stir::make_1d_array(0.F, -1*std::sin(alpha), std::cos(alpha)) + ); +} + +Succeeded +GeometryBlocksOnCylindrical:: +find_cartesian_coordinate_given_detection_position(CartesianCoordinate3D& cart_coord, + DetectionPosition<> det_pos) +{ + if (cartesian_coord_map_given_detection_position_keys.count(det_pos)) + { + cart_coord = cartesian_coord_map_given_detection_position_keys[det_pos]; + return Succeeded::yes; + } + else + { + warning("detection position with (tangential_coord, axial_coord, radial_coord)=(%d, %d, %d) does not exist in the inner map", + det_pos.tangential_coord(), det_pos.axial_coord(), det_pos.radial_coord()); + return Succeeded::no; + } +} + +Succeeded +GeometryBlocksOnCylindrical:: +find_detection_position_given_cartesian_coordinate(DetectionPosition<>& det_pos, + CartesianCoordinate3D cart_coord) +{ + /*! first round the cartesian coordinates, it might happen that the cart_coord + is not precisely pointing to the center of the crystal and + then the det_pos cannot be found using the map + */ + //rounding cart_coord to 3 decimal place and find det_pos + cart_coord.z() = (round(cart_coord.z()*1000.0))/1000.0; + cart_coord.y() = (round(cart_coord.y()*1000.0))/1000.0; + cart_coord.x() = (round(cart_coord.x()*1000.0))/1000.0; + if (detection_position_map_given_cartesian_coord_keys_3_decimal.count(cart_coord)) + { + det_pos = detection_position_map_given_cartesian_coord_keys_3_decimal[cart_coord]; + return Succeeded::yes; + } + else + { + //rounding cart_coord to 3 decimal place and find det_pos + cart_coord.z() = (round(cart_coord.z()*100.0))/100.0; + cart_coord.y() = (round(cart_coord.y()*100.0))/100.0; + cart_coord.x() = (round(cart_coord.x()*100.0))/100.0; + if (detection_position_map_given_cartesian_coord_keys_2_decimal.count(cart_coord)) + { + det_pos = detection_position_map_given_cartesian_coord_keys_2_decimal[cart_coord]; + return Succeeded::yes; + } + else + { + warning("cartesian coordinate (x, y, z)=(%f, %f, %f) does not exist in the inner map", + cart_coord.x(), cart_coord.y(), cart_coord.z()); + return Succeeded::no; + } + } +} + +const Scanner* +GeometryBlocksOnCylindrical:: +get_scanner_ptr() const +{ + return scanner_ptr.get(); +} + + + +END_NAMESPACE_STIR diff --git a/src/include/stir/IO/InterfileHeader.h b/src/include/stir/IO/InterfileHeader.h index 2a0882b53..a8943f168 100644 --- a/src/include/stir/IO/InterfileHeader.h +++ b/src/include/stir/IO/InterfileHeader.h @@ -24,6 +24,7 @@ \author Kris Thielemans \author Sanida Mustafovic \author PARAPET project + \author Parisa Khateri See http://stir.sourceforge.net for a description of the full proposal for Interfile headers for 3D PET. @@ -252,6 +253,15 @@ class InterfilePDFSHeader : public InterfileHeader float energy_resolution; //! Reference energy. float reference_energy; + + //! new variables for block geometry + std::string scanner_orientation; + std::string scanner_geometry; + float axial_distance_between_crystals_in_cm; + float transaxial_distance_between_crystals_in_cm; + float axial_distance_between_blocks_in_cm; + float transaxial_distance_between_blocks_in_cm; + // end scanner parameters double effective_central_bin_size_in_cm; diff --git a/src/include/stir/LORCoordinates.h b/src/include/stir/LORCoordinates.h index 3c62f9913..2499e259a 100644 --- a/src/include/stir/LORCoordinates.h +++ b/src/include/stir/LORCoordinates.h @@ -9,6 +9,7 @@ \brief defines various classes for specifying a line in 3 dimensions \warning This is all preliminary and likely to change. \author Kris Thielemans + \author Parisa Khateri */ @@ -308,6 +309,11 @@ class LORAs2Points : public LOR Succeeded get_intersections_with_cylinder(LORAs2Points&, const double radius) const; + + //! Calculate intersections with block. Used in: ProjDataInfoBlocksOnCylindrical::get_LOR + Succeeded + change_representation_for_block(LORInAxialAndNoArcCorrSinogramCoordinates&, + const double radius) const; private: CartesianCoordinate3D _p1; diff --git a/src/include/stir/LORCoordinates.inl b/src/include/stir/LORCoordinates.inl index 6c84a0947..593498a2f 100644 --- a/src/include/stir/LORCoordinates.inl +++ b/src/include/stir/LORCoordinates.inl @@ -7,7 +7,7 @@ \brief Implementations for LORCoordinates.h \warning This is all preliminary and likely to change. \author Kris Thielemans - + \author Parisa Khateri */ /* @@ -443,6 +443,40 @@ get_intersections_with_cylinder(LORAs2Points& lor, return find_LOR_intersections_with_cylinder(lor, *this, radius); } +template +Succeeded +LORAs2Points:: +change_representation_for_block(LORInAxialAndNoArcCorrSinogramCoordinates& lor, + const double radius) const +{ + const CartesianCoordinate3D& c1 = this->p1(); + const CartesianCoordinate3D& c2 = this->p2(); + + //To check if LOR is inside the detector + const CartesianCoordinate3D d = c2 - c1; + const double dxy2 = (square(d.x())+square(d.y())); + const double argsqrt= + (square(radius)*dxy2-square(d.x()*c1.y()-d.y()*c1.x())); + + LORInCylinderCoordinates cyl_coords; + cyl_coords.reset(static_cast(radius)); + + cyl_coords.p1().psi() = + from_min_pi_plus_pi_to_0_2pi(static_cast(atan2(c1.x(),-c1.y()))); + cyl_coords.p2().psi() = + from_min_pi_plus_pi_to_0_2pi(static_cast(atan2(c2.x(),-c2.y()))); + cyl_coords.p1().z() = + static_cast(c1.z()); + cyl_coords.p2().z() = + static_cast(c2.z()); + lor = cyl_coords; + + if (argsqrt<=0) + return Succeeded::no; // LOR is outside detector radius + else + return Succeeded::yes; +} + #define DEFINE_LOR_GET_FUNCTIONS(TYPE) \ template \ Succeeded \ diff --git a/src/include/stir/ProjDataInfoBlocksOnCylindrical.h b/src/include/stir/ProjDataInfoBlocksOnCylindrical.h new file mode 100644 index 000000000..38a276aaa --- /dev/null +++ b/src/include/stir/ProjDataInfoBlocksOnCylindrical.h @@ -0,0 +1,313 @@ + +/* + +TODO copyright and License + +*/ + +/*! + \file + \ingroup projdata + + \brief Declaration of class stir::ProjDataInfoBlocksOnCylindrical + + \author Parisa Khateri + +*/ +#ifndef __stir_ProjDataInfoBlocksOnCylindrical_H__ +#define __stir_ProjDataInfoBlocksOnCylindrical_H__ + + +#include "stir/ProjDataInfo.h" +#include +#include + +#ifndef STIR_NO_NAMESPACES +using std::vector; +using std::pair; +#endif + +START_NAMESPACE_STIR + +class Succeeded; +template class CartesianCoordinate3D; + +/*! + \ingroup projdata + \brief projection data info for data corresponding to a + 'Blocks-on-cylindrical' sampling. +*/ + +class ProjDataInfoBlocksOnCylindrical: public ProjDataInfo +{ +private: + typedef ProjDataInfo base_type; + typedef ProjDataInfoBlocksOnCylindrical self_type; + +public: + //! Type used by get_all_ring_pairs_for_segment_axial_pos_num() + typedef vector > RingNumPairs; + + //! Constructors + ProjDataInfoBlocksOnCylindrical(); + //! Constructor given all the necessary information + /*! The min and max ring difference in each segment are passed + as VectorWithOffsets. All three vectors have to have index ranges + from min_segment_num to max_segment_num. + + \warning Most of this library assumes that segment 0 corresponds + to an average ring difference of 0. + */ + ProjDataInfoBlocksOnCylindrical(const shared_ptr& scanner_ptr, + const VectorWithOffset& num_axial_poss_per_segment, //index ranges from min_segment_num to max_segment_num + const VectorWithOffset& min_ring_diff, + const VectorWithOffset& max_ring_diff, + const int num_views,const int num_tangential_poss); + + inline virtual float get_tantheta(const Bin&) const; + + inline float get_phi(const Bin&) const; + + inline float get_t(const Bin&) const; + + //! Return z-coordinate of the middle of the LOR + /*! + The 0 of the z-axis is chosen in the middle of the scanner. + */ + inline float get_m(const Bin&) const; + + virtual void + get_LOR(LORInAxialAndNoArcCorrSinogramCoordinates& lor, const Bin& bin) const; + + void set_azimuthal_angle_sampling(const float angle); + + //! set new number of views, covering the same azimuthal angle range + /*! calls ProjDataInfo::set_num_views(), but makes sure that we cover the + same range of angles as before (usually, but not necessarily, 180 degrees) + by adjusting azimuthal_angle_sampling. + */ + virtual void + set_num_views(const int new_num_views); + + //! Get the azimuthal sampling (in radians) + inline float get_azimuthal_angle_sampling() const; + virtual inline float get_sampling_in_t(const Bin&) const; + virtual inline float get_sampling_in_m(const Bin&) const; + + //! Get the axial sampling (e.g in z_direction) + /*! + \warning The implementation of this function currently assumes that the axial + sampling is equal to the ring spacing for non-spanned data + (i.e. no axial compression), while it is half the + ring spacing for spanned data. + */ + inline float get_axial_sampling(int segment_num) const; + + //! Get average ring difference for the given segment + inline float get_average_ring_difference(int segment_num) const; + //! Get minimum ring difference for the given segment + inline int get_min_ring_difference(int segment_num) const; + //! Get maximum ring difference for the given segment + inline int get_max_ring_difference(int segment_num) const; + + //! Set minimum ring difference + void set_min_ring_difference(int min_ring_diff_v, int segment_num); + //! Set maximum ring difference + void set_max_ring_difference(int max_ring_diff_v, int segment_num); + + virtual void set_num_axial_poss_per_segment(const VectorWithOffset& num_axial_poss_per_segment); + virtual void set_min_axial_pos_num(const int min_ax_pos_num, const int segment_num); + virtual void set_max_axial_pos_num(const int max_ax_pos_num, const int segment_num); + virtual void reduce_segment_range(const int min_segment_num, const int max_segment_num); + + //! Set detector ring radius for all views + inline void set_ring_radii_for_all_views(const VectorWithOffset& new_ring_radius); + + //! Get detector ring radius for all views + inline VectorWithOffset get_ring_radii_for_all_views() const; + + //! Get detector ring radius + inline float get_ring_radius() const; + inline float get_ring_radius( const int view_num) const; + + //! Get detector ring spacing + inline float get_ring_spacing() const; + + //! Set detector ring spacing + void set_ring_spacing(float ring_spacing_v); + + //! Get the mashing factor, i.e. how many 'original' views are combined. + /*! mashing factor = Ndet/(2Nview) + This gets the result by comparing the number of detectors in the scanner_ptr + with the actual number of views. + \warning In the debug version, it is checked with an assert() that the number of + detectors is an even multiple of the number of views. This is not checked in + the normal version though. + */ + inline int get_view_mashing_factor() const; + + //! Find which segment a particular ring difference belongs to + /*! + \return Succeeded::yes when a corresponding segment was found. + */ + inline Succeeded + get_segment_num_for_ring_difference(int& segment_num, const int ring_diff) const; + + //! Find to which segment and axial position a ring pair contributes + /*! + \a ring1, \a ring2 have to be between 0 and scanner.get_num_rings()-1. + \return Succeeded::yes when a corresponding segment was found. + \warning axial_pos_num returned might be outside the actual range in the proj_data_info. + + \For CTI data with span, this essentially implements a 'michelogram'. + + \warning Current implementation assumes that the axial positions start from 0 for + the first ring-pair in the segment. + + \warning The implementation of this function currently assumes that the axial + sampling is equal to the ring spacing for non-spanned data + (i.e. no axial compression), while it is half the ring spacing for spanned data. + */ + inline Succeeded + get_segment_axial_pos_num_for_ring_pair(int& segment_num, + int& axial_pos_num, + const int ring1, + const int ring2) const; + + //! Find all ring pairs that contribute to a segment and axial position + /*! + \a ring1, \a ring2 will be between 0 and scanner.get_num_rings()-1. + + \warning The implementation of this function currently assumes that the axial + sampling is equal to the ring spacing for non-spanned data + (i.e. no axial compression), while it is half the ring spacing for spanned data. + */ + inline const RingNumPairs& + get_all_ring_pairs_for_segment_axial_pos_num(const int segment_num, + const int axial_pos_num) const; + //! Find the number of ring pairs that contribute to a segment and axial position + /*! + \warning The implementation of this function currently assumes that the axial + sampling is equal to the ring spacing for non-spanned data + (i.e. no axial compression), while it is half the ring spacing for spanned data. + */ + inline unsigned + get_num_ring_pairs_for_segment_axial_pos_num(const int segment_num, + const int axial_pos_num) const; + + //! Find a ring pair that contributes to a segment and axial position + /*! + \a ring1, \a ring2 will be between 0 and scanner.get_num_rings()-1. + + \warning Currently only works when no axial compression is used for the segment (i.e. + min_ring_diff = max_ring_diff). Otherwise, a error() will be called. + + \warning The implementation of this function currently assumes that the axial + sampling is equal to the ring spacing for non-spanned data + (i.e. no axial compression), while it is half the ring spacing for spanned data. + */ + void + get_ring_pair_for_segment_axial_pos_num(int& ring1, + int& ring2, + const int segment_num, + const int axial_pos_num) const; + + virtual std::string parameter_info() const; + +protected: + + //! a variable that is set if the data corresponds to physical rings in the scanner + /*! This is (only) used to prevent get_segment_axial_pos_num_for_ring_pair() et al + to go wild. Indeed, for cases where there's cylindrical sampling, but not + really any physical rings associated to the sampling, those functions will + return invalid information. + + The prime case where this is used is for data corresponding to (nearly) + continuous detectors, such as DHCI systems, or the HiDAC. + + Ideally, this would be done by having a separate class for such systems which + does not contain the ring-difference et al information. This seems to make + the hierarchy too complicated though. + + \bug The value of this variable is currently set by checking if the scanner + is a HiDAC scanner. This needs to be changed. + if scanner is not HiDAC this variable is set to 1 otherwise to 0 (see constructor) + */ + bool sampling_corresponds_to_physical_rings; + +protected: + virtual bool blindly_equals(const root_type * const) const = 0; + +private: + float azimuthal_angle_sampling; + VectorWithOffset ring_radius; + float ring_spacing; + VectorWithOffset min_ring_diff; + VectorWithOffset max_ring_diff; + + /* + Next members have to be mutable as they can be modified by const member + functions. We need this because of the presence of set_min_ring_difference() + which invalidates these precalculated arrays. + If your compiler does not support mutable (and you don't want to upgrade + it to something more sensible), your best bet is to remove the + set_*ring_difference functions, and move the content of + initialise_ring_diff_arrays() to the constructor. (Not recommended!) + */ + + //! This member will signal if the arrays below contain sensible info or not + mutable bool ring_diff_arrays_computed; + + //! This member stores the offsets used in get_m() + /* + //! warning This is not used in block geometry. m is found directly from lors. + mutable VectorWithOffset m_offset; + */ + + //! This member stores the offsets used in get_segment_axial_pos_num_for_ring_pair() + mutable VectorWithOffset ax_pos_num_offset; + + //! This member stores a table converting ring differences to segment numbers + mutable VectorWithOffset ring_diff_to_segment_num; + + //! This member stores a table converting segment/axial_pos to ring1+ring2 + mutable VectorWithOffset > segment_axial_pos_to_ring1_plus_ring2; + + //! This function sets all of the above + void initialise_ring_diff_arrays() const; + + //! This function guarantees that ring_diff_arrays will be set but checks first if was done already + /*! This function is OPENMP thread-safe */ + inline void initialise_ring_diff_arrays_if_not_done_yet() const; + + //! This function checks if max_ring_diff is different from min_ring_diff (set to 2). + /*! in case of difference, there are 2 ax_pos per ring, i.e. an ax_pos between each two rings */ + inline int get_num_axial_poss_per_ring_inc(const int segment_num) const; + + //! This member will signal if the array below contain sensible info or not + mutable bool segment_axial_pos_to_ring_pair_allocated; + + //! This member stores a table used by get_all_ring_pairs_for_segment_axial_pos_num() + mutable VectorWithOffset< VectorWithOffset < shared_ptr > > + segment_axial_pos_to_ring_pair; + + //! allocate table + void allocate_segment_axial_pos_to_ring_pair() const; + + //! initialise one element of the above table + void compute_segment_axial_pos_to_ring_pair(const int segment_num, const int axial_pos_num) const; + + //! to be used in get LOR + virtual void + find_cartesian_coordinates_of_detection(CartesianCoordinate3D& coord_1, + CartesianCoordinate3D& coord_2, + const Bin& bin) const = 0; + +}; + + +END_NAMESPACE_STIR + +#include "stir/ProjDataInfoBlocksOnCylindrical.inl" + +#endif diff --git a/src/include/stir/ProjDataInfoBlocksOnCylindrical.inl b/src/include/stir/ProjDataInfoBlocksOnCylindrical.inl new file mode 100644 index 000000000..e767c77d1 --- /dev/null +++ b/src/include/stir/ProjDataInfoBlocksOnCylindrical.inl @@ -0,0 +1,291 @@ + +/* + +TODO copyright and License + +*/ + +/*! + \file + \ingroup projdata + + \brief Implementation of inline functions of class stir::ProjDataInfoBlocksOnCylindrical + + \author Parisa Khateri + +*/ +// for sqrt +#include +#include "stir/Bin.h" +#include "stir/Succeeded.h" +#include "stir/LORCoordinates.h" +#include "stir/is_null_ptr.h" +#include + +START_NAMESPACE_STIR + +void +ProjDataInfoBlocksOnCylindrical:: +initialise_ring_diff_arrays_if_not_done_yet() const +{ + // for efficiency reasons, use "Double-Checked-Locking(DCL) pattern" with OpenMP atomic operation + // OpenMP v3.1 or later required + // thanks to yohjp: http://stackoverflow.com/questions/27975737/how-to-handle-cached-data-structures-with-multi-threading-e-g-openmp +#if defined(STIR_OPENMP) && _OPENMP >=201012 + bool initialised; +#pragma omp atomic read + initialised = ring_diff_arrays_computed; + + if (!initialised) +#endif + { +#if defined(STIR_OPENMP) +#pragma omp critical(PROJDATAINFOCYLINDRICALRINGDIFFARRAY) +#endif + { + if (!ring_diff_arrays_computed) + initialise_ring_diff_arrays(); + } + } +} + +//! find phi from correspoding lor +float +ProjDataInfoBlocksOnCylindrical::get_phi(const Bin& bin)const +{ + LORInAxialAndNoArcCorrSinogramCoordinates lor; + get_LOR(lor, bin); + if (bin.view_num()==0 && lor.phi()>0.1) + return lor.phi()-_PI; + return lor.phi(); +} + +/*! warning In block geometry m is calculated directly from lor while in + cylindrical geometry m is calculated using m_offset and axial_sampling +*/ +float +ProjDataInfoBlocksOnCylindrical::get_m(const Bin& bin) const +{ + LORInAxialAndNoArcCorrSinogramCoordinates lor; + get_LOR(lor, bin); + //Parisa to Check + //std::cout<<"seg_ax_m_z1 = "< lor; + get_LOR(lor, bin); + const float delta_z = lor.z2() - lor.z1(); + if (fabs(delta_z)<0.0001F) + return 0; + const float R=get_ring_radius(bin.view_num()); + assert(R>=fabs(get_s(bin))); + return delta_z/(2*sqrt(square(R)-square(get_s(bin)))); +} + +float +ProjDataInfoBlocksOnCylindrical::get_sampling_in_m(const Bin& bin) const +{ + return get_axial_sampling(bin.segment_num()); +} + +/* + warning Theta is not uniform anymore, so sampling in t does not make sense anymore. + Sampling parameters remained unchanged to be consistent with cylindrical version. +*/ +float +ProjDataInfoBlocksOnCylindrical::get_sampling_in_t(const Bin& bin) const +{ + return get_axial_sampling(bin.segment_num())*get_costheta(bin); +} + +int +ProjDataInfoBlocksOnCylindrical:: +get_num_axial_poss_per_ring_inc(const int segment_num) const +{ + return + max_ring_diff[segment_num] != min_ring_diff[segment_num] ? + 2 : 1; +} + + +float +ProjDataInfoBlocksOnCylindrical::get_azimuthal_angle_sampling() const +{return azimuthal_angle_sampling;} + + +float +ProjDataInfoBlocksOnCylindrical::get_axial_sampling(int segment_num) const +{ + return ring_spacing/get_num_axial_poss_per_ring_inc(segment_num); +} + +float +ProjDataInfoBlocksOnCylindrical::get_average_ring_difference(int segment_num) const +{ + // KT 05/07/2001 use float division here. + // In any reasonable case, min+max_ring_diff will be even. + // But some day, an unreasonable case will walk in. + return (min_ring_diff[segment_num] + max_ring_diff[segment_num])/2.F; +} + +int +ProjDataInfoBlocksOnCylindrical::get_min_ring_difference(int segment_num) const +{ return min_ring_diff[segment_num]; } + +int +ProjDataInfoBlocksOnCylindrical::get_max_ring_difference(int segment_num) const +{ return max_ring_diff[segment_num]; } + +float +ProjDataInfoBlocksOnCylindrical::get_ring_radius() const +{ + if (this->ring_radius.get_min_index()!=0 || this->ring_radius.get_max_index()!=0) + { + // check if all elements are equal + for (VectorWithOffset::const_iterator iter=this->ring_radius.begin(); iter!= this->ring_radius.end(); ++iter) + { + if (*iter != *this->ring_radius.begin()) + error("get_ring_radius called for non-circular ring"); + } + } + return *this->ring_radius.begin(); +} + +void +ProjDataInfoBlocksOnCylindrical::set_ring_radii_for_all_views(const VectorWithOffset& new_ring_radius) +{ + if (new_ring_radius.get_min_index() != this->get_min_view_num() || + new_ring_radius.get_max_index() != this->get_max_view_num()) + { + error("error set_ring_radii_for_all_views: you need to use correct range of view numbers"); + } + + this->ring_radius = new_ring_radius; +} + +VectorWithOffset +ProjDataInfoBlocksOnCylindrical::get_ring_radii_for_all_views() const +{ + if (this->ring_radius.get_min_index()==0 && this->ring_radius.get_max_index()==0) + { + VectorWithOffset out(this->get_min_view_num(), this->get_max_view_num()); + out.fill(this->ring_radius[0]); + return out; + } + else + return this->ring_radius; +} + +float +ProjDataInfoBlocksOnCylindrical::get_ring_radius( const int view_num) const +{ + if (this->ring_radius.get_min_index()==0 && this->ring_radius.get_max_index()==0) + return ring_radius[0]; + else + return ring_radius[view_num]; +} + +float +ProjDataInfoBlocksOnCylindrical::get_ring_spacing() const +{ return ring_spacing;} + +int +ProjDataInfoBlocksOnCylindrical:: +get_view_mashing_factor() const +{ + // KT 10/05/2002 new assert + assert(get_scanner_ptr()->get_num_detectors_per_ring() > 0); + // KT 10/05/2002 moved assert here from constructor + assert(get_scanner_ptr()->get_num_detectors_per_ring() % (2*get_num_views()) == 0); + // KT 28/11/2001 do not pre-store anymore as set_num_views would invalidate it + return get_scanner_ptr()->get_num_detectors_per_ring() / (2*get_num_views()); +} + +Succeeded +ProjDataInfoBlocksOnCylindrical:: +get_segment_num_for_ring_difference(int& segment_num, const int ring_diff) const +{ + if (!sampling_corresponds_to_physical_rings) + return Succeeded::no; + + // check currently necessary as reduce_segment does not reduce the size of the ring_diff arrays + if (ring_diff > get_max_ring_difference(get_max_segment_num()) || + ring_diff < get_min_ring_difference(get_min_segment_num())) + return Succeeded::no; + + if (!ring_diff_arrays_computed) + initialise_ring_diff_arrays(); + + segment_num = ring_diff_to_segment_num[ring_diff]; + // warning: relies on initialise_ring_diff_arrays to set invalid ring_diff to a too large segment_num + if (segment_num <= get_max_segment_num()) + return Succeeded::yes; + else + return Succeeded::no; +} + + +Succeeded +ProjDataInfoBlocksOnCylindrical:: +get_segment_axial_pos_num_for_ring_pair(int& segment_num, + int& ax_pos_num, + const int ring1, + const int ring2) const +{ + assert(0<=ring1); + assert(ring1get_num_rings()); + assert(0<=ring2); + assert(ring2get_num_rings()); + + // KT 01/08/2002 swapped rings + if (get_segment_num_for_ring_difference(segment_num, ring2-ring1) == Succeeded::no) + return Succeeded::no; + + // see initialise_ring_diff_arrays() for some info + ax_pos_num = (ring1 + ring2 - ax_pos_num_offset[segment_num])* + get_num_axial_poss_per_ring_inc(segment_num)/2; + return Succeeded::yes; +} + +const ProjDataInfoBlocksOnCylindrical::RingNumPairs& +ProjDataInfoBlocksOnCylindrical:: +get_all_ring_pairs_for_segment_axial_pos_num(const int segment_num, + const int axial_pos_num) const +{ + if (!ring_diff_arrays_computed) + initialise_ring_diff_arrays(); + if (is_null_ptr(segment_axial_pos_to_ring_pair[segment_num][axial_pos_num])) + compute_segment_axial_pos_to_ring_pair(segment_num, axial_pos_num); + return *segment_axial_pos_to_ring_pair[segment_num][axial_pos_num]; +} + +unsigned +ProjDataInfoBlocksOnCylindrical:: +get_num_ring_pairs_for_segment_axial_pos_num(const int segment_num, + const int axial_pos_num) const +{ + return + static_cast( + this->get_all_ring_pairs_for_segment_axial_pos_num(segment_num,axial_pos_num).size()); +} + +END_NAMESPACE_STIR diff --git a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h new file mode 100644 index 000000000..1bdaa06aa --- /dev/null +++ b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h @@ -0,0 +1,313 @@ + +/* + +TODO copyright and License + +*/ + +/*! + \file + \ingroup projdata + + \brief Declaration of class stir::ProjDataInfoBlocksOnCylindricalNoArcCorr + + \author Parisa Khateri + +*/ +#ifndef __stir_ProjDataInfoBlocksOnCylindricalNoArcCorr_H__ +#define __stir_ProjDataInfoBlocksOnCylindricalNoArcCorr_H__ + + +#include "stir/ProjDataInfoBlocksOnCylindrical.h" +#include "stir/GeometryBlocksOnCylindrical.h" +#include "stir/DetectionPositionPair.h" +#include "stir/VectorWithOffset.h" +#include "stir/CartesianCoordinate3D.h" + +START_NAMESPACE_STIR + +class Succeeded; +/*! + \ingroup projdata + \brief Projection data info for data which are not arc-corrected. + + For this class, 'tangential_pos_num' actually indexes an angular coordinate + with a particular angular sampling (usually given by half the angle between + detectors). That is + \code + get_s(Bin(..., tang_pos_num)) == ring_radius * sin(tang_pos_num*angular_increment) + \endcode + + This class also contains some functions specific for (static) full-ring PET + scanners. In this case, it is assumed that for 'raw' data (i.e. no mashing) + sinogram space is 'interleaved': 2 adjacent LOR_angles are + merged to 1 'view', while the corresponding bins are + interleaved: + + \verbatim + before interleaving after interleaving + a00 a01 a02 ... view 0: a00 a10 a01 a11 ... + a10 a11 ... + a20 a21 a22 ... view 1: a20 a30 a21 a31 ... + a30 a31 ... + \endverbatim + This (standard) interleaving is done because for 'odd' LOR_angles there + is no LOR which goes through the origin. + + + \par Interchanging the 2 detectors + + When the ring difference = 0 (i.e. a 2D - or direct - sinogram), + interchanging the 2 detectors does not change the LOR. This is why + (in 2D) one gets away with a full sinogram size of + num_views * 2 * num_views, where the size of 'detector-space' is + twice as large. + However, in 3D, interchanging the detectors, also interchanges the + rings. One has 2 options: + - have 1 sinogram with twice as many views, together with the rings + as 'unordered pair' (i.e. ring_difference is always >0) + - have 2 sinograms of the same size as in 2D, together with the rings + as 'ordered pair' (i.e. ring_difference can be positive and negative). + In STIR, we use the second convention. + + \todo The detector specific functions possibly do not belong in this class. + One can easily imagine a case where the theta,phi,s,t coordinates are as + described, but there is no real correspondence with detectors (for instance, + a rotating system). Maybe they should be moved somewhere else? + */ +class ProjDataInfoBlocksOnCylindricalNoArcCorr : public ProjDataInfoBlocksOnCylindrical +{ +private: + typedef ProjDataInfoBlocksOnCylindrical base_type; +#ifdef SWIG + // SWIG needs this typedef to be public + public: +#endif + typedef ProjDataInfoBlocksOnCylindricalNoArcCorr self_type; + +public: + //! Default constructor (leaves object in ill-defined state) + ProjDataInfoBlocksOnCylindricalNoArcCorr(); + //! Constructor completely specifying all parameters + /*! \see ProjDataInfoCylindrical class documentation for info on parameters */ + ProjDataInfoBlocksOnCylindricalNoArcCorr(const shared_ptr scanner_ptr, + const float ring_radius, const float angular_increment, + const VectorWithOffset& num_axial_pos_per_segment, + const VectorWithOffset& min_ring_diff_v, + const VectorWithOffset& max_ring_diff_v, + const int num_views,const int num_tangential_poss); + + //! Constructor which gets \a ring_radius and \a angular_increment from the scanner + /*! \a angular_increment is determined as Pi divided by the number of detectors in a ring. + \todo only suitable for full-ring PET scanners*/ + ProjDataInfoBlocksOnCylindricalNoArcCorr(const shared_ptr scanner_ptr, + const VectorWithOffset& num_axial_pos_per_segment, + const VectorWithOffset& min_ring_diff_v, + const VectorWithOffset& max_ring_diff_v, + const int num_views,const int num_tangential_poss); + + ProjDataInfo* clone() const; + + bool operator==(const self_type&) const; + + //! Gets s coordinate in mm + /*! \warning + This does \c not take the 'interleaving' into account which is + customarily applied to raw PET data. + */ + inline virtual float get_s(const Bin&) const; + + //! Gets angular increment (in radians) + inline float get_angular_increment() const; + + virtual std::string parameter_info() const; + + //! \name Functions that convert between bins and detection positions + //@{ + //! This gets view_num and tang_pos_num for a particular detector pair + /*! This function makes only sense if the scanner is a full-ring scanner + with discrete detectors and there is no rotation or wobble. + + \arg view runs currently from 0 to num_views-1 + + \arg tang_pos_num is centred around 0, where 0 corresponds + to opposing detectors. The maximum range of tangential positions for any + scanner is -(num_detectors)/2&) const; + + + //! This routine gets the detector pair corresponding to a bin. + /*! + \see get_det_pair_for_view_tangential_pos_num() for + restrictions. In addition, this routine only works for span=1 data, + i.e. no axial compression. + \todo use member template for the coordT type to support continuous detectors. + \warning Will call error() if certain conditions are not met. + */ + inline void + get_det_pos_pair_for_bin(DetectionPositionPair<>&, + const Bin&) const; + + //! This routine returns the number of detector pairs that correspond to a bin + unsigned int + get_num_det_pos_pairs_for_bin(const Bin&) const; + + //! This routine fills a vector with all the detector pairs that correspond to a bin. + /*! + \see ProjDataInfoCylindrical::get_all_ring_pairs_for_segment_axial_pos_num + for restrictions. + \todo It might be possible to return some weight factors in case there is + no many-to-one correspondence between detection positions and bins + (for instance for continuous detectors, or rotating scanners, or + arc-corrected data). + */ + void + get_all_det_pos_pairs_for_bin(vector >&, + const Bin&) const; + + //! This gets Bin coordinates for a particular detector pair + /*! + \return Succeeded::yes when a corresponding segment is found + \see get_view_tangential_pos_num_for_det_num_pair() for restrictions + \obsolete + */ + inline Succeeded + get_bin_for_det_pair(Bin&, + const int det1_num, const int ring1_num, + const int det2_num, const int ring2_num) const; + + + //! This routine gets the detector pair corresponding to a bin. + /*! + \see get_det_pair_for_view_tangential_pos_num() for + restrictions. In addition, this routine only works for span=1 data, + i.e. no axial compression. + \obsolete + */ + inline void + get_det_pair_for_bin( + int& det1_num, int& ring1_num, + int& det2_num, int& ring2_num, + const Bin&) const; + + //@} + + virtual + Bin + get_bin(const LOR&) const; + + + //! \name set of obsolete functions to go between bins<->LORs (will disappear!) + //@{ + /*! \warning These function take a different convention for the axial coordinate + compare to the get_m(), get_LOR() etc. In the current function, the axial coordinate (z) + is zero in the first ring, while for get_m() etc it is zero in the centre of the scanner. + \obsolete + */ + Succeeded find_scanner_coordinates_given_cartesian_coordinates(int& det1, int& det2, int& ring1, int& ring2, + const CartesianCoordinate3D& c1, + const CartesianCoordinate3D& c2) const; + + virtual void find_cartesian_coordinates_of_detection(CartesianCoordinate3D& coord_1, + CartesianCoordinate3D& coord_2, + const Bin& bin) const; + + void find_cartesian_coordinates_given_scanner_coordinates (CartesianCoordinate3D& coord_1, + CartesianCoordinate3D& coord_2, + const int Ring_A,const int Ring_B, + const int det1, const int det2) const; + + void find_bin_given_cartesian_coordinates_of_detection(Bin& bin, + const CartesianCoordinate3D& coord_1, + const CartesianCoordinate3D& coord_2) const; + //@} + +private: + + float ring_radius; + float angular_increment; + + // used in get_view_tangential_pos_num_for_det_num_pair() + struct Det1Det2 { int det1_num; int det2_num; }; + mutable VectorWithOffset< VectorWithOffset > uncompressed_view_tangpos_to_det1det2; + mutable bool uncompressed_view_tangpos_to_det1det2_initialised; + //! build look-up table for get_view_tangential_pos_num_for_det_num_pair() + void initialise_uncompressed_view_tangpos_to_det1det2() const; + + // used in get_view_tangential_pos_num_for_det_num_pair() + // we prestore a lookup-table in terms for unmashed view/tangpos + struct ViewTangPosSwap { int view_num; int tang_pos_num; bool swap_detectors; }; + mutable VectorWithOffset< VectorWithOffset > det1det2_to_uncompressed_view_tangpos; + mutable bool det1det2_to_uncompressed_view_tangpos_initialised; + //! build look-up table for get_view_tangential_pos_num_for_det_num_pair() + void initialise_det1det2_to_uncompressed_view_tangpos() const; + + //! build look-up table unless already done before + inline void initialise_uncompressed_view_tangpos_to_det1det2_if_not_done_yet() const; + //! build look-up table unless already done before + inline void initialise_det1det2_to_uncompressed_view_tangpos_if_not_done_yet() const; + + virtual bool blindly_equals(const root_type * const) const; + + //! used to find scanner coordinates given cartesian coordinates and vice versa + shared_ptr crystal_map; + + }; + +END_NAMESPACE_STIR + +#include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl" + +#endif diff --git a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl new file mode 100644 index 000000000..7be9ac510 --- /dev/null +++ b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl @@ -0,0 +1,191 @@ + +/* + +TODO copyright and License + +*/ + +/*! + \file + \ingroup projdata + + \brief Implementation of inline functions of class + stir::ProjDataInfoBlocksOnCylindricalNoArcCorr + + \author Parisa Khateri + +*/ +#include "stir/Bin.h" +#include "stir/Succeeded.h" +#include "stir/LORCoordinates.h" +#include + +START_NAMESPACE_STIR + +void +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +initialise_uncompressed_view_tangpos_to_det1det2_if_not_done_yet() const +{ + // for efficiency reasons, use "Double-Checked-Locking(DCL) pattern" with OpenMP atomic operation + // OpenMP v3.1 or later required + // thanks to yohjp: http://stackoverflow.com/questions/27975737/how-to-handle-cached-data-structures-with-multi-threading-e-g-openmp +#if defined(STIR_OPENMP) && _OPENMP >=201012 + bool initialised; +#pragma omp atomic read + initialised = uncompressed_view_tangpos_to_det1det2_initialised; + + if (!initialised) +#endif + { +#if defined(STIR_OPENMP) +#pragma omp critical(PROJDATAINFOCYLINDRICALNOARCCORR_VIEWTANGPOS_TO_DETS) +#endif + { + if (!uncompressed_view_tangpos_to_det1det2_initialised) + initialise_uncompressed_view_tangpos_to_det1det2(); + } + } +} + +void +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +initialise_det1det2_to_uncompressed_view_tangpos_if_not_done_yet() const +{ + // as above +#if defined(STIR_OPENMP) && _OPENMP >=201012 + bool initialised; +#pragma omp atomic read + initialised = det1det2_to_uncompressed_view_tangpos_initialised; + + if (!initialised) +#endif + { +#if defined(STIR_OPENMP) +#pragma omp critical(PROJDATAINFOCYLINDRICALNOARCCORR_DETS_TO_VIEWTANGPOS) +#endif + { + if (!det1det2_to_uncompressed_view_tangpos_initialised) + initialise_det1det2_to_uncompressed_view_tangpos(); + } + } +} + +/*! warning In cylindrical s is found from bin: sin(beta) = sin(tang_pos*angular_increment) + In block it is calculated directly from corresponding lor +*/ +float +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +get_s(const Bin& bin) const +{ + LORInAxialAndNoArcCorrSinogramCoordinates lor; + get_LOR(lor, bin); + if (bin.view_num()==0 && lor.phi()>0.1) + return -1*ring_radius * sin(lor.beta()); + return ring_radius * sin(lor.beta()); +} + +float +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +get_angular_increment() const +{ + return angular_increment; +} + +void +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +get_det_num_pair_for_view_tangential_pos_num( + int& det1_num, + int& det2_num, + const int view_num, + const int tang_pos_num) const +{ + assert(get_view_mashing_factor() == 1); + this->initialise_uncompressed_view_tangpos_to_det1det2_if_not_done_yet(); + + det1_num = uncompressed_view_tangpos_to_det1det2[view_num][tang_pos_num].det1_num; + det2_num = uncompressed_view_tangpos_to_det1det2[view_num][tang_pos_num].det2_num; +} + +bool +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +get_view_tangential_pos_num_for_det_num_pair(int& view_num, + int& tang_pos_num, + const int det1_num, + const int det2_num) const +{ + assert(det1_num!=det2_num); + this->initialise_det1det2_to_uncompressed_view_tangpos_if_not_done_yet(); + + view_num = + det1det2_to_uncompressed_view_tangpos[det1_num][det2_num].view_num/get_view_mashing_factor(); + tang_pos_num = + det1det2_to_uncompressed_view_tangpos[det1_num][det2_num].tang_pos_num; + return + det1det2_to_uncompressed_view_tangpos[det1_num][det2_num].swap_detectors; +} + +Succeeded +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +get_bin_for_det_pair(Bin& bin, + const int det_num1, const int ring_num1, + const int det_num2, const int ring_num2) const +{ + if (get_view_tangential_pos_num_for_det_num_pair(bin.view_num(), bin.tangential_pos_num(), det_num1, det_num2)) + return get_segment_axial_pos_num_for_ring_pair(bin.segment_num(), bin.axial_pos_num(), ring_num1, ring_num2); + else + return get_segment_axial_pos_num_for_ring_pair(bin.segment_num(), bin.axial_pos_num(), ring_num2, ring_num1); +} + +Succeeded +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +get_bin_for_det_pos_pair(Bin& bin, + const DetectionPositionPair<>& dp) const +{ + return + get_bin_for_det_pair(bin, + dp.pos1().tangential_coord(), + dp.pos1().axial_coord(), + dp.pos2().tangential_coord(), + dp.pos2().axial_coord()); +} + +void +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +get_det_pair_for_bin( + int& det_num1, int& ring_num1, + int& det_num2, int& ring_num2, + const Bin& bin) const +{ + get_det_num_pair_for_view_tangential_pos_num(det_num1, det_num2, bin.view_num(), bin.tangential_pos_num()); + get_ring_pair_for_segment_axial_pos_num( ring_num1, ring_num2, bin.segment_num(), bin.axial_pos_num()); +} + +void +ProjDataInfoBlocksOnCylindricalNoArcCorr:: +get_det_pos_pair_for_bin( + DetectionPositionPair<>& dp, + const Bin& bin) const +{ + //lousy work around because types don't match TODO remove! +#if 1 + int t1=dp.pos1().tangential_coord(), + a1=dp.pos1().axial_coord(), + t2=dp.pos2().tangential_coord(), + a2=dp.pos2().axial_coord(); + get_det_pair_for_bin(t1, a1, t2, a2, bin); + dp.pos1().tangential_coord()=t1; + dp.pos1().axial_coord()=a1; + dp.pos2().tangential_coord()=t2; + dp.pos2().axial_coord()=a2; + +#else + + get_det_pair_for_bin(dp.pos1().tangential_coord(), + dp.pos1().axial_coord(), + dp.pos2().tangential_coord(), + dp.pos2().axial_coord(), + bin); +#endif +} + +END_NAMESPACE_STIR diff --git a/src/include/stir/Scanner.h b/src/include/stir/Scanner.h index 6256f509f..c622b1a4c 100644 --- a/src/include/stir/Scanner.h +++ b/src/include/stir/Scanner.h @@ -30,6 +30,8 @@ \author Sanida Mustafovic \author Charalampos Tsoumpas \author PARAPET project + \author Parisa Khateri + */ #ifndef __stir_buildblock_SCANNER_H__ #define __stir_buildblock_SCANNER_H__ @@ -134,7 +136,13 @@ class Scanner int num_transaxial_crystals_per_singles_unit_v, int num_detector_layers_v, float energy_resolution_v = -1.0f, - float reference_energy_v = -1.0f); + float reference_energy_v = -1.0f, + const std::string& scanner_orientation_v = "", + const std::string& scanner_geometry_v = "", + float axial_crystal_spacing_v = -1.0f, + float transaxial_crystal_spacing_v = -1.0f, + float axial_block_spacing_v = -1.0f, + float transaxial_block_spacing_v = -1.0f); //! constructor ( a single name) /*! size info is in mm @@ -153,7 +161,13 @@ class Scanner int num_transaxial_crystals_per_singles_unit_v, int num_detector_layers_v, float energy_resolution_v = -1.0f, - float reference_energy_v = -1.0f); + float reference_energy_v = -1.0f, + const std::string& scanner_orientation_v = "", + const std::string& scanner_geometry_v = "", + float axial_crystal_spacing_v = -1.0f, + float transaxial_crystal_spacing_v = -1.0f, + float axial_block_spacing_v = -1.0f, + float transaxial_block_spacing_v = -1.0f); @@ -262,6 +276,21 @@ class Scanner /* inline int get_num_layers_singles_units() const; */ inline int get_num_singles_units() const; + //! \name functions to get block geometry info + //@{ + //! get scanner orientation + inline std::string get_scanner_orientation() const; + //! get scanner geometry + inline std::string get_scanner_geometry() const; + //! get crystal spacing in axial direction + inline float get_axial_crystal_spacing() const; + //! get crystal spacing in transaxial direction + inline float get_transaxial_crystal_spacing() const; + //! get block spacing in axial direction + inline float get_axial_block_spacing() const; + //! get block spacing in transaxial direction + inline float get_transaxial_block_spacing() const; + //@} (end of get block geometry info) //@} (end of block/bucket info) @@ -318,6 +347,21 @@ class Scanner //! set number of transaxial crystals per singles unit inline void set_num_transaxial_crystals_per_singles_unit(const int & new_num); // TODO accomodate more complex geometries of singles units. + //@{ + //! name functions to set block geometry info + //! set scanner orientation + inline void set_scanner_orientation(const std::string& new_scanner_orientation); + //! set scanner geometry + inline void set_scanner_geometry(const std::string& new_scanner_geometry); + //! set crystal spacing in axial direction + inline void set_axial_crystal_spacing(const float & new_spacing); + //! set crystal spacing in transaxial direction + inline void set_transaxial_crystal_spacing(const float & new_spacing); + //! set block spacing in axial direction + inline void set_axial_block_spacing(const float & new_spacing); + //! set block spacing in transaxial direction + inline void set_transaxial_block_spacing(const float & new_spacing); + //@} (end of block geometry info) //@} (end of block/bucket info) //! set the energy resolution of the system @@ -327,7 +371,6 @@ class Scanner //! A negative value indicates, unknown || not set inline void set_reference_energy(const float new_num); //@} (end of set info) - //@} (end of set info) // Calculate a singles bin index from axial and transaxial singles bin coordinates. inline int get_singles_bin_index(int axial_index, int transaxial_index) const; @@ -382,6 +425,17 @@ class Scanner //! A negative value indicates, unknown. float reference_energy; + //! + //! \brief scanner info needed for block geometry + //! \author Parisa Khateri + //! A negative value indicates unknown. + std::string scanner_orientation; /*! scanner orientation */ + std::string scanner_geometry; /*! scanner geometry */ + float axial_crystal_spacing; /*! crystal pitch in axial direction in mm*/ + float transaxial_crystal_spacing; /*! crystal pitch in transaxial direction in mm*/ + float axial_block_spacing; /*! block pitch in axial direction in mm*/ + float transaxial_block_spacing; /*! block pitch in transaxial direction in mm*/ + // ! set all parameters, case where default_num_arccorrected_bins==max_num_non_arccorrected_bins void set_params(Type type_v, const std::list& list_of_names_v, @@ -398,7 +452,13 @@ class Scanner int num_transaxial_crystals_per_singles_unit_v, int num_detector_layers_v, float energy_resolution_v = -1.0f, - float reference_energy = -1.0f); + float reference_energy = -1.0f, + const std::string& scanner_orientation_v = "", + const std::string& scanner_geometry_v = "", + float axial_crystal_spacing_v = -1.0f, + float transaxial_crystal_spacing_v = -1.0f, + float axial_block_spacing_v = -1.0f, + float transaxial_block_spacing_v = -1.0f); // ! set all parameters void set_params(Type type_v, const std::list& list_of_names_v, @@ -416,7 +476,13 @@ class Scanner int num_transaxial_crystals_per_singles_unit_v, int num_detector_layers_v, float energy_resolution_v = -1.0f, - float reference_energy = -1.0f); + float reference_energy = -1.0f, + const std::string& scanner_orientation_v = "", + const std::string& scanner_geometry_v = "", + float axial_crystal_spacing_v = -1.0f, + float transaxial_crystal_spacing_v = -1.0f, + float axial_block_spacing_v = -1.0f, + float transaxial_block_spacing_v = -1.0f); }; diff --git a/src/include/stir/Scanner.inl b/src/include/stir/Scanner.inl index e5cadcd67..fde3c2078 100644 --- a/src/include/stir/Scanner.inl +++ b/src/include/stir/Scanner.inl @@ -28,6 +28,7 @@ \author Kris Thielemans \author Long Zhang (set*() functions) \author PARAPET project + \author Parisa Khateri */ @@ -229,6 +230,42 @@ Scanner::get_reference_energy() const return reference_energy; } +std::string +Scanner::get_scanner_orientation() const +{ + return scanner_orientation; +} + +std::string +Scanner::get_scanner_geometry() const +{ + return scanner_geometry; +} + +float +Scanner::get_axial_crystal_spacing() const +{ + return axial_crystal_spacing; +} + +float +Scanner::get_transaxial_crystal_spacing() const +{ + return transaxial_crystal_spacing; +} + +float +Scanner::get_transaxial_block_spacing() const +{ + return transaxial_block_spacing; +} + +float +Scanner::get_axial_block_spacing() const +{ + return axial_block_spacing; +} + //************************ set ******************************8 void Scanner::set_type(const Type & new_type) @@ -334,6 +371,36 @@ Scanner::set_reference_energy(const float new_num) reference_energy = new_num; } +void Scanner::set_scanner_orientation(const std::string& new_scanner_orientation) +{ + scanner_orientation = new_scanner_orientation; +} + +void Scanner::set_scanner_geometry(const std::string& new_scanner_geometry) +{ + scanner_geometry = new_scanner_geometry; +} + +void Scanner::set_axial_crystal_spacing(const float& new_spacing) +{ + axial_crystal_spacing = new_spacing; +} + +void Scanner::set_transaxial_crystal_spacing(const float& new_spacing) +{ + transaxial_crystal_spacing = new_spacing; +} + +void Scanner::set_transaxial_block_spacing(const float& new_spacing) +{ + transaxial_block_spacing = new_spacing; +} + +void Scanner::set_axial_block_spacing(const float& new_spacing) +{ + axial_block_spacing = new_spacing; +} + /******** Calculate singles bin index from detection position *********/ diff --git a/src/include/stir/listmode/CListRecordSAFIR.h b/src/include/stir/listmode/CListRecordSAFIR.h index fd360cb62..51838545d 100644 --- a/src/include/stir/listmode/CListRecordSAFIR.h +++ b/src/include/stir/listmode/CListRecordSAFIR.h @@ -27,6 +27,7 @@ \brief Declaration of class stir::CListEventSAFIR and stir::CListRecordSAFIR with supporting classes \author Jannis Fischer + \author Parisa Khateri */ #ifndef __stir_listmode_CListRecordSAFIR_H__ @@ -42,7 +43,7 @@ #include "boost/cstdint.hpp" #include "stir/listmode/DetectorCoordinateMapFromFile.h" - +#include "boost/make_shared.hpp" START_NAMESPACE_STIR @@ -76,6 +77,11 @@ class CListEventSAFIR : public CListEvent * Most scanners have listmode data that correspond to non arc-corrected data and * this check avoids a crash when an unsupported template is used as input. */ + + //! author Parisa Khateri + //! Override the default implementation + inline virtual void get_bin(Bin& bin, const ProjDataInfo& proj_data_info) const; + inline virtual bool is_valid_template(const ProjDataInfo&) const {return true;} //! Returns 0 if event is prompt and 1 if random/delayed diff --git a/src/include/stir/listmode/CListRecordSAFIR.inl b/src/include/stir/listmode/CListRecordSAFIR.inl index 3a9928024..2d0959c54 100644 --- a/src/include/stir/listmode/CListRecordSAFIR.inl +++ b/src/include/stir/listmode/CListRecordSAFIR.inl @@ -28,6 +28,10 @@ #include "stir/LORCoordinates.h" #include "stir/Succeeded.h" +#include "stir/ProjDataInfoCylindricalNoArcCorr.h" +#include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" +#include "stir/CartesianCoordinate3D.h" + START_NAMESPACE_STIR template @@ -47,6 +51,67 @@ CListEventSAFIR::get_LOR() const return lor; } +//! author Parisa Khateri +//! Overrides the default implementation to use get_detection_position() which should be faster. +template +void +CListEventSAFIR:: +get_bin(Bin& bin, const ProjDataInfo& proj_data_info) const +{ + DetectionPositionPair<> det_pos_pair; + static_cast(this)->get_data().get_detection_position_pair(det_pos_pair); + + //check aligned detectors + if (det_pos_pair.pos1().tangential_coord() == det_pos_pair.pos2().tangential_coord()) + { + /*std::cerr<<"WARNING: aligned detectors: det1="< c1 = map->get_detector_coordinate(det_pos_pair.pos1()); + stir::CartesianCoordinate3D c2 = map->get_detector_coordinate(det_pos_pair.pos2()); + int det1, det2, ring1, ring2; + + if(proj_data_info.get_scanner_ptr()->get_scanner_geometry() == "Cylindrical" + && bin.get_bin_value()!=-1) + { + //const ProjDataInfoCylindricalNoArcCorr& proj_data_info_cyl = + // dynamic_cast(proj_data_info); + + LORAs2Points lor; + lor.p1() = c1; + lor.p2() = c2; + bin = proj_data_info.get_bin(lor); + } + else if(proj_data_info.get_scanner_ptr()->get_scanner_geometry() == "BlocksOnCylindrical" + && bin.get_bin_value()!=-1) + { + const ProjDataInfoBlocksOnCylindricalNoArcCorr& proj_data_info_blk = + dynamic_cast(proj_data_info); + + if (proj_data_info_blk.find_scanner_coordinates_given_cartesian_coordinates(det1, det2, ring1, ring2, c1, c2) == Succeeded::no) + bin.set_bin_value(-1); + else + { + assert(!(ring1<0 || + ring1>=proj_data_info_blk.get_scanner_ptr()->get_num_rings() || + ring2<0 || + ring2>=proj_data_info_blk.get_scanner_ptr()->get_num_rings()) + ); + + if(proj_data_info_blk.get_bin_for_det_pair(bin, det1, ring1, det2, ring2)==Succeeded::yes) + bin.set_bin_value(1); + else + bin.set_bin_value(-1); + } + } +} + void CListEventDataSAFIR::get_detection_position_pair(DetectionPositionPair<>& det_pos_pair) { det_pos_pair.pos1().radial_coord() = layerA; diff --git a/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl b/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl index c6fb5b891..59a80f25c 100644 --- a/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl +++ b/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl @@ -8,6 +8,7 @@ \author Mustapha Sadki \author Kris Thielemans \author PARAPET project + \author Parisa Khateri */ /* @@ -33,7 +34,7 @@ */ #include "stir/ProjDataInfoCylindrical.h" #include "stir/recon_buildblock/SymmetryOperations_PET_CartesianGrid.h" - +#include "stir/ProjDataInfoBlocksOnCylindrical.h" START_NAMESPACE_STIR @@ -75,6 +76,10 @@ find_transform_z( const int segment_num, const int axial_pos_num) const { + int transform_z; + //cylindrical implementaion + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="Cylindrical") + { ProjDataInfoCylindrical* proj_data_info_cyl_ptr = static_cast(proj_data_info_ptr.get()); @@ -89,11 +94,34 @@ find_transform_z( + num_planes_per_scanner_ring*delta + 2*axial_pos_to_z_offset[segment_num]); // now use rounding to be safe - int transform_z = (int)floor(transform_z_float + 0.5); + transform_z = (int)floor(transform_z_float + 0.5); assert(fabs(transform_z-transform_z_float) < 10E-4); - return transform_z; } + } + //block implementaion + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="BlocksOnCylindrical") + { + ProjDataInfoBlocksOnCylindrical* proj_data_info_blk_ptr = + static_cast(proj_data_info_ptr.get()); + + const float delta = proj_data_info_blk_ptr->get_average_ring_difference(segment_num); + + + // Find symmetric value in Z by 'mirroring' it around the centre z of the LOR: + // Z+Q = 2*centre_of_LOR_in_image_coordinates == transform_z + { + // first compute it as floating point (although it has to be an int really) + const float transform_z_float = (2*num_planes_per_axial_pos[segment_num]*(axial_pos_num) + + num_planes_per_scanner_ring*delta + + 2*axial_pos_to_z_offset[segment_num]); + // now use rounding to be safe + transform_z = (int)floor(transform_z_float + 0.5); + assert(fabs(transform_z-transform_z_float) < 10E-4); + + } + } + return transform_z; } SymmetryOperation* @@ -103,6 +131,9 @@ find_sym_op_bin0( int view_num, int axial_pos_num) const { + //cylindrical implementaion + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="Cylindrical") + { // note: if do_symmetry_shift_z==true, then basic axial_pos_num will be 0 const int transform_z = find_transform_z(abs(segment_num), @@ -160,6 +191,43 @@ find_sym_op_bin0( return new SymmetryOperation_PET_CartesianGrid_z_shift(axial_pos_num, z_shift); } } + } + //block implementaion + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="BlocksOnCylindrical") + { + if (do_symmetry_90degrees_min_phi + || do_symmetry_swap_segment + || do_symmetry_swap_s + || do_symmetry_180degrees_min_phi) + { + warning("Currently, only symmetry along z is implemented for block geometry.\n"); + return new TrivialSymmetryOperation(); + } + + if (do_symmetry_shift_z) + { + Bin basic_bin(segment_num, view_num, axial_pos_num, 0); + find_basic_bin(basic_bin); + const int z_shift = + num_planes_per_axial_pos[segment_num] + *(axial_pos_num - basic_bin.axial_pos_num()); + + if (z_shift==0) + return new TrivialSymmetryOperation(); + else + return new SymmetryOperation_PET_CartesianGrid_z_shift(axial_pos_num, z_shift); + } + + if (!do_symmetry_90degrees_min_phi + && !do_symmetry_swap_segment + && !do_symmetry_swap_s + && !do_symmetry_180degrees_min_phi + && !do_symmetry_shift_z) + { + return new TrivialSymmetryOperation(); + } + + } } // from symmetries @@ -170,7 +238,10 @@ find_sym_op_general_bin( int segment_num, int view_num, int axial_pos_num) const -{ +{ + //cylindrical implementaion + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="Cylindrical") + { // note: if do_symmetry_shift_z==true, then basic axial_pos_num will be 0 const int transform_z = find_transform_z(abs(segment_num), @@ -297,7 +368,43 @@ find_sym_op_general_bin( return new SymmetryOperation_PET_CartesianGrid_z_shift(axial_pos_num, z_shift); } } - } + } + } + //block implementaion + //the implementation is as the above function for the current status of block symmetry. + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="BlocksOnCylindrical") + { + if (do_symmetry_90degrees_min_phi + || do_symmetry_swap_segment + || do_symmetry_swap_s + || do_symmetry_180degrees_min_phi) + { + warning("Currently, only symmetry along z is implemented for block geometry.\n"); + return new TrivialSymmetryOperation(); + } + if (do_symmetry_shift_z) + { + Bin basic_bin(segment_num, view_num, axial_pos_num, s); + find_basic_bin(basic_bin); + const int z_shift = + num_planes_per_axial_pos[segment_num] + *(axial_pos_num - basic_bin.axial_pos_num()); + + if (z_shift==0) + return new TrivialSymmetryOperation(); + else + return new SymmetryOperation_PET_CartesianGrid_z_shift(axial_pos_num, z_shift); + } + if (!do_symmetry_90degrees_min_phi + && !do_symmetry_swap_segment + && !do_symmetry_swap_s + && !do_symmetry_180degrees_min_phi + && !do_symmetry_shift_z) + { + return new TrivialSymmetryOperation(); + } + } + } @@ -341,9 +448,13 @@ bool DataSymmetriesForBins_PET_CartesianGrid:: find_basic_bin(int &segment_num, int &view_num, int &axial_pos_num, int &tangential_pos_num) const { + bool change=false; + //cylindrical implementaion + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="Cylindrical") + { ViewSegmentNumbers v_s(view_num, segment_num); - bool change=find_basic_view_segment_numbers(v_s); + change=find_basic_view_segment_numbers(v_s); view_num = v_s.view_num(); segment_num = v_s.segment_num(); @@ -352,6 +463,103 @@ find_basic_bin(int &segment_num, int &view_num, int &axial_pos_num, int &tangent if ( do_symmetry_shift_z && axial_pos_num != 0 ) { axial_pos_num = 0; change = true; } return change; + } + + //block implementaion + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="BlocksOnCylindrical") + { + /* + ax_pos_num = (ring1 + ring2 - ax_pos_num_offset[seg_num])*num_ax_pos_per_ring_inc(seg_num)/2 + ax_pos_num_offset = num_rings - 1 - (max_ax_pos_num + min_ax_pos_num)/num_ax_pos_per_ring_inc(seg_num) + Then + ax_pos_num = (ring1 + ring2 - num_rings + 1)*ax_pos_inc/2 - (max_ax_pos_num + min_ax_pos_num)/2 + and + max_ax_pos_num + min_ax_pos_num = (num_ax_pos_per_seg -1) + 0 = num_rings - seg_num - 1 + Then + ax_pos_num = (ring1 + ring2 - num_rings + 1)*ax_pos_inc/2 - (num_rings - seg_num - 1)/2 + if ax_pos_inc == 1 + ax_pos_num = (ring1 + ring2 - seg_num)/2 + */ + ProjDataInfoBlocksOnCylindrical* proj_data_info_blk_ptr = + static_cast(proj_data_info_ptr.get()); + if (do_symmetry_shift_z) + { + int ring1, ring2; + proj_data_info_blk_ptr->get_ring_pair_for_segment_axial_pos_num (ring1, ring2, segment_num, axial_pos_num); + //to check + //std::cout<<"before seg, ax, r1, r2 = "<get_scanner_ptr()->get_num_axial_crystals_per_block(); + int axial_blk_diff = ring2/num_axial_crys_per_block - ring1/num_axial_crys_per_block; + + if (axial_crys_diff >=0) + { // seg_num > =0 + if (axial_crys_diff%num_axial_crys_per_block == 0) + { // In this case, axial block difference can be only equal to axial_crys_diff/num_axial_crys_per_block. So we only have one type of related bins + // basic bin will be the first lor from the corresponding group + ring1= (ring1/num_axial_crys_per_block)*num_axial_crys_per_block; + ring2= ring1 + axial_crys_diff; + } + else + { /* In this case, axial block difference can be equal to axial_crys_diff/num_axial_crys_per_block + or one less. + So we can have two types of of related bins*/ + if (axial_blk_diff == axial_crys_diff/num_axial_crys_per_block) + {// basic bin will be the first lor from the corresponding group + ring1= (ring1/num_axial_crys_per_block)*num_axial_crys_per_block; + ring2= ring1 + axial_crys_diff; + } + else if (axial_blk_diff > axial_crys_diff/num_axial_crys_per_block) + {// basic bin will be the last lor from the corresponding group + ring1= (ring1/num_axial_crys_per_block)*num_axial_crys_per_block + num_axial_crys_per_block-1; + ring2= ring1 + axial_crys_diff; + } + } + } + else if (axial_crys_diff <0) + { // seg_num < 0 + if (abs(axial_crys_diff)%num_axial_crys_per_block == 0) + { // In this case, axial block difference can be only equal to axial_crys_diff/num_axial_crys_per_block. So we only have one type of related bins + // basic bin will be the first lor from the corresponding group + ring2= (ring2/num_axial_crys_per_block)*num_axial_crys_per_block; + ring1= ring2 - axial_crys_diff; + } + else + { /* In this case, axial block difference can be equal to axial_crys_diff/num_axial_crys_per_block + or one less. + So we can have two types of of related bins*/ + if (abs(axial_blk_diff) == abs(axial_crys_diff)/num_axial_crys_per_block) + {// basic bin will be the first lor from the corresponding group + ring2= (ring2/num_axial_crys_per_block)*num_axial_crys_per_block; + ring1= ring2 - axial_crys_diff; + } + else if (abs(axial_blk_diff) > abs(axial_crys_diff)/num_axial_crys_per_block) + {// basic bin will be the last lor from the corresponding group + ring2= (ring2/num_axial_crys_per_block)*num_axial_crys_per_block + num_axial_crys_per_block-1; + ring1= ring2 - axial_crys_diff; + } + } + } + + int segment_num_temp, axial_pos_num_temp; + proj_data_info_blk_ptr-> + get_segment_axial_pos_num_for_ring_pair(segment_num_temp, axial_pos_num_temp, ring1, ring2); + + //to check + //std::cout<<"after seg, ax, r1, r2 = "<get_scanner_ptr()->get_scanner_geometry()=="Cylindrical") + { + num = do_symmetry_180degrees_min_phi && (b.view_num() % (num_views/2)) != 0 ? 2 : 1; if (do_symmetry_90degrees_min_phi && (b.view_num() % (num_views/2)) != num_views/4) num *= 2; if (do_symmetry_swap_segment && b.segment_num() != 0) @@ -407,6 +619,34 @@ num_related_bins(const Bin& b) const if (do_symmetry_shift_z) num *= proj_data_info_ptr->get_num_axial_poss(b.segment_num()); + } + + //block implementaion + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="BlocksOnCylindrical") +{ + ProjDataInfoBlocksOnCylindrical* proj_data_info_blk_ptr = + static_cast(proj_data_info_ptr.get()); + if (do_symmetry_shift_z) + { + int ring1, ring2; + proj_data_info_blk_ptr->get_ring_pair_for_segment_axial_pos_num + (ring1, ring2, b.segment_num(), b.axial_pos_num()); + int axial_crys_diff = ring1 - ring2; + int num_axial_crys_per_block = proj_data_info_ptr->get_scanner_ptr()->get_num_axial_crystals_per_block(); + int axial_blk_diff = ring1/num_axial_crys_per_block - ring2/num_axial_crys_per_block; + + if (axial_blk_diff == axial_crys_diff/num_axial_crys_per_block) + { + num = num_axial_crys_per_block + - abs(axial_crys_diff)%num_axial_crys_per_block; + } + else if (axial_blk_diff > axial_crys_diff/num_axial_crys_per_block) + { + num = abs(axial_crys_diff)%num_axial_crys_per_block; + } + } + } + return num; } @@ -416,6 +656,9 @@ get_related_bins_factorised(std::vector& ax_tang_poss, const B const int min_axial_pos_num, const int max_axial_pos_num, const int min_tangential_pos_num, const int max_tangential_pos_num) const { + //cylindrical implementaion + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="Cylindrical") + { for (int axial_pos_num=do_symmetry_shift_z?min_axial_pos_num:b.axial_pos_num(); axial_pos_num <= (do_symmetry_shift_z?max_axial_pos_num:b.axial_pos_num()); ++axial_pos_num) @@ -427,7 +670,30 @@ get_related_bins_factorised(std::vector& ax_tang_poss, const B -b.tangential_pos_num() >= min_tangential_pos_num && -b.tangential_pos_num() <= max_tangential_pos_num) ax_tang_poss.push_back(AxTangPosNumbers(axial_pos_num, -b.tangential_pos_num())); + } } + + //block implementaion + //currently it only saves related bins according to z-symmetry + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="BlocksOnCylindrical") + { + for (int axial_pos_num=do_symmetry_shift_z?min_axial_pos_num:b.axial_pos_num(); + axial_pos_num <= (do_symmetry_shift_z?max_axial_pos_num:b.axial_pos_num()); + ++axial_pos_num) + { + if (b.tangential_pos_num() >= min_tangential_pos_num && + b.tangential_pos_num() <= max_tangential_pos_num) + { + Bin basic_bin(b); + find_basic_bin(basic_bin); + Bin bin_temp(b.segment_num(), b.view_num(), axial_pos_num, b.tangential_pos_num()); + find_basic_bin(bin_temp); + if (basic_bin == bin_temp) + ax_tang_poss.push_back(AxTangPosNumbers(axial_pos_num, b.tangential_pos_num())); + } + } + } + } void diff --git a/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx b/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx index 2fcfbf9fe..82f6a1ace 100644 --- a/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx +++ b/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx @@ -24,6 +24,8 @@ \author Kris Thielemans \author PARAPET project + \author Parisa Khateri + */ #include "stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.h" @@ -33,6 +35,8 @@ #include "stir/round.h" #include #include +#include "stir/ProjDataInfoBlocksOnCylindrical.h" + using std::min; using std::max; @@ -110,6 +114,74 @@ find_relation_between_coordinate_systems(int& num_planes_per_scanner_ring, } } +//overload for block geometry +static void +find_relation_between_coordinate_systems(int& num_planes_per_scanner_ring, + VectorWithOffset& num_planes_per_axial_pos, + VectorWithOffset& axial_pos_to_z_offset, + const ProjDataInfoBlocksOnCylindrical* proj_data_info_blk_ptr, + const DiscretisedDensityOnCartesianGrid<3,float> * cartesian_grid_info_ptr) + +{ + const int min_segment_num = proj_data_info_blk_ptr->get_min_segment_num(); + const int max_segment_num = proj_data_info_blk_ptr->get_max_segment_num(); + + num_planes_per_axial_pos = VectorWithOffset(min_segment_num, max_segment_num); + axial_pos_to_z_offset = VectorWithOffset(min_segment_num, max_segment_num); + + // TODO and WARNING: get_grid_spacing()[1] is z() + const float image_plane_spacing = cartesian_grid_info_ptr->get_grid_spacing()[1]; + + { + const float num_planes_per_scanner_ring_float = + proj_data_info_blk_ptr->get_ring_spacing() / image_plane_spacing; + + num_planes_per_scanner_ring = round(num_planes_per_scanner_ring_float); + //parisa: temporarily comment + /*if (fabs(num_planes_per_scanner_ring_float - num_planes_per_scanner_ring) > 1.E-2) + error("DataSymmetriesForBins_PET_CartesianGrid can currently only support z-grid spacing " + "equal to the ring spacing of the scanner divided by an integer. Sorry\n");*/ + } + + /* disabled as we support this now + if (fabs( cartesian_grid_info_ptr->get_origin().x()) > 1.E-2) + error("DataSymmetriesForBins_PET_CartesianGrid can currently only support x-origin = 0 " + "Sorry\n"); + if (fabs( cartesian_grid_info_ptr->get_origin().y()) > 1.E-2) + error("DataSymmetriesForBins_PET_CartesianGrid can currently only support y-origin = 0 " + "Sorry\n"); + */ + + for (int segment_num=min_segment_num; segment_num<=max_segment_num; ++segment_num) + { + { + const float + num_planes_per_axial_pos_float = + proj_data_info_blk_ptr->get_axial_sampling(segment_num)/image_plane_spacing; + + num_planes_per_axial_pos[segment_num] = round(num_planes_per_axial_pos_float); + //parisa: temporarily comment + /*if (fabs(num_planes_per_axial_pos_float - num_planes_per_axial_pos[segment_num]) > 1.E-5) + error("DataSymmetriesForBins_PET_CartesianGrid can currently only support z-grid spacing " + "equal to the axial sampling in the projection data divided by an integer. Sorry\n");*/ + + } + + const float delta = proj_data_info_blk_ptr->get_average_ring_difference(segment_num); + + // KT 20/06/2001 take origin.z() into account + axial_pos_to_z_offset[segment_num] = + (cartesian_grid_info_ptr->get_max_index() + cartesian_grid_info_ptr->get_min_index())/2.F + - cartesian_grid_info_ptr->get_origin().z()/image_plane_spacing + - + (num_planes_per_axial_pos[segment_num] + *(proj_data_info_blk_ptr->get_max_axial_pos_num(segment_num) + + proj_data_info_blk_ptr->get_min_axial_pos_num(segment_num)) + + num_planes_per_scanner_ring*delta)/2; + } +} + + /*! The DiscretisedDensity pointer has to point to an object of type DiscretisedDensityOnCartesianGrid (or a derived type). @@ -134,6 +206,9 @@ DataSymmetriesForBins_PET_CartesianGrid do_symmetry_swap_s(do_symmetry_swap_s_v), do_symmetry_shift_z(do_symmetry_shift_z) { + //Cylindrical implementation + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="Cylindrical") + { if(dynamic_cast(proj_data_info_ptr.get()) == NULL) error("DataSymmetriesForBins_PET_CartesianGrid constructed with wrong type of ProjDataInfo: %s\n" "(can only handle projection data corresponding to a cylinder)\n", @@ -215,6 +290,54 @@ DataSymmetriesForBins_PET_CartesianGrid axial_pos_to_z_offset, static_cast(proj_data_info_ptr.get()), cartesian_grid_info_ptr); + } + //Block implementation + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="BlocksOnCylindrical") + { + if (dynamic_cast(proj_data_info_ptr.get()) == NULL) + error("DataSymmetriesForBins_PET_CartesianGrid constructed with wrong type of ProjDataInfo: %s\n" + "(can only handle projection data corresponding to blocks on a cylinder)\n", + typeid(*proj_data_info_ptr).name()); + + const DiscretisedDensityOnCartesianGrid<3,float> * + cartesian_grid_info_ptr = + dynamic_cast *> + (image_info_ptr.get()); + + if (cartesian_grid_info_ptr == NULL) + error("DataSymmetriesForBins_PET_CartesianGrid constructed with wrong type of image info: %s\n", + typeid(*image_info_ptr).name()); + + // WARNING get_grid_spacing()[1] == z + //note: origin by default is (0,0,0) + const float z_origin_in_planes = + image_info_ptr->get_origin().z()/cartesian_grid_info_ptr->get_grid_spacing()[1]; + // z_origin_in_planes should be an integer + if (fabs(round(z_origin_in_planes) - z_origin_in_planes) > 1.E-3F) + error("DataSymmetriesForBins_PET_CartesianGrid: the shift in the " + "z-direction of the origin (which is %g) should be a multiple of the plane " + "separation (%g)\n", + image_info_ptr->get_origin().z(), cartesian_grid_info_ptr->get_grid_spacing()[1]); + + if (this->do_symmetry_90degrees_min_phi|| + this->do_symmetry_180degrees_min_phi|| + this->do_symmetry_swap_segment|| + this->do_symmetry_swap_s) + { + warning("Disabling all symmetries except for symmtery_z since they are not implemented in block geometry yet."); + this->do_symmetry_90degrees_min_phi = + this->do_symmetry_180degrees_min_phi = + this->do_symmetry_swap_segment = + this->do_symmetry_swap_s = false; + } + + find_relation_between_coordinate_systems( + num_planes_per_scanner_ring, + num_planes_per_axial_pos, + axial_pos_to_z_offset, + static_cast(proj_data_info_ptr.get()), + cartesian_grid_info_ptr); + } } diff --git a/src/recon_buildblock/ProjMatrixByBinUsingRayTracing.cxx b/src/recon_buildblock/ProjMatrixByBinUsingRayTracing.cxx index 0b6f56c53..2e21c5425 100644 --- a/src/recon_buildblock/ProjMatrixByBinUsingRayTracing.cxx +++ b/src/recon_buildblock/ProjMatrixByBinUsingRayTracing.cxx @@ -26,6 +26,7 @@ \author Mustapha Sadki \author Kris Thielemans \author PARAPET project + \author Parisa Khateri */ /* History @@ -58,6 +59,7 @@ #include #include #include +#include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" #ifndef STIR_NO_NAMESPACE using std::min; @@ -307,6 +309,8 @@ set_up( "Backprojecting with this matrix might have artefacts at views 0 and 90 degrees.\n"); if (use_actual_detector_boundaries) + { + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()== "Cylindrical") { const ProjDataInfoCylindricalNoArcCorr * proj_data_info_cyl_ptr = dynamic_cast(proj_data_info_ptr.get()); @@ -334,7 +338,38 @@ set_up( use_actual_detector_boundaries = false; } } + } + else + { + const ProjDataInfoBlocksOnCylindricalNoArcCorr * proj_data_info_blk_ptr = + dynamic_cast(proj_data_info_ptr.get()); + if (proj_data_info_blk_ptr== 0) + { + warning("ProjMatrixByBinUsingRayTracing: use_actual_detector_boundaries" + " is reset to false as the projection data should be non-arccorected.\n"); + use_actual_detector_boundaries = false; + } + else + { + bool nocompression = + proj_data_info_blk_ptr->get_view_mashing_factor()==1; + for (int segment_num=proj_data_info_blk_ptr->get_min_segment_num(); + nocompression && segment_num <= proj_data_info_blk_ptr->get_max_segment_num(); + ++segment_num) + nocompression= + proj_data_info_blk_ptr->get_min_ring_difference(segment_num) == + proj_data_info_blk_ptr->get_max_ring_difference(segment_num); + + if (!nocompression) + { + warning("ProjMatrixByBinUsingRayTracing: use_actual_detector_boundaries" + " is reset to false as the projection data as either mashed or uses axial compression\n"); + use_actual_detector_boundaries = false; + } + } + } + if (use_actual_detector_boundaries) warning("ProjMatrixByBinUsingRayTracing: use_actual_detector_boundaries==true\n"); @@ -563,6 +598,8 @@ calculate_proj_matrix_elems_for_one_bin( } else { + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()== "Cylindrical") + { // can be static_cast later on const ProjDataInfoCylindricalNoArcCorr& proj_data_info_noarccor = dynamic_cast(*proj_data_info_ptr); @@ -587,6 +624,16 @@ calculate_proj_matrix_elems_for_one_bin( const float old_s_in_mm=proj_data_info_ptr->get_s(bin); if (fabs(s_in_mm-old_s_in_mm)>proj_data_info_ptr->get_sampling_in_s(bin)*.0001) warning("tangential_pos_num %d old_s_in_mm %g new_s_in_mm %g\n",bin.tangential_pos_num(), old_s_in_mm, s_in_mm); + } + else + { + // can be static_cast later on + const ProjDataInfoBlocksOnCylindricalNoArcCorr& proj_data_info_noarccor = + dynamic_cast(*proj_data_info_ptr); + + phi = proj_data_info_noarccor.get_phi(bin); + s_in_mm = proj_data_info_noarccor.get_s(bin); + } } @@ -778,8 +825,11 @@ add_adjacent_z(ProjMatrixElemsForOneBin& lor, assert(z_of_first_voxel-.5<=right_edge_of_TOR); // first reserve enough memory for the whole vector // this speeds things up. - const int num_overlapping_voxels = - round(ceil(right_edge_of_TOR-z_of_first_voxel+.5)); + // !author Parisa Khateri: modify to take into account voxels before the first LOR in raytracing. + const int num_overlapping_voxels = z_of_first_voxel > 0.5? + round(ceil(right_edge_of_TOR-z_of_first_voxel+.5)) + 1: + round(ceil(right_edge_of_TOR-z_of_first_voxel+.5)); + lor.reserve(lor.size() * num_overlapping_voxels); // point to end of original LOR, i.e. first plane @@ -837,6 +887,47 @@ add_adjacent_z(ProjMatrixElemsForOneBin& lor, } } } // loop over z_index + + // !author Parisa Khateri: modify to take into account voxels before the first LOR in raytracing. + if (z_of_first_voxel > 0.5) + { + int z_index= -1; + const float overlap_of_voxel_with_TOR = z_of_first_voxel - .5F; + assert(num_overlapping_voxels>=z_index); + assert(overlap_of_voxel_with_TOR < 1.0001); + const int new_z = lor.begin()->coord1()+z_index; + if (overlap_of_voxel_with_TOR>.9999) // test if it is 1 + { + // just copy the value + std::size_t count = 0; // counter for elements in original LOR + for ( ProjMatrixElemsForOneBin::const_iterator element_ptr = lor.begin(); + count != org_size; //element_ptr != element_end; + ++element_ptr, ++count) + { + assert(lor.size()+1 <= lor.capacity()); // not really necessary now, but check on reserve() best for performance + assert(new_z == element_ptr->coord1()+z_index); + lor.push_back(ProjMatrixElemsForOneBin::value_type( + Coordinate3D(new_z, element_ptr->coord2(), element_ptr->coord3()), + element_ptr->get_value())); + } + } + else + { + // multiply the value with the overlap + std::size_t count = 0; // counter for elements in original LOR + for ( ProjMatrixElemsForOneBin::const_iterator element_ptr = lor.begin(); + count != org_size; //element_ptr != element_end; + ++element_ptr, ++count) + { + assert(lor.size()+1 <= lor.capacity()); + assert(new_z == element_ptr->coord1()+z_index); + lor.push_back(ProjMatrixElemsForOneBin::value_type( + Coordinate3D(new_z, element_ptr->coord2(), element_ptr->coord3()), + element_ptr->get_value()*overlap_of_voxel_with_TOR)); + } + } + + }//Parisa end // now check original z { From 4c62a5116afe01df4373743f4a013a6525240862 Mon Sep 17 00:00:00 2001 From: Michael Roethlisberger Date: Fri, 31 Aug 2018 18:03:22 +0200 Subject: [PATCH 02/81] Implementation of the generic geometry --- .../DualRingPrototype_crystal_map.txt | 2881 +++++++++++++++++ .../OSMAPOSL_QuadraticPrior.par | 105 + examples/SAFIR_genericScanner/README.txt | 38 + .../coincidencesLM.clm.safir | Bin 0 -> 12040 bytes .../listmode_input_SAFIR.par | 5 + .../SAFIR_genericScanner/lm_to_projdata.par | 41 + examples/SAFIR_genericScanner/muppet.hs | 54 + examples/SAFIR_genericScanner/muppet.s | 0 .../SAFIR_genericScanner/template_image.hv | 26 + .../SAFIR_genericScanner/template_image.v | Bin 0 -> 523900 bytes examples/SAFIR_genericScanner/test.sh | 26 + src/IO/InterfileHeader.cxx | 30 +- src/IO/interfile.cxx | 51 + src/buildblock/CMakeLists.txt | 2 + src/buildblock/ProjDataInfoGeneric.cxx | 514 +++ .../ProjDataInfoGenericNoArcCorr.cxx | 438 +++ src/buildblock/Scanner.cxx | 98 +- src/buildblock/VoxelsOnCartesianGrid.cxx | 23 + src/include/stir/IO/InterfileHeader.h | 1 + src/include/stir/ProjDataInfoGeneric.h | 302 ++ src/include/stir/ProjDataInfoGeneric.inl | 259 ++ .../stir/ProjDataInfoGenericNoArcCorr.h | 293 ++ .../stir/ProjDataInfoGenericNoArcCorr.inl | 143 + src/include/stir/Scanner.h | 36 + src/include/stir/Scanner.inl | 11 +- .../stir/listmode/CListRecordSAFIR.inl | 91 +- ...ataSymmetriesForBins_PET_CartesianGrid.inl | 136 +- ...ataSymmetriesForBins_PET_CartesianGrid.cxx | 89 + .../ProjMatrixByBinUsingRayTracing.cxx | 43 +- 29 files changed, 5690 insertions(+), 46 deletions(-) create mode 100644 examples/SAFIR_genericScanner/DualRingPrototype_crystal_map.txt create mode 100644 examples/SAFIR_genericScanner/OSMAPOSL_QuadraticPrior.par create mode 100644 examples/SAFIR_genericScanner/README.txt create mode 100644 examples/SAFIR_genericScanner/coincidencesLM.clm.safir create mode 100644 examples/SAFIR_genericScanner/listmode_input_SAFIR.par create mode 100644 examples/SAFIR_genericScanner/lm_to_projdata.par create mode 100644 examples/SAFIR_genericScanner/muppet.hs create mode 100644 examples/SAFIR_genericScanner/muppet.s create mode 100644 examples/SAFIR_genericScanner/template_image.hv create mode 100644 examples/SAFIR_genericScanner/template_image.v create mode 100755 examples/SAFIR_genericScanner/test.sh create mode 100644 src/buildblock/ProjDataInfoGeneric.cxx create mode 100644 src/buildblock/ProjDataInfoGenericNoArcCorr.cxx create mode 100644 src/include/stir/ProjDataInfoGeneric.h create mode 100644 src/include/stir/ProjDataInfoGeneric.inl create mode 100644 src/include/stir/ProjDataInfoGenericNoArcCorr.h create mode 100644 src/include/stir/ProjDataInfoGenericNoArcCorr.inl diff --git a/examples/SAFIR_genericScanner/DualRingPrototype_crystal_map.txt b/examples/SAFIR_genericScanner/DualRingPrototype_crystal_map.txt new file mode 100644 index 000000000..c48fdf54c --- /dev/null +++ b/examples/SAFIR_genericScanner/DualRingPrototype_crystal_map.txt @@ -0,0 +1,2881 @@ +#ring #detector #layer x y z +0 0 0 15.65 70.5 -16.75 +0 1 0 13.45 70.5 -16.75 +0 2 0 11.25 70.5 -16.75 +0 3 0 9.05 70.5 -16.75 +0 4 0 6.85 70.5 -16.75 +0 5 0 4.65 70.5 -16.75 +0 6 0 2.45 70.5 -16.75 +0 7 0 -0.25 70.5 -16.75 +0 8 0 -2.45 70.5 -16.75 +0 9 0 -4.65 70.5 -16.75 +0 10 0 -6.85 70.5 -16.75 +0 11 0 -9.05 70.5 -16.75 +0 12 0 -11.25 70.5 -16.75 +0 13 0 -13.45 70.5 -16.75 +0 14 0 -15.65 70.5 -16.75 +0 15 0 -21.6967 68.8798 -16.75 +0 16 0 -23.602 67.7798 -16.75 +0 17 0 -25.5072 66.6798 -16.75 +0 18 0 -27.4125 65.5798 -16.75 +0 19 0 -29.3177 64.4798 -16.75 +0 20 0 -31.223 63.3798 -16.75 +0 21 0 -33.1282 62.2798 -16.75 +0 22 0 -35.4665 60.9298 -16.75 +0 23 0 -37.3718 59.8298 -16.75 +0 24 0 -39.277 58.7298 -16.75 +0 25 0 -41.1823 57.6298 -16.75 +0 26 0 -43.0875 56.5298 -16.75 +0 27 0 -44.9928 55.4298 -16.75 +0 28 0 -46.898 54.3298 -16.75 +0 29 0 -48.8033 53.2298 -16.75 +0 30 0 -53.2298 48.8033 -16.75 +0 31 0 -54.3298 46.898 -16.75 +0 32 0 -55.4298 44.9928 -16.75 +0 33 0 -56.5298 43.0875 -16.75 +0 34 0 -57.6298 41.1823 -16.75 +0 35 0 -58.7298 39.277 -16.75 +0 36 0 -59.8298 37.3718 -16.75 +0 37 0 -61.1798 35.0335 -16.75 +0 38 0 -62.2798 33.1282 -16.75 +0 39 0 -63.3798 31.223 -16.75 +0 40 0 -64.4798 29.3177 -16.75 +0 41 0 -65.5798 27.4125 -16.75 +0 42 0 -66.6798 25.5072 -16.75 +0 43 0 -67.7798 23.602 -16.75 +0 44 0 -68.8798 21.6967 -16.75 +0 45 0 -70.5 15.65 -16.75 +0 46 0 -70.5 13.45 -16.75 +0 47 0 -70.5 11.25 -16.75 +0 48 0 -70.5 9.05 -16.75 +0 49 0 -70.5 6.85 -16.75 +0 50 0 -70.5 4.65 -16.75 +0 51 0 -70.5 2.45 -16.75 +0 52 0 -70.5 -0.25 -16.75 +0 53 0 -70.5 -2.45 -16.75 +0 54 0 -70.5 -4.65 -16.75 +0 55 0 -70.5 -6.85 -16.75 +0 56 0 -70.5 -9.05 -16.75 +0 57 0 -70.5 -11.25 -16.75 +0 58 0 -70.5 -13.45 -16.75 +0 59 0 -70.5 -15.65 -16.75 +0 60 0 -68.8798 -21.6967 -16.75 +0 61 0 -67.7798 -23.602 -16.75 +0 62 0 -66.6798 -25.5072 -16.75 +0 63 0 -65.5798 -27.4125 -16.75 +0 64 0 -64.4798 -29.3177 -16.75 +0 65 0 -63.3798 -31.223 -16.75 +0 66 0 -62.2798 -33.1282 -16.75 +0 67 0 -60.9298 -35.4665 -16.75 +0 68 0 -59.8298 -37.3718 -16.75 +0 69 0 -58.7298 -39.277 -16.75 +0 70 0 -57.6298 -41.1823 -16.75 +0 71 0 -56.5298 -43.0875 -16.75 +0 72 0 -55.4298 -44.9928 -16.75 +0 73 0 -54.3298 -46.898 -16.75 +0 74 0 -53.2298 -48.8033 -16.75 +0 75 0 -48.8033 -53.2298 -16.75 +0 76 0 -46.898 -54.3298 -16.75 +0 77 0 -44.9928 -55.4298 -16.75 +0 78 0 -43.0875 -56.5298 -16.75 +0 79 0 -41.1823 -57.6298 -16.75 +0 80 0 -39.277 -58.7298 -16.75 +0 81 0 -37.3718 -59.8298 -16.75 +0 82 0 -35.0335 -61.1798 -16.75 +0 83 0 -33.1282 -62.2798 -16.75 +0 84 0 -31.223 -63.3798 -16.75 +0 85 0 -29.3177 -64.4798 -16.75 +0 86 0 -27.4125 -65.5798 -16.75 +0 87 0 -25.5072 -66.6798 -16.75 +0 88 0 -23.602 -67.7798 -16.75 +0 89 0 -21.6967 -68.8798 -16.75 +0 90 0 -15.65 -70.5 -16.75 +0 91 0 -13.45 -70.5 -16.75 +0 92 0 -11.25 -70.5 -16.75 +0 93 0 -9.05 -70.5 -16.75 +0 94 0 -6.85 -70.5 -16.75 +0 95 0 -4.65 -70.5 -16.75 +0 96 0 -2.45 -70.5 -16.75 +0 97 0 0.25 -70.5 -16.75 +0 98 0 2.45 -70.5 -16.75 +0 99 0 4.65 -70.5 -16.75 +0 100 0 6.85 -70.5 -16.75 +0 101 0 9.05 -70.5 -16.75 +0 102 0 11.25 -70.5 -16.75 +0 103 0 13.45 -70.5 -16.75 +0 104 0 15.65 -70.5 -16.75 +0 105 0 21.6967 -68.8798 -16.75 +0 106 0 23.602 -67.7798 -16.75 +0 107 0 25.5072 -66.6798 -16.75 +0 108 0 27.4125 -65.5798 -16.75 +0 109 0 29.3177 -64.4798 -16.75 +0 110 0 31.223 -63.3798 -16.75 +0 111 0 33.1282 -62.2798 -16.75 +0 112 0 35.4665 -60.9298 -16.75 +0 113 0 37.3718 -59.8298 -16.75 +0 114 0 39.277 -58.7298 -16.75 +0 115 0 41.1823 -57.6298 -16.75 +0 116 0 43.0875 -56.5298 -16.75 +0 117 0 44.9928 -55.4298 -16.75 +0 118 0 46.898 -54.3298 -16.75 +0 119 0 48.8033 -53.2298 -16.75 +0 120 0 53.2298 -48.8033 -16.75 +0 121 0 54.3298 -46.898 -16.75 +0 122 0 55.4298 -44.9928 -16.75 +0 123 0 56.5298 -43.0875 -16.75 +0 124 0 57.6298 -41.1823 -16.75 +0 125 0 58.7298 -39.277 -16.75 +0 126 0 59.8298 -37.3718 -16.75 +0 127 0 61.1798 -35.0335 -16.75 +0 128 0 62.2798 -33.1282 -16.75 +0 129 0 63.3798 -31.223 -16.75 +0 130 0 64.4798 -29.3177 -16.75 +0 131 0 65.5798 -27.4125 -16.75 +0 132 0 66.6798 -25.5072 -16.75 +0 133 0 67.7798 -23.602 -16.75 +0 134 0 68.8798 -21.6967 -16.75 +0 135 0 70.5 -15.65 -16.75 +0 136 0 70.5 -13.45 -16.75 +0 137 0 70.5 -11.25 -16.75 +0 138 0 70.5 -9.05 -16.75 +0 139 0 70.5 -6.85 -16.75 +0 140 0 70.5 -4.65 -16.75 +0 141 0 70.5 -2.45 -16.75 +0 142 0 70.5 0.25 -16.75 +0 143 0 70.5 2.45 -16.75 +0 144 0 70.5 4.65 -16.75 +0 145 0 70.5 6.85 -16.75 +0 146 0 70.5 9.05 -16.75 +0 147 0 70.5 11.25 -16.75 +0 148 0 70.5 13.45 -16.75 +0 149 0 70.5 15.65 -16.75 +0 150 0 68.8798 21.6967 -16.75 +0 151 0 67.7798 23.602 -16.75 +0 152 0 66.6798 25.5072 -16.75 +0 153 0 65.5798 27.4125 -16.75 +0 154 0 64.4798 29.3177 -16.75 +0 155 0 63.3798 31.223 -16.75 +0 156 0 62.2798 33.1282 -16.75 +0 157 0 60.9298 35.4665 -16.75 +0 158 0 59.8298 37.3718 -16.75 +0 159 0 58.7298 39.277 -16.75 +0 160 0 57.6298 41.1823 -16.75 +0 161 0 56.5298 43.0875 -16.75 +0 162 0 55.4298 44.9928 -16.75 +0 163 0 54.3298 46.898 -16.75 +0 164 0 53.2298 48.8033 -16.75 +0 165 0 48.8033 53.2298 -16.75 +0 166 0 46.898 54.3298 -16.75 +0 167 0 44.9928 55.4298 -16.75 +0 168 0 43.0875 56.5298 -16.75 +0 169 0 41.1823 57.6298 -16.75 +0 170 0 39.277 58.7298 -16.75 +0 171 0 37.3718 59.8298 -16.75 +0 172 0 35.0335 61.1798 -16.75 +0 173 0 33.1282 62.2798 -16.75 +0 174 0 31.223 63.3798 -16.75 +0 175 0 29.3177 64.4798 -16.75 +0 176 0 27.4125 65.5798 -16.75 +0 177 0 25.5072 66.6798 -16.75 +0 178 0 23.602 67.7798 -16.75 +0 179 0 21.6967 68.8798 -16.75 +1 0 0 15.65 70.5 -14.55 +1 1 0 13.45 70.5 -14.55 +1 2 0 11.25 70.5 -14.55 +1 3 0 9.05 70.5 -14.55 +1 4 0 6.85 70.5 -14.55 +1 5 0 4.65 70.5 -14.55 +1 6 0 2.45 70.5 -14.55 +1 7 0 -0.25 70.5 -14.55 +1 8 0 -2.45 70.5 -14.55 +1 9 0 -4.65 70.5 -14.55 +1 10 0 -6.85 70.5 -14.55 +1 11 0 -9.05 70.5 -14.55 +1 12 0 -11.25 70.5 -14.55 +1 13 0 -13.45 70.5 -14.55 +1 14 0 -15.65 70.5 -14.55 +1 15 0 -21.6967 68.8798 -14.55 +1 16 0 -23.602 67.7798 -14.55 +1 17 0 -25.5072 66.6798 -14.55 +1 18 0 -27.4125 65.5798 -14.55 +1 19 0 -29.3177 64.4798 -14.55 +1 20 0 -31.223 63.3798 -14.55 +1 21 0 -33.1282 62.2798 -14.55 +1 22 0 -35.4665 60.9298 -14.55 +1 23 0 -37.3718 59.8298 -14.55 +1 24 0 -39.277 58.7298 -14.55 +1 25 0 -41.1823 57.6298 -14.55 +1 26 0 -43.0875 56.5298 -14.55 +1 27 0 -44.9928 55.4298 -14.55 +1 28 0 -46.898 54.3298 -14.55 +1 29 0 -48.8033 53.2298 -14.55 +1 30 0 -53.2298 48.8033 -14.55 +1 31 0 -54.3298 46.898 -14.55 +1 32 0 -55.4298 44.9928 -14.55 +1 33 0 -56.5298 43.0875 -14.55 +1 34 0 -57.6298 41.1823 -14.55 +1 35 0 -58.7298 39.277 -14.55 +1 36 0 -59.8298 37.3718 -14.55 +1 37 0 -61.1798 35.0335 -14.55 +1 38 0 -62.2798 33.1282 -14.55 +1 39 0 -63.3798 31.223 -14.55 +1 40 0 -64.4798 29.3177 -14.55 +1 41 0 -65.5798 27.4125 -14.55 +1 42 0 -66.6798 25.5072 -14.55 +1 43 0 -67.7798 23.602 -14.55 +1 44 0 -68.8798 21.6967 -14.55 +1 45 0 -70.5 15.65 -14.55 +1 46 0 -70.5 13.45 -14.55 +1 47 0 -70.5 11.25 -14.55 +1 48 0 -70.5 9.05 -14.55 +1 49 0 -70.5 6.85 -14.55 +1 50 0 -70.5 4.65 -14.55 +1 51 0 -70.5 2.45 -14.55 +1 52 0 -70.5 -0.25 -14.55 +1 53 0 -70.5 -2.45 -14.55 +1 54 0 -70.5 -4.65 -14.55 +1 55 0 -70.5 -6.85 -14.55 +1 56 0 -70.5 -9.05 -14.55 +1 57 0 -70.5 -11.25 -14.55 +1 58 0 -70.5 -13.45 -14.55 +1 59 0 -70.5 -15.65 -14.55 +1 60 0 -68.8798 -21.6967 -14.55 +1 61 0 -67.7798 -23.602 -14.55 +1 62 0 -66.6798 -25.5072 -14.55 +1 63 0 -65.5798 -27.4125 -14.55 +1 64 0 -64.4798 -29.3177 -14.55 +1 65 0 -63.3798 -31.223 -14.55 +1 66 0 -62.2798 -33.1282 -14.55 +1 67 0 -60.9298 -35.4665 -14.55 +1 68 0 -59.8298 -37.3718 -14.55 +1 69 0 -58.7298 -39.277 -14.55 +1 70 0 -57.6298 -41.1823 -14.55 +1 71 0 -56.5298 -43.0875 -14.55 +1 72 0 -55.4298 -44.9928 -14.55 +1 73 0 -54.3298 -46.898 -14.55 +1 74 0 -53.2298 -48.8033 -14.55 +1 75 0 -48.8033 -53.2298 -14.55 +1 76 0 -46.898 -54.3298 -14.55 +1 77 0 -44.9928 -55.4298 -14.55 +1 78 0 -43.0875 -56.5298 -14.55 +1 79 0 -41.1823 -57.6298 -14.55 +1 80 0 -39.277 -58.7298 -14.55 +1 81 0 -37.3718 -59.8298 -14.55 +1 82 0 -35.0335 -61.1798 -14.55 +1 83 0 -33.1282 -62.2798 -14.55 +1 84 0 -31.223 -63.3798 -14.55 +1 85 0 -29.3177 -64.4798 -14.55 +1 86 0 -27.4125 -65.5798 -14.55 +1 87 0 -25.5072 -66.6798 -14.55 +1 88 0 -23.602 -67.7798 -14.55 +1 89 0 -21.6967 -68.8798 -14.55 +1 90 0 -15.65 -70.5 -14.55 +1 91 0 -13.45 -70.5 -14.55 +1 92 0 -11.25 -70.5 -14.55 +1 93 0 -9.05 -70.5 -14.55 +1 94 0 -6.85 -70.5 -14.55 +1 95 0 -4.65 -70.5 -14.55 +1 96 0 -2.45 -70.5 -14.55 +1 97 0 0.25 -70.5 -14.55 +1 98 0 2.45 -70.5 -14.55 +1 99 0 4.65 -70.5 -14.55 +1 100 0 6.85 -70.5 -14.55 +1 101 0 9.05 -70.5 -14.55 +1 102 0 11.25 -70.5 -14.55 +1 103 0 13.45 -70.5 -14.55 +1 104 0 15.65 -70.5 -14.55 +1 105 0 21.6967 -68.8798 -14.55 +1 106 0 23.602 -67.7798 -14.55 +1 107 0 25.5072 -66.6798 -14.55 +1 108 0 27.4125 -65.5798 -14.55 +1 109 0 29.3177 -64.4798 -14.55 +1 110 0 31.223 -63.3798 -14.55 +1 111 0 33.1282 -62.2798 -14.55 +1 112 0 35.4665 -60.9298 -14.55 +1 113 0 37.3718 -59.8298 -14.55 +1 114 0 39.277 -58.7298 -14.55 +1 115 0 41.1823 -57.6298 -14.55 +1 116 0 43.0875 -56.5298 -14.55 +1 117 0 44.9928 -55.4298 -14.55 +1 118 0 46.898 -54.3298 -14.55 +1 119 0 48.8033 -53.2298 -14.55 +1 120 0 53.2298 -48.8033 -14.55 +1 121 0 54.3298 -46.898 -14.55 +1 122 0 55.4298 -44.9928 -14.55 +1 123 0 56.5298 -43.0875 -14.55 +1 124 0 57.6298 -41.1823 -14.55 +1 125 0 58.7298 -39.277 -14.55 +1 126 0 59.8298 -37.3718 -14.55 +1 127 0 61.1798 -35.0335 -14.55 +1 128 0 62.2798 -33.1282 -14.55 +1 129 0 63.3798 -31.223 -14.55 +1 130 0 64.4798 -29.3177 -14.55 +1 131 0 65.5798 -27.4125 -14.55 +1 132 0 66.6798 -25.5072 -14.55 +1 133 0 67.7798 -23.602 -14.55 +1 134 0 68.8798 -21.6967 -14.55 +1 135 0 70.5 -15.65 -14.55 +1 136 0 70.5 -13.45 -14.55 +1 137 0 70.5 -11.25 -14.55 +1 138 0 70.5 -9.05 -14.55 +1 139 0 70.5 -6.85 -14.55 +1 140 0 70.5 -4.65 -14.55 +1 141 0 70.5 -2.45 -14.55 +1 142 0 70.5 0.25 -14.55 +1 143 0 70.5 2.45 -14.55 +1 144 0 70.5 4.65 -14.55 +1 145 0 70.5 6.85 -14.55 +1 146 0 70.5 9.05 -14.55 +1 147 0 70.5 11.25 -14.55 +1 148 0 70.5 13.45 -14.55 +1 149 0 70.5 15.65 -14.55 +1 150 0 68.8798 21.6967 -14.55 +1 151 0 67.7798 23.602 -14.55 +1 152 0 66.6798 25.5072 -14.55 +1 153 0 65.5798 27.4125 -14.55 +1 154 0 64.4798 29.3177 -14.55 +1 155 0 63.3798 31.223 -14.55 +1 156 0 62.2798 33.1282 -14.55 +1 157 0 60.9298 35.4665 -14.55 +1 158 0 59.8298 37.3718 -14.55 +1 159 0 58.7298 39.277 -14.55 +1 160 0 57.6298 41.1823 -14.55 +1 161 0 56.5298 43.0875 -14.55 +1 162 0 55.4298 44.9928 -14.55 +1 163 0 54.3298 46.898 -14.55 +1 164 0 53.2298 48.8033 -14.55 +1 165 0 48.8033 53.2298 -14.55 +1 166 0 46.898 54.3298 -14.55 +1 167 0 44.9928 55.4298 -14.55 +1 168 0 43.0875 56.5298 -14.55 +1 169 0 41.1823 57.6298 -14.55 +1 170 0 39.277 58.7298 -14.55 +1 171 0 37.3718 59.8298 -14.55 +1 172 0 35.0335 61.1798 -14.55 +1 173 0 33.1282 62.2798 -14.55 +1 174 0 31.223 63.3798 -14.55 +1 175 0 29.3177 64.4798 -14.55 +1 176 0 27.4125 65.5798 -14.55 +1 177 0 25.5072 66.6798 -14.55 +1 178 0 23.602 67.7798 -14.55 +1 179 0 21.6967 68.8798 -14.55 +2 0 0 15.65 70.5 -12.35 +2 1 0 13.45 70.5 -12.35 +2 2 0 11.25 70.5 -12.35 +2 3 0 9.05 70.5 -12.35 +2 4 0 6.85 70.5 -12.35 +2 5 0 4.65 70.5 -12.35 +2 6 0 2.45 70.5 -12.35 +2 7 0 -0.25 70.5 -12.35 +2 8 0 -2.45 70.5 -12.35 +2 9 0 -4.65 70.5 -12.35 +2 10 0 -6.85 70.5 -12.35 +2 11 0 -9.05 70.5 -12.35 +2 12 0 -11.25 70.5 -12.35 +2 13 0 -13.45 70.5 -12.35 +2 14 0 -15.65 70.5 -12.35 +2 15 0 -21.6967 68.8798 -12.35 +2 16 0 -23.602 67.7798 -12.35 +2 17 0 -25.5072 66.6798 -12.35 +2 18 0 -27.4125 65.5798 -12.35 +2 19 0 -29.3177 64.4798 -12.35 +2 20 0 -31.223 63.3798 -12.35 +2 21 0 -33.1282 62.2798 -12.35 +2 22 0 -35.4665 60.9298 -12.35 +2 23 0 -37.3718 59.8298 -12.35 +2 24 0 -39.277 58.7298 -12.35 +2 25 0 -41.1823 57.6298 -12.35 +2 26 0 -43.0875 56.5298 -12.35 +2 27 0 -44.9928 55.4298 -12.35 +2 28 0 -46.898 54.3298 -12.35 +2 29 0 -48.8033 53.2298 -12.35 +2 30 0 -53.2298 48.8033 -12.35 +2 31 0 -54.3298 46.898 -12.35 +2 32 0 -55.4298 44.9928 -12.35 +2 33 0 -56.5298 43.0875 -12.35 +2 34 0 -57.6298 41.1823 -12.35 +2 35 0 -58.7298 39.277 -12.35 +2 36 0 -59.8298 37.3718 -12.35 +2 37 0 -61.1798 35.0335 -12.35 +2 38 0 -62.2798 33.1282 -12.35 +2 39 0 -63.3798 31.223 -12.35 +2 40 0 -64.4798 29.3177 -12.35 +2 41 0 -65.5798 27.4125 -12.35 +2 42 0 -66.6798 25.5072 -12.35 +2 43 0 -67.7798 23.602 -12.35 +2 44 0 -68.8798 21.6967 -12.35 +2 45 0 -70.5 15.65 -12.35 +2 46 0 -70.5 13.45 -12.35 +2 47 0 -70.5 11.25 -12.35 +2 48 0 -70.5 9.05 -12.35 +2 49 0 -70.5 6.85 -12.35 +2 50 0 -70.5 4.65 -12.35 +2 51 0 -70.5 2.45 -12.35 +2 52 0 -70.5 -0.25 -12.35 +2 53 0 -70.5 -2.45 -12.35 +2 54 0 -70.5 -4.65 -12.35 +2 55 0 -70.5 -6.85 -12.35 +2 56 0 -70.5 -9.05 -12.35 +2 57 0 -70.5 -11.25 -12.35 +2 58 0 -70.5 -13.45 -12.35 +2 59 0 -70.5 -15.65 -12.35 +2 60 0 -68.8798 -21.6967 -12.35 +2 61 0 -67.7798 -23.602 -12.35 +2 62 0 -66.6798 -25.5072 -12.35 +2 63 0 -65.5798 -27.4125 -12.35 +2 64 0 -64.4798 -29.3177 -12.35 +2 65 0 -63.3798 -31.223 -12.35 +2 66 0 -62.2798 -33.1282 -12.35 +2 67 0 -60.9298 -35.4665 -12.35 +2 68 0 -59.8298 -37.3718 -12.35 +2 69 0 -58.7298 -39.277 -12.35 +2 70 0 -57.6298 -41.1823 -12.35 +2 71 0 -56.5298 -43.0875 -12.35 +2 72 0 -55.4298 -44.9928 -12.35 +2 73 0 -54.3298 -46.898 -12.35 +2 74 0 -53.2298 -48.8033 -12.35 +2 75 0 -48.8033 -53.2298 -12.35 +2 76 0 -46.898 -54.3298 -12.35 +2 77 0 -44.9928 -55.4298 -12.35 +2 78 0 -43.0875 -56.5298 -12.35 +2 79 0 -41.1823 -57.6298 -12.35 +2 80 0 -39.277 -58.7298 -12.35 +2 81 0 -37.3718 -59.8298 -12.35 +2 82 0 -35.0335 -61.1798 -12.35 +2 83 0 -33.1282 -62.2798 -12.35 +2 84 0 -31.223 -63.3798 -12.35 +2 85 0 -29.3177 -64.4798 -12.35 +2 86 0 -27.4125 -65.5798 -12.35 +2 87 0 -25.5072 -66.6798 -12.35 +2 88 0 -23.602 -67.7798 -12.35 +2 89 0 -21.6967 -68.8798 -12.35 +2 90 0 -15.65 -70.5 -12.35 +2 91 0 -13.45 -70.5 -12.35 +2 92 0 -11.25 -70.5 -12.35 +2 93 0 -9.05 -70.5 -12.35 +2 94 0 -6.85 -70.5 -12.35 +2 95 0 -4.65 -70.5 -12.35 +2 96 0 -2.45 -70.5 -12.35 +2 97 0 0.25 -70.5 -12.35 +2 98 0 2.45 -70.5 -12.35 +2 99 0 4.65 -70.5 -12.35 +2 100 0 6.85 -70.5 -12.35 +2 101 0 9.05 -70.5 -12.35 +2 102 0 11.25 -70.5 -12.35 +2 103 0 13.45 -70.5 -12.35 +2 104 0 15.65 -70.5 -12.35 +2 105 0 21.6967 -68.8798 -12.35 +2 106 0 23.602 -67.7798 -12.35 +2 107 0 25.5072 -66.6798 -12.35 +2 108 0 27.4125 -65.5798 -12.35 +2 109 0 29.3177 -64.4798 -12.35 +2 110 0 31.223 -63.3798 -12.35 +2 111 0 33.1282 -62.2798 -12.35 +2 112 0 35.4665 -60.9298 -12.35 +2 113 0 37.3718 -59.8298 -12.35 +2 114 0 39.277 -58.7298 -12.35 +2 115 0 41.1823 -57.6298 -12.35 +2 116 0 43.0875 -56.5298 -12.35 +2 117 0 44.9928 -55.4298 -12.35 +2 118 0 46.898 -54.3298 -12.35 +2 119 0 48.8033 -53.2298 -12.35 +2 120 0 53.2298 -48.8033 -12.35 +2 121 0 54.3298 -46.898 -12.35 +2 122 0 55.4298 -44.9928 -12.35 +2 123 0 56.5298 -43.0875 -12.35 +2 124 0 57.6298 -41.1823 -12.35 +2 125 0 58.7298 -39.277 -12.35 +2 126 0 59.8298 -37.3718 -12.35 +2 127 0 61.1798 -35.0335 -12.35 +2 128 0 62.2798 -33.1282 -12.35 +2 129 0 63.3798 -31.223 -12.35 +2 130 0 64.4798 -29.3177 -12.35 +2 131 0 65.5798 -27.4125 -12.35 +2 132 0 66.6798 -25.5072 -12.35 +2 133 0 67.7798 -23.602 -12.35 +2 134 0 68.8798 -21.6967 -12.35 +2 135 0 70.5 -15.65 -12.35 +2 136 0 70.5 -13.45 -12.35 +2 137 0 70.5 -11.25 -12.35 +2 138 0 70.5 -9.05 -12.35 +2 139 0 70.5 -6.85 -12.35 +2 140 0 70.5 -4.65 -12.35 +2 141 0 70.5 -2.45 -12.35 +2 142 0 70.5 0.25 -12.35 +2 143 0 70.5 2.45 -12.35 +2 144 0 70.5 4.65 -12.35 +2 145 0 70.5 6.85 -12.35 +2 146 0 70.5 9.05 -12.35 +2 147 0 70.5 11.25 -12.35 +2 148 0 70.5 13.45 -12.35 +2 149 0 70.5 15.65 -12.35 +2 150 0 68.8798 21.6967 -12.35 +2 151 0 67.7798 23.602 -12.35 +2 152 0 66.6798 25.5072 -12.35 +2 153 0 65.5798 27.4125 -12.35 +2 154 0 64.4798 29.3177 -12.35 +2 155 0 63.3798 31.223 -12.35 +2 156 0 62.2798 33.1282 -12.35 +2 157 0 60.9298 35.4665 -12.35 +2 158 0 59.8298 37.3718 -12.35 +2 159 0 58.7298 39.277 -12.35 +2 160 0 57.6298 41.1823 -12.35 +2 161 0 56.5298 43.0875 -12.35 +2 162 0 55.4298 44.9928 -12.35 +2 163 0 54.3298 46.898 -12.35 +2 164 0 53.2298 48.8033 -12.35 +2 165 0 48.8033 53.2298 -12.35 +2 166 0 46.898 54.3298 -12.35 +2 167 0 44.9928 55.4298 -12.35 +2 168 0 43.0875 56.5298 -12.35 +2 169 0 41.1823 57.6298 -12.35 +2 170 0 39.277 58.7298 -12.35 +2 171 0 37.3718 59.8298 -12.35 +2 172 0 35.0335 61.1798 -12.35 +2 173 0 33.1282 62.2798 -12.35 +2 174 0 31.223 63.3798 -12.35 +2 175 0 29.3177 64.4798 -12.35 +2 176 0 27.4125 65.5798 -12.35 +2 177 0 25.5072 66.6798 -12.35 +2 178 0 23.602 67.7798 -12.35 +2 179 0 21.6967 68.8798 -12.35 +3 0 0 15.65 70.5 -10.15 +3 1 0 13.45 70.5 -10.15 +3 2 0 11.25 70.5 -10.15 +3 3 0 9.05 70.5 -10.15 +3 4 0 6.85 70.5 -10.15 +3 5 0 4.65 70.5 -10.15 +3 6 0 2.45 70.5 -10.15 +3 7 0 -0.25 70.5 -10.15 +3 8 0 -2.45 70.5 -10.15 +3 9 0 -4.65 70.5 -10.15 +3 10 0 -6.85 70.5 -10.15 +3 11 0 -9.05 70.5 -10.15 +3 12 0 -11.25 70.5 -10.15 +3 13 0 -13.45 70.5 -10.15 +3 14 0 -15.65 70.5 -10.15 +3 15 0 -21.6967 68.8798 -10.15 +3 16 0 -23.602 67.7798 -10.15 +3 17 0 -25.5072 66.6798 -10.15 +3 18 0 -27.4125 65.5798 -10.15 +3 19 0 -29.3177 64.4798 -10.15 +3 20 0 -31.223 63.3798 -10.15 +3 21 0 -33.1282 62.2798 -10.15 +3 22 0 -35.4665 60.9298 -10.15 +3 23 0 -37.3718 59.8298 -10.15 +3 24 0 -39.277 58.7298 -10.15 +3 25 0 -41.1823 57.6298 -10.15 +3 26 0 -43.0875 56.5298 -10.15 +3 27 0 -44.9928 55.4298 -10.15 +3 28 0 -46.898 54.3298 -10.15 +3 29 0 -48.8033 53.2298 -10.15 +3 30 0 -53.2298 48.8033 -10.15 +3 31 0 -54.3298 46.898 -10.15 +3 32 0 -55.4298 44.9928 -10.15 +3 33 0 -56.5298 43.0875 -10.15 +3 34 0 -57.6298 41.1823 -10.15 +3 35 0 -58.7298 39.277 -10.15 +3 36 0 -59.8298 37.3718 -10.15 +3 37 0 -61.1798 35.0335 -10.15 +3 38 0 -62.2798 33.1282 -10.15 +3 39 0 -63.3798 31.223 -10.15 +3 40 0 -64.4798 29.3177 -10.15 +3 41 0 -65.5798 27.4125 -10.15 +3 42 0 -66.6798 25.5072 -10.15 +3 43 0 -67.7798 23.602 -10.15 +3 44 0 -68.8798 21.6967 -10.15 +3 45 0 -70.5 15.65 -10.15 +3 46 0 -70.5 13.45 -10.15 +3 47 0 -70.5 11.25 -10.15 +3 48 0 -70.5 9.05 -10.15 +3 49 0 -70.5 6.85 -10.15 +3 50 0 -70.5 4.65 -10.15 +3 51 0 -70.5 2.45 -10.15 +3 52 0 -70.5 -0.25 -10.15 +3 53 0 -70.5 -2.45 -10.15 +3 54 0 -70.5 -4.65 -10.15 +3 55 0 -70.5 -6.85 -10.15 +3 56 0 -70.5 -9.05 -10.15 +3 57 0 -70.5 -11.25 -10.15 +3 58 0 -70.5 -13.45 -10.15 +3 59 0 -70.5 -15.65 -10.15 +3 60 0 -68.8798 -21.6967 -10.15 +3 61 0 -67.7798 -23.602 -10.15 +3 62 0 -66.6798 -25.5072 -10.15 +3 63 0 -65.5798 -27.4125 -10.15 +3 64 0 -64.4798 -29.3177 -10.15 +3 65 0 -63.3798 -31.223 -10.15 +3 66 0 -62.2798 -33.1282 -10.15 +3 67 0 -60.9298 -35.4665 -10.15 +3 68 0 -59.8298 -37.3718 -10.15 +3 69 0 -58.7298 -39.277 -10.15 +3 70 0 -57.6298 -41.1823 -10.15 +3 71 0 -56.5298 -43.0875 -10.15 +3 72 0 -55.4298 -44.9928 -10.15 +3 73 0 -54.3298 -46.898 -10.15 +3 74 0 -53.2298 -48.8033 -10.15 +3 75 0 -48.8033 -53.2298 -10.15 +3 76 0 -46.898 -54.3298 -10.15 +3 77 0 -44.9928 -55.4298 -10.15 +3 78 0 -43.0875 -56.5298 -10.15 +3 79 0 -41.1823 -57.6298 -10.15 +3 80 0 -39.277 -58.7298 -10.15 +3 81 0 -37.3718 -59.8298 -10.15 +3 82 0 -35.0335 -61.1798 -10.15 +3 83 0 -33.1282 -62.2798 -10.15 +3 84 0 -31.223 -63.3798 -10.15 +3 85 0 -29.3177 -64.4798 -10.15 +3 86 0 -27.4125 -65.5798 -10.15 +3 87 0 -25.5072 -66.6798 -10.15 +3 88 0 -23.602 -67.7798 -10.15 +3 89 0 -21.6967 -68.8798 -10.15 +3 90 0 -15.65 -70.5 -10.15 +3 91 0 -13.45 -70.5 -10.15 +3 92 0 -11.25 -70.5 -10.15 +3 93 0 -9.05 -70.5 -10.15 +3 94 0 -6.85 -70.5 -10.15 +3 95 0 -4.65 -70.5 -10.15 +3 96 0 -2.45 -70.5 -10.15 +3 97 0 0.25 -70.5 -10.15 +3 98 0 2.45 -70.5 -10.15 +3 99 0 4.65 -70.5 -10.15 +3 100 0 6.85 -70.5 -10.15 +3 101 0 9.05 -70.5 -10.15 +3 102 0 11.25 -70.5 -10.15 +3 103 0 13.45 -70.5 -10.15 +3 104 0 15.65 -70.5 -10.15 +3 105 0 21.6967 -68.8798 -10.15 +3 106 0 23.602 -67.7798 -10.15 +3 107 0 25.5072 -66.6798 -10.15 +3 108 0 27.4125 -65.5798 -10.15 +3 109 0 29.3177 -64.4798 -10.15 +3 110 0 31.223 -63.3798 -10.15 +3 111 0 33.1282 -62.2798 -10.15 +3 112 0 35.4665 -60.9298 -10.15 +3 113 0 37.3718 -59.8298 -10.15 +3 114 0 39.277 -58.7298 -10.15 +3 115 0 41.1823 -57.6298 -10.15 +3 116 0 43.0875 -56.5298 -10.15 +3 117 0 44.9928 -55.4298 -10.15 +3 118 0 46.898 -54.3298 -10.15 +3 119 0 48.8033 -53.2298 -10.15 +3 120 0 53.2298 -48.8033 -10.15 +3 121 0 54.3298 -46.898 -10.15 +3 122 0 55.4298 -44.9928 -10.15 +3 123 0 56.5298 -43.0875 -10.15 +3 124 0 57.6298 -41.1823 -10.15 +3 125 0 58.7298 -39.277 -10.15 +3 126 0 59.8298 -37.3718 -10.15 +3 127 0 61.1798 -35.0335 -10.15 +3 128 0 62.2798 -33.1282 -10.15 +3 129 0 63.3798 -31.223 -10.15 +3 130 0 64.4798 -29.3177 -10.15 +3 131 0 65.5798 -27.4125 -10.15 +3 132 0 66.6798 -25.5072 -10.15 +3 133 0 67.7798 -23.602 -10.15 +3 134 0 68.8798 -21.6967 -10.15 +3 135 0 70.5 -15.65 -10.15 +3 136 0 70.5 -13.45 -10.15 +3 137 0 70.5 -11.25 -10.15 +3 138 0 70.5 -9.05 -10.15 +3 139 0 70.5 -6.85 -10.15 +3 140 0 70.5 -4.65 -10.15 +3 141 0 70.5 -2.45 -10.15 +3 142 0 70.5 0.25 -10.15 +3 143 0 70.5 2.45 -10.15 +3 144 0 70.5 4.65 -10.15 +3 145 0 70.5 6.85 -10.15 +3 146 0 70.5 9.05 -10.15 +3 147 0 70.5 11.25 -10.15 +3 148 0 70.5 13.45 -10.15 +3 149 0 70.5 15.65 -10.15 +3 150 0 68.8798 21.6967 -10.15 +3 151 0 67.7798 23.602 -10.15 +3 152 0 66.6798 25.5072 -10.15 +3 153 0 65.5798 27.4125 -10.15 +3 154 0 64.4798 29.3177 -10.15 +3 155 0 63.3798 31.223 -10.15 +3 156 0 62.2798 33.1282 -10.15 +3 157 0 60.9298 35.4665 -10.15 +3 158 0 59.8298 37.3718 -10.15 +3 159 0 58.7298 39.277 -10.15 +3 160 0 57.6298 41.1823 -10.15 +3 161 0 56.5298 43.0875 -10.15 +3 162 0 55.4298 44.9928 -10.15 +3 163 0 54.3298 46.898 -10.15 +3 164 0 53.2298 48.8033 -10.15 +3 165 0 48.8033 53.2298 -10.15 +3 166 0 46.898 54.3298 -10.15 +3 167 0 44.9928 55.4298 -10.15 +3 168 0 43.0875 56.5298 -10.15 +3 169 0 41.1823 57.6298 -10.15 +3 170 0 39.277 58.7298 -10.15 +3 171 0 37.3718 59.8298 -10.15 +3 172 0 35.0335 61.1798 -10.15 +3 173 0 33.1282 62.2798 -10.15 +3 174 0 31.223 63.3798 -10.15 +3 175 0 29.3177 64.4798 -10.15 +3 176 0 27.4125 65.5798 -10.15 +3 177 0 25.5072 66.6798 -10.15 +3 178 0 23.602 67.7798 -10.15 +3 179 0 21.6967 68.8798 -10.15 +4 0 0 15.65 70.5 -7.95 +4 1 0 13.45 70.5 -7.95 +4 2 0 11.25 70.5 -7.95 +4 3 0 9.05 70.5 -7.95 +4 4 0 6.85 70.5 -7.95 +4 5 0 4.65 70.5 -7.95 +4 6 0 2.45 70.5 -7.95 +4 7 0 -0.25 70.5 -7.95 +4 8 0 -2.45 70.5 -7.95 +4 9 0 -4.65 70.5 -7.95 +4 10 0 -6.85 70.5 -7.95 +4 11 0 -9.05 70.5 -7.95 +4 12 0 -11.25 70.5 -7.95 +4 13 0 -13.45 70.5 -7.95 +4 14 0 -15.65 70.5 -7.95 +4 15 0 -21.6967 68.8798 -7.95 +4 16 0 -23.602 67.7798 -7.95 +4 17 0 -25.5072 66.6798 -7.95 +4 18 0 -27.4125 65.5798 -7.95 +4 19 0 -29.3177 64.4798 -7.95 +4 20 0 -31.223 63.3798 -7.95 +4 21 0 -33.1282 62.2798 -7.95 +4 22 0 -35.4665 60.9298 -7.95 +4 23 0 -37.3718 59.8298 -7.95 +4 24 0 -39.277 58.7298 -7.95 +4 25 0 -41.1823 57.6298 -7.95 +4 26 0 -43.0875 56.5298 -7.95 +4 27 0 -44.9928 55.4298 -7.95 +4 28 0 -46.898 54.3298 -7.95 +4 29 0 -48.8033 53.2298 -7.95 +4 30 0 -53.2298 48.8033 -7.95 +4 31 0 -54.3298 46.898 -7.95 +4 32 0 -55.4298 44.9928 -7.95 +4 33 0 -56.5298 43.0875 -7.95 +4 34 0 -57.6298 41.1823 -7.95 +4 35 0 -58.7298 39.277 -7.95 +4 36 0 -59.8298 37.3718 -7.95 +4 37 0 -61.1798 35.0335 -7.95 +4 38 0 -62.2798 33.1282 -7.95 +4 39 0 -63.3798 31.223 -7.95 +4 40 0 -64.4798 29.3177 -7.95 +4 41 0 -65.5798 27.4125 -7.95 +4 42 0 -66.6798 25.5072 -7.95 +4 43 0 -67.7798 23.602 -7.95 +4 44 0 -68.8798 21.6967 -7.95 +4 45 0 -70.5 15.65 -7.95 +4 46 0 -70.5 13.45 -7.95 +4 47 0 -70.5 11.25 -7.95 +4 48 0 -70.5 9.05 -7.95 +4 49 0 -70.5 6.85 -7.95 +4 50 0 -70.5 4.65 -7.95 +4 51 0 -70.5 2.45 -7.95 +4 52 0 -70.5 -0.25 -7.95 +4 53 0 -70.5 -2.45 -7.95 +4 54 0 -70.5 -4.65 -7.95 +4 55 0 -70.5 -6.85 -7.95 +4 56 0 -70.5 -9.05 -7.95 +4 57 0 -70.5 -11.25 -7.95 +4 58 0 -70.5 -13.45 -7.95 +4 59 0 -70.5 -15.65 -7.95 +4 60 0 -68.8798 -21.6967 -7.95 +4 61 0 -67.7798 -23.602 -7.95 +4 62 0 -66.6798 -25.5072 -7.95 +4 63 0 -65.5798 -27.4125 -7.95 +4 64 0 -64.4798 -29.3177 -7.95 +4 65 0 -63.3798 -31.223 -7.95 +4 66 0 -62.2798 -33.1282 -7.95 +4 67 0 -60.9298 -35.4665 -7.95 +4 68 0 -59.8298 -37.3718 -7.95 +4 69 0 -58.7298 -39.277 -7.95 +4 70 0 -57.6298 -41.1823 -7.95 +4 71 0 -56.5298 -43.0875 -7.95 +4 72 0 -55.4298 -44.9928 -7.95 +4 73 0 -54.3298 -46.898 -7.95 +4 74 0 -53.2298 -48.8033 -7.95 +4 75 0 -48.8033 -53.2298 -7.95 +4 76 0 -46.898 -54.3298 -7.95 +4 77 0 -44.9928 -55.4298 -7.95 +4 78 0 -43.0875 -56.5298 -7.95 +4 79 0 -41.1823 -57.6298 -7.95 +4 80 0 -39.277 -58.7298 -7.95 +4 81 0 -37.3718 -59.8298 -7.95 +4 82 0 -35.0335 -61.1798 -7.95 +4 83 0 -33.1282 -62.2798 -7.95 +4 84 0 -31.223 -63.3798 -7.95 +4 85 0 -29.3177 -64.4798 -7.95 +4 86 0 -27.4125 -65.5798 -7.95 +4 87 0 -25.5072 -66.6798 -7.95 +4 88 0 -23.602 -67.7798 -7.95 +4 89 0 -21.6967 -68.8798 -7.95 +4 90 0 -15.65 -70.5 -7.95 +4 91 0 -13.45 -70.5 -7.95 +4 92 0 -11.25 -70.5 -7.95 +4 93 0 -9.05 -70.5 -7.95 +4 94 0 -6.85 -70.5 -7.95 +4 95 0 -4.65 -70.5 -7.95 +4 96 0 -2.45 -70.5 -7.95 +4 97 0 0.25 -70.5 -7.95 +4 98 0 2.45 -70.5 -7.95 +4 99 0 4.65 -70.5 -7.95 +4 100 0 6.85 -70.5 -7.95 +4 101 0 9.05 -70.5 -7.95 +4 102 0 11.25 -70.5 -7.95 +4 103 0 13.45 -70.5 -7.95 +4 104 0 15.65 -70.5 -7.95 +4 105 0 21.6967 -68.8798 -7.95 +4 106 0 23.602 -67.7798 -7.95 +4 107 0 25.5072 -66.6798 -7.95 +4 108 0 27.4125 -65.5798 -7.95 +4 109 0 29.3177 -64.4798 -7.95 +4 110 0 31.223 -63.3798 -7.95 +4 111 0 33.1282 -62.2798 -7.95 +4 112 0 35.4665 -60.9298 -7.95 +4 113 0 37.3718 -59.8298 -7.95 +4 114 0 39.277 -58.7298 -7.95 +4 115 0 41.1823 -57.6298 -7.95 +4 116 0 43.0875 -56.5298 -7.95 +4 117 0 44.9928 -55.4298 -7.95 +4 118 0 46.898 -54.3298 -7.95 +4 119 0 48.8033 -53.2298 -7.95 +4 120 0 53.2298 -48.8033 -7.95 +4 121 0 54.3298 -46.898 -7.95 +4 122 0 55.4298 -44.9928 -7.95 +4 123 0 56.5298 -43.0875 -7.95 +4 124 0 57.6298 -41.1823 -7.95 +4 125 0 58.7298 -39.277 -7.95 +4 126 0 59.8298 -37.3718 -7.95 +4 127 0 61.1798 -35.0335 -7.95 +4 128 0 62.2798 -33.1282 -7.95 +4 129 0 63.3798 -31.223 -7.95 +4 130 0 64.4798 -29.3177 -7.95 +4 131 0 65.5798 -27.4125 -7.95 +4 132 0 66.6798 -25.5072 -7.95 +4 133 0 67.7798 -23.602 -7.95 +4 134 0 68.8798 -21.6967 -7.95 +4 135 0 70.5 -15.65 -7.95 +4 136 0 70.5 -13.45 -7.95 +4 137 0 70.5 -11.25 -7.95 +4 138 0 70.5 -9.05 -7.95 +4 139 0 70.5 -6.85 -7.95 +4 140 0 70.5 -4.65 -7.95 +4 141 0 70.5 -2.45 -7.95 +4 142 0 70.5 0.25 -7.95 +4 143 0 70.5 2.45 -7.95 +4 144 0 70.5 4.65 -7.95 +4 145 0 70.5 6.85 -7.95 +4 146 0 70.5 9.05 -7.95 +4 147 0 70.5 11.25 -7.95 +4 148 0 70.5 13.45 -7.95 +4 149 0 70.5 15.65 -7.95 +4 150 0 68.8798 21.6967 -7.95 +4 151 0 67.7798 23.602 -7.95 +4 152 0 66.6798 25.5072 -7.95 +4 153 0 65.5798 27.4125 -7.95 +4 154 0 64.4798 29.3177 -7.95 +4 155 0 63.3798 31.223 -7.95 +4 156 0 62.2798 33.1282 -7.95 +4 157 0 60.9298 35.4665 -7.95 +4 158 0 59.8298 37.3718 -7.95 +4 159 0 58.7298 39.277 -7.95 +4 160 0 57.6298 41.1823 -7.95 +4 161 0 56.5298 43.0875 -7.95 +4 162 0 55.4298 44.9928 -7.95 +4 163 0 54.3298 46.898 -7.95 +4 164 0 53.2298 48.8033 -7.95 +4 165 0 48.8033 53.2298 -7.95 +4 166 0 46.898 54.3298 -7.95 +4 167 0 44.9928 55.4298 -7.95 +4 168 0 43.0875 56.5298 -7.95 +4 169 0 41.1823 57.6298 -7.95 +4 170 0 39.277 58.7298 -7.95 +4 171 0 37.3718 59.8298 -7.95 +4 172 0 35.0335 61.1798 -7.95 +4 173 0 33.1282 62.2798 -7.95 +4 174 0 31.223 63.3798 -7.95 +4 175 0 29.3177 64.4798 -7.95 +4 176 0 27.4125 65.5798 -7.95 +4 177 0 25.5072 66.6798 -7.95 +4 178 0 23.602 67.7798 -7.95 +4 179 0 21.6967 68.8798 -7.95 +5 0 0 15.65 70.5 -5.75 +5 1 0 13.45 70.5 -5.75 +5 2 0 11.25 70.5 -5.75 +5 3 0 9.05 70.5 -5.75 +5 4 0 6.85 70.5 -5.75 +5 5 0 4.65 70.5 -5.75 +5 6 0 2.45 70.5 -5.75 +5 7 0 -0.25 70.5 -5.75 +5 8 0 -2.45 70.5 -5.75 +5 9 0 -4.65 70.5 -5.75 +5 10 0 -6.85 70.5 -5.75 +5 11 0 -9.05 70.5 -5.75 +5 12 0 -11.25 70.5 -5.75 +5 13 0 -13.45 70.5 -5.75 +5 14 0 -15.65 70.5 -5.75 +5 15 0 -21.6967 68.8798 -5.75 +5 16 0 -23.602 67.7798 -5.75 +5 17 0 -25.5072 66.6798 -5.75 +5 18 0 -27.4125 65.5798 -5.75 +5 19 0 -29.3177 64.4798 -5.75 +5 20 0 -31.223 63.3798 -5.75 +5 21 0 -33.1282 62.2798 -5.75 +5 22 0 -35.4665 60.9298 -5.75 +5 23 0 -37.3718 59.8298 -5.75 +5 24 0 -39.277 58.7298 -5.75 +5 25 0 -41.1823 57.6298 -5.75 +5 26 0 -43.0875 56.5298 -5.75 +5 27 0 -44.9928 55.4298 -5.75 +5 28 0 -46.898 54.3298 -5.75 +5 29 0 -48.8033 53.2298 -5.75 +5 30 0 -53.2298 48.8033 -5.75 +5 31 0 -54.3298 46.898 -5.75 +5 32 0 -55.4298 44.9928 -5.75 +5 33 0 -56.5298 43.0875 -5.75 +5 34 0 -57.6298 41.1823 -5.75 +5 35 0 -58.7298 39.277 -5.75 +5 36 0 -59.8298 37.3718 -5.75 +5 37 0 -61.1798 35.0335 -5.75 +5 38 0 -62.2798 33.1282 -5.75 +5 39 0 -63.3798 31.223 -5.75 +5 40 0 -64.4798 29.3177 -5.75 +5 41 0 -65.5798 27.4125 -5.75 +5 42 0 -66.6798 25.5072 -5.75 +5 43 0 -67.7798 23.602 -5.75 +5 44 0 -68.8798 21.6967 -5.75 +5 45 0 -70.5 15.65 -5.75 +5 46 0 -70.5 13.45 -5.75 +5 47 0 -70.5 11.25 -5.75 +5 48 0 -70.5 9.05 -5.75 +5 49 0 -70.5 6.85 -5.75 +5 50 0 -70.5 4.65 -5.75 +5 51 0 -70.5 2.45 -5.75 +5 52 0 -70.5 -0.25 -5.75 +5 53 0 -70.5 -2.45 -5.75 +5 54 0 -70.5 -4.65 -5.75 +5 55 0 -70.5 -6.85 -5.75 +5 56 0 -70.5 -9.05 -5.75 +5 57 0 -70.5 -11.25 -5.75 +5 58 0 -70.5 -13.45 -5.75 +5 59 0 -70.5 -15.65 -5.75 +5 60 0 -68.8798 -21.6967 -5.75 +5 61 0 -67.7798 -23.602 -5.75 +5 62 0 -66.6798 -25.5072 -5.75 +5 63 0 -65.5798 -27.4125 -5.75 +5 64 0 -64.4798 -29.3177 -5.75 +5 65 0 -63.3798 -31.223 -5.75 +5 66 0 -62.2798 -33.1282 -5.75 +5 67 0 -60.9298 -35.4665 -5.75 +5 68 0 -59.8298 -37.3718 -5.75 +5 69 0 -58.7298 -39.277 -5.75 +5 70 0 -57.6298 -41.1823 -5.75 +5 71 0 -56.5298 -43.0875 -5.75 +5 72 0 -55.4298 -44.9928 -5.75 +5 73 0 -54.3298 -46.898 -5.75 +5 74 0 -53.2298 -48.8033 -5.75 +5 75 0 -48.8033 -53.2298 -5.75 +5 76 0 -46.898 -54.3298 -5.75 +5 77 0 -44.9928 -55.4298 -5.75 +5 78 0 -43.0875 -56.5298 -5.75 +5 79 0 -41.1823 -57.6298 -5.75 +5 80 0 -39.277 -58.7298 -5.75 +5 81 0 -37.3718 -59.8298 -5.75 +5 82 0 -35.0335 -61.1798 -5.75 +5 83 0 -33.1282 -62.2798 -5.75 +5 84 0 -31.223 -63.3798 -5.75 +5 85 0 -29.3177 -64.4798 -5.75 +5 86 0 -27.4125 -65.5798 -5.75 +5 87 0 -25.5072 -66.6798 -5.75 +5 88 0 -23.602 -67.7798 -5.75 +5 89 0 -21.6967 -68.8798 -5.75 +5 90 0 -15.65 -70.5 -5.75 +5 91 0 -13.45 -70.5 -5.75 +5 92 0 -11.25 -70.5 -5.75 +5 93 0 -9.05 -70.5 -5.75 +5 94 0 -6.85 -70.5 -5.75 +5 95 0 -4.65 -70.5 -5.75 +5 96 0 -2.45 -70.5 -5.75 +5 97 0 0.25 -70.5 -5.75 +5 98 0 2.45 -70.5 -5.75 +5 99 0 4.65 -70.5 -5.75 +5 100 0 6.85 -70.5 -5.75 +5 101 0 9.05 -70.5 -5.75 +5 102 0 11.25 -70.5 -5.75 +5 103 0 13.45 -70.5 -5.75 +5 104 0 15.65 -70.5 -5.75 +5 105 0 21.6967 -68.8798 -5.75 +5 106 0 23.602 -67.7798 -5.75 +5 107 0 25.5072 -66.6798 -5.75 +5 108 0 27.4125 -65.5798 -5.75 +5 109 0 29.3177 -64.4798 -5.75 +5 110 0 31.223 -63.3798 -5.75 +5 111 0 33.1282 -62.2798 -5.75 +5 112 0 35.4665 -60.9298 -5.75 +5 113 0 37.3718 -59.8298 -5.75 +5 114 0 39.277 -58.7298 -5.75 +5 115 0 41.1823 -57.6298 -5.75 +5 116 0 43.0875 -56.5298 -5.75 +5 117 0 44.9928 -55.4298 -5.75 +5 118 0 46.898 -54.3298 -5.75 +5 119 0 48.8033 -53.2298 -5.75 +5 120 0 53.2298 -48.8033 -5.75 +5 121 0 54.3298 -46.898 -5.75 +5 122 0 55.4298 -44.9928 -5.75 +5 123 0 56.5298 -43.0875 -5.75 +5 124 0 57.6298 -41.1823 -5.75 +5 125 0 58.7298 -39.277 -5.75 +5 126 0 59.8298 -37.3718 -5.75 +5 127 0 61.1798 -35.0335 -5.75 +5 128 0 62.2798 -33.1282 -5.75 +5 129 0 63.3798 -31.223 -5.75 +5 130 0 64.4798 -29.3177 -5.75 +5 131 0 65.5798 -27.4125 -5.75 +5 132 0 66.6798 -25.5072 -5.75 +5 133 0 67.7798 -23.602 -5.75 +5 134 0 68.8798 -21.6967 -5.75 +5 135 0 70.5 -15.65 -5.75 +5 136 0 70.5 -13.45 -5.75 +5 137 0 70.5 -11.25 -5.75 +5 138 0 70.5 -9.05 -5.75 +5 139 0 70.5 -6.85 -5.75 +5 140 0 70.5 -4.65 -5.75 +5 141 0 70.5 -2.45 -5.75 +5 142 0 70.5 0.25 -5.75 +5 143 0 70.5 2.45 -5.75 +5 144 0 70.5 4.65 -5.75 +5 145 0 70.5 6.85 -5.75 +5 146 0 70.5 9.05 -5.75 +5 147 0 70.5 11.25 -5.75 +5 148 0 70.5 13.45 -5.75 +5 149 0 70.5 15.65 -5.75 +5 150 0 68.8798 21.6967 -5.75 +5 151 0 67.7798 23.602 -5.75 +5 152 0 66.6798 25.5072 -5.75 +5 153 0 65.5798 27.4125 -5.75 +5 154 0 64.4798 29.3177 -5.75 +5 155 0 63.3798 31.223 -5.75 +5 156 0 62.2798 33.1282 -5.75 +5 157 0 60.9298 35.4665 -5.75 +5 158 0 59.8298 37.3718 -5.75 +5 159 0 58.7298 39.277 -5.75 +5 160 0 57.6298 41.1823 -5.75 +5 161 0 56.5298 43.0875 -5.75 +5 162 0 55.4298 44.9928 -5.75 +5 163 0 54.3298 46.898 -5.75 +5 164 0 53.2298 48.8033 -5.75 +5 165 0 48.8033 53.2298 -5.75 +5 166 0 46.898 54.3298 -5.75 +5 167 0 44.9928 55.4298 -5.75 +5 168 0 43.0875 56.5298 -5.75 +5 169 0 41.1823 57.6298 -5.75 +5 170 0 39.277 58.7298 -5.75 +5 171 0 37.3718 59.8298 -5.75 +5 172 0 35.0335 61.1798 -5.75 +5 173 0 33.1282 62.2798 -5.75 +5 174 0 31.223 63.3798 -5.75 +5 175 0 29.3177 64.4798 -5.75 +5 176 0 27.4125 65.5798 -5.75 +5 177 0 25.5072 66.6798 -5.75 +5 178 0 23.602 67.7798 -5.75 +5 179 0 21.6967 68.8798 -5.75 +6 0 0 15.65 70.5 -3.55 +6 1 0 13.45 70.5 -3.55 +6 2 0 11.25 70.5 -3.55 +6 3 0 9.05 70.5 -3.55 +6 4 0 6.85 70.5 -3.55 +6 5 0 4.65 70.5 -3.55 +6 6 0 2.45 70.5 -3.55 +6 7 0 -0.25 70.5 -3.55 +6 8 0 -2.45 70.5 -3.55 +6 9 0 -4.65 70.5 -3.55 +6 10 0 -6.85 70.5 -3.55 +6 11 0 -9.05 70.5 -3.55 +6 12 0 -11.25 70.5 -3.55 +6 13 0 -13.45 70.5 -3.55 +6 14 0 -15.65 70.5 -3.55 +6 15 0 -21.6967 68.8798 -3.55 +6 16 0 -23.602 67.7798 -3.55 +6 17 0 -25.5072 66.6798 -3.55 +6 18 0 -27.4125 65.5798 -3.55 +6 19 0 -29.3177 64.4798 -3.55 +6 20 0 -31.223 63.3798 -3.55 +6 21 0 -33.1282 62.2798 -3.55 +6 22 0 -35.4665 60.9298 -3.55 +6 23 0 -37.3718 59.8298 -3.55 +6 24 0 -39.277 58.7298 -3.55 +6 25 0 -41.1823 57.6298 -3.55 +6 26 0 -43.0875 56.5298 -3.55 +6 27 0 -44.9928 55.4298 -3.55 +6 28 0 -46.898 54.3298 -3.55 +6 29 0 -48.8033 53.2298 -3.55 +6 30 0 -53.2298 48.8033 -3.55 +6 31 0 -54.3298 46.898 -3.55 +6 32 0 -55.4298 44.9928 -3.55 +6 33 0 -56.5298 43.0875 -3.55 +6 34 0 -57.6298 41.1823 -3.55 +6 35 0 -58.7298 39.277 -3.55 +6 36 0 -59.8298 37.3718 -3.55 +6 37 0 -61.1798 35.0335 -3.55 +6 38 0 -62.2798 33.1282 -3.55 +6 39 0 -63.3798 31.223 -3.55 +6 40 0 -64.4798 29.3177 -3.55 +6 41 0 -65.5798 27.4125 -3.55 +6 42 0 -66.6798 25.5072 -3.55 +6 43 0 -67.7798 23.602 -3.55 +6 44 0 -68.8798 21.6967 -3.55 +6 45 0 -70.5 15.65 -3.55 +6 46 0 -70.5 13.45 -3.55 +6 47 0 -70.5 11.25 -3.55 +6 48 0 -70.5 9.05 -3.55 +6 49 0 -70.5 6.85 -3.55 +6 50 0 -70.5 4.65 -3.55 +6 51 0 -70.5 2.45 -3.55 +6 52 0 -70.5 -0.25 -3.55 +6 53 0 -70.5 -2.45 -3.55 +6 54 0 -70.5 -4.65 -3.55 +6 55 0 -70.5 -6.85 -3.55 +6 56 0 -70.5 -9.05 -3.55 +6 57 0 -70.5 -11.25 -3.55 +6 58 0 -70.5 -13.45 -3.55 +6 59 0 -70.5 -15.65 -3.55 +6 60 0 -68.8798 -21.6967 -3.55 +6 61 0 -67.7798 -23.602 -3.55 +6 62 0 -66.6798 -25.5072 -3.55 +6 63 0 -65.5798 -27.4125 -3.55 +6 64 0 -64.4798 -29.3177 -3.55 +6 65 0 -63.3798 -31.223 -3.55 +6 66 0 -62.2798 -33.1282 -3.55 +6 67 0 -60.9298 -35.4665 -3.55 +6 68 0 -59.8298 -37.3718 -3.55 +6 69 0 -58.7298 -39.277 -3.55 +6 70 0 -57.6298 -41.1823 -3.55 +6 71 0 -56.5298 -43.0875 -3.55 +6 72 0 -55.4298 -44.9928 -3.55 +6 73 0 -54.3298 -46.898 -3.55 +6 74 0 -53.2298 -48.8033 -3.55 +6 75 0 -48.8033 -53.2298 -3.55 +6 76 0 -46.898 -54.3298 -3.55 +6 77 0 -44.9928 -55.4298 -3.55 +6 78 0 -43.0875 -56.5298 -3.55 +6 79 0 -41.1823 -57.6298 -3.55 +6 80 0 -39.277 -58.7298 -3.55 +6 81 0 -37.3718 -59.8298 -3.55 +6 82 0 -35.0335 -61.1798 -3.55 +6 83 0 -33.1282 -62.2798 -3.55 +6 84 0 -31.223 -63.3798 -3.55 +6 85 0 -29.3177 -64.4798 -3.55 +6 86 0 -27.4125 -65.5798 -3.55 +6 87 0 -25.5072 -66.6798 -3.55 +6 88 0 -23.602 -67.7798 -3.55 +6 89 0 -21.6967 -68.8798 -3.55 +6 90 0 -15.65 -70.5 -3.55 +6 91 0 -13.45 -70.5 -3.55 +6 92 0 -11.25 -70.5 -3.55 +6 93 0 -9.05 -70.5 -3.55 +6 94 0 -6.85 -70.5 -3.55 +6 95 0 -4.65 -70.5 -3.55 +6 96 0 -2.45 -70.5 -3.55 +6 97 0 0.25 -70.5 -3.55 +6 98 0 2.45 -70.5 -3.55 +6 99 0 4.65 -70.5 -3.55 +6 100 0 6.85 -70.5 -3.55 +6 101 0 9.05 -70.5 -3.55 +6 102 0 11.25 -70.5 -3.55 +6 103 0 13.45 -70.5 -3.55 +6 104 0 15.65 -70.5 -3.55 +6 105 0 21.6967 -68.8798 -3.55 +6 106 0 23.602 -67.7798 -3.55 +6 107 0 25.5072 -66.6798 -3.55 +6 108 0 27.4125 -65.5798 -3.55 +6 109 0 29.3177 -64.4798 -3.55 +6 110 0 31.223 -63.3798 -3.55 +6 111 0 33.1282 -62.2798 -3.55 +6 112 0 35.4665 -60.9298 -3.55 +6 113 0 37.3718 -59.8298 -3.55 +6 114 0 39.277 -58.7298 -3.55 +6 115 0 41.1823 -57.6298 -3.55 +6 116 0 43.0875 -56.5298 -3.55 +6 117 0 44.9928 -55.4298 -3.55 +6 118 0 46.898 -54.3298 -3.55 +6 119 0 48.8033 -53.2298 -3.55 +6 120 0 53.2298 -48.8033 -3.55 +6 121 0 54.3298 -46.898 -3.55 +6 122 0 55.4298 -44.9928 -3.55 +6 123 0 56.5298 -43.0875 -3.55 +6 124 0 57.6298 -41.1823 -3.55 +6 125 0 58.7298 -39.277 -3.55 +6 126 0 59.8298 -37.3718 -3.55 +6 127 0 61.1798 -35.0335 -3.55 +6 128 0 62.2798 -33.1282 -3.55 +6 129 0 63.3798 -31.223 -3.55 +6 130 0 64.4798 -29.3177 -3.55 +6 131 0 65.5798 -27.4125 -3.55 +6 132 0 66.6798 -25.5072 -3.55 +6 133 0 67.7798 -23.602 -3.55 +6 134 0 68.8798 -21.6967 -3.55 +6 135 0 70.5 -15.65 -3.55 +6 136 0 70.5 -13.45 -3.55 +6 137 0 70.5 -11.25 -3.55 +6 138 0 70.5 -9.05 -3.55 +6 139 0 70.5 -6.85 -3.55 +6 140 0 70.5 -4.65 -3.55 +6 141 0 70.5 -2.45 -3.55 +6 142 0 70.5 0.25 -3.55 +6 143 0 70.5 2.45 -3.55 +6 144 0 70.5 4.65 -3.55 +6 145 0 70.5 6.85 -3.55 +6 146 0 70.5 9.05 -3.55 +6 147 0 70.5 11.25 -3.55 +6 148 0 70.5 13.45 -3.55 +6 149 0 70.5 15.65 -3.55 +6 150 0 68.8798 21.6967 -3.55 +6 151 0 67.7798 23.602 -3.55 +6 152 0 66.6798 25.5072 -3.55 +6 153 0 65.5798 27.4125 -3.55 +6 154 0 64.4798 29.3177 -3.55 +6 155 0 63.3798 31.223 -3.55 +6 156 0 62.2798 33.1282 -3.55 +6 157 0 60.9298 35.4665 -3.55 +6 158 0 59.8298 37.3718 -3.55 +6 159 0 58.7298 39.277 -3.55 +6 160 0 57.6298 41.1823 -3.55 +6 161 0 56.5298 43.0875 -3.55 +6 162 0 55.4298 44.9928 -3.55 +6 163 0 54.3298 46.898 -3.55 +6 164 0 53.2298 48.8033 -3.55 +6 165 0 48.8033 53.2298 -3.55 +6 166 0 46.898 54.3298 -3.55 +6 167 0 44.9928 55.4298 -3.55 +6 168 0 43.0875 56.5298 -3.55 +6 169 0 41.1823 57.6298 -3.55 +6 170 0 39.277 58.7298 -3.55 +6 171 0 37.3718 59.8298 -3.55 +6 172 0 35.0335 61.1798 -3.55 +6 173 0 33.1282 62.2798 -3.55 +6 174 0 31.223 63.3798 -3.55 +6 175 0 29.3177 64.4798 -3.55 +6 176 0 27.4125 65.5798 -3.55 +6 177 0 25.5072 66.6798 -3.55 +6 178 0 23.602 67.7798 -3.55 +6 179 0 21.6967 68.8798 -3.55 +7 0 0 15.65 70.5 -1.35 +7 1 0 13.45 70.5 -1.35 +7 2 0 11.25 70.5 -1.35 +7 3 0 9.05 70.5 -1.35 +7 4 0 6.85 70.5 -1.35 +7 5 0 4.65 70.5 -1.35 +7 6 0 2.45 70.5 -1.35 +7 7 0 -0.25 70.5 -1.35 +7 8 0 -2.45 70.5 -1.35 +7 9 0 -4.65 70.5 -1.35 +7 10 0 -6.85 70.5 -1.35 +7 11 0 -9.05 70.5 -1.35 +7 12 0 -11.25 70.5 -1.35 +7 13 0 -13.45 70.5 -1.35 +7 14 0 -15.65 70.5 -1.35 +7 15 0 -21.6967 68.8798 -1.35 +7 16 0 -23.602 67.7798 -1.35 +7 17 0 -25.5072 66.6798 -1.35 +7 18 0 -27.4125 65.5798 -1.35 +7 19 0 -29.3177 64.4798 -1.35 +7 20 0 -31.223 63.3798 -1.35 +7 21 0 -33.1282 62.2798 -1.35 +7 22 0 -35.4665 60.9298 -1.35 +7 23 0 -37.3718 59.8298 -1.35 +7 24 0 -39.277 58.7298 -1.35 +7 25 0 -41.1823 57.6298 -1.35 +7 26 0 -43.0875 56.5298 -1.35 +7 27 0 -44.9928 55.4298 -1.35 +7 28 0 -46.898 54.3298 -1.35 +7 29 0 -48.8033 53.2298 -1.35 +7 30 0 -53.2298 48.8033 -1.35 +7 31 0 -54.3298 46.898 -1.35 +7 32 0 -55.4298 44.9928 -1.35 +7 33 0 -56.5298 43.0875 -1.35 +7 34 0 -57.6298 41.1823 -1.35 +7 35 0 -58.7298 39.277 -1.35 +7 36 0 -59.8298 37.3718 -1.35 +7 37 0 -61.1798 35.0335 -1.35 +7 38 0 -62.2798 33.1282 -1.35 +7 39 0 -63.3798 31.223 -1.35 +7 40 0 -64.4798 29.3177 -1.35 +7 41 0 -65.5798 27.4125 -1.35 +7 42 0 -66.6798 25.5072 -1.35 +7 43 0 -67.7798 23.602 -1.35 +7 44 0 -68.8798 21.6967 -1.35 +7 45 0 -70.5 15.65 -1.35 +7 46 0 -70.5 13.45 -1.35 +7 47 0 -70.5 11.25 -1.35 +7 48 0 -70.5 9.05 -1.35 +7 49 0 -70.5 6.85 -1.35 +7 50 0 -70.5 4.65 -1.35 +7 51 0 -70.5 2.45 -1.35 +7 52 0 -70.5 -0.25 -1.35 +7 53 0 -70.5 -2.45 -1.35 +7 54 0 -70.5 -4.65 -1.35 +7 55 0 -70.5 -6.85 -1.35 +7 56 0 -70.5 -9.05 -1.35 +7 57 0 -70.5 -11.25 -1.35 +7 58 0 -70.5 -13.45 -1.35 +7 59 0 -70.5 -15.65 -1.35 +7 60 0 -68.8798 -21.6967 -1.35 +7 61 0 -67.7798 -23.602 -1.35 +7 62 0 -66.6798 -25.5072 -1.35 +7 63 0 -65.5798 -27.4125 -1.35 +7 64 0 -64.4798 -29.3177 -1.35 +7 65 0 -63.3798 -31.223 -1.35 +7 66 0 -62.2798 -33.1282 -1.35 +7 67 0 -60.9298 -35.4665 -1.35 +7 68 0 -59.8298 -37.3718 -1.35 +7 69 0 -58.7298 -39.277 -1.35 +7 70 0 -57.6298 -41.1823 -1.35 +7 71 0 -56.5298 -43.0875 -1.35 +7 72 0 -55.4298 -44.9928 -1.35 +7 73 0 -54.3298 -46.898 -1.35 +7 74 0 -53.2298 -48.8033 -1.35 +7 75 0 -48.8033 -53.2298 -1.35 +7 76 0 -46.898 -54.3298 -1.35 +7 77 0 -44.9928 -55.4298 -1.35 +7 78 0 -43.0875 -56.5298 -1.35 +7 79 0 -41.1823 -57.6298 -1.35 +7 80 0 -39.277 -58.7298 -1.35 +7 81 0 -37.3718 -59.8298 -1.35 +7 82 0 -35.0335 -61.1798 -1.35 +7 83 0 -33.1282 -62.2798 -1.35 +7 84 0 -31.223 -63.3798 -1.35 +7 85 0 -29.3177 -64.4798 -1.35 +7 86 0 -27.4125 -65.5798 -1.35 +7 87 0 -25.5072 -66.6798 -1.35 +7 88 0 -23.602 -67.7798 -1.35 +7 89 0 -21.6967 -68.8798 -1.35 +7 90 0 -15.65 -70.5 -1.35 +7 91 0 -13.45 -70.5 -1.35 +7 92 0 -11.25 -70.5 -1.35 +7 93 0 -9.05 -70.5 -1.35 +7 94 0 -6.85 -70.5 -1.35 +7 95 0 -4.65 -70.5 -1.35 +7 96 0 -2.45 -70.5 -1.35 +7 97 0 0.25 -70.5 -1.35 +7 98 0 2.45 -70.5 -1.35 +7 99 0 4.65 -70.5 -1.35 +7 100 0 6.85 -70.5 -1.35 +7 101 0 9.05 -70.5 -1.35 +7 102 0 11.25 -70.5 -1.35 +7 103 0 13.45 -70.5 -1.35 +7 104 0 15.65 -70.5 -1.35 +7 105 0 21.6967 -68.8798 -1.35 +7 106 0 23.602 -67.7798 -1.35 +7 107 0 25.5072 -66.6798 -1.35 +7 108 0 27.4125 -65.5798 -1.35 +7 109 0 29.3177 -64.4798 -1.35 +7 110 0 31.223 -63.3798 -1.35 +7 111 0 33.1282 -62.2798 -1.35 +7 112 0 35.4665 -60.9298 -1.35 +7 113 0 37.3718 -59.8298 -1.35 +7 114 0 39.277 -58.7298 -1.35 +7 115 0 41.1823 -57.6298 -1.35 +7 116 0 43.0875 -56.5298 -1.35 +7 117 0 44.9928 -55.4298 -1.35 +7 118 0 46.898 -54.3298 -1.35 +7 119 0 48.8033 -53.2298 -1.35 +7 120 0 53.2298 -48.8033 -1.35 +7 121 0 54.3298 -46.898 -1.35 +7 122 0 55.4298 -44.9928 -1.35 +7 123 0 56.5298 -43.0875 -1.35 +7 124 0 57.6298 -41.1823 -1.35 +7 125 0 58.7298 -39.277 -1.35 +7 126 0 59.8298 -37.3718 -1.35 +7 127 0 61.1798 -35.0335 -1.35 +7 128 0 62.2798 -33.1282 -1.35 +7 129 0 63.3798 -31.223 -1.35 +7 130 0 64.4798 -29.3177 -1.35 +7 131 0 65.5798 -27.4125 -1.35 +7 132 0 66.6798 -25.5072 -1.35 +7 133 0 67.7798 -23.602 -1.35 +7 134 0 68.8798 -21.6967 -1.35 +7 135 0 70.5 -15.65 -1.35 +7 136 0 70.5 -13.45 -1.35 +7 137 0 70.5 -11.25 -1.35 +7 138 0 70.5 -9.05 -1.35 +7 139 0 70.5 -6.85 -1.35 +7 140 0 70.5 -4.65 -1.35 +7 141 0 70.5 -2.45 -1.35 +7 142 0 70.5 0.25 -1.35 +7 143 0 70.5 2.45 -1.35 +7 144 0 70.5 4.65 -1.35 +7 145 0 70.5 6.85 -1.35 +7 146 0 70.5 9.05 -1.35 +7 147 0 70.5 11.25 -1.35 +7 148 0 70.5 13.45 -1.35 +7 149 0 70.5 15.65 -1.35 +7 150 0 68.8798 21.6967 -1.35 +7 151 0 67.7798 23.602 -1.35 +7 152 0 66.6798 25.5072 -1.35 +7 153 0 65.5798 27.4125 -1.35 +7 154 0 64.4798 29.3177 -1.35 +7 155 0 63.3798 31.223 -1.35 +7 156 0 62.2798 33.1282 -1.35 +7 157 0 60.9298 35.4665 -1.35 +7 158 0 59.8298 37.3718 -1.35 +7 159 0 58.7298 39.277 -1.35 +7 160 0 57.6298 41.1823 -1.35 +7 161 0 56.5298 43.0875 -1.35 +7 162 0 55.4298 44.9928 -1.35 +7 163 0 54.3298 46.898 -1.35 +7 164 0 53.2298 48.8033 -1.35 +7 165 0 48.8033 53.2298 -1.35 +7 166 0 46.898 54.3298 -1.35 +7 167 0 44.9928 55.4298 -1.35 +7 168 0 43.0875 56.5298 -1.35 +7 169 0 41.1823 57.6298 -1.35 +7 170 0 39.277 58.7298 -1.35 +7 171 0 37.3718 59.8298 -1.35 +7 172 0 35.0335 61.1798 -1.35 +7 173 0 33.1282 62.2798 -1.35 +7 174 0 31.223 63.3798 -1.35 +7 175 0 29.3177 64.4798 -1.35 +7 176 0 27.4125 65.5798 -1.35 +7 177 0 25.5072 66.6798 -1.35 +7 178 0 23.602 67.7798 -1.35 +7 179 0 21.6967 68.8798 -1.35 +8 0 0 15.65 70.5 1.35 +8 1 0 13.45 70.5 1.35 +8 2 0 11.25 70.5 1.35 +8 3 0 9.05 70.5 1.35 +8 4 0 6.85 70.5 1.35 +8 5 0 4.65 70.5 1.35 +8 6 0 2.45 70.5 1.35 +8 7 0 -0.25 70.5 1.35 +8 8 0 -2.45 70.5 1.35 +8 9 0 -4.65 70.5 1.35 +8 10 0 -6.85 70.5 1.35 +8 11 0 -9.05 70.5 1.35 +8 12 0 -11.25 70.5 1.35 +8 13 0 -13.45 70.5 1.35 +8 14 0 -15.65 70.5 1.35 +8 15 0 -21.6967 68.8798 1.35 +8 16 0 -23.602 67.7798 1.35 +8 17 0 -25.5072 66.6798 1.35 +8 18 0 -27.4125 65.5798 1.35 +8 19 0 -29.3177 64.4798 1.35 +8 20 0 -31.223 63.3798 1.35 +8 21 0 -33.1282 62.2798 1.35 +8 22 0 -35.4665 60.9298 1.35 +8 23 0 -37.3718 59.8298 1.35 +8 24 0 -39.277 58.7298 1.35 +8 25 0 -41.1823 57.6298 1.35 +8 26 0 -43.0875 56.5298 1.35 +8 27 0 -44.9928 55.4298 1.35 +8 28 0 -46.898 54.3298 1.35 +8 29 0 -48.8033 53.2298 1.35 +8 30 0 -53.2298 48.8033 1.35 +8 31 0 -54.3298 46.898 1.35 +8 32 0 -55.4298 44.9928 1.35 +8 33 0 -56.5298 43.0875 1.35 +8 34 0 -57.6298 41.1823 1.35 +8 35 0 -58.7298 39.277 1.35 +8 36 0 -59.8298 37.3718 1.35 +8 37 0 -61.1798 35.0335 1.35 +8 38 0 -62.2798 33.1282 1.35 +8 39 0 -63.3798 31.223 1.35 +8 40 0 -64.4798 29.3177 1.35 +8 41 0 -65.5798 27.4125 1.35 +8 42 0 -66.6798 25.5072 1.35 +8 43 0 -67.7798 23.602 1.35 +8 44 0 -68.8798 21.6967 1.35 +8 45 0 -70.5 15.65 1.35 +8 46 0 -70.5 13.45 1.35 +8 47 0 -70.5 11.25 1.35 +8 48 0 -70.5 9.05 1.35 +8 49 0 -70.5 6.85 1.35 +8 50 0 -70.5 4.65 1.35 +8 51 0 -70.5 2.45 1.35 +8 52 0 -70.5 -0.25 1.35 +8 53 0 -70.5 -2.45 1.35 +8 54 0 -70.5 -4.65 1.35 +8 55 0 -70.5 -6.85 1.35 +8 56 0 -70.5 -9.05 1.35 +8 57 0 -70.5 -11.25 1.35 +8 58 0 -70.5 -13.45 1.35 +8 59 0 -70.5 -15.65 1.35 +8 60 0 -68.8798 -21.6967 1.35 +8 61 0 -67.7798 -23.602 1.35 +8 62 0 -66.6798 -25.5072 1.35 +8 63 0 -65.5798 -27.4125 1.35 +8 64 0 -64.4798 -29.3177 1.35 +8 65 0 -63.3798 -31.223 1.35 +8 66 0 -62.2798 -33.1282 1.35 +8 67 0 -60.9298 -35.4665 1.35 +8 68 0 -59.8298 -37.3718 1.35 +8 69 0 -58.7298 -39.277 1.35 +8 70 0 -57.6298 -41.1823 1.35 +8 71 0 -56.5298 -43.0875 1.35 +8 72 0 -55.4298 -44.9928 1.35 +8 73 0 -54.3298 -46.898 1.35 +8 74 0 -53.2298 -48.8033 1.35 +8 75 0 -48.8033 -53.2298 1.35 +8 76 0 -46.898 -54.3298 1.35 +8 77 0 -44.9928 -55.4298 1.35 +8 78 0 -43.0875 -56.5298 1.35 +8 79 0 -41.1823 -57.6298 1.35 +8 80 0 -39.277 -58.7298 1.35 +8 81 0 -37.3718 -59.8298 1.35 +8 82 0 -35.0335 -61.1798 1.35 +8 83 0 -33.1282 -62.2798 1.35 +8 84 0 -31.223 -63.3798 1.35 +8 85 0 -29.3177 -64.4798 1.35 +8 86 0 -27.4125 -65.5798 1.35 +8 87 0 -25.5072 -66.6798 1.35 +8 88 0 -23.602 -67.7798 1.35 +8 89 0 -21.6967 -68.8798 1.35 +8 90 0 -15.65 -70.5 1.35 +8 91 0 -13.45 -70.5 1.35 +8 92 0 -11.25 -70.5 1.35 +8 93 0 -9.05 -70.5 1.35 +8 94 0 -6.85 -70.5 1.35 +8 95 0 -4.65 -70.5 1.35 +8 96 0 -2.45 -70.5 1.35 +8 97 0 0.25 -70.5 1.35 +8 98 0 2.45 -70.5 1.35 +8 99 0 4.65 -70.5 1.35 +8 100 0 6.85 -70.5 1.35 +8 101 0 9.05 -70.5 1.35 +8 102 0 11.25 -70.5 1.35 +8 103 0 13.45 -70.5 1.35 +8 104 0 15.65 -70.5 1.35 +8 105 0 21.6967 -68.8798 1.35 +8 106 0 23.602 -67.7798 1.35 +8 107 0 25.5072 -66.6798 1.35 +8 108 0 27.4125 -65.5798 1.35 +8 109 0 29.3177 -64.4798 1.35 +8 110 0 31.223 -63.3798 1.35 +8 111 0 33.1282 -62.2798 1.35 +8 112 0 35.4665 -60.9298 1.35 +8 113 0 37.3718 -59.8298 1.35 +8 114 0 39.277 -58.7298 1.35 +8 115 0 41.1823 -57.6298 1.35 +8 116 0 43.0875 -56.5298 1.35 +8 117 0 44.9928 -55.4298 1.35 +8 118 0 46.898 -54.3298 1.35 +8 119 0 48.8033 -53.2298 1.35 +8 120 0 53.2298 -48.8033 1.35 +8 121 0 54.3298 -46.898 1.35 +8 122 0 55.4298 -44.9928 1.35 +8 123 0 56.5298 -43.0875 1.35 +8 124 0 57.6298 -41.1823 1.35 +8 125 0 58.7298 -39.277 1.35 +8 126 0 59.8298 -37.3718 1.35 +8 127 0 61.1798 -35.0335 1.35 +8 128 0 62.2798 -33.1282 1.35 +8 129 0 63.3798 -31.223 1.35 +8 130 0 64.4798 -29.3177 1.35 +8 131 0 65.5798 -27.4125 1.35 +8 132 0 66.6798 -25.5072 1.35 +8 133 0 67.7798 -23.602 1.35 +8 134 0 68.8798 -21.6967 1.35 +8 135 0 70.5 -15.65 1.35 +8 136 0 70.5 -13.45 1.35 +8 137 0 70.5 -11.25 1.35 +8 138 0 70.5 -9.05 1.35 +8 139 0 70.5 -6.85 1.35 +8 140 0 70.5 -4.65 1.35 +8 141 0 70.5 -2.45 1.35 +8 142 0 70.5 0.25 1.35 +8 143 0 70.5 2.45 1.35 +8 144 0 70.5 4.65 1.35 +8 145 0 70.5 6.85 1.35 +8 146 0 70.5 9.05 1.35 +8 147 0 70.5 11.25 1.35 +8 148 0 70.5 13.45 1.35 +8 149 0 70.5 15.65 1.35 +8 150 0 68.8798 21.6967 1.35 +8 151 0 67.7798 23.602 1.35 +8 152 0 66.6798 25.5072 1.35 +8 153 0 65.5798 27.4125 1.35 +8 154 0 64.4798 29.3177 1.35 +8 155 0 63.3798 31.223 1.35 +8 156 0 62.2798 33.1282 1.35 +8 157 0 60.9298 35.4665 1.35 +8 158 0 59.8298 37.3718 1.35 +8 159 0 58.7298 39.277 1.35 +8 160 0 57.6298 41.1823 1.35 +8 161 0 56.5298 43.0875 1.35 +8 162 0 55.4298 44.9928 1.35 +8 163 0 54.3298 46.898 1.35 +8 164 0 53.2298 48.8033 1.35 +8 165 0 48.8033 53.2298 1.35 +8 166 0 46.898 54.3298 1.35 +8 167 0 44.9928 55.4298 1.35 +8 168 0 43.0875 56.5298 1.35 +8 169 0 41.1823 57.6298 1.35 +8 170 0 39.277 58.7298 1.35 +8 171 0 37.3718 59.8298 1.35 +8 172 0 35.0335 61.1798 1.35 +8 173 0 33.1282 62.2798 1.35 +8 174 0 31.223 63.3798 1.35 +8 175 0 29.3177 64.4798 1.35 +8 176 0 27.4125 65.5798 1.35 +8 177 0 25.5072 66.6798 1.35 +8 178 0 23.602 67.7798 1.35 +8 179 0 21.6967 68.8798 1.35 +9 0 0 15.65 70.5 3.55 +9 1 0 13.45 70.5 3.55 +9 2 0 11.25 70.5 3.55 +9 3 0 9.05 70.5 3.55 +9 4 0 6.85 70.5 3.55 +9 5 0 4.65 70.5 3.55 +9 6 0 2.45 70.5 3.55 +9 7 0 -0.25 70.5 3.55 +9 8 0 -2.45 70.5 3.55 +9 9 0 -4.65 70.5 3.55 +9 10 0 -6.85 70.5 3.55 +9 11 0 -9.05 70.5 3.55 +9 12 0 -11.25 70.5 3.55 +9 13 0 -13.45 70.5 3.55 +9 14 0 -15.65 70.5 3.55 +9 15 0 -21.6967 68.8798 3.55 +9 16 0 -23.602 67.7798 3.55 +9 17 0 -25.5072 66.6798 3.55 +9 18 0 -27.4125 65.5798 3.55 +9 19 0 -29.3177 64.4798 3.55 +9 20 0 -31.223 63.3798 3.55 +9 21 0 -33.1282 62.2798 3.55 +9 22 0 -35.4665 60.9298 3.55 +9 23 0 -37.3718 59.8298 3.55 +9 24 0 -39.277 58.7298 3.55 +9 25 0 -41.1823 57.6298 3.55 +9 26 0 -43.0875 56.5298 3.55 +9 27 0 -44.9928 55.4298 3.55 +9 28 0 -46.898 54.3298 3.55 +9 29 0 -48.8033 53.2298 3.55 +9 30 0 -53.2298 48.8033 3.55 +9 31 0 -54.3298 46.898 3.55 +9 32 0 -55.4298 44.9928 3.55 +9 33 0 -56.5298 43.0875 3.55 +9 34 0 -57.6298 41.1823 3.55 +9 35 0 -58.7298 39.277 3.55 +9 36 0 -59.8298 37.3718 3.55 +9 37 0 -61.1798 35.0335 3.55 +9 38 0 -62.2798 33.1282 3.55 +9 39 0 -63.3798 31.223 3.55 +9 40 0 -64.4798 29.3177 3.55 +9 41 0 -65.5798 27.4125 3.55 +9 42 0 -66.6798 25.5072 3.55 +9 43 0 -67.7798 23.602 3.55 +9 44 0 -68.8798 21.6967 3.55 +9 45 0 -70.5 15.65 3.55 +9 46 0 -70.5 13.45 3.55 +9 47 0 -70.5 11.25 3.55 +9 48 0 -70.5 9.05 3.55 +9 49 0 -70.5 6.85 3.55 +9 50 0 -70.5 4.65 3.55 +9 51 0 -70.5 2.45 3.55 +9 52 0 -70.5 -0.25 3.55 +9 53 0 -70.5 -2.45 3.55 +9 54 0 -70.5 -4.65 3.55 +9 55 0 -70.5 -6.85 3.55 +9 56 0 -70.5 -9.05 3.55 +9 57 0 -70.5 -11.25 3.55 +9 58 0 -70.5 -13.45 3.55 +9 59 0 -70.5 -15.65 3.55 +9 60 0 -68.8798 -21.6967 3.55 +9 61 0 -67.7798 -23.602 3.55 +9 62 0 -66.6798 -25.5072 3.55 +9 63 0 -65.5798 -27.4125 3.55 +9 64 0 -64.4798 -29.3177 3.55 +9 65 0 -63.3798 -31.223 3.55 +9 66 0 -62.2798 -33.1282 3.55 +9 67 0 -60.9298 -35.4665 3.55 +9 68 0 -59.8298 -37.3718 3.55 +9 69 0 -58.7298 -39.277 3.55 +9 70 0 -57.6298 -41.1823 3.55 +9 71 0 -56.5298 -43.0875 3.55 +9 72 0 -55.4298 -44.9928 3.55 +9 73 0 -54.3298 -46.898 3.55 +9 74 0 -53.2298 -48.8033 3.55 +9 75 0 -48.8033 -53.2298 3.55 +9 76 0 -46.898 -54.3298 3.55 +9 77 0 -44.9928 -55.4298 3.55 +9 78 0 -43.0875 -56.5298 3.55 +9 79 0 -41.1823 -57.6298 3.55 +9 80 0 -39.277 -58.7298 3.55 +9 81 0 -37.3718 -59.8298 3.55 +9 82 0 -35.0335 -61.1798 3.55 +9 83 0 -33.1282 -62.2798 3.55 +9 84 0 -31.223 -63.3798 3.55 +9 85 0 -29.3177 -64.4798 3.55 +9 86 0 -27.4125 -65.5798 3.55 +9 87 0 -25.5072 -66.6798 3.55 +9 88 0 -23.602 -67.7798 3.55 +9 89 0 -21.6967 -68.8798 3.55 +9 90 0 -15.65 -70.5 3.55 +9 91 0 -13.45 -70.5 3.55 +9 92 0 -11.25 -70.5 3.55 +9 93 0 -9.05 -70.5 3.55 +9 94 0 -6.85 -70.5 3.55 +9 95 0 -4.65 -70.5 3.55 +9 96 0 -2.45 -70.5 3.55 +9 97 0 0.25 -70.5 3.55 +9 98 0 2.45 -70.5 3.55 +9 99 0 4.65 -70.5 3.55 +9 100 0 6.85 -70.5 3.55 +9 101 0 9.05 -70.5 3.55 +9 102 0 11.25 -70.5 3.55 +9 103 0 13.45 -70.5 3.55 +9 104 0 15.65 -70.5 3.55 +9 105 0 21.6967 -68.8798 3.55 +9 106 0 23.602 -67.7798 3.55 +9 107 0 25.5072 -66.6798 3.55 +9 108 0 27.4125 -65.5798 3.55 +9 109 0 29.3177 -64.4798 3.55 +9 110 0 31.223 -63.3798 3.55 +9 111 0 33.1282 -62.2798 3.55 +9 112 0 35.4665 -60.9298 3.55 +9 113 0 37.3718 -59.8298 3.55 +9 114 0 39.277 -58.7298 3.55 +9 115 0 41.1823 -57.6298 3.55 +9 116 0 43.0875 -56.5298 3.55 +9 117 0 44.9928 -55.4298 3.55 +9 118 0 46.898 -54.3298 3.55 +9 119 0 48.8033 -53.2298 3.55 +9 120 0 53.2298 -48.8033 3.55 +9 121 0 54.3298 -46.898 3.55 +9 122 0 55.4298 -44.9928 3.55 +9 123 0 56.5298 -43.0875 3.55 +9 124 0 57.6298 -41.1823 3.55 +9 125 0 58.7298 -39.277 3.55 +9 126 0 59.8298 -37.3718 3.55 +9 127 0 61.1798 -35.0335 3.55 +9 128 0 62.2798 -33.1282 3.55 +9 129 0 63.3798 -31.223 3.55 +9 130 0 64.4798 -29.3177 3.55 +9 131 0 65.5798 -27.4125 3.55 +9 132 0 66.6798 -25.5072 3.55 +9 133 0 67.7798 -23.602 3.55 +9 134 0 68.8798 -21.6967 3.55 +9 135 0 70.5 -15.65 3.55 +9 136 0 70.5 -13.45 3.55 +9 137 0 70.5 -11.25 3.55 +9 138 0 70.5 -9.05 3.55 +9 139 0 70.5 -6.85 3.55 +9 140 0 70.5 -4.65 3.55 +9 141 0 70.5 -2.45 3.55 +9 142 0 70.5 0.25 3.55 +9 143 0 70.5 2.45 3.55 +9 144 0 70.5 4.65 3.55 +9 145 0 70.5 6.85 3.55 +9 146 0 70.5 9.05 3.55 +9 147 0 70.5 11.25 3.55 +9 148 0 70.5 13.45 3.55 +9 149 0 70.5 15.65 3.55 +9 150 0 68.8798 21.6967 3.55 +9 151 0 67.7798 23.602 3.55 +9 152 0 66.6798 25.5072 3.55 +9 153 0 65.5798 27.4125 3.55 +9 154 0 64.4798 29.3177 3.55 +9 155 0 63.3798 31.223 3.55 +9 156 0 62.2798 33.1282 3.55 +9 157 0 60.9298 35.4665 3.55 +9 158 0 59.8298 37.3718 3.55 +9 159 0 58.7298 39.277 3.55 +9 160 0 57.6298 41.1823 3.55 +9 161 0 56.5298 43.0875 3.55 +9 162 0 55.4298 44.9928 3.55 +9 163 0 54.3298 46.898 3.55 +9 164 0 53.2298 48.8033 3.55 +9 165 0 48.8033 53.2298 3.55 +9 166 0 46.898 54.3298 3.55 +9 167 0 44.9928 55.4298 3.55 +9 168 0 43.0875 56.5298 3.55 +9 169 0 41.1823 57.6298 3.55 +9 170 0 39.277 58.7298 3.55 +9 171 0 37.3718 59.8298 3.55 +9 172 0 35.0335 61.1798 3.55 +9 173 0 33.1282 62.2798 3.55 +9 174 0 31.223 63.3798 3.55 +9 175 0 29.3177 64.4798 3.55 +9 176 0 27.4125 65.5798 3.55 +9 177 0 25.5072 66.6798 3.55 +9 178 0 23.602 67.7798 3.55 +9 179 0 21.6967 68.8798 3.55 +10 0 0 15.65 70.5 5.75 +10 1 0 13.45 70.5 5.75 +10 2 0 11.25 70.5 5.75 +10 3 0 9.05 70.5 5.75 +10 4 0 6.85 70.5 5.75 +10 5 0 4.65 70.5 5.75 +10 6 0 2.45 70.5 5.75 +10 7 0 -0.25 70.5 5.75 +10 8 0 -2.45 70.5 5.75 +10 9 0 -4.65 70.5 5.75 +10 10 0 -6.85 70.5 5.75 +10 11 0 -9.05 70.5 5.75 +10 12 0 -11.25 70.5 5.75 +10 13 0 -13.45 70.5 5.75 +10 14 0 -15.65 70.5 5.75 +10 15 0 -21.6967 68.8798 5.75 +10 16 0 -23.602 67.7798 5.75 +10 17 0 -25.5072 66.6798 5.75 +10 18 0 -27.4125 65.5798 5.75 +10 19 0 -29.3177 64.4798 5.75 +10 20 0 -31.223 63.3798 5.75 +10 21 0 -33.1282 62.2798 5.75 +10 22 0 -35.4665 60.9298 5.75 +10 23 0 -37.3718 59.8298 5.75 +10 24 0 -39.277 58.7298 5.75 +10 25 0 -41.1823 57.6298 5.75 +10 26 0 -43.0875 56.5298 5.75 +10 27 0 -44.9928 55.4298 5.75 +10 28 0 -46.898 54.3298 5.75 +10 29 0 -48.8033 53.2298 5.75 +10 30 0 -53.2298 48.8033 5.75 +10 31 0 -54.3298 46.898 5.75 +10 32 0 -55.4298 44.9928 5.75 +10 33 0 -56.5298 43.0875 5.75 +10 34 0 -57.6298 41.1823 5.75 +10 35 0 -58.7298 39.277 5.75 +10 36 0 -59.8298 37.3718 5.75 +10 37 0 -61.1798 35.0335 5.75 +10 38 0 -62.2798 33.1282 5.75 +10 39 0 -63.3798 31.223 5.75 +10 40 0 -64.4798 29.3177 5.75 +10 41 0 -65.5798 27.4125 5.75 +10 42 0 -66.6798 25.5072 5.75 +10 43 0 -67.7798 23.602 5.75 +10 44 0 -68.8798 21.6967 5.75 +10 45 0 -70.5 15.65 5.75 +10 46 0 -70.5 13.45 5.75 +10 47 0 -70.5 11.25 5.75 +10 48 0 -70.5 9.05 5.75 +10 49 0 -70.5 6.85 5.75 +10 50 0 -70.5 4.65 5.75 +10 51 0 -70.5 2.45 5.75 +10 52 0 -70.5 -0.25 5.75 +10 53 0 -70.5 -2.45 5.75 +10 54 0 -70.5 -4.65 5.75 +10 55 0 -70.5 -6.85 5.75 +10 56 0 -70.5 -9.05 5.75 +10 57 0 -70.5 -11.25 5.75 +10 58 0 -70.5 -13.45 5.75 +10 59 0 -70.5 -15.65 5.75 +10 60 0 -68.8798 -21.6967 5.75 +10 61 0 -67.7798 -23.602 5.75 +10 62 0 -66.6798 -25.5072 5.75 +10 63 0 -65.5798 -27.4125 5.75 +10 64 0 -64.4798 -29.3177 5.75 +10 65 0 -63.3798 -31.223 5.75 +10 66 0 -62.2798 -33.1282 5.75 +10 67 0 -60.9298 -35.4665 5.75 +10 68 0 -59.8298 -37.3718 5.75 +10 69 0 -58.7298 -39.277 5.75 +10 70 0 -57.6298 -41.1823 5.75 +10 71 0 -56.5298 -43.0875 5.75 +10 72 0 -55.4298 -44.9928 5.75 +10 73 0 -54.3298 -46.898 5.75 +10 74 0 -53.2298 -48.8033 5.75 +10 75 0 -48.8033 -53.2298 5.75 +10 76 0 -46.898 -54.3298 5.75 +10 77 0 -44.9928 -55.4298 5.75 +10 78 0 -43.0875 -56.5298 5.75 +10 79 0 -41.1823 -57.6298 5.75 +10 80 0 -39.277 -58.7298 5.75 +10 81 0 -37.3718 -59.8298 5.75 +10 82 0 -35.0335 -61.1798 5.75 +10 83 0 -33.1282 -62.2798 5.75 +10 84 0 -31.223 -63.3798 5.75 +10 85 0 -29.3177 -64.4798 5.75 +10 86 0 -27.4125 -65.5798 5.75 +10 87 0 -25.5072 -66.6798 5.75 +10 88 0 -23.602 -67.7798 5.75 +10 89 0 -21.6967 -68.8798 5.75 +10 90 0 -15.65 -70.5 5.75 +10 91 0 -13.45 -70.5 5.75 +10 92 0 -11.25 -70.5 5.75 +10 93 0 -9.05 -70.5 5.75 +10 94 0 -6.85 -70.5 5.75 +10 95 0 -4.65 -70.5 5.75 +10 96 0 -2.45 -70.5 5.75 +10 97 0 0.25 -70.5 5.75 +10 98 0 2.45 -70.5 5.75 +10 99 0 4.65 -70.5 5.75 +10 100 0 6.85 -70.5 5.75 +10 101 0 9.05 -70.5 5.75 +10 102 0 11.25 -70.5 5.75 +10 103 0 13.45 -70.5 5.75 +10 104 0 15.65 -70.5 5.75 +10 105 0 21.6967 -68.8798 5.75 +10 106 0 23.602 -67.7798 5.75 +10 107 0 25.5072 -66.6798 5.75 +10 108 0 27.4125 -65.5798 5.75 +10 109 0 29.3177 -64.4798 5.75 +10 110 0 31.223 -63.3798 5.75 +10 111 0 33.1282 -62.2798 5.75 +10 112 0 35.4665 -60.9298 5.75 +10 113 0 37.3718 -59.8298 5.75 +10 114 0 39.277 -58.7298 5.75 +10 115 0 41.1823 -57.6298 5.75 +10 116 0 43.0875 -56.5298 5.75 +10 117 0 44.9928 -55.4298 5.75 +10 118 0 46.898 -54.3298 5.75 +10 119 0 48.8033 -53.2298 5.75 +10 120 0 53.2298 -48.8033 5.75 +10 121 0 54.3298 -46.898 5.75 +10 122 0 55.4298 -44.9928 5.75 +10 123 0 56.5298 -43.0875 5.75 +10 124 0 57.6298 -41.1823 5.75 +10 125 0 58.7298 -39.277 5.75 +10 126 0 59.8298 -37.3718 5.75 +10 127 0 61.1798 -35.0335 5.75 +10 128 0 62.2798 -33.1282 5.75 +10 129 0 63.3798 -31.223 5.75 +10 130 0 64.4798 -29.3177 5.75 +10 131 0 65.5798 -27.4125 5.75 +10 132 0 66.6798 -25.5072 5.75 +10 133 0 67.7798 -23.602 5.75 +10 134 0 68.8798 -21.6967 5.75 +10 135 0 70.5 -15.65 5.75 +10 136 0 70.5 -13.45 5.75 +10 137 0 70.5 -11.25 5.75 +10 138 0 70.5 -9.05 5.75 +10 139 0 70.5 -6.85 5.75 +10 140 0 70.5 -4.65 5.75 +10 141 0 70.5 -2.45 5.75 +10 142 0 70.5 0.25 5.75 +10 143 0 70.5 2.45 5.75 +10 144 0 70.5 4.65 5.75 +10 145 0 70.5 6.85 5.75 +10 146 0 70.5 9.05 5.75 +10 147 0 70.5 11.25 5.75 +10 148 0 70.5 13.45 5.75 +10 149 0 70.5 15.65 5.75 +10 150 0 68.8798 21.6967 5.75 +10 151 0 67.7798 23.602 5.75 +10 152 0 66.6798 25.5072 5.75 +10 153 0 65.5798 27.4125 5.75 +10 154 0 64.4798 29.3177 5.75 +10 155 0 63.3798 31.223 5.75 +10 156 0 62.2798 33.1282 5.75 +10 157 0 60.9298 35.4665 5.75 +10 158 0 59.8298 37.3718 5.75 +10 159 0 58.7298 39.277 5.75 +10 160 0 57.6298 41.1823 5.75 +10 161 0 56.5298 43.0875 5.75 +10 162 0 55.4298 44.9928 5.75 +10 163 0 54.3298 46.898 5.75 +10 164 0 53.2298 48.8033 5.75 +10 165 0 48.8033 53.2298 5.75 +10 166 0 46.898 54.3298 5.75 +10 167 0 44.9928 55.4298 5.75 +10 168 0 43.0875 56.5298 5.75 +10 169 0 41.1823 57.6298 5.75 +10 170 0 39.277 58.7298 5.75 +10 171 0 37.3718 59.8298 5.75 +10 172 0 35.0335 61.1798 5.75 +10 173 0 33.1282 62.2798 5.75 +10 174 0 31.223 63.3798 5.75 +10 175 0 29.3177 64.4798 5.75 +10 176 0 27.4125 65.5798 5.75 +10 177 0 25.5072 66.6798 5.75 +10 178 0 23.602 67.7798 5.75 +10 179 0 21.6967 68.8798 5.75 +11 0 0 15.65 70.5 7.95 +11 1 0 13.45 70.5 7.95 +11 2 0 11.25 70.5 7.95 +11 3 0 9.05 70.5 7.95 +11 4 0 6.85 70.5 7.95 +11 5 0 4.65 70.5 7.95 +11 6 0 2.45 70.5 7.95 +11 7 0 -0.25 70.5 7.95 +11 8 0 -2.45 70.5 7.95 +11 9 0 -4.65 70.5 7.95 +11 10 0 -6.85 70.5 7.95 +11 11 0 -9.05 70.5 7.95 +11 12 0 -11.25 70.5 7.95 +11 13 0 -13.45 70.5 7.95 +11 14 0 -15.65 70.5 7.95 +11 15 0 -21.6967 68.8798 7.95 +11 16 0 -23.602 67.7798 7.95 +11 17 0 -25.5072 66.6798 7.95 +11 18 0 -27.4125 65.5798 7.95 +11 19 0 -29.3177 64.4798 7.95 +11 20 0 -31.223 63.3798 7.95 +11 21 0 -33.1282 62.2798 7.95 +11 22 0 -35.4665 60.9298 7.95 +11 23 0 -37.3718 59.8298 7.95 +11 24 0 -39.277 58.7298 7.95 +11 25 0 -41.1823 57.6298 7.95 +11 26 0 -43.0875 56.5298 7.95 +11 27 0 -44.9928 55.4298 7.95 +11 28 0 -46.898 54.3298 7.95 +11 29 0 -48.8033 53.2298 7.95 +11 30 0 -53.2298 48.8033 7.95 +11 31 0 -54.3298 46.898 7.95 +11 32 0 -55.4298 44.9928 7.95 +11 33 0 -56.5298 43.0875 7.95 +11 34 0 -57.6298 41.1823 7.95 +11 35 0 -58.7298 39.277 7.95 +11 36 0 -59.8298 37.3718 7.95 +11 37 0 -61.1798 35.0335 7.95 +11 38 0 -62.2798 33.1282 7.95 +11 39 0 -63.3798 31.223 7.95 +11 40 0 -64.4798 29.3177 7.95 +11 41 0 -65.5798 27.4125 7.95 +11 42 0 -66.6798 25.5072 7.95 +11 43 0 -67.7798 23.602 7.95 +11 44 0 -68.8798 21.6967 7.95 +11 45 0 -70.5 15.65 7.95 +11 46 0 -70.5 13.45 7.95 +11 47 0 -70.5 11.25 7.95 +11 48 0 -70.5 9.05 7.95 +11 49 0 -70.5 6.85 7.95 +11 50 0 -70.5 4.65 7.95 +11 51 0 -70.5 2.45 7.95 +11 52 0 -70.5 -0.25 7.95 +11 53 0 -70.5 -2.45 7.95 +11 54 0 -70.5 -4.65 7.95 +11 55 0 -70.5 -6.85 7.95 +11 56 0 -70.5 -9.05 7.95 +11 57 0 -70.5 -11.25 7.95 +11 58 0 -70.5 -13.45 7.95 +11 59 0 -70.5 -15.65 7.95 +11 60 0 -68.8798 -21.6967 7.95 +11 61 0 -67.7798 -23.602 7.95 +11 62 0 -66.6798 -25.5072 7.95 +11 63 0 -65.5798 -27.4125 7.95 +11 64 0 -64.4798 -29.3177 7.95 +11 65 0 -63.3798 -31.223 7.95 +11 66 0 -62.2798 -33.1282 7.95 +11 67 0 -60.9298 -35.4665 7.95 +11 68 0 -59.8298 -37.3718 7.95 +11 69 0 -58.7298 -39.277 7.95 +11 70 0 -57.6298 -41.1823 7.95 +11 71 0 -56.5298 -43.0875 7.95 +11 72 0 -55.4298 -44.9928 7.95 +11 73 0 -54.3298 -46.898 7.95 +11 74 0 -53.2298 -48.8033 7.95 +11 75 0 -48.8033 -53.2298 7.95 +11 76 0 -46.898 -54.3298 7.95 +11 77 0 -44.9928 -55.4298 7.95 +11 78 0 -43.0875 -56.5298 7.95 +11 79 0 -41.1823 -57.6298 7.95 +11 80 0 -39.277 -58.7298 7.95 +11 81 0 -37.3718 -59.8298 7.95 +11 82 0 -35.0335 -61.1798 7.95 +11 83 0 -33.1282 -62.2798 7.95 +11 84 0 -31.223 -63.3798 7.95 +11 85 0 -29.3177 -64.4798 7.95 +11 86 0 -27.4125 -65.5798 7.95 +11 87 0 -25.5072 -66.6798 7.95 +11 88 0 -23.602 -67.7798 7.95 +11 89 0 -21.6967 -68.8798 7.95 +11 90 0 -15.65 -70.5 7.95 +11 91 0 -13.45 -70.5 7.95 +11 92 0 -11.25 -70.5 7.95 +11 93 0 -9.05 -70.5 7.95 +11 94 0 -6.85 -70.5 7.95 +11 95 0 -4.65 -70.5 7.95 +11 96 0 -2.45 -70.5 7.95 +11 97 0 0.25 -70.5 7.95 +11 98 0 2.45 -70.5 7.95 +11 99 0 4.65 -70.5 7.95 +11 100 0 6.85 -70.5 7.95 +11 101 0 9.05 -70.5 7.95 +11 102 0 11.25 -70.5 7.95 +11 103 0 13.45 -70.5 7.95 +11 104 0 15.65 -70.5 7.95 +11 105 0 21.6967 -68.8798 7.95 +11 106 0 23.602 -67.7798 7.95 +11 107 0 25.5072 -66.6798 7.95 +11 108 0 27.4125 -65.5798 7.95 +11 109 0 29.3177 -64.4798 7.95 +11 110 0 31.223 -63.3798 7.95 +11 111 0 33.1282 -62.2798 7.95 +11 112 0 35.4665 -60.9298 7.95 +11 113 0 37.3718 -59.8298 7.95 +11 114 0 39.277 -58.7298 7.95 +11 115 0 41.1823 -57.6298 7.95 +11 116 0 43.0875 -56.5298 7.95 +11 117 0 44.9928 -55.4298 7.95 +11 118 0 46.898 -54.3298 7.95 +11 119 0 48.8033 -53.2298 7.95 +11 120 0 53.2298 -48.8033 7.95 +11 121 0 54.3298 -46.898 7.95 +11 122 0 55.4298 -44.9928 7.95 +11 123 0 56.5298 -43.0875 7.95 +11 124 0 57.6298 -41.1823 7.95 +11 125 0 58.7298 -39.277 7.95 +11 126 0 59.8298 -37.3718 7.95 +11 127 0 61.1798 -35.0335 7.95 +11 128 0 62.2798 -33.1282 7.95 +11 129 0 63.3798 -31.223 7.95 +11 130 0 64.4798 -29.3177 7.95 +11 131 0 65.5798 -27.4125 7.95 +11 132 0 66.6798 -25.5072 7.95 +11 133 0 67.7798 -23.602 7.95 +11 134 0 68.8798 -21.6967 7.95 +11 135 0 70.5 -15.65 7.95 +11 136 0 70.5 -13.45 7.95 +11 137 0 70.5 -11.25 7.95 +11 138 0 70.5 -9.05 7.95 +11 139 0 70.5 -6.85 7.95 +11 140 0 70.5 -4.65 7.95 +11 141 0 70.5 -2.45 7.95 +11 142 0 70.5 0.25 7.95 +11 143 0 70.5 2.45 7.95 +11 144 0 70.5 4.65 7.95 +11 145 0 70.5 6.85 7.95 +11 146 0 70.5 9.05 7.95 +11 147 0 70.5 11.25 7.95 +11 148 0 70.5 13.45 7.95 +11 149 0 70.5 15.65 7.95 +11 150 0 68.8798 21.6967 7.95 +11 151 0 67.7798 23.602 7.95 +11 152 0 66.6798 25.5072 7.95 +11 153 0 65.5798 27.4125 7.95 +11 154 0 64.4798 29.3177 7.95 +11 155 0 63.3798 31.223 7.95 +11 156 0 62.2798 33.1282 7.95 +11 157 0 60.9298 35.4665 7.95 +11 158 0 59.8298 37.3718 7.95 +11 159 0 58.7298 39.277 7.95 +11 160 0 57.6298 41.1823 7.95 +11 161 0 56.5298 43.0875 7.95 +11 162 0 55.4298 44.9928 7.95 +11 163 0 54.3298 46.898 7.95 +11 164 0 53.2298 48.8033 7.95 +11 165 0 48.8033 53.2298 7.95 +11 166 0 46.898 54.3298 7.95 +11 167 0 44.9928 55.4298 7.95 +11 168 0 43.0875 56.5298 7.95 +11 169 0 41.1823 57.6298 7.95 +11 170 0 39.277 58.7298 7.95 +11 171 0 37.3718 59.8298 7.95 +11 172 0 35.0335 61.1798 7.95 +11 173 0 33.1282 62.2798 7.95 +11 174 0 31.223 63.3798 7.95 +11 175 0 29.3177 64.4798 7.95 +11 176 0 27.4125 65.5798 7.95 +11 177 0 25.5072 66.6798 7.95 +11 178 0 23.602 67.7798 7.95 +11 179 0 21.6967 68.8798 7.95 +12 0 0 15.65 70.5 10.15 +12 1 0 13.45 70.5 10.15 +12 2 0 11.25 70.5 10.15 +12 3 0 9.05 70.5 10.15 +12 4 0 6.85 70.5 10.15 +12 5 0 4.65 70.5 10.15 +12 6 0 2.45 70.5 10.15 +12 7 0 -0.25 70.5 10.15 +12 8 0 -2.45 70.5 10.15 +12 9 0 -4.65 70.5 10.15 +12 10 0 -6.85 70.5 10.15 +12 11 0 -9.05 70.5 10.15 +12 12 0 -11.25 70.5 10.15 +12 13 0 -13.45 70.5 10.15 +12 14 0 -15.65 70.5 10.15 +12 15 0 -21.6967 68.8798 10.15 +12 16 0 -23.602 67.7798 10.15 +12 17 0 -25.5072 66.6798 10.15 +12 18 0 -27.4125 65.5798 10.15 +12 19 0 -29.3177 64.4798 10.15 +12 20 0 -31.223 63.3798 10.15 +12 21 0 -33.1282 62.2798 10.15 +12 22 0 -35.4665 60.9298 10.15 +12 23 0 -37.3718 59.8298 10.15 +12 24 0 -39.277 58.7298 10.15 +12 25 0 -41.1823 57.6298 10.15 +12 26 0 -43.0875 56.5298 10.15 +12 27 0 -44.9928 55.4298 10.15 +12 28 0 -46.898 54.3298 10.15 +12 29 0 -48.8033 53.2298 10.15 +12 30 0 -53.2298 48.8033 10.15 +12 31 0 -54.3298 46.898 10.15 +12 32 0 -55.4298 44.9928 10.15 +12 33 0 -56.5298 43.0875 10.15 +12 34 0 -57.6298 41.1823 10.15 +12 35 0 -58.7298 39.277 10.15 +12 36 0 -59.8298 37.3718 10.15 +12 37 0 -61.1798 35.0335 10.15 +12 38 0 -62.2798 33.1282 10.15 +12 39 0 -63.3798 31.223 10.15 +12 40 0 -64.4798 29.3177 10.15 +12 41 0 -65.5798 27.4125 10.15 +12 42 0 -66.6798 25.5072 10.15 +12 43 0 -67.7798 23.602 10.15 +12 44 0 -68.8798 21.6967 10.15 +12 45 0 -70.5 15.65 10.15 +12 46 0 -70.5 13.45 10.15 +12 47 0 -70.5 11.25 10.15 +12 48 0 -70.5 9.05 10.15 +12 49 0 -70.5 6.85 10.15 +12 50 0 -70.5 4.65 10.15 +12 51 0 -70.5 2.45 10.15 +12 52 0 -70.5 -0.25 10.15 +12 53 0 -70.5 -2.45 10.15 +12 54 0 -70.5 -4.65 10.15 +12 55 0 -70.5 -6.85 10.15 +12 56 0 -70.5 -9.05 10.15 +12 57 0 -70.5 -11.25 10.15 +12 58 0 -70.5 -13.45 10.15 +12 59 0 -70.5 -15.65 10.15 +12 60 0 -68.8798 -21.6967 10.15 +12 61 0 -67.7798 -23.602 10.15 +12 62 0 -66.6798 -25.5072 10.15 +12 63 0 -65.5798 -27.4125 10.15 +12 64 0 -64.4798 -29.3177 10.15 +12 65 0 -63.3798 -31.223 10.15 +12 66 0 -62.2798 -33.1282 10.15 +12 67 0 -60.9298 -35.4665 10.15 +12 68 0 -59.8298 -37.3718 10.15 +12 69 0 -58.7298 -39.277 10.15 +12 70 0 -57.6298 -41.1823 10.15 +12 71 0 -56.5298 -43.0875 10.15 +12 72 0 -55.4298 -44.9928 10.15 +12 73 0 -54.3298 -46.898 10.15 +12 74 0 -53.2298 -48.8033 10.15 +12 75 0 -48.8033 -53.2298 10.15 +12 76 0 -46.898 -54.3298 10.15 +12 77 0 -44.9928 -55.4298 10.15 +12 78 0 -43.0875 -56.5298 10.15 +12 79 0 -41.1823 -57.6298 10.15 +12 80 0 -39.277 -58.7298 10.15 +12 81 0 -37.3718 -59.8298 10.15 +12 82 0 -35.0335 -61.1798 10.15 +12 83 0 -33.1282 -62.2798 10.15 +12 84 0 -31.223 -63.3798 10.15 +12 85 0 -29.3177 -64.4798 10.15 +12 86 0 -27.4125 -65.5798 10.15 +12 87 0 -25.5072 -66.6798 10.15 +12 88 0 -23.602 -67.7798 10.15 +12 89 0 -21.6967 -68.8798 10.15 +12 90 0 -15.65 -70.5 10.15 +12 91 0 -13.45 -70.5 10.15 +12 92 0 -11.25 -70.5 10.15 +12 93 0 -9.05 -70.5 10.15 +12 94 0 -6.85 -70.5 10.15 +12 95 0 -4.65 -70.5 10.15 +12 96 0 -2.45 -70.5 10.15 +12 97 0 0.25 -70.5 10.15 +12 98 0 2.45 -70.5 10.15 +12 99 0 4.65 -70.5 10.15 +12 100 0 6.85 -70.5 10.15 +12 101 0 9.05 -70.5 10.15 +12 102 0 11.25 -70.5 10.15 +12 103 0 13.45 -70.5 10.15 +12 104 0 15.65 -70.5 10.15 +12 105 0 21.6967 -68.8798 10.15 +12 106 0 23.602 -67.7798 10.15 +12 107 0 25.5072 -66.6798 10.15 +12 108 0 27.4125 -65.5798 10.15 +12 109 0 29.3177 -64.4798 10.15 +12 110 0 31.223 -63.3798 10.15 +12 111 0 33.1282 -62.2798 10.15 +12 112 0 35.4665 -60.9298 10.15 +12 113 0 37.3718 -59.8298 10.15 +12 114 0 39.277 -58.7298 10.15 +12 115 0 41.1823 -57.6298 10.15 +12 116 0 43.0875 -56.5298 10.15 +12 117 0 44.9928 -55.4298 10.15 +12 118 0 46.898 -54.3298 10.15 +12 119 0 48.8033 -53.2298 10.15 +12 120 0 53.2298 -48.8033 10.15 +12 121 0 54.3298 -46.898 10.15 +12 122 0 55.4298 -44.9928 10.15 +12 123 0 56.5298 -43.0875 10.15 +12 124 0 57.6298 -41.1823 10.15 +12 125 0 58.7298 -39.277 10.15 +12 126 0 59.8298 -37.3718 10.15 +12 127 0 61.1798 -35.0335 10.15 +12 128 0 62.2798 -33.1282 10.15 +12 129 0 63.3798 -31.223 10.15 +12 130 0 64.4798 -29.3177 10.15 +12 131 0 65.5798 -27.4125 10.15 +12 132 0 66.6798 -25.5072 10.15 +12 133 0 67.7798 -23.602 10.15 +12 134 0 68.8798 -21.6967 10.15 +12 135 0 70.5 -15.65 10.15 +12 136 0 70.5 -13.45 10.15 +12 137 0 70.5 -11.25 10.15 +12 138 0 70.5 -9.05 10.15 +12 139 0 70.5 -6.85 10.15 +12 140 0 70.5 -4.65 10.15 +12 141 0 70.5 -2.45 10.15 +12 142 0 70.5 0.25 10.15 +12 143 0 70.5 2.45 10.15 +12 144 0 70.5 4.65 10.15 +12 145 0 70.5 6.85 10.15 +12 146 0 70.5 9.05 10.15 +12 147 0 70.5 11.25 10.15 +12 148 0 70.5 13.45 10.15 +12 149 0 70.5 15.65 10.15 +12 150 0 68.8798 21.6967 10.15 +12 151 0 67.7798 23.602 10.15 +12 152 0 66.6798 25.5072 10.15 +12 153 0 65.5798 27.4125 10.15 +12 154 0 64.4798 29.3177 10.15 +12 155 0 63.3798 31.223 10.15 +12 156 0 62.2798 33.1282 10.15 +12 157 0 60.9298 35.4665 10.15 +12 158 0 59.8298 37.3718 10.15 +12 159 0 58.7298 39.277 10.15 +12 160 0 57.6298 41.1823 10.15 +12 161 0 56.5298 43.0875 10.15 +12 162 0 55.4298 44.9928 10.15 +12 163 0 54.3298 46.898 10.15 +12 164 0 53.2298 48.8033 10.15 +12 165 0 48.8033 53.2298 10.15 +12 166 0 46.898 54.3298 10.15 +12 167 0 44.9928 55.4298 10.15 +12 168 0 43.0875 56.5298 10.15 +12 169 0 41.1823 57.6298 10.15 +12 170 0 39.277 58.7298 10.15 +12 171 0 37.3718 59.8298 10.15 +12 172 0 35.0335 61.1798 10.15 +12 173 0 33.1282 62.2798 10.15 +12 174 0 31.223 63.3798 10.15 +12 175 0 29.3177 64.4798 10.15 +12 176 0 27.4125 65.5798 10.15 +12 177 0 25.5072 66.6798 10.15 +12 178 0 23.602 67.7798 10.15 +12 179 0 21.6967 68.8798 10.15 +13 0 0 15.65 70.5 12.35 +13 1 0 13.45 70.5 12.35 +13 2 0 11.25 70.5 12.35 +13 3 0 9.05 70.5 12.35 +13 4 0 6.85 70.5 12.35 +13 5 0 4.65 70.5 12.35 +13 6 0 2.45 70.5 12.35 +13 7 0 -0.25 70.5 12.35 +13 8 0 -2.45 70.5 12.35 +13 9 0 -4.65 70.5 12.35 +13 10 0 -6.85 70.5 12.35 +13 11 0 -9.05 70.5 12.35 +13 12 0 -11.25 70.5 12.35 +13 13 0 -13.45 70.5 12.35 +13 14 0 -15.65 70.5 12.35 +13 15 0 -21.6967 68.8798 12.35 +13 16 0 -23.602 67.7798 12.35 +13 17 0 -25.5072 66.6798 12.35 +13 18 0 -27.4125 65.5798 12.35 +13 19 0 -29.3177 64.4798 12.35 +13 20 0 -31.223 63.3798 12.35 +13 21 0 -33.1282 62.2798 12.35 +13 22 0 -35.4665 60.9298 12.35 +13 23 0 -37.3718 59.8298 12.35 +13 24 0 -39.277 58.7298 12.35 +13 25 0 -41.1823 57.6298 12.35 +13 26 0 -43.0875 56.5298 12.35 +13 27 0 -44.9928 55.4298 12.35 +13 28 0 -46.898 54.3298 12.35 +13 29 0 -48.8033 53.2298 12.35 +13 30 0 -53.2298 48.8033 12.35 +13 31 0 -54.3298 46.898 12.35 +13 32 0 -55.4298 44.9928 12.35 +13 33 0 -56.5298 43.0875 12.35 +13 34 0 -57.6298 41.1823 12.35 +13 35 0 -58.7298 39.277 12.35 +13 36 0 -59.8298 37.3718 12.35 +13 37 0 -61.1798 35.0335 12.35 +13 38 0 -62.2798 33.1282 12.35 +13 39 0 -63.3798 31.223 12.35 +13 40 0 -64.4798 29.3177 12.35 +13 41 0 -65.5798 27.4125 12.35 +13 42 0 -66.6798 25.5072 12.35 +13 43 0 -67.7798 23.602 12.35 +13 44 0 -68.8798 21.6967 12.35 +13 45 0 -70.5 15.65 12.35 +13 46 0 -70.5 13.45 12.35 +13 47 0 -70.5 11.25 12.35 +13 48 0 -70.5 9.05 12.35 +13 49 0 -70.5 6.85 12.35 +13 50 0 -70.5 4.65 12.35 +13 51 0 -70.5 2.45 12.35 +13 52 0 -70.5 -0.25 12.35 +13 53 0 -70.5 -2.45 12.35 +13 54 0 -70.5 -4.65 12.35 +13 55 0 -70.5 -6.85 12.35 +13 56 0 -70.5 -9.05 12.35 +13 57 0 -70.5 -11.25 12.35 +13 58 0 -70.5 -13.45 12.35 +13 59 0 -70.5 -15.65 12.35 +13 60 0 -68.8798 -21.6967 12.35 +13 61 0 -67.7798 -23.602 12.35 +13 62 0 -66.6798 -25.5072 12.35 +13 63 0 -65.5798 -27.4125 12.35 +13 64 0 -64.4798 -29.3177 12.35 +13 65 0 -63.3798 -31.223 12.35 +13 66 0 -62.2798 -33.1282 12.35 +13 67 0 -60.9298 -35.4665 12.35 +13 68 0 -59.8298 -37.3718 12.35 +13 69 0 -58.7298 -39.277 12.35 +13 70 0 -57.6298 -41.1823 12.35 +13 71 0 -56.5298 -43.0875 12.35 +13 72 0 -55.4298 -44.9928 12.35 +13 73 0 -54.3298 -46.898 12.35 +13 74 0 -53.2298 -48.8033 12.35 +13 75 0 -48.8033 -53.2298 12.35 +13 76 0 -46.898 -54.3298 12.35 +13 77 0 -44.9928 -55.4298 12.35 +13 78 0 -43.0875 -56.5298 12.35 +13 79 0 -41.1823 -57.6298 12.35 +13 80 0 -39.277 -58.7298 12.35 +13 81 0 -37.3718 -59.8298 12.35 +13 82 0 -35.0335 -61.1798 12.35 +13 83 0 -33.1282 -62.2798 12.35 +13 84 0 -31.223 -63.3798 12.35 +13 85 0 -29.3177 -64.4798 12.35 +13 86 0 -27.4125 -65.5798 12.35 +13 87 0 -25.5072 -66.6798 12.35 +13 88 0 -23.602 -67.7798 12.35 +13 89 0 -21.6967 -68.8798 12.35 +13 90 0 -15.65 -70.5 12.35 +13 91 0 -13.45 -70.5 12.35 +13 92 0 -11.25 -70.5 12.35 +13 93 0 -9.05 -70.5 12.35 +13 94 0 -6.85 -70.5 12.35 +13 95 0 -4.65 -70.5 12.35 +13 96 0 -2.45 -70.5 12.35 +13 97 0 0.25 -70.5 12.35 +13 98 0 2.45 -70.5 12.35 +13 99 0 4.65 -70.5 12.35 +13 100 0 6.85 -70.5 12.35 +13 101 0 9.05 -70.5 12.35 +13 102 0 11.25 -70.5 12.35 +13 103 0 13.45 -70.5 12.35 +13 104 0 15.65 -70.5 12.35 +13 105 0 21.6967 -68.8798 12.35 +13 106 0 23.602 -67.7798 12.35 +13 107 0 25.5072 -66.6798 12.35 +13 108 0 27.4125 -65.5798 12.35 +13 109 0 29.3177 -64.4798 12.35 +13 110 0 31.223 -63.3798 12.35 +13 111 0 33.1282 -62.2798 12.35 +13 112 0 35.4665 -60.9298 12.35 +13 113 0 37.3718 -59.8298 12.35 +13 114 0 39.277 -58.7298 12.35 +13 115 0 41.1823 -57.6298 12.35 +13 116 0 43.0875 -56.5298 12.35 +13 117 0 44.9928 -55.4298 12.35 +13 118 0 46.898 -54.3298 12.35 +13 119 0 48.8033 -53.2298 12.35 +13 120 0 53.2298 -48.8033 12.35 +13 121 0 54.3298 -46.898 12.35 +13 122 0 55.4298 -44.9928 12.35 +13 123 0 56.5298 -43.0875 12.35 +13 124 0 57.6298 -41.1823 12.35 +13 125 0 58.7298 -39.277 12.35 +13 126 0 59.8298 -37.3718 12.35 +13 127 0 61.1798 -35.0335 12.35 +13 128 0 62.2798 -33.1282 12.35 +13 129 0 63.3798 -31.223 12.35 +13 130 0 64.4798 -29.3177 12.35 +13 131 0 65.5798 -27.4125 12.35 +13 132 0 66.6798 -25.5072 12.35 +13 133 0 67.7798 -23.602 12.35 +13 134 0 68.8798 -21.6967 12.35 +13 135 0 70.5 -15.65 12.35 +13 136 0 70.5 -13.45 12.35 +13 137 0 70.5 -11.25 12.35 +13 138 0 70.5 -9.05 12.35 +13 139 0 70.5 -6.85 12.35 +13 140 0 70.5 -4.65 12.35 +13 141 0 70.5 -2.45 12.35 +13 142 0 70.5 0.25 12.35 +13 143 0 70.5 2.45 12.35 +13 144 0 70.5 4.65 12.35 +13 145 0 70.5 6.85 12.35 +13 146 0 70.5 9.05 12.35 +13 147 0 70.5 11.25 12.35 +13 148 0 70.5 13.45 12.35 +13 149 0 70.5 15.65 12.35 +13 150 0 68.8798 21.6967 12.35 +13 151 0 67.7798 23.602 12.35 +13 152 0 66.6798 25.5072 12.35 +13 153 0 65.5798 27.4125 12.35 +13 154 0 64.4798 29.3177 12.35 +13 155 0 63.3798 31.223 12.35 +13 156 0 62.2798 33.1282 12.35 +13 157 0 60.9298 35.4665 12.35 +13 158 0 59.8298 37.3718 12.35 +13 159 0 58.7298 39.277 12.35 +13 160 0 57.6298 41.1823 12.35 +13 161 0 56.5298 43.0875 12.35 +13 162 0 55.4298 44.9928 12.35 +13 163 0 54.3298 46.898 12.35 +13 164 0 53.2298 48.8033 12.35 +13 165 0 48.8033 53.2298 12.35 +13 166 0 46.898 54.3298 12.35 +13 167 0 44.9928 55.4298 12.35 +13 168 0 43.0875 56.5298 12.35 +13 169 0 41.1823 57.6298 12.35 +13 170 0 39.277 58.7298 12.35 +13 171 0 37.3718 59.8298 12.35 +13 172 0 35.0335 61.1798 12.35 +13 173 0 33.1282 62.2798 12.35 +13 174 0 31.223 63.3798 12.35 +13 175 0 29.3177 64.4798 12.35 +13 176 0 27.4125 65.5798 12.35 +13 177 0 25.5072 66.6798 12.35 +13 178 0 23.602 67.7798 12.35 +13 179 0 21.6967 68.8798 12.35 +14 0 0 15.65 70.5 14.55 +14 1 0 13.45 70.5 14.55 +14 2 0 11.25 70.5 14.55 +14 3 0 9.05 70.5 14.55 +14 4 0 6.85 70.5 14.55 +14 5 0 4.65 70.5 14.55 +14 6 0 2.45 70.5 14.55 +14 7 0 -0.25 70.5 14.55 +14 8 0 -2.45 70.5 14.55 +14 9 0 -4.65 70.5 14.55 +14 10 0 -6.85 70.5 14.55 +14 11 0 -9.05 70.5 14.55 +14 12 0 -11.25 70.5 14.55 +14 13 0 -13.45 70.5 14.55 +14 14 0 -15.65 70.5 14.55 +14 15 0 -21.6967 68.8798 14.55 +14 16 0 -23.602 67.7798 14.55 +14 17 0 -25.5072 66.6798 14.55 +14 18 0 -27.4125 65.5798 14.55 +14 19 0 -29.3177 64.4798 14.55 +14 20 0 -31.223 63.3798 14.55 +14 21 0 -33.1282 62.2798 14.55 +14 22 0 -35.4665 60.9298 14.55 +14 23 0 -37.3718 59.8298 14.55 +14 24 0 -39.277 58.7298 14.55 +14 25 0 -41.1823 57.6298 14.55 +14 26 0 -43.0875 56.5298 14.55 +14 27 0 -44.9928 55.4298 14.55 +14 28 0 -46.898 54.3298 14.55 +14 29 0 -48.8033 53.2298 14.55 +14 30 0 -53.2298 48.8033 14.55 +14 31 0 -54.3298 46.898 14.55 +14 32 0 -55.4298 44.9928 14.55 +14 33 0 -56.5298 43.0875 14.55 +14 34 0 -57.6298 41.1823 14.55 +14 35 0 -58.7298 39.277 14.55 +14 36 0 -59.8298 37.3718 14.55 +14 37 0 -61.1798 35.0335 14.55 +14 38 0 -62.2798 33.1282 14.55 +14 39 0 -63.3798 31.223 14.55 +14 40 0 -64.4798 29.3177 14.55 +14 41 0 -65.5798 27.4125 14.55 +14 42 0 -66.6798 25.5072 14.55 +14 43 0 -67.7798 23.602 14.55 +14 44 0 -68.8798 21.6967 14.55 +14 45 0 -70.5 15.65 14.55 +14 46 0 -70.5 13.45 14.55 +14 47 0 -70.5 11.25 14.55 +14 48 0 -70.5 9.05 14.55 +14 49 0 -70.5 6.85 14.55 +14 50 0 -70.5 4.65 14.55 +14 51 0 -70.5 2.45 14.55 +14 52 0 -70.5 -0.25 14.55 +14 53 0 -70.5 -2.45 14.55 +14 54 0 -70.5 -4.65 14.55 +14 55 0 -70.5 -6.85 14.55 +14 56 0 -70.5 -9.05 14.55 +14 57 0 -70.5 -11.25 14.55 +14 58 0 -70.5 -13.45 14.55 +14 59 0 -70.5 -15.65 14.55 +14 60 0 -68.8798 -21.6967 14.55 +14 61 0 -67.7798 -23.602 14.55 +14 62 0 -66.6798 -25.5072 14.55 +14 63 0 -65.5798 -27.4125 14.55 +14 64 0 -64.4798 -29.3177 14.55 +14 65 0 -63.3798 -31.223 14.55 +14 66 0 -62.2798 -33.1282 14.55 +14 67 0 -60.9298 -35.4665 14.55 +14 68 0 -59.8298 -37.3718 14.55 +14 69 0 -58.7298 -39.277 14.55 +14 70 0 -57.6298 -41.1823 14.55 +14 71 0 -56.5298 -43.0875 14.55 +14 72 0 -55.4298 -44.9928 14.55 +14 73 0 -54.3298 -46.898 14.55 +14 74 0 -53.2298 -48.8033 14.55 +14 75 0 -48.8033 -53.2298 14.55 +14 76 0 -46.898 -54.3298 14.55 +14 77 0 -44.9928 -55.4298 14.55 +14 78 0 -43.0875 -56.5298 14.55 +14 79 0 -41.1823 -57.6298 14.55 +14 80 0 -39.277 -58.7298 14.55 +14 81 0 -37.3718 -59.8298 14.55 +14 82 0 -35.0335 -61.1798 14.55 +14 83 0 -33.1282 -62.2798 14.55 +14 84 0 -31.223 -63.3798 14.55 +14 85 0 -29.3177 -64.4798 14.55 +14 86 0 -27.4125 -65.5798 14.55 +14 87 0 -25.5072 -66.6798 14.55 +14 88 0 -23.602 -67.7798 14.55 +14 89 0 -21.6967 -68.8798 14.55 +14 90 0 -15.65 -70.5 14.55 +14 91 0 -13.45 -70.5 14.55 +14 92 0 -11.25 -70.5 14.55 +14 93 0 -9.05 -70.5 14.55 +14 94 0 -6.85 -70.5 14.55 +14 95 0 -4.65 -70.5 14.55 +14 96 0 -2.45 -70.5 14.55 +14 97 0 0.25 -70.5 14.55 +14 98 0 2.45 -70.5 14.55 +14 99 0 4.65 -70.5 14.55 +14 100 0 6.85 -70.5 14.55 +14 101 0 9.05 -70.5 14.55 +14 102 0 11.25 -70.5 14.55 +14 103 0 13.45 -70.5 14.55 +14 104 0 15.65 -70.5 14.55 +14 105 0 21.6967 -68.8798 14.55 +14 106 0 23.602 -67.7798 14.55 +14 107 0 25.5072 -66.6798 14.55 +14 108 0 27.4125 -65.5798 14.55 +14 109 0 29.3177 -64.4798 14.55 +14 110 0 31.223 -63.3798 14.55 +14 111 0 33.1282 -62.2798 14.55 +14 112 0 35.4665 -60.9298 14.55 +14 113 0 37.3718 -59.8298 14.55 +14 114 0 39.277 -58.7298 14.55 +14 115 0 41.1823 -57.6298 14.55 +14 116 0 43.0875 -56.5298 14.55 +14 117 0 44.9928 -55.4298 14.55 +14 118 0 46.898 -54.3298 14.55 +14 119 0 48.8033 -53.2298 14.55 +14 120 0 53.2298 -48.8033 14.55 +14 121 0 54.3298 -46.898 14.55 +14 122 0 55.4298 -44.9928 14.55 +14 123 0 56.5298 -43.0875 14.55 +14 124 0 57.6298 -41.1823 14.55 +14 125 0 58.7298 -39.277 14.55 +14 126 0 59.8298 -37.3718 14.55 +14 127 0 61.1798 -35.0335 14.55 +14 128 0 62.2798 -33.1282 14.55 +14 129 0 63.3798 -31.223 14.55 +14 130 0 64.4798 -29.3177 14.55 +14 131 0 65.5798 -27.4125 14.55 +14 132 0 66.6798 -25.5072 14.55 +14 133 0 67.7798 -23.602 14.55 +14 134 0 68.8798 -21.6967 14.55 +14 135 0 70.5 -15.65 14.55 +14 136 0 70.5 -13.45 14.55 +14 137 0 70.5 -11.25 14.55 +14 138 0 70.5 -9.05 14.55 +14 139 0 70.5 -6.85 14.55 +14 140 0 70.5 -4.65 14.55 +14 141 0 70.5 -2.45 14.55 +14 142 0 70.5 0.25 14.55 +14 143 0 70.5 2.45 14.55 +14 144 0 70.5 4.65 14.55 +14 145 0 70.5 6.85 14.55 +14 146 0 70.5 9.05 14.55 +14 147 0 70.5 11.25 14.55 +14 148 0 70.5 13.45 14.55 +14 149 0 70.5 15.65 14.55 +14 150 0 68.8798 21.6967 14.55 +14 151 0 67.7798 23.602 14.55 +14 152 0 66.6798 25.5072 14.55 +14 153 0 65.5798 27.4125 14.55 +14 154 0 64.4798 29.3177 14.55 +14 155 0 63.3798 31.223 14.55 +14 156 0 62.2798 33.1282 14.55 +14 157 0 60.9298 35.4665 14.55 +14 158 0 59.8298 37.3718 14.55 +14 159 0 58.7298 39.277 14.55 +14 160 0 57.6298 41.1823 14.55 +14 161 0 56.5298 43.0875 14.55 +14 162 0 55.4298 44.9928 14.55 +14 163 0 54.3298 46.898 14.55 +14 164 0 53.2298 48.8033 14.55 +14 165 0 48.8033 53.2298 14.55 +14 166 0 46.898 54.3298 14.55 +14 167 0 44.9928 55.4298 14.55 +14 168 0 43.0875 56.5298 14.55 +14 169 0 41.1823 57.6298 14.55 +14 170 0 39.277 58.7298 14.55 +14 171 0 37.3718 59.8298 14.55 +14 172 0 35.0335 61.1798 14.55 +14 173 0 33.1282 62.2798 14.55 +14 174 0 31.223 63.3798 14.55 +14 175 0 29.3177 64.4798 14.55 +14 176 0 27.4125 65.5798 14.55 +14 177 0 25.5072 66.6798 14.55 +14 178 0 23.602 67.7798 14.55 +14 179 0 21.6967 68.8798 14.55 +15 0 0 15.65 70.5 16.75 +15 1 0 13.45 70.5 16.75 +15 2 0 11.25 70.5 16.75 +15 3 0 9.05 70.5 16.75 +15 4 0 6.85 70.5 16.75 +15 5 0 4.65 70.5 16.75 +15 6 0 2.45 70.5 16.75 +15 7 0 -0.25 70.5 16.75 +15 8 0 -2.45 70.5 16.75 +15 9 0 -4.65 70.5 16.75 +15 10 0 -6.85 70.5 16.75 +15 11 0 -9.05 70.5 16.75 +15 12 0 -11.25 70.5 16.75 +15 13 0 -13.45 70.5 16.75 +15 14 0 -15.65 70.5 16.75 +15 15 0 -21.6967 68.8798 16.75 +15 16 0 -23.602 67.7798 16.75 +15 17 0 -25.5072 66.6798 16.75 +15 18 0 -27.4125 65.5798 16.75 +15 19 0 -29.3177 64.4798 16.75 +15 20 0 -31.223 63.3798 16.75 +15 21 0 -33.1282 62.2798 16.75 +15 22 0 -35.4665 60.9298 16.75 +15 23 0 -37.3718 59.8298 16.75 +15 24 0 -39.277 58.7298 16.75 +15 25 0 -41.1823 57.6298 16.75 +15 26 0 -43.0875 56.5298 16.75 +15 27 0 -44.9928 55.4298 16.75 +15 28 0 -46.898 54.3298 16.75 +15 29 0 -48.8033 53.2298 16.75 +15 30 0 -53.2298 48.8033 16.75 +15 31 0 -54.3298 46.898 16.75 +15 32 0 -55.4298 44.9928 16.75 +15 33 0 -56.5298 43.0875 16.75 +15 34 0 -57.6298 41.1823 16.75 +15 35 0 -58.7298 39.277 16.75 +15 36 0 -59.8298 37.3718 16.75 +15 37 0 -61.1798 35.0335 16.75 +15 38 0 -62.2798 33.1282 16.75 +15 39 0 -63.3798 31.223 16.75 +15 40 0 -64.4798 29.3177 16.75 +15 41 0 -65.5798 27.4125 16.75 +15 42 0 -66.6798 25.5072 16.75 +15 43 0 -67.7798 23.602 16.75 +15 44 0 -68.8798 21.6967 16.75 +15 45 0 -70.5 15.65 16.75 +15 46 0 -70.5 13.45 16.75 +15 47 0 -70.5 11.25 16.75 +15 48 0 -70.5 9.05 16.75 +15 49 0 -70.5 6.85 16.75 +15 50 0 -70.5 4.65 16.75 +15 51 0 -70.5 2.45 16.75 +15 52 0 -70.5 -0.25 16.75 +15 53 0 -70.5 -2.45 16.75 +15 54 0 -70.5 -4.65 16.75 +15 55 0 -70.5 -6.85 16.75 +15 56 0 -70.5 -9.05 16.75 +15 57 0 -70.5 -11.25 16.75 +15 58 0 -70.5 -13.45 16.75 +15 59 0 -70.5 -15.65 16.75 +15 60 0 -68.8798 -21.6967 16.75 +15 61 0 -67.7798 -23.602 16.75 +15 62 0 -66.6798 -25.5072 16.75 +15 63 0 -65.5798 -27.4125 16.75 +15 64 0 -64.4798 -29.3177 16.75 +15 65 0 -63.3798 -31.223 16.75 +15 66 0 -62.2798 -33.1282 16.75 +15 67 0 -60.9298 -35.4665 16.75 +15 68 0 -59.8298 -37.3718 16.75 +15 69 0 -58.7298 -39.277 16.75 +15 70 0 -57.6298 -41.1823 16.75 +15 71 0 -56.5298 -43.0875 16.75 +15 72 0 -55.4298 -44.9928 16.75 +15 73 0 -54.3298 -46.898 16.75 +15 74 0 -53.2298 -48.8033 16.75 +15 75 0 -48.8033 -53.2298 16.75 +15 76 0 -46.898 -54.3298 16.75 +15 77 0 -44.9928 -55.4298 16.75 +15 78 0 -43.0875 -56.5298 16.75 +15 79 0 -41.1823 -57.6298 16.75 +15 80 0 -39.277 -58.7298 16.75 +15 81 0 -37.3718 -59.8298 16.75 +15 82 0 -35.0335 -61.1798 16.75 +15 83 0 -33.1282 -62.2798 16.75 +15 84 0 -31.223 -63.3798 16.75 +15 85 0 -29.3177 -64.4798 16.75 +15 86 0 -27.4125 -65.5798 16.75 +15 87 0 -25.5072 -66.6798 16.75 +15 88 0 -23.602 -67.7798 16.75 +15 89 0 -21.6967 -68.8798 16.75 +15 90 0 -15.65 -70.5 16.75 +15 91 0 -13.45 -70.5 16.75 +15 92 0 -11.25 -70.5 16.75 +15 93 0 -9.05 -70.5 16.75 +15 94 0 -6.85 -70.5 16.75 +15 95 0 -4.65 -70.5 16.75 +15 96 0 -2.45 -70.5 16.75 +15 97 0 0.25 -70.5 16.75 +15 98 0 2.45 -70.5 16.75 +15 99 0 4.65 -70.5 16.75 +15 100 0 6.85 -70.5 16.75 +15 101 0 9.05 -70.5 16.75 +15 102 0 11.25 -70.5 16.75 +15 103 0 13.45 -70.5 16.75 +15 104 0 15.65 -70.5 16.75 +15 105 0 21.6967 -68.8798 16.75 +15 106 0 23.602 -67.7798 16.75 +15 107 0 25.5072 -66.6798 16.75 +15 108 0 27.4125 -65.5798 16.75 +15 109 0 29.3177 -64.4798 16.75 +15 110 0 31.223 -63.3798 16.75 +15 111 0 33.1282 -62.2798 16.75 +15 112 0 35.4665 -60.9298 16.75 +15 113 0 37.3718 -59.8298 16.75 +15 114 0 39.277 -58.7298 16.75 +15 115 0 41.1823 -57.6298 16.75 +15 116 0 43.0875 -56.5298 16.75 +15 117 0 44.9928 -55.4298 16.75 +15 118 0 46.898 -54.3298 16.75 +15 119 0 48.8033 -53.2298 16.75 +15 120 0 53.2298 -48.8033 16.75 +15 121 0 54.3298 -46.898 16.75 +15 122 0 55.4298 -44.9928 16.75 +15 123 0 56.5298 -43.0875 16.75 +15 124 0 57.6298 -41.1823 16.75 +15 125 0 58.7298 -39.277 16.75 +15 126 0 59.8298 -37.3718 16.75 +15 127 0 61.1798 -35.0335 16.75 +15 128 0 62.2798 -33.1282 16.75 +15 129 0 63.3798 -31.223 16.75 +15 130 0 64.4798 -29.3177 16.75 +15 131 0 65.5798 -27.4125 16.75 +15 132 0 66.6798 -25.5072 16.75 +15 133 0 67.7798 -23.602 16.75 +15 134 0 68.8798 -21.6967 16.75 +15 135 0 70.5 -15.65 16.75 +15 136 0 70.5 -13.45 16.75 +15 137 0 70.5 -11.25 16.75 +15 138 0 70.5 -9.05 16.75 +15 139 0 70.5 -6.85 16.75 +15 140 0 70.5 -4.65 16.75 +15 141 0 70.5 -2.45 16.75 +15 142 0 70.5 0.25 16.75 +15 143 0 70.5 2.45 16.75 +15 144 0 70.5 4.65 16.75 +15 145 0 70.5 6.85 16.75 +15 146 0 70.5 9.05 16.75 +15 147 0 70.5 11.25 16.75 +15 148 0 70.5 13.45 16.75 +15 149 0 70.5 15.65 16.75 +15 150 0 68.8798 21.6967 16.75 +15 151 0 67.7798 23.602 16.75 +15 152 0 66.6798 25.5072 16.75 +15 153 0 65.5798 27.4125 16.75 +15 154 0 64.4798 29.3177 16.75 +15 155 0 63.3798 31.223 16.75 +15 156 0 62.2798 33.1282 16.75 +15 157 0 60.9298 35.4665 16.75 +15 158 0 59.8298 37.3718 16.75 +15 159 0 58.7298 39.277 16.75 +15 160 0 57.6298 41.1823 16.75 +15 161 0 56.5298 43.0875 16.75 +15 162 0 55.4298 44.9928 16.75 +15 163 0 54.3298 46.898 16.75 +15 164 0 53.2298 48.8033 16.75 +15 165 0 48.8033 53.2298 16.75 +15 166 0 46.898 54.3298 16.75 +15 167 0 44.9928 55.4298 16.75 +15 168 0 43.0875 56.5298 16.75 +15 169 0 41.1823 57.6298 16.75 +15 170 0 39.277 58.7298 16.75 +15 171 0 37.3718 59.8298 16.75 +15 172 0 35.0335 61.1798 16.75 +15 173 0 33.1282 62.2798 16.75 +15 174 0 31.223 63.3798 16.75 +15 175 0 29.3177 64.4798 16.75 +15 176 0 27.4125 65.5798 16.75 +15 177 0 25.5072 66.6798 16.75 +15 178 0 23.602 67.7798 16.75 +15 179 0 21.6967 68.8798 16.75 diff --git a/examples/SAFIR_genericScanner/OSMAPOSL_QuadraticPrior.par b/examples/SAFIR_genericScanner/OSMAPOSL_QuadraticPrior.par new file mode 100644 index 000000000..daceda7aa --- /dev/null +++ b/examples/SAFIR_genericScanner/OSMAPOSL_QuadraticPrior.par @@ -0,0 +1,105 @@ +OSMAPOSLParameters := +; example file for using a quadratic prior (with One Step Late and subsets) + +objective function type:= PoissonLogLikelihoodWithLinearModelForMeanAndProjData +PoissonLogLikelihoodWithLinearModelForMeanAndProjData Parameters:= + +input file := test_generic_implementation_f1g1d0b0.hs + +; next keywords can be used to specify image size, but will be removed +; they are ignored when using an initial image +zoom := 1 +; use --1 for default sizes that cover the whole field of view +;XY output image size (in pixels) := 61 + +; if disabled, defaults to maximum segment number in the file +maximum absolute segment number to process := 1 +; see User's Guide to see when you need this +zero end planes of segment 0:= 1 + +recompute sensitivity := 1 +use subset sensitivities:=1 ; recommended +; optional filename to store/read the sensitivity image +; (if use subset sensitivity is off) + +; optional filename to store/read the subsensitivities +; use %d where you want the subset-number (a la printf) +subset sensitivity filenames:= sens_%d.hv + +; if the next parameter is disabled, +; the sensitivity will be computed +sensitivity filename:= sens.hv +; specify additive projection data to handle randoms or so +; see User's Guide for more info +additive sinogram := 0 + + +; here comes the prior stuff + +prior type := quadratic + Quadratic Prior Parameters:= + penalisation factor := 5 + ; next defaults to 0, set to 1 for 2D inverse Euclidean weights, 0 for 3D + only 2D:= 0 + ; next can be used to set weights explicitly. Needs to be a 3D array (of floats). + ; value of only_2D is ignored + ; following example uses 2D 'nearest neighbour' penalty + ; weights:={{{0,1,0},{1,0,1},{0,1,0}}} + ; use next parameter to specify an image with penalisation factors (a la Fessler) + ; see class documentation for more info + ; kappa filename:= + ; use next parameter to get gradient images at every subiteration + ; see class documentation + gradient filename prefix:= + END Quadratic Prior Parameters:= + +projector pair type := Matrix + Projector Pair Using Matrix Parameters := + Matrix type := Ray Tracing + Ray tracing matrix parameters := + ; use multiple (almost) parallel LORs for every bin in the sinogram + ; to avoid discretisation artefacts + number of rays in tangential direction to trace for each bin:=5 + ; you could disable some symmetries if you have enough memory + ; this would for instance allow you to increase the number of subsets + do_symmetry_90degrees_min_phi:=0 + do_symmetry_180degrees_min_phi:=0 + do_symmetry_swap_segment:=0 + do_symmetry_swap_s:=0 + do_symmetry_shift_z:= 0 + disable caching:= 1 + End Ray tracing matrix parameters := + End Projector Pair Using Matrix Parameters := + + +end PoissonLogLikelihoodWithLinearModelForMeanAndProjData Parameters:= + +;initial estimate:= +; enable this when you read an initial estimate with negative data +enforce initial positivity condition:=1 + +output filename prefix := test_OSMAPOSL +number of subsets:= 6 +number of subiterations:= 6 +save estimates at subiteration intervals:= 3 + +; enable this for multiplicative form of OSMAPOSL (see User's Guide) +;MAP model := multiplicative + +inter-update filter subiteration interval:= 0 +inter-update filter type := None + +inter-iteration filter subiteration interval:= 0 +inter-iteration filter type := Separable Cartesian Metz + Separable Cartesian Metz Filter Parameters := + x-dir filter FWHM (in mm):= 0.3 + y-dir filter FWHM (in mm):= 0.3 + z-dir filter FWHM (in mm):= 0.3 + x-dir filter Metz power:= 2.0 + y-dir filter Metz power:= 2.0 + z-dir filter Metz power:= 2.0 +END Separable Cartesian Metz Filter Parameters := + +post-filter type := None + +END := diff --git a/examples/SAFIR_genericScanner/README.txt b/examples/SAFIR_genericScanner/README.txt new file mode 100644 index 000000000..7e4eda640 --- /dev/null +++ b/examples/SAFIR_genericScanner/README.txt @@ -0,0 +1,38 @@ +The PET scanner prototype of the SAFIR project does not consists of symmetric +detector blocks. This led to the need for a new scanner geometry in STIR. It +is implemented named generic geometry. In short it uses the coordinates +provided by an external text file instead of the ones computed internally by +STIR itself (based on the symmetry of the detector). + +The files in this directory are to test the new implementation, to understand +the changes and as model for later usage. + +The bash script 'test.sh' runs the methods 'lm_to_projdata', 'backproject', +'forward_project' and 'OSMAPOSL' to test the functionality with the new scanner +geometry. It shows as well how the different parameter files are used. In the +next sections the different files and methods are explained more precisely. + +The first method call 'lm_to_projdata lm_to_projdata.par' turns the listmode +data in the file 'coincidencesLM.clm.safir' to projdata (a.k.a. sinogram data). +The parameter file contains the input SAFIR parameter file +'listmode_input_SAFIR.par' which specifies the above mentioned data file, the +crystal map file and the projdata file template 'muppet.hs'. +The 'lm_to_projdata.par' parameter file contains as well the projdata file +template and the output file name. More infos about the other entries can be +found in the online documentation of STIR and another example file can be found +in 'example/samples/lm_to_projdata.par'. + +The projdata template file can be created with the utility +'create_projdata_template'. The new entries for this implementation are 'name of +crystal map' which defines the name of the file which contains all coordinates +of the detectors like the example file 'DualRingPrototype_crystal_map.txt' and +the new 'Scanner geometry' option 'Generic'. For the other entries please check +the online documentation. + +'forward_project' and 'backproject' are the utilities that convert the sinogram + data into an image and vice versa. + +The OSMAPOSL method is an iterative algorithm to get an image from the +sinogram data. It uses a parameter file as defined in the STIR user guide or +online documentation. There is as well another example file in +'example/samples'. diff --git a/examples/SAFIR_genericScanner/coincidencesLM.clm.safir b/examples/SAFIR_genericScanner/coincidencesLM.clm.safir new file mode 100644 index 0000000000000000000000000000000000000000..521143db0a843ed9c5822637e4e6f5fa032b020a GIT binary patch literal 12040 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 lQ#q1HNjk?(_FkV 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 z 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 z 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

Changes breaking backwards compatibility from a user-perspective

STIR usages of these methods (OSSPS and SqrtHessianRowSum) have been updated to see no effective change in functionality. However, calling the Hessian vector product methods, via python etc., will result in a change in functionality as the sign of the output voxel values has changed. + +
  • Python/MATLAB wrappers no longer have ProjDataInfo::ProjDataInfoCTI, use ProjDataInfo::construct_proj_data_info instead. + However, this now returns an object of the appropriate (derived) type as opposed to just + ProjDataInfo. This should be transparant, except apparently for testing equality of objects. +
  • +

    Bug fixes

    • diff --git a/src/swig/factory_shared.i b/src/swig/factory_shared.i new file mode 100644 index 000000000..d44d54018 --- /dev/null +++ b/src/swig/factory_shared.i @@ -0,0 +1,95 @@ +/* A work-around by Kris Thielemans to be able to use %factory with %shared + See https://github.com/swig/swig/issues/2110 + + To avoid conflicts, the macro is called %factory_shared +*/ +/* + Implement a more natural wrap for factory methods, for example, if + you have: + + ---- geometry.h -------- + struct Geometry { + enum GeomType{ + POINT, + CIRCLE + }; + + virtual ~Geometry() {} + virtual int draw() = 0; + + // + // Factory method for all the Geometry objects + // + static Geometry *create(GeomType i); + }; + + struct Point : Geometry { + int draw() { return 1; } + double width() { return 1.0; } + }; + + struct Circle : Geometry { + int draw() { return 2; } + double radius() { return 1.5; } + }; + + // + // Factory method for all the Geometry objects + // + Geometry *Geometry::create(GeomType type) { + switch (type) { + case POINT: return new Point(); + case CIRCLE: return new Circle(); + default: return 0; + } + } + ---- geometry.h -------- + + + You can use the %factory with the Geometry::create method as follows: + + %newobject Geometry::create; + %factory(Geometry *Geometry::create, Point, Circle); + %include "geometry.h" + + and Geometry::create will return a 'Point' or 'Circle' instance + instead of the plain 'Geometry' type. For example, in python: + + circle = Geometry.create(Geometry.CIRCLE) + r = circle.radius() + + where circle is a Circle proxy instance. + + NOTES: remember to fully qualify all the type names and don't + use %factory inside a namespace declaration, ie, instead of + + namespace Foo { + %factory(Geometry *Geometry::create, Point, Circle); + } + + use + + %factory(Foo::Geometry *Foo::Geometry::create, Foo::Point, Foo::Circle); + + +*/ + +%define %_factory_dispatch_shared(Type) +if (!dcast) { + Type *dobj = dynamic_cast($1); + if (dobj) { + dcast = 1; + SWIG_SHARED_PTR_QNAMESPACE::shared_ptr< Type > *smartresult = new SWIG_SHARED_PTR_QNAMESPACE::shared_ptr< Type >(dobj); + %set_output(SWIG_NewPointerObj(%as_voidptr(smartresult),$descriptor(SWIG_SHARED_PTR_QNAMESPACE::shared_ptr *), $owner | %newpointer_flags)); + } +}%enddef + +%define %factory_shared(Method,Types...) +%typemap(out) Method { + int dcast = 0; + %formacro(%_factory_dispatch_shared, Types) + if (!dcast) { + %set_output(SWIG_NewPointerObj(%as_voidptr($1),$descriptor, $owner | %newpointer_flags)); + } +}%enddef + diff --git a/src/swig/stir.i b/src/swig/stir.i index 5a1247144..0417e189d 100644 --- a/src/swig/stir.i +++ b/src/swig/stir.i @@ -1,6 +1,6 @@ /* Copyright (C) 2011-07-01 - 2012, Kris Thielemans - Copyright (C) 2013, 2018, 2020 University College London + Copyright (C) 2013, 2018, 2020, 2021 University College London This file is part of STIR. SPDX-License-Identifier: Apache-2.0 @@ -40,6 +40,9 @@ #include "stir/Bin.h" #include "stir/ProjDataInfoCylindricalArcCorr.h" #include "stir/ProjDataInfoCylindricalNoArcCorr.h" + #include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" + #include "stir/ProjDataInfoGenericNoArcCorr.h" + #include "stir/Viewgram.h" #include "stir/RelatedViewgrams.h" #include "stir/Sinogram.h" @@ -475,6 +478,7 @@ #endif %include "attribute.i" +%include "factory_shared.i" %init %{ #if defined(SWIGPYTHON) @@ -801,6 +805,10 @@ namespace std { %shared_ptr(stir::ProjDataInfoCylindrical); %shared_ptr(stir::ProjDataInfoCylindricalArcCorr); %shared_ptr(stir::ProjDataInfoCylindricalNoArcCorr); +%shared_ptr(stir::ProjDataInfoGeneric); +%shared_ptr(stir::ProjDataInfoGenericNoArcCorr); +%shared_ptr(stir::ProjDataInfoBlocksOnCylindricalNoArcCorr); + %shared_ptr(stir::ProjData); %shared_ptr(stir::ProjDataFromStream); %shared_ptr(stir::ProjDataInterfile); @@ -1320,15 +1328,46 @@ namespace stir { %newobject stir::ProjDataInfo::ProjDataInfoGE; %newobject stir::ProjDataInfo::ProjDataInfoCTI; - -// ignore this to avoid problems with unique_ptr, and add it later +// ignore this to avoid problems with unique_ptr %ignore stir::ProjDataInfo::construct_proj_data_info; +// make sure we can use the new name anyway (although this removes +// ProjDataInfoCTI from the target language) +// See also the %extend trick below which currently doesn't work +%rename(construct_proj_data_info) ProjDataInfoCTI; + +%factory_shared(stir::ProjDataInfo*, + stir::ProjDataInfoCylindricalNoArcCorr, + stir::ProjDataInfoCylindricalArcCorr, + stir::ProjDataInfoBlocksOnCylindricalNoArcCorr, + stir::ProjDataInfoGenericNoArcCorr); +%factory_shared(stir::ProjDataInfo const*, + stir::ProjDataInfoCylindricalNoArcCorr, + stir::ProjDataInfoCylindricalArcCorr, + stir::ProjDataInfoBlocksOnCylindricalNoArcCorr, + stir::ProjDataInfoGenericNoArcCorr); %include "stir/ProjDataInfo.h" -%newobject *::construct_proj_data_info; +%include "stir/ProjDataInfoCylindrical.h" +%include "stir/ProjDataInfoCylindricalArcCorr.h" +%include "stir/ProjDataInfoCylindricalNoArcCorr.h" +%include "stir/ProjDataInfoGeneric.h" +%include "stir/ProjDataInfoGenericNoArcCorr.h" +%include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" + +%include "stir/Viewgram.h" +%include "stir/RelatedViewgrams.h" +%include "stir/Sinogram.h" +%include "stir/Segment.h" +%include "stir/SegmentByView.h" +%include "stir/SegmentBySinogram.h" + +%include "stir/ProjData.h" + +#if 0 %extend stir::ProjDataInfo { + // TODO this does not work due to %ignore statement above // work around the current SWIG limitation that it doesn't wrap unique_ptr. // we do this with the crazy (and ugly) way to let SWIG create a new function // which is the same as the original, but returns a bare pointer. @@ -1347,18 +1386,7 @@ namespace stir { arc_corrected).get(); } } -%include "stir/ProjDataInfoCylindrical.h" -%include "stir/ProjDataInfoCylindricalArcCorr.h" -%include "stir/ProjDataInfoCylindricalNoArcCorr.h" - -%include "stir/Viewgram.h" -%include "stir/RelatedViewgrams.h" -%include "stir/Sinogram.h" -%include "stir/Segment.h" -%include "stir/SegmentByView.h" -%include "stir/SegmentBySinogram.h" - -%include "stir/ProjData.h" +#endif namespace stir { %extend ProjData diff --git a/src/swig/test/python/test_buildblock.py b/src/swig/test/python/test_buildblock.py index 3056fcd57..b5bb8f548 100755 --- a/src/swig/test/python/test_buildblock.py +++ b/src/swig/test/python/test_buildblock.py @@ -221,20 +221,28 @@ def test_ProjDataInfo(): # const int span, const int max_delta, # const int num_views, const int num_tangential_poss, # - projdatainfo=ProjDataInfo.ProjDataInfoCTI(s,3,9,8,6) - #print projdatainfo + projdatainfo=ProjDataInfo.construct_proj_data_info(s,3,9,8,6) + #print( projdatainfo) assert projdatainfo.get_scanner().get_num_rings()==32 + # use arc-correction specific keywords + projdatainfo.set_tangential_sampling(5) # dangerous of course, but just for the test + assert projdatainfo.get_tangential_sampling() == 5 + # extract sinogram sinogram=projdatainfo.get_empty_sinogram(1,2) assert sinogram.sum()==0 assert sinogram.get_segment_num()==2 assert sinogram.get_axial_pos_num()==1 assert sinogram.get_num_views() == projdatainfo.get_num_views() - assert sinogram.get_proj_data_info() == projdatainfo + print(sinogram.get_proj_data_info()) + # TODO currently does not work due to TypeError + #assert sinogram.get_proj_data_info() == projdatainfo + assert sinogram.get_proj_data_info().parameter_info() == projdatainfo.parameter_info() + def test_ProjData_from_to_Array3D(): # define a projection with some dummy data (filled with segment no.) s=Scanner.get_scanner_from_name("ECAT 962") - projdatainfo=ProjDataInfo.ProjDataInfoCTI(s,3,9,8,6) + projdatainfo=ProjDataInfo.construct_proj_data_info(s,3,9,8,6) projdata=ProjDataInMemory(ExamInfo(),projdatainfo) for seg_idx in range(projdata.get_min_segment_num(),projdata.get_max_segment_num()+1): segment=projdata.get_empty_segment_by_sinogram(seg_idx) From df9b2fadce5ec114ae8bd6424d7cdf2e52d411c0 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Fri, 3 Dec 2021 10:37:24 +0000 Subject: [PATCH 41/81] [SWIG] ProjDataInfo changes - add generic/block proj_data_info - rename ProjDataInfo.ProjDataInfoCTI to ProjDataInfo.construct_proj_data_info, which now returns an object of the derived class. (Warning: the renaming breaks backwards compatibility) [ci skip] --- documentation/release_5.0.htm | 6 ++ src/swig/factory_shared.i | 95 +++++++++++++++++++++++++ src/swig/stir.i | 60 +++++++++++----- src/swig/test/matlab/test_IO.m | 4 +- src/swig/test/matlab/test_buildblock.m | 6 +- src/swig/test/python/test_IO.py | 9 ++- src/swig/test/python/test_buildblock.py | 18 +++-- src/swig/test/python/test_numpy.py | 4 +- 8 files changed, 171 insertions(+), 31 deletions(-) create mode 100644 src/swig/factory_shared.i diff --git a/documentation/release_5.0.htm b/documentation/release_5.0.htm index c329704ab..53c603134 100644 --- a/documentation/release_5.0.htm +++ b/documentation/release_5.0.htm @@ -69,9 +69,15 @@

      Changes breaking backwards compatibility from a user-perspective

      STIR usages of these methods (OSSPS and SqrtHessianRowSum) have been updated to see no effective change in functionality. However, calling the Hessian vector product methods, via python etc., will result in a change in functionality as the sign of the output voxel values has changed.
    • + +
    • Python/MATLAB wrappers no longer have ProjDataInfo::ProjDataInfoCTI, use ProjDataInfo::construct_proj_data_info instead. + However, this now returns an object of the appropriate (derived) type as opposed to just + ProjDataInfo. This should be transparant, except apparently for testing equality of objects. +
    +

    Bug fixes

    • diff --git a/src/swig/factory_shared.i b/src/swig/factory_shared.i new file mode 100644 index 000000000..d44d54018 --- /dev/null +++ b/src/swig/factory_shared.i @@ -0,0 +1,95 @@ +/* A work-around by Kris Thielemans to be able to use %factory with %shared + See https://github.com/swig/swig/issues/2110 + + To avoid conflicts, the macro is called %factory_shared +*/ +/* + Implement a more natural wrap for factory methods, for example, if + you have: + + ---- geometry.h -------- + struct Geometry { + enum GeomType{ + POINT, + CIRCLE + }; + + virtual ~Geometry() {} + virtual int draw() = 0; + + // + // Factory method for all the Geometry objects + // + static Geometry *create(GeomType i); + }; + + struct Point : Geometry { + int draw() { return 1; } + double width() { return 1.0; } + }; + + struct Circle : Geometry { + int draw() { return 2; } + double radius() { return 1.5; } + }; + + // + // Factory method for all the Geometry objects + // + Geometry *Geometry::create(GeomType type) { + switch (type) { + case POINT: return new Point(); + case CIRCLE: return new Circle(); + default: return 0; + } + } + ---- geometry.h -------- + + + You can use the %factory with the Geometry::create method as follows: + + %newobject Geometry::create; + %factory(Geometry *Geometry::create, Point, Circle); + %include "geometry.h" + + and Geometry::create will return a 'Point' or 'Circle' instance + instead of the plain 'Geometry' type. For example, in python: + + circle = Geometry.create(Geometry.CIRCLE) + r = circle.radius() + + where circle is a Circle proxy instance. + + NOTES: remember to fully qualify all the type names and don't + use %factory inside a namespace declaration, ie, instead of + + namespace Foo { + %factory(Geometry *Geometry::create, Point, Circle); + } + + use + + %factory(Foo::Geometry *Foo::Geometry::create, Foo::Point, Foo::Circle); + + +*/ + +%define %_factory_dispatch_shared(Type) +if (!dcast) { + Type *dobj = dynamic_cast($1); + if (dobj) { + dcast = 1; + SWIG_SHARED_PTR_QNAMESPACE::shared_ptr< Type > *smartresult = new SWIG_SHARED_PTR_QNAMESPACE::shared_ptr< Type >(dobj); + %set_output(SWIG_NewPointerObj(%as_voidptr(smartresult),$descriptor(SWIG_SHARED_PTR_QNAMESPACE::shared_ptr *), $owner | %newpointer_flags)); + } +}%enddef + +%define %factory_shared(Method,Types...) +%typemap(out) Method { + int dcast = 0; + %formacro(%_factory_dispatch_shared, Types) + if (!dcast) { + %set_output(SWIG_NewPointerObj(%as_voidptr($1),$descriptor, $owner | %newpointer_flags)); + } +}%enddef + diff --git a/src/swig/stir.i b/src/swig/stir.i index 5a1247144..0417e189d 100644 --- a/src/swig/stir.i +++ b/src/swig/stir.i @@ -1,6 +1,6 @@ /* Copyright (C) 2011-07-01 - 2012, Kris Thielemans - Copyright (C) 2013, 2018, 2020 University College London + Copyright (C) 2013, 2018, 2020, 2021 University College London This file is part of STIR. SPDX-License-Identifier: Apache-2.0 @@ -40,6 +40,9 @@ #include "stir/Bin.h" #include "stir/ProjDataInfoCylindricalArcCorr.h" #include "stir/ProjDataInfoCylindricalNoArcCorr.h" + #include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" + #include "stir/ProjDataInfoGenericNoArcCorr.h" + #include "stir/Viewgram.h" #include "stir/RelatedViewgrams.h" #include "stir/Sinogram.h" @@ -475,6 +478,7 @@ #endif %include "attribute.i" +%include "factory_shared.i" %init %{ #if defined(SWIGPYTHON) @@ -801,6 +805,10 @@ namespace std { %shared_ptr(stir::ProjDataInfoCylindrical); %shared_ptr(stir::ProjDataInfoCylindricalArcCorr); %shared_ptr(stir::ProjDataInfoCylindricalNoArcCorr); +%shared_ptr(stir::ProjDataInfoGeneric); +%shared_ptr(stir::ProjDataInfoGenericNoArcCorr); +%shared_ptr(stir::ProjDataInfoBlocksOnCylindricalNoArcCorr); + %shared_ptr(stir::ProjData); %shared_ptr(stir::ProjDataFromStream); %shared_ptr(stir::ProjDataInterfile); @@ -1320,15 +1328,46 @@ namespace stir { %newobject stir::ProjDataInfo::ProjDataInfoGE; %newobject stir::ProjDataInfo::ProjDataInfoCTI; - -// ignore this to avoid problems with unique_ptr, and add it later +// ignore this to avoid problems with unique_ptr %ignore stir::ProjDataInfo::construct_proj_data_info; +// make sure we can use the new name anyway (although this removes +// ProjDataInfoCTI from the target language) +// See also the %extend trick below which currently doesn't work +%rename(construct_proj_data_info) ProjDataInfoCTI; + +%factory_shared(stir::ProjDataInfo*, + stir::ProjDataInfoCylindricalNoArcCorr, + stir::ProjDataInfoCylindricalArcCorr, + stir::ProjDataInfoBlocksOnCylindricalNoArcCorr, + stir::ProjDataInfoGenericNoArcCorr); +%factory_shared(stir::ProjDataInfo const*, + stir::ProjDataInfoCylindricalNoArcCorr, + stir::ProjDataInfoCylindricalArcCorr, + stir::ProjDataInfoBlocksOnCylindricalNoArcCorr, + stir::ProjDataInfoGenericNoArcCorr); %include "stir/ProjDataInfo.h" -%newobject *::construct_proj_data_info; +%include "stir/ProjDataInfoCylindrical.h" +%include "stir/ProjDataInfoCylindricalArcCorr.h" +%include "stir/ProjDataInfoCylindricalNoArcCorr.h" +%include "stir/ProjDataInfoGeneric.h" +%include "stir/ProjDataInfoGenericNoArcCorr.h" +%include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" + +%include "stir/Viewgram.h" +%include "stir/RelatedViewgrams.h" +%include "stir/Sinogram.h" +%include "stir/Segment.h" +%include "stir/SegmentByView.h" +%include "stir/SegmentBySinogram.h" + +%include "stir/ProjData.h" + +#if 0 %extend stir::ProjDataInfo { + // TODO this does not work due to %ignore statement above // work around the current SWIG limitation that it doesn't wrap unique_ptr. // we do this with the crazy (and ugly) way to let SWIG create a new function // which is the same as the original, but returns a bare pointer. @@ -1347,18 +1386,7 @@ namespace stir { arc_corrected).get(); } } -%include "stir/ProjDataInfoCylindrical.h" -%include "stir/ProjDataInfoCylindricalArcCorr.h" -%include "stir/ProjDataInfoCylindricalNoArcCorr.h" - -%include "stir/Viewgram.h" -%include "stir/RelatedViewgrams.h" -%include "stir/Sinogram.h" -%include "stir/Segment.h" -%include "stir/SegmentByView.h" -%include "stir/SegmentBySinogram.h" - -%include "stir/ProjData.h" +#endif namespace stir { %extend ProjData diff --git a/src/swig/test/matlab/test_IO.m b/src/swig/test/matlab/test_IO.m index 84aace905..125dd27b2 100644 --- a/src/swig/test/matlab/test_IO.m +++ b/src/swig/test/matlab/test_IO.m @@ -39,12 +39,12 @@ % tmpdir.chdir() fprintf('Creating files in %s\n', pwd()) s=Scanner.get_scanner_from_name('ECAT 962'); - %ProjDataInfoCTI(const shared_ptr& scanner_ptr, + %construct_proj_data_info(const shared_ptr& scanner_ptr, % const int span, const int max_delta, % const int num_views, const int num_tangential_poss, % examinfo=ExamInfo(); - projdatainfo=ProjDataInfo.ProjDataInfoCTI(s,3,6,8,6); + projdatainfo=ProjDataInfo.construct_proj_data_info(s,3,6,8,6); assert (projdatainfo.get_scanner().get_num_rings()==32) projdata=ProjDataInterfile(examinfo, projdatainfo, 'stir_matlab_test.hs'); assert (projdata.get_min_segment_num()==-1) diff --git a/src/swig/test/matlab/test_buildblock.m b/src/swig/test/matlab/test_buildblock.m index c1bbb8ac5..79b43a1b9 100644 --- a/src/swig/test/matlab/test_buildblock.m +++ b/src/swig/test/matlab/test_buildblock.m @@ -144,9 +144,9 @@ assert (s.get_num_detectors_per_ring()==576) %% tests on ProjDataInfo % this doesn't work (no conversion), but probably rightly so -% projdatainfo=stir.ProjDataInfoCylindricalNoArcCorr (stir.ProjDataInfo.ProjDataInfoCTI(s,3,3,8,6)) +% projdatainfo=stir.ProjDataInfoCylindricalNoArcCorr (stir.ProjDataInfo.construct_proj_data_info(s,3,3,8,6)) s=stir.Scanner(stir.Scanner.E962()); -projdatainfo=stir.ProjDataInfo.ProjDataInfoCTI(s,3,9,8,6); +projdatainfo=stir.ProjDataInfo.construct_proj_data_info(s,3,9,8,6); %print projdatainfo assert( projdatainfo.get_scanner().get_num_rings()==32) sinogram=projdatainfo.get_empty_sinogram(1,2); @@ -168,7 +168,7 @@ success=stir.Succeeded(stir.Succeeded.yes()); s=stir.Scanner(stir.Scanner.E962()); -proj_data_info=stir.ProjDataInfo.ProjDataInfoCTI(s,3,9,8,6); +proj_data_info=stir.ProjDataInfo.construct_proj_data_info(s,3,9,8,6); proj_data=stir.ProjDataInMemory(stir.ExamInfo(), proj_data_info); seg=proj_data.get_segment_by_sinogram(0); diff --git a/src/swig/test/python/test_IO.py b/src/swig/test/python/test_IO.py index f0572856a..2aa490210 100755 --- a/src/swig/test/python/test_IO.py +++ b/src/swig/test/python/test_IO.py @@ -14,6 +14,7 @@ from stir import * from stirextra import * import os +import py.path # for Python2 and itertools.zip->zip (as in Python 3) try: import itertools.izip as zip @@ -44,12 +45,12 @@ def test_ProjDataInfo(tmpdir): tmpdir.chdir() print("Creating files in ", os.getcwd()) s=Scanner.get_scanner_from_name("ECAT 962") - #ProjDataInfoCTI(const shared_ptr& scanner_ptr, + #construct_proj_data_info(const shared_ptr& scanner_ptr, # const int span, const int max_delta, # const int num_views, const int num_tangential_poss, # examinfo=ExamInfo(); - projdatainfo=ProjDataInfo.ProjDataInfoCTI(s,3,6,8,6) + projdatainfo=ProjDataInfo.construct_proj_data_info(s,3,6,8,6) assert projdatainfo.get_scanner().get_num_rings()==32 projdata=ProjDataInterfile(examinfo, projdatainfo, "stir_python_test.hs") print(projdata.get_min_segment_num()) @@ -62,7 +63,8 @@ def test_ProjDataInfo(tmpdir): del projdata projdata2=ProjData.read_from_file('stir_python_test.hs'); - assert projdatainfo==projdata2.get_proj_data_info() + # work-around current problem due to type mismatch + assert projdatainfo.parameter_info()==projdata2.get_proj_data_info().parameter_info() for seg in range(projdata2.get_min_segment_num(), projdata2.get_max_segment_num()+1): # construct same segment data as above (TODO: better to stick it into a list or so) segment=projdatainfo.get_empty_segment_by_view(seg) @@ -73,3 +75,4 @@ def test_ProjDataInfo(tmpdir): for i1,i2 in zip(segment.flat(), segment2.flat()): assert abs(i1-i2)<.01 +#test_ProjDataInfo(py.path.local('.') diff --git a/src/swig/test/python/test_buildblock.py b/src/swig/test/python/test_buildblock.py index 3056fcd57..612c1cf3f 100755 --- a/src/swig/test/python/test_buildblock.py +++ b/src/swig/test/python/test_buildblock.py @@ -217,24 +217,32 @@ def test_Bin(): def test_ProjDataInfo(): s=Scanner.get_scanner_from_name("ECAT 962") - #ProjDataInfoCTI(const shared_ptr& scanner_ptr, + #construct_proj_data_info(const shared_ptr& scanner_ptr, # const int span, const int max_delta, # const int num_views, const int num_tangential_poss, # - projdatainfo=ProjDataInfo.ProjDataInfoCTI(s,3,9,8,6) - #print projdatainfo + projdatainfo=ProjDataInfo.construct_proj_data_info(s,3,9,8,6) + #print( projdatainfo) assert projdatainfo.get_scanner().get_num_rings()==32 + # use arc-correction specific keywords + projdatainfo.set_tangential_sampling(5) # dangerous of course, but just for the test + assert projdatainfo.get_tangential_sampling() == 5 + # extract sinogram sinogram=projdatainfo.get_empty_sinogram(1,2) assert sinogram.sum()==0 assert sinogram.get_segment_num()==2 assert sinogram.get_axial_pos_num()==1 assert sinogram.get_num_views() == projdatainfo.get_num_views() - assert sinogram.get_proj_data_info() == projdatainfo + print(sinogram.get_proj_data_info()) + # TODO currently does not work due to TypeError + #assert sinogram.get_proj_data_info() == projdatainfo + assert sinogram.get_proj_data_info().parameter_info() == projdatainfo.parameter_info() + def test_ProjData_from_to_Array3D(): # define a projection with some dummy data (filled with segment no.) s=Scanner.get_scanner_from_name("ECAT 962") - projdatainfo=ProjDataInfo.ProjDataInfoCTI(s,3,9,8,6) + projdatainfo=ProjDataInfo.construct_proj_data_info(s,3,9,8,6) projdata=ProjDataInMemory(ExamInfo(),projdatainfo) for seg_idx in range(projdata.get_min_segment_num(),projdata.get_max_segment_num()+1): segment=projdata.get_empty_segment_by_sinogram(seg_idx) diff --git a/src/swig/test/python/test_numpy.py b/src/swig/test/python/test_numpy.py index 7dbd43e11..fe0e0ecb8 100755 --- a/src/swig/test/python/test_numpy.py +++ b/src/swig/test/python/test_numpy.py @@ -66,11 +66,11 @@ def test_Array3Diterator(): def test_ProjData(): s=Scanner.get_scanner_from_name("ECAT 962") - #ProjDataInfoCTI(const shared_ptr& scanner_ptr, + #construct_proj_data_info(const shared_ptr& scanner_ptr, # const int span, const int max_delta, # const int num_views, const int num_tangential_poss, # - projdatainfo=ProjDataInfo.ProjDataInfoCTI(s,3,9,8,6) + projdatainfo=ProjDataInfo.construct_proj_data_info(s,3,9,8,6) #print projdatainfo projdata=ProjDataInMemory(ExamInfo(), projdatainfo) np=stirextra.to_numpy(projdata) From 4c5027a5f380bfe6ccc391a805ed34da1810ce25 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Fri, 3 Dec 2021 13:04:34 +0000 Subject: [PATCH 42/81] remove scanner orientation from Interfile and parameter_info --- src/IO/InterfileHeader.cxx | 5 ----- src/buildblock/Scanner.cxx | 8 +------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/IO/InterfileHeader.cxx b/src/IO/InterfileHeader.cxx index 94925dd08..d34b7694d 100644 --- a/src/IO/InterfileHeader.cxx +++ b/src/IO/InterfileHeader.cxx @@ -618,11 +618,6 @@ InterfilePDFSHeader::InterfilePDFSHeader() add_key("Reference energy (in keV)", &reference_energy); - // new keys for block geometry - scanner_orientation = "X"; - add_key("Scanner orientation (X or Y)", - KeyArgument::ASCII, &scanner_orientation); - scanner_geometry = "Cylindrical"; add_key("Scanner geometry (BlocksOnCylindrical/Cylindrical/Generic)", KeyArgument::ASCII, &scanner_geometry); diff --git a/src/buildblock/Scanner.cxx b/src/buildblock/Scanner.cxx index 2aff98e40..ffdb5104e 100644 --- a/src/buildblock/Scanner.cxx +++ b/src/buildblock/Scanner.cxx @@ -1208,11 +1208,6 @@ Scanner::parameter_info() const s << "Scanner geometry (BlocksOnCylindrical/Cylindrical/Generic) := " <); %shared_ptr(stir::Sinogram); %shared_ptr(stir::Viewgram); +%shared_ptr(stir::LORAs2Points); +%shared_ptr(stir::LOR); +%shared_ptr(stir::LORInAxialAndNoArcCorrSinogramCoordinates); #else namespace boost { template class shared_ptr @@ -881,6 +885,9 @@ T * operator-> () const; %include "stir/BasicCoordinate.h" %include "stir/Coordinate3D.h" +%include "stir/LORCoordinates.h" + +%template(FloatLOR) stir::LOR; // ignore non-const versions %ignore stir::CartesianCoordinate3D::z(); %ignore stir::CartesianCoordinate3D::y(); @@ -1103,6 +1110,7 @@ namespace stir { %ADD_indexaccess(int,T,VectorWithOffset); %template(FloatVectorWithOffset) VectorWithOffset; + %template(IntVectorWithOffset) VectorWithOffset; // TODO need to instantiate with name? %template (FloatNumericVectorWithOffset) NumericVectorWithOffset; @@ -1221,7 +1229,7 @@ namespace stir { // then setitem still doesn't modify the object for more than 1 level #if 1 // note: next line has no memory allocation problems because all Array<1,...> objects - // are auto-converted to shared_ptrs. + // are auto-converted to _ptrs. // however, cannot use setitem to modify so ideally we would define getitem only (at least for python) (TODO) // TODO DISABLE THIS %ADD_indexaccess(int,%arg(Array<1,float>),%arg(Array<2,float>)); @@ -1355,6 +1363,39 @@ namespace stir { %include "stir/ProjDataInfoGenericNoArcCorr.h" %include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" +%extend stir::ProjDataInfoBlocksOnCylindricalNoArcCorr { + +stir::LORInAxialAndNoArcCorrSinogramCoordinates get_lor(const Bin bin){ + stir::LORInAxialAndNoArcCorrSinogramCoordinates lor; + $self->get_LOR(lor,bin); + return lor; +} + +stir::CartesianCoordinate3D + find_cartesian_coordinate_of_detection_1(const Bin bin) const +{ + CartesianCoordinate3D coord_1; + CartesianCoordinate3D coord_2; + $self->find_cartesian_coordinates_of_detection(coord_1, + coord_2, + bin); + + return coord_1; +} + +stir::CartesianCoordinate3D + find_cartesian_coordinate_of_detection_2(const Bin bin) const +{ + CartesianCoordinate3D coord_1; + CartesianCoordinate3D coord_2; + $self->find_cartesian_coordinates_of_detection(coord_1, + coord_2, + bin); + + return coord_2; +} +} + %include "stir/Viewgram.h" %include "stir/RelatedViewgrams.h" %include "stir/Sinogram.h" From 0212f0aeb522f474070a7bff1ba26b374faae9a4 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Wed, 8 Dec 2021 15:24:06 +0000 Subject: [PATCH 44/81] split Scanner:read_detectormap_from_file splitted it into reading and setting such that we can set the coord_map in different ways in the future --- src/buildblock/Scanner.cxx | 27 +++++++++++++++++++++------ src/include/stir/Scanner.h | 11 +++++++++-- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/buildblock/Scanner.cxx b/src/buildblock/Scanner.cxx index 2aff98e40..be01e3200 100644 --- a/src/buildblock/Scanner.cxx +++ b/src/buildblock/Scanner.cxx @@ -769,15 +769,14 @@ set_params(Type type_v,const list& list_of_names_v, } // creates maps to convert between stir and 3d coordinates -void +Scanner::det_pos_to_coord_type Scanner:: -read_detectormap_from_file( const std::string& crystal_map_name ) +read_detectormap_from_file_help( const std::string& crystal_map_name ) { std::ifstream crystal_map_file(crystal_map_name.c_str()); if( !crystal_map_file ) { - std::cerr << "Error opening file " << crystal_map_name << "." << std::endl; - return; + error("Error opening file '" + crystal_map_name + "'"); } std::string line; //map containing the crystal map from the input file (safir -> coords) @@ -805,8 +804,14 @@ read_detectormap_from_file( const std::string& crystal_map_name ) coord_map[detpos] = coord; } + return coord_map; +} - // The detector crystal coordinates are now saved in coord_map the following way: +void +Scanner:: +set_detector_map( const Scanner::det_pos_to_coord_type& coord_map ) +{ + // The detector crystal coordinates are saved in coord_map the following way: // (detector#, ring#, 1)[(x,y,z)] // the detector# and ring# are determined outside of STIR (later given in input) // In order to fulfill the STIR convention we have to give the coordinates @@ -837,7 +842,7 @@ read_detectormap_from_file( const std::string& crystal_map_name ) detpos.tangential_coord() = det; detpos.radial_coord() = 0; input_index_to_stir_index[map_for_sorting_coordinates[*it]] = detpos; - stir_index_to_coord[detpos] = coord_map[map_for_sorting_coordinates[*it]]; + stir_index_to_coord[detpos] = coord_map.at(map_for_sorting_coordinates[*it]); det++; if (det == num_detectors_per_ring) { @@ -851,6 +856,16 @@ read_detectormap_from_file( const std::string& crystal_map_name ) } } +// creates maps to convert between stir and 3d coordinates +void +Scanner:: +read_detectormap_from_file( const std::string& crystal_map_name ) +{ + det_pos_to_coord_type coord_map = + read_detectormap_from_file_help(crystal_map_file_name); + set_detector_map(coord_map); +} + /*! \todo The current list is bound to be incomplete. would be better to stick it in set_params(). */ int diff --git a/src/include/stir/Scanner.h b/src/include/stir/Scanner.h index bbc2974e0..3d592568c 100644 --- a/src/include/stir/Scanner.h +++ b/src/include/stir/Scanner.h @@ -492,10 +492,17 @@ class Scanner return seed; } }; - boost::unordered_map, stir::DetectionPosition<>, ihash> input_index_to_stir_index; - boost::unordered_map, stir::CartesianCoordinate3D, ihash> stir_index_to_coord; + + typedef boost::unordered_map, stir::CartesianCoordinate3D, ihash> det_pos_to_coord_type; + typedef boost::unordered_map, stir::DetectionPosition<>, ihash> unordered_to_ordered_det_pos_type; + unordered_to_ordered_det_pos_type input_index_to_stir_index; + det_pos_to_coord_type stir_index_to_coord; std::string crystal_map_file_name; + static det_pos_to_coord_type + read_detectormap_from_file_help( const std::string& crystal_map_name ); + void set_detector_map( const det_pos_to_coord_type& coord_map ); + // function to create the maps void read_detectormap_from_file( const std::string& filename ); From 5d5e68df5e5daef71adb7a369db6a2a0c369ad72 Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Wed, 8 Dec 2021 17:24:38 +0000 Subject: [PATCH 45/81] update plot_scanner_LOR with example of plot from LORcoordinate; update the same to make plots with legends make :LORInAxialAndNoArcCorrSinogramCoordinates public for SWIG --- examples/python/plot_scanner_LORs.py | 102 +++++++++++++++++++++++---- src/include/stir/LORCoordinates.h | 4 ++ src/swig/stir.i | 1 + 3 files changed, 92 insertions(+), 15 deletions(-) diff --git a/examples/python/plot_scanner_LORs.py b/examples/python/plot_scanner_LORs.py index d95666db4..b8df180b8 100644 --- a/examples/python/plot_scanner_LORs.py +++ b/examples/python/plot_scanner_LORs.py @@ -32,10 +32,12 @@ import os import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D - +import matplotlib.cm as cm #%% definition of useful objects and variables scanner=stir.Scanner_get_scanner_from_name('SAFIRDualRingPrototype') +scanner.set_num_transaxial_blocks_per_bucket(2) +# scanner.set_intrinsic_azimuthal_tilt(45) # scanner.set_num_axial_crystals_per_block(1) # scanner.set_axial_block_spacing(scanner.get_axial_crystal_spacing()*scanner.get_num_axial_crystals_per_block()); # scanner.set_num_rings(1) @@ -52,6 +54,8 @@ NtCpBl=scanner.get_num_transaxial_crystals_per_block() NtBlpBu=scanner.get_num_transaxial_blocks_per_bucket() +NaCpBl=scanner.get_num_axial_crystals_per_block() +NaBlpBu=scanner.get_num_axial_blocks_per_bucket() NCpR=scanner.get_num_detectors_per_ring() min_r_diff=stir.IntVectorWithOffset((2*Nr-1)) @@ -81,34 +85,61 @@ #%% Plot 2D XY LORs for segment, axial position and tangential position =0 b1=stir.FloatCartesianCoordinate3D; b2=stir.FloatCartesianCoordinate3D; -lor=stir.FloatLOR; +lor=stir.FloatLORInAxialAndNoArcCorrSinogramCoordinates; -fig=plt.figure(figsize=(12, 12)) +fig=plt.figure() ax=plt.axes() plt.xlim([-rmax, rmax]) plt.ylim([-rmax, rmax]) ax.set_xlabel('X ') ax.set_ylabel('Y ') +color_v = iter(cm.tab20(numpy.linspace(0, 10, NCpR))) +c=next(color_v) +tB_num2=-1 + for v in range(0, Nv, 5): + tB_nim_i, tB_num_f=divmod(v/NtCpBl,1) + tB_num=int(tB_nim_i) bin=stir.Bin(0,v,0,0) + + if tB_num>tB_num2: + c=next(color_v) + + tB_num2=tB_num b1=proj_data_info_blocks.find_cartesian_coordinate_of_detection_1(bin) b2=proj_data_info_blocks.find_cartesian_coordinate_of_detection_2(bin) - plt.plot((b1.x(), b2.x()),(b1.y(), b2.y())) + + plt.plot((b1.x(), b2.x()),(b1.y(), b2.y()),color=c) + plt.plot(b1.x(),b1.y(),'o',color=c, label="Block %s - det %s" % (tB_num, v)) + + # Shrink current axis % + box = ax.get_position() + ax.set_position([box.x0, box.y0+box.y0*0.01, box.width * .985, box.height]) + plt.legend(loc='best',bbox_to_anchor=(1., 1.),fancybox=True) # plt.show() #if debugging we can se how the LORs are order + +plt.savefig('2D-2BlocksPerBuckets-XY-LOR.png', format='png', dpi=300) plt.show() -plt.savefig('2D-XY-LOR.png', format='png', dpi=300) +plt.close(); -plt.close() +# # %% the following is an example when using LOR coordinates +# fig=plt.figure(figsize=(12, 12)) +# ax=plt.axes() +# plt.xlim([-rmax, rmax]) +# plt.ylim([-rmax, rmax]) +# ax.set_xlabel('X ') +# ax.set_ylabel('Y ') # for v in range(0, Nv, 5): # bin=stir.Bin(0,v,0,0) # lor=proj_data_info_blocks.get_lor(bin) # phi=lor.phi(); -# plt.plot((b1.x(), b2.x()),(b1.y(), b2.y())) -# # plt.show() +# r=lor.radius() +# plt.plot((r*math.sin(phi), r*math.sin(phi+math.pi)),(-r*math.cos(phi), -r*math.cos(phi+math.pi))) +# plt.show() # plt.show() -# plt.savefig('2D-XY-LOR.png', format='png', dpi=300) +# plt.savefig('2D-XY-LOR-cyl.png', format='png', dpi=300) # plt.close() @@ -118,12 +149,34 @@ plt.ylim([-rmax, rmax]) ax.set_xlabel('Z ') ax.set_ylabel('Y ') -for a in range(0,(Nr-1), 1): + +color_a = iter(cm.tab20(numpy.linspace(0, 1, Nr))) +c=next(color_a) +aB_num2=-1 + +for a in range(0,(Nr), 1): + aB_nim_i, aB_num_f=divmod(a/NaCpBl,1) + aB_num=int(aB_nim_i) + bin=stir.Bin(0,v,0,0) + + if aB_num>aB_num2: + c=next(color_a) + + aB_num2=aB_num bin=stir.Bin((Nr-1),0,a,0) b1=proj_data_info_blocks.find_cartesian_coordinate_of_detection_1(bin) b2=proj_data_info_blocks.find_cartesian_coordinate_of_detection_2(bin) - plt.plot((b1.z(), b2.z()),(b1.y(), b2.y())) + + plt.plot((b1.z(), b2.z()),(b1.y(), b2.y()),color=c) + plt.plot(b1.z(),b1.y(),'o',color=c, label="Block %s - ring %s" % (aB_num, a)) + + # Shrink current axis + box = ax.get_position() + ax.set_position([box.x0-box.x0*0.01, box.y0+box.y0*0.01, box.width * .985, box.height]) + plt.legend(loc='best',bbox_to_anchor=(1., 1.),fancybox=True) + # plt.show() + plt.savefig('2D-YZ-LOR.png', format='png', dpi=300) plt.show() plt.close() @@ -137,12 +190,31 @@ ax.set_xlabel('X ') ax.set_ylabel('Y ') ax.set_zlabel('Z ') -for a in range(0,(Nr-1), 1): - for v in range(0, Nv, 15): +color_a = iter(cm.tab20(numpy.linspace(0, 1, Nr))) +c=next(color_a) +aB_num2=-1 + +for a in range(0,(Nr), 4): + for v in range(0, Nv, 30): + # aB_nim_i, aB_num_f=divmod(a/NaCpBl,1) + # aB_num=int(aB_nim_i) bin=stir.Bin((Nr-1),v,a,0) + + # if aB_num>aB_num2: + c=next(color_a) + + # aB_num2=aB_num b1=proj_data_info_blocks.find_cartesian_coordinate_of_detection_1(bin) b2=proj_data_info_blocks.find_cartesian_coordinate_of_detection_2(bin) - plt.plot((b1.x(), b2.x()),(b1.y(), b2.y()),(b1.z(), b2.z())) - plt.show() + plt.plot((b1.x(), b2.x()),(b1.y(), b2.y()),(b1.z(), b2.z()),color=c) + ax.scatter3D(b1.x(),b1.y(),b1.z(),'o',color=c, label="ring %s - detector %s" % (a, v)) + + # Shrink current axis by 1.5% + box = ax.get_position() + ax.set_position([box.x0-box.x0*0.12, box.y0+box.y0*0.01, box.width * .985, box.height]) + + plt.legend(loc='best',bbox_to_anchor=(1., 1.),fancybox=True) + # plt.show() plt.savefig('3dLOR.png', format='png', dpi=300) +plt.show() \ No newline at end of file diff --git a/src/include/stir/LORCoordinates.h b/src/include/stir/LORCoordinates.h index b6d9706b8..bd2760edf 100644 --- a/src/include/stir/LORCoordinates.h +++ b/src/include/stir/LORCoordinates.h @@ -433,6 +433,10 @@ class LORInAxialAndNoArcCorrSinogramCoordinates : public LOR, private LORCylindricalCoordinates_z_and_radius { private: +#ifdef SWIG + // SWIG needs this typedef to be public + public: +#endif typedef LORInAxialAndNoArcCorrSinogramCoordinates self_type; typedef LORCylindricalCoordinates_z_and_radius private_base_type; diff --git a/src/swig/stir.i b/src/swig/stir.i index 067f74cd8..52c606a3c 100644 --- a/src/swig/stir.i +++ b/src/swig/stir.i @@ -888,6 +888,7 @@ T * operator-> () const; %include "stir/LORCoordinates.h" %template(FloatLOR) stir::LOR; +%template(FloatLORInAxialAndNoArcCorrSinogramCoordinates) stir::LORInAxialAndNoArcCorrSinogramCoordinates; // ignore non-const versions %ignore stir::CartesianCoordinate3D::z(); %ignore stir::CartesianCoordinate3D::y(); From 4de493fb40e25d06a45946dfae33bb5343a06b8c Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Thu, 9 Dec 2021 17:35:16 +0000 Subject: [PATCH 46/81] move listmode/DetectorCoordinateMapFromFile to buildblock/DetectorCoordinateMap This class is currently only used by the SAFIR listmode code, but this prepares us for usage in Scanner (where the code was duplicated for the Generic case) --- documentation/release_5.0.htm | 3 ++- src/buildblock/CMakeLists.txt | 1 + .../DetectorCoordinateMap.cxx} | 27 +++++++++---------- ...eMapFromFile.h => DetectorCoordinateMap.h} | 26 +++++++----------- .../stir/IO/SAFIRCListmodeInputFileFormat.h | 2 +- .../stir/listmode/CListModeDataSAFIR.h | 4 +-- src/include/stir/listmode/CListRecordSAFIR.h | 10 +++---- .../CListModeDataSAFIR.cxx | 2 +- src/listmode_buildblock/CMakeLists.txt | 1 - 9 files changed, 34 insertions(+), 42 deletions(-) rename src/{listmode_buildblock/DetectorCoordinateMapFromFile.cxx => buildblock/DetectorCoordinateMap.cxx} (60%) rename src/include/stir/{listmode/DetectorCoordinateMapFromFile.h => DetectorCoordinateMap.h} (68%) diff --git a/documentation/release_5.0.htm b/documentation/release_5.0.htm index 53c603134..971ce4173 100644 --- a/documentation/release_5.0.htm +++ b/documentation/release_5.0.htm @@ -266,7 +266,8 @@

      Backward incompatibities

    • Poisson log-likelihood hierarchy has changed for gradient methods to now use actual_compute_subset_gradient_without_penalty in the derived classes (which has an extra argument add_sensitivity). Therefore developers that created their own derived class will need to adjust their class accordingly. The public class interface is identical though. -
    • + +
    • DetectorCoordinateMapFromfile (used by SAFIR listmode files) has been renamed to DetectorCoordinateMap and moved from listmode to buildblock

    New functionality

    diff --git a/src/buildblock/CMakeLists.txt b/src/buildblock/CMakeLists.txt index c1d1126a7..0e4e647f9 100644 --- a/src/buildblock/CMakeLists.txt +++ b/src/buildblock/CMakeLists.txt @@ -88,6 +88,7 @@ set(${dir_LIB_SOURCES} GeneralisedPoissonNoiseGenerator.cxx FilePath.cxx date_time_functions.cxx + DetectorCoordinateMap.cxx GeometryBlocksOnCylindrical.cxx ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx ProjDataInfoGeneric.cxx diff --git a/src/listmode_buildblock/DetectorCoordinateMapFromFile.cxx b/src/buildblock/DetectorCoordinateMap.cxx similarity index 60% rename from src/listmode_buildblock/DetectorCoordinateMapFromFile.cxx rename to src/buildblock/DetectorCoordinateMap.cxx index af06cff10..2f6a3d252 100644 --- a/src/listmode_buildblock/DetectorCoordinateMapFromFile.cxx +++ b/src/buildblock/DetectorCoordinateMap.cxx @@ -1,28 +1,27 @@ -/* DetectorCoordinateMapFromFile.cxx +/* + Copyright 2015 ETH Zurich, Institute of Particle Physics - Read List-Mode Event Data using map from file: Implementation + This file is part of STIR. - Copyright 2015 ETH Zurich, Institute of Particle Physics + SPDX-License-Identifier: Apache-2.0 - 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 + See STIR/LICENSE.txt for details +*/ +/*! - http://www.apache.org/licenses/LICENSE-2.0 + \file + \ingroup buildblock + \brief Implementation of class stir::DetectorCoordinateMap - 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. + \author Jannis Fischer */ #include "stir/error.h" -#include "stir/listmode/DetectorCoordinateMapFromFile.h" +#include "stir/DetectorCoordinateMap.h" START_NAMESPACE_STIR -void DetectorCoordinateMapFromFile::read_detectormap_from_file( const std::string& filename ) +void DetectorCoordinateMap::read_detectormap_from_file( const std::string& filename ) { std::ifstream myfile(filename.c_str()); if( !myfile ) diff --git a/src/include/stir/listmode/DetectorCoordinateMapFromFile.h b/src/include/stir/DetectorCoordinateMap.h similarity index 68% rename from src/include/stir/listmode/DetectorCoordinateMapFromFile.h rename to src/include/stir/DetectorCoordinateMap.h index ac746bddd..63f12031f 100644 --- a/src/include/stir/listmode/DetectorCoordinateMapFromFile.h +++ b/src/include/stir/DetectorCoordinateMap.h @@ -1,27 +1,19 @@ -/* DetectorCoordinateMapFromFile.h - Read List-Mode Event Data using map from file: Header File - +/* 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 + This file is part of STIR. - http://www.apache.org/licenses/LICENSE-2.0 + SPDX-License-Identifier: Apache-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. + See STIR/LICENSE.txt for details */ /*! \file \ingroup listmode - \brief Declaration of class stir::DetectorCoordinateMapFromFile + \brief Declaration of class stir::DetectorCoordinateMap \author Jannis Fischer */ @@ -36,8 +28,8 @@ #include "stir/CartesianCoordinate3D.h" #include "stir/DetectionPosition.h" -#ifndef __stir_listmode_DetectorCoordinateMapFromFile_H__ -#define __stir_listmode_DetectorCoordinateMapFromFile_H__ +#ifndef __stir_DetectorCoordinateMap_H__ +#define __stir_DetectorCoordinateMap_H__ START_NAMESPACE_STIR @@ -47,11 +39,11 @@ START_NAMESPACE_STIR ring,detector,(layer,)x,y,z An empty line will terminate the reading at that line. */ -class DetectorCoordinateMapFromFile +class DetectorCoordinateMap { public: //! Constructor calls read_detectormap_from_file( filename ). - DetectorCoordinateMapFromFile(const std::string& filename, double sigma = 0.0) : + DetectorCoordinateMap(const std::string& filename, double sigma = 0.0) : sigma(sigma), distribution(0.0, sigma) { read_detectormap_from_file( filename ); } diff --git a/src/include/stir/IO/SAFIRCListmodeInputFileFormat.h b/src/include/stir/IO/SAFIRCListmodeInputFileFormat.h index b4755ce90..ff23c886e 100644 --- a/src/include/stir/IO/SAFIRCListmodeInputFileFormat.h +++ b/src/include/stir/IO/SAFIRCListmodeInputFileFormat.h @@ -51,7 +51,7 @@ START_NAMESPACE_STIR /*! Class for reading SAFIR coincidence listmode data. It reads a parameter file, which refers to - - crystal map containing the mapping between detector index triple and cartesian coordinates of the crystal surfaces (see DetectorCoordinateMapFromFile) + - crystal map containing the mapping between detector index triple and cartesian coordinates of the crystal surfaces (see DetectorCoordinateMap) - the binary data file with the coincidence listmode data in SAFIR format (see CListModeDataSAFIR) - a template projection data file, which is used to generate the virtual cylindrical scanner diff --git a/src/include/stir/listmode/CListModeDataSAFIR.h b/src/include/stir/listmode/CListModeDataSAFIR.h index a7018c089..6cb7d0145 100644 --- a/src/include/stir/listmode/CListModeDataSAFIR.h +++ b/src/include/stir/listmode/CListModeDataSAFIR.h @@ -45,7 +45,7 @@ #include "stir/shared_ptr.h" #include "stir/listmode/CListRecordSAFIR.h" -#include "stir/listmode/DetectorCoordinateMapFromFile.h" +#include "stir/DetectorCoordinateMap.h" START_NAMESPACE_STIR @@ -90,7 +90,7 @@ template class CListModeDataSAFIR : public CListModeData mutable shared_ptr > current_lm_data_ptr; mutable std::vector< unsigned int> saved_get_positions; Succeeded open_lm_file() const; - shared_ptr map; + shared_ptr map; }; END_NAMESPACE_STIR diff --git a/src/include/stir/listmode/CListRecordSAFIR.h b/src/include/stir/listmode/CListRecordSAFIR.h index 088ee39b6..b5b729158 100644 --- a/src/include/stir/listmode/CListRecordSAFIR.h +++ b/src/include/stir/listmode/CListRecordSAFIR.h @@ -44,7 +44,7 @@ #include "boost/static_assert.hpp" #include "boost/cstdint.hpp" -#include "stir/listmode/DetectorCoordinateMapFromFile.h" +#include "stir/DetectorCoordinateMap.h" #include "boost/make_shared.hpp" START_NAMESPACE_STIR @@ -71,7 +71,7 @@ class CListEventSAFIR : public CListEvent public: /*! Constructor which initializes map upon construction. */ - inline CListEventSAFIR( shared_ptr map ) : map(map) {} + inline CListEventSAFIR( shared_ptr map ) : map(map) {} //! Returns LOR corresponding to the given event. inline virtual LORAs2Points get_LOR() const; @@ -91,14 +91,14 @@ class CListEventSAFIR : public CListEvent inline bool is_prompt() const { return !(static_cast(this)->is_prompt()); } //! Function to set map for detector indices to coordinates. - inline void set_map( shared_ptr new_map ) { map = new_map; } + inline void set_map( shared_ptr new_map ) { map = new_map; } private: friend class CListRecordSAFIR; /*! Default constructor will not work as it does not initialize a map to relate - detector indices and space coordinates. Always use other constructor with a map pointer. Or use set_map( shared_ptr new_map ) after default construction. + detector indices and space coordinates. Always use other constructor with a map pointer. Or use set_map( shared_ptr new_map ) after default construction. */ inline CListEventSAFIR( ) {} - shared_ptr map; + shared_ptr map; }; diff --git a/src/listmode_buildblock/CListModeDataSAFIR.cxx b/src/listmode_buildblock/CListModeDataSAFIR.cxx index 4baf4382a..b595348de 100644 --- a/src/listmode_buildblock/CListModeDataSAFIR.cxx +++ b/src/listmode_buildblock/CListModeDataSAFIR.cxx @@ -44,7 +44,7 @@ START_NAMESPACE_STIR; template CListModeDataSAFIR:: CListModeDataSAFIR(const std::string& listmode_filename, const std::string& crystal_map_filename, const std::string& template_proj_data_filename, const double lor_randomization_sigma) - : listmode_filename(listmode_filename), map(MAKE_SHARED(crystal_map_filename, lor_randomization_sigma)) + : listmode_filename(listmode_filename), map(MAKE_SHARED(crystal_map_filename, lor_randomization_sigma)) { this->exam_info_sptr.reset(new ExamInfo); diff --git a/src/listmode_buildblock/CMakeLists.txt b/src/listmode_buildblock/CMakeLists.txt index b3e42aa1d..eac6fa899 100644 --- a/src/listmode_buildblock/CMakeLists.txt +++ b/src/listmode_buildblock/CMakeLists.txt @@ -15,7 +15,6 @@ set(${dir_LIB_SOURCES} CListModeDataECAT8_32bit.cxx CListRecordECAT8_32bit.cxx CListModeDataSAFIR.cxx - DetectorCoordinateMapFromFile.cxx ) if (HAVE_HDF5) From 7ab36225a054f4eb3517c5b767d7afa5def4f687 Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Fri, 10 Dec 2021 16:50:53 +0000 Subject: [PATCH 47/81] update plot_scanner_LOR: - invert Y axis to much STIR - show information about the patient position: left, right, anterior, posterior, and head, feet - changed copyright and added authors --- examples/python/plot_scanner_LORs.py | 48 ++++++++++++++++------------ 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/examples/python/plot_scanner_LORs.py b/examples/python/plot_scanner_LORs.py index b8df180b8..369977793 100644 --- a/examples/python/plot_scanner_LORs.py +++ b/examples/python/plot_scanner_LORs.py @@ -1,23 +1,20 @@ -# Demo of how to use STIR from python to reconstruct some data +# Demo of how to use STIR from python to plot line of responses of a blocksOnCylindrical scanner in different 2D +# orientation and in 3Ds. The plots show the coordinate system used in STIR with labels L for left, R for right, +# P for posterior, A for anterior, H for Head and F for feet. This is easily modifiable for Cylindrical scanner. # To run in "normal" Python, you would type the following in the command line -# execfile('recon_demo.py') +# execfile('plot_scanner_LORs.py') # In ipython, you can use -# %run recon_demo.py +# %run plot_scanner_LORs.py -# Copyright 2012-06-05 - 2013 Kris Thielemans -# Copyright 2015 University College London +# Copyright 2021 University College London +# Copyright 2021 National Phyisical Laboratory + +# Author Daniel Deidda +# Author Kris Thielemans # This file is part of STIR. # -# This file is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation; either version 2.1 of the License, or -# (at your option) any later version. -# -# This file is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. +# SPDX-License-Identifier: Apache-2.0 # # See STIR/LICENSE.txt for details @@ -37,7 +34,7 @@ scanner=stir.Scanner_get_scanner_from_name('SAFIRDualRingPrototype') scanner.set_num_transaxial_blocks_per_bucket(2) -# scanner.set_intrinsic_azimuthal_tilt(45) +scanner.set_intrinsic_azimuthal_tilt(0) # scanner.set_num_axial_crystals_per_block(1) # scanner.set_axial_block_spacing(scanner.get_axial_crystal_spacing()*scanner.get_num_axial_crystals_per_block()); # scanner.set_num_rings(1) @@ -67,6 +64,7 @@ csi_minus_csiGaps=csi-(csi/tBl_s*2)*(tC_s/2+tBl_gap) rmax =r/math.cos(csi_minus_csiGaps) +# scanner.set_intrinsic_azimuthal_tilt(-csi_minus_csiGaps) #if you want to play with the orientation of the blocks #%% Create projection data info for Blocks on Cylindrical for i in range(0,2*Nr-1,1 ): @@ -115,11 +113,16 @@ # Shrink current axis % box = ax.get_position() - ax.set_position([box.x0, box.y0+box.y0*0.01, box.width * .985, box.height]) + ax.set_position([box.x0-box.y0*0.04, box.y0+box.y0*0.01, box.width, box.height]) + ax.set_aspect('equal', 'box') plt.legend(loc='best',bbox_to_anchor=(1., 1.),fancybox=True) # plt.show() #if debugging we can se how the LORs are order - -plt.savefig('2D-2BlocksPerBuckets-XY-LOR.png', format='png', dpi=300) +plt.gca().invert_yaxis() +plt.text(-65,65,"PL") +plt.text(65,65,"PR") +plt.text(-65,-65,"AL") +plt.text(65,-65,"AR") +plt.savefig('2D-2BlocksPerBuckets-ObliqueAt0degrees-XY-LOR.png', format='png', dpi=300) plt.show() plt.close(); @@ -138,6 +141,7 @@ # r=lor.radius() # plt.plot((r*math.sin(phi), r*math.sin(phi+math.pi)),(-r*math.cos(phi), -r*math.cos(phi+math.pi))) # plt.show() +# plt.gca().invert_yaxis() # plt.show() # plt.savefig('2D-XY-LOR-cyl.png', format='png', dpi=300) @@ -176,7 +180,11 @@ plt.legend(loc='best',bbox_to_anchor=(1., 1.),fancybox=True) # plt.show() - +plt.gca().invert_yaxis() +plt.text(0.2,69,"PH") +plt.text(0.2,-65,"AH") +plt.text(34,69,"PF") +plt.text(34,-65,"AF") plt.savefig('2D-YZ-LOR.png', format='png', dpi=300) plt.show() plt.close() @@ -215,6 +223,6 @@ plt.legend(loc='best',bbox_to_anchor=(1., 1.),fancybox=True) # plt.show() - +plt.gca().invert_yaxis() plt.savefig('3dLOR.png', format='png', dpi=300) plt.show() \ No newline at end of file From 96798d66bc9ca956bdfd1853766b59babb7fa697 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Fri, 10 Dec 2021 01:53:48 +0000 Subject: [PATCH 48/81] use DetectorCoordinateMap in Scanner As opposed to duplicating code, Use DetectorCoordinateMap directly in Scanner. However, the Scanner code was set-up to make a diffference between "original" indices and "sorted" detector-positions. So I've moved that code to DetectorCoordinateMap --- src/buildblock/DetectorCoordinateMap.cxx | 97 ++++++++++++++++- .../ProjDataInfoGenericNoArcCorr.cxx | 4 +- src/buildblock/Scanner.cxx | 103 +++--------------- src/include/stir/DetectorCoordinateMap.h | 85 +++++++++++---- src/include/stir/Scanner.h | 41 ++----- src/include/stir/Scanner.inl | 31 ++++-- .../stir/listmode/CListRecordSAFIR.inl | 12 +- 7 files changed, 209 insertions(+), 164 deletions(-) diff --git a/src/buildblock/DetectorCoordinateMap.cxx b/src/buildblock/DetectorCoordinateMap.cxx index 2f6a3d252..17f832c1c 100644 --- a/src/buildblock/DetectorCoordinateMap.cxx +++ b/src/buildblock/DetectorCoordinateMap.cxx @@ -1,6 +1,6 @@ /* - Copyright 2015 ETH Zurich, Institute of Particle Physics - + Copyright 2015, 2017 ETH Zurich, Institute of Particle Physics + Copyright (C) 2021 University College London This file is part of STIR. SPDX-License-Identifier: Apache-2.0 @@ -14,23 +14,25 @@ \brief Implementation of class stir::DetectorCoordinateMap \author Jannis Fischer + \author Parisa Khateri + \author Kris Thielemans */ #include "stir/error.h" #include "stir/DetectorCoordinateMap.h" +#include "stir/modulo.h" START_NAMESPACE_STIR -void DetectorCoordinateMap::read_detectormap_from_file( const std::string& filename ) +DetectorCoordinateMap::det_pos_to_coord_type DetectorCoordinateMap::read_detectormap_from_file_help( const std::string& filename ) { std::ifstream myfile(filename.c_str()); if( !myfile ) { - error("Error opening file " + filename + ".\n"); - return; + error("Error opening file '" + filename + "'"); } -// char line[80]; + det_pos_to_coord_type coord_map; std::string line; while( std::getline( myfile, line)) { @@ -54,5 +56,88 @@ void DetectorCoordinateMap::read_detectormap_from_file( const std::string& filen coord_map[detpos] = coord; } + return coord_map; } + + +void DetectorCoordinateMap::set_detector_map( const DetectorCoordinateMap::det_pos_to_coord_type& coord_map ) +{ + // The detector crystal coordinates are saved in coord_map the following way: + // (detector#, ring#, 1)[(x,y,z)] + // the detector# and ring# are determined outside of STIR (later given in input) + // In order to fulfill the STIR convention we have to give the coordinates + // detector# and ring# defined by ourself so that the start (0,0) goes to the + // coordinate with the smallest z and smallest y and the detector# is + // counterclockwise rising. + // To achieve this, we assign each coordinate the value 'coord_sorter' which + // is the assigned value of the criteria mentioned above. With it we sort the + // coordinates and fill the to maps 'input_index_to_det_pos' and + // 'det_pos_to_coord'. + std::vector coords_to_be_sorted; + boost::unordered_map > map_for_sorting_coordinates; + coords_to_be_sorted.reserve(coord_map.size()); + + unsigned min_tangential_coord = 1000000U; + unsigned min_axial_coord = 1000000U; + unsigned min_radial_coord = 1000000U; + num_tangential_coords = 0U; + num_axial_coords = 0U; + num_radial_coords = 0U; + for(auto it : coord_map) + { + double coord_sorter = it.second[1] * 100 + from_min_pi_plus_pi_to_0_2pi(std::atan2(it.second[3], -it.second[2])); + coords_to_be_sorted.push_back(coord_sorter); + map_for_sorting_coordinates[coord_sorter] = it.first; + + const auto detpos = it.first; + if (num_tangential_coords <= detpos.tangential_coord()) + num_tangential_coords = detpos.tangential_coord()+1; + if (min_tangential_coord > detpos.tangential_coord()) + min_tangential_coord = detpos.tangential_coord(); + if (num_axial_coords <= detpos.axial_coord()) + num_axial_coords = detpos.axial_coord()+1; + if (min_axial_coord > detpos.axial_coord()) + min_axial_coord = detpos.axial_coord(); + if (num_radial_coords <= detpos.radial_coord()) + num_radial_coords = detpos.radial_coord()+1; + if (min_radial_coord > detpos.radial_coord()) + min_radial_coord = detpos.radial_coord(); + } + + if ((min_tangential_coord != 0) || (min_axial_coord != 0) || (min_radial_coord != 0)) + error("DetectorCoordinateMap::set_detector_map: minimum indices have to be zero."); + if ((num_tangential_coords * num_axial_coords * num_radial_coords) != coord_map.size()) + error("DetectorCoordinateMap::set_detector_map: maximum indices inconsistent with a regular 3D array.\n" + "Sizes derived from indices: tangential " + std::to_string(num_tangential_coords) + + ", axial " +std::to_string(num_axial_coords) + ", radial " + std::to_string(num_radial_coords) + + "\nOveral size: " + std::to_string(coord_map.size())); + + std::sort(coords_to_be_sorted.begin(), coords_to_be_sorted.end()); + stir::DetectionPosition<> detpos(0,0,0); + for(std::vector::iterator it = coords_to_be_sorted.begin(); it != coords_to_be_sorted.end();++it) + { + input_index_to_det_pos[map_for_sorting_coordinates[*it]] = detpos; + det_pos_to_coord[detpos] = coord_map.at(map_for_sorting_coordinates[*it]); + ++detpos.tangential_coord(); + if (detpos.tangential_coord() == num_tangential_coords) + { + ++detpos.axial_coord(); + detpos.tangential_coord() = 0; + if (detpos.axial_coord() == num_axial_coords) + { + ++detpos.radial_coord(); + detpos.axial_coord() = 0; + } + } + } +} + +// creates maps to convert between stir and 3d coordinates +void DetectorCoordinateMap::read_detectormap_from_file( const std::string& filename ) +{ + det_pos_to_coord_type coord_map = + read_detectormap_from_file_help(filename); + set_detector_map(coord_map); +} + END_NAMESPACE_STIR diff --git a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx index 3b671f871..3fd8736ff 100644 --- a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx +++ b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx @@ -393,8 +393,8 @@ find_cartesian_coordinates_given_scanner_coordinates(CartesianCoordinate3Dget_coords_given_detpos(det_pos1); - coord_2 = get_scanner_ptr()->get_coords_given_detpos(det_pos2); + coord_1 = get_scanner_ptr()->get_coordinate_for_det_pos(det_pos1); + coord_2 = get_scanner_ptr()->get_coordinate_for_det_pos(det_pos2); #else if (crystal_map->find_cartesian_coordinate_given_detection_position(coord_1, det_pos1)==Succeeded::yes && crystal_map->find_cartesian_coordinate_given_detection_position(coord_2, det_pos2)==Succeeded::yes) diff --git a/src/buildblock/Scanner.cxx b/src/buildblock/Scanner.cxx index be01e3200..69b5446a6 100644 --- a/src/buildblock/Scanner.cxx +++ b/src/buildblock/Scanner.cxx @@ -4,7 +4,6 @@ Copyright (C) 2011, Kris Thielemans Copyright (C) 2010-2013, King's College London Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics - Copyright (C) 2013-2016,2019,2020 University College London Copyright (C) 2013-2016,2019-2021 University College London Copyright (C) 2017-2018, University of Leeds This file is part of STIR. @@ -36,7 +35,7 @@ #include "stir/Succeeded.h" #include "stir/interfile_keyword_functions.h" #include "stir/info.h" -#include "stir/modulo.h" +#include "stir/DetectorCoordinateMap.h" #include #include #ifdef BOOST_NO_STRINGSTREAM @@ -766,104 +765,28 @@ set_params(Type type_v,const list& list_of_names_v, { read_detectormap_from_file(crystal_map_file_name); } -} - -// creates maps to convert between stir and 3d coordinates -Scanner::det_pos_to_coord_type -Scanner:: -read_detectormap_from_file_help( const std::string& crystal_map_name ) -{ - std::ifstream crystal_map_file(crystal_map_name.c_str()); - if( !crystal_map_file ) - { - error("Error opening file '" + crystal_map_name + "'"); - } - std::string line; - //map containing the crystal map from the input file (safir -> coords) - boost::unordered_map, stir::CartesianCoordinate3D, ihash> coord_map; - // read in the file save the content in a map - while( std::getline( crystal_map_file, line)) - { - if( line.size() && line[0] == '#' ) continue; - bool has_layer_index = false; - stir::CartesianCoordinate3D coord; - stir::DetectionPosition<> detpos; - std::vector entry; - boost::split(entry, line, boost::is_any_of("\t,")); - if( !entry.size() ) break; - else if( entry.size() == 5 ) has_layer_index = false; - else if( entry.size() == 6 ) has_layer_index = true; - coord[1] = atof(entry[4+has_layer_index].c_str() ); - coord[2] = atof(entry[3+has_layer_index].c_str() ); - coord[3] = atof(entry[2+has_layer_index].c_str() ); - - if( !has_layer_index ) detpos.radial_coord() = 0; - else detpos.radial_coord() = atoi(entry[2].c_str()); - detpos.axial_coord() = atoi(entry[0].c_str()); - detpos.tangential_coord() = atoi(entry[1].c_str()); - - coord_map[detpos] = coord; - } - return coord_map; + else + { + this->detector_map_sptr = 0; + } } void Scanner:: -set_detector_map( const Scanner::det_pos_to_coord_type& coord_map ) +set_detector_map( const DetectorCoordinateMap::det_pos_to_coord_type& coord_map ) { - // The detector crystal coordinates are saved in coord_map the following way: - // (detector#, ring#, 1)[(x,y,z)] - // the detector# and ring# are determined outside of STIR (later given in input) - // In order to fulfill the STIR convention we have to give the coordinates - // detector# and ring# defined by ourself so that the start (0,0) goes to the - // coordinate with the smallest z and smallest y and the detector# is - // counterclockwise rising. - // To achieve this, we assign each coordinate the value 'coord_sorter' which - // is the assigned value of the criteria mentioned above. With it we sort the - // coordinates and fill the to maps 'input_index_to_stir_index' and - // 'stir_index_to_coord'. - std::vector coords_to_be_sorted; - boost::unordered_map > map_for_sorting_coordinates; - coords_to_be_sorted.reserve(coord_map.size()); - - for(auto it : coord_map) - { - double coord_sorter = it.second[1] * 100 + from_min_pi_plus_pi_to_0_2pi(std::atan2(it.second[3], -it.second[2])); - coords_to_be_sorted.push_back(coord_sorter); - map_for_sorting_coordinates[coord_sorter] = it.first; - } - std::sort(coords_to_be_sorted.begin(), coords_to_be_sorted.end()); - int ring = 0; - int det = 0; - for(std::vector::iterator it = coords_to_be_sorted.begin(); it != coords_to_be_sorted.end();++it) - { - stir::DetectionPosition<> detpos; - detpos.axial_coord() = ring; - detpos.tangential_coord() = det; - detpos.radial_coord() = 0; - input_index_to_stir_index[map_for_sorting_coordinates[*it]] = detpos; - stir_index_to_coord[detpos] = coord_map.at(map_for_sorting_coordinates[*it]); - det++; - if (det == num_detectors_per_ring) - { - ring++; - det = 0; - if (ring == num_rings && it != --coords_to_be_sorted.end()) - { - stir::error("Detector and RingNumber of the crystal map and ProjData are not the same!"); - } - } - } + this->detector_map_sptr.reset(new DetectorCoordinateMap(coord_map)); + if ((unsigned)num_detectors_per_ring != detector_map_sptr->get_num_tangential_coords() || + (unsigned)num_rings != detector_map_sptr->get_num_axial_coords() || + (unsigned)num_detector_layers != detector_map_sptr->get_num_radial_coords()) + error("Scanner:set_detector_map: inconsistent number of detectors"); } -// creates maps to convert between stir and 3d coordinates void Scanner:: -read_detectormap_from_file( const std::string& crystal_map_name ) +read_detectormap_from_file( const std::string& filename ) { - det_pos_to_coord_type coord_map = - read_detectormap_from_file_help(crystal_map_file_name); - set_detector_map(coord_map); + this->detector_map_sptr.reset(new DetectorCoordinateMap(filename)); } /*! \todo The current list is bound to be incomplete. would be better to stick it in set_params(). diff --git a/src/include/stir/DetectorCoordinateMap.h b/src/include/stir/DetectorCoordinateMap.h index 63f12031f..cfbe0441d 100644 --- a/src/include/stir/DetectorCoordinateMap.h +++ b/src/include/stir/DetectorCoordinateMap.h @@ -1,7 +1,7 @@ /* - Copyright 2015 ETH Zurich, Institute of Particle Physics + Copyright 2015, 2017 ETH Zurich, Institute of Particle Physics Copyright 2020 Positrigo AG, Zurich - + Copyright (C) 2021 University College London This file is part of STIR. SPDX-License-Identifier: Apache-2.0 @@ -12,10 +12,12 @@ /*! \file - \ingroup listmode + \ingroup buildblock \brief Declaration of class stir::DetectorCoordinateMap \author Jannis Fischer + \author Parisa Khateri + \author Kris Thielemans */ #include @@ -41,44 +43,85 @@ START_NAMESPACE_STIR */ class DetectorCoordinateMap { + struct ihash + : std::unary_function , std::size_t> + { + std::size_t operator()(stir::DetectionPosition<> const& detpos) const + { + std::size_t seed = 0; + boost::hash_combine(seed, detpos.axial_coord()); + boost::hash_combine(seed, detpos.radial_coord()); + boost::hash_combine(seed, detpos.tangential_coord()); + return seed; + } + }; public: + typedef boost::unordered_map, stir::CartesianCoordinate3D, ihash> det_pos_to_coord_type; + typedef boost::unordered_map, stir::DetectionPosition<>, ihash> unordered_to_ordered_det_pos_type; + //! Constructor calls read_detectormap_from_file( filename ). DetectorCoordinateMap(const std::string& filename, double sigma = 0.0) : sigma(sigma), distribution(0.0, sigma) { read_detectormap_from_file( filename ); } + //! Constructor calls set_detector_map(coord_map). + DetectorCoordinateMap(const det_pos_to_coord_type& coord_map, double sigma = 0.0) : + sigma(sigma), + distribution(0.0, sigma) + { set_detector_map( coord_map ); } //! Reads map from file and stores it. void read_detectormap_from_file( const std::string& filename ); + //! stores the map + /*! applies sorting to standard STIR order */ + void set_detector_map( const det_pos_to_coord_type& coord_map ); + stir::DetectionPosition<> get_det_pos_for_index(const stir::DetectionPosition<>& index) const + { + return input_index_to_det_pos.at(index); + } //! Returns a cartesian coordinate given a detection position. - stir::CartesianCoordinate3D get_detector_coordinate( const stir::DetectionPosition<>& det_pos ) + stir::CartesianCoordinate3D get_coordinate_for_det_pos( const stir::DetectionPosition<>& det_pos ) const { - auto coord = coord_map.at(det_pos); + auto coord = det_pos_to_coord.at(det_pos); coord.x() += distribution(generator); coord.y() += distribution(generator); coord.z() += distribution(generator); return coord; } + //! Returns a cartesian coordinate given an (unsorted) index. + stir::CartesianCoordinate3D get_coordinate_for_index( const stir::DetectionPosition<>& index ) const + { + return get_coordinate_for_det_pos(get_det_pos_for_index(index)); + } + + unsigned get_num_tangential_coords() const + { return num_tangential_coords; } + unsigned get_num_axial_coords() const + { return num_axial_coords; } + unsigned get_num_radial_coords() const + { return num_radial_coords; } + +protected: + DetectorCoordinateMap(double sigma = 0.0) : + sigma(sigma), + distribution(0.0, sigma) + {} private: - struct ihash - : std::unary_function , std::size_t> - { - std::size_t operator()(stir::DetectionPosition<> const& detpos) const - { - std::size_t seed = 0; - boost::hash_combine(seed, detpos.axial_coord()); - boost::hash_combine(seed, detpos.radial_coord()); - boost::hash_combine(seed, detpos.tangential_coord()); - return seed; - } - }; + unsigned num_tangential_coords; + unsigned num_axial_coords; + unsigned num_radial_coords; + unordered_to_ordered_det_pos_type input_index_to_det_pos; + det_pos_to_coord_type det_pos_to_coord; + + const double sigma; + mutable std::default_random_engine generator; + mutable std::normal_distribution distribution; + + static det_pos_to_coord_type + read_detectormap_from_file_help( const std::string& crystal_map_name ); - boost::unordered_map< stir::DetectionPosition<>, stir::CartesianCoordinate3D, ihash > coord_map; - const double sigma; - std::default_random_engine generator; - std::normal_distribution distribution; }; END_NAMESPACE_STIR diff --git a/src/include/stir/Scanner.h b/src/include/stir/Scanner.h index 3d592568c..1044b4af5 100644 --- a/src/include/stir/Scanner.h +++ b/src/include/stir/Scanner.h @@ -2,7 +2,7 @@ Copyright (C) 2000 PARAPET partners Copyright (C) 2000-2010, Hammersmith Imanet Ltd Copyright (C) 2011-2013, King's College London - Copyright (C) 2016, 2019, UCL + Copyright (C) 2016, 2019, 2021 UCL Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics Copyright (C 2017-2018, University of Leeds This file is part of STIR. @@ -33,6 +33,8 @@ #include "stir/DetectionPosition.h" #include "stir/CartesianCoordinate3D.h" +#include "stir/DetectorCoordinateMap.h" +#include "stir/shared_ptr.h" #include #include #include @@ -45,7 +47,6 @@ START_NAMESPACE_STIR class Succeeded; - /*! \ingroup buildblock \brief A class for storing some info on the scanner @@ -421,13 +422,14 @@ class Scanner // Get the transaxial singles bin coordinate from a singles bin. inline int get_transaxial_singles_unit(int singles_bin_index) const; - // Get the STIR detection position (det#, ring#, layer#) given the detection position id in the input crystal map + //! Get the STIR detection position (det#, ring#, layer#) given the detection position id in the input crystal map // used in CListRecordSAFIR.inl for accessing the coordinates - inline stir::DetectionPosition<> get_detpos_given_id(const stir::DetectionPosition<> & det_pos) const; - // Get the Cartesian coordinates (x,y,z) given the STIR detection position (det#, ring#, layer#) + inline stir::DetectionPosition<> get_det_pos_for_index(const stir::DetectionPosition<> & det_pos) const; + //! Get the Cartesian coordinates (x,y,z) given the STIR detection position (det#, ring#, layer#) // used in ProjInfoDataGenericNoArcCorr.cxx for accessing the coordinates - inline stir::CartesianCoordinate3D get_coords_given_detpos(const stir::DetectionPosition<> det_pos) const; - + inline stir::CartesianCoordinate3D get_coordinate_for_det_pos(const stir::DetectionPosition<>& det_pos) const; + //! Get the Cartesian coordinates (x,y,z) given the detection position id in the input crystal map + inline stir::CartesianCoordinate3D get_coordinate_for_index(const stir::DetectionPosition<>& det_pos) const; private: Type type; std::list list_of_names; @@ -477,31 +479,10 @@ class Scanner float axial_block_spacing; /*! block pitch in axial direction in mm*/ float transaxial_block_spacing; /*! block pitch in transaxial direction in mm*/ - //! - //!\brief map and hash function for saving the coords in generic geometry - //!\author Michael Roethlisberger - struct ihash - : std::unary_function , std::size_t> - { - std::size_t operator()(stir::DetectionPosition<> const& detpos) const - { - std::size_t seed = 0; - boost::hash_combine(seed, detpos.axial_coord()); - boost::hash_combine(seed, detpos.radial_coord()); - boost::hash_combine(seed, detpos.tangential_coord()); - return seed; - } - }; - - typedef boost::unordered_map, stir::CartesianCoordinate3D, ihash> det_pos_to_coord_type; - typedef boost::unordered_map, stir::DetectionPosition<>, ihash> unordered_to_ordered_det_pos_type; - unordered_to_ordered_det_pos_type input_index_to_stir_index; - det_pos_to_coord_type stir_index_to_coord; std::string crystal_map_file_name; + shared_ptr detector_map_sptr; - static det_pos_to_coord_type - read_detectormap_from_file_help( const std::string& crystal_map_name ); - void set_detector_map( const det_pos_to_coord_type& coord_map ); + void set_detector_map( const DetectorCoordinateMap::det_pos_to_coord_type& coord_map ); // function to create the maps void read_detectormap_from_file( const std::string& filename ); diff --git a/src/include/stir/Scanner.inl b/src/include/stir/Scanner.inl index 8c6fab608..45e8e2e17 100644 --- a/src/include/stir/Scanner.inl +++ b/src/include/stir/Scanner.inl @@ -3,7 +3,7 @@ /* Copyright (C) 2000 PARAPET partners Copyright (C) 2000- 2007, Hammersmith Imanet Ltd - Copyright (C) 2016, UCL + Copyright (C) 2016, 2021 UCL Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics This file is part of STIR. @@ -25,6 +25,7 @@ */ +#include "stir/error.h" START_NAMESPACE_STIR @@ -462,18 +463,30 @@ Scanner::get_transaxial_singles_unit(int singles_bin_index) const { // For retrieving the coordinates / detector, ring id from the scanner stir::DetectionPosition<> -Scanner::get_detpos_given_id(const stir::DetectionPosition<> & det_pos) const{ - if (crystal_map_file_name == ""){ +Scanner::get_det_pos_for_index(const stir::DetectionPosition<> & det_pos) const +{ + if (!detector_map_sptr) stir::error("Crystal Map not defined!"); - } - return input_index_to_stir_index.at(det_pos); + + return detector_map_sptr->get_det_pos_for_index(det_pos); } + stir::CartesianCoordinate3D -Scanner::get_coords_given_detpos(const stir::DetectionPosition<> det_pos) const{ - if (crystal_map_file_name == ""){ +Scanner::get_coordinate_for_det_pos(const stir::DetectionPosition<>& det_pos) const +{ + if (!detector_map_sptr) stir::error("Crystal Map not defined!"); - } - return stir_index_to_coord.at(det_pos); + + return detector_map_sptr->get_coordinate_for_det_pos(det_pos); +} + +stir::CartesianCoordinate3D +Scanner::get_coordinate_for_index(const stir::DetectionPosition<>& index) const +{ + if (!detector_map_sptr) + stir::error("Crystal Map not defined!"); + + return detector_map_sptr->get_coordinate_for_index(index); } END_NAMESPACE_STIR diff --git a/src/include/stir/listmode/CListRecordSAFIR.inl b/src/include/stir/listmode/CListRecordSAFIR.inl index 6fb6c588b..85adaa52d 100644 --- a/src/include/stir/listmode/CListRecordSAFIR.inl +++ b/src/include/stir/listmode/CListRecordSAFIR.inl @@ -46,8 +46,8 @@ CListEventSAFIR::get_LOR() const if(!map) stir::error("Crystal map not set."); - lor.p1() = map->get_detector_coordinate(det_pos_pair.pos1()); - lor.p2() = map->get_detector_coordinate(det_pos_pair.pos2()); + lor.p1() = map->get_coordinate_for_index(det_pos_pair.pos1()); + lor.p2() = map->get_coordinate_for_index(det_pos_pair.pos2()); return lor; } @@ -79,8 +79,8 @@ get_bin(Bin& bin, const ProjDataInfo& proj_data_info) const //transform det_pos_pair into stir coordinates DetectionPosition<> pos1 = det_pos_pair.pos1(); DetectionPosition<> pos2 = det_pos_pair.pos2(); - det_pos_pair.pos1() = proj_data_info_gen.get_scanner_ptr()->get_detpos_given_id(pos1); - det_pos_pair.pos2() = proj_data_info_gen.get_scanner_ptr()->get_detpos_given_id(pos2); + det_pos_pair.pos1() = proj_data_info_gen.get_scanner_ptr()->get_det_pos_for_index(pos1); + det_pos_pair.pos2() = proj_data_info_gen.get_scanner_ptr()->get_det_pos_for_index(pos2); if (proj_data_info_gen.get_bin_for_det_pos_pair(bin, det_pos_pair) == Succeeded::yes) bin.set_bin_value(1); @@ -91,8 +91,8 @@ get_bin(Bin& bin, const ProjDataInfo& proj_data_info) const { if(!map) stir::error("Crystal map not set."); - stir::CartesianCoordinate3D c1 = map->get_detector_coordinate(det_pos_pair.pos1()); - stir::CartesianCoordinate3D c2 = map->get_detector_coordinate(det_pos_pair.pos2()); + stir::CartesianCoordinate3D c1 = map->get_coordinate_for_index(det_pos_pair.pos1()); + stir::CartesianCoordinate3D c2 = map->get_coordinate_for_index(det_pos_pair.pos2()); int det1, det2, ring1, ring2; if(proj_data_info.get_scanner_ptr()->get_scanner_geometry() == "Cylindrical" From 93d188e50131a7abcd57e2c940838ac287262825 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Sat, 11 Dec 2021 19:12:20 +0000 Subject: [PATCH 49/81] add comparison operators to DetectionPosition --- documentation/release_5.0.htm | 2 + src/include/stir/DetectionPosition.h | 7 ++- src/include/stir/DetectionPosition.inl | 11 +++- src/test/CMakeLists.txt | 1 + src/test/test_DetectionPosition.cxx | 72 ++++++++++++++++++++++++++ 5 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 src/test/test_DetectionPosition.cxx diff --git a/documentation/release_5.0.htm b/documentation/release_5.0.htm index 971ce4173..9e59fd9b2 100644 --- a/documentation/release_5.0.htm +++ b/documentation/release_5.0.htm @@ -297,6 +297,8 @@

    New functionality

  • KeyParser::parameter_info() now outputs vectorised keys as well
  • +
  • DetectionPosition now has all comparison operators. +
  • diff --git a/src/include/stir/DetectionPosition.h b/src/include/stir/DetectionPosition.h index dfb01df12..a4bb7f8ff 100644 --- a/src/include/stir/DetectionPosition.h +++ b/src/include/stir/DetectionPosition.h @@ -18,6 +18,7 @@ #define __stir_DetectionPosition_H__ #include "stir/common.h" +#include START_NAMESPACE_STIR /*! \ingroup projdata @@ -54,7 +55,9 @@ START_NAMESPACE_STIR The class is templated to allow for systems with continuous detection. */ template -class DetectionPosition + class DetectionPosition: + boost::partially_ordered, // have operator>, <= etc for free + boost::equality_comparable > > // have operator!= for free { public: inline explicit @@ -71,7 +74,7 @@ class DetectionPosition //! \name comparison operators //@{ inline bool operator==(const DetectionPosition&) const; - inline bool operator!=(const DetectionPosition&) const; + inline bool operator<(const DetectionPosition&) const; //@} private : coordT tangential; diff --git a/src/include/stir/DetectionPosition.inl b/src/include/stir/DetectionPosition.inl index 158b8d7cd..4e6826271 100644 --- a/src/include/stir/DetectionPosition.inl +++ b/src/include/stir/DetectionPosition.inl @@ -9,6 +9,7 @@ */ /* Copyright (C) 2002- 2009, Hammersmith Imanet Ltd + Copyright (C) 2021, University College London This file is part of STIR. SPDX-License-Identifier: Apache-2.0 @@ -78,8 +79,14 @@ operator==(const DetectionPosition& d) const template bool DetectionPosition:: -operator!=(const DetectionPosition& d) const -{ return !(*this==d); } +operator<(const DetectionPosition& d) const +{ + return + (tangential < d.tangential) || + ((tangential == d.tangential) && + ((axial < d.axial) || + ((axial == d.axial) && (radial < d.radial)))); +} END_NAMESPACE_STIR diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index c7d45e4ff..32c95cbe7 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -56,6 +56,7 @@ set(buildblock_simple_tests test_Scanner.cxx test_ArcCorrection.cxx test_find_fwhm_in_image.cxx + test_DetectionPosition.cxx test_proj_data_info.cxx test_proj_data.cxx test_proj_data_maths.cxx diff --git a/src/test/test_DetectionPosition.cxx b/src/test/test_DetectionPosition.cxx new file mode 100644 index 000000000..ba74bbba9 --- /dev/null +++ b/src/test/test_DetectionPosition.cxx @@ -0,0 +1,72 @@ +// +// + +/*! + \file + + \brief A simple program to test the stir::DetectionPosition class + + \author Kris Thielemans + +*/ +/* + Copyright (C) 2021, University College London + This file is part of STIR. + + SPDX-License-Identifier: Apache-2.0 + + See STIR/LICENSE.txt for details +*/ + +#include "stir/DetectionPosition.h" +#include "stir/RunTests.h" + +using std::cerr; +using std::endl; + +START_NAMESPACE_STIR + +/*! + \brief Class with tests for DetectionPosition. +*/ +class DetectionPosition_Tests : public RunTests +{ +public: + void run_tests(); +}; + + +void +DetectionPosition_Tests::run_tests() +{ + cerr << "Testing DetectionPosition classes" << endl + <<" (There should be only informative messages here starting with 'Testing')" << endl; + + DetectionPosition<> pos012(0,1,2); + DetectionPosition<> pos013(0,1,3); + DetectionPosition<> pos023(0,2,3); + DetectionPosition<> pos103(1,0,3); + + check(pos012 != pos013, "012 != 013"); + check(pos012 == pos012, "012 == 012"); + check(pos012 < pos013, "012 < 013"); + check(pos012 < pos023, "012 < 023"); + check(pos012 < pos103, "012 < 103"); + check(pos103 > pos012, "103 > 012"); + check(pos012 <= pos013, "012 <= 013"); +} + +END_NAMESPACE_STIR + + + +USING_NAMESPACE_STIR + + +int main() +{ + DetectionPosition_Tests tests; + tests.run_tests(); + return tests.main_return_value(); + +} From 6516c76b635081b175a2235c727283e6305f1da2 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Sat, 11 Dec 2021 19:34:35 +0000 Subject: [PATCH 50/81] simplify GeometryBlocksOnCylindrical --- .../GeometryBlocksOnCylindrical.cxx | 33 ------------------- .../stir/GeometryBlocksOnCylindrical.h | 32 ++++-------------- .../test_blocks_on_cylindrical_projectors.cxx | 1 + 3 files changed, 7 insertions(+), 59 deletions(-) diff --git a/src/buildblock/GeometryBlocksOnCylindrical.cxx b/src/buildblock/GeometryBlocksOnCylindrical.cxx index 45becfe0f..f9003bf6c 100755 --- a/src/buildblock/GeometryBlocksOnCylindrical.cxx +++ b/src/buildblock/GeometryBlocksOnCylindrical.cxx @@ -54,39 +54,6 @@ GeometryBlocksOnCylindrical(const shared_ptr &scanner_ptr_v): build_crystal_maps(); } -bool -GeometryBlocksOnCylindrical:: -compare_det_pos:: -operator() (const stir::DetectionPosition<>& det_pos1, - const stir::DetectionPosition<>& det_pos2) const -{ - if ( det_pos1.tangential_coord()& cart_coord1, - const stir::CartesianCoordinate3D& cart_coord2) const -{ - if (cart_coord1.z() GeometryBlocksOnCylindrical:: get_rotation_matrix(float alpha) const diff --git a/src/include/stir/GeometryBlocksOnCylindrical.h b/src/include/stir/GeometryBlocksOnCylindrical.h index 354ecfcb1..9609f1b45 100644 --- a/src/include/stir/GeometryBlocksOnCylindrical.h +++ b/src/include/stir/GeometryBlocksOnCylindrical.h @@ -32,12 +32,7 @@ limitations under the License. #include "stir/Scanner.h" #include "stir/shared_ptr.h" #include "stir/Array.h" -#include "stir/make_array.h" -#include "stir/numerics/MatrixFunction.h" #include -#include -#include -#include #include "stir/Succeeded.h" START_NAMESPACE_STIR @@ -68,25 +63,13 @@ class GeometryBlocksOnCylindrical //! Destructor ~GeometryBlocksOnCylindrical() {} - - //! comparison operator for DetectionPosition class. needded to be used as a key type in building map - class compare_det_pos{ - public: - bool operator() (const stir::DetectionPosition<>& , const stir::DetectionPosition<>&) const; - }; - - //! comparison operator for CartesianCoordinate3D class. needded to be used as a key type in building map - class compare_cartesian_coord{ - public: - bool operator() (const stir::CartesianCoordinate3D& , const stir::CartesianCoordinate3D&) const; - }; - + private: //! Get rotation matrix for a given angle around z axis stir::Array<2, float> get_rotation_matrix(float alpha) const; //! Build crystal map in cartesian coordinate void build_crystal_maps(); - + public: //! Get cartesian coordinate for a given detection position inline Succeeded find_cartesian_coordinate_given_detection_position(CartesianCoordinate3D& , @@ -96,7 +79,7 @@ class GeometryBlocksOnCylindrical Succeeded find_detection_position_given_cartesian_coordinate(DetectionPosition<>&, const CartesianCoordinate3D&) const; - + private: //! Get scanner pointer inline const Scanner* get_scanner_ptr() const; @@ -105,14 +88,11 @@ class GeometryBlocksOnCylindrical //! member variables shared_ptr scanner_ptr; std::map, - stir::CartesianCoordinate3D, - stir::GeometryBlocksOnCylindrical::compare_det_pos> cartesian_coord_map_given_detection_position_keys; + stir::CartesianCoordinate3D> cartesian_coord_map_given_detection_position_keys; std::map, - stir::DetectionPosition<>, - stir::GeometryBlocksOnCylindrical::compare_cartesian_coord> detection_position_map_given_cartesian_coord_keys_3_decimal; + stir::DetectionPosition<>> detection_position_map_given_cartesian_coord_keys_3_decimal; std::map, - stir::DetectionPosition<>, - stir::GeometryBlocksOnCylindrical::compare_cartesian_coord> detection_position_map_given_cartesian_coord_keys_2_decimal; + stir::DetectionPosition<>> detection_position_map_given_cartesian_coord_keys_2_decimal; }; diff --git a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx index d05a241b3..289a3c3ec 100755 --- a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx +++ b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx @@ -25,6 +25,7 @@ */ #include "stir/info.h" +#include "stir/make_array.h" #include "stir/ProjDataInMemory.h" #include "stir/DiscretisedDensity.h" #include "stir/ProjDataInterfile.h" From 4d9830b9282d55bbcedc9445b26ef8d6e7f5ec53 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Sun, 12 Dec 2021 09:06:20 +0000 Subject: [PATCH 51/81] minor clean-up of test_proj_data_info - unsafe use of copy scanner ptrs in DOI test - remove unused variables --- src/test/test_proj_data_info.cxx | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/test/test_proj_data_info.cxx b/src/test/test_proj_data_info.cxx index 54cb3a1c3..4a3c5f01a 100755 --- a/src/test/test_proj_data_info.cxx +++ b/src/test/test_proj_data_info.cxx @@ -445,11 +445,9 @@ void ProjDataInfoTests::run_Blocks_DOI_test() { CPUTimer timer; - shared_ptr scannerBlocks_ptr, scannerBlocksDOI_ptr; - scannerBlocks_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + shared_ptr scannerBlocks_ptr(new Scanner (Scanner::SAFIRDualRingPrototype)); scannerBlocks_ptr->set_average_depth_of_interaction(0); scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); - VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); @@ -472,7 +470,7 @@ ProjDataInfoTests::run_Blocks_DOI_test() scannerBlocks_ptr->get_max_num_views(), scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); - scannerBlocksDOI_ptr=scannerBlocks_ptr; + shared_ptr scannerBlocksDOI_ptr(new Scanner(*scannerBlocks_ptr)); scannerBlocksDOI_ptr->set_average_depth_of_interaction(0.1); shared_ptr proj_data_info_blocks_doi5_ptr; proj_data_info_blocks_doi5_ptr.reset( @@ -488,7 +486,7 @@ ProjDataInfoTests::run_Blocks_DOI_test() int Bring1, Bring2, Bdet1,Bdet2, BDring1, BDring2, BDdet1, BDdet2; CartesianCoordinate3D< float> b1,b2,bd1,bd2; - float doi=scannerBlocksDOI_ptr->get_average_depth_of_interaction(); + //float doi=scannerBlocksDOI_ptr->get_average_depth_of_interaction(); timer.reset(); timer.start(); for (int seg =proj_data_info_blocks_doi0_ptr->get_min_segment_num(); seg <=proj_data_info_blocks_doi0_ptr->get_max_segment_num(); ++ seg) @@ -522,6 +520,7 @@ ProjDataInfoTests::run_Blocks_DOI_test() // set_tolerance(10E-2); check(b1!=bd1, "detector position should be different with different DOIs"); check(b2!=bd2, "detector position should be different with different DOIs"); + // TODO improve check here } timer.stop(); std::cerr<< "-- CPU Time " << timer.value() << '\n'; @@ -563,11 +562,11 @@ ProjDataInfoTests::run_coordinate_test() float csi=_PI/num_transaxial_buckets; float trans_blocks_gap=transaxial_block_spacing-num_transaxial_crystals_per_block*transaxial_crystal_spacing; - float csi_minus_csiGaps=csi-(csi/transaxial_block_spacing/2)* - (transaxial_crystal_spacing/2+trans_blocks_gap); + /*float csi_minus_csiGaps=csi-(csi/transaxial_block_spacing/2)* + (transaxial_crystal_spacing/2+trans_blocks_gap);*/ - float dx=scannerBlocks_ptr->get_effective_ring_radius()*sin(csi_minus_csiGaps); - float dy=scannerBlocks_ptr->get_effective_ring_radius()-scannerBlocks_ptr->get_effective_ring_radius()*cos(csi_minus_csiGaps); + //float dx=scannerBlocks_ptr->get_effective_ring_radius()*sin(csi_minus_csiGaps); + //float dy=scannerBlocks_ptr->get_effective_ring_radius()-scannerBlocks_ptr->get_effective_ring_radius()*cos(csi_minus_csiGaps); shared_ptr scannerCyl_ptr; scannerCyl_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); @@ -660,7 +659,7 @@ ProjDataInfoTests::run_coordinate_test() const float old_phi=proj_data_info_cyl_ptr->get_phi(bin); if (fabs(phi-old_phi)>=2*_PI/num_detectors){ - float ang=2*_PI/num_detectors/2; + //float ang=2*_PI/num_detectors/2; // warning("view %d old_phi %g new_phi %g\n",bin.view_num(), old_phi, phi); lorC1 = LORInAxialAndNoArcCorrSinogramCoordinates(lorC.z2(), @@ -836,10 +835,7 @@ ProjDataInfoTests::run_coordinate_test_for_realistic_scanner() proj_data_info_cyl_ptr->get_LOR(lorC,bin); proj_data_info_blocks_ptr->get_LOR(lorB,bin); - - const int num_detectors = - proj_data_info_cyl_ptr->get_scanner_ptr()->get_num_detectors_per_ring(); - + int det_num1=0, det_num2=0; proj_data_info_cyl_ptr-> get_det_num_pair_for_view_tangential_pos_num(det_num1, @@ -982,10 +978,10 @@ run_lor_get_s_test(){ proj_data_info_blocks_ptr->get_bin_for_det_pair(bin, Cdet1,Cring1,Cdet2,Cring1); proj_data_info_blocks_ptr->get_LOR(lorB,bin); - float R=block_trans_spacing*(sin(_PI*5/12)+sin(_PI/4)+sin(_PI/12)); - float s=R*cos(_PI/3)+ + /*float R=block_trans_spacing*(sin(_PI*5/12)+sin(_PI/4)+sin(_PI/12)); + float s=R*cos(_PI/3)+ transaxial_crystal_spacing/2*sin(_PI/4)+ - (i)*transaxial_crystal_spacing*sin(_PI/4); + (i)*transaxial_crystal_spacing*sin(_PI/4);*/ float s_step=transaxial_crystal_spacing*sin(_PI/4); From 195c3e8183f5dbb6ae7728702ba7036a474fdad2 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Sun, 12 Dec 2021 09:07:30 +0000 Subject: [PATCH 52/81] derived GeometryBlocksOnCylindrical from DetectorCoordinateMap --- src/buildblock/GeometryBlocksOnCylindrical.cxx | 2 ++ src/include/stir/GeometryBlocksOnCylindrical.h | 6 ++---- src/include/stir/GeometryBlocksOnCylindrical.inl | 13 ++----------- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/buildblock/GeometryBlocksOnCylindrical.cxx b/src/buildblock/GeometryBlocksOnCylindrical.cxx index f9003bf6c..3991b411b 100755 --- a/src/buildblock/GeometryBlocksOnCylindrical.cxx +++ b/src/buildblock/GeometryBlocksOnCylindrical.cxx @@ -131,6 +131,7 @@ build_crystal_maps() float transaxial_crystal_spacing = get_scanner_ptr()->get_transaxial_crystal_spacing(); std::string scanner_orientation = get_scanner_ptr()->get_scanner_orientation(); + det_pos_to_coord_type cartesian_coord_map_given_detection_position_keys; // check for the scanner orientation /*Building starts from a bucket perpendicular to y axis, from its first crystal. see start_x*/ @@ -205,6 +206,7 @@ build_crystal_maps() cart_coord.x() = (round(cart_coord.x()*100.0F))/100.0F; detection_position_map_given_cartesian_coord_keys_2_decimal[cart_coord] = det_pos; } + set_detector_map(cartesian_coord_map_given_detection_position_keys); } diff --git a/src/include/stir/GeometryBlocksOnCylindrical.h b/src/include/stir/GeometryBlocksOnCylindrical.h index 9609f1b45..c55e047b2 100644 --- a/src/include/stir/GeometryBlocksOnCylindrical.h +++ b/src/include/stir/GeometryBlocksOnCylindrical.h @@ -27,7 +27,7 @@ limitations under the License. #ifndef __stir_GeometryBlocksOnCylindrical_H__ #define __stir_GeometryBlocksOnCylindrical_H__ -#include "stir/DetectionPosition.h" +#include "stir/DetectorCoordinateMap.h" #include "stir/CartesianCoordinate3D.h" #include "stir/Scanner.h" #include "stir/shared_ptr.h" @@ -50,7 +50,7 @@ START_NAMESPACE_STIR */ -class GeometryBlocksOnCylindrical +class GeometryBlocksOnCylindrical: public DetectorCoordinateMap { @@ -87,8 +87,6 @@ class GeometryBlocksOnCylindrical private: //! member variables shared_ptr scanner_ptr; - std::map, - stir::CartesianCoordinate3D> cartesian_coord_map_given_detection_position_keys; std::map, stir::DetectionPosition<>> detection_position_map_given_cartesian_coord_keys_3_decimal; std::map, diff --git a/src/include/stir/GeometryBlocksOnCylindrical.inl b/src/include/stir/GeometryBlocksOnCylindrical.inl index 699d43d32..31a8f6342 100644 --- a/src/include/stir/GeometryBlocksOnCylindrical.inl +++ b/src/include/stir/GeometryBlocksOnCylindrical.inl @@ -32,17 +32,8 @@ GeometryBlocksOnCylindrical:: find_cartesian_coordinate_given_detection_position(CartesianCoordinate3D& cart_coord, const DetectionPosition<>& det_pos) const { - if (cartesian_coord_map_given_detection_position_keys.count(det_pos)) - { - cart_coord = cartesian_coord_map_given_detection_position_keys.at(det_pos); - return Succeeded::yes; - } - else - { - warning("detection position with (tangential_coord, axial_coord, radial_coord)=(%d, %d, %d) does not exist in the inner map", - det_pos.tangential_coord(), det_pos.axial_coord(), det_pos.radial_coord()); - return Succeeded::no; - } + cart_coord = this->get_coordinate_for_det_pos(det_pos); + return Succeeded::yes; } const Scanner* From 3750b0af5cd66a4c65541b24fd7bbc50b538586b Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Sun, 12 Dec 2021 09:54:28 +0000 Subject: [PATCH 53/81] moved detpos to coordinate to DetectorCoordinateMap --- src/buildblock/DetectorCoordinateMap.cxx | 63 ++++++++++++++++++- .../GeometryBlocksOnCylindrical.cxx | 60 +----------------- src/include/stir/DetectorCoordinateMap.h | 10 +++ .../stir/GeometryBlocksOnCylindrical.h | 10 --- 4 files changed, 73 insertions(+), 70 deletions(-) diff --git a/src/buildblock/DetectorCoordinateMap.cxx b/src/buildblock/DetectorCoordinateMap.cxx index 17f832c1c..6f9fd41be 100644 --- a/src/buildblock/DetectorCoordinateMap.cxx +++ b/src/buildblock/DetectorCoordinateMap.cxx @@ -15,12 +15,14 @@ \author Jannis Fischer \author Parisa Khateri + \author Michael Roethlisberger \author Kris Thielemans */ #include "stir/error.h" #include "stir/DetectorCoordinateMap.h" #include "stir/modulo.h" +#include "stir/Succeeded.h" START_NAMESPACE_STIR @@ -117,7 +119,18 @@ void DetectorCoordinateMap::set_detector_map( const DetectorCoordinateMap::det_p for(std::vector::iterator it = coords_to_be_sorted.begin(); it != coords_to_be_sorted.end();++it) { input_index_to_det_pos[map_for_sorting_coordinates[*it]] = detpos; - det_pos_to_coord[detpos] = coord_map.at(map_for_sorting_coordinates[*it]); + auto cart_coord = coord_map.at(map_for_sorting_coordinates[*it]); + // rounding cart_coord to 3 and 2 decimal points then filling maps + cart_coord.z() = (round(cart_coord.z()*1000.0F))/1000.0F; + cart_coord.y() = (round(cart_coord.y()*1000.0F))/1000.0F; + cart_coord.x() = (round(cart_coord.x()*1000.0F))/1000.0F; + det_pos_to_coord[detpos] = cart_coord; + detection_position_map_given_cartesian_coord_keys_3_decimal[cart_coord] = detpos; //used to find bin from listmode data + cart_coord.z() = (round(cart_coord.z()*100.0F))/100.0F; + cart_coord.y() = (round(cart_coord.y()*100.0F))/100.0F; + cart_coord.x() = (round(cart_coord.x()*100.0F))/100.0F; + detection_position_map_given_cartesian_coord_keys_2_decimal[cart_coord] = detpos; + ++detpos.tangential_coord(); if (detpos.tangential_coord() == num_tangential_coords) { @@ -140,4 +153,52 @@ void DetectorCoordinateMap::read_detectormap_from_file( const std::string& filen set_detector_map(coord_map); } +Succeeded +DetectorCoordinateMap:: +find_detection_position_given_cartesian_coordinate(DetectionPosition<>& det_pos, + const CartesianCoordinate3D& cart_coord) const +{ + /*! first round the cartesian coordinates, it might happen that the cart_coord + is not precisely pointing to the center of the crystal and + then the det_pos cannot be found using the map + */ + //rounding cart_coord to 3 decimal place and find det_pos + CartesianCoordinate3D rounded_cart_coord; + rounded_cart_coord.z() = round(cart_coord.z()*1000.0F)/1000.0F; + rounded_cart_coord.y() = round(cart_coord.y()*1000.0F)/1000.0F; + rounded_cart_coord.x() = round(cart_coord.x()*1000.0F)/1000.0F; + if (detection_position_map_given_cartesian_coord_keys_3_decimal.count(rounded_cart_coord)) + { + det_pos = detection_position_map_given_cartesian_coord_keys_3_decimal.at(rounded_cart_coord); + return Succeeded::yes; + } + else + { + //rounding cart_coord to 2 decimal place and find det_pos + rounded_cart_coord.z() = round(cart_coord.z()*100.0F)/100.0F; + rounded_cart_coord.y() = round(cart_coord.y()*100.0f)/100.0F; + rounded_cart_coord.x() = round(cart_coord.x()*100.0F)/100.0F; + if (detection_position_map_given_cartesian_coord_keys_2_decimal.count(rounded_cart_coord)) + { + det_pos = detection_position_map_given_cartesian_coord_keys_2_decimal.at(rounded_cart_coord); + return Succeeded::yes; + } + else + { + rounded_cart_coord.z() = round(cart_coord.z()*10.0F)/10.0F; + rounded_cart_coord.y() = round(cart_coord.y()*10.0f)/10.0F; + rounded_cart_coord.x() = round(cart_coord.x()*10.0F)/10.0F; + if (detection_position_map_given_cartesian_coord_keys_2_decimal.count(rounded_cart_coord)) + { + det_pos = detection_position_map_given_cartesian_coord_keys_2_decimal.at(rounded_cart_coord); + return Succeeded::yes; + }else{ + warning("cartesian coordinate (x, y, z)=(%f, %f, %f) does not exist in the inner map", + cart_coord.x(), cart_coord.y(), cart_coord.z()); + return Succeeded::no; + } + } + } +} + END_NAMESPACE_STIR diff --git a/src/buildblock/GeometryBlocksOnCylindrical.cxx b/src/buildblock/GeometryBlocksOnCylindrical.cxx index 3991b411b..0069a649f 100755 --- a/src/buildblock/GeometryBlocksOnCylindrical.cxx +++ b/src/buildblock/GeometryBlocksOnCylindrical.cxx @@ -65,55 +65,6 @@ get_rotation_matrix(float alpha) const ); } - -Succeeded -GeometryBlocksOnCylindrical:: -find_detection_position_given_cartesian_coordinate(DetectionPosition<>& det_pos, - const CartesianCoordinate3D& cart_coord) const -{ - /*! first round the cartesian coordinates, it might happen that the cart_coord - is not precisely pointing to the center of the crystal and - then the det_pos cannot be found using the map - */ - //rounding cart_coord to 3 decimal place and find det_pos - CartesianCoordinate3D rounded_cart_coord; - rounded_cart_coord.z() = round(cart_coord.z()*1000.0F)/1000.0F; - rounded_cart_coord.y() = round(cart_coord.y()*1000.0F)/1000.0F; - rounded_cart_coord.x() = round(cart_coord.x()*1000.0F)/1000.0F; - if (detection_position_map_given_cartesian_coord_keys_3_decimal.count(rounded_cart_coord)) - { - det_pos = detection_position_map_given_cartesian_coord_keys_3_decimal.at(rounded_cart_coord); - return Succeeded::yes; - } - else - { - //rounding cart_coord to 2 decimal place and find det_pos - rounded_cart_coord.z() = round(cart_coord.z()*100.0F)/100.0F; - rounded_cart_coord.y() = round(cart_coord.y()*100.0f)/100.0F; - rounded_cart_coord.x() = round(cart_coord.x()*100.0F)/100.0F; - if (detection_position_map_given_cartesian_coord_keys_2_decimal.count(rounded_cart_coord)) - { - det_pos = detection_position_map_given_cartesian_coord_keys_2_decimal.at(rounded_cart_coord); - return Succeeded::yes; - } - else - { - rounded_cart_coord.z() = round(cart_coord.z()*10.0F)/10.0F; - rounded_cart_coord.y() = round(cart_coord.y()*10.0f)/10.0F; - rounded_cart_coord.x() = round(cart_coord.x()*10.0F)/10.0F; - if (detection_position_map_given_cartesian_coord_keys_2_decimal.count(rounded_cart_coord)) - { - det_pos = detection_position_map_given_cartesian_coord_keys_2_decimal.at(rounded_cart_coord); - return Succeeded::yes; - }else{ - warning("cartesian coordinate (x, y, z)=(%f, %f, %f) does not exist in the inner map", - cart_coord.x(), cart_coord.y(), cart_coord.z()); - return Succeeded::no; - } - } - } -} - void GeometryBlocksOnCylindrical:: build_crystal_maps() @@ -195,16 +146,7 @@ build_crystal_maps() stir::CartesianCoordinate3D cart_coord = stir::matrix_multiply(rotation_matrix, transformed_coord); - // rounding cart_coord to 3 and 2 decimal points then filling maps - cart_coord.z() = (round(cart_coord.z()*1000.0F))/1000.0F; - cart_coord.y() = (round(cart_coord.y()*1000.0F))/1000.0F; - cart_coord.x() = (round(cart_coord.x()*1000.0F))/1000.0F; - cartesian_coord_map_given_detection_position_keys[det_pos] = cart_coord; //used to find s, m, phi, theta - detection_position_map_given_cartesian_coord_keys_3_decimal[cart_coord] = det_pos; //used to find bin from listmode data - cart_coord.z() = (round(cart_coord.z()*100.0F))/100.0F; - cart_coord.y() = (round(cart_coord.y()*100.0F))/100.0F; - cart_coord.x() = (round(cart_coord.x()*100.0F))/100.0F; - detection_position_map_given_cartesian_coord_keys_2_decimal[cart_coord] = det_pos; + cartesian_coord_map_given_detection_position_keys[det_pos] = cart_coord; } set_detector_map(cartesian_coord_map_given_detection_position_keys); } diff --git a/src/include/stir/DetectorCoordinateMap.h b/src/include/stir/DetectorCoordinateMap.h index cfbe0441d..e27a88c0d 100644 --- a/src/include/stir/DetectorCoordinateMap.h +++ b/src/include/stir/DetectorCoordinateMap.h @@ -35,6 +35,8 @@ START_NAMESPACE_STIR +class Succeeded; + /*! Class providing map functionality to convert detector indices to spatial coordinates. Map files can have 5 or 6 tab- or comma-separated columns. Lines beginning with '#' are ignored. The layer column is optional \par Format: @@ -95,6 +97,10 @@ class DetectorCoordinateMap return get_coordinate_for_det_pos(get_det_pos_for_index(index)); } + Succeeded + find_detection_position_given_cartesian_coordinate(DetectionPosition<>& det_pos, + const CartesianCoordinate3D& cart_coord) const; + unsigned get_num_tangential_coords() const { return num_tangential_coords; } unsigned get_num_axial_coords() const @@ -114,6 +120,10 @@ class DetectorCoordinateMap unsigned num_radial_coords; unordered_to_ordered_det_pos_type input_index_to_det_pos; det_pos_to_coord_type det_pos_to_coord; + std::map, + stir::DetectionPosition<>> detection_position_map_given_cartesian_coord_keys_3_decimal; + std::map, + stir::DetectionPosition<>> detection_position_map_given_cartesian_coord_keys_2_decimal; const double sigma; mutable std::default_random_engine generator; diff --git a/src/include/stir/GeometryBlocksOnCylindrical.h b/src/include/stir/GeometryBlocksOnCylindrical.h index c55e047b2..f479532e8 100644 --- a/src/include/stir/GeometryBlocksOnCylindrical.h +++ b/src/include/stir/GeometryBlocksOnCylindrical.h @@ -74,11 +74,6 @@ class GeometryBlocksOnCylindrical: public DetectorCoordinateMap inline Succeeded find_cartesian_coordinate_given_detection_position(CartesianCoordinate3D& , const DetectionPosition<>&) const; - - //! Get cartesian coordinate for a given detection position - Succeeded - find_detection_position_given_cartesian_coordinate(DetectionPosition<>&, - const CartesianCoordinate3D&) const; private: //! Get scanner pointer inline const Scanner* get_scanner_ptr() const; @@ -87,11 +82,6 @@ class GeometryBlocksOnCylindrical: public DetectorCoordinateMap private: //! member variables shared_ptr scanner_ptr; - std::map, - stir::DetectionPosition<>> detection_position_map_given_cartesian_coord_keys_3_decimal; - std::map, - stir::DetectionPosition<>> detection_position_map_given_cartesian_coord_keys_2_decimal; - }; END_NAMESPACE_STIR From 869a5257c701a55df1f7c7f664c860e5d56fd656 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Sun, 12 Dec 2021 18:02:54 +0000 Subject: [PATCH 54/81] remove duplication in crystal_map code - derive GeometryBlocksOnCylindrical from DetectorCoordinateMap - in Scanner, set detector_map for BlocksOnCylindrical, and therefore remove crystal_map in ProjDataInfoBlocksOnCylindricalNoArcCorr. - remove now duplicate members in ProjDataInfoBlocksOnCylindricalNoArcCorr that were already in the Generic case - fix some functions in the Generic case to use the Scanner functions WARNING: Scanner now has a set_up() function that needs to be called after set_*() functions. This will initialise the detector map. --- documentation/release_5.0.htm | 7 +- .../GeometryBlocksOnCylindrical.cxx | 40 +++---- ...ojDataInfoBlocksOnCylindricalNoArcCorr.cxx | 112 ++---------------- .../ProjDataInfoGenericNoArcCorr.cxx | 19 +-- src/buildblock/Scanner.cxx | 57 ++++++--- .../stir/GeometryBlocksOnCylindrical.h | 31 +---- .../stir/GeometryBlocksOnCylindrical.inl | 48 -------- ...ProjDataInfoBlocksOnCylindricalNoArcCorr.h | 24 +--- .../stir/ProjDataInfoGenericNoArcCorr.h | 5 +- src/include/stir/Scanner.h | 31 ++++- src/include/stir/Scanner.inl | 62 ++++++++-- .../test_blocks_on_cylindrical_projectors.cxx | 4 +- src/test/test_proj_data_info.cxx | 30 +++-- 13 files changed, 180 insertions(+), 290 deletions(-) delete mode 100644 src/include/stir/GeometryBlocksOnCylindrical.inl diff --git a/documentation/release_5.0.htm b/documentation/release_5.0.htm index 9e59fd9b2..db349ce78 100644 --- a/documentation/release_5.0.htm +++ b/documentation/release_5.0.htm @@ -267,7 +267,12 @@

    Backward incompatibities

    in the derived classes (which has an extra argument add_sensitivity). Therefore developers that created their own derived class will need to adjust their class accordingly. The public class interface is identical though. -
  • DetectorCoordinateMapFromfile (used by SAFIR listmode files) has been renamed to DetectorCoordinateMap and moved from listmode to buildblock
  • +
  • DetectorCoordinateMapFromfile (used by SAFIR listmode files) has been renamed to DetectorCoordinateMap and moved from listmode to buildblock.
  • +
  • Scanner now has a set_up() function. This needs to be called after + using any of the set_*() functions (except set_params). For the blocks/generic + scanners, this will fill in the detector maps etc. + You will get a run-time exception if you forgot to do this. +
  • New functionality

    diff --git a/src/buildblock/GeometryBlocksOnCylindrical.cxx b/src/buildblock/GeometryBlocksOnCylindrical.cxx index 0069a649f..f80be9784 100755 --- a/src/buildblock/GeometryBlocksOnCylindrical.cxx +++ b/src/buildblock/GeometryBlocksOnCylindrical.cxx @@ -42,16 +42,10 @@ limitations under the License. START_NAMESPACE_STIR - -GeometryBlocksOnCylindrical:: -GeometryBlocksOnCylindrical() -{} - GeometryBlocksOnCylindrical:: -GeometryBlocksOnCylindrical(const shared_ptr &scanner_ptr_v): - scanner_ptr(scanner_ptr_v) +GeometryBlocksOnCylindrical(const Scanner& scanner) { - build_crystal_maps(); + build_crystal_maps(scanner); } stir::Array<2, float> @@ -67,20 +61,20 @@ get_rotation_matrix(float alpha) const void GeometryBlocksOnCylindrical:: -build_crystal_maps() +build_crystal_maps(const Scanner& scanner) { // local variables to describe scanner - int num_axial_crystals_per_block = get_scanner_ptr()->get_num_axial_crystals_per_block(); - int num_transaxial_crystals_per_block = get_scanner_ptr()->get_num_transaxial_crystals_per_block(); - int num_axial_blocks = get_scanner_ptr()->get_num_axial_blocks(); - int num_transaxial_blocks_per_bucket = get_scanner_ptr()->get_num_transaxial_blocks_per_bucket(); - int num_transaxial_buckets = get_scanner_ptr()->get_num_transaxial_blocks()/num_transaxial_blocks_per_bucket; - int num_detectors_per_ring = get_scanner_ptr()->get_num_detectors_per_ring(); - float axial_block_spacing = get_scanner_ptr()->get_axial_block_spacing(); - float transaxial_block_spacing = get_scanner_ptr()->get_transaxial_block_spacing(); - float axial_crystal_spacing = get_scanner_ptr()->get_axial_crystal_spacing(); - float transaxial_crystal_spacing = get_scanner_ptr()->get_transaxial_crystal_spacing(); - std::string scanner_orientation = get_scanner_ptr()->get_scanner_orientation(); + int num_axial_crystals_per_block = scanner.get_num_axial_crystals_per_block(); + int num_transaxial_crystals_per_block = scanner.get_num_transaxial_crystals_per_block(); + int num_axial_blocks = scanner.get_num_axial_blocks(); + int num_transaxial_blocks_per_bucket = scanner.get_num_transaxial_blocks_per_bucket(); + int num_transaxial_buckets = scanner.get_num_transaxial_blocks()/num_transaxial_blocks_per_bucket; + int num_detectors_per_ring = scanner.get_num_detectors_per_ring(); + float axial_block_spacing = scanner.get_axial_block_spacing(); + float transaxial_block_spacing = scanner.get_transaxial_block_spacing(); + float axial_crystal_spacing = scanner.get_axial_crystal_spacing(); + float transaxial_crystal_spacing = scanner.get_transaxial_crystal_spacing(); + std::string scanner_orientation = scanner.get_scanner_orientation(); det_pos_to_coord_type cartesian_coord_map_given_detection_position_keys; // check for the scanner orientation @@ -95,10 +89,10 @@ build_crystal_maps() float csi_minus_csiGaps=csi-(csi/transaxial_block_spacing*2)* (transaxial_crystal_spacing/2+trans_blocks_gap); // distance between the center of the scannner and the first crystal in the bucket, r=Reffective/cos(csi) - float r=get_scanner_ptr()->get_effective_ring_radius()/cos(csi_minus_csiGaps); + float r=scanner.get_effective_ring_radius()/cos(csi_minus_csiGaps); float start_z = 0; - float start_y = -1*get_scanner_ptr()->get_effective_ring_radius(); + float start_y = -1*scanner.get_effective_ring_radius(); float start_x = -1*r*sin(csi_minus_csiGaps);//( // ((num_transaxial_blocks_per_bucket-1)/2.)*transaxial_block_spacing // + ((num_transaxial_crystals_per_block-1)/2.)*transaxial_crystal_spacing @@ -131,7 +125,7 @@ build_crystal_maps() ax_block_num*axial_block_spacing + ax_crys_num*axial_crystal_spacing, 0., trans_block_num*transaxial_block_spacing + trans_crys_num*transaxial_crystal_spacing); - float alpha = get_scanner_ptr()->get_intrinsic_azimuthal_tilt()+ + float alpha = scanner.get_intrinsic_azimuthal_tilt()+ trans_bucket_num*(2*_PI)/num_transaxial_buckets+csi_minus_csiGaps; stir::Array<2, float> rotation_matrix = get_rotation_matrix(alpha); diff --git a/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx b/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx index 9c37908f3..5f5e6a689 100644 --- a/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx +++ b/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx @@ -61,7 +61,10 @@ ProjDataInfoBlocksOnCylindricalNoArcCorr(const shared_ptr scanner_ptr, min_ring_diff_v, max_ring_diff_v, num_views, num_tangential_poss) { - crystal_map.reset(new GeometryBlocksOnCylindrical(scanner_ptr)); + if (is_null_ptr(scanner_ptr)) + error("ProjDataInfoBlocksOnCylindricalNoArcCorr needs to be initialised with a non-empty Scanner"); + if (scanner_ptr->get_scanner_geometry() != "BlocksOnCylindrical") + error("ProjDataInfoBlocksOnCylindricalNoArcCorr needs to be initialised with a Scanner with appropriate geometry"); } ProjDataInfoBlocksOnCylindricalNoArcCorr:: @@ -75,8 +78,10 @@ ProjDataInfoBlocksOnCylindricalNoArcCorr(const shared_ptr scanner_ptr, min_ring_diff_v, max_ring_diff_v, num_views, num_tangential_poss) { - assert(!is_null_ptr(scanner_ptr)); - crystal_map.reset(new GeometryBlocksOnCylindrical(scanner_ptr)); + if (is_null_ptr(scanner_ptr)) + error("ProjDataInfoBlocksOnCylindricalNoArcCorr needs to be initialised with a non-empty Scanner"); + if (scanner_ptr->get_scanner_geometry() != "BlocksOnCylindrical") + error("ProjDataInfoBlocksOnCylindricalNoArcCorr needs to be initialised with a Scanner with appropriate geometry"); } @@ -134,8 +139,8 @@ find_scanner_coordinates_given_cartesian_coordinates(int& det1, int& det2, int& DetectionPosition<> det_pos1; DetectionPosition<> det_pos2; - if (crystal_map->find_detection_position_given_cartesian_coordinate(det_pos1, c1)==Succeeded::no || - crystal_map->find_detection_position_given_cartesian_coordinate(det_pos2, c2)==Succeeded::no) + if (get_scanner_ptr()->find_detection_position_given_cartesian_coordinate(det_pos1, c1)==Succeeded::no || + get_scanner_ptr()->find_detection_position_given_cartesian_coordinate(det_pos2, c2)==Succeeded::no) { return Succeeded::no; } @@ -155,61 +160,6 @@ find_scanner_coordinates_given_cartesian_coordinates(int& det1, int& det2, int& ? Succeeded::yes : Succeeded::no; } - -void -ProjDataInfoBlocksOnCylindricalNoArcCorr:: -find_cartesian_coordinates_of_detection( - CartesianCoordinate3D& coord_1, - CartesianCoordinate3D& coord_2, - const Bin& bin) const -{ - // find detectors - int det_num_a; - int det_num_b; - int ring_a; - int ring_b; - get_det_pair_for_bin(det_num_a, ring_a, - det_num_b, ring_b, bin); - - // find corresponding cartesian coordinates - find_cartesian_coordinates_given_scanner_coordinates(coord_1,coord_2, - ring_a,ring_b,det_num_a,det_num_b); - return; -} - -//!warning Use crystal map -void -ProjDataInfoBlocksOnCylindricalNoArcCorr:: -find_cartesian_coordinates_given_scanner_coordinates(CartesianCoordinate3D& coord_1, - CartesianCoordinate3D& coord_2, - const int Ring_A,const int Ring_B, - const int det1, const int det2) const -{ - assert(0<=det1); - assert(det1get_num_detectors_per_ring()); - assert(0<=det2); - assert(det2get_num_detectors_per_ring()); - - DetectionPosition<> det_pos1; - DetectionPosition<> det_pos2; - det_pos1.tangential_coord() = det1; - det_pos2.tangential_coord() = det2; - det_pos1.axial_coord() = Ring_A; - det_pos2.axial_coord() = Ring_B; - - if (crystal_map->find_cartesian_coordinate_given_detection_position(coord_1, det_pos1)==Succeeded::yes && - crystal_map->find_cartesian_coordinate_given_detection_position(coord_2, det_pos2)==Succeeded::yes) - { - return; - } - else - { - error("couldn't find corresponding cartesian coordinates for given detection positions.\n"); - return; - } -} - - void ProjDataInfoBlocksOnCylindricalNoArcCorr:: find_bin_given_cartesian_coordinates_of_detection(Bin& bin, @@ -247,46 +197,4 @@ find_bin_given_cartesian_coordinates_of_detection(Bin& bin, bin.set_bin_value(-1); } -//!warning Use crystal map -Bin -ProjDataInfoBlocksOnCylindricalNoArcCorr:: -get_bin(const LOR& lor) const -{ - Bin bin; - - const LORAs2Points & lor_as_2points = dynamic_cast &>(lor); - - CartesianCoordinate3D _p1 = lor_as_2points.p1(); - CartesianCoordinate3D _p2 = lor_as_2points.p2(); - - DetectionPosition<> det_pos1; - DetectionPosition<> det_pos2; - if (crystal_map->find_detection_position_given_cartesian_coordinate(det_pos1, _p1)==Succeeded::no || - crystal_map->find_detection_position_given_cartesian_coordinate(det_pos2, _p2)==Succeeded::no) - { - bin.set_bin_value(-1); - return bin; - } - - DetectionPositionPair<> det_pos_pair; - det_pos_pair.pos1() = det_pos1; - det_pos_pair.pos2() = det_pos2; - - - if (get_bin_for_det_pos_pair(bin, det_pos_pair) == Succeeded::yes&& - bin.tangential_pos_num() >= get_min_tangential_pos_num() && - bin.tangential_pos_num() <= get_max_tangential_pos_num()) - { - bin.set_bin_value(1); - return bin; - } - else - { - bin.set_bin_value(-1); - return bin; - } - -} - - END_NAMESPACE_STIR diff --git a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx index 3fd8736ff..99636f485 100644 --- a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx +++ b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx @@ -392,21 +392,8 @@ find_cartesian_coordinates_given_scanner_coordinates(CartesianCoordinate3Dget_coordinate_for_det_pos(det_pos1); coord_2 = get_scanner_ptr()->get_coordinate_for_det_pos(det_pos2); -#else - if (crystal_map->find_cartesian_coordinate_given_detection_position(coord_1, det_pos1)==Succeeded::yes && - crystal_map->find_cartesian_coordinate_given_detection_position(coord_2, det_pos2)==Succeeded::yes) - { - return; - } - else - { - error("couldn't find corresponding cartesian coordinates for given detection positions.\n"); - return; - } -#endif } @@ -423,9 +410,9 @@ get_bin(const LOR& lor) const DetectionPosition<> det_pos1; DetectionPosition<> det_pos2; - // TODO GENERICvsBLOCK crystal_map should not be used - if (crystal_map->find_detection_position_given_cartesian_coordinate(det_pos1, _p1)==Succeeded::no || - crystal_map->find_detection_position_given_cartesian_coordinate(det_pos2, _p2)==Succeeded::no) + + if (get_scanner_ptr()->find_detection_position_given_cartesian_coordinate(det_pos1, _p1)==Succeeded::no || + get_scanner_ptr()->find_detection_position_given_cartesian_coordinate(det_pos2, _p2)==Succeeded::no) { bin.set_bin_value(-1); return bin; diff --git a/src/buildblock/Scanner.cxx b/src/buildblock/Scanner.cxx index 69b5446a6..b88bbedc7 100644 --- a/src/buildblock/Scanner.cxx +++ b/src/buildblock/Scanner.cxx @@ -36,6 +36,7 @@ #include "stir/interfile_keyword_functions.h" #include "stir/info.h" #include "stir/DetectorCoordinateMap.h" +#include "stir/GeometryBlocksOnCylindrical.h" #include #include #ifdef BOOST_NO_STRINGSTREAM @@ -71,6 +72,7 @@ static list Scanner::Scanner(Type scanner_type) + : _already_setup(false) { // set_params parameters: @@ -572,6 +574,7 @@ Scanner::Scanner(Type type_v, const list& list_of_names_v, float axial_block_spacing_v, float transaxial_block_spacing_v, const std::string& crystal_map_file_name_v) +: _already_setup(false) { set_params(type_v, list_of_names_v, num_rings_v, max_num_non_arccorrected_bins_v, @@ -618,6 +621,7 @@ Scanner::Scanner(Type type_v, const string& name, float axial_block_spacing_v, float transaxial_block_spacing_v, const std::string& crystal_map_file_name_v) + : _already_setup(false) { set_params(type_v, string_list(name), num_rings_v, max_num_non_arccorrected_bins_v, @@ -750,25 +754,50 @@ set_params(Type type_v,const list& list_of_names_v, scanner_orientation = scanner_orientation_v; - if (scanner_geometry_v == "") - scanner_geometry = "Cylindrical"; - else - scanner_geometry = scanner_geometry_v; - axial_crystal_spacing = axial_crystal_spacing_v; transaxial_crystal_spacing = transaxial_crystal_spacing_v; axial_block_spacing = axial_block_spacing_v; transaxial_block_spacing = transaxial_block_spacing_v; crystal_map_file_name = crystal_map_file_name_v; - if (crystal_map_file_name != "") - { - read_detectormap_from_file(crystal_map_file_name); - } + + if (scanner_geometry_v == "") + set_scanner_geometry("Cylindrical"); else - { - this->detector_map_sptr = 0; - } + set_scanner_geometry(scanner_geometry_v); + + set_up(); +} + +void Scanner::set_scanner_geometry(const std::string& new_scanner_geometry) +{ + scanner_geometry = new_scanner_geometry; + _already_setup = false; +} + +void Scanner::set_up() +{ + if (scanner_geometry == "Generic") + { + if (crystal_map_file_name == "") + error("Scanner: scanner_geometry=Generic needs a crystal map"); + + read_detectormap_from_file(crystal_map_file_name); + } + else + { + if (crystal_map_file_name != "") + error("Scanner: use scanner_geometry=Generic when specifying a crystal map"); + if (scanner_geometry == "BlocksOnCylindrical") + this->detector_map_sptr.reset(new GeometryBlocksOnCylindrical(*this)); + else + { + this->detector_map_sptr = 0; + if (scanner_geometry != "Cylindrical") + error("Scanner::scanner_geometry needs to be one of Cylindrical, BlocksOnCylindrical, Generic"); + } + } + _already_setup = true; } void @@ -1224,15 +1253,15 @@ Scanner* Scanner::ask_parameters() //This is needed for finding effective central bin size, because it is different for different geometries. const string ScannerGeometry = ask_string("Enter the scanner geometry ( BlocksOnCylindrical / Cylindrical / Generic ) :", "Cylindrical"); - scanner_ptr->set_scanner_geometry(ScannerGeometry); if (ScannerGeometry == "Generic") { string CrystalMapFileName = ask_string("Enter the name of the crystal map: ", ""); scanner_ptr->set_crystal_map_file_name(CrystalMapFileName); - scanner_ptr->read_detectormap_from_file(CrystalMapFileName); } + // will also read detector-map from file + scanner_ptr->set_scanner_geometry(ScannerGeometry); return scanner_ptr; } diff --git a/src/include/stir/GeometryBlocksOnCylindrical.h b/src/include/stir/GeometryBlocksOnCylindrical.h index f479532e8..a14f5cea5 100644 --- a/src/include/stir/GeometryBlocksOnCylindrical.h +++ b/src/include/stir/GeometryBlocksOnCylindrical.h @@ -28,12 +28,7 @@ limitations under the License. #define __stir_GeometryBlocksOnCylindrical_H__ #include "stir/DetectorCoordinateMap.h" -#include "stir/CartesianCoordinate3D.h" -#include "stir/Scanner.h" -#include "stir/shared_ptr.h" #include "stir/Array.h" -#include -#include "stir/Succeeded.h" START_NAMESPACE_STIR @@ -56,36 +51,16 @@ class GeometryBlocksOnCylindrical: public DetectorCoordinateMap public: - //! Consstructors - GeometryBlocksOnCylindrical(); + GeometryBlocksOnCylindrical(const Scanner &scanner); - GeometryBlocksOnCylindrical(const shared_ptr &scanner_ptr_v); - - //! Destructor - ~GeometryBlocksOnCylindrical() {} - private: +private: //! Get rotation matrix for a given angle around z axis stir::Array<2, float> get_rotation_matrix(float alpha) const; //! Build crystal map in cartesian coordinate - void build_crystal_maps(); - public: - //! Get cartesian coordinate for a given detection position - inline Succeeded - find_cartesian_coordinate_given_detection_position(CartesianCoordinate3D& , - const DetectionPosition<>&) const; - private: - //! Get scanner pointer - inline const Scanner* get_scanner_ptr() const; - - -private: - //! member variables - shared_ptr scanner_ptr; + void build_crystal_maps(const Scanner& scanner); }; END_NAMESPACE_STIR -#include "stir/GeometryBlocksOnCylindrical.inl" - #endif diff --git a/src/include/stir/GeometryBlocksOnCylindrical.inl b/src/include/stir/GeometryBlocksOnCylindrical.inl deleted file mode 100644 index 31a8f6342..000000000 --- a/src/include/stir/GeometryBlocksOnCylindrical.inl +++ /dev/null @@ -1,48 +0,0 @@ - -/* -Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics - -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 projdata - - \brief Implementation of inline functions of class stir::GeometryBlocksOnCylindrical - - \author Parisa Khateri - -*/ - -START_NAMESPACE_STIR - -Succeeded -GeometryBlocksOnCylindrical:: -find_cartesian_coordinate_given_detection_position(CartesianCoordinate3D& cart_coord, - const DetectionPosition<>& det_pos) const -{ - cart_coord = this->get_coordinate_for_det_pos(det_pos); - return Succeeded::yes; -} - -const Scanner* -GeometryBlocksOnCylindrical:: -get_scanner_ptr() const -{ - return scanner_ptr.get(); -} - - - -END_NAMESPACE_STIR diff --git a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h index bc3aca2e4..ef7dc8186 100644 --- a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h +++ b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h @@ -119,31 +119,12 @@ class ProjDataInfoBlocksOnCylindricalNoArcCorr : public ProjDataInfoGenericNoArc virtual std::string parameter_info() const; - virtual - Bin - get_bin(const LOR&) const; - - //! \name set of obsolete functions to go between bins<->LORs (will disappear!) //@{ - /*! \warning These function take a different convention for the axial coordinate - compare to the get_m(), get_LOR() etc. In the current function, the axial coordinate (z) - is zero in the first ring, while for get_m() etc it is zero in the centre of the scanner. - \obsolete - */ Succeeded find_scanner_coordinates_given_cartesian_coordinates(int& det1, int& det2, int& ring1, int& ring2, const CartesianCoordinate3D& c1, const CartesianCoordinate3D& c2) const; - virtual void find_cartesian_coordinates_of_detection(CartesianCoordinate3D& coord_1, - CartesianCoordinate3D& coord_2, - const Bin& bin) const override; - - void find_cartesian_coordinates_given_scanner_coordinates (CartesianCoordinate3D& coord_1, - CartesianCoordinate3D& coord_2, - const int Ring_A,const int Ring_B, - const int det1, const int det2) const override; - void find_bin_given_cartesian_coordinates_of_detection(Bin& bin, const CartesianCoordinate3D& coord_1, const CartesianCoordinate3D& coord_2) const; @@ -153,10 +134,7 @@ class ProjDataInfoBlocksOnCylindricalNoArcCorr : public ProjDataInfoGenericNoArc virtual bool blindly_equals(const root_type * const) const; - //! used to find scanner coordinates given cartesian coordinates and vice versa - shared_ptr crystal_map; - - }; +}; END_NAMESPACE_STIR diff --git a/src/include/stir/ProjDataInfoGenericNoArcCorr.h b/src/include/stir/ProjDataInfoGenericNoArcCorr.h index 118e62108..85dd3262a 100644 --- a/src/include/stir/ProjDataInfoGenericNoArcCorr.h +++ b/src/include/stir/ProjDataInfoGenericNoArcCorr.h @@ -297,10 +297,7 @@ class ProjDataInfoGenericNoArcCorr : public ProjDataInfoGeneric inline void initialise_det1det2_to_uncompressed_view_tangpos_if_not_done_yet() const; protected: virtual bool blindly_equals(const root_type * const) const; - private: - //! \todo Has to be removed - shared_ptr crystal_map; - }; +}; END_NAMESPACE_STIR diff --git a/src/include/stir/Scanner.h b/src/include/stir/Scanner.h index 1044b4af5..c354d1648 100644 --- a/src/include/stir/Scanner.h +++ b/src/include/stir/Scanner.h @@ -91,6 +91,8 @@ class Succeeded; \todo Some scanners do not have all info filled in at present. Values are then set to 0. + \warning You have to call set_up() after using the \c set_* functions (except set_params()). + \todo a hierarchy distinguishing between different types of scanners \todo derive from ParsingObject @@ -131,7 +133,9 @@ class Scanner Advance, DiscoveryLS, DiscoveryST, DiscoverySTE, DiscoveryRX, Discovery600, PETMR_Signa, Discovery690, DiscoveryMI3ring, DiscoveryMI4ring, HZLR, RATPET, PANDA, HYPERimage, nanoPET, HRRT, Allegro, GeminiTF, SAFIRDualRingPrototype, User_defined_scanner, Unknown_scanner}; - + + virtual ~Scanner() {} + //! constructor that takes scanner type as an input argument Scanner(Type scanner_type); @@ -188,7 +192,11 @@ class Scanner float transaxial_block_spacing_v = -1.0f, const std::string& crystal_map_file_name = ""); - + //! Initialised internal geometry + /*! Currently called in the set_params() functions, but needs to be + called explicitly when afterwards using any of the other \c set_ functions + */ + virtual void set_up(); //! get scanner parameters as a std::string std::string parameter_info() const; @@ -296,6 +304,8 @@ class Scanner /*! Some scanners (including many Siemens scanners) insert virtual crystals in the sinogram data. The other members of the class return the size of the "virtual" block. With these functions you can find its true size (or set it). + + You have to call set_up() after using the \c set_* functions. */ //@{! int get_num_virtual_axial_crystals_per_block() const; @@ -339,7 +349,10 @@ class Scanner //@} (end of get detector responce info) //! \name Functions setting info - /*! Be careful to keep consistency by setting all relevant parameters*/ + /*! Be careful to keep consistency by setting all relevant parameters. + + You have to call set_up() after using any of these. + */ //@{ // zlong, 08-04-2004, add set_methods //! set scanner type @@ -384,7 +397,8 @@ class Scanner //! set scanner orientation inline void set_scanner_orientation(const std::string& new_scanner_orientation); //! set scanner geometry - inline void set_scanner_geometry(const std::string& new_scanner_geometry); + /*! Will also read the detector map from file if the geometry is \c Generic */ + void set_scanner_geometry(const std::string& new_scanner_geometry); //! set crystal spacing in axial direction inline void set_axial_crystal_spacing(const float & new_spacing); //! set crystal spacing in transaxial direction @@ -394,6 +408,7 @@ class Scanner //! set block spacing in transaxial direction inline void set_transaxial_block_spacing(const float & new_spacing); //! set crystal map file name for the generic geometry + /*! \warning, data is not read yet. use set_scanner_geometry() after calling this function */ inline void set_crystal_map_file_name(const std::string& new_crystal_map_file_name); //@} (end of block geometry info) @@ -429,8 +444,14 @@ class Scanner // used in ProjInfoDataGenericNoArcCorr.cxx for accessing the coordinates inline stir::CartesianCoordinate3D get_coordinate_for_det_pos(const stir::DetectionPosition<>& det_pos) const; //! Get the Cartesian coordinates (x,y,z) given the detection position id in the input crystal map - inline stir::CartesianCoordinate3D get_coordinate_for_index(const stir::DetectionPosition<>& det_pos) const; + inline stir::CartesianCoordinate3D get_coordinate_for_index(const stir::DetectionPosition<>& det_pos) const; + //! Find detection position at a coordinate + // used in ProjInfoDataGenericNoArcCorr.cxx for accessing the get_bin + inline Succeeded + find_detection_position_given_cartesian_coordinate(DetectionPosition<>& det_pos, + const CartesianCoordinate3D& cart_coord) const; private: + bool _already_setup; Type type; std::list list_of_names; int num_rings; /* number of direct planes */ diff --git a/src/include/stir/Scanner.inl b/src/include/stir/Scanner.inl index 45e8e2e17..85ba13477 100644 --- a/src/include/stir/Scanner.inl +++ b/src/include/stir/Scanner.inl @@ -3,7 +3,7 @@ /* Copyright (C) 2000 PARAPET partners Copyright (C) 2000- 2007, Hammersmith Imanet Ltd - Copyright (C) 2016, 2021 UCL + Copyright (C) 2016, 2021 University College London Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics This file is part of STIR. @@ -26,6 +26,7 @@ */ #include "stir/error.h" +#include "stir/Succeeded.h" START_NAMESPACE_STIR @@ -278,37 +279,44 @@ Scanner::get_crystal_map_file_name() const void Scanner::set_type(const Type & new_type) { type = new_type; + _already_setup = false; } void Scanner::set_num_rings(const int & new_num) { num_rings = new_num; + _already_setup = false; } void Scanner::set_num_detectors_per_ring(const int & new_num) { num_detectors_per_ring = new_num; + _already_setup = false; } void Scanner::set_max_num_non_arccorrected_bins(const int& new_num) { max_num_non_arccorrected_bins = new_num; + _already_setup = false; } void Scanner::set_default_num_arccorrected_bins(const int& new_num) { default_num_arccorrected_bins = new_num; + _already_setup = false; } void Scanner::set_inner_ring_radius(const float & new_radius) { inner_ring_radius = new_radius; + _already_setup = false; } void Scanner::set_average_depth_of_interaction(const float & new_depth_of_interaction) { average_depth_of_interaction = new_depth_of_interaction; + _already_setup = false; } bool Scanner::has_energy_information() const @@ -325,37 +333,44 @@ void Scanner::set_ring_spacing(const float& new_spacing) void Scanner::set_default_bin_size(const float & new_size) { bin_size = new_size; + _already_setup = false; } void Scanner::set_intrinsic_azimuthal_tilt(const float new_tilt) { intrinsic_tilt = new_tilt; + _already_setup = false; } void Scanner::set_num_transaxial_blocks_per_bucket(const int& new_num) { num_transaxial_blocks_per_bucket = new_num; + _already_setup = false; } void Scanner::set_num_axial_blocks_per_bucket(const int& new_num) { num_axial_blocks_per_bucket = new_num; + _already_setup = false; } void Scanner::set_num_detector_layers(const int& new_num) { num_detector_layers = new_num; + _already_setup = false; } void Scanner::set_num_axial_crystals_per_block(const int& new_num) { num_axial_crystals_per_block = new_num; + _already_setup = false; } void Scanner::set_num_transaxial_crystals_per_block(const int& new_num) { num_transaxial_crystals_per_block = new_num; + _already_setup = false; } @@ -363,58 +378,63 @@ void Scanner::set_num_transaxial_crystals_per_block(const int& new_num) void Scanner::set_num_axial_crystals_per_singles_unit(const int& new_num) { num_axial_crystals_per_singles_unit = new_num; + _already_setup = false; } void Scanner::set_num_transaxial_crystals_per_singles_unit(const int& new_num) { num_transaxial_crystals_per_singles_unit = new_num; + _already_setup = false; } void Scanner::set_energy_resolution(const float new_num) { energy_resolution = new_num; + _already_setup = false; } void Scanner::set_reference_energy(const float new_num) { reference_energy = new_num; + _already_setup = false; } void Scanner::set_scanner_orientation(const std::string& new_scanner_orientation) { scanner_orientation = new_scanner_orientation; -} - -void Scanner::set_scanner_geometry(const std::string& new_scanner_geometry) -{ - scanner_geometry = new_scanner_geometry; + _already_setup = false; } void Scanner::set_axial_crystal_spacing(const float& new_spacing) { axial_crystal_spacing = new_spacing; + _already_setup = false; } void Scanner::set_transaxial_crystal_spacing(const float& new_spacing) { transaxial_crystal_spacing = new_spacing; + _already_setup = false; } void Scanner::set_transaxial_block_spacing(const float& new_spacing) { transaxial_block_spacing = new_spacing; + _already_setup = false; } void Scanner::set_axial_block_spacing(const float& new_spacing) { axial_block_spacing = new_spacing; + _already_setup = false; } void Scanner::set_crystal_map_file_name(const std::string& new_crystal_map_file_name) { crystal_map_file_name = new_crystal_map_file_name; + _already_setup = false; } /******** Calculate singles bin index from detection position *********/ @@ -466,7 +486,7 @@ stir::DetectionPosition<> Scanner::get_det_pos_for_index(const stir::DetectionPosition<> & det_pos) const { if (!detector_map_sptr) - stir::error("Crystal Map not defined!"); + stir::error("Scanner: detector_map not defined. Did you run set_up()?"); return detector_map_sptr->get_det_pos_for_index(det_pos); } @@ -474,19 +494,35 @@ Scanner::get_det_pos_for_index(const stir::DetectionPosition<> & det_pos) const stir::CartesianCoordinate3D Scanner::get_coordinate_for_det_pos(const stir::DetectionPosition<>& det_pos) const { - if (!detector_map_sptr) - stir::error("Crystal Map not defined!"); + if (!_already_setup) + stir::error("Scanner: you forgot to call set_up()."); + if (!detector_map_sptr) + stir::error("Scanner: detector_map not defined. Did you run set_up()?"); - return detector_map_sptr->get_coordinate_for_det_pos(det_pos); + return detector_map_sptr->get_coordinate_for_det_pos(det_pos); } stir::CartesianCoordinate3D Scanner::get_coordinate_for_index(const stir::DetectionPosition<>& index) const { - if (!detector_map_sptr) - stir::error("Crystal Map not defined!"); + if (!_already_setup) + stir::error("Scanner: you forgot to call set_up()."); + if (!detector_map_sptr) + stir::error("Scanner: detector_map not defined. Did you run set_up()?"); + + return detector_map_sptr->get_coordinate_for_index(index); +} + +Succeeded +Scanner::find_detection_position_given_cartesian_coordinate(DetectionPosition<>& det_pos, + const CartesianCoordinate3D& cart_coord) const +{ + if (!_already_setup) + stir::error("Scanner: you forgot to call set_up()."); + if (!detector_map_sptr) + stir::error("Scanner: detector_map not defined. Did you run set_up()?"); - return detector_map_sptr->get_coordinate_for_index(index); + return detector_map_sptr->find_detection_position_given_cartesian_coordinate(det_pos, cart_coord); } END_NAMESPACE_STIR diff --git a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx index 289a3c3ec..f2bfa3a2b 100755 --- a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx +++ b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx @@ -139,7 +139,7 @@ BlocksTests::run_plane_symmetry_test(){ scannerBlocks_ptr->set_num_rings(1); scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); - + scannerBlocks_ptr->set_up(); VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); @@ -337,7 +337,7 @@ write_to_file("image_for2",*image2_sptr); scannerBlocks_ptr->set_num_rings(1); scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); - + scannerBlocks_ptr->set_up(); VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); diff --git a/src/test/test_proj_data_info.cxx b/src/test/test_proj_data_info.cxx index 4a3c5f01a..dcaed6070 100755 --- a/src/test/test_proj_data_info.cxx +++ b/src/test/test_proj_data_info.cxx @@ -448,6 +448,7 @@ ProjDataInfoTests::run_Blocks_DOI_test() shared_ptr scannerBlocks_ptr(new Scanner (Scanner::SAFIRDualRingPrototype)); scannerBlocks_ptr->set_average_depth_of_interaction(0); scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); + scannerBlocks_ptr->set_up(); VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); @@ -472,6 +473,7 @@ ProjDataInfoTests::run_Blocks_DOI_test() shared_ptr scannerBlocksDOI_ptr(new Scanner(*scannerBlocks_ptr)); scannerBlocksDOI_ptr->set_average_depth_of_interaction(0.1); + scannerBlocksDOI_ptr->set_up(); shared_ptr proj_data_info_blocks_doi5_ptr; proj_data_info_blocks_doi5_ptr.reset( new ProjDataInfoBlocksOnCylindricalNoArcCorr( @@ -552,16 +554,17 @@ ProjDataInfoTests::run_coordinate_test() scannerBlocks_ptr->set_num_rings(1); scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); - - int num_transaxial_blocks_per_bucket = scannerBlocks_ptr->get_num_transaxial_blocks_per_bucket(); - int num_transaxial_crystals_per_block = scannerBlocks_ptr->get_num_transaxial_crystals_per_block(); - int num_transaxial_buckets = scannerBlocks_ptr->get_num_transaxial_blocks()/num_transaxial_blocks_per_bucket; - float transaxial_block_spacing = scannerBlocks_ptr->get_transaxial_block_spacing(); - float transaxial_crystal_spacing = scannerBlocks_ptr->get_transaxial_crystal_spacing(); + scannerBlocks_ptr->set_up(); + + // int num_transaxial_blocks_per_bucket = scannerBlocks_ptr->get_num_transaxial_blocks_per_bucket(); + // int num_transaxial_crystals_per_block = scannerBlocks_ptr->get_num_transaxial_crystals_per_block(); + // int num_transaxial_buckets = scannerBlocks_ptr->get_num_transaxial_blocks()/num_transaxial_blocks_per_bucket; + // float transaxial_block_spacing = scannerBlocks_ptr->get_transaxial_block_spacing(); + // float transaxial_crystal_spacing = scannerBlocks_ptr->get_transaxial_crystal_spacing(); // estimate the angle covered by a bucket, alpha - float csi=_PI/num_transaxial_buckets; - float trans_blocks_gap=transaxial_block_spacing-num_transaxial_crystals_per_block*transaxial_crystal_spacing; + //float csi=_PI/num_transaxial_buckets; + //float trans_blocks_gap=transaxial_block_spacing-num_transaxial_crystals_per_block*transaxial_crystal_spacing; /*float csi_minus_csiGaps=csi-(csi/transaxial_block_spacing/2)* (transaxial_crystal_spacing/2+trans_blocks_gap);*/ @@ -581,6 +584,7 @@ ProjDataInfoTests::run_coordinate_test() scannerCyl_ptr->set_num_rings(1); scannerCyl_ptr->set_scanner_geometry("Cylindrical"); + scannerCyl_ptr->set_up(); VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); @@ -765,7 +769,8 @@ ProjDataInfoTests::run_coordinate_test_for_realistic_scanner() scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); - + scannerBlocks_ptr->set_up(); + shared_ptr scannerCyl_ptr; scannerCyl_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); scannerCyl_ptr->set_axial_block_spacing(scannerCyl_ptr->get_axial_crystal_spacing()* @@ -774,7 +779,8 @@ ProjDataInfoTests::run_coordinate_test_for_realistic_scanner() scannerCyl_ptr->get_num_transaxial_crystals_per_block()); scannerCyl_ptr->set_scanner_geometry("Cylindrical"); - + scannerCyl_ptr->set_up(); + VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); @@ -893,7 +899,8 @@ run_lor_get_s_test(){ scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); - + scannerBlocks_ptr->set_up(); + shared_ptr scannerCyl_ptr; scannerCyl_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); scannerCyl_ptr->set_axial_block_spacing(scannerCyl_ptr->get_axial_crystal_spacing()* @@ -902,6 +909,7 @@ run_lor_get_s_test(){ scannerCyl_ptr->get_num_transaxial_crystals_per_block()); scannerCyl_ptr->set_scanner_geometry("Cylindrical"); + scannerCyl_ptr->set_up(); VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); From bd63609876feaa1b5fb3220206066254b7df302f Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Sun, 12 Dec 2021 21:41:25 +0000 Subject: [PATCH 55/81] Generic/Blocks: remove obsolete variables angular_increment and ring_radius were not longer used, so I've removed them --- ...ojDataInfoBlocksOnCylindricalNoArcCorr.cxx | 19 ----------- .../ProjDataInfoGenericNoArcCorr.cxx | 24 +------------- ...ProjDataInfoBlocksOnCylindricalNoArcCorr.h | 27 ++-------------- .../stir/ProjDataInfoGenericNoArcCorr.h | 32 ++----------------- .../stir/ProjDataInfoGenericNoArcCorr.inl | 7 ---- 5 files changed, 6 insertions(+), 103 deletions(-) diff --git a/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx b/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx index 5f5e6a689..29c20ce37 100644 --- a/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx +++ b/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx @@ -48,25 +48,6 @@ ProjDataInfoBlocksOnCylindricalNoArcCorr:: ProjDataInfoBlocksOnCylindricalNoArcCorr() {} -ProjDataInfoBlocksOnCylindricalNoArcCorr:: -ProjDataInfoBlocksOnCylindricalNoArcCorr(const shared_ptr scanner_ptr, - const float ring_radius_v, const float angular_increment_v, - const VectorWithOffset& num_axial_pos_per_segment, - const VectorWithOffset& min_ring_diff_v, - const VectorWithOffset& max_ring_diff_v, - const int num_views,const int num_tangential_poss) - : ProjDataInfoGenericNoArcCorr(scanner_ptr, - ring_radius_v, angular_increment_v, - num_axial_pos_per_segment, - min_ring_diff_v, max_ring_diff_v, - num_views, num_tangential_poss) -{ - if (is_null_ptr(scanner_ptr)) - error("ProjDataInfoBlocksOnCylindricalNoArcCorr needs to be initialised with a non-empty Scanner"); - if (scanner_ptr->get_scanner_geometry() != "BlocksOnCylindrical") - error("ProjDataInfoBlocksOnCylindricalNoArcCorr needs to be initialised with a Scanner with appropriate geometry"); -} - ProjDataInfoBlocksOnCylindricalNoArcCorr:: ProjDataInfoBlocksOnCylindricalNoArcCorr(const shared_ptr scanner_ptr, const VectorWithOffset& num_axial_pos_per_segment, diff --git a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx index 99636f485..42fc89a47 100644 --- a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx +++ b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx @@ -50,24 +50,6 @@ ProjDataInfoGenericNoArcCorr:: ProjDataInfoGenericNoArcCorr() {} -ProjDataInfoGenericNoArcCorr:: -ProjDataInfoGenericNoArcCorr(const shared_ptr scanner_ptr, - const float ring_radius_v, const float angular_increment_v, - const VectorWithOffset& num_axial_pos_per_segment, - const VectorWithOffset& min_ring_diff_v, - const VectorWithOffset& max_ring_diff_v, - const int num_views,const int num_tangential_poss) -: ProjDataInfoGeneric(scanner_ptr, - num_axial_pos_per_segment, - min_ring_diff_v, max_ring_diff_v, - num_views, num_tangential_poss), - ring_radius(ring_radius_v), - angular_increment(angular_increment_v) -{ - uncompressed_view_tangpos_to_det1det2_initialised = false; - det1det2_to_uncompressed_view_tangpos_initialised = false; -} - ProjDataInfoGenericNoArcCorr:: ProjDataInfoGenericNoArcCorr(const shared_ptr scanner_ptr, const VectorWithOffset& num_axial_pos_per_segment, @@ -80,8 +62,6 @@ ProjDataInfoGenericNoArcCorr(const shared_ptr scanner_ptr, num_views, num_tangential_poss) { assert(!is_null_ptr(scanner_ptr)); - ring_radius = scanner_ptr->get_effective_ring_radius(); - angular_increment = static_cast(_PI/scanner_ptr->get_num_detectors_per_ring()); uncompressed_view_tangpos_to_det1det2_initialised = false; det1det2_to_uncompressed_view_tangpos_initialised = false; } @@ -101,9 +81,7 @@ operator==(const self_type& that) const { if (!base_type::blindly_equals(&that)) return false; - return - fabs(this->ring_radius - that.ring_radius) < 0.05F && - fabs(this->angular_increment - that.angular_increment) < 0.05F; + return true; } bool diff --git a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h index ef7dc8186..5d5ef419a 100644 --- a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h +++ b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h @@ -36,14 +36,7 @@ START_NAMESPACE_STIR class Succeeded; /*! \ingroup projdata - \brief Projection data info for data which are not arc-corrected. - - For this class, 'tangential_pos_num' actually indexes an angular coordinate - with a particular angular sampling (usually given by half the angle between - detectors). That is - \code - get_s(Bin(..., tang_pos_num)) == ring_radius * sin(tang_pos_num*angular_increment) - \endcode + \brief Projection data info for data from a scanner with discrete dtectors organised by blocks This class also contains some functions specific for (static) full-ring PET scanners. In this case, it is assumed that for 'raw' data (i.e. no mashing) @@ -77,10 +70,6 @@ class Succeeded; as 'ordered pair' (i.e. ring_difference can be positive and negative). In STIR, we use the second convention. - \todo The detector specific functions possibly do not belong in this class. - One can easily imagine a case where the theta,phi,s,t coordinates are as - described, but there is no real correspondence with detectors (for instance, - a rotating system). Maybe they should be moved somewhere else? */ class ProjDataInfoBlocksOnCylindricalNoArcCorr : public ProjDataInfoGenericNoArcCorr { @@ -95,19 +84,9 @@ class ProjDataInfoBlocksOnCylindricalNoArcCorr : public ProjDataInfoGenericNoArc public: //! Default constructor (leaves object in ill-defined state) ProjDataInfoBlocksOnCylindricalNoArcCorr(); - //! Constructor completely specifying all parameters - /*! \see ProjDataInfoCylindrical class documentation for info on parameters */ - ProjDataInfoBlocksOnCylindricalNoArcCorr(const shared_ptr scanner_ptr, - const float ring_radius, const float angular_increment, - const VectorWithOffset& num_axial_pos_per_segment, - const VectorWithOffset& min_ring_diff_v, - const VectorWithOffset& max_ring_diff_v, - const int num_views,const int num_tangential_poss); - //! Constructor which gets \a ring_radius and \a angular_increment from the scanner - /*! \a angular_increment is determined as Pi divided by the number of detectors in a ring. - \todo only suitable for full-ring PET scanners*/ - ProjDataInfoBlocksOnCylindricalNoArcCorr(const shared_ptr scanner_ptr, + //! Constructor which gets geometry from the scanner + ProjDataInfoBlocksOnCylindricalNoArcCorr(const shared_ptr scanner_ptr, const VectorWithOffset& num_axial_pos_per_segment, const VectorWithOffset& min_ring_diff_v, const VectorWithOffset& max_ring_diff_v, diff --git a/src/include/stir/ProjDataInfoGenericNoArcCorr.h b/src/include/stir/ProjDataInfoGenericNoArcCorr.h index 85dd3262a..621413f01 100644 --- a/src/include/stir/ProjDataInfoGenericNoArcCorr.h +++ b/src/include/stir/ProjDataInfoGenericNoArcCorr.h @@ -34,14 +34,7 @@ START_NAMESPACE_STIR class Succeeded; /*! \ingroup projdata - \brief Projection data info for data which are not arc-corrected. - - For this class, 'tangential_pos_num' actually indexes an angular coordinate - with a particular angular sampling (usually given by half the angle between - detectors). That is - \code - get_s(Bin(..., tang_pos_num)) == ring_radius * sin(tang_pos_num*angular_increment) - \endcode + \brief Projection data info for data for a scanner with discrete detectors This class also contains some functions specific for (static) full-ring PET scanners. In this case, it is assumed that for 'raw' data (i.e. no mashing) @@ -74,11 +67,6 @@ class Succeeded; - have 2 sinograms of the same size as in 2D, together with the rings as 'ordered pair' (i.e. ring_difference can be positive and negative). In STIR, we use the second convention. - - \todo The detector specific functions possibly do not belong in this class. - One can easily imagine a case where the theta,phi,s,t coordinates are as - described, but there is no real correspondence with detectors (for instance, - a rotating system). Maybe they should be moved somewhere else? */ class ProjDataInfoGenericNoArcCorr : public ProjDataInfoGeneric { @@ -93,18 +81,8 @@ class ProjDataInfoGenericNoArcCorr : public ProjDataInfoGeneric public: //! Default constructor (leaves object in ill-defined state) ProjDataInfoGenericNoArcCorr(); - //! Constructor completely specifying all parameters - /*! \see ProjDataInfoCylindrical class documentation for info on parameters */ - ProjDataInfoGenericNoArcCorr(const shared_ptr scanner_ptr, - const float ring_radius, const float angular_increment, - const VectorWithOffset& num_axial_pos_per_segment, - const VectorWithOffset& min_ring_diff_v, - const VectorWithOffset& max_ring_diff_v, - const int num_views,const int num_tangential_poss); - //! Constructor which gets \a ring_radius and \a angular_increment from the scanner - /*! \a angular_increment is determined as Pi divided by the number of detectors in a ring. - \todo only suitable for full-ring PET scanners*/ + //! Constructor which gets geometry from the scanner ProjDataInfoGenericNoArcCorr(const shared_ptr scanner_ptr, const VectorWithOffset& num_axial_pos_per_segment, const VectorWithOffset& min_ring_diff_v, @@ -122,9 +100,6 @@ class ProjDataInfoGenericNoArcCorr : public ProjDataInfoGeneric */ inline virtual float get_s(const Bin&) const; - //! Gets angular increment (in radians) - inline float get_angular_increment() const; - virtual std::string parameter_info() const; //! \name Functions that convert between bins and detection positions @@ -273,9 +248,6 @@ class ProjDataInfoGenericNoArcCorr : public ProjDataInfoGeneric private: - float ring_radius; - float angular_increment; - // used in get_view_tangential_pos_num_for_det_num_pair() struct Det1Det2 { int det1_num; int det2_num; }; mutable VectorWithOffset< VectorWithOffset > uncompressed_view_tangpos_to_det1det2; diff --git a/src/include/stir/ProjDataInfoGenericNoArcCorr.inl b/src/include/stir/ProjDataInfoGenericNoArcCorr.inl index a05b31b48..f2363b465 100644 --- a/src/include/stir/ProjDataInfoGenericNoArcCorr.inl +++ b/src/include/stir/ProjDataInfoGenericNoArcCorr.inl @@ -126,13 +126,6 @@ get_s(const Bin& bin) const } } -float -ProjDataInfoGenericNoArcCorr:: -get_angular_increment() const -{ - return angular_increment; -} - void ProjDataInfoGenericNoArcCorr:: get_det_num_pair_for_view_tangential_pos_num( From be9f8e896458dd12ed3881216e1815b672412f2d Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Mon, 13 Dec 2021 09:11:13 +0000 Subject: [PATCH 56/81] add missing include --- src/include/stir/DetectorCoordinateMap.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/include/stir/DetectorCoordinateMap.h b/src/include/stir/DetectorCoordinateMap.h index e27a88c0d..c81553f4b 100644 --- a/src/include/stir/DetectorCoordinateMap.h +++ b/src/include/stir/DetectorCoordinateMap.h @@ -24,6 +24,7 @@ #include #include #include +#include #include #include From 02c7e4a7c4ccc10c60a47036c724402193d8dae9 Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Mon, 13 Dec 2021 12:19:28 +0000 Subject: [PATCH 57/81] support multiple buckets axially --- src/buildblock/GeometryBlocksOnCylindrical.cxx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/buildblock/GeometryBlocksOnCylindrical.cxx b/src/buildblock/GeometryBlocksOnCylindrical.cxx index f80be9784..2f8fdf240 100755 --- a/src/buildblock/GeometryBlocksOnCylindrical.cxx +++ b/src/buildblock/GeometryBlocksOnCylindrical.cxx @@ -68,7 +68,9 @@ build_crystal_maps(const Scanner& scanner) int num_transaxial_crystals_per_block = scanner.get_num_transaxial_crystals_per_block(); int num_axial_blocks = scanner.get_num_axial_blocks(); int num_transaxial_blocks_per_bucket = scanner.get_num_transaxial_blocks_per_bucket(); + int num_axial_blocks_per_bucket = scanner.get_num_axial_blocks_per_bucket(); int num_transaxial_buckets = scanner.get_num_transaxial_blocks()/num_transaxial_blocks_per_bucket; + int num_axial_buckets = scanner.get_num_axial_blocks()/num_axial_blocks_per_bucket; int num_detectors_per_ring = scanner.get_num_detectors_per_ring(); float axial_block_spacing = scanner.get_axial_block_spacing(); float transaxial_block_spacing = scanner.get_transaxial_block_spacing(); @@ -100,11 +102,12 @@ build_crystal_maps(const Scanner& scanner) stir::CartesianCoordinate3D start_point(start_z, start_y, start_x); - for (int ax_block_num=0; ax_block_num det_pos(tangential_coord, axial_coord, radial_coord); From bab46a69ea686eee327b79de7d088549a9dcb831 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Thu, 16 Dec 2021 00:17:08 +0000 Subject: [PATCH 58/81] git ignore more output of SAFIR test script --- examples/SAFIR_genericScanner/.gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/SAFIR_genericScanner/.gitignore b/examples/SAFIR_genericScanner/.gitignore index 29d8de657..8fd467b83 100644 --- a/examples/SAFIR_genericScanner/.gitignore +++ b/examples/SAFIR_genericScanner/.gitignore @@ -1,3 +1,4 @@ test_generic_implementation* test_forward* -test_back.*v \ No newline at end of file +test_*.*v +sens_*.*v \ No newline at end of file From 12a400a5667521c1e5caf8a757d19534e09d7f37 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Thu, 16 Dec 2021 00:53:13 +0000 Subject: [PATCH 59/81] Re-work SAFIR listmode code - make the "map" optional. If it is not present, use the map from the scanner in the template (the template wasn't used before). - simplify code in get_bin - improve documentation a tiny bit Functionality should still be the same as before, but code is smaller and (hopefully) clearer. An advantage is that a map no longer needs to be specified for BlocksOnCylindrical. --- .../listmode_input_SAFIR.par | 1 - src/include/stir/DetectorCoordinateMap.h | 2 + .../stir/IO/SAFIRCListmodeInputFileFormat.h | 21 +-- src/include/stir/Scanner.h | 4 + src/include/stir/listmode/CListRecordSAFIR.h | 35 +++-- .../stir/listmode/CListRecordSAFIR.inl | 130 ++++++++---------- .../CListModeDataSAFIR.cxx | 59 +++++--- 7 files changed, 131 insertions(+), 121 deletions(-) diff --git a/examples/SAFIR_genericScanner/listmode_input_SAFIR.par b/examples/SAFIR_genericScanner/listmode_input_SAFIR.par index ef925270f..5fd11a2fc 100644 --- a/examples/SAFIR_genericScanner/listmode_input_SAFIR.par +++ b/examples/SAFIR_genericScanner/listmode_input_SAFIR.par @@ -1,5 +1,4 @@ CListModeDataSAFIR Parameters:= listmode data filename:= coincidencesLM.clm.safir - crystal map filename:= DualRingPrototype_crystal_map.txt template projection data filename:= muppet.hs END CListModeDataSAFIR Parameters:= diff --git a/src/include/stir/DetectorCoordinateMap.h b/src/include/stir/DetectorCoordinateMap.h index c81553f4b..6e5006b02 100644 --- a/src/include/stir/DetectorCoordinateMap.h +++ b/src/include/stir/DetectorCoordinateMap.h @@ -43,6 +43,8 @@ class Succeeded; \par Format: ring,detector,(layer,)x,y,z An empty line will terminate the reading at that line. + + Optionally LOR end-points can be randomly displaced using a Gaussian distribution with standard deviation \sigma (in mm). */ class DetectorCoordinateMap { diff --git a/src/include/stir/IO/SAFIRCListmodeInputFileFormat.h b/src/include/stir/IO/SAFIRCListmodeInputFileFormat.h index ff23c886e..f9909cd94 100644 --- a/src/include/stir/IO/SAFIRCListmodeInputFileFormat.h +++ b/src/include/stir/IO/SAFIRCListmodeInputFileFormat.h @@ -48,20 +48,24 @@ START_NAMESPACE_STIR -/*! Class for reading SAFIR coincidence listmode data. +/*! \brief Class for reading SAFIR coincidence listmode data. It reads a parameter file, which refers to - - crystal map containing the mapping between detector index triple and cartesian coordinates of the crystal surfaces (see DetectorCoordinateMap) + - optional crystal map containing the mapping between detector index triple and cartesian coordinates of the crystal surfaces (see DetectorCoordinateMap) - the binary data file with the coincidence listmode data in SAFIR format (see CListModeDataSAFIR) - - a template projection data file, which is used to generate the virtual cylindrical scanner + - a template projection data file, which defines the scanner + + If the map is not defined, the scanner detectors will be used. Otherwise, the nearest LOR of the scanner will be selected for each event. An example of such a parameter file would be \code CListModeDataSAFIR Parameters:= listmode data filename:= listmode_input.clm.safir - ; the following two examples are also default to the key parser - crystal map filename:= crystal_map_front.txt - template projection data filename:= safir_20.hs + template projection data filename:= + ; optional map specifying the actual location of the crystals + crystal map filename:= crystal_map.txt + ; optional random displacement of the LOR end-points in mm (only used of a map is present) + LOR randomization (Gaussian) sigma:=0 END CListModeDataSAFIR Parameters:= \endcode @@ -144,8 +148,8 @@ class SAFIRCListmodeInputFileFormat : public InputFileFormat, publ void set_defaults() { base_type::set_defaults(); - crystal_map_filename = "crystal_map_front.txt"; - template_proj_data_filename = "safir_20.hs"; + crystal_map_filename = ""; + template_proj_data_filename = ""; lor_randomization_sigma = 0.0; } @@ -161,7 +165,6 @@ class SAFIRCListmodeInputFileFormat : public InputFileFormat, publ bool post_processing() { if( !file_exists(listmode_filename) ) return true; - else if( !file_exists(crystal_map_filename) ) return true; else if( !file_exists(template_proj_data_filename) ) return true; else { did_parsing = true; diff --git a/src/include/stir/Scanner.h b/src/include/stir/Scanner.h index c354d1648..5846f6b32 100644 --- a/src/include/stir/Scanner.h +++ b/src/include/stir/Scanner.h @@ -450,6 +450,10 @@ class Scanner inline Succeeded find_detection_position_given_cartesian_coordinate(DetectionPosition<>& det_pos, const CartesianCoordinate3D& cart_coord) const; + + shared_ptr get_detector_map_sptr() const + { return detector_map_sptr; } + private: bool _already_setup; Type type; diff --git a/src/include/stir/listmode/CListRecordSAFIR.h b/src/include/stir/listmode/CListRecordSAFIR.h index b5b729158..cc0e4bc5e 100644 --- a/src/include/stir/listmode/CListRecordSAFIR.h +++ b/src/include/stir/listmode/CListRecordSAFIR.h @@ -50,7 +50,7 @@ START_NAMESPACE_STIR /*! -Provides interface of the record class to STIR by implementing get_LOR(). It uses a map from detector indices to coordinates to specify LORAs2Points from given detection pair indices. +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. The record has the following format (for little-endian byte order) \code @@ -69,36 +69,41 @@ template class CListEventSAFIR : public CListEvent { public: - /*! Constructor which initializes map upon construction. + /*! Default constructor will not work as it does not initialize a map to relate + detector indices and space coordinates. Always use either set_scanner_sptr or set_map_sptr after default construction. */ - inline CListEventSAFIR( shared_ptr map ) : map(map) {} - + inline CListEventSAFIR( ) {} + //! Returns LOR corresponding to the given event. inline virtual LORAs2Points get_LOR() const; + + //! Override the default implementation + inline virtual void get_bin(Bin& bin, const ProjDataInfo& proj_data_info) const; + //! This method checks if the template is valid for LmToProjData /*! Used before the actual processing of the data (see issue #61), before calling get_bin() * Most scanners have listmode data that correspond to non arc-corrected data and * this check avoids a crash when an unsupported template is used as input. */ - - //! author Parisa Khateri - //! Override the default implementation - inline virtual void get_bin(Bin& bin, const ProjDataInfo& proj_data_info) const; - inline virtual bool is_valid_template(const ProjDataInfo&) const {return true;} //! Returns 0 if event is prompt and 1 if random/delayed inline bool is_prompt() const { return !(static_cast(this)->is_prompt()); } //! Function to set map for detector indices to coordinates. - inline void set_map( shared_ptr new_map ) { map = new_map; } + /*! Use a null pointer to disable the mapping functionality */ + inline void set_map_sptr( shared_ptr new_map_sptr ) { map_sptr = new_map_sptr; } + /*! Set the scanner */ + /*! Currently only used if the map is not set. */ + inline void set_scanner_sptr( shared_ptr new_scanner_sptr ) { scanner_sptr = new_scanner_sptr; } + private: friend class CListRecordSAFIR; - /*! Default constructor will not work as it does not initialize a map to relate - detector indices and space coordinates. Always use other constructor with a map pointer. Or use set_map( shared_ptr new_map ) after default construction. - */ - inline CListEventSAFIR( ) {} - shared_ptr map; + shared_ptr map_sptr; + shared_ptr scanner_sptr; + + const DetectorCoordinateMap& map_to_use() const + { return map_sptr ? *map_sptr : *this->scanner_sptr->get_detector_map_sptr(); } }; diff --git a/src/include/stir/listmode/CListRecordSAFIR.inl b/src/include/stir/listmode/CListRecordSAFIR.inl index 85adaa52d..76ffbf565 100644 --- a/src/include/stir/listmode/CListRecordSAFIR.inl +++ b/src/include/stir/listmode/CListRecordSAFIR.inl @@ -5,6 +5,7 @@ Copyright 2015 ETH Zurich, Institute of Particle Physics Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics 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. @@ -19,6 +20,17 @@ limitations under the License. */ +/*! + + \file + \ingroup listmode + \brief Inline implementation of class stir::CListEventSAFIR and stir::CListRecordSAFIR with supporting classes + + \author Jannis Fischer + \author Parisa Khateri + \author Kris Thielemans +*/ + #include #include "stir/LORCoordinates.h" @@ -44,91 +56,57 @@ CListEventSAFIR::get_LOR() const static_cast(this)->get_data().get_detection_position_pair(det_pos_pair); - if(!map) stir::error("Crystal map not set."); + 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()); - lor.p1() = map->get_coordinate_for_index(det_pos_pair.pos1()); - lor.p2() = map->get_coordinate_for_index(det_pos_pair.pos2()); + return lor; +} - return lor; +namespace detail +{ +template +static inline bool +get_bin_for_det_pos_pair(Bin& bin, DetectionPositionPair<>& det_pos_pair, const ProjDataInfo& proj_data_info) +{ + if (auto proj_data_info_ptr = dynamic_cast(&proj_data_info)) + { + if (proj_data_info_ptr->get_bin_for_det_pos_pair(bin, det_pos_pair) == Succeeded::yes) + bin.set_bin_value(1); + else + bin.set_bin_value(-1); + return true; + } + else + return false; } +} // namespace detail -//! author Parisa Khateri -//! Overrides the default implementation to use get_detection_position() which should be faster. template void -CListEventSAFIR:: -get_bin(Bin& bin, const ProjDataInfo& proj_data_info) const +CListEventSAFIR::get_bin(Bin& bin, const ProjDataInfo& proj_data_info) const { - DetectionPositionPair<> det_pos_pair; - static_cast(this)->get_data().get_detection_position_pair(det_pos_pair); + DetectionPositionPair<> det_pos_pair; + static_cast(this)->get_data().get_detection_position_pair(det_pos_pair); - //check aligned detectors - if (det_pos_pair.pos1().tangential_coord() == det_pos_pair.pos2().tangential_coord()) - { - /*std::cerr<<"WARNING: aligned detectors: det1="<get_scanner_geometry() == "Generic" && bin.get_bin_value() != -1) - { - const ProjDataInfoGenericNoArcCorr& proj_data_info_gen = - dynamic_cast(proj_data_info); - //transform det_pos_pair into stir coordinates - DetectionPosition<> pos1 = det_pos_pair.pos1(); - DetectionPosition<> pos2 = det_pos_pair.pos2(); - det_pos_pair.pos1() = proj_data_info_gen.get_scanner_ptr()->get_det_pos_for_index(pos1); - det_pos_pair.pos2() = proj_data_info_gen.get_scanner_ptr()->get_det_pos_for_index(pos2); - - if (proj_data_info_gen.get_bin_for_det_pos_pair(bin, det_pos_pair) == Succeeded::yes) - bin.set_bin_value(1); - else - bin.set_bin_value(-1); - } - else - { - if(!map) stir::error("Crystal map not set."); - - stir::CartesianCoordinate3D c1 = map->get_coordinate_for_index(det_pos_pair.pos1()); - stir::CartesianCoordinate3D c2 = map->get_coordinate_for_index(det_pos_pair.pos2()); - int det1, det2, ring1, ring2; - - if(proj_data_info.get_scanner_ptr()->get_scanner_geometry() == "Cylindrical" - && bin.get_bin_value()!=-1) - { - //const ProjDataInfoCylindricalNoArcCorr& proj_data_info_cyl = - // dynamic_cast(proj_data_info); - - LORAs2Points lor; - lor.p1() = c1; - lor.p2() = c2; - bin = proj_data_info.get_bin(lor); - } - else if(proj_data_info.get_scanner_ptr()->get_scanner_geometry() == "BlocksOnCylindrical" - && bin.get_bin_value()!=-1) - { - const ProjDataInfoBlocksOnCylindricalNoArcCorr& proj_data_info_blk = - dynamic_cast(proj_data_info); - - if (proj_data_info_blk.find_scanner_coordinates_given_cartesian_coordinates(det1, det2, ring1, ring2, c1, c2) == Succeeded::no) - bin.set_bin_value(-1); - else + if (!map_sptr) + { + // transform det_pos_pair into stir conventions + det_pos_pair.pos1() = map_to_use().get_det_pos_for_index(det_pos_pair.pos1()); + det_pos_pair.pos2() = map_to_use().get_det_pos_for_index(det_pos_pair.pos2()); + + if (!detail::get_bin_for_det_pos_pair(bin, det_pos_pair, proj_data_info)) { - assert(!(ring1<0 || - ring1>=proj_data_info_blk.get_scanner_ptr()->get_num_rings() || - ring2<0 || - ring2>=proj_data_info_blk.get_scanner_ptr()->get_num_rings()) - ); - - if(proj_data_info_blk.get_bin_for_det_pair(bin, det1, ring1, det2, ring2)==Succeeded::yes) - bin.set_bin_value(1); - else - bin.set_bin_value(-1); - } - } - } + if (!detail::get_bin_for_det_pos_pair(bin, det_pos_pair, proj_data_info)) + error("Wrong type of proj-data-info for SAFIR"); + } + } + else + { + 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()); + const LORAs2Points lor(c1, c2); + bin = proj_data_info.get_bin(lor); + } } void CListEventDataSAFIR::get_detection_position_pair(DetectionPositionPair<>& det_pos_pair) diff --git a/src/listmode_buildblock/CListModeDataSAFIR.cxx b/src/listmode_buildblock/CListModeDataSAFIR.cxx index b595348de..d46ba09c3 100644 --- a/src/listmode_buildblock/CListModeDataSAFIR.cxx +++ b/src/listmode_buildblock/CListModeDataSAFIR.cxx @@ -4,6 +4,7 @@ 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. @@ -17,7 +18,15 @@ Coincidence LM Data Class for SAFIR: Implementation 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 +*/ #include #include #include @@ -40,23 +49,32 @@ using std::istream; START_NAMESPACE_STIR; - template -CListModeDataSAFIR:: -CListModeDataSAFIR(const std::string& listmode_filename, const std::string& crystal_map_filename, const std::string& template_proj_data_filename, const double lor_randomization_sigma) - : listmode_filename(listmode_filename), map(MAKE_SHARED(crystal_map_filename, lor_randomization_sigma)) +CListModeDataSAFIR::CListModeDataSAFIR(const std::string& listmode_filename, + const std::string& crystal_map_filename, + const std::string& template_proj_data_filename, + const double lor_randomization_sigma) + : listmode_filename(listmode_filename) { - this->exam_info_sptr.reset(new ExamInfo); - - // Here we are reading the scanner data from the template projdata - 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 ) - { - error("CListModeDataSAFIR: Could not open listmode file " +listmode_filename + "\n"); - } + if (!crystal_map_filename.empty()) + { + map = MAKE_SHARED(crystal_map_filename, lor_randomization_sigma); + } + else + { + if (lor_randomization_sigma != 0) + error("SAFIR currently does not support LOR-randomisation unless a map is specified"); + } + this->exam_info_sptr.reset(new ExamInfo); + + // Here we are reading the scanner data from the template projdata + 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) + { + error("CListModeDataSAFIR: Could not open listmode file " + listmode_filename + "\n"); + } } @@ -73,9 +91,10 @@ shared_ptr CListModeDataSAFIR:: get_empty_record_sptr() const { - shared_ptr sptr(new CListRecordSAFIR); - sptr->event_SAFIR().set_map(map); - return static_pointer_cast(sptr); + shared_ptr sptr(new CListRecordSAFIR); + sptr->event_SAFIR().set_scanner_sptr(this->get_proj_data_info_sptr()->get_scanner_sptr()); + sptr->event_SAFIR().set_map_sptr(map); + return static_pointer_cast(sptr); } template @@ -85,8 +104,8 @@ get_next_record(CListRecord& record_of_general_type) const { CListRecordT& record = static_cast(record_of_general_type); Succeeded status = current_lm_data_ptr->get_next_record(record); - if( status == Succeeded::yes ) record.event_SAFIR().set_map(map); - return status; + // if( status == Succeeded::yes ) record.event_SAFIR().set_map_sptr(map); + return status; } From d188992708232caeb21050bbf53c652d6557c6d9 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Thu, 16 Dec 2021 11:26:54 +0000 Subject: [PATCH 60/81] add scanner.set_up() --- examples/python/plot_scanner_LORs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/python/plot_scanner_LORs.py b/examples/python/plot_scanner_LORs.py index 369977793..adf9bc819 100644 --- a/examples/python/plot_scanner_LORs.py +++ b/examples/python/plot_scanner_LORs.py @@ -66,6 +66,7 @@ # scanner.set_intrinsic_azimuthal_tilt(-csi_minus_csiGaps) #if you want to play with the orientation of the blocks +scanner.set_up() #%% Create projection data info for Blocks on Cylindrical for i in range(0,2*Nr-1,1 ): min_r_diff[i]=-Nr+1+i From 1bf66a765d889702ea4306b4c8769ca4c7559ebe Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Thu, 16 Dec 2021 11:27:14 +0000 Subject: [PATCH 61/81] blocks: disable sorting of detectors for now --- src/buildblock/DetectorCoordinateMap.cxx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/buildblock/DetectorCoordinateMap.cxx b/src/buildblock/DetectorCoordinateMap.cxx index 6f9fd41be..a9b200dd1 100644 --- a/src/buildblock/DetectorCoordinateMap.cxx +++ b/src/buildblock/DetectorCoordinateMap.cxx @@ -118,8 +118,13 @@ void DetectorCoordinateMap::set_detector_map( const DetectorCoordinateMap::det_p stir::DetectionPosition<> detpos(0,0,0); for(std::vector::iterator it = coords_to_be_sorted.begin(); it != coords_to_be_sorted.end();++it) { +#if 0 input_index_to_det_pos[map_for_sorting_coordinates[*it]] = detpos; auto cart_coord = coord_map.at(map_for_sorting_coordinates[*it]); +#else + input_index_to_det_pos[detpos] = detpos; + auto cart_coord = coord_map.at(detpos); +#endif // rounding cart_coord to 3 and 2 decimal points then filling maps cart_coord.z() = (round(cart_coord.z()*1000.0F))/1000.0F; cart_coord.y() = (round(cart_coord.y()*1000.0F))/1000.0F; From 1cd049bc728b12ca9fecc6f72b977da64c557181 Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Thu, 16 Dec 2021 14:32:26 +0000 Subject: [PATCH 62/81] adding test_DetectorCoordinateMap.cxx --- examples/python/plot_scanner_LORs.py | 6 +- .../GeometryBlocksOnCylindrical.cxx | 2 +- src/test/CMakeLists.txt | 1 + src/test/test_DetectorCoordinateMap.cxx | 208 ++++++++++++++++++ 4 files changed, 213 insertions(+), 4 deletions(-) create mode 100644 src/test/test_DetectorCoordinateMap.cxx diff --git a/examples/python/plot_scanner_LORs.py b/examples/python/plot_scanner_LORs.py index adf9bc819..31ab67d6e 100644 --- a/examples/python/plot_scanner_LORs.py +++ b/examples/python/plot_scanner_LORs.py @@ -33,12 +33,13 @@ #%% definition of useful objects and variables scanner=stir.Scanner_get_scanner_from_name('SAFIRDualRingPrototype') -scanner.set_num_transaxial_blocks_per_bucket(2) +scanner.set_num_transaxial_blocks_per_bucket(1) scanner.set_intrinsic_azimuthal_tilt(0) # scanner.set_num_axial_crystals_per_block(1) # scanner.set_axial_block_spacing(scanner.get_axial_crystal_spacing()*scanner.get_num_axial_crystals_per_block()); # scanner.set_num_rings(1) scanner.set_scanner_geometry("BlocksOnCylindrical") +# scanner.set_up(); Nr=scanner.get_num_rings() Nv=scanner.get_max_num_views() @@ -64,8 +65,7 @@ csi_minus_csiGaps=csi-(csi/tBl_s*2)*(tC_s/2+tBl_gap) rmax =r/math.cos(csi_minus_csiGaps) -# scanner.set_intrinsic_azimuthal_tilt(-csi_minus_csiGaps) #if you want to play with the orientation of the blocks - +scanner.set_intrinsic_azimuthal_tilt(-csi_minus_csiGaps) #if you want to play with the orientation of the blocks scanner.set_up() #%% Create projection data info for Blocks on Cylindrical for i in range(0,2*Nr-1,1 ): diff --git a/src/buildblock/GeometryBlocksOnCylindrical.cxx b/src/buildblock/GeometryBlocksOnCylindrical.cxx index 2f8fdf240..e9009ae5c 100755 --- a/src/buildblock/GeometryBlocksOnCylindrical.cxx +++ b/src/buildblock/GeometryBlocksOnCylindrical.cxx @@ -125,7 +125,7 @@ build_crystal_maps(const Scanner& scanner) int radial_coord = 0; stir::DetectionPosition<> det_pos(tangential_coord, axial_coord, radial_coord); - //calculate cartesion coordinate for a given detector + //calculate cartesian coordinate for a given detector stir::CartesianCoordinate3D transformation_matrix( ax_block_num*axial_block_spacing + ax_crys_num*axial_crystal_spacing, 0., diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index 32c95cbe7..1b5b9b0bb 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -58,6 +58,7 @@ set(buildblock_simple_tests test_find_fwhm_in_image.cxx test_DetectionPosition.cxx test_proj_data_info.cxx + test_DetectorCoordinateMap.cxx test_proj_data.cxx test_proj_data_maths.cxx test_export_array.cxx diff --git a/src/test/test_DetectorCoordinateMap.cxx b/src/test/test_DetectorCoordinateMap.cxx new file mode 100644 index 000000000..c64482d5c --- /dev/null +++ b/src/test/test_DetectorCoordinateMap.cxx @@ -0,0 +1,208 @@ +/*! + + \file + \ingroup recontest + + \brief Test program for detection position map using stir::ProjDataInfoBlockOnCylindrical + + \author Daniel Deidda + +*/ +/* Copyright (C) 2021, National Physical Laboratory + This file is part of STIR. + + SPDX-License-Identifier: Apache-2.0 AND License-ref-PARAPET-license + + See STIR/LICENSE.txt for details +*/ + +#include "stir/info.h" +#include "stir/make_array.h" +#include "stir/ProjDataInMemory.h" +#include "stir/DiscretisedDensity.h" +#include "stir/ProjDataInterfile.h" +#include "stir/recon_buildblock/ProjMatrixElemsForOneBin.h" +#include "stir/recon_buildblock/ProjMatrixByBinUsingRayTracing.h" +#include "stir/ExamInfo.h" +#include "stir/LORCoordinates.h" +#include "stir/ProjDataInfo.h" +#include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" +#include "stir/ProjDataInfoCylindricalNoArcCorr.h" +#include "stir/recon_buildblock/ProjMatrixByBinUsingRayTracing.h" +#include "stir/Sinogram.h" +#include "stir/Viewgram.h" +#include "stir/Succeeded.h" +#include "stir/RunTests.h" +#include "stir/Scanner.h" +#include "stir/copy_fill.h" +#include "stir/IndexRange3D.h" +#include "stir/CPUTimer.h" +#include "stir/Shape/Shape3DWithOrientation.h" +#include "stir/Shape/Ellipsoid.h" +#include "stir/Shape/Box3D.h" +#include "stir/VoxelsOnCartesianGrid.h" +#include "stir/recon_buildblock/ForwardProjectorByBin.h" +#include "stir/recon_buildblock/ForwardProjectorByBinUsingProjMatrixByBin.h" +#include "stir/IO/write_to_file.h" +//#include "stir/Shape/Shape3D.h" + +START_NAMESPACE_STIR + + +/*! + \ingroup test + \brief Test class for Blocks +*/ +class DetectionPosMapTests: public RunTests +{ +public: + void run_tests(); + float calculate_angle_within_half_bucket(const shared_ptr scanner_ptr, + const shared_ptr proj_data_info_ptr); +private: + void run_coordinate_test_for_different_block_orientation(); + void run_map_orientation_test(); +}; + +float +DetectionPosMapTests::calculate_angle_within_half_bucket(const shared_ptr scanner_ptr, + const shared_ptr proj_data_info_ptr){ + Bin bin; + LORInAxialAndNoArcCorrSinogramCoordinates lorB; + float csi; + float C_spacing=scanner_ptr->get_transaxial_crystal_spacing(); + float csi_crystal=std::atan((C_spacing)/scanner_ptr->get_effective_ring_radius()); +// float bucket_spacing=scanner_ptr->get_transaxial_block_spacing()*C_spacing; +// float blocks_gap=scanner_ptr->get_transaxial_block_spacing() +// -scanner_ptr->get_num_transaxial_crystals_per_block()*C_spacing; +// float csi_gap=std::atan((blocks_gap)/scanner_ptr->get_effective_ring_radius()); + +// get angle within half bucket + for (int view = 0; view <= scanner_ptr->get_max_num_views(); view++){ + int bucket_num=view/(scanner_ptr->get_num_transaxial_crystals_per_block()*scanner_ptr->get_num_transaxial_blocks_per_bucket()); + if (bucket_num>0) + break; + + bin.segment_num() = 0; + bin.axial_pos_num() = 0; + bin.view_num() = view; + bin.tangential_pos_num() = 0; + + proj_data_info_ptr->get_LOR(lorB,bin); + csi=lorB.phi(); + } + return (csi+csi_crystal)/2; +} + +/*! + The following test checks that the y position of the detectors in buckets that are parallel to the x axis are the same. + The calculation of csi is only valid for the scanner defined in stir::Scanner, if we modify the number of blocks per bucket + csi will be affected. However this does not happen when csi is calculated in the same way we do in the crystal map. +*/ +void +DetectionPosMapTests::run_coordinate_test_for_different_block_orientation() +{ + CPUTimer timer; + shared_ptr scannerBlocks_ptr; + scannerBlocks_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + + scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); + scannerBlocks_ptr->set_num_transaxial_blocks_per_bucket(1); + scannerBlocks_ptr->set_up(); + + + + VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + + for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ + min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + if (iget_num_rings()) + num_axial_pos_per_segment[i]=i+1; + else + num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; + } + + shared_ptr proj_data_info_blocks_ptr; + proj_data_info_blocks_ptr.reset( + new ProjDataInfoBlocksOnCylindricalNoArcCorr( + scannerBlocks_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + scannerBlocks_ptr->get_max_num_views(), + scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); + + Bin bin, bin0=Bin(0,0,0,0); + CartesianCoordinate3D< float> b1,b2,b01,b02; + + // estimate the angle covered by half bucket, csi + float csi; + csi=calculate_angle_within_half_bucket(scannerBlocks_ptr, + proj_data_info_blocks_ptr); + + shared_ptr scannerBlocks_firstFlat_ptr; + scannerBlocks_firstFlat_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + scannerBlocks_firstFlat_ptr->set_scanner_geometry("BlocksOnCylindrical"); + scannerBlocks_firstFlat_ptr->set_num_transaxial_blocks_per_bucket(1); + scannerBlocks_firstFlat_ptr->set_intrinsic_azimuthal_tilt(-csi); + scannerBlocks_firstFlat_ptr->set_up(); + + shared_ptr proj_data_info_blocks_firstFlat_ptr; + proj_data_info_blocks_firstFlat_ptr.reset( + new ProjDataInfoBlocksOnCylindricalNoArcCorr( + scannerBlocks_firstFlat_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + scannerBlocks_firstFlat_ptr->get_max_num_views(), + scannerBlocks_firstFlat_ptr->get_max_num_non_arccorrected_bins())); + timer.reset(); timer.start(); + + for (int view = 0; view <= proj_data_info_blocks_firstFlat_ptr->get_max_view_num(); view++) + { + int bucket_num=view/(scannerBlocks_firstFlat_ptr->get_num_transaxial_crystals_per_block()* + scannerBlocks_firstFlat_ptr->get_num_transaxial_blocks_per_bucket()); + if (bucket_num>0) + break; + + bin.segment_num() = 0; + bin.axial_pos_num() = 0; + bin.view_num() = view; + bin.tangential_pos_num() = 0; + + + // check cartesian coordinates of detectors + proj_data_info_blocks_firstFlat_ptr->find_cartesian_coordinates_of_detection(b1,b2,bin); + proj_data_info_blocks_firstFlat_ptr->find_cartesian_coordinates_of_detection(b01,b02,bin0); + + + check_if_equal(b1.y(),b01.y(), " checking cartesian coordinate y1 are the same on a flat bucket"); + check_if_equal(b2.y(),b02.y(), " checking cartesian coordinate y2 are the same on a flat bucket"); + check_if_equal(b1.y(),-b2.y(), " checking cartesian coordinate y1 and y2 are of opposite sign on opposite flat buckets"); + + } + timer.stop(); std::cerr<< "-- CPU Time " << timer.value() << '\n'; + +} + + +void +DetectionPosMapTests:: +run_tests() +{ + + std::cerr << "-------- Testing DetectorCoordinateMap --------\n"; + run_coordinate_test_for_different_block_orientation(); +} +END_NAMESPACE_STIR + + +USING_NAMESPACE_STIR + +int main() +{ + DetectionPosMapTests tests; + tests.run_tests(); + return tests.main_return_value(); +} From b8b3f1f01dcb77b72a2f9ca3b85a1a9cfc590596 Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Thu, 16 Dec 2021 15:21:54 +0000 Subject: [PATCH 63/81] change test name --- src/test/test_DetectorCoordinateMap.cxx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/test_DetectorCoordinateMap.cxx b/src/test/test_DetectorCoordinateMap.cxx index c64482d5c..a874f8aa2 100644 --- a/src/test/test_DetectorCoordinateMap.cxx +++ b/src/test/test_DetectorCoordinateMap.cxx @@ -60,7 +60,7 @@ class DetectionPosMapTests: public RunTests float calculate_angle_within_half_bucket(const shared_ptr scanner_ptr, const shared_ptr proj_data_info_ptr); private: - void run_coordinate_test_for_different_block_orientation(); + void run_coordinate_test_for_flat_first_bucket(); void run_map_orientation_test(); }; @@ -100,7 +100,7 @@ DetectionPosMapTests::calculate_angle_within_half_bucket(const shared_ptr scannerBlocks_ptr; @@ -193,7 +193,7 @@ run_tests() { std::cerr << "-------- Testing DetectorCoordinateMap --------\n"; - run_coordinate_test_for_different_block_orientation(); + run_coordinate_test_for_flat_first_bucket(); } END_NAMESPACE_STIR From 9508ab10552a1b14de5e5690b8975f72d0b1aa80 Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Wed, 22 Dec 2021 11:26:13 +0000 Subject: [PATCH 64/81] - make DetectioPosMapTests friend class of Scanner - add test for re-oriented map - make get_sampling functions abs() --- src/buildblock/ProjDataInfo.cxx | 6 +- src/include/stir/Scanner.h | 2 + src/test/test_DetectorCoordinateMap.cxx | 105 ++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 3 deletions(-) diff --git a/src/buildblock/ProjDataInfo.cxx b/src/buildblock/ProjDataInfo.cxx index 99bbb76d9..c2c9f0cd6 100644 --- a/src/buildblock/ProjDataInfo.cxx +++ b/src/buildblock/ProjDataInfo.cxx @@ -69,7 +69,7 @@ float ProjDataInfo::get_sampling_in_t(const Bin& bin) const { return - (get_t(Bin(bin.segment_num(), bin.view_num(), bin.axial_pos_num()+1,bin.tangential_pos_num())) - + abs(get_t(Bin(bin.segment_num(), bin.view_num(), bin.axial_pos_num()+1,bin.tangential_pos_num())) - get_t(Bin(bin.segment_num(), bin.view_num(), bin.axial_pos_num()-1,bin.tangential_pos_num())) )/2; } @@ -78,7 +78,7 @@ float ProjDataInfo::get_sampling_in_m(const Bin& bin) const { return - (get_m(Bin(bin.segment_num(), bin.view_num(), bin.axial_pos_num()+1,bin.tangential_pos_num())) - + abs(get_m(Bin(bin.segment_num(), bin.view_num(), bin.axial_pos_num()+1,bin.tangential_pos_num())) - get_m(Bin(bin.segment_num(), bin.view_num(), bin.axial_pos_num()-1,bin.tangential_pos_num())) )/2; } @@ -88,7 +88,7 @@ float ProjDataInfo::get_sampling_in_s(const Bin& bin) const { return - (get_s(Bin(bin.segment_num(), bin.view_num(), bin.axial_pos_num(),bin.tangential_pos_num()+1)) - + abs(get_s(Bin(bin.segment_num(), bin.view_num(), bin.axial_pos_num(),bin.tangential_pos_num()+1)) - get_s(Bin(bin.segment_num(), bin.view_num(), bin.axial_pos_num(),bin.tangential_pos_num()-1)) )/2; } diff --git a/src/include/stir/Scanner.h b/src/include/stir/Scanner.h index 5846f6b32..d2e06680b 100644 --- a/src/include/stir/Scanner.h +++ b/src/include/stir/Scanner.h @@ -99,6 +99,8 @@ class Succeeded; */ class Scanner { + friend class DetectionPosMapTests; + public: /************* static members*****************************/ diff --git a/src/test/test_DetectorCoordinateMap.cxx b/src/test/test_DetectorCoordinateMap.cxx index a874f8aa2..054e480b5 100644 --- a/src/test/test_DetectorCoordinateMap.cxx +++ b/src/test/test_DetectorCoordinateMap.cxx @@ -24,6 +24,7 @@ #include "stir/recon_buildblock/ProjMatrixElemsForOneBin.h" #include "stir/recon_buildblock/ProjMatrixByBinUsingRayTracing.h" #include "stir/ExamInfo.h" +#include "stir/DetectorCoordinateMap.h" #include "stir/LORCoordinates.h" #include "stir/ProjDataInfo.h" #include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" @@ -186,6 +187,109 @@ DetectionPosMapTests::run_coordinate_test_for_flat_first_bucket() } +void +DetectionPosMapTests::run_map_orientation_test() +{ + CPUTimer timer; + shared_ptr map_ptr; + shared_ptr scannerBlocks_ptr; + scannerBlocks_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + + scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); + scannerBlocks_ptr->set_num_transaxial_blocks_per_bucket(1); + scannerBlocks_ptr->set_up(); + + VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + + for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ + min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + if (iget_num_rings()) + num_axial_pos_per_segment[i]=i+1; + else + num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; + } + + shared_ptr proj_data_info_blocks_ptr; + proj_data_info_blocks_ptr.reset( + new ProjDataInfoBlocksOnCylindricalNoArcCorr( + scannerBlocks_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + scannerBlocks_ptr->get_max_num_views(), + scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); + + Bin bin, bin0=Bin(0,0,0,0); + CartesianCoordinate3D< float> b1,b2,rb1,rb2; + DetectionPosition<> det_pos, det_pos_ord; + CartesianCoordinate3D coord_ord; + map_ptr=scannerBlocks_ptr->get_detector_map_sptr(); + int rad_size=map_ptr->get_num_radial_coords(); + int ax_size=map_ptr->get_num_axial_coords(); + int tang_size=map_ptr->get_num_tangential_coords(); + + DetectorCoordinateMap::det_pos_to_coord_type coord_map_reordered; + +// reorder the tangential positions + for (int rad=0;radget_coordinate_for_det_pos(det_pos_ord); + coord_map_reordered[det_pos]=coord_ord; + } + + shared_ptr scannerBlocks_reord_ptr; + scannerBlocks_reord_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + scannerBlocks_reord_ptr->set_scanner_geometry("BlocksOnCylindrical"); + scannerBlocks_reord_ptr->set_num_transaxial_blocks_per_bucket(1); + scannerBlocks_reord_ptr->set_detector_map(coord_map_reordered); + scannerBlocks_reord_ptr->set_up(); + + shared_ptr proj_data_info_blocks_reord_ptr; + proj_data_info_blocks_reord_ptr.reset( + new ProjDataInfoBlocksOnCylindricalNoArcCorr( + scannerBlocks_reord_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + scannerBlocks_reord_ptr->get_max_num_views(), + scannerBlocks_reord_ptr->get_max_num_non_arccorrected_bins())); + timer.reset(); timer.start(); + + for (int view = 0; view <= proj_data_info_blocks_reord_ptr->get_max_view_num(); view++) + { + + bin.segment_num() = 0; + bin.axial_pos_num() = 0; + bin.view_num() = view; + bin.tangential_pos_num() = 0; + +// bin_ord.segment_num() = 0; +// bin_ord.axial_pos_num() = 0; +// bin_ord.view_num() = proj_data_info_blocks_reord_ptr->get_max_view_num()-view; +// bin_ord.tangential_pos_num() = 0; + + +// // check cartesian coordinates of detectors + proj_data_info_blocks_ptr->find_cartesian_coordinates_of_detection(b1,b2,bin); + proj_data_info_blocks_reord_ptr->find_cartesian_coordinates_of_detection(rb1,b2,bin); + + + check_if_equal(b1,rb1, " checking cartesian coordinate y1 are the same on a flat bucket"); + } + timer.stop(); std::cerr<< "-- CPU Time " << timer.value() << '\n'; + +} + void DetectionPosMapTests:: @@ -193,6 +297,7 @@ run_tests() { std::cerr << "-------- Testing DetectorCoordinateMap --------\n"; + run_map_orientation_test(); run_coordinate_test_for_flat_first_bucket(); } END_NAMESPACE_STIR From 78d611aea36de94817067b265db50ebdfa7ea541 Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Tue, 11 Jan 2022 14:44:29 +0000 Subject: [PATCH 65/81] use make_shared() for pointers; disable sort add check on map_filename when map object is not already defined --- src/buildblock/DetectorCoordinateMap.cxx | 2 +- src/buildblock/Scanner.cxx | 6 +- src/include/stir/Scanner.h | 2 +- .../test_blocks_on_cylindrical_projectors.cxx | 248 +++++++++++++++--- src/test/test_DetectorCoordinateMap.cxx | 133 +--------- src/test/test_proj_data_info.cxx | 70 ++--- 6 files changed, 251 insertions(+), 210 deletions(-) diff --git a/src/buildblock/DetectorCoordinateMap.cxx b/src/buildblock/DetectorCoordinateMap.cxx index a9b200dd1..cf8e5daab 100644 --- a/src/buildblock/DetectorCoordinateMap.cxx +++ b/src/buildblock/DetectorCoordinateMap.cxx @@ -114,7 +114,7 @@ void DetectorCoordinateMap::set_detector_map( const DetectorCoordinateMap::det_p ", axial " +std::to_string(num_axial_coords) + ", radial " + std::to_string(num_radial_coords) + "\nOveral size: " + std::to_string(coord_map.size())); - std::sort(coords_to_be_sorted.begin(), coords_to_be_sorted.end()); +// std::sort(coords_to_be_sorted.begin(), coords_to_be_sorted.end()); stir::DetectionPosition<> detpos(0,0,0); for(std::vector::iterator it = coords_to_be_sorted.begin(); it != coords_to_be_sorted.end();++it) { diff --git a/src/buildblock/Scanner.cxx b/src/buildblock/Scanner.cxx index b88bbedc7..725a528a9 100644 --- a/src/buildblock/Scanner.cxx +++ b/src/buildblock/Scanner.cxx @@ -779,10 +779,12 @@ void Scanner::set_up() { if (scanner_geometry == "Generic") { - if (crystal_map_file_name == "") + if (!this->detector_map_sptr){ + if (crystal_map_file_name == "") error("Scanner: scanner_geometry=Generic needs a crystal map"); - + read_detectormap_from_file(crystal_map_file_name); + } } else { diff --git a/src/include/stir/Scanner.h b/src/include/stir/Scanner.h index d2e06680b..eeeeebdc0 100644 --- a/src/include/stir/Scanner.h +++ b/src/include/stir/Scanner.h @@ -99,7 +99,7 @@ class Succeeded; */ class Scanner { - friend class DetectionPosMapTests; + friend class BlocksTests; public: diff --git a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx index f2bfa3a2b..a17689ae7 100755 --- a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx +++ b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx @@ -35,6 +35,7 @@ #include "stir/LORCoordinates.h" #include "stir/ProjDataInfo.h" #include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" +#include "stir/ProjDataInfoGenericNoArcCorr.h" #include "stir/ProjDataInfoCylindricalNoArcCorr.h" #include "stir/recon_buildblock/ProjMatrixByBinUsingRayTracing.h" #include "stir/Sinogram.h" @@ -68,6 +69,7 @@ class BlocksTests: public RunTests private: void run_symmetry_test(); void run_plane_symmetry_test(); + void run_map_orientation_test(); }; /*! The following is a test for symmetries: a simulated image is created with a plane at known angles, @@ -126,8 +128,7 @@ BlocksTests::run_plane_symmetry_test(){ // create projadata info - shared_ptr scannerBlocks_ptr; - scannerBlocks_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + auto scannerBlocks_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); scannerBlocks_ptr->set_num_axial_crystals_per_block(1); scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing()* scannerBlocks_ptr->get_num_axial_crystals_per_block()); @@ -154,53 +155,46 @@ BlocksTests::run_plane_symmetry_test(){ num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; } - shared_ptr proj_data_info_blocks_ptr; - proj_data_info_blocks_ptr.reset( - new ProjDataInfoBlocksOnCylindricalNoArcCorr( + auto proj_data_info_blocks_ptr=std::make_shared( scannerBlocks_ptr, num_axial_pos_per_segment, min_ring_diff_v, max_ring_diff_v, scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); + scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); // now forward-project image - shared_ptr forw_projector_sptr,forw_projector2_sptr; - shared_ptr projdata, projdata2; shared_ptr > image_sptr(image.clone()); write_to_file("plane60",*image_sptr); shared_ptr > image2_sptr(image2.clone()); write_to_file("plane30",*image2_sptr); - shared_ptr PM(new ProjMatrixByBinUsingRayTracing()); - forw_projector_sptr.reset(new ForwardProjectorByBinUsingProjMatrixByBin(PM)); + auto PM=std::make_shared(); + auto forw_projector_sptr=std::make_shared(PM); info(boost::format("Test blocks on Cylindrical: Forward projector used: %1%") % forw_projector_sptr->parameter_info()); forw_projector_sptr->set_up(proj_data_info_blocks_ptr, image_sptr); - - forw_projector2_sptr.reset(new ForwardProjectorByBinUsingProjMatrixByBin(PM)); + auto forw_projector2_sptr=std::make_shared(PM); forw_projector2_sptr->set_up(proj_data_info_blocks_ptr, image2_sptr); //-- ExamInfo - shared_ptr exam_info_sptr(new ExamInfo()); + auto exam_info_sptr=std::make_shared(); exam_info_sptr->imaging_modality = ImagingModality::PT; - projdata.reset(new ProjDataInterfile(exam_info_sptr, + auto projdata=std::make_shared(exam_info_sptr, proj_data_info_blocks_ptr, "sino1_from_plane.hs", - std::ios::out | std::ios::trunc | std::ios::in)); + std::ios::out | std::ios::trunc | std::ios::in); - forw_projector_sptr->forward_project(*projdata, *image_sptr); - - projdata2.reset(new ProjDataInterfile(exam_info_sptr, + auto projdata2=std::make_shared(exam_info_sptr, proj_data_info_blocks_ptr, "sino2_from_plane.hs", - std::ios::out | std::ios::trunc | std::ios::in)); + std::ios::out | std::ios::trunc | std::ios::in); forw_projector2_sptr->forward_project(*projdata2, *image2_sptr); @@ -324,8 +318,7 @@ write_to_file("image_for2",*image2_sptr); // create projadata info - shared_ptr scannerBlocks_ptr; - scannerBlocks_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + auto scannerBlocks_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); scannerBlocks_ptr->set_num_axial_crystals_per_block(1); scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing()* scannerBlocks_ptr->get_num_axial_crystals_per_block()); @@ -352,42 +345,38 @@ write_to_file("image_for2",*image2_sptr); num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; } - shared_ptr proj_data_info_blocks_ptr; - proj_data_info_blocks_ptr.reset( - new ProjDataInfoBlocksOnCylindricalNoArcCorr( + auto proj_data_info_blocks_ptr=std::make_shared( scannerBlocks_ptr, num_axial_pos_per_segment, min_ring_diff_v, max_ring_diff_v, scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); + scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); // now forward-project images - shared_ptr forw_projector1_sptr, forw_projector2_sptr; - shared_ptr projdata1, projdata2; - shared_ptr PM(new ProjMatrixByBinUsingRayTracing()); - forw_projector1_sptr.reset(new ForwardProjectorByBinUsingProjMatrixByBin(PM)); + auto PM=std::make_shared(); + auto forw_projector1_sptr=std::make_shared(PM); info(boost::format("Test blocks on Cylindrical: Forward projector used: %1%") % forw_projector1_sptr->parameter_info()); forw_projector1_sptr->set_up(proj_data_info_blocks_ptr, image1_sptr); - forw_projector2_sptr.reset(new ForwardProjectorByBinUsingProjMatrixByBin(PM)); + auto forw_projector2_sptr=std::make_shared(PM); forw_projector2_sptr->set_up(proj_data_info_blocks_ptr, image2_sptr); //-- ExamInfo - shared_ptr exam_info_sptr(new ExamInfo()); + auto exam_info_sptr=std::make_shared(); exam_info_sptr->imaging_modality = ImagingModality::PT; - projdata1.reset(new ProjDataInterfile(exam_info_sptr, + auto projdata1=std::make_shared(exam_info_sptr, proj_data_info_blocks_ptr, - "sino1_from_image.hs",std::ios::out | std::ios::trunc | std::ios::in)); + "sino1_from_image.hs",std::ios::out | std::ios::trunc | std::ios::in); - projdata2.reset(new ProjDataInterfile(exam_info_sptr, + auto projdata2=std::make_shared(exam_info_sptr, proj_data_info_blocks_ptr, - "sino2_from_image.hs",std::ios::out | std::ios::trunc | std::ios::in)); + "sino2_from_image.hs",std::ios::out | std::ios::trunc | std::ios::in); forw_projector1_sptr->forward_project(*projdata1, *image1_sptr); forw_projector2_sptr->forward_project(*projdata2, *image2_sptr); @@ -419,14 +408,199 @@ write_to_file("image_for2",*image2_sptr); check(bin1_150!= bin2_150,"the two data have different symmetries, the values should be different"); } +void +BlocksTests::run_map_orientation_test() +{ + CPUTimer timer; + + CartesianCoordinate3D origin (0,0,0); + CartesianCoordinate3D grid_spacing (1.1,2.2,2.2); + float theta1=0; + + const IndexRange<3> range(Coordinate3D(0,-45,-44), + Coordinate3D(24,44,45)); + VoxelsOnCartesianGrid image(range,origin, grid_spacing); + + const Array<2,float> direction_vectors=make_array(make_1d_array(1.F,0.F,0.F), + make_1d_array(0.F,cos(theta1),sin(theta1)), + make_1d_array(0.F,-sin(theta1),cos(theta1))); + + Ellipsoid ellipsoid(CartesianCoordinate3D(/*radius_z*/6*grid_spacing.z(), + /*radius_y*/6*grid_spacing.y(), + /*radius_x*/6*grid_spacing.x()), + /*centre*/CartesianCoordinate3D((image.get_min_index()+image.get_max_index())/2*grid_spacing.z(), + -34*grid_spacing.y(), + 0), + direction_vectors); + + ellipsoid.construct_volume(image, make_coordinate(3,3,3)); + + VoxelsOnCartesianGrid image1=image; + VoxelsOnCartesianGrid image22=image; +// rotate by 30 degrees + for(int i=30; i<90; i+=30){ + theta1=i*_PI/180; + + CartesianCoordinate3D origin1 ((image.get_min_index()+image.get_max_index())/2*grid_spacing.z(), + -34*grid_spacing.y()*cos(theta1), + 34*grid_spacing.y()*sin(theta1)); + + ellipsoid.set_origin(origin1); + ellipsoid.construct_volume(image1, make_coordinate(3,3,3)); + image+=image1; +} + shared_ptr > image1_sptr(image.clone()); + write_to_file("image_to_fwp",*image1_sptr); + + + image=*image.get_empty_copy(); + + shared_ptr map_ptr; + auto scannerBlocks_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); + scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); + scannerBlocks_ptr->set_num_transaxial_blocks_per_bucket(1); + scannerBlocks_ptr->set_up(); + + VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + + for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ + min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + if (iget_num_rings()) + num_axial_pos_per_segment[i]=i+1; + else + num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; + } + + auto proj_data_info_blocks_ptr = std::make_shared( + scannerBlocks_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + scannerBlocks_ptr->get_max_num_views(), + scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); + + Bin bin, bin1,bin2, binR1; + CartesianCoordinate3D< float> b1,b2,rb1,rb2; + DetectionPosition<> det_pos, det_pos_ord; + DetectionPositionPair<> dp1, dp2, dpR1; + CartesianCoordinate3D coord_ord; + map_ptr=scannerBlocks_ptr->get_detector_map_sptr(); + int rad_size=map_ptr->get_num_radial_coords(); + int ax_size=map_ptr->get_num_axial_coords(); + int tang_size=map_ptr->get_num_tangential_coords(); + + DetectorCoordinateMap::det_pos_to_coord_type coord_map_reordered; + +// reorder the tangential positions + for (int rad=0;radget_coordinate_for_det_pos(det_pos_ord); + coord_map_reordered[det_pos]=coord_ord; + } + + auto scannerBlocks_reord_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); + scannerBlocks_reord_ptr->set_scanner_geometry("Generic"); +// scannerBlocks_reord_ptr->set_num_transaxial_blocks_per_bucket(1); + scannerBlocks_reord_ptr->set_detector_map(coord_map_reordered); + scannerBlocks_reord_ptr->set_up(); + + auto proj_data_info_blocks_reord_ptr= std::make_shared( + scannerBlocks_reord_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + scannerBlocks_reord_ptr->get_max_num_views(), + scannerBlocks_reord_ptr->get_max_num_non_arccorrected_bins()); + timer.reset(); timer.start(); + + + // now forward-project images + + auto PM= std::make_shared(); + + auto forw_projector1_sptr= std::make_shared(PM); + info(boost::format("Test blocks on Cylindrical: Forward projector used: %1%") % forw_projector1_sptr->parameter_info()); + forw_projector1_sptr->set_up(proj_data_info_blocks_ptr, + image1_sptr); + + auto forw_projector2_sptr= std::make_shared(PM); + forw_projector2_sptr->set_up(proj_data_info_blocks_reord_ptr, + image1_sptr); + + //-- ExamInfo + auto exam_info_sptr= std::make_shared(); + exam_info_sptr->imaging_modality = ImagingModality::PT; + + auto projdata1= std::make_shared(exam_info_sptr, + proj_data_info_blocks_ptr);//, +// "sino1_map.hs",std::ios::out | std::ios::trunc | std::ios::in)); + + auto projdata2= std::make_shared(exam_info_sptr, + proj_data_info_blocks_reord_ptr);//, +// "sino2_map.hs",std::ios::out | std::ios::trunc | std::ios::in)); + + forw_projector1_sptr->forward_project(*projdata1, *image1_sptr); + forw_projector2_sptr->forward_project(*projdata2, *image1_sptr); + + + for (int view = 0; view <= proj_data_info_blocks_reord_ptr->get_max_view_num(); view++) + { + + bin.segment_num() = 0; + bin.axial_pos_num() = 0; + bin.view_num() = view; + bin.tangential_pos_num() = 0; + + +// bin_ord.segment_num() = 0; +// bin_ord.axial_pos_num() = 0; +// bin_ord.view_num() = proj_data_info_blocks_reord_ptr->get_max_view_num()-view; +// bin_ord.tangential_pos_num() = 0; + + proj_data_info_blocks_ptr->get_det_pos_pair_for_bin(dp1,bin); + proj_data_info_blocks_reord_ptr->get_det_pos_pair_for_bin(dp2,bin); + +// rojDataInfoGenericNoArcCorr::get_ + proj_data_info_blocks_ptr->get_bin_for_det_pos_pair(bin1,dp1); + proj_data_info_blocks_reord_ptr->get_bin_for_det_pos_pair(bin2,dp2); + +// // check cartesian coordinates of detectors + proj_data_info_blocks_ptr->find_cartesian_coordinates_of_detection(b1,b2,bin1); + proj_data_info_blocks_reord_ptr->find_cartesian_coordinates_of_detection(rb1,rb2,bin1); + +// now get det_pos from the reordered coord ir shouls be different from the one obtained for bin and bin1 + proj_data_info_blocks_ptr->find_bin_given_cartesian_coordinates_of_detection(binR1,rb1, rb2); + proj_data_info_blocks_ptr->get_det_pos_pair_for_bin(dpR1,binR1); + + check_if_equal(projdata1->get_bin_value(bin1),projdata2->get_bin_value(bin2), " checking cartesian coordinate y1 are the same on a flat bucket"); + check(b1!=rb1, " checking cartesian coordinate of detector 1 are different if we use a reordered map"); + check(b2!=rb2, " checking cartesian coordinate of detector 2 are different if we use a reordered map"); + check(dp1.pos1().tangential_coord()!=dpR1.pos1().tangential_coord(), " checking det_pos.tangential is different if we use a reordered map"); + } + timer.stop(); std::cerr<< "-- CPU Time " << timer.value() << '\n'; + +} + + void BlocksTests:: run_tests() { std::cerr << "-------- Testing Blocks Geometry --------\n"; - run_symmetry_test(); - run_plane_symmetry_test(); + run_map_orientation_test(); +// run_symmetry_test(); +// run_plane_symmetry_test(); } END_NAMESPACE_STIR diff --git a/src/test/test_DetectorCoordinateMap.cxx b/src/test/test_DetectorCoordinateMap.cxx index 054e480b5..6fb252a2f 100644 --- a/src/test/test_DetectorCoordinateMap.cxx +++ b/src/test/test_DetectorCoordinateMap.cxx @@ -24,7 +24,6 @@ #include "stir/recon_buildblock/ProjMatrixElemsForOneBin.h" #include "stir/recon_buildblock/ProjMatrixByBinUsingRayTracing.h" #include "stir/ExamInfo.h" -#include "stir/DetectorCoordinateMap.h" #include "stir/LORCoordinates.h" #include "stir/ProjDataInfo.h" #include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" @@ -38,18 +37,17 @@ #include "stir/copy_fill.h" #include "stir/IndexRange3D.h" #include "stir/CPUTimer.h" -#include "stir/Shape/Shape3DWithOrientation.h" -#include "stir/Shape/Ellipsoid.h" -#include "stir/Shape/Box3D.h" #include "stir/VoxelsOnCartesianGrid.h" #include "stir/recon_buildblock/ForwardProjectorByBin.h" #include "stir/recon_buildblock/ForwardProjectorByBinUsingProjMatrixByBin.h" #include "stir/IO/write_to_file.h" //#include "stir/Shape/Shape3D.h" -START_NAMESPACE_STIR - +#include "stir/Shape/Shape3DWithOrientation.h" +#include "stir/Shape/Ellipsoid.h" +#include "stir/Shape/Box3D.h" +START_NAMESPACE_STIR /*! \ingroup test \brief Test class for Blocks @@ -62,7 +60,6 @@ class DetectionPosMapTests: public RunTests const shared_ptr proj_data_info_ptr); private: void run_coordinate_test_for_flat_first_bucket(); - void run_map_orientation_test(); }; float @@ -104,8 +101,7 @@ void DetectionPosMapTests::run_coordinate_test_for_flat_first_bucket() { CPUTimer timer; - shared_ptr scannerBlocks_ptr; - scannerBlocks_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + auto scannerBlocks_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); scannerBlocks_ptr->set_num_transaxial_blocks_per_bucket(1); @@ -126,14 +122,12 @@ DetectionPosMapTests::run_coordinate_test_for_flat_first_bucket() num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; } - shared_ptr proj_data_info_blocks_ptr; - proj_data_info_blocks_ptr.reset( - new ProjDataInfoBlocksOnCylindricalNoArcCorr( + auto proj_data_info_blocks_ptr=std::make_shared( scannerBlocks_ptr, num_axial_pos_per_segment, min_ring_diff_v, max_ring_diff_v, scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); + scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); Bin bin, bin0=Bin(0,0,0,0); CartesianCoordinate3D< float> b1,b2,b01,b02; @@ -143,21 +137,18 @@ DetectionPosMapTests::run_coordinate_test_for_flat_first_bucket() csi=calculate_angle_within_half_bucket(scannerBlocks_ptr, proj_data_info_blocks_ptr); - shared_ptr scannerBlocks_firstFlat_ptr; - scannerBlocks_firstFlat_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + auto scannerBlocks_firstFlat_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); scannerBlocks_firstFlat_ptr->set_scanner_geometry("BlocksOnCylindrical"); scannerBlocks_firstFlat_ptr->set_num_transaxial_blocks_per_bucket(1); scannerBlocks_firstFlat_ptr->set_intrinsic_azimuthal_tilt(-csi); scannerBlocks_firstFlat_ptr->set_up(); - shared_ptr proj_data_info_blocks_firstFlat_ptr; - proj_data_info_blocks_firstFlat_ptr.reset( - new ProjDataInfoBlocksOnCylindricalNoArcCorr( + auto proj_data_info_blocks_firstFlat_ptr=std::make_shared( scannerBlocks_firstFlat_ptr, num_axial_pos_per_segment, min_ring_diff_v, max_ring_diff_v, scannerBlocks_firstFlat_ptr->get_max_num_views(), - scannerBlocks_firstFlat_ptr->get_max_num_non_arccorrected_bins())); + scannerBlocks_firstFlat_ptr->get_max_num_non_arccorrected_bins()); timer.reset(); timer.start(); for (int view = 0; view <= proj_data_info_blocks_firstFlat_ptr->get_max_view_num(); view++) @@ -187,109 +178,6 @@ DetectionPosMapTests::run_coordinate_test_for_flat_first_bucket() } -void -DetectionPosMapTests::run_map_orientation_test() -{ - CPUTimer timer; - shared_ptr map_ptr; - shared_ptr scannerBlocks_ptr; - scannerBlocks_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); - - scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); - scannerBlocks_ptr->set_num_transaxial_blocks_per_bucket(1); - scannerBlocks_ptr->set_up(); - - VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); - VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - - for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ - min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; - max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; - if (iget_num_rings()) - num_axial_pos_per_segment[i]=i+1; - else - num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; - } - - shared_ptr proj_data_info_blocks_ptr; - proj_data_info_blocks_ptr.reset( - new ProjDataInfoBlocksOnCylindricalNoArcCorr( - scannerBlocks_ptr, - num_axial_pos_per_segment, - min_ring_diff_v, max_ring_diff_v, - scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); - - Bin bin, bin0=Bin(0,0,0,0); - CartesianCoordinate3D< float> b1,b2,rb1,rb2; - DetectionPosition<> det_pos, det_pos_ord; - CartesianCoordinate3D coord_ord; - map_ptr=scannerBlocks_ptr->get_detector_map_sptr(); - int rad_size=map_ptr->get_num_radial_coords(); - int ax_size=map_ptr->get_num_axial_coords(); - int tang_size=map_ptr->get_num_tangential_coords(); - - DetectorCoordinateMap::det_pos_to_coord_type coord_map_reordered; - -// reorder the tangential positions - for (int rad=0;radget_coordinate_for_det_pos(det_pos_ord); - coord_map_reordered[det_pos]=coord_ord; - } - - shared_ptr scannerBlocks_reord_ptr; - scannerBlocks_reord_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); - scannerBlocks_reord_ptr->set_scanner_geometry("BlocksOnCylindrical"); - scannerBlocks_reord_ptr->set_num_transaxial_blocks_per_bucket(1); - scannerBlocks_reord_ptr->set_detector_map(coord_map_reordered); - scannerBlocks_reord_ptr->set_up(); - - shared_ptr proj_data_info_blocks_reord_ptr; - proj_data_info_blocks_reord_ptr.reset( - new ProjDataInfoBlocksOnCylindricalNoArcCorr( - scannerBlocks_reord_ptr, - num_axial_pos_per_segment, - min_ring_diff_v, max_ring_diff_v, - scannerBlocks_reord_ptr->get_max_num_views(), - scannerBlocks_reord_ptr->get_max_num_non_arccorrected_bins())); - timer.reset(); timer.start(); - - for (int view = 0; view <= proj_data_info_blocks_reord_ptr->get_max_view_num(); view++) - { - - bin.segment_num() = 0; - bin.axial_pos_num() = 0; - bin.view_num() = view; - bin.tangential_pos_num() = 0; - -// bin_ord.segment_num() = 0; -// bin_ord.axial_pos_num() = 0; -// bin_ord.view_num() = proj_data_info_blocks_reord_ptr->get_max_view_num()-view; -// bin_ord.tangential_pos_num() = 0; - - -// // check cartesian coordinates of detectors - proj_data_info_blocks_ptr->find_cartesian_coordinates_of_detection(b1,b2,bin); - proj_data_info_blocks_reord_ptr->find_cartesian_coordinates_of_detection(rb1,b2,bin); - - - check_if_equal(b1,rb1, " checking cartesian coordinate y1 are the same on a flat bucket"); - } - timer.stop(); std::cerr<< "-- CPU Time " << timer.value() << '\n'; - -} - void DetectionPosMapTests:: @@ -297,7 +185,6 @@ run_tests() { std::cerr << "-------- Testing DetectorCoordinateMap --------\n"; - run_map_orientation_test(); run_coordinate_test_for_flat_first_bucket(); } END_NAMESPACE_STIR diff --git a/src/test/test_proj_data_info.cxx b/src/test/test_proj_data_info.cxx index dcaed6070..032c110ee 100755 --- a/src/test/test_proj_data_info.cxx +++ b/src/test/test_proj_data_info.cxx @@ -445,7 +445,7 @@ void ProjDataInfoTests::run_Blocks_DOI_test() { CPUTimer timer; - shared_ptr scannerBlocks_ptr(new Scanner (Scanner::SAFIRDualRingPrototype)); + auto scannerBlocks_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); scannerBlocks_ptr->set_average_depth_of_interaction(0); scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); scannerBlocks_ptr->set_up(); @@ -462,26 +462,22 @@ ProjDataInfoTests::run_Blocks_DOI_test() num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; } - shared_ptr proj_data_info_blocks_doi0_ptr; - proj_data_info_blocks_doi0_ptr.reset( - new ProjDataInfoBlocksOnCylindricalNoArcCorr( + auto proj_data_info_blocks_doi0_ptr=std::make_shared( scannerBlocks_ptr, num_axial_pos_per_segment, min_ring_diff_v, max_ring_diff_v, scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); + scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); - shared_ptr scannerBlocksDOI_ptr(new Scanner(*scannerBlocks_ptr)); + auto scannerBlocksDOI_ptr=std::make_shared(*scannerBlocks_ptr); scannerBlocksDOI_ptr->set_average_depth_of_interaction(0.1); scannerBlocksDOI_ptr->set_up(); - shared_ptr proj_data_info_blocks_doi5_ptr; - proj_data_info_blocks_doi5_ptr.reset( - new ProjDataInfoBlocksOnCylindricalNoArcCorr( + auto proj_data_info_blocks_doi5_ptr=std::make_shared( scannerBlocksDOI_ptr, num_axial_pos_per_segment, min_ring_diff_v, max_ring_diff_v, scannerBlocksDOI_ptr->get_max_num_views(), - scannerBlocksDOI_ptr->get_max_num_non_arccorrected_bins())); + scannerBlocksDOI_ptr->get_max_num_non_arccorrected_bins()); Bin bin; LORInAxialAndNoArcCorrSinogramCoordinates lor; @@ -541,8 +537,7 @@ void ProjDataInfoTests::run_coordinate_test() { CPUTimer timer; - shared_ptr scannerBlocks_ptr; - scannerBlocks_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + auto scannerBlocks_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); scannerBlocks_ptr->set_num_axial_crystals_per_block(1); scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing()* scannerBlocks_ptr->get_num_axial_crystals_per_block()); @@ -571,8 +566,7 @@ ProjDataInfoTests::run_coordinate_test() //float dx=scannerBlocks_ptr->get_effective_ring_radius()*sin(csi_minus_csiGaps); //float dy=scannerBlocks_ptr->get_effective_ring_radius()-scannerBlocks_ptr->get_effective_ring_radius()*cos(csi_minus_csiGaps); - shared_ptr scannerCyl_ptr; - scannerCyl_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + auto scannerCyl_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); scannerCyl_ptr->set_num_axial_crystals_per_block(1); scannerCyl_ptr->set_axial_block_spacing(scannerCyl_ptr->get_axial_crystal_spacing()* scannerCyl_ptr->get_num_axial_crystals_per_block()); @@ -599,23 +593,19 @@ ProjDataInfoTests::run_coordinate_test() num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; } - shared_ptr proj_data_info_blocks_ptr; - proj_data_info_blocks_ptr.reset( - new ProjDataInfoBlocksOnCylindricalNoArcCorr( + auto proj_data_info_blocks_ptr=std::make_shared( scannerBlocks_ptr, num_axial_pos_per_segment, min_ring_diff_v, max_ring_diff_v, scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); + scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); - shared_ptr proj_data_info_cyl_ptr; - proj_data_info_cyl_ptr.reset( - new ProjDataInfoCylindricalNoArcCorr( + auto proj_data_info_cyl_ptr=std::make_shared( scannerCyl_ptr, num_axial_pos_per_segment, min_ring_diff_v, max_ring_diff_v, scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); + scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); Bin bin, binRT; @@ -761,8 +751,7 @@ void ProjDataInfoTests::run_coordinate_test_for_realistic_scanner() { CPUTimer timer; - shared_ptr scannerBlocks_ptr; - scannerBlocks_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + auto scannerBlocks_ptr=std::make_shared(Scanner::SAFIRDualRingPrototype); scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing()* scannerBlocks_ptr->get_num_axial_crystals_per_block()); scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing()* @@ -771,8 +760,7 @@ ProjDataInfoTests::run_coordinate_test_for_realistic_scanner() scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); scannerBlocks_ptr->set_up(); - shared_ptr scannerCyl_ptr; - scannerCyl_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + auto scannerCyl_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); scannerCyl_ptr->set_axial_block_spacing(scannerCyl_ptr->get_axial_crystal_spacing()* scannerCyl_ptr->get_num_axial_crystals_per_block()); scannerCyl_ptr->set_transaxial_block_spacing(scannerCyl_ptr->get_transaxial_crystal_spacing()* @@ -794,23 +782,19 @@ ProjDataInfoTests::run_coordinate_test_for_realistic_scanner() num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; } - shared_ptr proj_data_info_blocks_ptr; - proj_data_info_blocks_ptr.reset( - new ProjDataInfoBlocksOnCylindricalNoArcCorr( + auto proj_data_info_blocks_ptr=std::make_shared( scannerBlocks_ptr, num_axial_pos_per_segment, min_ring_diff_v, max_ring_diff_v, scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); + scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); - shared_ptr proj_data_info_cyl_ptr; - proj_data_info_cyl_ptr.reset( - new ProjDataInfoCylindricalNoArcCorr( + auto proj_data_info_cyl_ptr=std::make_shared( scannerCyl_ptr, num_axial_pos_per_segment, min_ring_diff_v, max_ring_diff_v, scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); + scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); Bin bin; @@ -891,8 +875,7 @@ void ProjDataInfoTests:: run_lor_get_s_test(){ CPUTimer timer; - shared_ptr scannerBlocks_ptr; - scannerBlocks_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + auto scannerBlocks_ptr=std::make_shared(Scanner::SAFIRDualRingPrototype); scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing()* scannerBlocks_ptr->get_num_axial_crystals_per_block()); scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing()* @@ -901,8 +884,7 @@ run_lor_get_s_test(){ scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); scannerBlocks_ptr->set_up(); - shared_ptr scannerCyl_ptr; - scannerCyl_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + auto scannerCyl_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); scannerCyl_ptr->set_axial_block_spacing(scannerCyl_ptr->get_axial_crystal_spacing()* scannerCyl_ptr->get_num_axial_crystals_per_block()); scannerCyl_ptr->set_transaxial_block_spacing(scannerCyl_ptr->get_transaxial_crystal_spacing()* @@ -924,23 +906,19 @@ run_lor_get_s_test(){ num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; } - shared_ptr proj_data_info_blocks_ptr; - proj_data_info_blocks_ptr.reset( - new ProjDataInfoBlocksOnCylindricalNoArcCorr( + auto proj_data_info_blocks_ptr=std::make_shared( scannerBlocks_ptr, num_axial_pos_per_segment, min_ring_diff_v, max_ring_diff_v, scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); + scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); - shared_ptr proj_data_info_cyl_ptr; - proj_data_info_cyl_ptr.reset( - new ProjDataInfoCylindricalNoArcCorr( + auto proj_data_info_cyl_ptr=std::make_shared( scannerCyl_ptr, num_axial_pos_per_segment, min_ring_diff_v, max_ring_diff_v, scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); + scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); // select detection position 1 LORInAxialAndNoArcCorrSinogramCoordinates lorB; From 1f06a94df6a53a22190f686785a7472523b63c24 Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Wed, 12 Jan 2022 14:13:51 +0000 Subject: [PATCH 66/81] add get_m test (it's failing now) --- src/test/test_proj_data_info.cxx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/test_proj_data_info.cxx b/src/test/test_proj_data_info.cxx index 032c110ee..8780fc6fc 100755 --- a/src/test/test_proj_data_info.cxx +++ b/src/test/test_proj_data_info.cxx @@ -738,6 +738,7 @@ ProjDataInfoTests::run_coordinate_test() else{ check(false, "phi is different"); } + check_if_equal(proj_data_info_blocks_ptr->get_m(bin),proj_data_info_cyl_ptr->get_m(bin)," test get_m Cylindrical"); } timer.stop(); std::cerr<< "-- CPU Time " << timer.value() << '\n'; @@ -860,6 +861,8 @@ ProjDataInfoTests::run_coordinate_test_for_realistic_scanner() check_if_equal(b1.x(),c1.x(), " checking cartesian coordinate x1"); check_if_equal(b2.x(),c2.x(), " checking cartesian coordinate x2"); + set_tolerance(10E-4); + check_if_equal(proj_data_info_blocks_ptr->get_m(bin),proj_data_info_cyl_ptr->get_m(bin)," test get_m Cylindrical"); } timer.stop(); std::cerr<< "-- CPU Time " << timer.value() << '\n'; From ef507d1757494a9566c550fc057c963352fea24b Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Fri, 21 Jan 2022 16:00:36 +0000 Subject: [PATCH 67/81] Creating z_shift for coordinates to make cylindrical and blocksOnCylindrical consistent: - added z_shift in crystal_map - create z_shift memeber in genericNoarcCorr added to find_cartesian_coordinates_given_scanner_coordinates - subtructed z_shift in get_LOR - subtructed in find_scanner_coordinates_given_cartesian_coordinates - explicitly disabled symmetry_on_z_shift, fixed the proj operation issue - fixed tests, adding axial check and get_m --- .../GeometryBlocksOnCylindrical.cxx | 5 +- ...ojDataInfoBlocksOnCylindricalNoArcCorr.cxx | 4 +- src/buildblock/ProjDataInfoGeneric.cxx | 5 +- .../ProjDataInfoGenericNoArcCorr.cxx | 27 ++++ src/include/stir/ProjDataInfoGeneric.h | 3 +- src/recon_buildblock/BackProjectorByBin.cxx | 2 + ...ataSymmetriesForBins_PET_CartesianGrid.cxx | 3 +- .../test_blocks_on_cylindrical_projectors.cxx | 137 +++++++++++++++++- src/test/test_proj_data_info.cxx | 33 +++-- 9 files changed, 194 insertions(+), 25 deletions(-) diff --git a/src/buildblock/GeometryBlocksOnCylindrical.cxx b/src/buildblock/GeometryBlocksOnCylindrical.cxx index e9009ae5c..455992fe6 100755 --- a/src/buildblock/GeometryBlocksOnCylindrical.cxx +++ b/src/buildblock/GeometryBlocksOnCylindrical.cxx @@ -88,12 +88,15 @@ build_crystal_maps(const Scanner& scanner) // estimate the angle covered by half bucket, csi float csi=_PI/num_transaxial_buckets; float trans_blocks_gap=transaxial_block_spacing-num_transaxial_crystals_per_block*transaxial_crystal_spacing; + float ax_blocks_gap=axial_block_spacing-num_axial_crystals_per_block*axial_crystal_spacing; float csi_minus_csiGaps=csi-(csi/transaxial_block_spacing*2)* (transaxial_crystal_spacing/2+trans_blocks_gap); // distance between the center of the scannner and the first crystal in the bucket, r=Reffective/cos(csi) float r=scanner.get_effective_ring_radius()/cos(csi_minus_csiGaps); - float start_z = 0; + float start_z = -(axial_block_spacing*(num_axial_blocks_per_bucket)*num_axial_buckets + -axial_crystal_spacing - ax_blocks_gap + *(num_axial_blocks_per_bucket*num_axial_buckets-1))/2; float start_y = -1*scanner.get_effective_ring_radius(); float start_x = -1*r*sin(csi_minus_csiGaps);//( // ((num_transaxial_blocks_per_bucket-1)/2.)*transaxial_block_spacing diff --git a/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx b/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx index 29c20ce37..d5393a476 100644 --- a/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx +++ b/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx @@ -120,8 +120,8 @@ find_scanner_coordinates_given_cartesian_coordinates(int& det1, int& det2, int& DetectionPosition<> det_pos1; DetectionPosition<> det_pos2; - if (get_scanner_ptr()->find_detection_position_given_cartesian_coordinate(det_pos1, c1)==Succeeded::no || - get_scanner_ptr()->find_detection_position_given_cartesian_coordinate(det_pos2, c2)==Succeeded::no) + if (get_scanner_ptr()->find_detection_position_given_cartesian_coordinate(det_pos1, c1-this->z_shift)==Succeeded::no || + get_scanner_ptr()->find_detection_position_given_cartesian_coordinate(det_pos2, c2-this->z_shift)==Succeeded::no) { return Succeeded::no; } diff --git a/src/buildblock/ProjDataInfoGeneric.cxx b/src/buildblock/ProjDataInfoGeneric.cxx index 404d29dc0..e97c76ec8 100755 --- a/src/buildblock/ProjDataInfoGeneric.cxx +++ b/src/buildblock/ProjDataInfoGeneric.cxx @@ -108,7 +108,10 @@ get_LOR(LORInAxialAndNoArcCorrSinogramCoordinates& lor, CartesianCoordinate3D _p1; CartesianCoordinate3D _p2; find_cartesian_coordinates_of_detection(_p1, _p2, bin); - + + _p1.z()-=z_shift.z(); + _p2.z()-=z_shift.z(); + LORAs2Points lor_as_2_points(_p1, _p2); const double R = sqrt(max(square(_p1.x())+square(_p1.y()), square(_p2.x())+square(_p2.y()))); diff --git a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx index 42fc89a47..1c963994b 100644 --- a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx +++ b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx @@ -64,6 +64,31 @@ ProjDataInfoGenericNoArcCorr(const shared_ptr scanner_ptr, assert(!is_null_ptr(scanner_ptr)); uncompressed_view_tangpos_to_det1det2_initialised = false; det1det2_to_uncompressed_view_tangpos_initialised = false; + + float ax_blocks_gap=scanner_ptr->get_axial_block_spacing() + -scanner_ptr->get_num_axial_crystals_per_block() + *scanner_ptr->get_axial_crystal_spacing(); + + CartesianCoordinate3D< float> b1,b2; + Bin bin; + bin.segment_num() = 0; + bin.axial_pos_num() = 0; + bin.view_num() = 0; + bin.tangential_pos_num() = 0; + find_cartesian_coordinates_of_detection(b1,b2,bin); + float shift=b1.z(); + + float shift2=(scanner_ptr->get_axial_block_spacing() + *scanner_ptr->get_num_axial_blocks_per_bucket() + *scanner_ptr->get_num_axial_buckets() + -scanner_ptr->get_axial_crystal_spacing() + -ax_blocks_gap*(scanner_ptr->get_num_axial_blocks_per_bucket() + *scanner_ptr->get_num_axial_buckets()-1) + )/2; + + this->z_shift.z()=shift2; + this->z_shift.y()=0; + this->z_shift.x()=0; } @@ -372,6 +397,8 @@ find_cartesian_coordinates_given_scanner_coordinates(CartesianCoordinate3Dget_coordinate_for_det_pos(det_pos1); coord_2 = get_scanner_ptr()->get_coordinate_for_det_pos(det_pos2); + coord_1.z() += z_shift.z(); + coord_2.z() += z_shift.z(); } diff --git a/src/include/stir/ProjDataInfoGeneric.h b/src/include/stir/ProjDataInfoGeneric.h index e7dc1bb9b..275c14630 100644 --- a/src/include/stir/ProjDataInfoGeneric.h +++ b/src/include/stir/ProjDataInfoGeneric.h @@ -118,7 +118,8 @@ class ProjDataInfoGeneric: public ProjDataInfoCylindrical virtual void find_cartesian_coordinates_of_detection(CartesianCoordinate3D& coord_1, CartesianCoordinate3D& coord_2, const Bin& bin) const = 0; - +protected: + CartesianCoordinate3D z_shift; }; diff --git a/src/recon_buildblock/BackProjectorByBin.cxx b/src/recon_buildblock/BackProjectorByBin.cxx index 004894839..cadb7c2f1 100644 --- a/src/recon_buildblock/BackProjectorByBin.cxx +++ b/src/recon_buildblock/BackProjectorByBin.cxx @@ -306,6 +306,8 @@ void BackProjectorByBin:: start_accumulating_in_new_target() { + if (!this->_already_set_up) + error("BackProjectorByBin method called without calling set_up first."); #ifdef STIR_OPENMP if (omp_get_num_threads()!=1) error("BackProjectorByBin::start_accumulating_in_new_target cannot be called inside a thread"); diff --git a/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx b/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx index cc8af5f1f..d761172dd 100644 --- a/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx +++ b/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx @@ -380,7 +380,8 @@ DataSymmetriesForBins_PET_CartesianGrid this->do_symmetry_90degrees_min_phi = this->do_symmetry_180degrees_min_phi = this->do_symmetry_swap_segment = - this->do_symmetry_swap_s = false; + this->do_symmetry_swap_s = + this->do_symmetry_shift_z=false; } if (!dynamic_cast(proj_data_info_ptr.get())->axial_sampling_is_uniform()) { diff --git a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx index a17689ae7..4174c57d5 100755 --- a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx +++ b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx @@ -52,6 +52,7 @@ #include "stir/VoxelsOnCartesianGrid.h" #include "stir/recon_buildblock/ForwardProjectorByBin.h" #include "stir/recon_buildblock/ForwardProjectorByBinUsingProjMatrixByBin.h" +#include "stir/recon_buildblock/BackProjectorByBinUsingProjMatrixByBin.h" #include "stir/IO/write_to_file.h" //#include "stir/Shape/Shape3D.h" @@ -70,8 +71,129 @@ class BlocksTests: public RunTests void run_symmetry_test(); void run_plane_symmetry_test(); void run_map_orientation_test(); + void run_axial_projection_test(); }; +void +BlocksTests::run_axial_projection_test(){ + + CartesianCoordinate3D origin (0,0,0); + CartesianCoordinate3D grid_spacing (1.1,2.2,2.2); + + const IndexRange<3> + range(Coordinate3D(0,-45,-45), + Coordinate3D(24,44,44)); + VoxelsOnCartesianGrid image(range,origin, grid_spacing); + +// 60 degrees + float phi1= 0*_PI/180; + const Array<2,float> direction_vectors= + make_array(make_1d_array(1.F,0.F,0.F), + make_1d_array(0.F,cos(float(_PI)-phi1),sin(float(_PI)-phi1)), + make_1d_array(0.F,-sin(float(_PI)-phi1),cos(float(_PI)-phi1))); + + Ellipsoid + plane(CartesianCoordinate3D(/*edge_z*/50*grid_spacing.z(), + /*edge_y*/2*grid_spacing.y(), + /*edge_x*/2*grid_spacing.x()), + /*centre*/CartesianCoordinate3D((image.get_min_index()+image.get_max_index())/2*grid_spacing.z(), + 0*grid_spacing.y(), + 0), + direction_vectors); + + plane.construct_volume(image, make_coordinate(3,3,3)); + + + +// create projadata info + + auto scannerBlocks_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); +// scannerBlocks_ptr->set_num_axial_crystals_per_block(1); +// scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing()* +// scannerBlocks_ptr->get_num_axial_crystals_per_block()); +// scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing()* +// scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); +//// scannerBlocks_ptr->set_num_transaxial_crystals_per_block(1); +// scannerBlocks_ptr->set_num_axial_blocks_per_bucket(1); +//// scannerBlocks_ptr->set_num_transaxial_blocks_per_bucket(1); +// scannerBlocks_ptr->set_num_rings(1); + + scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); + scannerBlocks_ptr->set_up(); + + VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + + for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ + min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + if (iget_num_rings()) + num_axial_pos_per_segment[i]=i+1; + else + num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; + } + + auto proj_data_info_blocks_ptr=std::make_shared( + scannerBlocks_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + scannerBlocks_ptr->get_max_num_views(), + scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); +// now forward-project image + + shared_ptr > image_sptr(image.clone()); + shared_ptr > bck_proj_image_sptr(image.clone()); + write_to_file("axial_test",*image_sptr); + + auto PM=std::make_shared(); +// PM->set_do_symmetry_90degrees_min_phi(false); +// PM->set_do_symmetry_shift_z(false); +// PM->set_do_symmetry_swap_segment(false); + + auto forw_projector_sptr=std::make_shared(PM); + auto bck_projector_sptr=std::make_shared(PM); + info(boost::format("Test blocks on Cylindrical: Forward projector used: %1%") % forw_projector_sptr->parameter_info()); + + forw_projector_sptr->set_up(proj_data_info_blocks_ptr, + image_sptr); + bck_projector_sptr->set_up(proj_data_info_blocks_ptr, + bck_proj_image_sptr); + + //-- ExamInfo + auto exam_info_sptr=std::make_shared(); + exam_info_sptr->imaging_modality = ImagingModality::PT; + + auto projdata=std::make_shared(exam_info_sptr, + proj_data_info_blocks_ptr, + "test_axial.hs", + std::ios::out | std::ios::trunc | std::ios::in); + + forw_projector_sptr->forward_project(*projdata, *image_sptr); + + bck_projector_sptr->back_project(*bck_proj_image_sptr, *projdata,0,1); + write_to_file("back_proj_axial_test",*bck_proj_image_sptr); + + int min_z = bck_proj_image_sptr->get_min_index(); + int max_z = bck_proj_image_sptr->get_max_index(); + int min_y = (*bck_proj_image_sptr)[min_z].get_min_index(); + int max_y = (*bck_proj_image_sptr)[min_z].get_max_index(); + int min_x = (*bck_proj_image_sptr)[min_z][min_y].get_min_index(); + int max_x = (*bck_proj_image_sptr)[min_z][min_y].get_max_index(); + +// get two planes in the image that are equidistant from the z center + int centre_z=(max_z-min_z)/2; + int plane_idA=centre_z-5; + int plane_idB=centre_z+5; + + for(int y=min_y; yset_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing()* scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); // scannerBlocks_ptr->set_num_transaxial_crystals_per_block(1); - scannerBlocks_ptr->set_num_axial_blocks_per_bucket(1); + scannerBlocks_ptr->set_num_axial_blocks_per_bucket(2); // scannerBlocks_ptr->set_num_transaxial_blocks_per_bucket(1); - scannerBlocks_ptr->set_num_rings(1); + scannerBlocks_ptr->set_num_rings(2); scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); scannerBlocks_ptr->set_up(); @@ -325,10 +447,10 @@ write_to_file("image_for2",*image2_sptr); scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing()* scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); // scannerBlocks_ptr->set_num_transaxial_crystals_per_block(1); - scannerBlocks_ptr->set_num_axial_blocks_per_bucket(1); + scannerBlocks_ptr->set_num_axial_blocks_per_bucket(2); // scannerBlocks_ptr->set_num_transaxial_blocks_per_bucket(1); - scannerBlocks_ptr->set_num_rings(1); - + scannerBlocks_ptr->set_num_rings(2); + scannerBlocks_ptr->set_intrinsic_azimuthal_tilt(-30); scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); scannerBlocks_ptr->set_up(); @@ -598,9 +720,10 @@ run_tests() { std::cerr << "-------- Testing Blocks Geometry --------\n"; + run_axial_projection_test(); run_map_orientation_test(); -// run_symmetry_test(); -// run_plane_symmetry_test(); + run_symmetry_test(); + run_plane_symmetry_test(); } END_NAMESPACE_STIR diff --git a/src/test/test_proj_data_info.cxx b/src/test/test_proj_data_info.cxx index 8780fc6fc..eb9847b94 100755 --- a/src/test/test_proj_data_info.cxx +++ b/src/test/test_proj_data_info.cxx @@ -538,15 +538,15 @@ ProjDataInfoTests::run_coordinate_test() { CPUTimer timer; auto scannerBlocks_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); - scannerBlocks_ptr->set_num_axial_crystals_per_block(1); + scannerBlocks_ptr->set_num_axial_crystals_per_block(2); scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing()* scannerBlocks_ptr->get_num_axial_crystals_per_block()); scannerBlocks_ptr->set_num_transaxial_crystals_per_block(1); - scannerBlocks_ptr->set_num_axial_blocks_per_bucket(1); + scannerBlocks_ptr->set_num_axial_blocks_per_bucket(2); scannerBlocks_ptr->set_num_transaxial_blocks_per_bucket(1); scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing()* scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); - scannerBlocks_ptr->set_num_rings(1); + scannerBlocks_ptr->set_num_rings(4); scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); scannerBlocks_ptr->set_up(); @@ -567,16 +567,16 @@ ProjDataInfoTests::run_coordinate_test() //float dy=scannerBlocks_ptr->get_effective_ring_radius()-scannerBlocks_ptr->get_effective_ring_radius()*cos(csi_minus_csiGaps); auto scannerCyl_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); - scannerCyl_ptr->set_num_axial_crystals_per_block(1); + scannerCyl_ptr->set_num_axial_crystals_per_block(2); scannerCyl_ptr->set_axial_block_spacing(scannerCyl_ptr->get_axial_crystal_spacing()* scannerCyl_ptr->get_num_axial_crystals_per_block()); scannerCyl_ptr->set_transaxial_block_spacing(scannerCyl_ptr->get_transaxial_crystal_spacing()* scannerCyl_ptr->get_num_transaxial_crystals_per_block()); scannerCyl_ptr->set_num_transaxial_crystals_per_block(1); - scannerCyl_ptr->set_num_axial_blocks_per_bucket(1); + scannerCyl_ptr->set_num_axial_blocks_per_bucket(2); scannerCyl_ptr->set_num_transaxial_blocks_per_bucket(1); - scannerCyl_ptr->set_num_rings(1); + scannerCyl_ptr->set_num_rings(4); scannerCyl_ptr->set_scanner_geometry("Cylindrical"); scannerCyl_ptr->set_up(); @@ -730,9 +730,12 @@ ProjDataInfoTests::run_coordinate_test() " PHIB="+ std::to_string(lorB.phi())+ " view="+ std::to_string(view)+ " Btest if BlocksOnCylindrical LOR.s is the same as the LOR produced by Cylindrical");//) - check_if_equal(lorB.beta(),-lorC1.beta()," test if BlocksOnCylindrical LOR.beta is the same as the LOR produced by Cylindrical"); - check_if_equal(lorB.z1(),lorC1.z1()," test if BlocksOnCylindrical LOR.z1 is the same as the LOR produced by Cylindrical"); - check_if_equal(lorB.z2(),lorC1.z2()," test if BlocksOnCylindrical LOR.z2 is the same as the LOR produced by Cylindrical"); + check_if_equal(lorB.beta(),-lorC1.beta()," Btest if BlocksOnCylindrical LOR.beta is the same as the LOR produced by Cylindrical"); + check_if_equal(lorB.z1(),lorC1.z2(),"tang_pos="+ std::to_string(tang)+ + " ax_pos="+ std::to_string(ax)+ + " segment="+ std::to_string(seg)+ + " view="+ std::to_string(view)+" Btest if BlocksOnCylindrical LOR.z1 is the same as the LOR produced by Cylindrical"); + check_if_equal(lorB.z2(),lorC1.z1()," Btest if BlocksOnCylindrical LOR.z2 is the same as the LOR produced by Cylindrical"); } else{ @@ -854,14 +857,20 @@ ProjDataInfoTests::run_coordinate_test_for_realistic_scanner() set_tolerance(max_tolerance); - check_if_equal(b1.z(),c1.z(), " checking cartesian coordinate z1"); - check_if_equal(b2.z(),c2.z(), " checking cartesian coordinate z2"); check_if_equal(b1.y(),c1.y(), " checking cartesian coordinate y1"); check_if_equal(b2.y(),c2.y(), " checking cartesian coordinate y2"); check_if_equal(b1.x(),c1.x(), " checking cartesian coordinate x1"); check_if_equal(b2.x(),c2.x(), " checking cartesian coordinate x2"); - set_tolerance(10E-4); +/*!calculate the max axial tolerance, the difference between m (blocks vs cylindrical) happens when the point A is at + * the beginning of a transaxial bucket and the point B is in the middle of the bucket (which is the point where the radius of circle + * and the radius of the poligon have the biggest difference + * !*/ + float psi=atan(abs(lorB.z2()-lorB.z1())/r); + float max_ax_tolerance=abs(scannerBlocks_ptr->get_effective_ring_radius()-r)/cos(psi); + set_tolerance(max_ax_tolerance); + check_if_equal(b1.z(),c1.z(), " checking cartesian coordinate z1"); + check_if_equal(b2.z(),c2.z(), " checking cartesian coordinate z2"); check_if_equal(proj_data_info_blocks_ptr->get_m(bin),proj_data_info_cyl_ptr->get_m(bin)," test get_m Cylindrical"); } timer.stop(); std::cerr<< "-- CPU Time " << timer.value() << '\n'; From e20ef08468c25ba052f8fe3dcb5fdd7da08b038a Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Mon, 24 Jan 2022 18:21:14 +0000 Subject: [PATCH 68/81] fix z_shift repetition in ProjDataInfoGeneric... add doxygen for last test --- src/buildblock/ProjDataInfoGenericNoArcCorr.cxx | 12 +++--------- .../test_blocks_on_cylindrical_projectors.cxx | 7 ++++++- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx index 1c963994b..070ed7af7 100644 --- a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx +++ b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx @@ -75,18 +75,12 @@ ProjDataInfoGenericNoArcCorr(const shared_ptr scanner_ptr, bin.axial_pos_num() = 0; bin.view_num() = 0; bin.tangential_pos_num() = 0; +// setting shift_z to 0 before it is actually estimated. Otherwise the next function will use it + this->z_shift.z()=0; find_cartesian_coordinates_of_detection(b1,b2,bin); float shift=b1.z(); - float shift2=(scanner_ptr->get_axial_block_spacing() - *scanner_ptr->get_num_axial_blocks_per_bucket() - *scanner_ptr->get_num_axial_buckets() - -scanner_ptr->get_axial_crystal_spacing() - -ax_blocks_gap*(scanner_ptr->get_num_axial_blocks_per_bucket() - *scanner_ptr->get_num_axial_buckets()-1) - )/2; - - this->z_shift.z()=shift2; + this->z_shift.z()=shift; this->z_shift.y()=0; this->z_shift.x()=0; } diff --git a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx index 4174c57d5..117721fd8 100755 --- a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx +++ b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx @@ -74,6 +74,10 @@ class BlocksTests: public RunTests void run_axial_projection_test(); }; +/*! The following is a test for axial symmetries: a simulated image is created with a line along the z direction + * the image is forward projected to a sinogram and the sinogram back projected to an image this image should + * be symmetrical along z +*/ void BlocksTests::run_axial_projection_test(){ @@ -373,7 +377,8 @@ BlocksTests::run_plane_symmetry_test(){ /*! The following is a test for symmetries: a simulated image is created with spherical source in front of each detector block, * the forward projected sinogram should show the same bin values in simmetric places (in this test a dodecagone scanner is * used so we have symmetry every 30 degrees. The above is repeated for an image with sources in front of the dodecagone corners. - * the sinogram should have now different values at fixed bin compared the previous image + * the sinogram should have now different values at fixed bin compared the previous image. In this test we are testing a projdata_info + * with a negative view offset. */ void BlocksTests::run_symmetry_test(){ From b86ff417f50185e5eb42035390fa90f69ea28d4b Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Tue, 25 Jan 2022 10:38:28 +0000 Subject: [PATCH 69/81] [skip ci] modify tests documentation --- .../test_blocks_on_cylindrical_projectors.cxx | 14 +++++++------- src/test/test_proj_data_info.cxx | 11 ++++++----- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx index 117721fd8..e53190021 100755 --- a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx +++ b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx @@ -74,8 +74,8 @@ class BlocksTests: public RunTests void run_axial_projection_test(); }; -/*! The following is a test for axial symmetries: a simulated image is created with a line along the z direction - * the image is forward projected to a sinogram and the sinogram back projected to an image this image should +/*! The following is a test for axial symmetries: a simulated image is created with a line along the z direction. + * The image is forward projected to a sinogram and the sinogram back projected to an image. This image should * be symmetrical along z */ void @@ -199,7 +199,7 @@ BlocksTests::run_axial_projection_test(){ } /*! The following is a test for symmetries: a simulated image is created with a plane at known angles, - * the forward projected sinogram should show the makimum value at the bin corresponding to the angl phi + * the forward projected sinogram should show the maximum value at the bin corresponding to the angle phi * equal to the orientation of the plane */ void @@ -375,10 +375,10 @@ BlocksTests::run_plane_symmetry_test(){ } /*! The following is a test for symmetries: a simulated image is created with spherical source in front of each detector block, - * the forward projected sinogram should show the same bin values in simmetric places (in this test a dodecagone scanner is - * used so we have symmetry every 30 degrees. The above is repeated for an image with sources in front of the dodecagone corners. - * the sinogram should have now different values at fixed bin compared the previous image. In this test we are testing a projdata_info - * with a negative view offset. + * the forward projected sinogram should show the same bin values in symmetric places (in this test a dodecagon scanner is + * used so we have symmetry every 30 degrees. The above is repeated for an image with sources in front of the dodecagon corners. + * The sinogram should have now different values at fixed bin compared the previous image. In this test we are also testing a + * projdata_info with a negative view offset. */ void BlocksTests::run_symmetry_test(){ diff --git a/src/test/test_proj_data_info.cxx b/src/test/test_proj_data_info.cxx index eb9847b94..b587219e4 100755 --- a/src/test/test_proj_data_info.cxx +++ b/src/test/test_proj_data_info.cxx @@ -526,12 +526,13 @@ ProjDataInfoTests::run_Blocks_DOI_test() /*! The following tests the consistency of coordinates obtained with a cilindrical scanner - and those of a blocks on cylindrical scanner. For this test, a scanner with 1 ring and - 1 detector per block is used. + and those of a blocks on cylindrical scanner. For this test, a scanner with 4 rings, 2 + detector per block and 2 blcs per bucket is used axially. However, transaxially we have + 1 crystal per block a 1 block per bucket In this function, an extra test is performed to check that a roundtrip transformation: detector_ID->cartesian_coord_detection_pos->detector_ID - provide the same as the starting point + provides the same as the starting point */ void ProjDataInfoTests::run_coordinate_test() @@ -879,9 +880,9 @@ ProjDataInfoTests::run_coordinate_test_for_realistic_scanner() /*! The following tests the function get_s() for the BlockOnCylindrical case. the first test - checks that all lines passing for the center provide s=0. The second test check that + checks that all lines passing for the center provide s=0. The second test checks that parallel lines are always at the same angle phi, and that the step between consecutive - lines is the same and equal to the one calculate geometrically. + lines is the same and equal to the one calculated geometrically. */ void ProjDataInfoTests:: From 656889a5156491f467a6f2f15e2b1f9b6bf825db Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Tue, 25 Jan 2022 14:30:24 +0000 Subject: [PATCH 70/81] [skip ci] modify tests documentation --- .../test_blocks_on_cylindrical_projectors.cxx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx index e53190021..2fdaf1f17 100755 --- a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx +++ b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx @@ -535,6 +535,12 @@ write_to_file("image_for2",*image2_sptr); check(bin1_150!= bin2_150,"the two data have different symmetries, the values should be different"); } +/*!The following is a test for the crystal maps. Two scanners and ProjDataInfo are created, one with the standard map orientation + * and the other with an orientation along the view which is opposite to the first one. A simulated sphere was forward projected + * to look at bin values in the two cases. The bin obtained from the two different projdata will have different coordinates but + * the same value. +*/ + void BlocksTests::run_map_orientation_test() { @@ -688,16 +694,9 @@ BlocksTests::run_map_orientation_test() bin.view_num() = view; bin.tangential_pos_num() = 0; - -// bin_ord.segment_num() = 0; -// bin_ord.axial_pos_num() = 0; -// bin_ord.view_num() = proj_data_info_blocks_reord_ptr->get_max_view_num()-view; -// bin_ord.tangential_pos_num() = 0; - proj_data_info_blocks_ptr->get_det_pos_pair_for_bin(dp1,bin); proj_data_info_blocks_reord_ptr->get_det_pos_pair_for_bin(dp2,bin); -// rojDataInfoGenericNoArcCorr::get_ proj_data_info_blocks_ptr->get_bin_for_det_pos_pair(bin1,dp1); proj_data_info_blocks_reord_ptr->get_bin_for_det_pos_pair(bin2,dp2); From 5147f7b501575ba65f64ab346590296dff8fa560 Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Mon, 14 Feb 2022 17:07:58 +0000 Subject: [PATCH 71/81] fixing error with negative view offset created text for voxel on cartesian grid --- src/buildblock/VoxelsOnCartesianGrid.cxx | 10 +++- .../test_blocks_on_cylindrical_projectors.cxx | 50 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/buildblock/VoxelsOnCartesianGrid.cxx b/src/buildblock/VoxelsOnCartesianGrid.cxx index 0d1e66e5a..abd94574d 100644 --- a/src/buildblock/VoxelsOnCartesianGrid.cxx +++ b/src/buildblock/VoxelsOnCartesianGrid.cxx @@ -287,10 +287,16 @@ construct_from_projdata_info(const shared_ptr < const ExamInfo > & exam_info_spt if (sizes.x()==-1 || sizes.y()==-1) { + std::vector radii; + for (int view=0; view=2*FOVradius_in_pixs+1 const float FOVradius_in_mm = - max(proj_data_info.get_s(Bin(0,0,0,proj_data_info.get_max_tangential_pos_num())), - -proj_data_info.get_s(Bin(0,0,0,proj_data_info.get_min_tangential_pos_num()))); + *std::max_element(radii.begin(), radii.end()); + if (sizes.x()==-1) x_size_used = 2*static_cast(ceil(FOVradius_in_mm / get_voxel_size().x())) + 1; if (sizes.y()==-1) diff --git a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx index 2fdaf1f17..ed25e80a7 100755 --- a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx +++ b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx @@ -54,6 +54,7 @@ #include "stir/recon_buildblock/ForwardProjectorByBinUsingProjMatrixByBin.h" #include "stir/recon_buildblock/BackProjectorByBinUsingProjMatrixByBin.h" #include "stir/IO/write_to_file.h" +#include "stir/VoxelsOnCartesianGrid.h" //#include "stir/Shape/Shape3D.h" START_NAMESPACE_STIR @@ -72,8 +73,56 @@ class BlocksTests: public RunTests void run_plane_symmetry_test(); void run_map_orientation_test(); void run_axial_projection_test(); + void run_voxelOnCylindrical_with_negative_offset(); }; + +void +BlocksTests::run_voxelOnCylindrical_with_negative_offset(){ + // create projadata info + + auto scannerBlocks_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); + + scannerBlocks_ptr->set_intrinsic_azimuthal_tilt(-30); + scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); + scannerBlocks_ptr->set_up(); + + VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + + for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ + min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + if (iget_num_rings()) + num_axial_pos_per_segment[i]=i+1; + else + num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; + } + + auto proj_data_info_blocks_ptr=std::make_shared( + scannerBlocks_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + scannerBlocks_ptr->get_max_num_views(), + scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); + + bool is_ok=true; + + try{ + auto grid =std::make_shared >(*proj_data_info_blocks_ptr, + 1, + CartesianCoordinate3D(0.F,0.F,0.F), + CartesianCoordinate3D(-1,-1,-1)); + } + catch(...){ + is_ok=false; + } + + check_if_equal(is_ok,true); +} + + /*! The following is a test for axial symmetries: a simulated image is created with a line along the z direction. * The image is forward projected to a sinogram and the sinogram back projected to an image. This image should * be symmetrical along z @@ -724,6 +773,7 @@ run_tests() { std::cerr << "-------- Testing Blocks Geometry --------\n"; + run_voxelOnCylindrical_with_negative_offset(); run_axial_projection_test(); run_map_orientation_test(); run_symmetry_test(); From e9e699009270630035440a48b966e7e4ab334935 Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Mon, 14 Feb 2022 17:12:50 +0000 Subject: [PATCH 72/81] ci-skip changed name of test --- src/recon_test/test_blocks_on_cylindrical_projectors.cxx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx index ed25e80a7..5c9fa4a97 100755 --- a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx +++ b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx @@ -73,12 +73,12 @@ class BlocksTests: public RunTests void run_plane_symmetry_test(); void run_map_orientation_test(); void run_axial_projection_test(); - void run_voxelOnCylindrical_with_negative_offset(); + void run_voxelOnCartesianGrid_with_negative_offset(); }; void -BlocksTests::run_voxelOnCylindrical_with_negative_offset(){ +BlocksTests::run_voxelOnCartesianGrid_with_negative_offset(){ // create projadata info auto scannerBlocks_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); @@ -773,7 +773,7 @@ run_tests() { std::cerr << "-------- Testing Blocks Geometry --------\n"; - run_voxelOnCylindrical_with_negative_offset(); + run_voxelOnCartesianGrid_with_negative_offset(); run_axial_projection_test(); run_map_orientation_test(); run_symmetry_test(); From d0f26d7eba5640827bb15d364138d01e7b5f64bc Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Tue, 15 Feb 2022 08:09:17 +0000 Subject: [PATCH 73/81] [Appveyor] run ctest with --output-on-failure as opposed -VV, we only output failed tests now, preventing log overrun [actions skip] --- .appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index e1065e01e..a644a5202 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -30,6 +30,6 @@ build_script: test_script: - cd C:\projects\stir\build - - ctest -VV -C %CONFIGURATION% + - ctest --output-on-failure -C %CONFIGURATION% - cd ..\recon_test_pack - run_tests --nointbp "C:\projects\stir\install\bin\" From 20bfaebca0d5b4e8a230a555ba076ed6c6f2c022 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Tue, 15 Feb 2022 12:34:25 +0000 Subject: [PATCH 74/81] reduced verbosity and warnings in test_blocks_on_cylindrical_projectors --- .../test_blocks_on_cylindrical_projectors.cxx | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx index 5c9fa4a97..6a45f676e 100755 --- a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx +++ b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx @@ -8,7 +8,7 @@ \author Daniel Deidda */ -/* Copyright (C) 2021, National Physical Laboratory +/* Copyright (C) 2021-2022, National Physical Laboratory This file is part of STIR. This file is free software; you can redistribute it and/or modify @@ -32,6 +32,7 @@ #include "stir/recon_buildblock/ProjMatrixElemsForOneBin.h" #include "stir/recon_buildblock/ProjMatrixByBinUsingRayTracing.h" #include "stir/ExamInfo.h" +#include "stir/Verbosity.h" #include "stir/LORCoordinates.h" #include "stir/ProjDataInfo.h" #include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" @@ -129,6 +130,10 @@ BlocksTests::run_voxelOnCartesianGrid_with_negative_offset(){ */ void BlocksTests::run_axial_projection_test(){ + + //-- ExamInfo + auto exam_info_sptr = std::make_shared(); + exam_info_sptr->imaging_modality = ImagingModality::PT; CartesianCoordinate3D origin (0,0,0); CartesianCoordinate3D grid_spacing (1.1,2.2,2.2); @@ -136,7 +141,7 @@ BlocksTests::run_axial_projection_test(){ const IndexRange<3> range(Coordinate3D(0,-45,-45), Coordinate3D(24,44,44)); - VoxelsOnCartesianGrid image(range,origin, grid_spacing); + VoxelsOnCartesianGrid image(exam_info_sptr, range, origin, grid_spacing); // 60 degrees float phi1= 0*_PI/180; @@ -212,11 +217,7 @@ BlocksTests::run_axial_projection_test(){ image_sptr); bck_projector_sptr->set_up(proj_data_info_blocks_ptr, bck_proj_image_sptr); - - //-- ExamInfo - auto exam_info_sptr=std::make_shared(); - exam_info_sptr->imaging_modality = ImagingModality::PT; - + auto projdata=std::make_shared(exam_info_sptr, proj_data_info_blocks_ptr, "test_axial.hs", @@ -253,7 +254,11 @@ BlocksTests::run_axial_projection_test(){ */ void BlocksTests::run_plane_symmetry_test(){ - + + //-- ExamInfo + auto exam_info_sptr = std::make_shared(); + exam_info_sptr->imaging_modality = ImagingModality::PT; + CartesianCoordinate3D origin (0,0,0); CartesianCoordinate3D grid_spacing (1.1,2.2,2.2); float phi1; @@ -261,7 +266,7 @@ BlocksTests::run_plane_symmetry_test(){ const IndexRange<3> range(Coordinate3D(0,-45,-44), Coordinate3D(24,44,45)); - VoxelsOnCartesianGrid image(range,origin, grid_spacing); + VoxelsOnCartesianGrid image(exam_info_sptr, range, origin, grid_spacing); // 60 degrees phi1= 60*_PI/180; @@ -355,10 +360,6 @@ BlocksTests::run_plane_symmetry_test(){ forw_projector2_sptr->set_up(proj_data_info_blocks_ptr, image2_sptr); - //-- ExamInfo - auto exam_info_sptr=std::make_shared(); - exam_info_sptr->imaging_modality = ImagingModality::PT; - auto projdata=std::make_shared(exam_info_sptr, proj_data_info_blocks_ptr, "sino1_from_plane.hs", @@ -432,14 +433,18 @@ BlocksTests::run_plane_symmetry_test(){ void BlocksTests::run_symmetry_test(){ - CartesianCoordinate3D origin (0,0,0); + //-- ExamInfo + auto exam_info_sptr = std::make_shared(); + exam_info_sptr->imaging_modality = ImagingModality::PT; + + CartesianCoordinate3D origin(0, 0, 0); CartesianCoordinate3D grid_spacing (1.1,2.2,2.2); float theta1=0;float theta2=0; const IndexRange<3> range(Coordinate3D(0,-45,-44), Coordinate3D(24,44,45)); - VoxelsOnCartesianGrid image(range,origin, grid_spacing); + VoxelsOnCartesianGrid image(exam_info_sptr, range,origin, grid_spacing); const Array<2,float> direction_vectors= make_array(make_1d_array(1.F,0.F,0.F), @@ -541,11 +546,7 @@ write_to_file("image_for2",*image2_sptr); auto forw_projector2_sptr=std::make_shared(PM); forw_projector2_sptr->set_up(proj_data_info_blocks_ptr, image2_sptr); - - //-- ExamInfo - auto exam_info_sptr=std::make_shared(); - exam_info_sptr->imaging_modality = ImagingModality::PT; - + auto projdata1=std::make_shared(exam_info_sptr, proj_data_info_blocks_ptr, "sino1_from_image.hs",std::ios::out | std::ios::trunc | std::ios::in); @@ -594,14 +595,17 @@ void BlocksTests::run_map_orientation_test() { CPUTimer timer; - + //-- ExamInfo + auto exam_info_sptr = std::make_shared(); + exam_info_sptr->imaging_modality = ImagingModality::PT; + CartesianCoordinate3D origin (0,0,0); CartesianCoordinate3D grid_spacing (1.1,2.2,2.2); float theta1=0; const IndexRange<3> range(Coordinate3D(0,-45,-44), Coordinate3D(24,44,45)); - VoxelsOnCartesianGrid image(range,origin, grid_spacing); + VoxelsOnCartesianGrid image(exam_info_sptr, range, origin, grid_spacing); const Array<2,float> direction_vectors=make_array(make_1d_array(1.F,0.F,0.F), make_1d_array(0.F,cos(theta1),sin(theta1)), @@ -717,12 +721,8 @@ BlocksTests::run_map_orientation_test() auto forw_projector2_sptr= std::make_shared(PM); forw_projector2_sptr->set_up(proj_data_info_blocks_reord_ptr, - image1_sptr); - - //-- ExamInfo - auto exam_info_sptr= std::make_shared(); - exam_info_sptr->imaging_modality = ImagingModality::PT; - + image1_sptr); + auto projdata1= std::make_shared(exam_info_sptr, proj_data_info_blocks_ptr);//, // "sino1_map.hs",std::ios::out | std::ios::trunc | std::ios::in)); @@ -786,6 +786,7 @@ USING_NAMESPACE_STIR int main() { + Verbosity::set(1); BlocksTests tests; tests.run_tests(); return tests.main_return_value(); From 95af0ddb6026589297e23d19a203b8825096ad52 Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Thu, 17 Feb 2022 16:58:39 +0000 Subject: [PATCH 75/81] remove repetition code in test by using a function, change warning for symmetries --- ...ataSymmetriesForBins_PET_CartesianGrid.inl | 2 +- .../test_blocks_on_cylindrical_projectors.cxx | 155 +++++++----------- src/test/test_proj_data_info.cxx | 126 ++++++-------- 3 files changed, 115 insertions(+), 168 deletions(-) diff --git a/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl b/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl index 20a118f46..d7b491d34 100644 --- a/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl +++ b/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl @@ -217,7 +217,7 @@ find_sym_op_bin0( || do_symmetry_swap_s || do_symmetry_180degrees_min_phi) { - warning("Currently, only symmetry along z is implemented for block geometry.\n"); + warning("Currently, no symmetry is implemented for block geometry.\n"); return new TrivialSymmetryOperation(); } diff --git a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx index 6a45f676e..16eca9e37 100755 --- a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx +++ b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx @@ -75,9 +75,43 @@ class BlocksTests: public RunTests void run_map_orientation_test(); void run_axial_projection_test(); void run_voxelOnCartesianGrid_with_negative_offset(); -}; + shared_ptr set_blocks_projdata_info(VectorWithOffset& num_axial_pos_per_segment, + VectorWithOffset& min_ring_diff_v, + VectorWithOffset& max_ring_diff_v, + shared_ptr scanner_ptr); + }; +/*! The following is a function to allow a projdata_info blocksONCylindrical to be created from the scanner. +*/ +shared_ptr +BlocksTests::set_blocks_projdata_info(VectorWithOffset& num_axial_pos_per_segment, + VectorWithOffset& min_ring_diff_v, + VectorWithOffset& max_ring_diff_v, + shared_ptr scanner_ptr){ + + for (int i=0; i<2*scanner_ptr->get_num_rings()-1; i++){ + min_ring_diff_v[i]=-scanner_ptr->get_num_rings()+1+i; + max_ring_diff_v[i]=-scanner_ptr->get_num_rings()+1+i; + if (iget_num_rings()) + num_axial_pos_per_segment[i]=i+1; + else + num_axial_pos_per_segment[i]=2*scanner_ptr->get_num_rings()-i-1; + } + + auto proj_data_info_blocks_ptr=std::make_shared( + scanner_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + scanner_ptr->get_max_num_views(), + scanner_ptr->get_max_num_non_arccorrected_bins()); + + return proj_data_info_blocks_ptr; +} +/*! The following is a test for the view offset with voxelOnCartesianGrid: ascanner is created and its relative + * projdata info using a negative offset is set. When calling the voxelOnCartesianGrid constructor if everything ok + * a variable called is_OK is set to true and false otherwise. If false the test will fail. +*/ void BlocksTests::run_voxelOnCartesianGrid_with_negative_offset(){ // create projadata info @@ -91,22 +125,11 @@ BlocksTests::run_voxelOnCartesianGrid_with_negative_offset(){ VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - - for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ - min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; - max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; - if (iget_num_rings()) - num_axial_pos_per_segment[i]=i+1; - else - num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; - } - - auto proj_data_info_blocks_ptr=std::make_shared( - scannerBlocks_ptr, - num_axial_pos_per_segment, - min_ring_diff_v, max_ring_diff_v, - scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); + auto proj_data_info_blocks_ptr=std::make_shared(); + proj_data_info_blocks_ptr=set_blocks_projdata_info(num_axial_pos_per_segment, + min_ring_diff_v, + max_ring_diff_v, + scannerBlocks_ptr); bool is_ok=true; @@ -166,39 +189,17 @@ BlocksTests::run_axial_projection_test(){ // create projadata info auto scannerBlocks_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); -// scannerBlocks_ptr->set_num_axial_crystals_per_block(1); -// scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing()* -// scannerBlocks_ptr->get_num_axial_crystals_per_block()); -// scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing()* -// scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); -//// scannerBlocks_ptr->set_num_transaxial_crystals_per_block(1); -// scannerBlocks_ptr->set_num_axial_blocks_per_bucket(1); -//// scannerBlocks_ptr->set_num_transaxial_blocks_per_bucket(1); -// scannerBlocks_ptr->set_num_rings(1); - scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); scannerBlocks_ptr->set_up(); VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - - for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ - min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; - max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; - if (iget_num_rings()) - num_axial_pos_per_segment[i]=i+1; - else - num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; - } - - auto proj_data_info_blocks_ptr=std::make_shared( - scannerBlocks_ptr, - num_axial_pos_per_segment, - min_ring_diff_v, max_ring_diff_v, - scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); -// now forward-project image + auto proj_data_info_blocks_ptr=std::make_shared(); + proj_data_info_blocks_ptr=set_blocks_projdata_info(num_axial_pos_per_segment, + min_ring_diff_v, + max_ring_diff_v, + scannerBlocks_ptr); // now forward-project image shared_ptr > image_sptr(image.clone()); shared_ptr > bck_proj_image_sptr(image.clone()); @@ -325,23 +326,13 @@ BlocksTests::run_plane_symmetry_test(){ VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + auto proj_data_info_blocks_ptr=std::make_shared(); + proj_data_info_blocks_ptr=set_blocks_projdata_info(num_axial_pos_per_segment, + min_ring_diff_v, + max_ring_diff_v, + scannerBlocks_ptr); - for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ - min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; - max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; - if (iget_num_rings()) - num_axial_pos_per_segment[i]=i+1; - else - num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; - } - - auto proj_data_info_blocks_ptr=std::make_shared( - scannerBlocks_ptr, - num_axial_pos_per_segment, - min_ring_diff_v, max_ring_diff_v, - scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); -// now forward-project image + // now forward-project image shared_ptr > image_sptr(image.clone()); write_to_file("plane60",*image_sptr); @@ -516,23 +507,11 @@ write_to_file("image_for2",*image2_sptr); VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - - for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ - min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; - max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; - if (iget_num_rings()) - num_axial_pos_per_segment[i]=i+1; - else - num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; - } - - auto proj_data_info_blocks_ptr=std::make_shared( - scannerBlocks_ptr, - num_axial_pos_per_segment, - min_ring_diff_v, max_ring_diff_v, - scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); - + auto proj_data_info_blocks_ptr=std::make_shared(); + proj_data_info_blocks_ptr=set_blocks_projdata_info(num_axial_pos_per_segment, + min_ring_diff_v, + max_ring_diff_v, + scannerBlocks_ptr); // now forward-project images auto PM=std::make_shared(); @@ -650,23 +629,11 @@ BlocksTests::run_map_orientation_test() VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - - for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ - min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; - max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; - if (iget_num_rings()) - num_axial_pos_per_segment[i]=i+1; - else - num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; - } - - auto proj_data_info_blocks_ptr = std::make_shared( - scannerBlocks_ptr, - num_axial_pos_per_segment, - min_ring_diff_v, max_ring_diff_v, - scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); - + auto proj_data_info_blocks_ptr=std::make_shared(); + proj_data_info_blocks_ptr=set_blocks_projdata_info(num_axial_pos_per_segment, + min_ring_diff_v, + max_ring_diff_v, + scannerBlocks_ptr); Bin bin, bin1,bin2, binR1; CartesianCoordinate3D< float> b1,b2,rb1,rb2; DetectionPosition<> det_pos, det_pos_ord; diff --git a/src/test/test_proj_data_info.cxx b/src/test/test_proj_data_info.cxx index b587219e4..f2f9e379b 100755 --- a/src/test/test_proj_data_info.cxx +++ b/src/test/test_proj_data_info.cxx @@ -100,12 +100,45 @@ class ProjDataInfoTests: public RunTests protected: void test_generic_proj_data_info(ProjDataInfo& proj_data_info); + shared_ptr set_blocks_projdata_info(VectorWithOffset& num_axial_pos_per_segment, + VectorWithOffset& min_ring_diff_v, + VectorWithOffset& max_ring_diff_v, + shared_ptr scanner_ptr); + void run_coordinate_test(); void run_coordinate_test_for_realistic_scanner(); void run_Blocks_DOI_test(); void run_lor_get_s_test(); }; +/*! The following is a function to allow a projdata_info blocksONCylindrical to be created from the scanner. +*/ +shared_ptr +ProjDataInfoTests:: +set_blocks_projdata_info(VectorWithOffset& num_axial_pos_per_segment, + VectorWithOffset& min_ring_diff_v, + VectorWithOffset& max_ring_diff_v, + shared_ptr scanner_ptr){ + + for (int i=0; i<2*scanner_ptr->get_num_rings()-1; i++){ + min_ring_diff_v[i]=-scanner_ptr->get_num_rings()+1+i; + max_ring_diff_v[i]=-scanner_ptr->get_num_rings()+1+i; + if (iget_num_rings()) + num_axial_pos_per_segment[i]=i+1; + else + num_axial_pos_per_segment[i]=2*scanner_ptr->get_num_rings()-i-1; + } + + auto proj_data_info_blocks_ptr=std::make_shared( + scanner_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + scanner_ptr->get_max_num_views(), + scanner_ptr->get_max_num_non_arccorrected_bins()); + + return proj_data_info_blocks_ptr; +} + void ProjDataInfoTests:: test_generic_proj_data_info(ProjDataInfo& proj_data_info) @@ -453,21 +486,11 @@ ProjDataInfoTests::run_Blocks_DOI_test() VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ - min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; - max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; - if (iget_num_rings()) - num_axial_pos_per_segment[i]=i+1; - else - num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; - } - - auto proj_data_info_blocks_doi0_ptr=std::make_shared( - scannerBlocks_ptr, - num_axial_pos_per_segment, - min_ring_diff_v, max_ring_diff_v, - scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); + auto proj_data_info_blocks_doi0_ptr=std::make_shared(); + proj_data_info_blocks_doi0_ptr=set_blocks_projdata_info(num_axial_pos_per_segment, + min_ring_diff_v, + max_ring_diff_v, + scannerBlocks_ptr); auto scannerBlocksDOI_ptr=std::make_shared(*scannerBlocks_ptr); scannerBlocksDOI_ptr->set_average_depth_of_interaction(0.1); @@ -552,21 +575,6 @@ ProjDataInfoTests::run_coordinate_test() scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); scannerBlocks_ptr->set_up(); - // int num_transaxial_blocks_per_bucket = scannerBlocks_ptr->get_num_transaxial_blocks_per_bucket(); - // int num_transaxial_crystals_per_block = scannerBlocks_ptr->get_num_transaxial_crystals_per_block(); - // int num_transaxial_buckets = scannerBlocks_ptr->get_num_transaxial_blocks()/num_transaxial_blocks_per_bucket; - // float transaxial_block_spacing = scannerBlocks_ptr->get_transaxial_block_spacing(); - // float transaxial_crystal_spacing = scannerBlocks_ptr->get_transaxial_crystal_spacing(); -// estimate the angle covered by a bucket, alpha - - //float csi=_PI/num_transaxial_buckets; - //float trans_blocks_gap=transaxial_block_spacing-num_transaxial_crystals_per_block*transaxial_crystal_spacing; - /*float csi_minus_csiGaps=csi-(csi/transaxial_block_spacing/2)* - (transaxial_crystal_spacing/2+trans_blocks_gap);*/ - - //float dx=scannerBlocks_ptr->get_effective_ring_radius()*sin(csi_minus_csiGaps); - //float dy=scannerBlocks_ptr->get_effective_ring_radius()-scannerBlocks_ptr->get_effective_ring_radius()*cos(csi_minus_csiGaps); - auto scannerCyl_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); scannerCyl_ptr->set_num_axial_crystals_per_block(2); scannerCyl_ptr->set_axial_block_spacing(scannerCyl_ptr->get_axial_crystal_spacing()* @@ -585,21 +593,12 @@ ProjDataInfoTests::run_coordinate_test() VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ - min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; - max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; - if (iget_num_rings()) - num_axial_pos_per_segment[i]=i+1; - else - num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; - } - auto proj_data_info_blocks_ptr=std::make_shared( - scannerBlocks_ptr, - num_axial_pos_per_segment, - min_ring_diff_v, max_ring_diff_v, - scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); + auto proj_data_info_blocks_ptr=std::make_shared(); + proj_data_info_blocks_ptr=set_blocks_projdata_info(num_axial_pos_per_segment, + min_ring_diff_v, + max_ring_diff_v, + scannerBlocks_ptr); auto proj_data_info_cyl_ptr=std::make_shared( scannerCyl_ptr, @@ -778,21 +777,12 @@ ProjDataInfoTests::run_coordinate_test_for_realistic_scanner() VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ - min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; - max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; - if (iget_num_rings()) - num_axial_pos_per_segment[i]=i+1; - else - num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; - } - auto proj_data_info_blocks_ptr=std::make_shared( - scannerBlocks_ptr, - num_axial_pos_per_segment, - min_ring_diff_v, max_ring_diff_v, - scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); + auto proj_data_info_blocks_ptr=std::make_shared(); + proj_data_info_blocks_ptr=set_blocks_projdata_info(num_axial_pos_per_segment, + min_ring_diff_v, + max_ring_diff_v, + scannerBlocks_ptr); auto proj_data_info_cyl_ptr=std::make_shared( scannerCyl_ptr, @@ -910,21 +900,11 @@ run_lor_get_s_test(){ VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ - min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; - max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; - if (iget_num_rings()) - num_axial_pos_per_segment[i]=i+1; - else - num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; - } - - auto proj_data_info_blocks_ptr=std::make_shared( - scannerBlocks_ptr, - num_axial_pos_per_segment, - min_ring_diff_v, max_ring_diff_v, - scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); + auto proj_data_info_blocks_ptr=std::make_shared(); + proj_data_info_blocks_ptr=set_blocks_projdata_info(num_axial_pos_per_segment, + min_ring_diff_v, + max_ring_diff_v, + scannerBlocks_ptr); auto proj_data_info_cyl_ptr=std::make_shared( scannerCyl_ptr, From e50aeb042b6f000fa1f5bf1d6cdabf847740f2cc Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Thu, 17 Feb 2022 17:15:28 +0000 Subject: [PATCH 76/81] disabling cache --- src/recon_test/test_blocks_on_cylindrical_projectors.cxx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx index 16eca9e37..451d142f6 100755 --- a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx +++ b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx @@ -206,6 +206,7 @@ BlocksTests::run_axial_projection_test(){ write_to_file("axial_test",*image_sptr); auto PM=std::make_shared(); + PM->enable_cache(flase); // PM->set_do_symmetry_90degrees_min_phi(false); // PM->set_do_symmetry_shift_z(false); // PM->set_do_symmetry_swap_segment(false); @@ -341,6 +342,7 @@ BlocksTests::run_plane_symmetry_test(){ write_to_file("plane30",*image2_sptr); auto PM=std::make_shared(); + PM->enable_cache(flase); auto forw_projector_sptr=std::make_shared(PM); info(boost::format("Test blocks on Cylindrical: Forward projector used: %1%") % forw_projector_sptr->parameter_info()); @@ -515,6 +517,7 @@ write_to_file("image_for2",*image2_sptr); // now forward-project images auto PM=std::make_shared(); + PM->enable_cache(flase); auto forw_projector1_sptr=std::make_shared(PM); info(boost::format("Test blocks on Cylindrical: Forward projector used: %1%") % forw_projector1_sptr->parameter_info()); @@ -680,7 +683,7 @@ BlocksTests::run_map_orientation_test() // now forward-project images auto PM= std::make_shared(); - + PM->enable_cache(flase); auto forw_projector1_sptr= std::make_shared(PM); info(boost::format("Test blocks on Cylindrical: Forward projector used: %1%") % forw_projector1_sptr->parameter_info()); forw_projector1_sptr->set_up(proj_data_info_blocks_ptr, From 50c0fee9c3a2a46d0d6740e54bbb0f9de6835c47 Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Fri, 18 Feb 2022 11:00:25 +0000 Subject: [PATCH 77/81] kill warnings and correct typo for enable_cache(false) --- src/buildblock/ProjDataInfoGenericNoArcCorr.cxx | 4 ---- src/include/stir/DetectorCoordinateMap.h | 6 +++--- src/include/stir/ProjDataInfoGeneric.inl | 4 ++-- src/recon_test/test_blocks_on_cylindrical_projectors.cxx | 8 ++++---- src/test/test_proj_data_info.cxx | 1 - 5 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx index 070ed7af7..bff19f716 100644 --- a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx +++ b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx @@ -65,10 +65,6 @@ ProjDataInfoGenericNoArcCorr(const shared_ptr scanner_ptr, uncompressed_view_tangpos_to_det1det2_initialised = false; det1det2_to_uncompressed_view_tangpos_initialised = false; - float ax_blocks_gap=scanner_ptr->get_axial_block_spacing() - -scanner_ptr->get_num_axial_crystals_per_block() - *scanner_ptr->get_axial_crystal_spacing(); - CartesianCoordinate3D< float> b1,b2; Bin bin; bin.segment_num() = 0; diff --git a/src/include/stir/DetectorCoordinateMap.h b/src/include/stir/DetectorCoordinateMap.h index 6e5006b02..72cf3acee 100644 --- a/src/include/stir/DetectorCoordinateMap.h +++ b/src/include/stir/DetectorCoordinateMap.h @@ -89,9 +89,9 @@ class DetectorCoordinateMap stir::CartesianCoordinate3D get_coordinate_for_det_pos( const stir::DetectionPosition<>& det_pos ) const { auto coord = det_pos_to_coord.at(det_pos); - coord.x() += distribution(generator); - coord.y() += distribution(generator); - coord.z() += distribution(generator); + coord.x() += static_cast(distribution(generator)); + coord.y() += static_cast(distribution(generator)); + coord.z() += static_cast(distribution(generator)); return coord; } //! Returns a cartesian coordinate given an (unsorted) index. diff --git a/src/include/stir/ProjDataInfoGeneric.inl b/src/include/stir/ProjDataInfoGeneric.inl index acdc1e5bd..a1eb94919 100644 --- a/src/include/stir/ProjDataInfoGeneric.inl +++ b/src/include/stir/ProjDataInfoGeneric.inl @@ -49,7 +49,7 @@ ProjDataInfoGeneric::get_phi(const Bin& bin)const LORInAxialAndNoArcCorrSinogramCoordinates lor; get_LOR(lor, bin); if (bin.view_num()==0 && lor.phi()>0.1) - return lor.phi()-_PI; + return static_cast(lor.phi())-_PI; return lor.phi(); } @@ -61,7 +61,7 @@ ProjDataInfoGeneric::get_m(const Bin& bin) const { LORInAxialAndNoArcCorrSinogramCoordinates lor; get_LOR(lor, bin); - return (lor.z1() + lor.z2())/2.; + return (static_cast(lor.z1() + lor.z2()))/2.; } float diff --git a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx index 451d142f6..f6bc7c601 100755 --- a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx +++ b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx @@ -206,7 +206,7 @@ BlocksTests::run_axial_projection_test(){ write_to_file("axial_test",*image_sptr); auto PM=std::make_shared(); - PM->enable_cache(flase); + PM->enable_cache(false); // PM->set_do_symmetry_90degrees_min_phi(false); // PM->set_do_symmetry_shift_z(false); // PM->set_do_symmetry_swap_segment(false); @@ -342,7 +342,7 @@ BlocksTests::run_plane_symmetry_test(){ write_to_file("plane30",*image2_sptr); auto PM=std::make_shared(); - PM->enable_cache(flase); + PM->enable_cache(false); auto forw_projector_sptr=std::make_shared(PM); info(boost::format("Test blocks on Cylindrical: Forward projector used: %1%") % forw_projector_sptr->parameter_info()); @@ -517,7 +517,7 @@ write_to_file("image_for2",*image2_sptr); // now forward-project images auto PM=std::make_shared(); - PM->enable_cache(flase); + PM->enable_cache(false); auto forw_projector1_sptr=std::make_shared(PM); info(boost::format("Test blocks on Cylindrical: Forward projector used: %1%") % forw_projector1_sptr->parameter_info()); @@ -683,7 +683,7 @@ BlocksTests::run_map_orientation_test() // now forward-project images auto PM= std::make_shared(); - PM->enable_cache(flase); + PM->enable_cache(false); auto forw_projector1_sptr= std::make_shared(PM); info(boost::format("Test blocks on Cylindrical: Forward projector used: %1%") % forw_projector1_sptr->parameter_info()); forw_projector1_sptr->set_up(proj_data_info_blocks_ptr, diff --git a/src/test/test_proj_data_info.cxx b/src/test/test_proj_data_info.cxx index f2f9e379b..b09339309 100755 --- a/src/test/test_proj_data_info.cxx +++ b/src/test/test_proj_data_info.cxx @@ -947,7 +947,6 @@ run_lor_get_s_test(){ int det_id_diff=scannerBlocks_ptr->get_num_detectors_per_ring()*(_PI/2)/(2*_PI); int Ctb=scannerCyl_ptr->get_num_transaxial_crystals_per_block(); float transaxial_crystal_spacing=scannerBlocks_ptr->get_transaxial_crystal_spacing(); - float block_trans_spacing=scannerBlocks_ptr->get_transaxial_block_spacing(); float prev_s=0; float prev_phi=0; for (int i=0; iget_num_transaxial_crystals_per_block();i++){ From 894842f23576113e65dd02dcb32470a8f797b0eb Mon Sep 17 00:00:00 2001 From: Daniel Deidda Date: Fri, 18 Feb 2022 13:33:44 +0000 Subject: [PATCH 78/81] Update src/include/stir/ProjDataInfoGeneric.inl Co-authored-by: Kris Thielemans --- src/include/stir/ProjDataInfoGeneric.inl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/include/stir/ProjDataInfoGeneric.inl b/src/include/stir/ProjDataInfoGeneric.inl index a1eb94919..3561b0ec3 100644 --- a/src/include/stir/ProjDataInfoGeneric.inl +++ b/src/include/stir/ProjDataInfoGeneric.inl @@ -61,7 +61,7 @@ ProjDataInfoGeneric::get_m(const Bin& bin) const { LORInAxialAndNoArcCorrSinogramCoordinates lor; get_LOR(lor, bin); - return (static_cast(lor.z1() + lor.z2()))/2.; + return (static_cast(lor.z1() + lor.z2()))/2.F; } float From 7e75c246b9ccfe370c336839c7367b521d87920d Mon Sep 17 00:00:00 2001 From: Daniel Deidda Date: Fri, 18 Feb 2022 13:34:05 +0000 Subject: [PATCH 79/81] Update src/include/stir/ProjDataInfoGeneric.inl Co-authored-by: Kris Thielemans --- src/include/stir/ProjDataInfoGeneric.inl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/include/stir/ProjDataInfoGeneric.inl b/src/include/stir/ProjDataInfoGeneric.inl index 3561b0ec3..20ae2cf21 100644 --- a/src/include/stir/ProjDataInfoGeneric.inl +++ b/src/include/stir/ProjDataInfoGeneric.inl @@ -49,7 +49,7 @@ ProjDataInfoGeneric::get_phi(const Bin& bin)const LORInAxialAndNoArcCorrSinogramCoordinates lor; get_LOR(lor, bin); if (bin.view_num()==0 && lor.phi()>0.1) - return static_cast(lor.phi())-_PI; + return static_cast(lor.phi()-_PI); return lor.phi(); } From 026ba198f914fe5de9858ad4bf1d5baf9a9549e2 Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Fri, 18 Feb 2022 15:37:50 +0000 Subject: [PATCH 80/81] clean-up repetition by using template function and fix white spacing --- .../test_blocks_on_cylindrical_projectors.cxx | 1171 +++++---- src/test/test_proj_data_info.cxx | 2195 ++++++++--------- 2 files changed, 1570 insertions(+), 1796 deletions(-) mode change 100755 => 100644 src/recon_test/test_blocks_on_cylindrical_projectors.cxx mode change 100755 => 100644 src/test/test_proj_data_info.cxx diff --git a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx old mode 100755 new mode 100644 index f6bc7c601..925168eb6 --- a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx +++ b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx @@ -60,701 +60,636 @@ START_NAMESPACE_STIR - /*! \ingroup test - \brief Test class for Blocks + \brief Test class for Blocks */ -class BlocksTests: public RunTests +class BlocksTests : public RunTests { public: void run_tests(); + private: void run_symmetry_test(); void run_plane_symmetry_test(); void run_map_orientation_test(); void run_axial_projection_test(); void run_voxelOnCartesianGrid_with_negative_offset(); - shared_ptr set_blocks_projdata_info(VectorWithOffset& num_axial_pos_per_segment, - VectorWithOffset& min_ring_diff_v, - VectorWithOffset& max_ring_diff_v, - shared_ptr scanner_ptr); - }; + template + shared_ptr set_blocks_projdata_info(shared_ptr scanner_sptr); +}; /*! The following is a function to allow a projdata_info blocksONCylindrical to be created from the scanner. -*/ -shared_ptr -BlocksTests::set_blocks_projdata_info(VectorWithOffset& num_axial_pos_per_segment, - VectorWithOffset& min_ring_diff_v, - VectorWithOffset& max_ring_diff_v, - shared_ptr scanner_ptr){ - - for (int i=0; i<2*scanner_ptr->get_num_rings()-1; i++){ - min_ring_diff_v[i]=-scanner_ptr->get_num_rings()+1+i; - max_ring_diff_v[i]=-scanner_ptr->get_num_rings()+1+i; - if (iget_num_rings()) - num_axial_pos_per_segment[i]=i+1; - else - num_axial_pos_per_segment[i]=2*scanner_ptr->get_num_rings()-i-1; - } - - auto proj_data_info_blocks_ptr=std::make_shared( - scanner_ptr, - num_axial_pos_per_segment, - min_ring_diff_v, max_ring_diff_v, - scanner_ptr->get_max_num_views(), - scanner_ptr->get_max_num_non_arccorrected_bins()); - - return proj_data_info_blocks_ptr; -} + */ +template +shared_ptr +BlocksTests::set_blocks_projdata_info(shared_ptr scanner_sptr) +{ + VectorWithOffset num_axial_pos_per_segment(scanner_sptr->get_num_rings() * 2 - 1); + VectorWithOffset min_ring_diff_v(scanner_sptr->get_num_rings() * 2 - 1); + VectorWithOffset max_ring_diff_v(scanner_sptr->get_num_rings() * 2 - 1); + for (int i = 0; i < 2 * scanner_sptr->get_num_rings() - 1; i++) + { + min_ring_diff_v[i] = -scanner_sptr->get_num_rings() + 1 + i; + max_ring_diff_v[i] = -scanner_sptr->get_num_rings() + 1 + i; + if (i < scanner_sptr->get_num_rings()) + num_axial_pos_per_segment[i] = i + 1; + else + num_axial_pos_per_segment[i] = 2 * scanner_sptr->get_num_rings() - i - 1; + } + + auto proj_data_info_blocks_sptr + = std::make_shared(scanner_sptr, num_axial_pos_per_segment, min_ring_diff_v, max_ring_diff_v, + scanner_sptr->get_max_num_views(), scanner_sptr->get_max_num_non_arccorrected_bins()); + return proj_data_info_blocks_sptr; +} /*! The following is a test for the view offset with voxelOnCartesianGrid: ascanner is created and its relative * projdata info using a negative offset is set. When calling the voxelOnCartesianGrid constructor if everything ok * a variable called is_OK is set to true and false otherwise. If false the test will fail. -*/ + */ void -BlocksTests::run_voxelOnCartesianGrid_with_negative_offset(){ - // create projadata info - - auto scannerBlocks_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); - - scannerBlocks_ptr->set_intrinsic_azimuthal_tilt(-30); - scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); - scannerBlocks_ptr->set_up(); - - VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); - VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - auto proj_data_info_blocks_ptr=std::make_shared(); - proj_data_info_blocks_ptr=set_blocks_projdata_info(num_axial_pos_per_segment, - min_ring_diff_v, - max_ring_diff_v, - scannerBlocks_ptr); - - bool is_ok=true; - - try{ - auto grid =std::make_shared >(*proj_data_info_blocks_ptr, - 1, - CartesianCoordinate3D(0.F,0.F,0.F), - CartesianCoordinate3D(-1,-1,-1)); - } - catch(...){ - is_ok=false; - } - - check_if_equal(is_ok,true); -} +BlocksTests::run_voxelOnCartesianGrid_with_negative_offset() +{ + // create projadata info + + auto scannerBlocks_sptr = std::make_shared(Scanner::SAFIRDualRingPrototype); + + scannerBlocks_sptr->set_intrinsic_azimuthal_tilt(-30); + scannerBlocks_sptr->set_scanner_geometry("BlocksOnCylindrical"); + scannerBlocks_sptr->set_up(); + + auto proj_data_info_blocks_sptr = std::make_shared(); + proj_data_info_blocks_sptr = set_blocks_projdata_info(scannerBlocks_sptr); + bool is_ok = true; + + try + { + auto grid = std::make_shared>( + *proj_data_info_blocks_sptr, 1, CartesianCoordinate3D(0.F, 0.F, 0.F), CartesianCoordinate3D(-1, -1, -1)); + } + catch (...) + { + is_ok = false; + } + + check_if_equal(is_ok, true); +} /*! The following is a test for axial symmetries: a simulated image is created with a line along the z direction. - * The image is forward projected to a sinogram and the sinogram back projected to an image. This image should + * The image is forward projected to a sinogram and the sinogram back projected to an image. This image should * be symmetrical along z -*/ + */ void -BlocksTests::run_axial_projection_test(){ +BlocksTests::run_axial_projection_test() +{ //-- ExamInfo auto exam_info_sptr = std::make_shared(); exam_info_sptr->imaging_modality = ImagingModality::PT; - - CartesianCoordinate3D origin (0,0,0); - CartesianCoordinate3D grid_spacing (1.1,2.2,2.2); - - const IndexRange<3> - range(Coordinate3D(0,-45,-45), - Coordinate3D(24,44,44)); - VoxelsOnCartesianGrid image(exam_info_sptr, range, origin, grid_spacing); - -// 60 degrees - float phi1= 0*_PI/180; - const Array<2,float> direction_vectors= - make_array(make_1d_array(1.F,0.F,0.F), - make_1d_array(0.F,cos(float(_PI)-phi1),sin(float(_PI)-phi1)), - make_1d_array(0.F,-sin(float(_PI)-phi1),cos(float(_PI)-phi1))); - - Ellipsoid - plane(CartesianCoordinate3D(/*edge_z*/50*grid_spacing.z(), - /*edge_y*/2*grid_spacing.y(), - /*edge_x*/2*grid_spacing.x()), - /*centre*/CartesianCoordinate3D((image.get_min_index()+image.get_max_index())/2*grid_spacing.z(), - 0*grid_spacing.y(), - 0), - direction_vectors); - - plane.construct_volume(image, make_coordinate(3,3,3)); - - - -// create projadata info - - auto scannerBlocks_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); - scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); - scannerBlocks_ptr->set_up(); - - VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); - VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - auto proj_data_info_blocks_ptr=std::make_shared(); - proj_data_info_blocks_ptr=set_blocks_projdata_info(num_axial_pos_per_segment, - min_ring_diff_v, - max_ring_diff_v, - scannerBlocks_ptr); // now forward-project image - - shared_ptr > image_sptr(image.clone()); - shared_ptr > bck_proj_image_sptr(image.clone()); - write_to_file("axial_test",*image_sptr); - - auto PM=std::make_shared(); - PM->enable_cache(false); -// PM->set_do_symmetry_90degrees_min_phi(false); -// PM->set_do_symmetry_shift_z(false); -// PM->set_do_symmetry_swap_segment(false); - - auto forw_projector_sptr=std::make_shared(PM); - auto bck_projector_sptr=std::make_shared(PM); - info(boost::format("Test blocks on Cylindrical: Forward projector used: %1%") % forw_projector_sptr->parameter_info()); - - forw_projector_sptr->set_up(proj_data_info_blocks_ptr, - image_sptr); - bck_projector_sptr->set_up(proj_data_info_blocks_ptr, - bck_proj_image_sptr); - - auto projdata=std::make_shared(exam_info_sptr, - proj_data_info_blocks_ptr, - "test_axial.hs", - std::ios::out | std::ios::trunc | std::ios::in); - - forw_projector_sptr->forward_project(*projdata, *image_sptr); - - bck_projector_sptr->back_project(*bck_proj_image_sptr, *projdata,0,1); - write_to_file("back_proj_axial_test",*bck_proj_image_sptr); - - int min_z = bck_proj_image_sptr->get_min_index(); - int max_z = bck_proj_image_sptr->get_max_index(); - int min_y = (*bck_proj_image_sptr)[min_z].get_min_index(); - int max_y = (*bck_proj_image_sptr)[min_z].get_max_index(); - int min_x = (*bck_proj_image_sptr)[min_z][min_y].get_min_index(); - int max_x = (*bck_proj_image_sptr)[min_z][min_y].get_max_index(); - -// get two planes in the image that are equidistant from the z center - int centre_z=(max_z-min_z)/2; - int plane_idA=centre_z-5; - int plane_idB=centre_z+5; - - for(int y=min_y; y origin(0, 0, 0); + CartesianCoordinate3D grid_spacing(1.1, 2.2, 2.2); + + const IndexRange<3> range(Coordinate3D(0, -45, -45), Coordinate3D(24, 44, 44)); + VoxelsOnCartesianGrid image(exam_info_sptr, range, origin, grid_spacing); + + // 60 degrees + float phi1 = 0 * _PI / 180; + const Array<2, float> direction_vectors + = make_array(make_1d_array(1.F, 0.F, 0.F), make_1d_array(0.F, cos(float(_PI) - phi1), sin(float(_PI) - phi1)), + make_1d_array(0.F, -sin(float(_PI) - phi1), cos(float(_PI) - phi1))); + + Ellipsoid plane(CartesianCoordinate3D(/*edge_z*/ 50 * grid_spacing.z(), + /*edge_y*/ 2 * grid_spacing.y(), + /*edge_x*/ 2 * grid_spacing.x()), + /*centre*/ + CartesianCoordinate3D((image.get_min_index() + image.get_max_index()) / 2 * grid_spacing.z(), + 0 * grid_spacing.y(), 0), + direction_vectors); + + plane.construct_volume(image, make_coordinate(3, 3, 3)); + + // create projadata info + + auto scannerBlocks_sptr = std::make_shared(Scanner::SAFIRDualRingPrototype); + scannerBlocks_sptr->set_scanner_geometry("BlocksOnCylindrical"); + scannerBlocks_sptr->set_up(); + + auto proj_data_info_blocks_sptr = std::make_shared(); + proj_data_info_blocks_sptr + = set_blocks_projdata_info(scannerBlocks_sptr); // now forward-project image + + shared_ptr> image_sptr(image.clone()); + shared_ptr> bck_proj_image_sptr(image.clone()); + write_to_file("axial_test", *image_sptr); + + auto PM = std::make_shared(); + PM->enable_cache(false); + // PM->set_do_symmetry_90degrees_min_phi(false); + // PM->set_do_symmetry_shift_z(false); + // PM->set_do_symmetry_swap_segment(false); + + auto forw_projector_sptr = std::make_shared(PM); + auto bck_projector_sptr = std::make_shared(PM); + info(boost::format("Test blocks on Cylindrical: Forward projector used: %1%") % forw_projector_sptr->parameter_info()); + + forw_projector_sptr->set_up(proj_data_info_blocks_sptr, image_sptr); + bck_projector_sptr->set_up(proj_data_info_blocks_sptr, bck_proj_image_sptr); + + auto projdata = std::make_shared(exam_info_sptr, proj_data_info_blocks_sptr, "test_axial.hs", + std::ios::out | std::ios::trunc | std::ios::in); + + forw_projector_sptr->forward_project(*projdata, *image_sptr); + + bck_projector_sptr->back_project(*bck_proj_image_sptr, *projdata, 0, 1); + write_to_file("back_proj_axial_test", *bck_proj_image_sptr); + + int min_z = bck_proj_image_sptr->get_min_index(); + int max_z = bck_proj_image_sptr->get_max_index(); + int min_y = (*bck_proj_image_sptr)[min_z].get_min_index(); + int max_y = (*bck_proj_image_sptr)[min_z].get_max_index(); + int min_x = (*bck_proj_image_sptr)[min_z][min_y].get_min_index(); + int max_x = (*bck_proj_image_sptr)[min_z][min_y].get_max_index(); + + // get two planes in the image that are equidistant from the z center + int centre_z = (max_z - min_z) / 2; + int plane_idA = centre_z - 5; + int plane_idB = centre_z + 5; + + for (int y = min_y; y < max_y; y++) + for (int x = min_x; x < max_x; x++) + { + check_if_equal((*bck_proj_image_sptr)[plane_idA][y][x], (*bck_proj_image_sptr)[plane_idB][y][x], + "checking the symmetry along the axial direction"); + } } /*! The following is a test for symmetries: a simulated image is created with a plane at known angles, - * the forward projected sinogram should show the maximum value at the bin corresponding to the angle phi + * the forward projected sinogram should show the maximum value at the bin corresponding to the angle phi * equal to the orientation of the plane -*/ + */ void -BlocksTests::run_plane_symmetry_test(){ +BlocksTests::run_plane_symmetry_test() +{ //-- ExamInfo auto exam_info_sptr = std::make_shared(); exam_info_sptr->imaging_modality = ImagingModality::PT; - - CartesianCoordinate3D origin (0,0,0); - CartesianCoordinate3D grid_spacing (1.1,2.2,2.2); - float phi1; - float phi2; - const IndexRange<3> - range(Coordinate3D(0,-45,-44), - Coordinate3D(24,44,45)); - VoxelsOnCartesianGrid image(exam_info_sptr, range, origin, grid_spacing); - -// 60 degrees - phi1= 60*_PI/180; - const Array<2,float> direction_vectors= - make_array(make_1d_array(1.F,0.F,0.F), - make_1d_array(0.F,cos(float(_PI)-phi1),sin(float(_PI)-phi1)), - make_1d_array(0.F,-sin(float(_PI)-phi1),cos(float(_PI)-phi1))); - - Ellipsoid - plane(CartesianCoordinate3D(/*edge_z*/25*grid_spacing.z(), - /*edge_y*/91*grid_spacing.y(), - /*edge_x*/5*grid_spacing.x()), - /*centre*/CartesianCoordinate3D((image.get_min_index()+image.get_max_index())/2*grid_spacing.z(), - 0*grid_spacing.y(), - 0), - direction_vectors); - - plane.construct_volume(image, make_coordinate(3,3,3)); - - -// rotate by 30 degrees - phi2=30*_PI/180; - VoxelsOnCartesianGrid image2=*image.get_empty_copy(); - const Array<2,float> direction2=make_array(make_1d_array(1.F,0.F,0.F), - make_1d_array(0.F,cos(float(_PI)-phi2),sin(float(_PI)-phi2)), - make_1d_array(0.F,-sin(float(_PI)-phi2),cos(float(_PI)-phi2))); - - Ellipsoid - plane2(CartesianCoordinate3D(/*edge_z*/25*grid_spacing.z(), - /*edge_y*/91*grid_spacing.y(), - /*edge_x*/5*grid_spacing.x()), - /*centre*/CartesianCoordinate3D((image.get_min_index()+image.get_max_index())/2*grid_spacing.z(), - 0*grid_spacing.y(), - 0), - direction2); -// plane.set_direction_vectors(direction2); - - plane2.construct_volume(image2, make_coordinate(3,3,3)); - -// create projadata info - - auto scannerBlocks_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); - scannerBlocks_ptr->set_num_axial_crystals_per_block(1); - scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing()* - scannerBlocks_ptr->get_num_axial_crystals_per_block()); - scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing()* - scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); -// scannerBlocks_ptr->set_num_transaxial_crystals_per_block(1); - scannerBlocks_ptr->set_num_axial_blocks_per_bucket(2); -// scannerBlocks_ptr->set_num_transaxial_blocks_per_bucket(1); - scannerBlocks_ptr->set_num_rings(2); - - scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); - scannerBlocks_ptr->set_up(); - - VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); - VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - auto proj_data_info_blocks_ptr=std::make_shared(); - proj_data_info_blocks_ptr=set_blocks_projdata_info(num_axial_pos_per_segment, - min_ring_diff_v, - max_ring_diff_v, - scannerBlocks_ptr); - - // now forward-project image - - shared_ptr > image_sptr(image.clone()); - write_to_file("plane60",*image_sptr); - - shared_ptr > image2_sptr(image2.clone()); - write_to_file("plane30",*image2_sptr); - - auto PM=std::make_shared(); - PM->enable_cache(false); - auto forw_projector_sptr=std::make_shared(PM); - info(boost::format("Test blocks on Cylindrical: Forward projector used: %1%") % forw_projector_sptr->parameter_info()); - - forw_projector_sptr->set_up(proj_data_info_blocks_ptr, - image_sptr); - - auto forw_projector2_sptr=std::make_shared(PM); - forw_projector2_sptr->set_up(proj_data_info_blocks_ptr, - image2_sptr); - - auto projdata=std::make_shared(exam_info_sptr, - proj_data_info_blocks_ptr, - "sino1_from_plane.hs", - std::ios::out | std::ios::trunc | std::ios::in); - - forw_projector_sptr->forward_project(*projdata, *image_sptr); - - auto projdata2=std::make_shared(exam_info_sptr, - proj_data_info_blocks_ptr, - "sino2_from_plane.hs", - std::ios::out | std::ios::trunc | std::ios::in); - - - forw_projector2_sptr->forward_project(*projdata2, *image2_sptr); - - int view1_num = 0, view2_num = 0; - LORInAxialAndNoArcCorrSinogramCoordinates lorB1; - for(int i=0;iget_max_view_num();i++){ - Bin bin(0,i,0,0); - proj_data_info_blocks_ptr->get_LOR(lorB1,bin); - if(abs(lorB1.phi()-phi1)/phi1<=1E-2){ - view1_num=i; - break; + + CartesianCoordinate3D origin(0, 0, 0); + CartesianCoordinate3D grid_spacing(1.1, 2.2, 2.2); + float phi1; + float phi2; + const IndexRange<3> range(Coordinate3D(0, -45, -44), Coordinate3D(24, 44, 45)); + VoxelsOnCartesianGrid image(exam_info_sptr, range, origin, grid_spacing); + + // 60 degrees + phi1 = 60 * _PI / 180; + const Array<2, float> direction_vectors + = make_array(make_1d_array(1.F, 0.F, 0.F), make_1d_array(0.F, cos(float(_PI) - phi1), sin(float(_PI) - phi1)), + make_1d_array(0.F, -sin(float(_PI) - phi1), cos(float(_PI) - phi1))); + + Ellipsoid plane(CartesianCoordinate3D(/*edge_z*/ 25 * grid_spacing.z(), + /*edge_y*/ 91 * grid_spacing.y(), + /*edge_x*/ 5 * grid_spacing.x()), + /*centre*/ + CartesianCoordinate3D((image.get_min_index() + image.get_max_index()) / 2 * grid_spacing.z(), + 0 * grid_spacing.y(), 0), + direction_vectors); + + plane.construct_volume(image, make_coordinate(3, 3, 3)); + + // rotate by 30 degrees + phi2 = 30 * _PI / 180; + VoxelsOnCartesianGrid image2 = *image.get_empty_copy(); + const Array<2, float> direction2 + = make_array(make_1d_array(1.F, 0.F, 0.F), make_1d_array(0.F, cos(float(_PI) - phi2), sin(float(_PI) - phi2)), + make_1d_array(0.F, -sin(float(_PI) - phi2), cos(float(_PI) - phi2))); + + Ellipsoid plane2(CartesianCoordinate3D(/*edge_z*/ 25 * grid_spacing.z(), + /*edge_y*/ 91 * grid_spacing.y(), + /*edge_x*/ 5 * grid_spacing.x()), + /*centre*/ + CartesianCoordinate3D((image.get_min_index() + image.get_max_index()) / 2 * grid_spacing.z(), + 0 * grid_spacing.y(), 0), + direction2); + // plane.set_direction_vectors(direction2); + + plane2.construct_volume(image2, make_coordinate(3, 3, 3)); + + // create projadata info + + auto scannerBlocks_sptr = std::make_shared(Scanner::SAFIRDualRingPrototype); + scannerBlocks_sptr->set_num_axial_crystals_per_block(1); + scannerBlocks_sptr->set_axial_block_spacing(scannerBlocks_sptr->get_axial_crystal_spacing() + * scannerBlocks_sptr->get_num_axial_crystals_per_block()); + scannerBlocks_sptr->set_transaxial_block_spacing(scannerBlocks_sptr->get_transaxial_crystal_spacing() + * scannerBlocks_sptr->get_num_transaxial_crystals_per_block()); + // scannerBlocks_sptr->set_num_transaxial_crystals_per_block(1); + scannerBlocks_sptr->set_num_axial_blocks_per_bucket(2); + // scannerBlocks_sptr->set_num_transaxial_blocks_per_bucket(1); + scannerBlocks_sptr->set_num_rings(2); + + scannerBlocks_sptr->set_scanner_geometry("BlocksOnCylindrical"); + scannerBlocks_sptr->set_up(); + + auto proj_data_info_blocks_sptr = std::make_shared(); + proj_data_info_blocks_sptr = set_blocks_projdata_info(scannerBlocks_sptr); + + // now forward-project image + + shared_ptr> image_sptr(image.clone()); + write_to_file("plane60", *image_sptr); + + shared_ptr> image2_sptr(image2.clone()); + write_to_file("plane30", *image2_sptr); + + auto PM = std::make_shared(); + PM->enable_cache(false); + auto forw_projector_sptr = std::make_shared(PM); + info(boost::format("Test blocks on Cylindrical: Forward projector used: %1%") % forw_projector_sptr->parameter_info()); + + forw_projector_sptr->set_up(proj_data_info_blocks_sptr, image_sptr); + + auto forw_projector2_sptr = std::make_shared(PM); + forw_projector2_sptr->set_up(proj_data_info_blocks_sptr, image2_sptr); + + auto projdata = std::make_shared(exam_info_sptr, proj_data_info_blocks_sptr, "sino1_from_plane.hs", + std::ios::out | std::ios::trunc | std::ios::in); + + forw_projector_sptr->forward_project(*projdata, *image_sptr); + + auto projdata2 = std::make_shared(exam_info_sptr, proj_data_info_blocks_sptr, "sino2_from_plane.hs", + std::ios::out | std::ios::trunc | std::ios::in); + + forw_projector2_sptr->forward_project(*projdata2, *image2_sptr); + + int view1_num = 0, view2_num = 0; + LORInAxialAndNoArcCorrSinogramCoordinates lorB1; + for (int i = 0; i < projdata->get_max_view_num(); i++) + { + Bin bin(0, i, 0, 0); + proj_data_info_blocks_sptr->get_LOR(lorB1, bin); + if (abs(lorB1.phi() - phi1) / phi1 <= 1E-2) + { + view1_num = i; + break; } } - - LORInAxialAndNoArcCorrSinogramCoordinates lorB2; - for(int i=0;iget_max_view_num();i++){ - Bin bin(0,i,0,0); - proj_data_info_blocks_ptr->get_LOR(lorB2,bin); - if(abs(lorB2.phi()-phi2)/phi2<=1E-2){ - view2_num=i; - break; + + LORInAxialAndNoArcCorrSinogramCoordinates lorB2; + for (int i = 0; i < projdata2->get_max_view_num(); i++) + { + Bin bin(0, i, 0, 0); + proj_data_info_blocks_sptr->get_LOR(lorB2, bin); + if (abs(lorB2.phi() - phi2) / phi2 <= 1E-2) + { + view2_num = i; + break; } } - - float max1 =projdata->get_sinogram(0,0).find_max(); - float max2 =projdata2->get_sinogram(0,0).find_max(); - -// find the tang position with the max value - int tang1_num=0,tang2_num=0; - for(int tang=projdata->get_min_tangential_pos_num();tangget_max_tangential_pos_num();tang++){ - - if((max1-projdata->get_sinogram(0,0).at(view1_num).at(tang))/max1<1E-3) { - tang1_num=tang; - break; - } + + float max1 = projdata->get_sinogram(0, 0).find_max(); + float max2 = projdata2->get_sinogram(0, 0).find_max(); + + // find the tang position with the max value + int tang1_num = 0, tang2_num = 0; + for (int tang = projdata->get_min_tangential_pos_num(); tang < projdata->get_max_tangential_pos_num(); tang++) + { + + if ((max1 - projdata->get_sinogram(0, 0).at(view1_num).at(tang)) / max1 < 1E-3) + { + tang1_num = tang; + break; } + } + + for (int tang = projdata2->get_min_tangential_pos_num(); tang < projdata2->get_max_tangential_pos_num(); tang++) + { - for(int tang=projdata2->get_min_tangential_pos_num();tangget_max_tangential_pos_num();tang++){ - - if((max2-projdata2->get_sinogram(0,0).at(view2_num).at(tang))/max2<1E-3) { - tang2_num=tang; - break; - } + if ((max2 - projdata2->get_sinogram(0, 0).at(view2_num).at(tang)) / max2 < 1E-3) + { + tang2_num = tang; + break; } + } - float bin1=projdata->get_sinogram(0,0).at(view1_num).at(tang1_num); - float bin2=projdata2->get_sinogram(0,0).at(view2_num).at(tang2_num); - set_tolerance(10E-2); - check_if_equal(bin1, max1,"the value seen in the block at 60 degrees should be the same as the max value of the sinogram"); - check_if_equal(bin2, max2,"the value seen in the block at 30 degrees should be the same as the max value of the sinogram"); + float bin1 = projdata->get_sinogram(0, 0).at(view1_num).at(tang1_num); + float bin2 = projdata2->get_sinogram(0, 0).at(view2_num).at(tang2_num); + set_tolerance(10E-2); + check_if_equal(bin1, max1, "the value seen in the block at 60 degrees should be the same as the max value of the sinogram"); + check_if_equal(bin2, max2, "the value seen in the block at 30 degrees should be the same as the max value of the sinogram"); } /*! The following is a test for symmetries: a simulated image is created with spherical source in front of each detector block, - * the forward projected sinogram should show the same bin values in symmetric places (in this test a dodecagon scanner is - * used so we have symmetry every 30 degrees. The above is repeated for an image with sources in front of the dodecagon corners. - * The sinogram should have now different values at fixed bin compared the previous image. In this test we are also testing a + * the forward projected sinogram should show the same bin values in symmetric places (in this test a dodecagon scanner is + * used so we have symmetry every 30 degrees. The above is repeated for an image with sources in front of the dodecagon corners. + * The sinogram should have now different values at fixed bin compared the previous image. In this test we are also testing a * projdata_info with a negative view offset. -*/ + */ void -BlocksTests::run_symmetry_test(){ - - //-- ExamInfo +BlocksTests::run_symmetry_test() +{ + + //-- ExamInfo auto exam_info_sptr = std::make_shared(); exam_info_sptr->imaging_modality = ImagingModality::PT; - CartesianCoordinate3D origin(0, 0, 0); - CartesianCoordinate3D grid_spacing (1.1,2.2,2.2); - float theta1=0;float theta2=0; - - const IndexRange<3> - range(Coordinate3D(0,-45,-44), - Coordinate3D(24,44,45)); - VoxelsOnCartesianGrid image(exam_info_sptr, range,origin, grid_spacing); - - const Array<2,float> direction_vectors= - make_array(make_1d_array(1.F,0.F,0.F), - make_1d_array(0.F,cos(theta1),sin(theta1)), - make_1d_array(0.F,-sin(theta1),cos(theta1))); - - Ellipsoid - ellipsoid(CartesianCoordinate3D(/*radius_z*/6*grid_spacing.z(), - /*radius_y*/6*grid_spacing.y(), - /*radius_x*/6*grid_spacing.x()), - /*centre*/CartesianCoordinate3D((image.get_min_index()+image.get_max_index())/2*grid_spacing.z(), - -34*grid_spacing.y(), - 0), - direction_vectors); - - ellipsoid.construct_volume(image, make_coordinate(3,3,3)); - - VoxelsOnCartesianGrid image1=image; - VoxelsOnCartesianGrid image22=image; -// rotate by 30 degrees, this scanner is a dodecagon and there is a 30 degrees angle between consecutive blocks - for(int i=30; i<360; i+=30){ - theta1=i*_PI/180; - - CartesianCoordinate3D origin1 ((image.get_min_index()+image.get_max_index())/2*grid_spacing.z(), - -34*grid_spacing.y()*cos(theta1), - 34*grid_spacing.y()*sin(theta1)); - - ellipsoid.set_origin(origin1); - ellipsoid.construct_volume(image1, make_coordinate(3,3,3)); - image+=image1; -} - shared_ptr > image1_sptr(image.clone()); - write_to_file("image_for",*image1_sptr); - - - image=*image.get_empty_copy(); - for(int i=15; i<360; i+=30){ - theta2=i*_PI/180; - - CartesianCoordinate3D origin2 ((image.get_min_index()+image.get_max_index())/2*grid_spacing.z(), - -34*grid_spacing.y()*cos(theta2), - 34*grid_spacing.y()*sin(theta2)); - - ellipsoid.set_origin(origin2); - ellipsoid.construct_volume(image22, make_coordinate(3,3,3)); - image+=image22; -} + CartesianCoordinate3D origin(0, 0, 0); + CartesianCoordinate3D grid_spacing(1.1, 2.2, 2.2); + float theta1 = 0; + float theta2 = 0; + + const IndexRange<3> range(Coordinate3D(0, -45, -44), Coordinate3D(24, 44, 45)); + VoxelsOnCartesianGrid image(exam_info_sptr, range, origin, grid_spacing); + + const Array<2, float> direction_vectors = make_array(make_1d_array(1.F, 0.F, 0.F), make_1d_array(0.F, cos(theta1), sin(theta1)), + make_1d_array(0.F, -sin(theta1), cos(theta1))); + + Ellipsoid ellipsoid(CartesianCoordinate3D(/*radius_z*/ 6 * grid_spacing.z(), + /*radius_y*/ 6 * grid_spacing.y(), + /*radius_x*/ 6 * grid_spacing.x()), + /*centre*/ + CartesianCoordinate3D((image.get_min_index() + image.get_max_index()) / 2 * grid_spacing.z(), + -34 * grid_spacing.y(), 0), + direction_vectors); + + ellipsoid.construct_volume(image, make_coordinate(3, 3, 3)); -shared_ptr > image2_sptr(image.clone()); -write_to_file("image_for2",*image2_sptr); - - - -// create projadata info - auto scannerBlocks_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); - scannerBlocks_ptr->set_num_axial_crystals_per_block(1); - scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing()* - scannerBlocks_ptr->get_num_axial_crystals_per_block()); - scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing()* - scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); -// scannerBlocks_ptr->set_num_transaxial_crystals_per_block(1); - scannerBlocks_ptr->set_num_axial_blocks_per_bucket(2); -// scannerBlocks_ptr->set_num_transaxial_blocks_per_bucket(1); - scannerBlocks_ptr->set_num_rings(2); - scannerBlocks_ptr->set_intrinsic_azimuthal_tilt(-30); - scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); - scannerBlocks_ptr->set_up(); - - VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); - VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - auto proj_data_info_blocks_ptr=std::make_shared(); - proj_data_info_blocks_ptr=set_blocks_projdata_info(num_axial_pos_per_segment, - min_ring_diff_v, - max_ring_diff_v, - scannerBlocks_ptr); -// now forward-project images - - auto PM=std::make_shared(); - PM->enable_cache(false); - auto forw_projector1_sptr=std::make_shared(PM); - info(boost::format("Test blocks on Cylindrical: Forward projector used: %1%") % forw_projector1_sptr->parameter_info()); - - forw_projector1_sptr->set_up(proj_data_info_blocks_ptr, - image1_sptr); - - - auto forw_projector2_sptr=std::make_shared(PM); - forw_projector2_sptr->set_up(proj_data_info_blocks_ptr, - image2_sptr); - - auto projdata1=std::make_shared(exam_info_sptr, - proj_data_info_blocks_ptr, - "sino1_from_image.hs",std::ios::out | std::ios::trunc | std::ios::in); - - auto projdata2=std::make_shared(exam_info_sptr, - proj_data_info_blocks_ptr, - "sino2_from_image.hs",std::ios::out | std::ios::trunc | std::ios::in); - - forw_projector1_sptr->forward_project(*projdata1, *image1_sptr); - forw_projector2_sptr->forward_project(*projdata2, *image2_sptr); - int crystals_in_ring=scannerBlocks_ptr->get_num_detectors_per_ring(); - float bin1_0=projdata1->get_sinogram(0,0).at(0/crystals_in_ring*_PI).at(0); - float bin1_90=projdata1->get_sinogram(0,0).at(90/crystals_in_ring*_PI).at(0); - float bin1_30=projdata1->get_sinogram(0,0).at(30/crystals_in_ring*_PI).at(0); - float bin1_60=projdata1->get_sinogram(0,0).at(60/crystals_in_ring*_PI).at(0); - float bin1_150=projdata1->get_sinogram(0,0).at(150/crystals_in_ring*_PI).at(0); - -// values of the asymetric image - float bin2_0=projdata2->get_sinogram(0,0).at(0/crystals_in_ring*_PI).at(0); - float bin2_90=projdata2->get_sinogram(0,0).at(90/crystals_in_ring*_PI).at(0); - float bin2_30=projdata2->get_sinogram(0,0).at(30/crystals_in_ring*_PI).at(0); - float bin2_60=projdata2->get_sinogram(0,0).at(60/crystals_in_ring*_PI).at(0); - float bin2_150=projdata2->get_sinogram(0,0).at(150/crystals_in_ring*_PI).at(0); - - set_tolerance(10E-3); - check_if_equal(bin1_0, bin1_90,"the value seen in the block 0 should be the same as the one at angle 90"); - check_if_equal(bin1_30, bin1_150,"the value seen in the block at angle 30 should be the same as the one at angle 150 "); - check_if_equal(bin1_30, bin1_60,"the value seen in the block at angle 30 should be the same as the one at angle 60"); - - - check(bin1_0!= bin2_0,"the two data have different symmetries, the values should be different"); - check(bin1_30!= bin2_30,"the two data have different symmetries, the values should be different "); - check(bin1_60!= bin2_60,"the two data have different symmetries, the values should be different"); - check(bin1_90!= bin2_90,"the two data have different symmetries, the values should be different"); - check(bin1_30!= bin2_30,"the two data have different symmetries, the values should be different"); - check(bin1_150!= bin2_150,"the two data have different symmetries, the values should be different"); + VoxelsOnCartesianGrid image1 = image; + VoxelsOnCartesianGrid image22 = image; + // rotate by 30 degrees, this scanner is a dodecagon and there is a 30 degrees angle between consecutive blocks + for (int i = 30; i < 360; i += 30) + { + theta1 = i * _PI / 180; + + CartesianCoordinate3D origin1((image.get_min_index() + image.get_max_index()) / 2 * grid_spacing.z(), + -34 * grid_spacing.y() * cos(theta1), 34 * grid_spacing.y() * sin(theta1)); + + ellipsoid.set_origin(origin1); + ellipsoid.construct_volume(image1, make_coordinate(3, 3, 3)); + image += image1; + } + shared_ptr> image1_sptr(image.clone()); + write_to_file("image_for", *image1_sptr); + + image = *image.get_empty_copy(); + for (int i = 15; i < 360; i += 30) + { + theta2 = i * _PI / 180; + + CartesianCoordinate3D origin2((image.get_min_index() + image.get_max_index()) / 2 * grid_spacing.z(), + -34 * grid_spacing.y() * cos(theta2), 34 * grid_spacing.y() * sin(theta2)); + + ellipsoid.set_origin(origin2); + ellipsoid.construct_volume(image22, make_coordinate(3, 3, 3)); + image += image22; + } + + shared_ptr> image2_sptr(image.clone()); + write_to_file("image_for2", *image2_sptr); + + // create projadata info + auto scannerBlocks_sptr = std::make_shared(Scanner::SAFIRDualRingPrototype); + scannerBlocks_sptr->set_num_axial_crystals_per_block(1); + scannerBlocks_sptr->set_axial_block_spacing(scannerBlocks_sptr->get_axial_crystal_spacing() + * scannerBlocks_sptr->get_num_axial_crystals_per_block()); + scannerBlocks_sptr->set_transaxial_block_spacing(scannerBlocks_sptr->get_transaxial_crystal_spacing() + * scannerBlocks_sptr->get_num_transaxial_crystals_per_block()); + // scannerBlocks_sptr->set_num_transaxial_crystals_per_block(1); + scannerBlocks_sptr->set_num_axial_blocks_per_bucket(2); + // scannerBlocks_sptr->set_num_transaxial_blocks_per_bucket(1); + scannerBlocks_sptr->set_num_rings(2); + scannerBlocks_sptr->set_intrinsic_azimuthal_tilt(-30); + scannerBlocks_sptr->set_scanner_geometry("BlocksOnCylindrical"); + scannerBlocks_sptr->set_up(); + + auto proj_data_info_blocks_sptr = std::make_shared(); + proj_data_info_blocks_sptr = set_blocks_projdata_info(scannerBlocks_sptr); + // now forward-project images + + auto PM = std::make_shared(); + PM->enable_cache(false); + auto forw_projector1_sptr = std::make_shared(PM); + info(boost::format("Test blocks on Cylindrical: Forward projector used: %1%") % forw_projector1_sptr->parameter_info()); + + forw_projector1_sptr->set_up(proj_data_info_blocks_sptr, image1_sptr); + + auto forw_projector2_sptr = std::make_shared(PM); + forw_projector2_sptr->set_up(proj_data_info_blocks_sptr, image2_sptr); + + auto projdata1 = std::make_shared(exam_info_sptr, proj_data_info_blocks_sptr, "sino1_from_image.hs", + std::ios::out | std::ios::trunc | std::ios::in); + + auto projdata2 = std::make_shared(exam_info_sptr, proj_data_info_blocks_sptr, "sino2_from_image.hs", + std::ios::out | std::ios::trunc | std::ios::in); + + forw_projector1_sptr->forward_project(*projdata1, *image1_sptr); + forw_projector2_sptr->forward_project(*projdata2, *image2_sptr); + int crystals_in_ring = scannerBlocks_sptr->get_num_detectors_per_ring(); + float bin1_0 = projdata1->get_sinogram(0, 0).at(0 / crystals_in_ring * _PI).at(0); + float bin1_90 = projdata1->get_sinogram(0, 0).at(90 / crystals_in_ring * _PI).at(0); + float bin1_30 = projdata1->get_sinogram(0, 0).at(30 / crystals_in_ring * _PI).at(0); + float bin1_60 = projdata1->get_sinogram(0, 0).at(60 / crystals_in_ring * _PI).at(0); + float bin1_150 = projdata1->get_sinogram(0, 0).at(150 / crystals_in_ring * _PI).at(0); + + // values of the asymetric image + float bin2_0 = projdata2->get_sinogram(0, 0).at(0 / crystals_in_ring * _PI).at(0); + float bin2_90 = projdata2->get_sinogram(0, 0).at(90 / crystals_in_ring * _PI).at(0); + float bin2_30 = projdata2->get_sinogram(0, 0).at(30 / crystals_in_ring * _PI).at(0); + float bin2_60 = projdata2->get_sinogram(0, 0).at(60 / crystals_in_ring * _PI).at(0); + float bin2_150 = projdata2->get_sinogram(0, 0).at(150 / crystals_in_ring * _PI).at(0); + + set_tolerance(10E-3); + check_if_equal(bin1_0, bin1_90, "the value seen in the block 0 should be the same as the one at angle 90"); + check_if_equal(bin1_30, bin1_150, "the value seen in the block at angle 30 should be the same as the one at angle 150 "); + check_if_equal(bin1_30, bin1_60, "the value seen in the block at angle 30 should be the same as the one at angle 60"); + + check(bin1_0 != bin2_0, "the two data have different symmetries, the values should be different"); + check(bin1_30 != bin2_30, "the two data have different symmetries, the values should be different "); + check(bin1_60 != bin2_60, "the two data have different symmetries, the values should be different"); + check(bin1_90 != bin2_90, "the two data have different symmetries, the values should be different"); + check(bin1_30 != bin2_30, "the two data have different symmetries, the values should be different"); + check(bin1_150 != bin2_150, "the two data have different symmetries, the values should be different"); } -/*!The following is a test for the crystal maps. Two scanners and ProjDataInfo are created, one with the standard map orientation +/*!The following is a test for the crystal maps. Two scanners and ProjDataInfo are created, one with the standard map orientation * and the other with an orientation along the view which is opposite to the first one. A simulated sphere was forward projected - * to look at bin values in the two cases. The bin obtained from the two different projdata will have different coordinates but + * to look at bin values in the two cases. The bin obtained from the two different projdata will have different coordinates but * the same value. -*/ + */ void BlocksTests::run_map_orientation_test() { - CPUTimer timer; + CPUTimer timer; //-- ExamInfo auto exam_info_sptr = std::make_shared(); exam_info_sptr->imaging_modality = ImagingModality::PT; - CartesianCoordinate3D origin (0,0,0); - CartesianCoordinate3D grid_spacing (1.1,2.2,2.2); - float theta1=0; - - const IndexRange<3> range(Coordinate3D(0,-45,-44), - Coordinate3D(24,44,45)); - VoxelsOnCartesianGrid image(exam_info_sptr, range, origin, grid_spacing); - - const Array<2,float> direction_vectors=make_array(make_1d_array(1.F,0.F,0.F), - make_1d_array(0.F,cos(theta1),sin(theta1)), - make_1d_array(0.F,-sin(theta1),cos(theta1))); - - Ellipsoid ellipsoid(CartesianCoordinate3D(/*radius_z*/6*grid_spacing.z(), - /*radius_y*/6*grid_spacing.y(), - /*radius_x*/6*grid_spacing.x()), - /*centre*/CartesianCoordinate3D((image.get_min_index()+image.get_max_index())/2*grid_spacing.z(), - -34*grid_spacing.y(), - 0), - direction_vectors); - - ellipsoid.construct_volume(image, make_coordinate(3,3,3)); - - VoxelsOnCartesianGrid image1=image; - VoxelsOnCartesianGrid image22=image; -// rotate by 30 degrees - for(int i=30; i<90; i+=30){ - theta1=i*_PI/180; - - CartesianCoordinate3D origin1 ((image.get_min_index()+image.get_max_index())/2*grid_spacing.z(), - -34*grid_spacing.y()*cos(theta1), - 34*grid_spacing.y()*sin(theta1)); - - ellipsoid.set_origin(origin1); - ellipsoid.construct_volume(image1, make_coordinate(3,3,3)); - image+=image1; -} - shared_ptr > image1_sptr(image.clone()); - write_to_file("image_to_fwp",*image1_sptr); - - - image=*image.get_empty_copy(); - - shared_ptr map_ptr; - auto scannerBlocks_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); - scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); - scannerBlocks_ptr->set_num_transaxial_blocks_per_bucket(1); - scannerBlocks_ptr->set_up(); - - VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); - VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - auto proj_data_info_blocks_ptr=std::make_shared(); - proj_data_info_blocks_ptr=set_blocks_projdata_info(num_axial_pos_per_segment, - min_ring_diff_v, - max_ring_diff_v, - scannerBlocks_ptr); - Bin bin, bin1,bin2, binR1; - CartesianCoordinate3D< float> b1,b2,rb1,rb2; - DetectionPosition<> det_pos, det_pos_ord; - DetectionPositionPair<> dp1, dp2, dpR1; - CartesianCoordinate3D coord_ord; - map_ptr=scannerBlocks_ptr->get_detector_map_sptr(); - int rad_size=map_ptr->get_num_radial_coords(); - int ax_size=map_ptr->get_num_axial_coords(); - int tang_size=map_ptr->get_num_tangential_coords(); - - DetectorCoordinateMap::det_pos_to_coord_type coord_map_reordered; - -// reorder the tangential positions - for (int rad=0;radget_coordinate_for_det_pos(det_pos_ord); - coord_map_reordered[det_pos]=coord_ord; - } - - auto scannerBlocks_reord_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); - scannerBlocks_reord_ptr->set_scanner_geometry("Generic"); -// scannerBlocks_reord_ptr->set_num_transaxial_blocks_per_bucket(1); - scannerBlocks_reord_ptr->set_detector_map(coord_map_reordered); - scannerBlocks_reord_ptr->set_up(); - - auto proj_data_info_blocks_reord_ptr= std::make_shared( - scannerBlocks_reord_ptr, - num_axial_pos_per_segment, - min_ring_diff_v, max_ring_diff_v, - scannerBlocks_reord_ptr->get_max_num_views(), - scannerBlocks_reord_ptr->get_max_num_non_arccorrected_bins()); - timer.reset(); timer.start(); - - - // now forward-project images - - auto PM= std::make_shared(); - PM->enable_cache(false); - auto forw_projector1_sptr= std::make_shared(PM); - info(boost::format("Test blocks on Cylindrical: Forward projector used: %1%") % forw_projector1_sptr->parameter_info()); - forw_projector1_sptr->set_up(proj_data_info_blocks_ptr, - image1_sptr); - - auto forw_projector2_sptr= std::make_shared(PM); - forw_projector2_sptr->set_up(proj_data_info_blocks_reord_ptr, - image1_sptr); - - auto projdata1= std::make_shared(exam_info_sptr, - proj_data_info_blocks_ptr);//, -// "sino1_map.hs",std::ios::out | std::ios::trunc | std::ios::in)); - - auto projdata2= std::make_shared(exam_info_sptr, - proj_data_info_blocks_reord_ptr);//, -// "sino2_map.hs",std::ios::out | std::ios::trunc | std::ios::in)); - - forw_projector1_sptr->forward_project(*projdata1, *image1_sptr); - forw_projector2_sptr->forward_project(*projdata2, *image1_sptr); - - - for (int view = 0; view <= proj_data_info_blocks_reord_ptr->get_max_view_num(); view++) + CartesianCoordinate3D origin(0, 0, 0); + CartesianCoordinate3D grid_spacing(1.1, 2.2, 2.2); + float theta1 = 0; + + const IndexRange<3> range(Coordinate3D(0, -45, -44), Coordinate3D(24, 44, 45)); + VoxelsOnCartesianGrid image(exam_info_sptr, range, origin, grid_spacing); + + const Array<2, float> direction_vectors = make_array(make_1d_array(1.F, 0.F, 0.F), make_1d_array(0.F, cos(theta1), sin(theta1)), + make_1d_array(0.F, -sin(theta1), cos(theta1))); + + Ellipsoid ellipsoid(CartesianCoordinate3D(/*radius_z*/ 6 * grid_spacing.z(), + /*radius_y*/ 6 * grid_spacing.y(), + /*radius_x*/ 6 * grid_spacing.x()), + /*centre*/ + CartesianCoordinate3D((image.get_min_index() + image.get_max_index()) / 2 * grid_spacing.z(), + -34 * grid_spacing.y(), 0), + direction_vectors); + + ellipsoid.construct_volume(image, make_coordinate(3, 3, 3)); + + VoxelsOnCartesianGrid image1 = image; + VoxelsOnCartesianGrid image22 = image; + // rotate by 30 degrees + for (int i = 30; i < 90; i += 30) { - - bin.segment_num() = 0; - bin.axial_pos_num() = 0; - bin.view_num() = view; - bin.tangential_pos_num() = 0; - - proj_data_info_blocks_ptr->get_det_pos_pair_for_bin(dp1,bin); - proj_data_info_blocks_reord_ptr->get_det_pos_pair_for_bin(dp2,bin); - - proj_data_info_blocks_ptr->get_bin_for_det_pos_pair(bin1,dp1); - proj_data_info_blocks_reord_ptr->get_bin_for_det_pos_pair(bin2,dp2); - -// // check cartesian coordinates of detectors - proj_data_info_blocks_ptr->find_cartesian_coordinates_of_detection(b1,b2,bin1); - proj_data_info_blocks_reord_ptr->find_cartesian_coordinates_of_detection(rb1,rb2,bin1); - -// now get det_pos from the reordered coord ir shouls be different from the one obtained for bin and bin1 - proj_data_info_blocks_ptr->find_bin_given_cartesian_coordinates_of_detection(binR1,rb1, rb2); - proj_data_info_blocks_ptr->get_det_pos_pair_for_bin(dpR1,binR1); - - check_if_equal(projdata1->get_bin_value(bin1),projdata2->get_bin_value(bin2), " checking cartesian coordinate y1 are the same on a flat bucket"); - check(b1!=rb1, " checking cartesian coordinate of detector 1 are different if we use a reordered map"); - check(b2!=rb2, " checking cartesian coordinate of detector 2 are different if we use a reordered map"); - check(dp1.pos1().tangential_coord()!=dpR1.pos1().tangential_coord(), " checking det_pos.tangential is different if we use a reordered map"); + theta1 = i * _PI / 180; + + CartesianCoordinate3D origin1((image.get_min_index() + image.get_max_index()) / 2 * grid_spacing.z(), + -34 * grid_spacing.y() * cos(theta1), 34 * grid_spacing.y() * sin(theta1)); + + ellipsoid.set_origin(origin1); + ellipsoid.construct_volume(image1, make_coordinate(3, 3, 3)); + image += image1; } - timer.stop(); std::cerr<< "-- CPU Time " << timer.value() << '\n'; - -} + shared_ptr> image1_sptr(image.clone()); + write_to_file("image_to_fwp", *image1_sptr); + + image = *image.get_empty_copy(); + + shared_ptr map_sptr; + auto scannerBlocks_sptr = std::make_shared(Scanner::SAFIRDualRingPrototype); + scannerBlocks_sptr->set_scanner_geometry("BlocksOnCylindrical"); + scannerBlocks_sptr->set_num_transaxial_blocks_per_bucket(1); + scannerBlocks_sptr->set_up(); + + VectorWithOffset num_axial_pos_per_segment(scannerBlocks_sptr->get_num_rings() * 2 - 1); + VectorWithOffset min_ring_diff_v(scannerBlocks_sptr->get_num_rings() * 2 - 1); + VectorWithOffset max_ring_diff_v(scannerBlocks_sptr->get_num_rings() * 2 - 1); + auto proj_data_info_blocks_sptr = std::make_shared(); + proj_data_info_blocks_sptr = set_blocks_projdata_info(scannerBlocks_sptr); + Bin bin, bin1, bin2, binR1; + CartesianCoordinate3D b1, b2, rb1, rb2; + DetectionPosition<> det_pos, det_pos_ord; + DetectionPositionPair<> dp1, dp2, dpR1; + CartesianCoordinate3D coord_ord; + map_sptr = scannerBlocks_sptr->get_detector_map_sptr(); + int rad_size = map_sptr->get_num_radial_coords(); + int ax_size = map_sptr->get_num_axial_coords(); + int tang_size = map_sptr->get_num_tangential_coords(); + + DetectorCoordinateMap::det_pos_to_coord_type coord_map_reordered; + + // reorder the tangential positions + for (int rad = 0; rad < rad_size; rad++) + for (int ax = 0; ax < ax_size; ax++) + for (int tang = 0; tang < tang_size; tang++) + { + det_pos.radial_coord() = rad; + det_pos.axial_coord() = ax; + det_pos.tangential_coord() = tang; + det_pos_ord.radial_coord() = rad; + det_pos_ord.axial_coord() = ax; + det_pos_ord.tangential_coord() = tang_size - 1 - tang; + + coord_ord = map_sptr->get_coordinate_for_det_pos(det_pos_ord); + coord_map_reordered[det_pos] = coord_ord; + } + + auto scannerBlocks_reord_sptr = std::make_shared(Scanner::SAFIRDualRingPrototype); + scannerBlocks_reord_sptr->set_scanner_geometry("Generic"); + // scannerBlocks_reord_sptr->set_num_transaxial_blocks_per_bucket(1); + scannerBlocks_reord_sptr->set_detector_map(coord_map_reordered); + scannerBlocks_reord_sptr->set_up(); + + auto proj_data_info_blocks_reord_sptr = std::make_shared(); + proj_data_info_blocks_reord_sptr = set_blocks_projdata_info(scannerBlocks_reord_sptr); + timer.reset(); + timer.start(); + // now forward-project images + + auto PM = std::make_shared(); + PM->enable_cache(false); + auto forw_projector1_sptr = std::make_shared(PM); + info(boost::format("Test blocks on Cylindrical: Forward projector used: %1%") % forw_projector1_sptr->parameter_info()); + forw_projector1_sptr->set_up(proj_data_info_blocks_sptr, image1_sptr); + + auto forw_projector2_sptr = std::make_shared(PM); + forw_projector2_sptr->set_up(proj_data_info_blocks_reord_sptr, image1_sptr); + + auto projdata1 = std::make_shared(exam_info_sptr, + proj_data_info_blocks_sptr); //, + // "sino1_map.hs",std::ios::out | std::ios::trunc | std::ios::in)); + + auto projdata2 = std::make_shared(exam_info_sptr, + proj_data_info_blocks_reord_sptr); //, + // "sino2_map.hs",std::ios::out | std::ios::trunc | std::ios::in)); + + forw_projector1_sptr->forward_project(*projdata1, *image1_sptr); + forw_projector2_sptr->forward_project(*projdata2, *image1_sptr); + + for (int view = 0; view <= proj_data_info_blocks_reord_sptr->get_max_view_num(); view++) + { + + bin.segment_num() = 0; + bin.axial_pos_num() = 0; + bin.view_num() = view; + bin.tangential_pos_num() = 0; + + proj_data_info_blocks_sptr->get_det_pos_pair_for_bin(dp1, bin); + proj_data_info_blocks_reord_sptr->get_det_pos_pair_for_bin(dp2, bin); + + proj_data_info_blocks_sptr->get_bin_for_det_pos_pair(bin1, dp1); + proj_data_info_blocks_reord_sptr->get_bin_for_det_pos_pair(bin2, dp2); + + // // check cartesian coordinates of detectors + proj_data_info_blocks_sptr->find_cartesian_coordinates_of_detection(b1, b2, bin1); + proj_data_info_blocks_reord_sptr->find_cartesian_coordinates_of_detection(rb1, rb2, bin1); + + // now get det_pos from the reordered coord ir shouls be different from the one obtained for bin and bin1 + proj_data_info_blocks_sptr->find_bin_given_cartesian_coordinates_of_detection(binR1, rb1, rb2); + proj_data_info_blocks_sptr->get_det_pos_pair_for_bin(dpR1, binR1); + + check_if_equal(projdata1->get_bin_value(bin1), projdata2->get_bin_value(bin2), + " checking cartesian coordinate y1 are the same on a flat bucket"); + check(b1 != rb1, " checking cartesian coordinate of detector 1 are different if we use a reordered map"); + check(b2 != rb2, " checking cartesian coordinate of detector 2 are different if we use a reordered map"); + check(dp1.pos1().tangential_coord() != dpR1.pos1().tangential_coord(), + " checking det_pos.tangential is different if we use a reordered map"); + } + timer.stop(); + std::cerr << "-- CPU Time " << timer.value() << '\n'; +} void -BlocksTests:: -run_tests() +BlocksTests::run_tests() { - - std::cerr << "-------- Testing Blocks Geometry --------\n"; - run_voxelOnCartesianGrid_with_negative_offset(); - run_axial_projection_test(); - run_map_orientation_test(); - run_symmetry_test(); - run_plane_symmetry_test(); + + std::cerr << "-------- Testing Blocks Geometry --------\n"; + run_voxelOnCartesianGrid_with_negative_offset(); + run_axial_projection_test(); + run_map_orientation_test(); + run_symmetry_test(); + run_plane_symmetry_test(); } END_NAMESPACE_STIR - USING_NAMESPACE_STIR -int main() +int +main() { Verbosity::set(1); BlocksTests tests; diff --git a/src/test/test_proj_data_info.cxx b/src/test/test_proj_data_info.cxx old mode 100755 new mode 100644 index b09339309..7ac8e335c --- a/src/test/test_proj_data_info.cxx +++ b/src/test/test_proj_data_info.cxx @@ -56,37 +56,38 @@ using std::size_t; START_NAMESPACE_STIR -static inline -int intabs(const int x) -{ return x>=0?x:-x; } +static inline int +intabs(const int x) +{ + return x >= 0 ? x : -x; +} // prints a michelogram to the screen #if 1 // TODO move somewhere else -void michelogram(const ProjDataInfoCylindrical& proj_data_info) +void +michelogram(const ProjDataInfoCylindrical& proj_data_info) { cerr << '{'; - for (int ring1=0; ring1get_num_rings(); ++ring1) - { - cerr << '{'; - for (int ring2=0; ring2get_num_rings(); ++ring2) + for (int ring1 = 0; ring1 < proj_data_info.get_scanner_ptr()->get_num_rings(); ++ring1) { - int segment_num=0; - int ax_pos_num = 0; - if (proj_data_info.get_segment_axial_pos_num_for_ring_pair(segment_num, ax_pos_num, ring1, ring2) - == Succeeded::yes) - cerr << '{' << setw(3) << segment_num << ',' << setw(2) << ax_pos_num << "}"; - else - cerr << "{} "; - if (ring2 != proj_data_info.get_scanner_ptr()->get_num_rings()-1) + cerr << '{'; + for (int ring2 = 0; ring2 < proj_data_info.get_scanner_ptr()->get_num_rings(); ++ring2) + { + int segment_num = 0; + int ax_pos_num = 0; + if (proj_data_info.get_segment_axial_pos_num_for_ring_pair(segment_num, ax_pos_num, ring1, ring2) == Succeeded::yes) + cerr << '{' << setw(3) << segment_num << ',' << setw(2) << ax_pos_num << "}"; + else + cerr << "{} "; + if (ring2 != proj_data_info.get_scanner_ptr()->get_num_rings() - 1) + cerr << ','; + } + cerr << '}'; + if (ring1 != proj_data_info.get_scanner_ptr()->get_num_rings() - 1) cerr << ','; - + cerr << endl; } - cerr << '}'; - if (ring1 != proj_data_info.get_scanner_ptr()->get_num_rings()-1) - cerr << ','; - cerr << endl; - } cerr << '}' << endl; } #endif @@ -95,16 +96,13 @@ void michelogram(const ProjDataInfoCylindrical& proj_data_info) \ingroup test \brief Test class for ProjDataInfo */ -class ProjDataInfoTests: public RunTests +class ProjDataInfoTests : public RunTests { -protected: +protected: void test_generic_proj_data_info(ProjDataInfo& proj_data_info); - - shared_ptr set_blocks_projdata_info(VectorWithOffset& num_axial_pos_per_segment, - VectorWithOffset& min_ring_diff_v, - VectorWithOffset& max_ring_diff_v, - shared_ptr scanner_ptr); - + + template + shared_ptr set_blocks_projdata_info(shared_ptr scanner_sptr); void run_coordinate_test(); void run_coordinate_test_for_realistic_scanner(); void run_Blocks_DOI_test(); @@ -112,234 +110,201 @@ class ProjDataInfoTests: public RunTests }; /*! The following is a function to allow a projdata_info blocksONCylindrical to be created from the scanner. -*/ -shared_ptr -ProjDataInfoTests:: -set_blocks_projdata_info(VectorWithOffset& num_axial_pos_per_segment, - VectorWithOffset& min_ring_diff_v, - VectorWithOffset& max_ring_diff_v, - shared_ptr scanner_ptr){ - - for (int i=0; i<2*scanner_ptr->get_num_rings()-1; i++){ - min_ring_diff_v[i]=-scanner_ptr->get_num_rings()+1+i; - max_ring_diff_v[i]=-scanner_ptr->get_num_rings()+1+i; - if (iget_num_rings()) - num_axial_pos_per_segment[i]=i+1; - else - num_axial_pos_per_segment[i]=2*scanner_ptr->get_num_rings()-i-1; - } - - auto proj_data_info_blocks_ptr=std::make_shared( - scanner_ptr, - num_axial_pos_per_segment, - min_ring_diff_v, max_ring_diff_v, - scanner_ptr->get_max_num_views(), - scanner_ptr->get_max_num_non_arccorrected_bins()); - - return proj_data_info_blocks_ptr; + */ +template +shared_ptr +ProjDataInfoTests::set_blocks_projdata_info(shared_ptr scanner_sptr) +{ + VectorWithOffset num_axial_pos_per_segment(scanner_sptr->get_num_rings() * 2 - 1); + VectorWithOffset min_ring_diff_v(scanner_sptr->get_num_rings() * 2 - 1); + VectorWithOffset max_ring_diff_v(scanner_sptr->get_num_rings() * 2 - 1); + for (int i = 0; i < 2 * scanner_sptr->get_num_rings() - 1; i++) + { + min_ring_diff_v[i] = -scanner_sptr->get_num_rings() + 1 + i; + max_ring_diff_v[i] = -scanner_sptr->get_num_rings() + 1 + i; + if (i < scanner_sptr->get_num_rings()) + num_axial_pos_per_segment[i] = i + 1; + else + num_axial_pos_per_segment[i] = 2 * scanner_sptr->get_num_rings() - i - 1; + } + + auto proj_data_info_blocks_sptr + = std::make_shared(scanner_sptr, num_axial_pos_per_segment, min_ring_diff_v, max_ring_diff_v, + scanner_sptr->get_max_num_views(), scanner_sptr->get_max_num_non_arccorrected_bins()); + + return proj_data_info_blocks_sptr; } void -ProjDataInfoTests:: -test_generic_proj_data_info(ProjDataInfo& proj_data_info) +ProjDataInfoTests::test_generic_proj_data_info(ProjDataInfo& proj_data_info) { cerr << "\tTests on get_LOR/get_bin\n"; - int max_diff_segment_num=0; - int max_diff_view_num=0; - int max_diff_axial_pos_num=0; - int max_diff_tangential_pos_num=0; + int max_diff_segment_num = 0; + int max_diff_view_num = 0; + int max_diff_axial_pos_num = 0; + int max_diff_tangential_pos_num = 0; #ifdef STIR_OPENMP -#pragma omp parallel for schedule(dynamic) +# pragma omp parallel for schedule(dynamic) #endif - for (int segment_num=proj_data_info.get_min_segment_num(); - segment_num<=proj_data_info.get_max_segment_num(); - ++segment_num) - { - for (int view_num=proj_data_info.get_min_view_num(); - view_num<=proj_data_info.get_max_view_num(); - view_num+=3) - { - // loop over axial_positions. Avoid using first and last positions, as - // if there is axial compression, the central LOR of a bin might actually not - // fall within the scanner. In this case, the get_bin(get_LOR(org_bin)) code - // will return an out-of-range bin (i.e. value<0). - int axial_pos_num_margin=0; - const ProjDataInfoCylindrical* const proj_data_info_cyl_ptr = - dynamic_cast(&proj_data_info); - if (proj_data_info_cyl_ptr!=0) - { - axial_pos_num_margin = - std::max( - round(ceil(proj_data_info_cyl_ptr->get_average_ring_difference(segment_num) - - proj_data_info_cyl_ptr->get_min_ring_difference(segment_num))), - round(ceil(proj_data_info_cyl_ptr->get_max_ring_difference(segment_num) - - proj_data_info_cyl_ptr->get_average_ring_difference(segment_num)))); - } - for (int axial_pos_num=proj_data_info.get_min_axial_pos_num(segment_num)+axial_pos_num_margin ; - axial_pos_num<=proj_data_info.get_max_axial_pos_num(segment_num)-axial_pos_num_margin; - axial_pos_num+=3) - { - for (int tangential_pos_num=proj_data_info.get_min_tangential_pos_num()+1; - tangential_pos_num<=proj_data_info.get_max_tangential_pos_num()-1; - tangential_pos_num+=1) - { - const Bin org_bin(segment_num,view_num,axial_pos_num,tangential_pos_num, /* value*/1); - LORInAxialAndNoArcCorrSinogramCoordinates lor; - proj_data_info.get_LOR(lor, org_bin); - { - const Bin new_bin = proj_data_info.get_bin(lor); + for (int segment_num = proj_data_info.get_min_segment_num(); segment_num <= proj_data_info.get_max_segment_num(); ++segment_num) + { + for (int view_num = proj_data_info.get_min_view_num(); view_num <= proj_data_info.get_max_view_num(); view_num += 3) + { + // loop over axial_positions. Avoid using first and last positions, as + // if there is axial compression, the central LOR of a bin might actually not + // fall within the scanner. In this case, the get_bin(get_LOR(org_bin)) code + // will return an out-of-range bin (i.e. value<0). + int axial_pos_num_margin = 0; + const ProjDataInfoCylindrical* const proj_data_info_cyl_ptr + = dynamic_cast(&proj_data_info); + if (proj_data_info_cyl_ptr != 0) + { + axial_pos_num_margin = std::max(round(ceil(proj_data_info_cyl_ptr->get_average_ring_difference(segment_num) + - proj_data_info_cyl_ptr->get_min_ring_difference(segment_num))), + round(ceil(proj_data_info_cyl_ptr->get_max_ring_difference(segment_num) + - proj_data_info_cyl_ptr->get_average_ring_difference(segment_num)))); + } + for (int axial_pos_num = proj_data_info.get_min_axial_pos_num(segment_num) + axial_pos_num_margin; + axial_pos_num <= proj_data_info.get_max_axial_pos_num(segment_num) - axial_pos_num_margin; axial_pos_num += 3) + { + for (int tangential_pos_num = proj_data_info.get_min_tangential_pos_num() + 1; + tangential_pos_num <= proj_data_info.get_max_tangential_pos_num() - 1; tangential_pos_num += 1) + { + const Bin org_bin(segment_num, view_num, axial_pos_num, tangential_pos_num, /* value*/ 1); + LORInAxialAndNoArcCorrSinogramCoordinates lor; + proj_data_info.get_LOR(lor, org_bin); + { + const Bin new_bin = proj_data_info.get_bin(lor); #if 1 - const int diff_segment_num = - intabs(org_bin.segment_num() - new_bin.segment_num()); - const int diff_view_num = - intabs(org_bin.view_num() - new_bin.view_num()); - const int diff_axial_pos_num = - intabs(org_bin.axial_pos_num() - new_bin.axial_pos_num()); - const int diff_tangential_pos_num = - intabs(org_bin.tangential_pos_num() - new_bin.tangential_pos_num()); - if (new_bin.get_bin_value()>0) - { - if (diff_segment_num>max_diff_segment_num) - max_diff_segment_num=diff_segment_num; - if (diff_view_num>max_diff_view_num) - max_diff_view_num=diff_view_num; - if (diff_axial_pos_num>max_diff_axial_pos_num) - max_diff_axial_pos_num=diff_axial_pos_num; - if (diff_tangential_pos_num>max_diff_tangential_pos_num) - max_diff_tangential_pos_num=diff_tangential_pos_num; - } -#ifdef STIR_OPENMP + const int diff_segment_num = intabs(org_bin.segment_num() - new_bin.segment_num()); + const int diff_view_num = intabs(org_bin.view_num() - new_bin.view_num()); + const int diff_axial_pos_num = intabs(org_bin.axial_pos_num() - new_bin.axial_pos_num()); + const int diff_tangential_pos_num = intabs(org_bin.tangential_pos_num() - new_bin.tangential_pos_num()); + if (new_bin.get_bin_value() > 0) + { + if (diff_segment_num > max_diff_segment_num) + max_diff_segment_num = diff_segment_num; + if (diff_view_num > max_diff_view_num) + max_diff_view_num = diff_view_num; + if (diff_axial_pos_num > max_diff_axial_pos_num) + max_diff_axial_pos_num = diff_axial_pos_num; + if (diff_tangential_pos_num > max_diff_tangential_pos_num) + max_diff_tangential_pos_num = diff_tangential_pos_num; + } +# ifdef STIR_OPENMP // add a pragma to avoid cerr output being jumbled up if there are any errors -#pragma omp critical(TESTPROJDATAINFO) -#endif - if (!check(org_bin.get_bin_value() == new_bin.get_bin_value(), "round-trip get_LOR then get_bin: value") || - !check(diff_segment_num<=0, "round-trip get_LOR then get_bin: segment") || - !check(diff_view_num<=1, "round-trip get_LOR then get_bin: view") || - !check(diff_axial_pos_num<=1, "round-trip get_LOR then get_bin: axial_pos") || - !check(diff_tangential_pos_num<=1, "round-trip get_LOR then get_bin: tangential_pos")) +# pragma omp critical(TESTPROJDATAINFO) +# endif + if (!check(org_bin.get_bin_value() == new_bin.get_bin_value(), "round-trip get_LOR then get_bin: value") + || !check(diff_segment_num <= 0, "round-trip get_LOR then get_bin: segment") + || !check(diff_view_num <= 1, "round-trip get_LOR then get_bin: view") + || !check(diff_axial_pos_num <= 1, "round-trip get_LOR then get_bin: axial_pos") + || !check(diff_tangential_pos_num <= 1, "round-trip get_LOR then get_bin: tangential_pos")) #else - if (!check(org_bin == new_bin, "round-trip get_LOR then get_bin")) + if (!check(org_bin == new_bin, "round-trip get_LOR then get_bin")) #endif - { - cerr << "\tProblem at segment = " << org_bin.segment_num() - << ", axial pos " << org_bin.axial_pos_num() - << ", view = " << org_bin.view_num() - << ", tangential_pos_num = " << org_bin.tangential_pos_num() << "\n"; - if (new_bin.get_bin_value()>0) - cerr << "\tround-trip to segment = " << new_bin.segment_num() - << ", axial pos " << new_bin.axial_pos_num() - << ", view = " << new_bin.view_num() - << ", tangential_pos_num = " << new_bin.tangential_pos_num() - <<'\n'; - } - } - // repeat test but with different type of LOR - { - LORAs2Points lor_as_points; - lor.get_intersections_with_cylinder(lor_as_points, lor.radius()); - const Bin new_bin = proj_data_info.get_bin(lor_as_points); + { + cerr << "\tProblem at segment = " << org_bin.segment_num() << ", axial pos " << org_bin.axial_pos_num() + << ", view = " << org_bin.view_num() << ", tangential_pos_num = " << org_bin.tangential_pos_num() + << "\n"; + if (new_bin.get_bin_value() > 0) + cerr << "\tround-trip to segment = " << new_bin.segment_num() << ", axial pos " + << new_bin.axial_pos_num() << ", view = " << new_bin.view_num() + << ", tangential_pos_num = " << new_bin.tangential_pos_num() << '\n'; + } + } + // repeat test but with different type of LOR + { + LORAs2Points lor_as_points; + lor.get_intersections_with_cylinder(lor_as_points, lor.radius()); + const Bin new_bin = proj_data_info.get_bin(lor_as_points); #if 1 - const int diff_segment_num = - intabs(org_bin.segment_num() - new_bin.segment_num()); - const int diff_view_num = - intabs(org_bin.view_num() - new_bin.view_num()); - const int diff_axial_pos_num = - intabs(org_bin.axial_pos_num() - new_bin.axial_pos_num()); - const int diff_tangential_pos_num = - intabs(org_bin.tangential_pos_num() - new_bin.tangential_pos_num()); - if (new_bin.get_bin_value()>0) - { - if (diff_segment_num>max_diff_segment_num) - max_diff_segment_num=diff_segment_num; - if (diff_view_num>max_diff_view_num) - max_diff_view_num=diff_view_num; - if (diff_axial_pos_num>max_diff_axial_pos_num) - max_diff_axial_pos_num=diff_axial_pos_num; - if (diff_tangential_pos_num>max_diff_tangential_pos_num) - max_diff_tangential_pos_num=diff_tangential_pos_num; - } - if (!check(org_bin.get_bin_value() == new_bin.get_bin_value(), "round-trip get_LOR then get_bin (LORAs2Points): value") || - !check(diff_segment_num<=0, "round-trip get_LOR then get_bin (LORAs2Points): segment") || - !check(diff_view_num<=1, "round-trip get_LOR then get_bin (LORAs2Points): view") || - !check(diff_axial_pos_num<=1, "round-trip get_LOR then get_bin (LORAs2Points): axial_pos") || - !check(diff_tangential_pos_num<=1, "round-trip get_LOR then get_bin (LORAs2Points): tangential_pos")) - + const int diff_segment_num = intabs(org_bin.segment_num() - new_bin.segment_num()); + const int diff_view_num = intabs(org_bin.view_num() - new_bin.view_num()); + const int diff_axial_pos_num = intabs(org_bin.axial_pos_num() - new_bin.axial_pos_num()); + const int diff_tangential_pos_num = intabs(org_bin.tangential_pos_num() - new_bin.tangential_pos_num()); + if (new_bin.get_bin_value() > 0) + { + if (diff_segment_num > max_diff_segment_num) + max_diff_segment_num = diff_segment_num; + if (diff_view_num > max_diff_view_num) + max_diff_view_num = diff_view_num; + if (diff_axial_pos_num > max_diff_axial_pos_num) + max_diff_axial_pos_num = diff_axial_pos_num; + if (diff_tangential_pos_num > max_diff_tangential_pos_num) + max_diff_tangential_pos_num = diff_tangential_pos_num; + } + if (!check(org_bin.get_bin_value() == new_bin.get_bin_value(), + "round-trip get_LOR then get_bin (LORAs2Points): value") + || !check(diff_segment_num <= 0, "round-trip get_LOR then get_bin (LORAs2Points): segment") + || !check(diff_view_num <= 1, "round-trip get_LOR then get_bin (LORAs2Points): view") + || !check(diff_axial_pos_num <= 1, "round-trip get_LOR then get_bin (LORAs2Points): axial_pos") + || !check(diff_tangential_pos_num <= 1, "round-trip get_LOR then get_bin (LORAs2Points): tangential_pos")) + #else - if (!check(org_bin == new_bin, "round-trip get_LOR then get_bin")) + if (!check(org_bin == new_bin, "round-trip get_LOR then get_bin")) #endif - { - cerr << "\tProblem at segment = " << org_bin.segment_num() - << ", axial pos " << org_bin.axial_pos_num() - << ", view = " << org_bin.view_num() - << ", tangential_pos_num = " << org_bin.tangential_pos_num() << "\n"; - if (new_bin.get_bin_value()>0) - cerr << "\tround-trip to segment = " << new_bin.segment_num() - << ", axial pos " << new_bin.axial_pos_num() - << ", view = " << new_bin.view_num() - << ", tangential_pos_num = " << new_bin.tangential_pos_num() - <<'\n'; - } - } - } - } - } - } - cerr << "Max Deviation: segment = " << max_diff_segment_num - << ", axial pos " << max_diff_axial_pos_num - << ", view = " << max_diff_view_num - << ", tangential_pos_num = " << max_diff_tangential_pos_num << "\n"; - - // test on reduce_segment_range and operator>= - { - shared_ptr smaller(proj_data_info.clone()); - check(proj_data_info >= *smaller, "check on operator>= and equal objects"); - smaller->set_min_tangential_pos_num(0); - check(proj_data_info >= *smaller, "check on tangential_pos and operator>="); - smaller->set_min_axial_pos_num(4, 0); - check(proj_data_info >= *smaller, "check on axial_pos and operator>="); - - smaller->reduce_segment_range(0,0); - check(proj_data_info >= *smaller, "check on reduce_segment_range and operator>="); - // make one range larger, so should now fail - smaller->set_min_tangential_pos_num(proj_data_info.get_min_tangential_pos_num() - 4); - check(!(proj_data_info >= *smaller), "check on mixed case with tangential_pos_num and operator>="); - // reset and do the same for axial pos - smaller->set_min_tangential_pos_num(proj_data_info.get_min_tangential_pos_num() + 4); - check(proj_data_info >= *smaller, "check on reduced segments and tangential_pos and operator>="); - smaller->set_max_axial_pos_num(proj_data_info.get_max_axial_pos_num(0)+4, 0); - check(!(proj_data_info >= *smaller), "check on mixed case with axial_pos_num and operator>="); + { + cerr << "\tProblem at segment = " << org_bin.segment_num() << ", axial pos " << org_bin.axial_pos_num() + << ", view = " << org_bin.view_num() << ", tangential_pos_num = " << org_bin.tangential_pos_num() + << "\n"; + if (new_bin.get_bin_value() > 0) + cerr << "\tround-trip to segment = " << new_bin.segment_num() << ", axial pos " + << new_bin.axial_pos_num() << ", view = " << new_bin.view_num() + << ", tangential_pos_num = " << new_bin.tangential_pos_num() << '\n'; + } + } + } + } + } } -} + cerr << "Max Deviation: segment = " << max_diff_segment_num << ", axial pos " << max_diff_axial_pos_num + << ", view = " << max_diff_view_num << ", tangential_pos_num = " << max_diff_tangential_pos_num << "\n"; + // test on reduce_segment_range and operator>= + { + shared_ptr smaller(proj_data_info.clone()); + check(proj_data_info >= *smaller, "check on operator>= and equal objects"); + smaller->set_min_tangential_pos_num(0); + check(proj_data_info >= *smaller, "check on tangential_pos and operator>="); + smaller->set_min_axial_pos_num(4, 0); + check(proj_data_info >= *smaller, "check on axial_pos and operator>="); + + smaller->reduce_segment_range(0, 0); + check(proj_data_info >= *smaller, "check on reduce_segment_range and operator>="); + // make one range larger, so should now fail + smaller->set_min_tangential_pos_num(proj_data_info.get_min_tangential_pos_num() - 4); + check(!(proj_data_info >= *smaller), "check on mixed case with tangential_pos_num and operator>="); + // reset and do the same for axial pos + smaller->set_min_tangential_pos_num(proj_data_info.get_min_tangential_pos_num() + 4); + check(proj_data_info >= *smaller, "check on reduced segments and tangential_pos and operator>="); + smaller->set_max_axial_pos_num(proj_data_info.get_max_axial_pos_num(0) + 4, 0); + check(!(proj_data_info >= *smaller), "check on mixed case with axial_pos_num and operator>="); + } +} /*! \ingroup test \brief Test class for ProjDataInfoCylindrical */ -class ProjDataInfoCylindricalTests: public ProjDataInfoTests +class ProjDataInfoCylindricalTests : public ProjDataInfoTests { -protected: +protected: void test_cylindrical_proj_data_info(ProjDataInfoCylindrical& proj_data_info); }; - void -ProjDataInfoCylindricalTests:: -test_cylindrical_proj_data_info(ProjDataInfoCylindrical& proj_data_info) +ProjDataInfoCylindricalTests::test_cylindrical_proj_data_info(ProjDataInfoCylindrical& proj_data_info) { cerr << "\tTesting consistency between different implementations of geometric info\n"; { - const Bin bin(proj_data_info.get_max_segment_num(), - 1, - proj_data_info.get_max_axial_pos_num(proj_data_info.get_max_segment_num())/2, - 1); - check_if_equal(proj_data_info.get_sampling_in_m(bin), - proj_data_info.ProjDataInfo::get_sampling_in_m(bin), - "test consistency get_sampling_in_m"); - check_if_equal(proj_data_info.get_sampling_in_t(bin), - proj_data_info.ProjDataInfo::get_sampling_in_t(bin), - "test consistency get_sampling_in_t"); + const Bin bin(proj_data_info.get_max_segment_num(), 1, + proj_data_info.get_max_axial_pos_num(proj_data_info.get_max_segment_num()) / 2, 1); + check_if_equal(proj_data_info.get_sampling_in_m(bin), proj_data_info.ProjDataInfo::get_sampling_in_m(bin), + "test consistency get_sampling_in_m"); + check_if_equal(proj_data_info.get_sampling_in_t(bin), proj_data_info.ProjDataInfo::get_sampling_in_t(bin), + "test consistency get_sampling_in_t"); #if 0 // ProjDataInfo has no default implementation for get_tantheta // I just leave the code here to make this explicit @@ -347,750 +312,674 @@ test_cylindrical_proj_data_info(ProjDataInfoCylindrical& proj_data_info) proj_data_info.ProjDataInfo::get_tantheta(bin), "test consistency get_tantheta"); #endif - check_if_equal(proj_data_info.get_costheta(bin), - proj_data_info.ProjDataInfo::get_costheta(bin), - "test consistency get_costheta"); + check_if_equal(proj_data_info.get_costheta(bin), proj_data_info.ProjDataInfo::get_costheta(bin), + "test consistency get_costheta"); - check_if_equal(proj_data_info.get_costheta(bin), - cos(atan(proj_data_info.get_tantheta(bin))), - "cross check get_costheta and get_tantheta"); + check_if_equal(proj_data_info.get_costheta(bin), cos(atan(proj_data_info.get_tantheta(bin))), + "cross check get_costheta and get_tantheta"); // try the same with a non-standard ring spacing const float old_ring_spacing = proj_data_info.get_ring_spacing(); proj_data_info.set_ring_spacing(2.1F); - check_if_equal(proj_data_info.get_sampling_in_m(bin), - proj_data_info.ProjDataInfo::get_sampling_in_m(bin), - "test consistency get_sampling_in_m"); - check_if_equal(proj_data_info.get_sampling_in_t(bin), - proj_data_info.ProjDataInfo::get_sampling_in_t(bin), - "test consistency get_sampling_in_t"); + check_if_equal(proj_data_info.get_sampling_in_m(bin), proj_data_info.ProjDataInfo::get_sampling_in_m(bin), + "test consistency get_sampling_in_m"); + check_if_equal(proj_data_info.get_sampling_in_t(bin), proj_data_info.ProjDataInfo::get_sampling_in_t(bin), + "test consistency get_sampling_in_t"); #if 0 check_if_equal(proj_data_info.get_tantheta(bin), proj_data_info.ProjDataInfo::get_tantheta(bin), "test consistency get_tantheta"); #endif - check_if_equal(proj_data_info.get_costheta(bin), - proj_data_info.ProjDataInfo::get_costheta(bin), - "test consistency get_costheta"); + check_if_equal(proj_data_info.get_costheta(bin), proj_data_info.ProjDataInfo::get_costheta(bin), + "test consistency get_costheta"); - check_if_equal(proj_data_info.get_costheta(bin), - cos(atan(proj_data_info.get_tantheta(bin))), - "cross check get_costheta and get_tantheta"); + check_if_equal(proj_data_info.get_costheta(bin), cos(atan(proj_data_info.get_tantheta(bin))), + "cross check get_costheta and get_tantheta"); // set back to usual value proj_data_info.set_ring_spacing(old_ring_spacing); } if (proj_data_info.get_max_ring_difference(0) == proj_data_info.get_min_ring_difference(0) && proj_data_info.get_max_ring_difference(1) == proj_data_info.get_min_ring_difference(1) - && proj_data_info.get_max_ring_difference(2) == proj_data_info.get_min_ring_difference(2) - ) + && proj_data_info.get_max_ring_difference(2) == proj_data_info.get_min_ring_difference(2)) { // these tests work only without axial compression cerr << "\tTest ring pair to segment,ax_pos (span 1)\n"; #ifdef STIR_OPENMP -#pragma omp parallel for schedule(dynamic) +# pragma omp parallel for schedule(dynamic) #endif - for (int ring1=0; ring1get_num_rings(); ++ring1) - for (int ring2=0; ring2get_num_rings(); ++ring2) - { - int segment_num = 0, axial_pos_num = 0; - check(proj_data_info. - get_segment_axial_pos_num_for_ring_pair(segment_num, - axial_pos_num, - ring1, - ring2) == - Succeeded::yes, - "test if segment,ax_pos_num found for a ring pair"); - check_if_equal(segment_num, ring2-ring1, - "test if segment_num is equal to ring difference\n"); - check_if_equal(axial_pos_num, min(ring2,ring1), - "test if segment_num is equal to ring difference\n"); - - int check_ring1 = 0, check_ring2 = 0; - proj_data_info. - get_ring_pair_for_segment_axial_pos_num(check_ring1, - check_ring2, - segment_num, - axial_pos_num); - check_if_equal(ring1, check_ring1, - "test ring1 equal after going to segment/ax_pos and returning\n"); - check_if_equal(ring2, check_ring2, - "test ring2 equal after going to segment/ax_pos and returning\n"); - - const ProjDataInfoCylindrical::RingNumPairs& ring_pairs = - proj_data_info. - get_all_ring_pairs_for_segment_axial_pos_num(segment_num, - axial_pos_num); - - check_if_equal(ring_pairs.size(), static_cast(1), - "test total number of ring-pairs for 1 segment/ax_pos should be 1 for span=1\n"); - check_if_equal(ring1, ring_pairs[0].first, - "test ring1 equal after going to segment/ax_pos and returning (version with all ring_pairs)\n"); - check_if_equal(ring2, ring_pairs[0].second, - "test ring2 equal after going to segment/ax_pos and returning (version with all ring_pairs)\n"); - } - } + for (int ring1 = 0; ring1 < proj_data_info.get_scanner_ptr()->get_num_rings(); ++ring1) + for (int ring2 = 0; ring2 < proj_data_info.get_scanner_ptr()->get_num_rings(); ++ring2) + { + int segment_num = 0, axial_pos_num = 0; + check(proj_data_info.get_segment_axial_pos_num_for_ring_pair(segment_num, axial_pos_num, ring1, ring2) + == Succeeded::yes, + "test if segment,ax_pos_num found for a ring pair"); + check_if_equal(segment_num, ring2 - ring1, "test if segment_num is equal to ring difference\n"); + check_if_equal(axial_pos_num, min(ring2, ring1), "test if segment_num is equal to ring difference\n"); + + int check_ring1 = 0, check_ring2 = 0; + proj_data_info.get_ring_pair_for_segment_axial_pos_num(check_ring1, check_ring2, segment_num, axial_pos_num); + check_if_equal(ring1, check_ring1, "test ring1 equal after going to segment/ax_pos and returning\n"); + check_if_equal(ring2, check_ring2, "test ring2 equal after going to segment/ax_pos and returning\n"); + + const ProjDataInfoCylindrical::RingNumPairs& ring_pairs + = proj_data_info.get_all_ring_pairs_for_segment_axial_pos_num(segment_num, axial_pos_num); + + check_if_equal(ring_pairs.size(), static_cast(1), + "test total number of ring-pairs for 1 segment/ax_pos should be 1 for span=1\n"); + check_if_equal(ring1, ring_pairs[0].first, + "test ring1 equal after going to segment/ax_pos and returning (version with all ring_pairs)\n"); + check_if_equal(ring2, ring_pairs[0].second, + "test ring2 equal after going to segment/ax_pos and returning (version with all ring_pairs)\n"); + } + } cerr << "\tTest ring pair to segment,ax_pos and vice versa (for any axial compression)\n"; { #ifdef STIR_OPENMP -#pragma omp parallel for schedule(dynamic) +# pragma omp parallel for schedule(dynamic) #endif - for (int segment_num=proj_data_info.get_min_segment_num(); - segment_num<=proj_data_info.get_max_segment_num(); - ++segment_num) - for (int axial_pos_num=proj_data_info.get_min_axial_pos_num(segment_num); - axial_pos_num<=proj_data_info.get_max_axial_pos_num(segment_num); - ++axial_pos_num) - { - const ProjDataInfoCylindrical::RingNumPairs& ring_pairs = - proj_data_info. - get_all_ring_pairs_for_segment_axial_pos_num(segment_num, - axial_pos_num); - for (ProjDataInfoCylindrical::RingNumPairs::const_iterator iter = ring_pairs.begin(); - iter != ring_pairs.end(); - ++iter) - { - int check_segment_num = 0, check_axial_pos_num = 0; - check(proj_data_info. - get_segment_axial_pos_num_for_ring_pair(check_segment_num, - check_axial_pos_num, - iter->first, - iter->second) == - Succeeded::yes, - "test if segment,ax_pos_num found for a ring pair"); - check_if_equal(check_segment_num, segment_num, - "test if segment_num is consistent\n"); - check_if_equal(check_axial_pos_num, axial_pos_num, - "test if axial_pos_num is consistent\n"); - } - } + for (int segment_num = proj_data_info.get_min_segment_num(); segment_num <= proj_data_info.get_max_segment_num(); + ++segment_num) + for (int axial_pos_num = proj_data_info.get_min_axial_pos_num(segment_num); + axial_pos_num <= proj_data_info.get_max_axial_pos_num(segment_num); ++axial_pos_num) + { + const ProjDataInfoCylindrical::RingNumPairs& ring_pairs + = proj_data_info.get_all_ring_pairs_for_segment_axial_pos_num(segment_num, axial_pos_num); + for (ProjDataInfoCylindrical::RingNumPairs::const_iterator iter = ring_pairs.begin(); iter != ring_pairs.end(); ++iter) + { + int check_segment_num = 0, check_axial_pos_num = 0; + check(proj_data_info.get_segment_axial_pos_num_for_ring_pair(check_segment_num, check_axial_pos_num, iter->first, + iter->second) + == Succeeded::yes, + "test if segment,ax_pos_num found for a ring pair"); + check_if_equal(check_segment_num, segment_num, "test if segment_num is consistent\n"); + check_if_equal(check_axial_pos_num, axial_pos_num, "test if axial_pos_num is consistent\n"); + } + } } test_generic_proj_data_info(proj_data_info); } /*! - The following tests that detection position is affected by the value of DOI + The following tests that detection position is affected by the value of DOI */ void ProjDataInfoTests::run_Blocks_DOI_test() { - CPUTimer timer; - auto scannerBlocks_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); - scannerBlocks_ptr->set_average_depth_of_interaction(0); - scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); - scannerBlocks_ptr->set_up(); - VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); - VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - - auto proj_data_info_blocks_doi0_ptr=std::make_shared(); - proj_data_info_blocks_doi0_ptr=set_blocks_projdata_info(num_axial_pos_per_segment, - min_ring_diff_v, - max_ring_diff_v, - scannerBlocks_ptr); - - auto scannerBlocksDOI_ptr=std::make_shared(*scannerBlocks_ptr); - scannerBlocksDOI_ptr->set_average_depth_of_interaction(0.1); - scannerBlocksDOI_ptr->set_up(); - auto proj_data_info_blocks_doi5_ptr=std::make_shared( - scannerBlocksDOI_ptr, - num_axial_pos_per_segment, - min_ring_diff_v, max_ring_diff_v, - scannerBlocksDOI_ptr->get_max_num_views(), - scannerBlocksDOI_ptr->get_max_num_non_arccorrected_bins()); - - Bin bin; - LORInAxialAndNoArcCorrSinogramCoordinates lor; - - int Bring1, Bring2, Bdet1,Bdet2, BDring1, BDring2, BDdet1, BDdet2; - CartesianCoordinate3D< float> b1,b2,bd1,bd2; - //float doi=scannerBlocksDOI_ptr->get_average_depth_of_interaction(); - timer.reset(); timer.start(); - - for (int seg =proj_data_info_blocks_doi0_ptr->get_min_segment_num(); seg <=proj_data_info_blocks_doi0_ptr->get_max_segment_num(); ++ seg) - for (int ax =proj_data_info_blocks_doi0_ptr->get_min_axial_pos_num(seg); ax <=proj_data_info_blocks_doi0_ptr->get_max_axial_pos_num(seg); ++ax) - for (int view = 0; view <=proj_data_info_blocks_doi0_ptr->get_max_view_num(); view++) - for (int tang =proj_data_info_blocks_doi0_ptr->get_min_tangential_pos_num(); tang <=proj_data_info_blocks_doi0_ptr->get_max_tangential_pos_num(); ++tang) - { - bin.segment_num() = seg; - bin.axial_pos_num() = ax; - bin.view_num() = view; - bin.tangential_pos_num() = tang; - - -// check det_pos instead - proj_data_info_blocks_doi0_ptr->get_det_pair_for_bin(Bdet1, Bring1, Bdet2,Bring2,bin); - proj_data_info_blocks_doi5_ptr->get_det_pair_for_bin(BDdet1, BDring1, BDdet2,BDring2,bin); - - - proj_data_info_blocks_doi0_ptr->get_LOR(lor,bin); - set_tolerance(10E-4); - - check_if_equal(Bdet1,BDdet1, ""); - check_if_equal(Bdet2,BDdet2, ""); - check_if_equal(Bring1,BDring1, ""); - check_if_equal(Bring2,BDring2, ""); - -// checkcartesian coordinates of detectors - proj_data_info_blocks_doi0_ptr->find_cartesian_coordinates_of_detection(b1,b2,bin); - proj_data_info_blocks_doi5_ptr->find_cartesian_coordinates_of_detection(bd1,bd2,bin); - -// set_tolerance(10E-2); - check(b1!=bd1, "detector position should be different with different DOIs"); - check(b2!=bd2, "detector position should be different with different DOIs"); - // TODO improve check here - } - timer.stop(); std::cerr<< "-- CPU Time " << timer.value() << '\n'; - + CPUTimer timer; + auto scannerBlocks_ptr = std::make_shared(Scanner::SAFIRDualRingPrototype); + scannerBlocks_ptr->set_average_depth_of_interaction(0); + scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); + scannerBlocks_ptr->set_up(); + + auto proj_data_info_blocks_doi0_ptr = std::make_shared(); + proj_data_info_blocks_doi0_ptr = set_blocks_projdata_info(scannerBlocks_ptr); + + auto scannerBlocksDOI_ptr = std::make_shared(*scannerBlocks_ptr); + scannerBlocksDOI_ptr->set_average_depth_of_interaction(0.1); + scannerBlocksDOI_ptr->set_up(); + + auto proj_data_info_blocks_doi01_ptr = std::make_shared(); + proj_data_info_blocks_doi01_ptr = set_blocks_projdata_info(scannerBlocksDOI_ptr); + + Bin bin; + LORInAxialAndNoArcCorrSinogramCoordinates lor; + + int Bring1, Bring2, Bdet1, Bdet2, BDring1, BDring2, BDdet1, BDdet2; + CartesianCoordinate3D b1, b2, bd1, bd2; + // float doi=scannerBlocksDOI_ptr->get_average_depth_of_interaction(); + timer.reset(); + timer.start(); + + for (int seg = proj_data_info_blocks_doi0_ptr->get_min_segment_num(); + seg <= proj_data_info_blocks_doi0_ptr->get_max_segment_num(); ++seg) + for (int ax = proj_data_info_blocks_doi0_ptr->get_min_axial_pos_num(seg); + ax <= proj_data_info_blocks_doi0_ptr->get_max_axial_pos_num(seg); ++ax) + for (int view = 0; view <= proj_data_info_blocks_doi0_ptr->get_max_view_num(); view++) + for (int tang = proj_data_info_blocks_doi0_ptr->get_min_tangential_pos_num(); + tang <= proj_data_info_blocks_doi0_ptr->get_max_tangential_pos_num(); ++tang) + { + bin.segment_num() = seg; + bin.axial_pos_num() = ax; + bin.view_num() = view; + bin.tangential_pos_num() = tang; + + // check det_pos instead + proj_data_info_blocks_doi0_ptr->get_det_pair_for_bin(Bdet1, Bring1, Bdet2, Bring2, bin); + proj_data_info_blocks_doi01_ptr->get_det_pair_for_bin(BDdet1, BDring1, BDdet2, BDring2, bin); + + proj_data_info_blocks_doi0_ptr->get_LOR(lor, bin); + set_tolerance(10E-4); + + check_if_equal(Bdet1, BDdet1, ""); + check_if_equal(Bdet2, BDdet2, ""); + check_if_equal(Bring1, BDring1, ""); + check_if_equal(Bring2, BDring2, ""); + + // checkcartesian coordinates of detectors + proj_data_info_blocks_doi0_ptr->find_cartesian_coordinates_of_detection(b1, b2, bin); + proj_data_info_blocks_doi01_ptr->find_cartesian_coordinates_of_detection(bd1, bd2, bin); + + // set_tolerance(10E-2); + check(b1 != bd1, "detector position should be different with different DOIs"); + check(b2 != bd2, "detector position should be different with different DOIs"); + // TODO improve check here + } + timer.stop(); + std::cerr << "-- CPU Time " << timer.value() << '\n'; } /*! The following tests the consistency of coordinates obtained with a cilindrical scanner - and those of a blocks on cylindrical scanner. For this test, a scanner with 4 rings, 2 - detector per block and 2 blcs per bucket is used axially. However, transaxially we have + and those of a blocks on cylindrical scanner. For this test, a scanner with 4 rings, 2 + detector per block and 2 blcs per bucket is used axially. However, transaxially we have 1 crystal per block a 1 block per bucket - + In this function, an extra test is performed to check that a roundtrip - transformation: detector_ID->cartesian_coord_detection_pos->detector_ID + transformation: detector_ID->cartesian_coord_detection_pos->detector_ID provides the same as the starting point */ void ProjDataInfoTests::run_coordinate_test() { - CPUTimer timer; - auto scannerBlocks_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); - scannerBlocks_ptr->set_num_axial_crystals_per_block(2); - scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing()* - scannerBlocks_ptr->get_num_axial_crystals_per_block()); - scannerBlocks_ptr->set_num_transaxial_crystals_per_block(1); - scannerBlocks_ptr->set_num_axial_blocks_per_bucket(2); - scannerBlocks_ptr->set_num_transaxial_blocks_per_bucket(1); - scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing()* - scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); - scannerBlocks_ptr->set_num_rings(4); - - scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); - scannerBlocks_ptr->set_up(); - - auto scannerCyl_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); - scannerCyl_ptr->set_num_axial_crystals_per_block(2); - scannerCyl_ptr->set_axial_block_spacing(scannerCyl_ptr->get_axial_crystal_spacing()* - scannerCyl_ptr->get_num_axial_crystals_per_block()); - scannerCyl_ptr->set_transaxial_block_spacing(scannerCyl_ptr->get_transaxial_crystal_spacing()* - scannerCyl_ptr->get_num_transaxial_crystals_per_block()); - scannerCyl_ptr->set_num_transaxial_crystals_per_block(1); - scannerCyl_ptr->set_num_axial_blocks_per_bucket(2); - scannerCyl_ptr->set_num_transaxial_blocks_per_bucket(1); - - scannerCyl_ptr->set_num_rings(4); - scannerCyl_ptr->set_scanner_geometry("Cylindrical"); - scannerCyl_ptr->set_up(); - - VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); - VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - - - auto proj_data_info_blocks_ptr=std::make_shared(); - proj_data_info_blocks_ptr=set_blocks_projdata_info(num_axial_pos_per_segment, - min_ring_diff_v, - max_ring_diff_v, - scannerBlocks_ptr); - - auto proj_data_info_cyl_ptr=std::make_shared( - scannerCyl_ptr, - num_axial_pos_per_segment, - min_ring_diff_v, max_ring_diff_v, - scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); - - Bin bin, binRT; - - int Bring1, Bring2, Bdet1,Bdet2, Cring1, Cring2, Cdet1, Cdet2; - int RTring1, RTring2, RTdet1,RTdet2; - CartesianCoordinate3D< float> b1,b2,c1,c2,roundt1, roundt2; - timer.reset(); timer.start(); - LORInAxialAndNoArcCorrSinogramCoordinates lorB; - LORInAxialAndNoArcCorrSinogramCoordinates lorC; - - LORInAxialAndNoArcCorrSinogramCoordinates lorC1, lorCn; - - for (int seg = proj_data_info_blocks_ptr->get_min_segment_num(); seg <= proj_data_info_blocks_ptr->get_max_segment_num(); ++ seg) - for (int ax = proj_data_info_blocks_ptr->get_min_axial_pos_num(seg); ax <= proj_data_info_blocks_ptr->get_max_axial_pos_num(seg); ++ax) - for (int view = 0; view <= proj_data_info_blocks_ptr->get_max_view_num(); view++) - for (int tang = proj_data_info_blocks_ptr->get_min_tangential_pos_num(); tang <= proj_data_info_blocks_ptr->get_max_tangential_pos_num(); ++tang) - { - bin.segment_num() = seg; - bin.axial_pos_num() = ax; - bin.view_num() = view; - bin.tangential_pos_num() = tang; - - proj_data_info_cyl_ptr->get_LOR(lorC,bin); - proj_data_info_blocks_ptr->get_LOR(lorB,bin); - - const int num_detectors = - proj_data_info_cyl_ptr->get_scanner_ptr()->get_num_detectors_per_ring(); - - int det_num1=0, det_num2=0; - proj_data_info_cyl_ptr-> - get_det_num_pair_for_view_tangential_pos_num(det_num1, - det_num2, - bin.view_num(), - bin.tangential_pos_num()); - - float phi; - phi = static_cast( - (det_num1+det_num2)*_PI/num_detectors-_PI/2 + proj_data_info_cyl_ptr->get_azimuthal_angle_offset() ); - - lorC1 = LORInAxialAndNoArcCorrSinogramCoordinates(lorC.z1(), - lorC.z2(), - phi,//lorC.phi(), - lorC.beta(), - proj_data_info_cyl_ptr->get_ring_radius()); - - const float old_phi=proj_data_info_cyl_ptr->get_phi(bin); - if (fabs(phi-old_phi)>=2*_PI/num_detectors){ - //float ang=2*_PI/num_detectors/2; -// warning("view %d old_phi %g new_phi %g\n",bin.view_num(), old_phi, phi); - - lorC1 = LORInAxialAndNoArcCorrSinogramCoordinates(lorC.z2(), - lorC.z1(), - phi,//lorC.phi(), - -lorC.beta(), - proj_data_info_cyl_ptr->get_ring_radius()); - } -// check det_pos instead - proj_data_info_blocks_ptr->get_det_pair_for_bin(Bdet1, Bring1, Bdet2,Bring2,bin); - proj_data_info_cyl_ptr->get_det_pair_for_bin(Cdet1, Cring1, Cdet2,Cring2,bin); - - set_tolerance(10E-4); - - check_if_equal(Bdet1,Cdet1, ""); - check_if_equal(Bdet2,Cdet2, ""); - check_if_equal(Bring1,Cring1, ""); - check_if_equal(Bring2,Cring2, ""); - -// test round trip from detector ID to coordinates and from cordinates to detecto IDs - proj_data_info_blocks_ptr->find_cartesian_coordinates_given_scanner_coordinates(roundt1, roundt2,Bring1, Bring2, Bdet1, Bdet2); - proj_data_info_blocks_ptr->find_bin_given_cartesian_coordinates_of_detection(binRT,roundt1, roundt2); - proj_data_info_blocks_ptr->get_det_pair_for_bin(RTdet1, RTring1, RTdet2,RTring2,bin); - - check_if_equal(Bdet1,RTdet1, "Roundtrip from detector A ID to coordinates and from cordinates to detector A ID"); - check_if_equal(Bdet2,RTdet2, "Roundtrip from detector B ID to coordinates and from cordinates to detector B ID"); - check_if_equal(Bring1,RTring1, "Roundtrip from ring A ID to coordinates and from cordinates to ring A ID"); - check_if_equal(Bring2,RTring2, "Roundtrip from ring B ID to coordinates and from cordinates to ring B ID"); - - -// checkcartesian coordinates of detectors - proj_data_info_cyl_ptr->find_cartesian_coordinates_of_detection(c1,c2,bin); - proj_data_info_blocks_ptr->find_cartesian_coordinates_of_detection(b1,b2,bin); - - check_if_equal(b1,c1, ""); - check_if_equal(b2,c2, ""); - - set_tolerance(10E-3); - - if (abs(lorB.phi() - lorC1.phi()) < tolerance ) - { - - check_if_equal(proj_data_info_blocks_ptr->get_s(bin),lorB.s(), "A get_s() from projdata is different from Block on Cylindrical LOR.s()"); - - check_if_equal(lorB.s(),lorC1.s(),"tang_pos="+ std::to_string(tang)+ - " PHI-C="+ std::to_string(lorC1.phi())+ - " PHI-B="+ std::to_string(lorB.phi())+ - " view="+ std::to_string(view)+ - " Atest if BlocksOnCylindrical LOR.s is the same as the LOR produced by Cylindrical");//) - - check_if_equal(lorB.beta(),lorC1.beta(),"tang_pos="+ std::to_string(tang)+ - " ax_pos="+ std::to_string(ax)+ - " segment="+ std::to_string(seg)+ - " view="+ std::to_string(view)+" test if BlocksOnCylindrical LOR.beta is the same as the LOR produced by Cylindrical"); - check_if_equal(lorB.z1(),lorC1.z1(),"tang_pos="+ std::to_string(tang)+ - " ax_pos="+ std::to_string(ax)+ - " segment="+ std::to_string(seg)+ - " view="+ std::to_string(view)+" test if BlocksOnCylindrical LOR.z1 is the same as the LOR produced by Cylindrical"); - check_if_equal(lorB.z2(),lorC1.z2(),"tang_pos="+ std::to_string(tang)+ - " ax_pos="+ std::to_string(ax)+ - " segment="+ std::to_string(seg)+ - " view="+ std::to_string(view)+" test if BlocksOnCylindrical LOR.z2 is the same as the LOR produced by Cylindrical"); - -// TODO: fix problem with interleaving when calculating Phi - } - else if (abs(lorB.phi() - lorC1.phi())+_PI < tolerance || abs(lorB.phi() - lorC1.phi())-_PI < tolerance){ - - - check_if_equal(proj_data_info_blocks_ptr->get_s(bin),lorB.s(), "B get_s() from projdata is different from Block on Cylindrical LOR.s()"); - check_if_equal(proj_data_info_blocks_ptr->get_phi(bin), phi-_PI, "B get_phi() from projdata Cylinder is different from Block on Cylindrical"); - - check_if_equal(lorB.s(),-lorC1.s(),"tang_pos="+ std::to_string(tang)+ - " PHYC="+ std::to_string(lorC1.phi())+ - " PHIB="+ std::to_string(lorB.phi())+ - " view="+ std::to_string(view)+ - " Btest if BlocksOnCylindrical LOR.s is the same as the LOR produced by Cylindrical");//) - check_if_equal(lorB.beta(),-lorC1.beta()," Btest if BlocksOnCylindrical LOR.beta is the same as the LOR produced by Cylindrical"); - check_if_equal(lorB.z1(),lorC1.z2(),"tang_pos="+ std::to_string(tang)+ - " ax_pos="+ std::to_string(ax)+ - " segment="+ std::to_string(seg)+ - " view="+ std::to_string(view)+" Btest if BlocksOnCylindrical LOR.z1 is the same as the LOR produced by Cylindrical"); - check_if_equal(lorB.z2(),lorC1.z1()," Btest if BlocksOnCylindrical LOR.z2 is the same as the LOR produced by Cylindrical"); - - } - else{ - check(false, "phi is different"); - } - check_if_equal(proj_data_info_blocks_ptr->get_m(bin),proj_data_info_cyl_ptr->get_m(bin)," test get_m Cylindrical"); + CPUTimer timer; + auto scannerBlocks_ptr = std::make_shared(Scanner::SAFIRDualRingPrototype); + scannerBlocks_ptr->set_num_axial_crystals_per_block(2); + scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing() + * scannerBlocks_ptr->get_num_axial_crystals_per_block()); + scannerBlocks_ptr->set_num_transaxial_crystals_per_block(1); + scannerBlocks_ptr->set_num_axial_blocks_per_bucket(2); + scannerBlocks_ptr->set_num_transaxial_blocks_per_bucket(1); + scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing() + * scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); + scannerBlocks_ptr->set_num_rings(4); + + scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); + scannerBlocks_ptr->set_up(); + + auto scannerCyl_ptr = std::make_shared(Scanner::SAFIRDualRingPrototype); + scannerCyl_ptr->set_num_axial_crystals_per_block(2); + scannerCyl_ptr->set_axial_block_spacing(scannerCyl_ptr->get_axial_crystal_spacing() + * scannerCyl_ptr->get_num_axial_crystals_per_block()); + scannerCyl_ptr->set_transaxial_block_spacing(scannerCyl_ptr->get_transaxial_crystal_spacing() + * scannerCyl_ptr->get_num_transaxial_crystals_per_block()); + scannerCyl_ptr->set_num_transaxial_crystals_per_block(1); + scannerCyl_ptr->set_num_axial_blocks_per_bucket(2); + scannerCyl_ptr->set_num_transaxial_blocks_per_bucket(1); + + scannerCyl_ptr->set_num_rings(4); + scannerCyl_ptr->set_scanner_geometry("Cylindrical"); + scannerCyl_ptr->set_up(); + + auto proj_data_info_blocks_ptr = std::make_shared(); + proj_data_info_blocks_ptr = set_blocks_projdata_info(scannerBlocks_ptr); + + auto proj_data_info_cyl_ptr = std::make_shared(); + proj_data_info_cyl_ptr = set_blocks_projdata_info(scannerCyl_ptr); + Bin bin, binRT; + + int Bring1, Bring2, Bdet1, Bdet2, Cring1, Cring2, Cdet1, Cdet2; + int RTring1, RTring2, RTdet1, RTdet2; + CartesianCoordinate3D b1, b2, c1, c2, roundt1, roundt2; + timer.reset(); + timer.start(); + LORInAxialAndNoArcCorrSinogramCoordinates lorB; + LORInAxialAndNoArcCorrSinogramCoordinates lorC; + + LORInAxialAndNoArcCorrSinogramCoordinates lorC1, lorCn; + + for (int seg = proj_data_info_blocks_ptr->get_min_segment_num(); seg <= proj_data_info_blocks_ptr->get_max_segment_num(); ++seg) + for (int ax = proj_data_info_blocks_ptr->get_min_axial_pos_num(seg); + ax <= proj_data_info_blocks_ptr->get_max_axial_pos_num(seg); ++ax) + for (int view = 0; view <= proj_data_info_blocks_ptr->get_max_view_num(); view++) + for (int tang = proj_data_info_blocks_ptr->get_min_tangential_pos_num(); + tang <= proj_data_info_blocks_ptr->get_max_tangential_pos_num(); ++tang) + { + bin.segment_num() = seg; + bin.axial_pos_num() = ax; + bin.view_num() = view; + bin.tangential_pos_num() = tang; + + proj_data_info_cyl_ptr->get_LOR(lorC, bin); + proj_data_info_blocks_ptr->get_LOR(lorB, bin); + + const int num_detectors = proj_data_info_cyl_ptr->get_scanner_ptr()->get_num_detectors_per_ring(); + + int det_num1 = 0, det_num2 = 0; + proj_data_info_cyl_ptr->get_det_num_pair_for_view_tangential_pos_num(det_num1, det_num2, bin.view_num(), + bin.tangential_pos_num()); + + float phi; + phi = static_cast((det_num1 + det_num2) * _PI / num_detectors - _PI / 2 + + proj_data_info_cyl_ptr->get_azimuthal_angle_offset()); + + lorC1 = LORInAxialAndNoArcCorrSinogramCoordinates(lorC.z1(), lorC.z2(), + phi, // lorC.phi(), + lorC.beta(), proj_data_info_cyl_ptr->get_ring_radius()); + + const float old_phi = proj_data_info_cyl_ptr->get_phi(bin); + if (fabs(phi - old_phi) >= 2 * _PI / num_detectors) + { + // float ang=2*_PI/num_detectors/2; + // warning("view %d old_phi %g new_phi %g\n",bin.view_num(), old_phi, phi); + + lorC1 = LORInAxialAndNoArcCorrSinogramCoordinates(lorC.z2(), lorC.z1(), + phi, // lorC.phi(), + -lorC.beta(), proj_data_info_cyl_ptr->get_ring_radius()); } - timer.stop(); std::cerr<< "-- CPU Time " << timer.value() << '\n'; - + // check det_pos instead + proj_data_info_blocks_ptr->get_det_pair_for_bin(Bdet1, Bring1, Bdet2, Bring2, bin); + proj_data_info_cyl_ptr->get_det_pair_for_bin(Cdet1, Cring1, Cdet2, Cring2, bin); + + set_tolerance(10E-4); + + check_if_equal(Bdet1, Cdet1, ""); + check_if_equal(Bdet2, Cdet2, ""); + check_if_equal(Bring1, Cring1, ""); + check_if_equal(Bring2, Cring2, ""); + + // test round trip from detector ID to coordinates and from cordinates to detecto IDs + proj_data_info_blocks_ptr->find_cartesian_coordinates_given_scanner_coordinates(roundt1, roundt2, Bring1, Bring2, + Bdet1, Bdet2); + proj_data_info_blocks_ptr->find_bin_given_cartesian_coordinates_of_detection(binRT, roundt1, roundt2); + proj_data_info_blocks_ptr->get_det_pair_for_bin(RTdet1, RTring1, RTdet2, RTring2, bin); + + check_if_equal(Bdet1, RTdet1, "Roundtrip from detector A ID to coordinates and from cordinates to detector A ID"); + check_if_equal(Bdet2, RTdet2, "Roundtrip from detector B ID to coordinates and from cordinates to detector B ID"); + check_if_equal(Bring1, RTring1, "Roundtrip from ring A ID to coordinates and from cordinates to ring A ID"); + check_if_equal(Bring2, RTring2, "Roundtrip from ring B ID to coordinates and from cordinates to ring B ID"); + + // checkcartesian coordinates of detectors + proj_data_info_cyl_ptr->find_cartesian_coordinates_of_detection(c1, c2, bin); + proj_data_info_blocks_ptr->find_cartesian_coordinates_of_detection(b1, b2, bin); + + check_if_equal(b1, c1, ""); + check_if_equal(b2, c2, ""); + + set_tolerance(10E-3); + + if (abs(lorB.phi() - lorC1.phi()) < tolerance) + { + + check_if_equal(proj_data_info_blocks_ptr->get_s(bin), lorB.s(), + "A get_s() from projdata is different from Block on Cylindrical LOR.s()"); + + check_if_equal(lorB.s(), lorC1.s(), + "tang_pos=" + std::to_string(tang) + " PHI-C=" + std::to_string(lorC1.phi()) + + " PHI-B=" + std::to_string(lorB.phi()) + " view=" + std::to_string(view) + + " Atest if BlocksOnCylindrical LOR.s is the same as the LOR produced by Cylindrical"); //) + + check_if_equal(lorB.beta(), lorC1.beta(), + "tang_pos=" + std::to_string(tang) + " ax_pos=" + std::to_string(ax) + + " segment=" + std::to_string(seg) + " view=" + std::to_string(view) + + " test if BlocksOnCylindrical LOR.beta is the same as the LOR produced by Cylindrical"); + check_if_equal(lorB.z1(), lorC1.z1(), + "tang_pos=" + std::to_string(tang) + " ax_pos=" + std::to_string(ax) + + " segment=" + std::to_string(seg) + " view=" + std::to_string(view) + + " test if BlocksOnCylindrical LOR.z1 is the same as the LOR produced by Cylindrical"); + check_if_equal(lorB.z2(), lorC1.z2(), + "tang_pos=" + std::to_string(tang) + " ax_pos=" + std::to_string(ax) + + " segment=" + std::to_string(seg) + " view=" + std::to_string(view) + + " test if BlocksOnCylindrical LOR.z2 is the same as the LOR produced by Cylindrical"); + + // TODO: fix problem with interleaving when calculating Phi + } + else if (abs(lorB.phi() - lorC1.phi()) + _PI < tolerance || abs(lorB.phi() - lorC1.phi()) - _PI < tolerance) + { + + check_if_equal(proj_data_info_blocks_ptr->get_s(bin), lorB.s(), + "B get_s() from projdata is different from Block on Cylindrical LOR.s()"); + check_if_equal(proj_data_info_blocks_ptr->get_phi(bin), phi - _PI, + "B get_phi() from projdata Cylinder is different from Block on Cylindrical"); + + check_if_equal(lorB.s(), -lorC1.s(), + "tang_pos=" + std::to_string(tang) + " PHYC=" + std::to_string(lorC1.phi()) + + " PHIB=" + std::to_string(lorB.phi()) + " view=" + std::to_string(view) + + " Btest if BlocksOnCylindrical LOR.s is the same as the LOR produced by Cylindrical"); //) + check_if_equal(lorB.beta(), -lorC1.beta(), + " Btest if BlocksOnCylindrical LOR.beta is the same as the LOR produced by Cylindrical"); + check_if_equal(lorB.z1(), lorC1.z2(), + "tang_pos=" + std::to_string(tang) + " ax_pos=" + std::to_string(ax) + + " segment=" + std::to_string(seg) + " view=" + std::to_string(view) + + " Btest if BlocksOnCylindrical LOR.z1 is the same as the LOR produced by Cylindrical"); + check_if_equal(lorB.z2(), lorC1.z1(), + " Btest if BlocksOnCylindrical LOR.z2 is the same as the LOR produced by Cylindrical"); + } + else + { + check(false, "phi is different"); + } + check_if_equal(proj_data_info_blocks_ptr->get_m(bin), proj_data_info_cyl_ptr->get_m(bin), " test get_m Cylindrical"); + } + timer.stop(); + std::cerr << "-- CPU Time " << timer.value() << '\n'; } /*! - The following test is similar to the above but for a scanner that has multiple rings and - multiple detectors per block. + The following test is similar to the above but for a scanner that has multiple rings and + multiple detectors per block. */ void ProjDataInfoTests::run_coordinate_test_for_realistic_scanner() { - CPUTimer timer; - auto scannerBlocks_ptr=std::make_shared(Scanner::SAFIRDualRingPrototype); - scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing()* - scannerBlocks_ptr->get_num_axial_crystals_per_block()); - scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing()* - scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); - - scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); - scannerBlocks_ptr->set_up(); - - auto scannerCyl_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); - scannerCyl_ptr->set_axial_block_spacing(scannerCyl_ptr->get_axial_crystal_spacing()* - scannerCyl_ptr->get_num_axial_crystals_per_block()); - scannerCyl_ptr->set_transaxial_block_spacing(scannerCyl_ptr->get_transaxial_crystal_spacing()* - scannerCyl_ptr->get_num_transaxial_crystals_per_block()); - - scannerCyl_ptr->set_scanner_geometry("Cylindrical"); - scannerCyl_ptr->set_up(); - - VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); - VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - - - auto proj_data_info_blocks_ptr=std::make_shared(); - proj_data_info_blocks_ptr=set_blocks_projdata_info(num_axial_pos_per_segment, - min_ring_diff_v, - max_ring_diff_v, - scannerBlocks_ptr); - - auto proj_data_info_cyl_ptr=std::make_shared( - scannerCyl_ptr, - num_axial_pos_per_segment, - min_ring_diff_v, max_ring_diff_v, - scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); - - Bin bin; - - int Bring1, Bring2, Bdet1,Bdet2, Cring1, Cring2, Cdet1, Cdet2; - CartesianCoordinate3D< float> b1,b2,c1,c2; - - // estimate the angle covered by half bucket, csi - float csi=_PI/scannerBlocks_ptr->get_num_transaxial_buckets(); - // distance between the center of the scannner and the first crystal in the bucket, r=Reffective/cos(csi) - float r=scannerBlocks_ptr->get_effective_ring_radius()/cos(csi); - float max_tolerance=abs(scannerBlocks_ptr->get_effective_ring_radius()-r); - - timer.reset(); timer.start(); - LORInAxialAndNoArcCorrSinogramCoordinates lorB; - LORInAxialAndNoArcCorrSinogramCoordinates lorC; - - LORInAxialAndNoArcCorrSinogramCoordinates lorC1, lorCn; - - for (int seg = proj_data_info_blocks_ptr->get_min_segment_num(); seg <= proj_data_info_blocks_ptr->get_max_segment_num(); ++ seg) - for (int ax = proj_data_info_blocks_ptr->get_min_axial_pos_num(seg); ax <= proj_data_info_blocks_ptr->get_max_axial_pos_num(seg); ++ax) - for (int view = 0; view <= proj_data_info_blocks_ptr->get_max_view_num(); view++) - for (int tang = proj_data_info_blocks_ptr->get_min_tangential_pos_num(); tang <= proj_data_info_blocks_ptr->get_max_tangential_pos_num(); ++tang) - { - bin.segment_num() = seg; - bin.axial_pos_num() = ax; - bin.view_num() = view; - bin.tangential_pos_num() = tang; - - proj_data_info_cyl_ptr->get_LOR(lorC,bin); - proj_data_info_blocks_ptr->get_LOR(lorB,bin); - - int det_num1=0, det_num2=0; - proj_data_info_cyl_ptr-> - get_det_num_pair_for_view_tangential_pos_num(det_num1, - det_num2, - bin.view_num(), - bin.tangential_pos_num()); - - -// check det_pos instead - proj_data_info_blocks_ptr->get_det_pair_for_bin(Bdet1, Bring1, Bdet2,Bring2,bin); - proj_data_info_cyl_ptr->get_det_pair_for_bin(Cdet1, Cring1, Cdet2,Cring2,bin); - - set_tolerance(10E-4); - - check_if_equal(Bdet1,Cdet1, ""); - check_if_equal(Bdet2,Cdet2, ""); - check_if_equal(Bring1,Cring1, ""); - check_if_equal(Bring2,Cring2, ""); - -// check cartesian coordinates of detectors - proj_data_info_cyl_ptr->find_cartesian_coordinates_of_detection(c1,c2,bin); - proj_data_info_blocks_ptr->find_cartesian_coordinates_of_detection(b1,b2,bin); - - // we expect to be differences of the order of the mm in x and y due to the difference in geometry - - set_tolerance(max_tolerance); - - check_if_equal(b1.y(),c1.y(), " checking cartesian coordinate y1"); - check_if_equal(b2.y(),c2.y(), " checking cartesian coordinate y2"); - check_if_equal(b1.x(),c1.x(), " checking cartesian coordinate x1"); - check_if_equal(b2.x(),c2.x(), " checking cartesian coordinate x2"); - -/*!calculate the max axial tolerance, the difference between m (blocks vs cylindrical) happens when the point A is at - * the beginning of a transaxial bucket and the point B is in the middle of the bucket (which is the point where the radius of circle - * and the radius of the poligon have the biggest difference - * !*/ - float psi=atan(abs(lorB.z2()-lorB.z1())/r); - float max_ax_tolerance=abs(scannerBlocks_ptr->get_effective_ring_radius()-r)/cos(psi); - set_tolerance(max_ax_tolerance); - check_if_equal(b1.z(),c1.z(), " checking cartesian coordinate z1"); - check_if_equal(b2.z(),c2.z(), " checking cartesian coordinate z2"); - check_if_equal(proj_data_info_blocks_ptr->get_m(bin),proj_data_info_cyl_ptr->get_m(bin)," test get_m Cylindrical"); - } - timer.stop(); std::cerr<< "-- CPU Time " << timer.value() << '\n'; - + CPUTimer timer; + auto scannerBlocks_ptr = std::make_shared(Scanner::SAFIRDualRingPrototype); + scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing() + * scannerBlocks_ptr->get_num_axial_crystals_per_block()); + scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing() + * scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); + + scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); + scannerBlocks_ptr->set_up(); + + auto scannerCyl_ptr = std::make_shared(Scanner::SAFIRDualRingPrototype); + scannerCyl_ptr->set_axial_block_spacing(scannerCyl_ptr->get_axial_crystal_spacing() + * scannerCyl_ptr->get_num_axial_crystals_per_block()); + scannerCyl_ptr->set_transaxial_block_spacing(scannerCyl_ptr->get_transaxial_crystal_spacing() + * scannerCyl_ptr->get_num_transaxial_crystals_per_block()); + + scannerCyl_ptr->set_scanner_geometry("Cylindrical"); + scannerCyl_ptr->set_up(); + + auto proj_data_info_blocks_ptr = std::make_shared(); + proj_data_info_blocks_ptr = set_blocks_projdata_info(scannerBlocks_ptr); + + auto proj_data_info_cyl_ptr = std::make_shared(); + proj_data_info_cyl_ptr = set_blocks_projdata_info(scannerCyl_ptr); + + Bin bin; + + int Bring1, Bring2, Bdet1, Bdet2, Cring1, Cring2, Cdet1, Cdet2; + CartesianCoordinate3D b1, b2, c1, c2; + + // estimate the angle covered by half bucket, csi + float csi = _PI / scannerBlocks_ptr->get_num_transaxial_buckets(); + // distance between the center of the scannner and the first crystal in the bucket, r=Reffective/cos(csi) + float r = scannerBlocks_ptr->get_effective_ring_radius() / cos(csi); + float max_tolerance = abs(scannerBlocks_ptr->get_effective_ring_radius() - r); + + timer.reset(); + timer.start(); + LORInAxialAndNoArcCorrSinogramCoordinates lorB; + LORInAxialAndNoArcCorrSinogramCoordinates lorC; + + LORInAxialAndNoArcCorrSinogramCoordinates lorC1, lorCn; + + for (int seg = proj_data_info_blocks_ptr->get_min_segment_num(); seg <= proj_data_info_blocks_ptr->get_max_segment_num(); ++seg) + for (int ax = proj_data_info_blocks_ptr->get_min_axial_pos_num(seg); + ax <= proj_data_info_blocks_ptr->get_max_axial_pos_num(seg); ++ax) + for (int view = 0; view <= proj_data_info_blocks_ptr->get_max_view_num(); view++) + for (int tang = proj_data_info_blocks_ptr->get_min_tangential_pos_num(); + tang <= proj_data_info_blocks_ptr->get_max_tangential_pos_num(); ++tang) + { + bin.segment_num() = seg; + bin.axial_pos_num() = ax; + bin.view_num() = view; + bin.tangential_pos_num() = tang; + + proj_data_info_cyl_ptr->get_LOR(lorC, bin); + proj_data_info_blocks_ptr->get_LOR(lorB, bin); + + int det_num1 = 0, det_num2 = 0; + proj_data_info_cyl_ptr->get_det_num_pair_for_view_tangential_pos_num(det_num1, det_num2, bin.view_num(), + bin.tangential_pos_num()); + + // check det_pos instead + proj_data_info_blocks_ptr->get_det_pair_for_bin(Bdet1, Bring1, Bdet2, Bring2, bin); + proj_data_info_cyl_ptr->get_det_pair_for_bin(Cdet1, Cring1, Cdet2, Cring2, bin); + + set_tolerance(10E-4); + + check_if_equal(Bdet1, Cdet1, ""); + check_if_equal(Bdet2, Cdet2, ""); + check_if_equal(Bring1, Cring1, ""); + check_if_equal(Bring2, Cring2, ""); + + // check cartesian coordinates of detectors + proj_data_info_cyl_ptr->find_cartesian_coordinates_of_detection(c1, c2, bin); + proj_data_info_blocks_ptr->find_cartesian_coordinates_of_detection(b1, b2, bin); + + // we expect to be differences of the order of the mm in x and y due to the difference in geometry + + set_tolerance(max_tolerance); + + check_if_equal(b1.y(), c1.y(), " checking cartesian coordinate y1"); + check_if_equal(b2.y(), c2.y(), " checking cartesian coordinate y2"); + check_if_equal(b1.x(), c1.x(), " checking cartesian coordinate x1"); + check_if_equal(b2.x(), c2.x(), " checking cartesian coordinate x2"); + + /*!calculate the max axial tolerance, the difference between m (blocks vs cylindrical) happens when the point A is at + * the beginning of a transaxial bucket and the point B is in the middle of the bucket (which is the point where the + * radius of circle and the radius of the poligon have the biggest difference + * !*/ + float psi = atan(abs(lorB.z2() - lorB.z1()) / r); + float max_ax_tolerance = abs(scannerBlocks_ptr->get_effective_ring_radius() - r) / cos(psi); + set_tolerance(max_ax_tolerance); + check_if_equal(b1.z(), c1.z(), " checking cartesian coordinate z1"); + check_if_equal(b2.z(), c2.z(), " checking cartesian coordinate z2"); + check_if_equal(proj_data_info_blocks_ptr->get_m(bin), proj_data_info_cyl_ptr->get_m(bin), " test get_m Cylindrical"); + } + timer.stop(); + std::cerr << "-- CPU Time " << timer.value() << '\n'; } /*! - The following tests the function get_s() for the BlockOnCylindrical case. the first test - checks that all lines passing for the center provide s=0. The second test checks that - parallel lines are always at the same angle phi, and that the step between consecutive + The following tests the function get_s() for the BlockOnCylindrical case. the first test + checks that all lines passing for the center provide s=0. The second test checks that + parallel lines are always at the same angle phi, and that the step between consecutive lines is the same and equal to the one calculated geometrically. */ void -ProjDataInfoTests:: -run_lor_get_s_test(){ - CPUTimer timer; - auto scannerBlocks_ptr=std::make_shared(Scanner::SAFIRDualRingPrototype); - scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing()* - scannerBlocks_ptr->get_num_axial_crystals_per_block()); - scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing()* - scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); - - scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); - scannerBlocks_ptr->set_up(); - - auto scannerCyl_ptr=std::make_shared (Scanner::SAFIRDualRingPrototype); - scannerCyl_ptr->set_axial_block_spacing(scannerCyl_ptr->get_axial_crystal_spacing()* - scannerCyl_ptr->get_num_axial_crystals_per_block()); - scannerCyl_ptr->set_transaxial_block_spacing(scannerCyl_ptr->get_transaxial_crystal_spacing()* - scannerCyl_ptr->get_num_transaxial_crystals_per_block()); - - scannerCyl_ptr->set_scanner_geometry("Cylindrical"); - scannerCyl_ptr->set_up(); - - VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); - VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); - - auto proj_data_info_blocks_ptr=std::make_shared(); - proj_data_info_blocks_ptr=set_blocks_projdata_info(num_axial_pos_per_segment, - min_ring_diff_v, - max_ring_diff_v, - scannerBlocks_ptr); - - auto proj_data_info_cyl_ptr=std::make_shared( - scannerCyl_ptr, - num_axial_pos_per_segment, - min_ring_diff_v, max_ring_diff_v, - scannerBlocks_ptr->get_max_num_views(), - scannerBlocks_ptr->get_max_num_non_arccorrected_bins()); -// select detection position 1 - - LORInAxialAndNoArcCorrSinogramCoordinates lorB; - LORInAxialAndNoArcCorrSinogramCoordinates lorC; - int Cring1, Cring2, Cdet1, Cdet2; - Bin bin; -// Det<> pos1(0,0,0); - set_tolerance(10E-4); - for (int i=0; iget_num_detectors_per_ring();i++){ - - Cring1=0;Cdet1=i; - Cring2=0;Cdet2=scannerCyl_ptr->get_num_detectors_per_ring()/2+Cdet1; - if(Cdet2>=scannerCyl_ptr->get_num_detectors_per_ring()) - Cdet2=Cdet1-scannerCyl_ptr->get_num_detectors_per_ring()/2; - - proj_data_info_cyl_ptr->get_bin_for_det_pair(bin, Cdet1,Cring1,Cdet2,Cring2); - proj_data_info_cyl_ptr->get_LOR(lorC,bin); - - proj_data_info_blocks_ptr->get_bin_for_det_pair(bin, Cdet1,Cring1,Cdet2,Cring2); - proj_data_info_blocks_ptr->get_LOR(lorB,bin); - - check_if_equal(0., lorC.s(),std::to_string(i)+ " Cylinder get_s() should be zero when the LOR passes at the center of the scanner"); - check_if_equal(0., lorB.s(),std::to_string(i)+ " Blocks get_s() should be zero when the LOR passes at the center of the scanner"); +ProjDataInfoTests::run_lor_get_s_test() +{ + CPUTimer timer; + auto scannerBlocks_ptr = std::make_shared(Scanner::SAFIRDualRingPrototype); + scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing() + * scannerBlocks_ptr->get_num_axial_crystals_per_block()); + scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing() + * scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); + + scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); + scannerBlocks_ptr->set_up(); + + auto scannerCyl_ptr = std::make_shared(Scanner::SAFIRDualRingPrototype); + scannerCyl_ptr->set_axial_block_spacing(scannerCyl_ptr->get_axial_crystal_spacing() + * scannerCyl_ptr->get_num_axial_crystals_per_block()); + scannerCyl_ptr->set_transaxial_block_spacing(scannerCyl_ptr->get_transaxial_crystal_spacing() + * scannerCyl_ptr->get_num_transaxial_crystals_per_block()); + + scannerCyl_ptr->set_scanner_geometry("Cylindrical"); + scannerCyl_ptr->set_up(); + + auto proj_data_info_blocks_ptr = std::make_shared(); + proj_data_info_blocks_ptr = set_blocks_projdata_info(scannerBlocks_ptr); + + auto proj_data_info_cyl_ptr = std::make_shared(); + proj_data_info_cyl_ptr = set_blocks_projdata_info(scannerCyl_ptr); + // select detection position 1 + + LORInAxialAndNoArcCorrSinogramCoordinates lorB; + LORInAxialAndNoArcCorrSinogramCoordinates lorC; + int Cring1, Cring2, Cdet1, Cdet2; + Bin bin; + // Det<> pos1(0,0,0); + set_tolerance(10E-4); + for (int i = 0; i < scannerCyl_ptr->get_num_detectors_per_ring(); i++) + { + + Cring1 = 0; + Cdet1 = i; + Cring2 = 0; + Cdet2 = scannerCyl_ptr->get_num_detectors_per_ring() / 2 + Cdet1; + if (Cdet2 >= scannerCyl_ptr->get_num_detectors_per_ring()) + Cdet2 = Cdet1 - scannerCyl_ptr->get_num_detectors_per_ring() / 2; + + proj_data_info_cyl_ptr->get_bin_for_det_pair(bin, Cdet1, Cring1, Cdet2, Cring2); + proj_data_info_cyl_ptr->get_LOR(lorC, bin); + + proj_data_info_blocks_ptr->get_bin_for_det_pair(bin, Cdet1, Cring1, Cdet2, Cring2); + proj_data_info_blocks_ptr->get_LOR(lorB, bin); + + check_if_equal(0., lorC.s(), + std::to_string(i) + " Cylinder get_s() should be zero when the LOR passes at the center of the scanner"); + check_if_equal(0., lorB.s(), + std::to_string(i) + " Blocks get_s() should be zero when the LOR passes at the center of the scanner"); } - -// Check get_s() (for BlocksOnCylindrical) when the line is at a given angle. We consider two blocks at a relative angle of 90 degrees -// the angle covered by the detectors is 120 (each block is 30 degrees and the detector are 4 blocks apart). The following LOR will be -// obtained by increasing detID1 and decreasing detID2 so that they are always parallel. -// - -// Let's calculate the relative ID difference between block1 and block2 in coincidence -// num_det_per_ring:2PI=det_id_diff:PI/2 - int det_id_diff=scannerBlocks_ptr->get_num_detectors_per_ring()*(_PI/2)/(2*_PI); - int Ctb=scannerCyl_ptr->get_num_transaxial_crystals_per_block(); - float transaxial_crystal_spacing=scannerBlocks_ptr->get_transaxial_crystal_spacing(); - float prev_s=0; - float prev_phi=0; - for (int i=0; iget_num_transaxial_crystals_per_block();i++){ - - Cring1=0;Cdet1=i+2*Ctb; - Cring2=0;Cdet2=2*Ctb+det_id_diff+Ctb-1 -i; - - proj_data_info_blocks_ptr->get_bin_for_det_pair(bin, Cdet1,Cring1,Cdet2,Cring1); - proj_data_info_blocks_ptr->get_LOR(lorB,bin); - /*float R=block_trans_spacing*(sin(_PI*5/12)+sin(_PI/4)+sin(_PI/12)); - float s=R*cos(_PI/3)+ - transaxial_crystal_spacing/2*sin(_PI/4)+ - (i)*transaxial_crystal_spacing*sin(_PI/4);*/ - - float s_step=transaxial_crystal_spacing*sin(_PI/4); - -// the following fails at the moment -// check_if_equal(s, lorB.s(),std::to_string(i)+ " Blocks get_s() is different"); -// the first value we expect to be different - set_tolerance(10E-3); - if (i>0){ - check_if_equal(s_step, lorB.s()-prev_s,std::to_string(i)+ " Blocks get_s() the step is different"); - check_if_equal(0.F, lorB.phi()-prev_phi, " Blocks get_phi() should be always the same as we are considering parallel LORs"); + + // Check get_s() (for BlocksOnCylindrical) when the line is at a given angle. We consider two blocks at a relative angle of + // 90 degrees the angle covered by the detectors is 120 (each block is 30 degrees and the detector are 4 blocks apart). The + // following LOR will be obtained by increasing detID1 and decreasing detID2 so that they are always parallel. + // + + // Let's calculate the relative ID difference between block1 and block2 in coincidence + // num_det_per_ring:2PI=det_id_diff:PI/2 + int det_id_diff = scannerBlocks_ptr->get_num_detectors_per_ring() / 4; + int Ctb = scannerCyl_ptr->get_num_transaxial_crystals_per_block(); + float transaxial_crystal_spacing = scannerBlocks_ptr->get_transaxial_crystal_spacing(); + float prev_s = 0; + float prev_phi = 0; + for (int i = 0; i < scannerCyl_ptr->get_num_transaxial_crystals_per_block(); i++) + { + + Cring1 = 0; + Cdet1 = i + 2 * Ctb; + Cring2 = 0; + Cdet2 = 2 * Ctb + det_id_diff + Ctb - 1 - i; + + proj_data_info_blocks_ptr->get_bin_for_det_pair(bin, Cdet1, Cring1, Cdet2, Cring1); + proj_data_info_blocks_ptr->get_LOR(lorB, bin); + /*float R=block_trans_spacing*(sin(_PI*5/12)+sin(_PI/4)+sin(_PI/12)); + float s=R*cos(_PI/3)+ + transaxial_crystal_spacing/2*sin(_PI/4)+ + (i)*transaxial_crystal_spacing*sin(_PI/4);*/ + + float s_step = transaxial_crystal_spacing * sin(_PI / 4); + + // the following fails at the moment + // check_if_equal(s, lorB.s(),std::to_string(i)+ " Blocks get_s() is different"); + // the first value we expect to be different + set_tolerance(10E-3); + if (i > 0) + { + check_if_equal(s_step, lorB.s() - prev_s, std::to_string(i) + " Blocks get_s() the step is different"); + check_if_equal(0.F, lorB.phi() - prev_phi, + " Blocks get_phi() should be always the same as we are considering parallel LORs"); } - prev_s=lorB.s(); - prev_phi=lorB.phi(); - + prev_s = lorB.s(); + prev_phi = lorB.phi(); } } - /*! \ingroup test \brief Test class for ProjDataInfoCylindricalArcCorr */ -class ProjDataInfoCylindricalArcCorrTests: public ProjDataInfoCylindricalTests +class ProjDataInfoCylindricalArcCorrTests : public ProjDataInfoCylindricalTests { -public: +public: void run_tests(); }; - void ProjDataInfoCylindricalArcCorrTests::run_tests() -{ - - std::cerr << "-------- Testing ProjData Geometry --------\n"; - - std::cerr << "-------- Testing DOI for blocks --------\n"; - run_Blocks_DOI_test(); - std::cerr << "-------- Testing coordinates --------\n"; - run_lor_get_s_test(); - run_coordinate_test(); - run_coordinate_test_for_realistic_scanner(); - +{ + + std::cerr << "-------- Testing ProjData Geometry --------\n"; + + std::cerr << "-------- Testing DOI for blocks --------\n"; + run_Blocks_DOI_test(); + std::cerr << "-------- Testing coordinates --------\n"; + run_lor_get_s_test(); + run_coordinate_test(); + run_coordinate_test_for_realistic_scanner(); + cerr << "-------- Testing ProjDataInfoCylindricalArcCorr --------\n"; { // Test on the empty constructor - + ProjDataInfoCylindricalArcCorr ob1; - + // Test on set.* & get.* + constructor const float test_tangential_sampling = 1.5; - //const float test_azimuthal_angle_sampling = 10.1; - + // const float test_azimuthal_angle_sampling = 10.1; + ob1.set_tangential_sampling(test_tangential_sampling); // Set_azimuthal_angle_sampling // ob1.set_azimuthal_angle_sampling(test_azimuthal_angle_sampling); - - - check_if_equal( ob1.get_tangential_sampling(), test_tangential_sampling,"test on tangential_sampling"); - //check_if_zero( ob1.get_azimuthal_angle_sampling() - test_azimuthal_angle_sampling, " test on azimuthal_angle_sampling"); - + + check_if_equal(ob1.get_tangential_sampling(), test_tangential_sampling, "test on tangential_sampling"); + // check_if_zero( ob1.get_azimuthal_angle_sampling() - test_azimuthal_angle_sampling, " test on azimuthal_angle_sampling"); } { shared_ptr scanner_ptr(new Scanner(Scanner::E953)); - - VectorWithOffset num_axial_pos_per_segment(-1,1); - VectorWithOffset min_ring_diff(-1,1); - VectorWithOffset max_ring_diff(-1,1); + + VectorWithOffset num_axial_pos_per_segment(-1, 1); + VectorWithOffset min_ring_diff(-1, 1); + VectorWithOffset max_ring_diff(-1, 1); // simulate span=3 for segment 0, span=1 for segment 2 - num_axial_pos_per_segment[-1]=14; - num_axial_pos_per_segment[0]=31; - num_axial_pos_per_segment[1]=14; + num_axial_pos_per_segment[-1] = 14; + num_axial_pos_per_segment[0] = 31; + num_axial_pos_per_segment[1] = 14; // KT 28/11/2001 corrected typo (bug): min_ring_diff[-1] was initialised twice, and max_ring_diff[-1] wasn't min_ring_diff[-1] = max_ring_diff[-1] = -2; - min_ring_diff[ 0] = -1; max_ring_diff[ 0] = 1; + min_ring_diff[0] = -1; + max_ring_diff[0] = 1; min_ring_diff[+1] = max_ring_diff[+1] = +2; const int num_views = 96; const int num_tangential_poss = 128; - + const float bin_size = 1.2F; - - - //Test on the constructor - ProjDataInfoCylindricalArcCorr - ob2(scanner_ptr, bin_size, - num_axial_pos_per_segment, min_ring_diff, max_ring_diff, - num_views,num_tangential_poss); - - check_if_equal( ob2.get_tangential_sampling(), bin_size,"test on tangential_sampling"); - check_if_equal( ob2.get_azimuthal_angle_sampling() , _PI/num_views, " test on azimuthal_angle_sampling"); - check_if_equal( ob2.get_axial_sampling(1), scanner_ptr->get_ring_spacing(), "test on axial_sampling"); - check_if_equal( ob2.get_axial_sampling(0), scanner_ptr->get_ring_spacing()/2, "test on axial_sampling for segment0"); - + + // Test on the constructor + ProjDataInfoCylindricalArcCorr ob2(scanner_ptr, bin_size, num_axial_pos_per_segment, min_ring_diff, max_ring_diff, num_views, + num_tangential_poss); + + check_if_equal(ob2.get_tangential_sampling(), bin_size, "test on tangential_sampling"); + check_if_equal(ob2.get_azimuthal_angle_sampling(), _PI / num_views, " test on azimuthal_angle_sampling"); + check_if_equal(ob2.get_axial_sampling(1), scanner_ptr->get_ring_spacing(), "test on axial_sampling"); + check_if_equal(ob2.get_axial_sampling(0), scanner_ptr->get_ring_spacing() / 2, "test on axial_sampling for segment0"); + { // segment 0 - Bin bin(0,10,10,20); + Bin bin(0, 10, 10, 20); float theta = ob2.get_tantheta(bin); - float phi = ob2.get_phi(bin); + float phi = ob2.get_phi(bin); // Get t float t = ob2.get_t(bin); //! Get s float s = ob2.get_s(bin); - - check_if_equal( theta, 0.F,"test on get_tantheta, seg 0"); - check_if_equal( phi, 10*ob2.get_azimuthal_angle_sampling()+ ob2.get_azimuthal_angle_offset(), " get_phi , seg 0"); + + check_if_equal(theta, 0.F, "test on get_tantheta, seg 0"); + check_if_equal(phi, 10 * ob2.get_azimuthal_angle_sampling() + ob2.get_azimuthal_angle_offset(), " get_phi , seg 0"); // KT 25/10/2000 adjust to new convention - const float ax_pos_origin = - (ob2.get_min_axial_pos_num(0) + ob2.get_max_axial_pos_num(0))/2.F; - check_if_equal( t, (10-ax_pos_origin)*ob2.get_axial_sampling(0) , "get_t, seg 0"); - check_if_equal( s, 20*ob2.get_tangential_sampling() , "get_s, seg 0"); + const float ax_pos_origin = (ob2.get_min_axial_pos_num(0) + ob2.get_max_axial_pos_num(0)) / 2.F; + check_if_equal(t, (10 - ax_pos_origin) * ob2.get_axial_sampling(0), "get_t, seg 0"); + check_if_equal(s, 20 * ob2.get_tangential_sampling(), "get_s, seg 0"); } { // Segment 1 - Bin bin (1,10,10,20); + Bin bin(1, 10, 10, 20); float theta = ob2.get_tantheta(bin); - float phi = ob2.get_phi(bin); + float phi = ob2.get_phi(bin); // Get t float t = ob2.get_t(bin); // Get s float s = ob2.get_s(bin); - - float thetatest = 2*ob2.get_axial_sampling(1)/(2*sqrt(square(scanner_ptr->get_effective_ring_radius())-square(s))); - - check_if_equal( theta, thetatest,"test on get_tantheta, seg 1"); - check_if_equal( phi, 10*ob2.get_azimuthal_angle_sampling()+ ob2.get_azimuthal_angle_offset(), " get_phi , seg 1"); + + float thetatest = 2 * ob2.get_axial_sampling(1) / (2 * sqrt(square(scanner_ptr->get_effective_ring_radius()) - square(s))); + + check_if_equal(theta, thetatest, "test on get_tantheta, seg 1"); + check_if_equal(phi, 10 * ob2.get_azimuthal_angle_sampling() + ob2.get_azimuthal_angle_offset(), " get_phi , seg 1"); // KT 25/10/2000 adjust to new convention - const float ax_pos_origin = - (ob2.get_min_axial_pos_num(1) + ob2.get_max_axial_pos_num(1))/2.F; - check_if_equal( t, (10-ax_pos_origin)/sqrt(1+square(thetatest))*ob2.get_axial_sampling(1) , "get_t, seg 1"); - check_if_equal( s, 20*ob2.get_tangential_sampling() , "get_s, seg 1"); + const float ax_pos_origin = (ob2.get_min_axial_pos_num(1) + ob2.get_max_axial_pos_num(1)) / 2.F; + check_if_equal(t, (10 - ax_pos_origin) / sqrt(1 + square(thetatest)) * ob2.get_axial_sampling(1), "get_t, seg 1"); + check_if_equal(s, 20 * ob2.get_tangential_sampling(), "get_s, seg 1"); } #if 0 @@ -1123,316 +1012,281 @@ ProjDataInfoCylindricalArcCorrTests::run_tests() } #endif - - shared_ptr scanner_ptr(new Scanner(Scanner::E953)); + shared_ptr scanner_ptr(new Scanner(Scanner::E953)); cerr << "Tests with proj_data_info without mashing and axial compression\n\n"; - // Note: test without axial compression requires that all ring differences + // Note: test without axial compression requires that all ring differences // are in some segment, so use maximum ring difference shared_ptr proj_data_info_ptr( - ProjDataInfo::construct_proj_data_info(scanner_ptr, - /*span*/1, scanner_ptr->get_num_rings()-1, - /*views*/ scanner_ptr->get_num_detectors_per_ring()/2, - /*tang_pos*/64, - /*arc_corrected*/ true)); - test_cylindrical_proj_data_info(dynamic_cast(*proj_data_info_ptr)); + ProjDataInfo::construct_proj_data_info(scanner_ptr, + /*span*/ 1, scanner_ptr->get_num_rings() - 1, + /*views*/ scanner_ptr->get_num_detectors_per_ring() / 2, + /*tang_pos*/ 64, + /*arc_corrected*/ true)); + test_cylindrical_proj_data_info(dynamic_cast(*proj_data_info_ptr)); cerr << "\nTests with proj_data_info with mashing and axial compression (span 5)\n\n"; - proj_data_info_ptr = - ProjDataInfo::construct_proj_data_info(scanner_ptr, - /*span*/5, scanner_ptr->get_num_rings()-1, - /*views*/ scanner_ptr->get_num_detectors_per_ring()/2/8, - /*tang_pos*/64, - /*arc_corrected*/ true); - test_cylindrical_proj_data_info(dynamic_cast(*proj_data_info_ptr)); + proj_data_info_ptr = ProjDataInfo::construct_proj_data_info(scanner_ptr, + /*span*/ 5, scanner_ptr->get_num_rings() - 1, + /*views*/ scanner_ptr->get_num_detectors_per_ring() / 2 / 8, + /*tang_pos*/ 64, + /*arc_corrected*/ true); + test_cylindrical_proj_data_info(dynamic_cast(*proj_data_info_ptr)); cerr << "\nTests with proj_data_info with mashing and axial compression (span 4)\n\n"; - proj_data_info_ptr = - ProjDataInfo::construct_proj_data_info(scanner_ptr, - /*span*/4, scanner_ptr->get_num_rings() - 1, - /*views*/ scanner_ptr->get_num_detectors_per_ring() / 2 / 8, - /*tang_pos*/64, - /*arc_corrected*/ true); + proj_data_info_ptr = ProjDataInfo::construct_proj_data_info(scanner_ptr, + /*span*/ 4, scanner_ptr->get_num_rings() - 1, + /*views*/ scanner_ptr->get_num_detectors_per_ring() / 2 / 8, + /*tang_pos*/ 64, + /*arc_corrected*/ true); #if 0 // disabled to get noninteractive test michelogram(dynamic_cast(*proj_data_info_ptr)); cerr << endl; #endif - test_cylindrical_proj_data_info(dynamic_cast(*proj_data_info_ptr)); + test_cylindrical_proj_data_info(dynamic_cast(*proj_data_info_ptr)); } - /*! \ingroup test \brief Test class for ProjDataInfoCylindricalNoArcCorr */ -class ProjDataInfoCylindricalNoArcCorrTests: public ProjDataInfoCylindricalTests +class ProjDataInfoCylindricalNoArcCorrTests : public ProjDataInfoCylindricalTests { -public: +public: void run_tests(); + private: void test_proj_data_info(ProjDataInfoCylindricalNoArcCorr& proj_data_info); }; - void -ProjDataInfoCylindricalNoArcCorrTests:: -run_tests() -{ +ProjDataInfoCylindricalNoArcCorrTests::run_tests() +{ cerr << "\n-------- Testing ProjDataInfoCylindricalNoArcCorr --------\n"; shared_ptr scanner_ptr(new Scanner(Scanner::E953)); cerr << "Tests with proj_data_info without mashing and axial compression\n\n"; - // Note: test without axial compression requires that all ring differences + // Note: test without axial compression requires that all ring differences // are in some segment, so use maximum ring difference shared_ptr proj_data_info_ptr( - ProjDataInfo::construct_proj_data_info(scanner_ptr, - /*span*/1, scanner_ptr->get_num_rings()-1, - /*views*/ scanner_ptr->get_num_detectors_per_ring()/2, - /*tang_pos*/64, - /*arc_corrected*/ false)); - test_proj_data_info(dynamic_cast(*proj_data_info_ptr)); + ProjDataInfo::construct_proj_data_info(scanner_ptr, + /*span*/ 1, scanner_ptr->get_num_rings() - 1, + /*views*/ scanner_ptr->get_num_detectors_per_ring() / 2, + /*tang_pos*/ 64, + /*arc_corrected*/ false)); + test_proj_data_info(dynamic_cast(*proj_data_info_ptr)); cerr << "\nTests with proj_data_info with mashing and axial compression (span 5)\n\n"; - proj_data_info_ptr = - ProjDataInfo::construct_proj_data_info(scanner_ptr, - /*span*/5, scanner_ptr->get_num_rings()-1, - /*views*/ scanner_ptr->get_num_detectors_per_ring()/2/8, - /*tang_pos*/64, - /*arc_corrected*/ false); - test_proj_data_info(dynamic_cast(*proj_data_info_ptr)); + proj_data_info_ptr = ProjDataInfo::construct_proj_data_info(scanner_ptr, + /*span*/ 5, scanner_ptr->get_num_rings() - 1, + /*views*/ scanner_ptr->get_num_detectors_per_ring() / 2 / 8, + /*tang_pos*/ 64, + /*arc_corrected*/ false); + test_proj_data_info(dynamic_cast(*proj_data_info_ptr)); cerr << "\nTests with proj_data_info with mashing and axial compression (span 2)\n\n"; - proj_data_info_ptr = - ProjDataInfo::construct_proj_data_info(scanner_ptr, - /*span*/2, scanner_ptr->get_num_rings() - 7, - /*views*/ scanner_ptr->get_num_detectors_per_ring() / 2 / 8, - /*tang_pos*/64, - /*arc_corrected*/ false); - test_proj_data_info(dynamic_cast(*proj_data_info_ptr)); + proj_data_info_ptr = ProjDataInfo::construct_proj_data_info(scanner_ptr, + /*span*/ 2, scanner_ptr->get_num_rings() - 7, + /*views*/ scanner_ptr->get_num_detectors_per_ring() / 2 / 8, + /*tang_pos*/ 64, + /*arc_corrected*/ false); + test_proj_data_info(dynamic_cast(*proj_data_info_ptr)); } void -ProjDataInfoCylindricalNoArcCorrTests:: -test_proj_data_info(ProjDataInfoCylindricalNoArcCorr& proj_data_info) +ProjDataInfoCylindricalNoArcCorrTests::test_proj_data_info(ProjDataInfoCylindricalNoArcCorr& proj_data_info) { test_cylindrical_proj_data_info(proj_data_info); const int num_detectors = proj_data_info.get_scanner_ptr()->get_num_detectors_per_ring(); #ifndef TEST_ONLY_GET_BIN - if (proj_data_info.get_view_mashing_factor()==1) + if (proj_data_info.get_view_mashing_factor() == 1) { // these tests work only without mashing cerr << "\n\tTest code for sinogram <-> detector conversions."; - -#ifdef STIR_OPENMP - #pragma omp parallel for schedule(dynamic) -#endif + +# ifdef STIR_OPENMP +# pragma omp parallel for schedule(dynamic) +# endif for (int det_num_a = 0; det_num_a < num_detectors; det_num_a++) - for (int det_num_b = 0; det_num_b < num_detectors; det_num_b++) - { - int det1, det2; - bool positive_segment; + for (int det_num_b = 0; det_num_b < num_detectors; det_num_b++) + { + int det1, det2; + bool positive_segment; int tang_pos_num; int view; - - // skip case of equal detectors (as this is a singular LOR) - if (det_num_a == det_num_b) - continue; - - positive_segment = - proj_data_info.get_view_tangential_pos_num_for_det_num_pair(view, tang_pos_num, det_num_a, det_num_b); - proj_data_info.get_det_num_pair_for_view_tangential_pos_num(det1, det2, view, tang_pos_num); - if (!check((det_num_a == det1 && det_num_b == det2 && positive_segment) || - (det_num_a == det2 && det_num_b == det1 && !positive_segment))) - { -#ifdef STIR_OPENMP + + // skip case of equal detectors (as this is a singular LOR) + if (det_num_a == det_num_b) + continue; + + positive_segment + = proj_data_info.get_view_tangential_pos_num_for_det_num_pair(view, tang_pos_num, det_num_a, det_num_b); + proj_data_info.get_det_num_pair_for_view_tangential_pos_num(det1, det2, view, tang_pos_num); + if (!check((det_num_a == det1 && det_num_b == det2 && positive_segment) + || (det_num_a == det2 && det_num_b == det1 && !positive_segment))) + { +# ifdef STIR_OPENMP // add a pragma to avoid cerr output being jumbled up if there are any errors -#pragma omp critical(TESTPROJDATAINFO) -#endif - cerr << "Problem at det1 = " << det_num_a << ", det2 = " << det_num_b - << "\n dets -> sino -> dets gives new detector numbers " - << det1 << ", " << det2 << endl; - continue; - } - if (!check(view < num_detectors/2)) - { -#ifdef STIR_OPENMP +# pragma omp critical(TESTPROJDATAINFO) +# endif + cerr << "Problem at det1 = " << det_num_a << ", det2 = " << det_num_b + << "\n dets -> sino -> dets gives new detector numbers " << det1 << ", " << det2 << endl; + continue; + } + if (!check(view < num_detectors / 2)) + { +# ifdef STIR_OPENMP // add a pragma to avoid cerr output being jumbled up if there are any errors -#pragma omp critical(TESTPROJDATAINFO) -#endif - cerr << "Problem at det1 = " << det_num_a << ", det2 = " << det_num_b - << ":\n view is too big : " << view << endl; - } - if (!check(tang_pos_num < num_detectors/2 && tang_pos_num >= -(num_detectors/2))) - { -#ifdef STIR_OPENMP +# pragma omp critical(TESTPROJDATAINFO) +# endif + cerr << "Problem at det1 = " << det_num_a << ", det2 = " << det_num_b << ":\n view is too big : " << view + << endl; + } + if (!check(tang_pos_num < num_detectors / 2 && tang_pos_num >= -(num_detectors / 2))) + { +# ifdef STIR_OPENMP // add a pragma to avoid cerr output being jumbled up if there are any errors -#pragma omp critical(TESTPROJDATAINFO) -#endif - cerr << "Problem at det1 = " << det_num_a << ", det2 = " << det_num_b - << ":\n tang_pos_num is out of range : " << tang_pos_num << endl; - } - } // end of detectors_to_sinogram, sinogram_to_detector test - - -#ifdef STIR_OPENMP -#pragma omp parallel for -#endif - for (int view = 0; view < num_detectors/2; ++view) - for (int tang_pos_num = -(num_detectors/2)+1; tang_pos_num < num_detectors/2; ++tang_pos_num) - { - int new_tang_pos_num, new_view; - bool positive_segment; +# pragma omp critical(TESTPROJDATAINFO) +# endif + cerr << "Problem at det1 = " << det_num_a << ", det2 = " << det_num_b + << ":\n tang_pos_num is out of range : " << tang_pos_num << endl; + } + } // end of detectors_to_sinogram, sinogram_to_detector test + +# ifdef STIR_OPENMP +# pragma omp parallel for +# endif + for (int view = 0; view < num_detectors / 2; ++view) + for (int tang_pos_num = -(num_detectors / 2) + 1; tang_pos_num < num_detectors / 2; ++tang_pos_num) + { + int new_tang_pos_num, new_view; + bool positive_segment; int det_num_a; int det_num_b; - proj_data_info.get_det_num_pair_for_view_tangential_pos_num(det_num_a, det_num_b, view, tang_pos_num); - positive_segment = - proj_data_info.get_view_tangential_pos_num_for_det_num_pair(new_view, new_tang_pos_num, det_num_a, det_num_b); + proj_data_info.get_det_num_pair_for_view_tangential_pos_num(det_num_a, det_num_b, view, tang_pos_num); + positive_segment + = proj_data_info.get_view_tangential_pos_num_for_det_num_pair(new_view, new_tang_pos_num, det_num_a, det_num_b); - if (tang_pos_num != new_tang_pos_num || view != new_view || !positive_segment) - { -#ifdef STIR_OPENMP - // add a pragma to avoid cerr output being jumbled up if there are any errors -#pragma omp critical(TESTPROJDATAINFO) -#endif + if (tang_pos_num != new_tang_pos_num || view != new_view || !positive_segment) + { +# ifdef STIR_OPENMP + // add a pragma to avoid cerr output being jumbled up if there are any errors +# pragma omp critical(TESTPROJDATAINFO) +# endif { cerr << "Problem at view = " << view << ", tang_pos_num = " << tang_pos_num - << "\n sino -> dets -> sino gives new view, tang_pos_num :" - << new_view << ", " << new_tang_pos_num - << " with detector swapping " << positive_segment - << endl; + << "\n sino -> dets -> sino gives new view, tang_pos_num :" << new_view << ", " << new_tang_pos_num + << " with detector swapping " << positive_segment << endl; } - } - } // end of sinogram_to_detector, detectors_to_sinogram test - + } + } // end of sinogram_to_detector, detectors_to_sinogram test + } // end of tests that work only without mashing - if (proj_data_info.get_view_mashing_factor()==1 + if (proj_data_info.get_view_mashing_factor() == 1 && proj_data_info.get_max_ring_difference(0) == proj_data_info.get_min_ring_difference(0) && proj_data_info.get_max_ring_difference(1) == proj_data_info.get_min_ring_difference(1) - && proj_data_info.get_max_ring_difference(2) == proj_data_info.get_min_ring_difference(2) - ) + && proj_data_info.get_max_ring_difference(2) == proj_data_info.get_min_ring_difference(2)) { // these tests work only without mashing and axial compression - + cerr << "\n\tTest code for detector,ring -> bin and back conversions."; - + DetectionPositionPair<> det_pos_pair; - for (det_pos_pair.pos1().axial_coord() = 0; - det_pos_pair.pos1().axial_coord() <= 2; - det_pos_pair.pos1().axial_coord()++) - for (det_pos_pair.pos2().axial_coord() = 0; - det_pos_pair.pos2().axial_coord() <= 2; - det_pos_pair.pos2().axial_coord()++) -#ifdef STIR_OPENMP - // insert a parallel for here for testing. - // we do it at this level to avoid too much overhead for the thread creation, while still having enough jobs to do - // note: for-loop writing somewhat awkwardly as openmp needs int variables for the loop -#pragma omp parallel for firstprivate(det_pos_pair) -#endif - for (int tangential_coord1 = 0; - tangential_coord1 < num_detectors; - tangential_coord1++) - for (det_pos_pair.pos2().tangential_coord() = 0; - det_pos_pair.pos2().tangential_coord() < (unsigned)num_detectors; + for (det_pos_pair.pos1().axial_coord() = 0; det_pos_pair.pos1().axial_coord() <= 2; det_pos_pair.pos1().axial_coord()++) + for (det_pos_pair.pos2().axial_coord() = 0; det_pos_pair.pos2().axial_coord() <= 2; det_pos_pair.pos2().axial_coord()++) +# ifdef STIR_OPENMP + // insert a parallel for here for testing. + // we do it at this level to avoid too much overhead for the thread creation, while still having enough jobs to do + // note: for-loop writing somewhat awkwardly as openmp needs int variables for the loop +# pragma omp parallel for firstprivate(det_pos_pair) +# endif + for (int tangential_coord1 = 0; tangential_coord1 < num_detectors; tangential_coord1++) + for (det_pos_pair.pos2().tangential_coord() = 0; det_pos_pair.pos2().tangential_coord() < (unsigned)num_detectors; det_pos_pair.pos2().tangential_coord()++) - { + { // set from for-loop variable det_pos_pair.pos1().tangential_coord() = (unsigned)tangential_coord1; - // skip case of equal detector numbers (as this is either a singular LOR) - // or an LOR parallel to the scanner axis - if (det_pos_pair.pos1().tangential_coord() == det_pos_pair.pos2().tangential_coord()) - continue; - Bin bin; - DetectionPositionPair<> new_det_pos_pair; - const bool there_is_a_bin = - proj_data_info.get_bin_for_det_pos_pair(bin, det_pos_pair) == - Succeeded::yes; - if (there_is_a_bin) - proj_data_info.get_det_pos_pair_for_bin(new_det_pos_pair, bin); -#ifdef STIR_OPENMP - // add a pragma to avoid cerr output being jumbled up if there are any errors -#pragma omp critical(TESTPROJDATAINFO) -#endif - if (!check(there_is_a_bin, "checking if there is a bin for this det_pos_pair") || - !check(det_pos_pair == new_det_pos_pair, "checking if we round-trip to the same detection positions")) - { - cerr << "Problem at det1 = " << det_pos_pair.pos1().tangential_coord() - << ", det2 = " << det_pos_pair.pos2().tangential_coord() - << ", ring1 = " << det_pos_pair.pos1().axial_coord() - << ", ring2 = " << det_pos_pair.pos2().axial_coord() - << endl; - if (there_is_a_bin) - cerr << " dets,rings -> bin -> dets,rings, gives new numbers:\n\t" - << "det1 = " << new_det_pos_pair.pos1().tangential_coord() - - << ", det2 = " << new_det_pos_pair.pos2().tangential_coord() - << ", ring1 = " << new_det_pos_pair.pos1().axial_coord() - << ", ring2 = " << new_det_pos_pair.pos2().axial_coord() - << endl; - } - - } // end of get_bin_for_det_pos_pair and vice versa code + // skip case of equal detector numbers (as this is either a singular LOR) + // or an LOR parallel to the scanner axis + if (det_pos_pair.pos1().tangential_coord() == det_pos_pair.pos2().tangential_coord()) + continue; + Bin bin; + DetectionPositionPair<> new_det_pos_pair; + const bool there_is_a_bin = proj_data_info.get_bin_for_det_pos_pair(bin, det_pos_pair) == Succeeded::yes; + if (there_is_a_bin) + proj_data_info.get_det_pos_pair_for_bin(new_det_pos_pair, bin); +# ifdef STIR_OPENMP + // add a pragma to avoid cerr output being jumbled up if there are any errors +# pragma omp critical(TESTPROJDATAINFO) +# endif + if (!check(there_is_a_bin, "checking if there is a bin for this det_pos_pair") + || !check(det_pos_pair == new_det_pos_pair, "checking if we round-trip to the same detection positions")) + { + cerr << "Problem at det1 = " << det_pos_pair.pos1().tangential_coord() + << ", det2 = " << det_pos_pair.pos2().tangential_coord() + << ", ring1 = " << det_pos_pair.pos1().axial_coord() << ", ring2 = " << det_pos_pair.pos2().axial_coord() + << endl; + if (there_is_a_bin) + cerr << " dets,rings -> bin -> dets,rings, gives new numbers:\n\t" + << "det1 = " << new_det_pos_pair.pos1().tangential_coord() + + << ", det2 = " << new_det_pos_pair.pos2().tangential_coord() + << ", ring1 = " << new_det_pos_pair.pos1().axial_coord() + << ", ring2 = " << new_det_pos_pair.pos2().axial_coord() << endl; + } + + } // end of get_bin_for_det_pos_pair and vice versa code cerr << "\n\tTest code for bin -> detector,ring and back conversions. (This might take a while...)"; { - Bin bin; - // set value for comparison later on - bin.set_bin_value(0); - for (bin.segment_num() = max(-5,proj_data_info.get_min_segment_num()); - bin.segment_num() <= min(5,proj_data_info.get_max_segment_num()); - ++bin.segment_num()) - for (bin.axial_pos_num() = proj_data_info.get_min_axial_pos_num(bin.segment_num()); - bin.axial_pos_num() <= proj_data_info.get_max_axial_pos_num(bin.segment_num()); - ++bin.axial_pos_num()) -#ifdef STIR_OPENMP - // insert a parallel for here for testing. - // we do it at this level to avoid too much overhead for the thread creation, while still having enough jobs to do - // Note that the omp construct needs an int loop variable -#pragma omp parallel for firstprivate(bin) -#endif - for (int tangential_pos_num = -(num_detectors/2)+1; - tangential_pos_num < num_detectors/2; - ++tangential_pos_num) - for (bin.view_num() = 0; bin.view_num() < num_detectors/2; ++bin.view_num()) - { + Bin bin; + // set value for comparison later on + bin.set_bin_value(0); + for (bin.segment_num() = max(-5, proj_data_info.get_min_segment_num()); + bin.segment_num() <= min(5, proj_data_info.get_max_segment_num()); ++bin.segment_num()) + for (bin.axial_pos_num() = proj_data_info.get_min_axial_pos_num(bin.segment_num()); + bin.axial_pos_num() <= proj_data_info.get_max_axial_pos_num(bin.segment_num()); ++bin.axial_pos_num()) +# ifdef STIR_OPENMP + // insert a parallel for here for testing. + // we do it at this level to avoid too much overhead for the thread creation, while still having enough jobs to do + // Note that the omp construct needs an int loop variable +# pragma omp parallel for firstprivate(bin) +# endif + for (int tangential_pos_num = -(num_detectors / 2) + 1; tangential_pos_num < num_detectors / 2; ++tangential_pos_num) + for (bin.view_num() = 0; bin.view_num() < num_detectors / 2; ++bin.view_num()) + { // set from for-loop variable bin.tangential_pos_num() = tangential_pos_num; - Bin new_bin; - // set value for comparison with bin - new_bin.set_bin_value(0); - DetectionPositionPair<> det_pos_pair; - proj_data_info.get_det_pos_pair_for_bin(det_pos_pair, bin); - - const bool there_is_a_bin = - proj_data_info.get_bin_for_det_pos_pair(new_bin, - det_pos_pair) == - Succeeded::yes; -#ifdef STIR_OPENMP + Bin new_bin; + // set value for comparison with bin + new_bin.set_bin_value(0); + DetectionPositionPair<> det_pos_pair; + proj_data_info.get_det_pos_pair_for_bin(det_pos_pair, bin); + + const bool there_is_a_bin = proj_data_info.get_bin_for_det_pos_pair(new_bin, det_pos_pair) == Succeeded::yes; +# ifdef STIR_OPENMP // add a pragma to avoid cerr output being jumbled up if there are any errors -#pragma omp critical(TESTPROJDATAINFO) -#endif - if (!check(there_is_a_bin, "checking if there is a bin for this det_pos_pair") || - !check(bin == new_bin, "checking if we round-trip to the same bin")) - { - cerr << "Problem at segment = " << bin.segment_num() - << ", axial pos " << bin.axial_pos_num() - << ", view = " << bin.view_num() - << ", tangential_pos_num = " << bin.tangential_pos_num() << "\n"; - if (there_is_a_bin) - cerr << " bin -> dets -> bin, gives new numbers:\n\t" - << "segment = " << new_bin.segment_num() - << ", axial pos " << new_bin.axial_pos_num() - << ", view = " << new_bin.view_num() - << ", tangential_pos_num = " << new_bin.tangential_pos_num() - << endl; - } - - } // end of get_det_pos_pair_for_bin and back code +# pragma omp critical(TESTPROJDATAINFO) +# endif + if (!check(there_is_a_bin, "checking if there is a bin for this det_pos_pair") + || !check(bin == new_bin, "checking if we round-trip to the same bin")) + { + cerr << "Problem at segment = " << bin.segment_num() << ", axial pos " << bin.axial_pos_num() + << ", view = " << bin.view_num() << ", tangential_pos_num = " << bin.tangential_pos_num() << "\n"; + if (there_is_a_bin) + cerr << " bin -> dets -> bin, gives new numbers:\n\t" + << "segment = " << new_bin.segment_num() << ", axial pos " << new_bin.axial_pos_num() + << ", view = " << new_bin.view_num() << ", tangential_pos_num = " << new_bin.tangential_pos_num() + << endl; + } + + } // end of get_det_pos_pair_for_bin and back code } } // end of tests which require no mashing nor axial compression @@ -1442,114 +1296,99 @@ test_proj_data_info(ProjDataInfoCylindricalNoArcCorr& proj_data_info) Bin bin; // set value for comparison later on bin.set_bin_value(0); - std::vector > det_pos_pairs; -#ifdef STIR_OPENMP + std::vector> det_pos_pairs; +# ifdef STIR_OPENMP //#pragma omp parallel for schedule(dynamic) -#endif - for (bin.segment_num() = proj_data_info.get_min_segment_num(); - bin.segment_num() <= proj_data_info.get_max_segment_num(); - ++bin.segment_num()) +# endif + for (bin.segment_num() = proj_data_info.get_min_segment_num(); bin.segment_num() <= proj_data_info.get_max_segment_num(); + ++bin.segment_num()) for (bin.axial_pos_num() = proj_data_info.get_min_axial_pos_num(bin.segment_num()); - bin.axial_pos_num() <= proj_data_info.get_max_axial_pos_num(bin.segment_num()); - ++bin.axial_pos_num()) - for (bin.view_num() = proj_data_info.get_min_view_num(); - bin.view_num() <= proj_data_info.get_max_view_num(); - ++bin.view_num()) - for (bin.tangential_pos_num() = proj_data_info.get_min_tangential_pos_num(); - bin.tangential_pos_num() <= proj_data_info.get_max_tangential_pos_num(); - ++bin.tangential_pos_num()) - { - proj_data_info.get_all_det_pos_pairs_for_bin(det_pos_pairs, bin); - Bin new_bin; - // set value for comparison with bin - new_bin.set_bin_value(0); - for (std::vector >::const_iterator det_pos_pair_iter = det_pos_pairs.begin(); - det_pos_pair_iter != det_pos_pairs.end(); - ++det_pos_pair_iter) - { - const bool there_is_a_bin = - proj_data_info.get_bin_for_det_pos_pair(new_bin, - *det_pos_pair_iter) == - Succeeded::yes; - if (!check(there_is_a_bin, "checking if there is a bin for this det_pos_pair") || - !check(bin == new_bin, "checking if we round-trip to the same bin")) - { - cerr << "Problem at segment = " << bin.segment_num() - << ", axial pos " << bin.axial_pos_num() - << ", view = " << bin.view_num() - << ", tangential_pos_num = " << bin.tangential_pos_num() << "\n"; - if (there_is_a_bin) - cerr << " bin -> dets -> bin, gives new numbers:\n\t" - << "segment = " << new_bin.segment_num() - << ", axial pos " << new_bin.axial_pos_num() - << ", view = " << new_bin.view_num() - << ", tangential_pos_num = " << new_bin.tangential_pos_num() - << endl; - } - } // end of iteration of det_pos_pairs - } // end of loop over all bins - } // end of get_all_det_pairs_for_bin and back code -#endif //TEST_ONLY_GET_BIN + bin.axial_pos_num() <= proj_data_info.get_max_axial_pos_num(bin.segment_num()); ++bin.axial_pos_num()) + for (bin.view_num() = proj_data_info.get_min_view_num(); bin.view_num() <= proj_data_info.get_max_view_num(); + ++bin.view_num()) + for (bin.tangential_pos_num() = proj_data_info.get_min_tangential_pos_num(); + bin.tangential_pos_num() <= proj_data_info.get_max_tangential_pos_num(); ++bin.tangential_pos_num()) + { + proj_data_info.get_all_det_pos_pairs_for_bin(det_pos_pairs, bin); + Bin new_bin; + // set value for comparison with bin + new_bin.set_bin_value(0); + for (std::vector>::const_iterator det_pos_pair_iter = det_pos_pairs.begin(); + det_pos_pair_iter != det_pos_pairs.end(); ++det_pos_pair_iter) + { + const bool there_is_a_bin + = proj_data_info.get_bin_for_det_pos_pair(new_bin, *det_pos_pair_iter) == Succeeded::yes; + if (!check(there_is_a_bin, "checking if there is a bin for this det_pos_pair") + || !check(bin == new_bin, "checking if we round-trip to the same bin")) + { + cerr << "Problem at segment = " << bin.segment_num() << ", axial pos " << bin.axial_pos_num() + << ", view = " << bin.view_num() << ", tangential_pos_num = " << bin.tangential_pos_num() << "\n"; + if (there_is_a_bin) + cerr << " bin -> dets -> bin, gives new numbers:\n\t" + << "segment = " << new_bin.segment_num() << ", axial pos " << new_bin.axial_pos_num() + << ", view = " << new_bin.view_num() << ", tangential_pos_num = " << new_bin.tangential_pos_num() + << endl; + } + } // end of iteration of det_pos_pairs + } // end of loop over all bins + } // end of get_all_det_pairs_for_bin and back code +#endif // TEST_ONLY_GET_BIN { cerr << endl; cerr << "\tTesting find scanner coordinates given cartesian and vice versa." << endl; - { - const int num_detectors_per_ring = - proj_data_info.get_scanner_ptr()->get_num_detectors_per_ring(); - const int num_rings = - proj_data_info.get_scanner_ptr()->get_num_rings(); + { + const int num_detectors_per_ring = proj_data_info.get_scanner_ptr()->get_num_detectors_per_ring(); + const int num_rings = proj_data_info.get_scanner_ptr()->get_num_rings(); #ifdef STIR_OPENMP -#pragma omp parallel for schedule(dynamic) +# pragma omp parallel for schedule(dynamic) #endif - for ( int Ring_A = 0; Ring_A < num_rings; Ring_A+=num_rings/3) - for ( int Ring_B = 0; Ring_B < num_rings; Ring_B+=num_rings/3) - for ( int det1 =0; det1 < num_detectors_per_ring; ++det1) - for ( int det2 =0; det2 < num_detectors_per_ring; ++det2) - { - if (det1==det2) - continue; - CartesianCoordinate3D coord_1; - CartesianCoordinate3D coord_2; - - proj_data_info.find_cartesian_coordinates_given_scanner_coordinates (coord_1,coord_2, - Ring_A,Ring_B, - det1,det2); - - const CartesianCoordinate3D coord_1_new = coord_1 + (coord_2-coord_1)*5; - const CartesianCoordinate3D coord_2_new = coord_1 + (coord_2-coord_1)*2; - - int det1_f, det2_f,ring1_f, ring2_f; - - check(proj_data_info.find_scanner_coordinates_given_cartesian_coordinates(det1_f, det2_f, ring1_f, ring2_f, - coord_1_new, coord_2_new) == - Succeeded::yes); - if (det1_f == det1 && Ring_A == ring1_f) - { - check_if_equal( det1_f, det1, "test on det1"); - check_if_equal( Ring_A, ring1_f, "test on ring1"); - check_if_equal( det2_f, det2, "test on det2"); - check_if_equal( Ring_B, ring2_f, "test on ring1"); - } - else - { - check_if_equal( det2_f, det1, "test on det1"); - check_if_equal( Ring_B, ring1_f, "test on ring1"); - check_if_equal( det1_f, det2, "test on det2"); - check_if_equal( Ring_A, ring2_f, "test on ring1"); - } - } + for (int Ring_A = 0; Ring_A < num_rings; Ring_A += num_rings / 3) + for (int Ring_B = 0; Ring_B < num_rings; Ring_B += num_rings / 3) + for (int det1 = 0; det1 < num_detectors_per_ring; ++det1) + for (int det2 = 0; det2 < num_detectors_per_ring; ++det2) + { + if (det1 == det2) + continue; + CartesianCoordinate3D coord_1; + CartesianCoordinate3D coord_2; + + proj_data_info.find_cartesian_coordinates_given_scanner_coordinates(coord_1, coord_2, Ring_A, Ring_B, det1, det2); + + const CartesianCoordinate3D coord_1_new = coord_1 + (coord_2 - coord_1) * 5; + const CartesianCoordinate3D coord_2_new = coord_1 + (coord_2 - coord_1) * 2; + + int det1_f, det2_f, ring1_f, ring2_f; + + check(proj_data_info.find_scanner_coordinates_given_cartesian_coordinates(det1_f, det2_f, ring1_f, ring2_f, + coord_1_new, coord_2_new) + == Succeeded::yes); + if (det1_f == det1 && Ring_A == ring1_f) + { + check_if_equal(det1_f, det1, "test on det1"); + check_if_equal(Ring_A, ring1_f, "test on ring1"); + check_if_equal(det2_f, det2, "test on det2"); + check_if_equal(Ring_B, ring2_f, "test on ring1"); + } + else + { + check_if_equal(det2_f, det1, "test on det1"); + check_if_equal(Ring_B, ring1_f, "test on ring1"); + check_if_equal(det1_f, det2, "test on det2"); + check_if_equal(Ring_A, ring2_f, "test on ring1"); + } + } } } } END_NAMESPACE_STIR - USING_NAMESPACE_STIR -int main() +int +main() { set_default_num_threads(); From 535c5ce910d75c4228bc372f0f3a3babe71e4597 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Sat, 19 Feb 2022 10:21:08 +0000 Subject: [PATCH 81/81] update release notes for block feature [ci skip] --- documentation/release_5.0.htm | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/documentation/release_5.0.htm b/documentation/release_5.0.htm index cfa760382..3ed9befe6 100644 --- a/documentation/release_5.0.htm +++ b/documentation/release_5.0.htm @@ -15,6 +15,17 @@

    Overall summary

    uAV~X;wh*1!v$i`M)dAFO=C0eWWyFlYcjMg8qtIm7lx6 z+0Ne4bL#9l`Tpd}%;Cxb`hJ$ZkaaQt4fUAY=4gBako1-W3vDSZFoBEcVRrIb^vChO=5%VBy@e&v*Pt27u z#v)>I*t$U*)`H4c$gBrhU-DA1cCXrse#kh?gA+bdw5N%fC?=KYRsD#)-E{hW-}BIj zcSjcg!~)K1N=uT$&#WJ3v)M6Vp*;GW|DWSE#>UviKCCjAd^x*3EA4||Dn4Sld)-)% z;(fZ<`{e&C*?8seTO5gxbH;?OYsp?jcYtF;=2{H*2J69)w|bb z+EsUDD|Ok^mBrO^t9%#I=VtYd1>#g;1X=juLs92Jwf~P%QP=76J*BT#ldqCDp3#37 z*6)1Xh`y~x*GV6SUZL$W*c%^ye=fXKdIuYYpG+ki0AgLL$hTu{ADF+lBo<{mk=M&V z)iao{qRVLOUj9z!)9RY6KacQ(e){_*Y1W~_M6~B~v5!x->9)?xKSthnuQqKz0QP9A z-Q%~Ymp@kipzY6zjo*;>2V@1~e=piNg zk-6xy`lrC5-%5Rn`Tu;e7FT|ZPw=rW{#2fb3vrHeH+r!4en>C(M|mv#hVl_NFOX*a zjCZE8->7FY`V@GuIxd&C%DC=N?`R`suUTX>>F}7c53_YWo!`BrCv))Q;U0J^bwvC- zk&fr7e;R#uVT;SrBh)im9kB*pr*8d-Jo$6vih3i44pBDd<#X^L{8i{oT0i{h=!p3T zkcWL9LdQJV1Am12Bd(5-?|yhK`3q#5iPP?dBF5&=si%;K&FbKFWSM@yg>T3A$G-)C z3+CJTyT8v!D*!gl|8MPLIy#%n_oZQj58z8+p7dC6GIWe*J${&Sy}Imi;6wGLFE~@( zE2UdUk`wUX1N+G{2#6W+UU`D{Ft4e{&Q4=Lh-u)M9n5 z!1LpLe-z_f0O5D`HM!oW$h)nwE6C@_w-Q*}@-y#l*!X4qF~%j{H;UhC%6}(2mG3F# z_+h-GTA41-8StfWrp`qAZs_UCMOs;1?}O(QYjpVSeC4ko+X3*M>@qTT%=cF@t}CR6 zFV7%bgbv>kSF$(B_Lo2GRZwqPTlMHTk3SMVN!|(M!+|-H-={aCL&vaBYrqn79!^1< z{QJtc8a*2f#18rJ?d?+b@-m(cO-D(uw;s*BAV`R1cv1}Q%F6El|7Vsn-35`4_l3@%F_ zq94)M5p{0wP5;Q8j-{Z)*0tW)8`_J+Byo&-N3PYU;g`^ocq@F4m*vbh%L%IZ$_ zDfX4&3**VF_&Rj>PuOW`(0rb)c^xThnQ7ku89S;`^Xa21})HgkLW2BJ?fj4ta*7 zr^D9zY&)`uebIi{qDS5}O|`wp0b~QfYW#Ha_23+Q#Qo5_x!JhTwM3^LIQkZQo(vy0 zoghzKSCUV|KWmIO*RPf2Jx#T9&{W<9((jXgJUCZ>mf^#WG0vmq-I{C$dLlej`C;H< z^bqhL=+*Kbiw?bd!OP{HMBb+CJ7f{-+O=2FR)xKHmgn*fvSX!p%XcpRYv|YDq3~L= zucE{6qpyq68QE|UF)#zMzU^t=eF8*GUo77cHr)+9SH8#}dMDbQFKu^yYR3&`m;=*!-nDe!OxMmfh^XAxH9t0x4tpg z57*v4pjE!X+KpJg9$k=s864x-s(ko&$U2O}Z0&{odF|b)O!RRR6gGw@Hk*y&iB~;$eQH~KPbU{@fG!D z=t1b{_a4o)@m$(eTjwGc#=ICV&t$k1HrN28@evajEny1~^$Z3rpq0D;*g*gABnlC`3z zqhtI#;K|af=-F`ib%mdWuSH#B@axEXhp)7uQ}Tw$2Qn~7 zdE=Pde_azfcv?SV&WHU&zmQdy)ZQ((okSug~{9y~x-iy9RzvT3n&aswFjlsYmZC%{mt5{0g#bK*Y4W;qYtrjQwTI`RojN*6Z`>_*g^2 ze((B0BYHCVTzIs4qn-iyNnkwsd9a1@1K~%VrIgWOvw_-*vazlm2)+UqlRrQnYiJ*Q zs>~#Kdwc^OX2B6d$99ToY!p7Q0)HrZ?8CHMapslZ1Dr|LpuR7uW2JswCjD;Z7Hcze z|2_gA`aB31=xF}r&nfvqww1D@r1e%d?r&mF?2R7*V(d!h$Gg`MX_LvqkIn*9!PfHW zZ~j|OJJ8vEPtu+a(VHS&ial=LxT{&B_H z{_^waM)}Txdv|$PgM2YM=Gh8m!oCAxW17dnZ<`-GY4aKST`cW9`3J+(;C0&iBD`LG zmB(t&8J6I!`}zILVChZL1^|1Uq(8{C^X@glI8^z}0`g7i`yuttht-|!N_Xp6`VJW# zN_(2u?eY!r9_n1OKJpEa=P~V-@G;+B?#yFvx&%F0-rmweK8`%>V~%Cl!5f3vTTYT^ zk+iti=+Lhh;CiqLD3C`T=6ZgHH;5nIP9E_!{0vTFz5J~_WqQuS$N55x@nPtQnZ_!Q z->G~%ciLN#)k$yt|80<+D-VB9ACqTq@bAt}cBj{8wDWB+l5C=F)WhYixU*e*C0ZTlan`U&NKL^To!o zzwtAF3VZ190rGA_{_a($-l})RzwHd|bonl%|NEuuN8t+j3+O$%_$V8+bS0G^;<$UZs$4)Y3Q(nPQ0 zTaxNb@B+HGKJn}Sd}SLugFn{T-Kx#A;E3n1s59ck=Zf}7>=<*uRoRI1Gtr-x-pGGm z((gFeJeT}>GWN`4{m0nrQM|Q3c^Ezc9snZNBIW;;xu`RLN1^^2b~)M_{XV+InaoF( z{gW$OYt8+M`Y%Ykov^MRUo7qL^&Colu%cbi2DSCxGzk>Ne zdL7Ki^Jfsxvr#7hUh@(m^_5K%E z%NmZt&UL|x%u5tmaku|3~+$DG+;U1Rw5hvjjfTJEV4f|YeB3QOR|_l+ZQXB#P6Z(qmCW%2ZC|v zpUB(5PsFCI8Q)*|c4c=*-^@1m!eVf~K2Brvq*!}ac%^dBfYK#Rp0eWB5jVN1KX~h3U#4u5Rmlz8`u- z-fwFAT=0TEJpm7fUyyz*9j=u3AbEaTtn3!Q1=6NJlgNh16Z?}>^wqr0!iO92`$~I4 z+8fGmLDt}zz`pwMWpp?BAt3g`=2?74mWo~Z|G|#<^8pb1wykP@nctIrUMv`9{9dF_ z_^17L^(pv0T!n+_dXwjFxA5cB7_ z;wE|OwX;UK&}R(ZdYSjXLB4U;soT~48SmOO(BbQ3)};7tn)tng{C@hR4P`#-JNHlN z>j%kqCVK=7)!uBrBF3lI(`u~U$437D#x=lq?$)8owd%d2n3RuF=kJY=^`*itX(9TA zkGRlOtnG=~J8Q9lzs35LT}A#5*(06FitCiWM;|J{-YUOud0pCh@>n_D3@D`med_ zd#TgCQksF^8~dT&{~O3Z_+{3r{weCdME}mhf1s#u;A*l=dwb|#dy_Su?1|27!j^}M z4d})C9QUgA^3=<7lKh*Hcgs6K+H7zz-k4`w!?($E7KpKp_&H8Lrjv_H@vcO61NwW? zVom%*SJESHb$E7M^6p97<>vEQfNlQjiyo3(PE%UF8sD6PT{C4B55=8=tTuARxmTJttK=FoU{ z=nbdl+#<5rR~Dpitc~v!#cuXks*O&0PQ!oR|Npj@E&Da!@2$UM*0%#}->*a*a4(cUt6GE}-Bi2xiS@4&|7CW} zlrN)qg5QJ>BYRriVo~zA{R-U+AJOjb*#2W^e0G_&?2BZ7>+74w&)QcyMH_c?)%HX& z_apu;q3Z)^^RUW3sdyBzBlfOf4sQ)g%AmLQ=V|YG_lT!TpV^tOQ?XyFd=bxA@-xl9wGE=auh?S@ zp_>?!nUj^nv}YexHU9BC0F`a|$H(odhIi+C?ZV#DKV#g^vsd1%qW|fMKY)RZC<;z*pmF;tcxe&Uoqvsl6p5$v} z!TS1B`QD&U#D%cKV7d>HUI(*P>{sJid#2v_R=8}Pig$I>SCx5P{JT}V?uqmKe`nSI zpXwjeb8z8DHi7M(j3&B9n^~rBEH4Dld-pmIbeU8 zKP$bOuG8uM1{e-vO^xelzOZYTcNf(2QF>ad^WV3;0G*Kk3JfqmcV+Wg8_MJb|9AgX zj7Qej8ss+_^0?%DBF=~^N3wx+GC6_H$zzodUCRbb={E!%PZnc5H~I}H?vqE6W%%L2 z^M@*ElXp3p^RMy*dYe;;IZ%z*zrFiEaXCLNdX#ER@Xv2ZGSwlRL7nZ95szKnkzU15t7 z{r_)Wi^Odj_T?Vx9P%`WfC;$4~P@~l+G{ws?Z`pjMQ0b|r)Y!5|=vhyvHRLh>BNm2io;v2@hq6ofLJwKk zXAr&ZA?tr&|7DFVyx)GPVr_hsEh29I6I|<}U!U$yCLCms94h?=o7$6A2X!Z7 z3-(CIJ1;oM9^@Z&t+W?BOZ(qwPRbX;?--Zu_0v8nzU|6?vws`=rZe%C#ibVyO~rb; zrJ#;uaCmcFne zF?5{_FUH5&K}Ei!;8Ns)3v3X!=xZINe>}5@_h{mqyX;c@5p45sU`PF^*h9wI@*-vb z-q&pR>z}1>opYt_{SL-Vec1@u!zcO>zv-M->HU4rtMx7WEj!$Z|8!UV&*Qg&=ICSW zqaz+{3zz9VBy?`W6fe)>8( z-KpG&V)Ay_AmZQ-WIb>@y&J&@`lfK%xDP>3K*!qHj<)BH{}-O^)8(6CW8T81Kd1k* z>OWZh$H1S~-geU5uclvxFW0|4eAl|Ke1FTU`dCtTr#Ac3Rh!xI>UWM;5c~3XCsO?w z#b>{x5BBrfu3#v7Bt3p1@5&|FJ7I%}dl3&~EohVW1l}1?9v|<7Q@Wcs@f;xQP4BDK zbE5KLlTVmq<)D>kk8C6SAB#zq@0C9&PPmthcmA@QYwO2^2k?KREA*NE6=S>;9ruH+ zf9ZZ~-R0bXFXYcd>-p)!#;B9tp;wc>UJ3?_8<*>^v!ut^Ad7S7InrmqV?g*sn{m%$ z1U=WYO&K3?qFa0Y=+sa7f!gd=U%mVhD<9R)E7F&l2h+42=em39(?RlEH%6aip1!2* zTiw$SQSN_m6~1Lo9&3ze=!2NNeyC@b;$OB{y~pb3cI?oH4?5e-#_LBsNVDe0 zw?3(}$8=?}F`ogm9%H?_XR`w3a54QI3| ze@pL|euZ}5VuMa}+@F<{qyL00^pS6rJPWCgRJJeqUwe(A-}`+Z+Rnard46%AvkPl; z(x8o1+DweeQC+opK3w1XH7D5#x{hYq< zh(829Ss(l38_-v5_#ah|k-b6p`HSFRoSac(6CwGfRqS!Z@Co;;fh z$9#JV?yp~m!I!~j!;i5~*y-Zt^6T*)3V%w%&o=|3jnzWsLjP_!)`_+BSqv_c?=0nJ zI%~Kd?YqCxpsdD@2I{TTk_|gY1H*eFjwC_PfL26A6LQ;fEfE&8)l#re*T>P996W;8M|29 zRxBxh&^s9UIoL9Jx25xnCAE9#+~?(=hF?Q&Zs&IBj}|BMXSuuS%hh=EGdWqiH{d_3 zt-0zKAM$TV2C8Eay07)}DC2gIJo{TGkEH+a#qRLc7n+k5VY|O@rAMl#S$SvU9kKtl z?nGRQxLCQdn7@OQzgy9t{1k9Eh&cXXIOQK=VEI}!o8;$cA7d9j;XUDWV>YP7{0N^J zM$eV>$&^{3{2pXe@Ud?UpNKure)1lGeqH($co%%DJj=af6~1>j`#c1Poosdn`}jk$P6(BN>eSJ8OFv5qg-r#ay8=IS>h9!%ACtMu@dSJ6Mx{}(&G z_eyrOd=cYbCf^R<2#+JPmZtw7clQEr*K{?Ef8|C5aoi(5SS2M0J=JJa zw{EJWn(8U4sj4m!LbsnORx}{YN|nyYRayLrY)MHnv&}OH`eBT`t@ty zr*ALs*Z$Y@%roX3bIdWv7<0@q=bCHnbt2wwhwkZa@dZu1tH;;UgY|tb+z-%&-G;)S zhW0LSBAk9I`x?`)(BW$J3!9ROAK9z+Kj8nG4eV#~UBp)Nr{&vh`03^%E~WUy`CfV} zJ`d?*0(dNV1Y0eTe@uTPz_I#15^UnH9pH}5#ue`2ru@x_2NA#GH^u9tkN0TVA)5|% zpjjL30Qs0#VpqO{_C481Jjg@OThVOkxlYCW-wPO12VTb)dep1|HhFy=5AKzJ=!R3AR_hgs7G5mS(&*0Ms-|@;DgD3FQ*=Qb??@9lU zjP!nx^`gH$wN(Bv{WehdXIFM8y}sIAJlikX4_4>!A!|bM-GW>7v%5BBKDFpGSu6mHReYGr=FjtrnlA$PdPQ0iRtXZ{Ef@l!uH=+MGM& zFZtgLuhMNT^xo6>uWOyA;$AKvOy5>^i*?{D=D_RCNYt$dy6?~3m!ymE+(CJ(=Gybp z8TdSi#=MXBk_uhKmEt#?=cAjguB|)&M%cg{E%wmjTJc@wyR>a-sKw3s@>9@?k!kod zzsSn?h>O`;-~j0+I*RedZ#c*6Z=L4ay+!!SSzWdFG#-ab$=1dO)`i;p=DzRPk4_!x zCVD?*S9-SSYmo8>S5A%%r0~NP>>PHzR^OfcW^;T_$MbNwpR>b5+6|ZPFz23to2>3a zbIACmH_C@EM$9gOVoEj?{CacRZf&fPtFXfaeXOB_af{!I#JFY=&t7=no*&gFmyfyg z8oYUv4W-lf;m2wF6@0#@-4XBe4_~|vZfi6*ks1{F%hd1xi=<_Z- z@1pMqSDpUq@TurC)}DRPM_l_B-SEy%#lyK_aj*L`upRwiG(UoWL|@+q>6`r0_1R9b2GTEhF9$aSpMo=|3pwVY86ZuT!oIDYH4FS)z9e6j zTB7d0Vr*hvD)yh|Wbs~1jN$We5xZg?-i>W~Oa0*^UWZ*`Z_q^EgTR;2#CjI7WKLsp zQ_O{BXfpmb8ZJ|}E4qo`YW<3{#n>+gV?Fpfn%J9%4d;nNm!Q2LZKm#eFydXvura!R z^tBcwpMf8u|AF#P$j7@5F}7pn2ZJTNa{5luhy8cb29}lM83rDOZlMR;7r)b}YcyZr zSD~%QkHcrCa+|bFTCF?)+(LhwO500(Gk#A~{3gA^XJcIB8Np87=|=IsGM_JEnRT$Z z?-&K%0=KSqn}G3q!Fp{TrbooeRb-g~H%Xq~759-5Q;tRxxZBZ%ukS3ec^dJwxX=t$ zALBRxkCoD^Qfp()4`LjbNHHEqpz&O{`2E^q`4*|7u4h;6-fj{&7cS&5heEdEJC0|t z`?mV)+g;OP0p8|p`a|Q=4{n_P+VKf_8?+nSl^yljh>7~V8jmO7;vT6-ckz9ZWFh(( zlMMYV_-obkoh0@BvGwtL3~sEnr#@TI-68*aQ<8pPe>2suhU-B8lr&qa;#0z-FS0n`O!qrNi)E48}TlC_Zz+hrlV|@lxowHkqDb zkIm37LOaj6+y)nYg-v6Xil@bIx6c62l#-^}+^Xn%jdB&__t_!zx(L6L^4@qK z$xc^*o8X&w*2ZGFzIKi_{CEaFA@ly=eDyP=yX6O}5BlA~3K;qhkd`*q%8@?|UDzUQ z6t*ZJy+Zza_+#~Xl=d_b_dIF4F7mU* zXt=%QQ~XxJ7ruq(B>2(ruS%=bO;Z>1AmZ%i_{~wC2_HPd7vgHrCWj0CQYn1MyesbQ zmMM2gF$RfrKiaWUsj;@!Mt#J(4d~E>{|I$$aDgww$NDFZ6?1Mm8K%guR<|$O7^m;5 z?*kY6jhF+*zt~H*vEdo~VI6g6qBFi(tW!5P*4}L@k+BYbg7y`-&?_;PQ^4rAsy^s{ zFCYFD^!F);?N_krN!Im<`3saIR#wp&*Wz4j79KY%N4qlKSIdvnrl0zYj73CiA$1CqFMZ64q#QL_Md>(Y_gGa=&g^`z@hkps|lIqpXSH{2iP45lz zQ>9asA5b6Tah}v0ZZQ~ZVffcnDN&vT=Hw{mrg9bC8BzEC)e1Mcu{M9El4-8GcDlB+ z`3}nASDE?*j|tjV)kQwW{04P>^;d$ASQXQtZN)u9|~WbE61Bb_VN!^J*z4EV6}+S<~ou>DDJ&fBvg>U&5d z)mQO}v2K%JtdC9PqyM+SMat|{*fe}U^x9c@UuldKvSx5A)vb{qAT5`VE6PjA8v2F} zLf(3~uuIq@RTuUtsVjjMa0M7PsRV6PEe;)_eT>~i<>)(j94h~abfy$M2E+H0*eB+7 z#Jp@GI9q-ANUpEk{lUfXhr)-yhJ2ff0kQWPr+g>7Y$d-OzK?b@w0jo5Ro>cE%&8&D zlcdL#Tfo_9E(N!f#>+2}5~)o7l0GZa#74fW+!sE0hi*X|aDaAKNgeRvLqWSr-9oT` zQ_UXNkjwo^=$Xa7{yq2^QtTZcQ};St_{O}hr2nz|)b<_p$?Cd`XA#A|Bi6BYH1DA; z!>^Pd1-G~Im)L|I3SHKN3m?hhM|UNa3-l54gzxSrg)MfMj+fe%XX4YQK6qWH+*4Xb zuj9ecb&k3PQqV?hUDZ%K=WJ4raa$sV{|C?Tqgmi#%D({lM~qhy!;M|BhOE^7ye6b% zcK?u(GmCZ3{LeO`!`n^OrSU!07=vYa8^7WnGv-?+-Hh)+O||`1%;TBp_R#)1ZCjMb zN+o^0-BpZn7PkJS{KIVjb9<#Pq8*E8#QHVb!WVNP#w%6c8tuN)QhEIp{KH1Gq>;+8 zPh6yKfpV2RWiaB!1a*malcku`)h)oNY(4B2A;{wCa~<#X_p%Ely{3{Iy{2Yk%u zI3swkE9(*er>l(rjmo?5ar=ev_w3{7#>1^9!zSPx`d>+3Ye%*no5mXQgnOgtdmXs& zyYHh5`DbWzURS}l_%6A=(%6?Yst=i4)Gb5PAN*e6v{|5h`uq4)x!oAYHBmmsKh<{~ zng#Mlqgw&SeZX+-PXs?FO;i7@^8Vem_aY|2Rg@zpepre)G?Xo_hksdqrX*Hezz?!9 zba4KYt?#V+O8Q-d-u+EB035&`JK9H`Cx0z|;j69WscOG7+(q=erMdQhOkY=jt^4@T zDECv}U|jaV_an+n+25K}c~t%>bffS+uDiBhyij==ox-pCNmcwmxTZA}b0ymV{$M<& z;rAN&P4c}d&BkvznVz8ACzL1S8RIgp%UI~+K)9vK9cU)Oi79b@kjMS{`jMKmnK2hm z3%&I55;;zWUn0Mb)T)1WD)t<$U9~gfGr`z9?G0~yvSZ~}!+l$MF#WI5r+rMwn7o4D z0`iXPPKs|igr0-sJJg+x=60~mm%b1@^c8-W^Z!4&@{`bvfPWIraPg*(^P*;FC)2#^ zv$Omq@MX6CD0mgxk4p#Ry%`vDW-^&(tNWdNAAPfX`CR3g)BG(vr8_UauN-Uoce-k^ z>PmU>BE~9Ia7ZK-f`cwxpE!a!@&xpI7ga8uHf~-HA1_?x;)dS?+Em#I*W}qpZ0t4ThPbc zIspw^7vFIRo87Mburx>A_racIzq7m8;}>)M4m4LQuMPi5SH4xmgYn80eSckD+-J?x zwo2FFF$G+u?lN_^C>!hIoMEi|XtZmT_vVMY$*;$5{n+CXbLTL_! z{VE=@#|huMw5jF?tHgl2;p)-Mr+X$XA4yk&QK4jZI5pg$h2OB=C1;(YO~icFi4 z=x4D}Uozi<=5qBXNV)!|DnFrqIbS_r-?82ffxiHZIc06h&d2*J%5i^R!Q(XLFDWk+ zOFu>j{!@1E+{J)_b`}MdNoY9s4B>do8%6n+nha4M&tLb%=`q)QrqP$3% z&t+GmTMw=Sq<8i^`F7*}BmFkw`y||Sa5VaN#G)&;ZDqf$$+rW%vC3M&{go$>ab5m- zg8y@Mm3Il9ou{v<`zu$=ww|^7pJN$cDsN+6JVn+;=)>;Ltg>723V*XF%+^-^DSX@y zuAn#nkNBKTQoe`GJJ96>`NQb3q%++w;=!Ttzf~S0{T7d5c+3FBnRw1rtk1D0i}+B& z?|v|$Cp+e!Wam2kb`#^S1*hR(4gONB2jiNHm{j;?Rl7yn=e{$&qvu7Q!zYi>2@~otUB+d?JCdf`j7PegOPk#%;*cZAzQqu??wD}{gln`LVxzjhLAnhc56i3 zPo`Ia7k1khp)ucz??kN8?rH5>_>!0~F0-CrukL!ZgQP{xNgD4?9Ic(XmVSCcvT~C4 z|C8%=`>DNZ@ub|3Pq0C@wco(+0q6au^iloA{J#v}nDYnfw}CzQWX#Fp?0Y5LzQ*c} zuFB^l9t2&CWopb~Jv6VADNVI|j4|>#e8Pt=MYA>@=2UT?y`OUIE36U4^KW)3?t6N} z?H%oW$IZCJ{|;>T-a!7WxEXdEp?#&>I7!Dg{ZF+2FWPwjIo{Ds_vZuVT55eQdpEJA z5BrMm=_LKEV&i-DAzl|gkjr=IuTk9s@@)og(p=g9Ci??1E#5aQ=K4_f8i;O+@)Y?6 z(kOYh$hJjywYrFnF)w1xnFzmWcP;+ea~HqicuRRPKX?I+y>jKF#%GE5ZVnn!%Ir+?fXqzC}z(!HMwpp2fFW2hw|cwDs^212#b$bMI_0 z{QX7HJcxa9sYK3A#4hhsE#njId^pZcif{UDt^Gf6)vd+!B zo3jPdqsr^UN9@vHYVF8+D&M7VaVz~~bNbt`!Q*1!-EfQWikJ{{Vv#m+cGp7p!Ek%4 zUjv?m$Bxalv%Bk*!#~8LY$U$X-o7fo*EsOa;y$(;?Z@qJ#Q*XL{xH>CK8qcSJ;py^ zfDf8x!;>ZFvcpCEY&&yf3$fubHrIbTp8aD?b{C&F)#t_Jx=s0GWF0`CBgLlP&84Tq z1`z{e-54sx8dzzpopViuuX`UimVTp@_tAbWyl2Ya3ReYh0_WH_{7~I*tbq&k^^33- z+2(coK97E{5xe$WkW4wBEdT851wOsGcQ`H~m;Oqze|mte<9(k)$?;usu}eBw{qAVj zQ%}$IU2=_Xs{2{kU>rSSyd(ZyE8hwZ2CHDK6Db(`#PIt!V>(By9Haj>{l@#3R~d&RMf|Jd~Rldt~F`FCjoDZbi zfwRam(|a$UB5SM*y_HAP=dQ+V$3kHfbIzF)I<^*&9!Lt5Tgte!0nY^Cn75A66a)csFcUm^DGtKY%qyu?}U{sLEM<;Ck}LeO`KBdC@+3?cBm1Bi^AYo|TAO#d{kuC&tm6?-Vv@6Dz|O z^R0X1@b-RGyl{#WyDZ;@bxY~D@SfDQW7HT+^IT&y#(r<$fbN7@>0qH^pRL;u(>^@bN8vMu>}#F^pj z{RErX7qt1GV;O&I{h2=g9j+t9Mtky%U&c3ss#_SJW#Ymg$#)~YMmLt`H?qM&p*Ovj z!u6FOFJGZA8^zgdoX-^dgQ{}S^^SE`Tom`0JjVuavca0s+}># z-s?Z*igU&z*&?pIq5PdKa{AFVVqFot)c2)pE1f357d`>U--~C2nS9nzG|x`NC-#W{ z*;m%1M_T(1?RIpGy-q7z#JHcV9QV)fut&`OKJ=X}zqqFVf418Hr}S%$_FLcZ@9={~ z;BM@2rnz!C{okVVC@`LTE|Tm8|9^4C`k&A(u7!>1Vez~7wdh@@-=*N^=p4RYmeP)b ztSDFH0~huRSQaOq5F6k6&#_Ay=L56pH&&{r$H+qV^7ez-AY%VAxUfyc$GK=?jhLZ4 z4h);Ls~;`DS}Mt}WP`w`_GKTzJMN|b9j`bGxshzgS+m?ru%M3f%~Q6 zeV2a&d-Qci8NWxb_%3sKEql0o=uPJI4D0!Y^w>pe?xKs36E!B+HF@^Q z4$s>cY)hX~thN5%`8$+sA0f-5WO4r%d*kdZ zd*mxd@>%}6j`FwY|1LQf(r44I_UFO|;p?Gmz&2?+K9Cyk6z(nMI^`BHY*pne5j#$E zf4ZZ7E_dI!y*@87FWkT9PqVH2w#44F1W&WPNf9nk4h>^AX zd-K2eO=+9^mpbx^3F%MJG>>$DZ~dFwm9GdJgwMy>*#xrY^o&?%tc!aC`|aZS{}?dh zL>-!A_<-}k;`wB6e$Z?Ve1bi`+?=F4=+j)t^;v%%9NSmiJ5}tbi+J-dutoJyKIc56 z6!!&-^%H#^@V=j(?>uvTGC4cT?;>+giQmS%2HD!|aCB4tYGDIy8^qeuBK&N^9d^MRO9$wU(RO{h(r^^(w^5^2&fU%Bqh050ImULz3-2EUQ zH@?@>Z!Z0U-uZBL5%~G0+Ovhh@DU&4{qxv6FM((KI8Tc+!}J+?t@L{`F)`lXPj6RV z&iB|q{(oxNyJS9Q?&aIBK=aSKwxI9j_&NV+b50W9k&=+0Lj zNUj5=!{JKoaS_?sY;pXCBkmJ=!S4@lC=HVjojd4z6d2cPXUds&le`1t%%C_=k8{*H zc($TDM!UDQ`vZ6e_#?LdHCsE=i*K`6ooUq_Le9Qj$;_+3UA6lMu8L>-#rqBHP%j29 z8--_YX?uJhBIjsOEGphBc$E&$jN`ZQ`DNX;XCJTdk-NpnDa~m|*kB)WC(=@D{xtGe z#fMlw>-fjB+P^I=p(h_I-p5#BU$hPXj`5&XDds@*moJw_0<`gYY;|I)$yDrjOWM7jgfa`Xpm=1-++s7tdU3--D$` z+!H6h9}v&S_)9XXGk+j#P;#~&d!V>Kx>tHw8iL0dZElc{GxpFm=;D2x*k|@uUvZZF zVSLUq=Kb(&QTHZ%f;Qe`_@T1<^zs%9lA$|!w%}YbBWvqpBfW3o+Yt5a^7mXji*uf{ z#&4-oajsF0_~Z<|%KwVrWyShGl>DQmQBwFv#r}Q+vNf>Jvt&KDt9oVFV0W@k zLem$V0p5W2$MV~vnbpXiaHGJ`sR4|&a3?#qaxS_{*bd*vjDI{!+LIi+>Gu+lkHr~cJdcmx zyk+;|AM^To-gTrb1FW62$K}qrSc|e>`juGV~P-)&bu}Kaa1+dN@jXV>}+Bub5c8E7V)Njo=@bTH%k< zpLtci3*VpVKMCFN8c4sF+2BXo$69)y`I0vm->fK!74b|u_spTS)0$yF_qSYaui^JG zJeHF|EKH9RZ|w`R+x2Z7_D)Ihj!t~5A{(uImUKJ4Z==XKmTy+~uJLCBX*a5!`^d3|89^TWz8>LUGf2NE5 z>{p*4Z@aop$#V$&j^H2Qj8CNx*$0#T5qjJYcbk~;5Sst)GN(EK!X zm#U4mbL0|Std(!TeL;S<_HXGop3&|EKUNC-a{az6AF*-+I{i-mE^Q)C98cca#_lF^ z^g}Bq}P8#yCi(wLM7ihI_8q_1=bJ=i{9 z(rh1u{}a;LhWp?(tk4_D6k=Q{cwsJ=s8 zsxGykvUXRuGOpIkY_sO1Bi_mKyyCB2*+F={ZH}LAuhC}Quf(rJCTmXd3^n|5Uv>@M zf2W_Y_c-~OhbPD{)aS>PC+mNhG)ntf;OuTY6`I&zr|4d_kK7RN!?e9hdKS%-(kJk{1kB)8O8hC_3C> z;}PTiMJcz3x)bb{W=hkgF-_>`zpj4Cl$jryI1qCr+Y3zm*57?ib}ao*2mA4%&DbFL z|M@k*p5Qs_$22xOQ+lGg_`jdo3*vy7kUys!b6_@kM{B>O6lY?w2AFrn?@Bgmtj%No zpYCdXJR3b(Q#Q%^OGkAT&sZ|!mt7{mfL$NP=NRn|0b`Arg~z$d1Ei7CY&5I+1sj#w zx6t`HZR31)sx$%rCc1BDoG+&H5%Q;zIqdL|`b$Cvb8av5pp{zZkCTYa=h_(Po2!-i(*1N?<)OY^Wh%z z0FMLpYwqM{Yxe{G*#*YB&d;;Sbh|*_{;%-WEzx#26?V_y;(ep62bu@;eFPZ}lODr! z6uLhcufAv|ve{-xt55(3ZrKjkJ$7rVIY`&RXoL zGrld|<{8)v?+KkrOK*FEh)=8a^OW`{ko7oqVpH*M;8}3>Io8Z5p zKEqEgF@F8%qkZ-u9D5}9NE4+Gb-72DV(pt}&7X*72mSVfi+%9pWYXTgpm?u5`x?3~ z<;R_iZDBlKhku<+F_$-&#K7V=ZenS&v;1$tHZsvExlkHstv($8+t3~+t${lqY`~+L zemBbx1>3u7`-MCB;^zFg58NDl*eclq%}n`7y&G$5S-boPS6O~CI(}dL{+_Lpu)ptG z6fyRA@<$wh4sHjw+0z=b9sWI)k5qTNKEto<`?Al#?b}tmKl-}-8F;KFYs8N``QZX} zF^-ds!@6+0O8eu{AoarMhw5AT-%peWH+fG_{d_*tCV#NHeiFLkzT$W2Z&EIG)O;p~ zCub4w!v7ACPsGuy<#Rfgr19htC;#gVFEeJ#;MPb(;fLZk4jiDoi~R9;gq$n2{gU!W zTE1#2PfizwF7|SyLd!s!_{!F-wni4dfe)Fi+u3Bg-weNwa&IYq6Sy_qFNa&8 zF4pSUOJ9i}--z>r;#t{PZ8zmRF+Nk#8|VBD`SJ36BO5FKA^Ab_;z#y*^$X#`-t(2e z(p7sFW$%<-0XLX@r@-}xiti{b zR-Ub#8mBSx8QXS7d$@`+`grfFxF0?pKH7(E2I^~F&_&Ah3yfXn8eslu*kwD^SIWowb+D&=;u_fq9p7sI!Y(bqX>Pr|1|`BeDx zef2BaL&|;#D*W`x)2bEGuj{6E5aOS$)`{g#KE% zpb5LKqrAKHFq(+16W|ufuLtf6f2RCAFxJWxOth<@`|8u6|;bA6^spYq|0Bf$nR zWEtSRgl}aL^KM3CUgsNIqr%6Q&}$(&aVHDk2z%|Q?JqlPHdqW!f{VEJywpqGncxr6 zz9gS$pGzIeo3Zb$pnX8`KjuCqANJjiZKlYt(k?@LT32yaS@4|=XD=1|qs-hc{H`zD zNGW1k#H($z9V``ou8#(DBI3i4##%f&3vDm;@X06ON65$ATB1ID`v~+y;rCVE1$?{F zci7nD*ZQneznStFWw>OZcCkNu4{eOiXVk9&cU2d5t&)eI7WZdu@Gq#3_%%aa89wZJ zk^DS$JJI_xb@WRvME98dW)lA^V#+3Pb@H)huK*uZHw$i!d_CL%^Em9h$Q)Woq6ytHm1;cM1}Vtuqu!fP9NK75Q7F_+LePsk=aazO=pkc5uV7UdB7-)&}CqSHW}1^gP;VJ4pR? za7)O3ocw8G+H~a@hi5x$e)x_!f0eSeAzL4O1>KftZa{l6xGDd7Odl~v+r=PjO0g!i zY2Sk8JKDS_A7c>X|DrOVEWY!AXJPlS>$Y$at5(Wa!2Q)_;CgU_`RTgK!#d@a2Wayioo7J~>Ou*d^wA5pRa5k2w?fOFxpg zUKaPKucChiubCkG7h@Lle7XD*xJ+MoMC>W{2(cE0eb~CVw+-E5J`crjqBfJIdbr`% zsdJ6FH6?d{npEy{Zm#d8Xk&hCM(!8+?zV6V`sn9ceY_^co}rJr>+lR8IYGL_xLOCY z9n^hZ+E{sWyuS+`Ej6RP1YPLgB8`<^?=0+6*@zq;(!!+A zpg&x>1ZGV&KMY?Ezv(HJ(dYD!E8>2c%x!2Jq!`z*UoP>@;u|1G*U)?{#)QI ze4f*Gi1I;AzJnk6?&3Txt#^NTh5Fd1nC}^XD87M;Uv|24r#J&YLH*E1V`IFr%xP^4T^t`@f zF5WFa7)=?hm(u%c`p4gtEn?hWQr}bE`t zP~0ibMt0P<`{C?Xx;vxF!oK2S@l7*(hlr_hPgCg78~!YP9)QldPx2G^apuqYc#M>P zTwnaShzX&;D~~gh33xw9-ZA#)Pc|o&bDg>TyerA8>JEgD^W(+%=IpW|-1T@xKik5O z2IuRy3r;-BoTrA}i~FS5gTz`Ex_+wDx+VWMT=@7rqA0-phM}(PHb(AKB`aU@|DgkGx;66%o}+7|J>PPW92)c?kKJc0LU=@;5fL-U=c+Idp!qnE?QoZ&Osl}+ZI`j`XaMG=RGspo6OdSrfQ z$1Ce2=2Gm}(oX%q1-}El7+t!KERFh&GrEV_HR96mJeRzj-izgjYBx-OcPaOx&wFTR z%A2RfZ%6)AfBE8>^3T=rt?FLrCm4r6HoJ$$>ozIQsm1={J3tey&s(4!pu7VZYj3&9 zZ_CLRe%%3oA=qy1_!(F+j!%MdzAzGP#H!dMEtA4$ei-qg58V$zSLQ38IR@{FcbF3Mz4#vjRWyI; z`UyY!gy#y4zLzk?{4s~(w+Lz2@G1Nzviqg<7$JYQ{z~MSVO&DL)Aao^DBi_7QyI=z zN094WwtS|m^z>Q#xIZD+dHTN6c$%lhdi^STwn}zWF40MS@tiKVR@{s(ct+eGto|qR zv4;@9({0GGxAI^zOagmJ_3Fkp6|t^(E_)YT3*2w`NMFyMFLmC!v1f1hX!}>LI8z@T z&l`Ii)92_rls$G{P<$68o53e8q5p@quafOfw%J}jV!(X#d-Abq`kYM$XZ*?c^?Nt_ znD_R8S@Dgih{v(+jYG4Ix`@$D%8~d=#N}cxT@G&?lY8MF!)HIdKh;(2hmy^~Pbd#+ zF8V2+4_cRt_fl4Y=56u2{N>u#)4_X#>5tH_L#9uAH)YUXNo$<3|F!EZ{7>>cWeI=z zzPJ$MpNhx5_{3a&?kP>dtCjw5f&I~MOvYX0_mqD?zn6e(%>6N)wY5I{*PQPczxhs^ zYVVoEUTq+{@b6(#KV|EEv47ecY~>Gkf)O)sSHFml_0fm+sX3U53-K&3Kg77~27kHq zD!PSiI9)#GoPAWXIbDrK@;usy*u%4;x=+(7NBiGg?dD7DTk`wB@6xqN935^>yl$SD zPw_u^(n;#X=3H!v@Bhc%EZ#qed&}bN$6A$n=97L)-KFxIHYe};{z04xWzV78mRxb) zb5vvPTw%2Qy6CL^#l6?qF1`=Qy3c&x=1qt)GvXA>EI z1~)~&Squl(;%njg=X z{?>~hWb9hJ3u!&6de14omzz(+d$r`5Z0j-jEwEl~sE;|uE2EElk2Icjd<^am@*217 zx8VIv+4A@f#x7(Q7mKqk>qv1wcXcEE#l+sqv1W`_UM;;#msh(Ae~Z0taqqO9xo`lv z9?%CFlZD2?oQN~!)cZ#H#p*6lw*((+dHNwTUP^{eygw>`82CT9va9uLpOC(d@5VKs zD%LpbX5B9M#opnlplL3?eV2{Vce}mNMdX6ZQ}y?#yBE!woz+dl59nB&kH1d-DmkTM zUo;lY8s#=A^lxse#mL`*o6%u8cpiKiT|fO;8?!UOqx2E$NIz?k`CjSmf9V*T^h$MN zSbQ%cn~7Fji|1>9&sDq^eG*tL*nUc_wz2ZjN{m_l-08Oxz0%qHilYd*H&qr+`~){}!BmP4WGND*q6FlcnZy z8+|vD>d?$k|9fdId2_YcyNqrwe&12%8}&z!Z;1a{HroH*@cwGK6!QL^mpxiC!I+-q zcct{te&8Lj@%Rm9`>?aR)I2HPNjZpeU2~gL#}~n=EC>Y?jN~+>bd_>_;s24!_2woyS)=Ze|z%$VZQN6g^}L3f_X>Kk2G|-W;$ei2q%f-_HJz z$$M`r@t$bs4V~`&Nyo%$D>ld5vLkG{_}A8r@_!wx5+`6pj- zUX?Dl_dblySFwG}0qb%_yo`54v)^a~8iRIF3IWqx3Ch>uKx_v#-e;n`~KE+w)l9{ZR4Fk@t#{yX22B7B}l-mb9mD zb6ia4&Te~2{?H%ItbdXIHA!oKtpnNb#b5i5)Og1`;+5U?eXlVovBenkhHs?5BCk0c zXVTTMK}CKjy%*ARCD;NUfma(k`>f(w1|Ls`vq#)p)Jft0-X)4}dldJx1GtjoL>%ZQaN3s7~rS1Rbbu(Kx(Yqq{KSKUXtsNPg@X699(thMLUd8tx=X91|3mb&~ z_w(Pa(GQ?s%=3sHqv#wlvW*T^u*6njyHU=Pjdi*Y8@$9Ho{_iLD<7+jHh#Mj_f)z4 zQE_JUzp4M9^tGg?@fb(09sRyhjLi6CA}+TYt7HrLNpz)I5f6-MaW6ZZ-VqCy(76nk zN#P@@axNb*=0nH(IQ!1{KU>L>Y_Ld7c-GqAW3Qxor2g!I{w=OC&RsS&?)DGm*h9T8 zHr>W2!auADao#(AweO)+=@vRqZY=iAlr)}F=sOy|UHy1%7;4V~kDQ(P?O zNWvdto`n4>bRWkKuHrrH@R$GaYvlpv!nt%ih>quhF&|=|xV!S3(rk9AIzQ`_k9iTX z;{RXQ#5Lc4u!sxA*=fYT&Do*b8u2)tpQUr`nZp00E{!qg6aVQ~#J4y*O!<4(nHRr% zkC-@-p4-vio}qX~oYFP+)uDSLm8F;mz17c$kIQ;mS)YymL#})uF>V!k_S?#r`O4c0SsmpJp$0ygkBT{Qn)V3!G8(gdX=B@00q3>qS_<+HY}s&|#H)0)K-1!(hS|9cYHhhke42Zr}rZvq9{qZ>!m%eq;Tu zk`FuoKf2QK?g>veCSqFEy=vU+l%3loukZoqO!*ix|A6lvK*p>yc|L5A(>MG*r{fdS zTl5?teVqQW7R-@WDZdVu=|9H)=v@2~KJ{92@f(!XJyCUk{lp$*w0?JL_J0__n0x=5 z7rC<^x@Wq|UNG*N=pDa_uWU~?XT6ne$>iR(JWP38W9Xjvxh2-dxpa%YQD3kc^PEj$ zK8%v)HP+tsj~IHI{490b!Ke8BkpAbGYwkmmYt4(#;d`QcO6TM$UsyHYnZa0flm9pD z;2y?VQtBSH!vC{2Hdtz`$q~PYjBiP%eaN?s@(Jwn5p{QyE9Q2UZtZkBUOEP@M9038 zu`X~I!^d-_gl&d_o3Zng+QnJi?h?C~UpCh^WJY#M8dEOHLt0H&5oLnF_gW81m6`GM%4Qeg|xYn<0OM{CeO}{e|F2_@6mfA#Q}Nq}!WIp5ABZNdcfoD7|6I%`d{0Erz7=Ok z;dA-PY;cSIjbSEpP!~K)4>?GgSy&J#%x{{VH_}^l(IIoUx`o~x# zpC^xV?c)Cm43eKq&R@~#0MJ=*yuY<-vikk$vjcq(1CQ#g?TKPPoJno;9tyWqz8<{Y zxe33^&Sb}J^|2{i{y_OeIydO!J^2UlT3?D-IF9~z>q9(>Zxh7#PUH8)17?8EOL*S z+BcAnp?CcM(4NY(l&_ZjmaX`0Pplmytp|T-^6d}rZuQqbY-+y6?}v-;&~C&A^PQbs zuP^Iy5i`zapHcF0t{Bfk4gw=4z1jT!d+C3tCBEpH(sX0Kl{oaM`S)OVGE*Pbh<|Yp z+nZc%(oZij^V-|B3-OJ z3*DyZ6NzudJ7w{#HQ$6z3%kS)^)tb5u|;pV%sM1C*EwHkA7cE6@{I$;&QE%mqEY$p zxjYX_omC{^|8ZXNQ~k7%^E~}5WP@AOJxI1o$#ER{9szH6u2#oyw?#XL9`sKa!*?{) z-i15?F1yEfAHetU+DM8$alLpE`-To_e1~@%=yjuZqt%aS(kJ*cbjQoT;W@`O;I(ic zW1o0_xnWmQ7Jp0DiSqg65`W^ovg|=~=_ll|jwQX||0h>}vhTx}Ue>OIzOe?xbA$8s z*MRR~_c`bA?mcP)~uKMu2BI_-kqr8jsTk^H(^I0(7nc7f3p8rM6@2NbTykF|7 z{SJ0VF)e(-JN*@FMCcapYGz+X_YS*F0qf9rbkxq$XDWXOz8SCm!QU$%R%=r{gWMC3 z>B>*5?`*359|}I{eJAh3ko6$){ZRcOaMqLRIc#ALr@xh+CFj_tWOS^f|J^mv+2|Ew z-r??p#MX44cJ?Ke$MASqAL4(0jDGCh;=Re7OvP`?{^0ke{owVHJi)iS$UdmCSnslx ze24E=uUB_4Sxh>Z<=U{_i@_qNY+>Baiv({cm1U)Y;YPLf8}Z!I5L^z|MDai zJXVM?;X7h&@vhYrV|5~1^ujk}I*UAC)Bo$zweo|^xfkX4ZSr23@_32NncpxL@0C5F zJzvRY!LRl^ygL5B1RSb-8k$xz@66w;=Kc))hvVZnuIZbd);#k3vLPAQ!?@fmug|m@ z{E@cqE7Glu`&;a?6J3uapBP;{*MC9Vx#a&c{STyzeQF*3a{KMScEvqN-2b#rwHB87 z{~Y|p-O{CaPnW(zho9k3-z?^9KV!U`vN0>(McK}HzXU%_-?zbwmlH#-+#2%&+7BCB zXG^*D(I(itm*CZ%#^3XjFMwyG$=GczZPO7R&-w;K6MkQTzZl%IIVoQU?iTIQug2>q zWVwgEP6e;h?h18cL&jDW>sPX#}ME9m`A_Z1ts*YS+@FJ0Ne_8XoXRrlvFBhb9X zzdhe6-p9B8=BG7_ar%Ews#9KU?|!p9IpTW+=~m>}Ci)kz?&K58;K-3qC0DF}DV@XT z#ejT)Hk-ij+?6ajACF=1VUu_#_Xc&Z@ddu0+{<=7(9Pu|@0nw5>eh&v?dXp*7cWA? zx00Unv46_dk70|K;RYE0g-uC5%r~MwPJjD_(jN3$!e^e~Pkrh0XICZ;CY~9^{}U;l zz*hBQg8fjQnNRcazfoGQkAN{3%)8<={2*CN z+bMe{tIY4xL2&D$_nvn0S@~sqCb<|{4%|*~pJAtC*z}IhWKJ(Khu@noc`xv2G^5R{ z$@mzP(!2UT5I*F%RNWBi0%d!Wd<5C2$-AE`^`*;>Y;Z9BFC_bRWc<^s70+(Iv)CFk z#Ms9?I6?a}(akcZu~)hrFY9*k%!9q-TTsa=b6Si`>6I*|XAg4TukNFg_?};2JbS6D z2d z#nrT3yVs-_(bGS(b{4;>n{6D1%E!D2pBSm`)u!TmoK<_rlDS^o$ERfe;EKKbIDBp+ zXNxx0lX%9NRPb--)8cbFhW&cNnV;F#^tcUv5_#?GDj49s>#KM&kT-?Qbn)pusU@Ra-pjmeb3{5jU1Q}wgD!~Hcs z`xu&8-h=)%Ubo^CV;*A>`|cSX=6$zwJbgvn*pRIDF=3nZ3G?@R@IQuM&|N&&s`8Wn zy6j(*ab*1!p1Y%&Cha0EZ{qXf(9wM2OY;5M;R^PgV0`XW-rIP8g#TD4v#+oVdu7+r zX>am8Z4ZJU+nKDI$u807ab(yFuMdMcxG8?q=yeRacBAJ&_=l9&mf~LQNqw0k@q47? zL&~$s^ubkDUK9!L1iXgmtKK`hleCE#eiUCmge{h8JFH1O0UxGI$bUmek{zksSDOdm zUa&9wcvI3EvEx$xg+JbBPJEQyXWJ_+W}j&Hbvpb;o%?|NE&MlcO0t{Ceg@oZ@G3gj zi@V}Xey98z{MOsk-h-Umu}jGEhQ8S~>4RU`U;&zQv^7TMu*Jz}!cXIVW4QVm(gd`4Q>HP_ws$<56FC1XKhbtJuBV^5CgJh zoyp2O$uL8`wXZaouI735T5Geoku6~t>qn`vDJj!C*$G# z1LI6D{HI0TT)YOr$9Pw?Hzq|aiNYi$;5X^G8fhAB20o zA!)mXjw`h13;9pcrtIflsCTUA$Lj0gpp#-w$Nkq}Gz-Wbe$B?k_^$#J_@U@hX@E3U zIbuQReF9nX2KI!DdGWfKvK|;VoiFX(SsP1S!oSj>3mcdtDVef!{Z3>E9{uV6jJoit z)t)17Nxr+#eL*a^1YIXS@qKFZD%~Ebyg@sIvzDo@mR_(AABUrOcM;ikw>!y{~B43lNb zi4=9i!7Ir7V=<+qKI~ADVm`eBjz$yq%(R;${iue^&6PCb#{}P5GS=B!^0U3ey_>P4 zNAVklUC4WSr}tSUKAPW0r(4HZ;bN1R@o0!{Mz@IF?gzu&N>O*)AKodIg^P$-3#^kULYmQhG@%=6NMZr(L z6@3Z+*e8vG-wb}Xa)XqrtL{mEbsgk79iMf(YHecoS0~U68Kp!zb=GjB<4A+5o+-H}-SQBFIFTnRCa6Ud_ zPu4E~`Nm>DR`|s{`4(+r|JMq(s@H$=6d3+p2RB$g!>=!RjsC5}@h)I-W_N{rtRpRG z8>9++t3IP_jVj)$i@AG}w1SPrsg_3@lk@C*s+Z7z6q?};;wL`i^>rw}-WlyGxQNwH z8{b3Gtki!`Wn-1aI^N5CxQV~4XZ{au^nN_tJ<<=w$@}TLyEIU`5dA!9KXv9{GMj86 zi};X?lRsJhS#|qLgXP2S9Uyn&_A!}8Bx-pJ5PrOfmsa!Trf27Z4 z?DS)8mWp#{!Ebh-|09cT8@Si)*N)ac_I7pZ*fhBS@6}SAO-6j-v*|i$>e(m(L#G7Y zdg^9&$io$W0hhtmt6u`nQyvdaky@pQEivY+v`Nv#8hD!WM6jaVh|g5z@UL9?C^B|7 z*6g)N8}^EQQ+8^n{|{~d!VjvS(smtfzKot-s;|pOe2F<*Z!Qd%U!UAx(e8M>hU0@r z-Vfc2Xr5OWaq=W|A=?FL2AGH6m+vc`#$Prk$JO$awXM+MaWspSBd(P2*hD#WioMDr zxFzxn!LZRBX_yrH<$;%9Ds?E2?yR*NukYAD#hfUiNsM~}7h`_~8vB#N?))>|mn^T6 zc_X?WugysHb$Gd2huX^z(QYOgcY|MwSF7>@{Ho^2CHT$v?aQ^r%Z=&9_e0kt*3ako z!5QM_ckzE$e}mQCrOhe$%tG@N_!wR-aPP@4)#nLlrpZr`>XlPzD%>J)3|!2Mui-sH zYJo2)hhD4Hhdn01mBBgEmS|$!r*xQO@E?<|MEg^1+{+Zd%Q!>1pLP7~F27GiHvo;; zP^`N+SNVDDg|m&|tzG#m{66d-_YD*2q?~-B)7smSESXHtHS*)x;RyA2fd{C+&3w8< z{M89q>Hgw)r-8}x28|T%XN&8H^ z!#|GGXN&`y;_RWp+?ubPk@2T!Vyu5Jt;F{W%JulRbvO@4Q!iEF3%>?>s}CErt6vY? zN%?rNg6>;nSl7Mxb8uHF8;iJ4&UP~<^07WuFI1kZew9ArzM+K;Vw_9jnpl$Wg>Eoh z#I>!o=NIV$bEdaG5_>B9pyK;_9c=NmcIEq$w%+*Dy`@eYzLy`@WG{uTg)RC=A7Urn zlWFo_0>`7<1n;;{wx=k*$@C3v;>>KJcEi-gT%4%i`QUca5^ewVQkV3D-$6PA{ddVQ z*4e{uc*ML3`!vCAWIyp4S~L;o@Pcpg;6^Hua2TW0$G;4;qfZgDo*8Oiz5 z^~&x?iZklZ;juoOIETx==-hQPK5;ADjbwQWuY|tN{L+*4b1eLsV%J0Pm#a5F@{91@ zoUEbWEV#JLzsgNyq+fDggE$G!Wy|x>jFE0rUa9S|@`sc89lmBQsk{KclXQoaj!4SO z+4B{=e@?!YwJ}e_4|XI+JcnJ4ZcDNFEqP-W?<^GG`Sc8?_?=TXx(}{jHi|F)$3(?h z+r!Qh-76ITBmI7QosM1{O5T$mL$|^h+C!!KPG44^csBSodJgrQGI1f_58Y|x-%h=K zQ1u)%-z3Wfaz3bTEPZamLoBaG%;BS}B2H*i7MHX6Xa_luW22I|RynjW8QK%xJc@gx ze26yI#gcg$zXLDzqhG9#Qdxs|8m#TWdZOQp`tb+@m z_|t2vX5&GJvh&yFo>TcKm01@T zx(E6;+!xsH6ZRY%8vmX2I~@OcbU#S`4sD;|3)jL~CsOBLNl7|H*|X=|`%7V$Y#%y2 z3w}o3pI@F+Cf>2g&UPkQ0-eVvgS^{QfiH_|JK$sf6=%{P0yoxo_|gLTOnTTD#oqC4 zvhS?Cy3uzxz<$cdbR_McZK~~i*Fv{8{a<5?*t^V>TF_plyp#NQ=$c5cvmIL&XUXen z`)RyqEB_9izN?eWfgi&US6)v)-<7vFD85IwpYlNR&EzNdG#B3q&f*yfJ7f##@hBTa z4EqaL8qZ(+R;GFipE;TSuLT{OU!=`1@ZGw(wnp41f2m~OANO|29`ff(Ym9-g7bFib)I~wvY(Qz1^9+Zx=X543fZ-$2$Q5mD{XZYRjVu^O8 z2Keyj{n6|V_ak|8B>xz{pD6xrZhT&WkLMRN@VgGanI4JfDi_0jlnsn~)i@@{b|-VT z*ehxK2wVKMtDbDT@B=X+d05-~wQHdFcG~c{;&49$o5-vjXuPIgn1 z`zCbcjlE3fektEc-68OYD9o6WoAkE)a`bcfeX&NG|2HcK&6m`ls9j3WwLtn7 z=VXh?6Z?|5N4y-qQ6Ddw7sjNx&wERqaftKucJH^woQ&_Egpa3hn&&5*57x2&?rN>7 zALM!4I(*;<{NXHo#EPt)y>sbj`18S7^TOXsYonh+-Zs8GflYQG!wcehr}~9_@gv|7 zok_Yg80P?=X!LDCdd9k1p~q10Q*>s-^5 zE&WbVcRhY*>rY*lp&Kf7!5^V~9X$>-2gLIv-lb`2Oh)%NzJJRVG0}6k^6n#rJ3i&T z>^z>gjc)cHlJS05KX>WJ`WVk-vfkiAJ{@}AAh+miP-CAOIij)u#vwa2r`FVROFRhQk(=IhFDA~rpU{~Pq)9*q6cJW%Y(7Mp|S zZ@deX?5n?R%+VL&pEnOqHIChq_YeNotDM>2nGcyc)$VtC=~Z}n7F)b8xF_3A$N$UL z;zo8^M4nT~!AFbzgY%SZyz;`P;=519GmnGi+vwLp|F~xk+B3-?wpaysiu~bH%*CaR z{K1^~4!-;IBmLz+#&4Rs1M#_#J%*Fza?o0x#~x)Fo$r=EN1jh4gUOil;W%?H!`~!* zivCly|DRm1^1Yq4yNhkF#Pe8od_upE;Wr=dI{J5z_ep$r2WJ=u{#dd1sHV+@-1${a zp1blBu`D|VT`zP~rM}X#PWuAsRQQ+&%h6;2Rx@;b(uZZmj0KC zIT8O;v&z23`=7_*+h6J;>y!GmKE^Y-c+Q(IH-`3&?Ux#l$#lGd9u+=s4_L&GG<-rn z?qM${e}#_kNaAVo5PZbX(czEp>v}xhb9^T2&zg}{;-+z*` zw5$JBbqzb%7nb68uz&BB>URwt!nPmwKEhmUe!R!9(l~t+{=%3a>LM=gh;}OZE+ot6 zl;eEq0y16*7jZHCydG{mSOI5(*Mjze@%{Nc*1tK*sq#ALt)Y4N_&RKPiE&;EMm_QoW4c?iI`4&B*v$ zy!)AaUBC7kS?E8yOY9*d-_IgW_LbI_|CPQ+i5tDq-YCD2oZ}jb`{yDSERpXG*Ff*s zcf0p3&U9iQdXo09No`%leQ-Wa{!KjGBgDJYSx-Fgm8W0o-PGh6hr;qe(SpU)259xQV{4mKqT($$7><3@3-)~8Klc_2u#9B3*z7Ye* z(K*)9VPI=#ZB5?^?L_qx;698t=13Jy0vdyCbNnOzvSY?K;+?r{u`!`@@+4na%X>5a ze@42(ez4j2%s^|r{^{56?8WdemGlwsFs!YQFBp&BY+_6*v&D(nQ~!=!z1kabVXq~v7On>B;kBSrP zk@sEWdA;&_bO@dLE64sY_5*dw3D_4rN-9hJ8pVF_E^v%_F&4e~pRC39;p6dMWSc%t zWrOGOwDuSOuWepa(sC)iukS9t0ktIlpGr&9ze4-}ljm8^Ge-O7$lcb9pRuc|KB-uohmJ*=dSdd0*(MHeEi$s9Ck_7=hlo^OMAlE zBjn5QIf349)B79U$)d^p(Yjrp#P;9+|GK*$FuTj@PT&^|5HP?XSs5V<7feI6sf;Yb zOeW}+rf!OnjutT@;+009s4PU*2P0`@ZL#_q>08|L%B~@G@iJ{o?Yw z!KZ5a+~$mN_4V<&J>nzA=s`X>dzN=(@x*N<`^k6dp0%JDEgre(b?y892gomAr%<-N+5vYa1! z<+EnyTsfbAv){hkc(3DMvDf@3{$Asoy1kFCjD5gwP29~7&j(n?)+e21zCxdG)Au{h zM&1*zI3Qg6v|^t8*f$e5 zoAbAs^DpIxFZ09C2itq&^TGZdbf_I5{-@F}ds}P?Tr^Ov>{TnzQrSa_UHR&@Y`kfp z{AMeAYPnae^^&bqBTmLV>#Lp6rQLzL+>r|6YC*W`r%% z*!UJUZenBB$n)e+liw(vIdHT58u>Z$g?ItQ5Aa5my|AI!ey6!`9osVY%h1Hhbm`;S z_ciDmyS%URD(OYinJ@pw;G^4p>${Q7EBjf-{(g3zZ?1oi&589%=&xPzVmv7zT+z;K)u7BD2;6CZ;Y*{|gJbD5j+$_Ck#&ooenFD>&*8DsVD8G4|BOM=P zEp76_B>REu)0|10k81bl&K?`J`}sllN7Q*9KzUYmy)yDMI&N^y@Up z$#-A*ezbm;_Oy>EIUW z@MC-O(@{Rl^>+Ec&R)*1_w&UJeK;o;x56!a;T~1vT6TM$T+2MPZWQJ7gS7J;z$NZq z{Iz&=%~o8;2gW1!mK!_C|DJD`F3|sb#mA8N-Hh(*xyNp<8;+u5u8j_aLml06KWVe{ zVs<7j3U3C=EuW$7i#M`K7CAnwtOK^zRn+&+swnO_~Z5ZUI1OM44udy z?l0s_{+ou}6Uc9DkGM}88?)}!rqRQC(0rr*K1R=swP&v7zWxEa-pC)GFPHB`M4<-z zat|eA9>3I-HKb>u@kz>#mp-Dut$uvvzFh6Tf%fms$=7l%GF871d%nIOTRm^fH~uv- zQN3lb(s--bbdRg&G;`xO1U2{bdRA)BeayQ1NA;}Be?I89VCbUX=1chBeYU*+RXo5C zi`ad+e8HAWJu|*mJTI0HWc{ze7GLZLRs0vLGlRW>_+u{nF4Sgxw@TTW+Oc00kGog* z3jCd=bMGZ<=b?033%~EW`at7#ux|rSM{~{c{{tU42Ip$ymbI0tZ~dwlviCj4z?xnB zF@3~k@ijW+K5%%S#VdjSIpfrfv3XK6pG!!b7s@i`WBuXyzqp8o*dat2iApFwPK|196Z z{sJ=oEjD+dk4;nYu7!_DACh+2vP$|vZ8o(XxQsuqUR!=M(0rxxit;}r8(-LWo4xdX zweox9KSAGj43_Vmj5$+P&L#J&z?f$^FIMj^wq6BK*Y{12u7v%WUd zdk#I9(X%^tDPbT{vayvqNak08nl}$Fke}rZpHQ1Lqko{pk-WHfSw46-t zKg?oNu0i+Zmoq)*-iJ=R;eQZrQ|38ba~OMS+IW`G*vT^s=bu`BXXkKj z+^yZ4_;;0dU+liI^|*WwYd)P`u51ZCD@V0yH&kZY ztM>FO-P^N9JS(_Wy<^xI>ik(eB>hQ!zR$RSP#?o|SZ(hgk*?t%sLzbkeCgQHr2mf4 zjgJS({4Loo{g;5BnSjF%EJJhuus{nmaI*5c{$le)={LaM9n5$A?R(wr=6C#CeW`fy&Qj&hncf?) z4&12TOl`(idK9^rk-1yY{eFGV^`6vY%B&ydd+cejPrAm2@em($=fDQ_?Q8izbtPmS zIY+(=BkCvi1K(73ud<8bRy=m)cXdk0oQt3MtjL=DhXdZha)zOMZ7<(VWxfunllibK zTf9e*@Aj+vcm}Xu*)u@X`6BnN8*iCaseh5&ayIY6#ws1^=7MuT>*Mi*zSrw_ch>^f z$uHp_?+@o&{ro;bd0xy|g7W`x3}~aDTp+slx69fL>292K>a_X(o}j&X`fkGbF8l0! zZT&<#@sjs8CZmai)9^R)$3y184f0~D_$l5sa35oOq&@Nkc#C~5&#RsWntkSt^HuY8 z&VqN7(Pr@(e#koW@3kZ5BQWdmerS)loVkB$Q3b+YgBYzb=?a@uoc`64= zR|YFJ*YTB|tYJSi#(7^M&nFJS+Y!w9yw6yCh%a_tXWt{gM1DHo&(W4RZ00< zw0yT#vT$xFG7bUjRB|CzS61rcFe^rNHC!6)93yH5lm7Mjjxm3Fg z)eEF-r=2%&V>)IvGgimH`+c!Z@}9spLM7q-3v26 z8hC$zt|R|CbHM$K#)1Av%Tjy(a=Zz!3qxeb#Pfe6_aeBM4{n6<&5K;0@?~w;wdMQO z<@=QlV{n+fy{#6zhV^-__8v#GzU{88N1J&cCpPxGm#~%`U;F|NCX@C12r?D@bm2z% z)#@ILZXgp_E}eE3kQ-Xl=G_o#Jl3X+e01$X=3XEE6nGHdn=j4tVv{l5LmXUyH>7O* z^;E_*et+qj_PKN;|EK?4|GfcbZCtM14R8^;Ke69L|F@G5%oYnTm0u*kB)oyPpEFm* zSNXk^F)in7&IG681wNzP*`wG`-7egSH&uCjK88PE`Xc8{v0W5+fwRcO*Q@A~c)C|U zDoM>d>5LDZ}~nlS({_WOqlmZCZZYB z@L!|dyqA6g-j!(1D;bM=%8k=$=Egx}9>SkZrUdDX?O8BB3M@cpkoy5ThTlSOK`Z<- z5zQEW74ES{yKqGNXQP43&?^Rv72e2b+YjNNgFg>0gtOo~XVL*CKKh__syutm7%(o% z8%MSEPTh+1u=djLLNw_LOr6ZHe)4xIi;Y!unS9Q(tCYP}`V745;33i=?-iEy@QZ2U z<9xCU@dEMfq0)bi?uz!RcZGENI0cS%`n_CilHl|PD~c<<`GD<5P%`nvLoOFmySr+ zV3RJ}M%9Ni$mpYtz4$(QkK)Cb<00d=r#2d7>PMO9WVT9|{zfw&Vn^a-f9YG%Z=+k$ z@AccWrC%w1GK>$*tJ0?zpiTATr;4()k+DBof6?o4{Mb1qW#gbwmRQZ1JT}cy9=ozG zEQB}7uYg+z+Wv?ij?iZ4k$9V|<5R7jiG^YEV=z8x$~Uzc-b!-u$1?2%rYlQKgg2t> zwbH|6C-BP#>6SXt^KN(sUQ1aY-o*p$?-SzdRd6$2#v=FQV{b3s#n!{@(bwQL@irRUk#Xk-&P}64Y84uM?z1SR`Q+_J>9vFYUOgo{UB^O)&LH_;l6ft&? z{d2W+&H%&8#7%LDbXDDoc1lPZB*vZzYT933x@U6~?X+m{`BlT*>_| z_h-uay?I>Q8K>BqvwscEy3mr&nd40KF8RAg+p*SFp^z_a!l)}>1}BhZN4aiP_c&j!l>_bdrd$4my8q4?XM8{DjFkHg ziR&%$pm>!rO$=|46lOD!i$YnnAjMGkuBpJT_S%~c>V1;IuS3hO5IYXoqg)xv<=d3>b-5p@@xF^slTTX*D%vKIO~E#U~S``=o2y8i74{t6Zm1NS-Hn;$esk<0&Hq8Dx0?W8wW1rH#^L_gqh5r_P?ke9@ zCy?@XlMg>IPda^c_lREQU6`#be$E_-ucB}4=9hd6SiTqCP@eG(6w+xgykgXz zMK(T3JB##}Yp~4YoU77j9eq;S7P8}^Yjf(QzvRcz=8<9=?+EE$?bOjgP5sEM)bC=v zMPve*8+BzH@j?R&$;8&gD@UpHgS<~?W8t79mc8rJI zN6FcxK|a1Xg-jPFsCNgPgtrMU?C6{E8c{F$b^KheyM6d+W3@Vg>(MjF{715r?f3sn zdYDYcV4L=KgfBWP{fNx_-9tPI9_*Z4As?UijOt(c#p<4FZ{J9+sx0T8ouvbpOV1~p zb6{YKbZm}Jf%$9N=frmIng1pZ)}<~QNd124)K9vlte{U|J>Dt$Uy6Q8zga_OD6c5b zHONnuZFisK0=l2Zo|f{(#*(hZT-QR2mEQ)tkTtmJe@uA3w&>&TwM}t$yhpu#$a}Vy zc~blj^1a%*d2P2=`~8q9cvIAmjt`@=$?Z=jaTZ<;9qRvOoo;Gay(h>Yk3Jnlr_7(& zJcQQeA4J0sWL?}MzpMVHhNqACE;=5d|G%Jnu4Q}id!TpuYHaH9dj>;ivhfaOHlRn2MHX0<=Mg zX9Lv%G1jS{wJiCZy++h;gl{dFhsL*==Yiemvq`##em%j~!l^>dOymzsSc)4A@w~#*&W}SP5eEgdj zUZl_Yu%XXM=nha5YsI(7pSiaE9K52=1ad2b(j{msu&R^K+SA&><_*fSHjam3`Cj~W zF#FdJ&DD?UYpwi~%J#w=vvxJ;aIN%D+#7$~|CM1^V;x&Ah0g#(d(5hA>-D_x>wNbC z{`w5r`1UdFF4WIg_49H#SG$SL_$7PAz50F%;P)ucJk40;3>EvkFm+ryyqp21lkLhh zk9z94^ipM)cj~-V|AEWohxGF|%5v{_Ej+R&YYhXXU)hoY8zMc=(`X1Wv@0Z`rblZL!n)jn~f1&nc^b+r!cve|^nfm9l z{TRGybcnxmZj8N!^pEIK?k)0J%lhQYeJUMvY)o<`{&@RyotyQ&G60o56U3kQkXbig z#yo1`??RuG@a^C2>j8Rx9q%Q^(Eir^5nOM)d82u9i3+2Cz zP9>W$nhRGBluiG$KdpYgCI7UbtPW?XThm77|Cgk1lg?g{^X>_B$Ti4U=yR>IW99iF z|0|@nh~9_7rF@ZZAjQG21i7dBTJ^+1F;|OT8_4Tf>8+{l8pC#w%6uVaB=(b#*7HmzYVttT$)i9jfn)#a8JF=!Noo zc?aXQbp3|(CH(LOIv>UlU!m*Y!8PQ59q{`R`F^^7j=l0cF=b9vJU_0z3eEgHPP;v5 z&SlvnoXzsPYn4;bJS)4Oj@|L^VMh;{F?0;=Lh5H8)Yu;wKSz>}A5J1;zb~)rZ zh`*MkzVpu2+FqyKJ~Az`tI;XOI?q8akiLu`&ZqC${+HOf)-dP1d;{F-oAs-|n>bzv z>4Kfj5A!1**Js){KcUY}cu&ssKj-N*M#q<+_tGPA-{<;#vsheCXF63TE6cd`DT^=u zH`;|>b*8lIl(Amx_-7H`B4t@$=FV)dt+}_9JzySw_Rg8o3)sEH`#?kHz@f%tIsS;> z)ZAPAp5mQ{dGx+sz26WlyhwW=77wS3FY~PYJ)U{=_@ABUAqVQ~5qoi!kMexvVCcHI zI2T?-?s59%Eb)G{6 zc)vOS=XkKWV$g4Vi(6-#BL9cyHx1um(?s^?`-Qw0eYJjHjJ{^DT#w3c_*=&RY;x~_ zZ=~lec(HWGE$iJBw1+Lj;vq4x3C$W&MQ_BfqjmYjOnf3={H^+8u(G?pnrzN>%^ZF2 zjJJ`@*kE~XDc>{tJU{f)@qRLQsP|kz)BggoSC+pyWh`n#csIqjRJ)Vj0t!%s$U5RGACrJll_pRh=c=PabF5E{O@xdzX=%f4}BHzS&f^9we zbWNAvKB_#*FDK3tE7HEJYUK>nudU}A_8hFVTw{;;&B^BN`hEmH#3w(X+mp`LxAI5! zrJ=#L4>q%D2ECTYKK91`KzOUH{TjF81iH{E*PG?{jo$NbTrgN^o=W#o_GO-D zJ&(;fFK$N*wq{NrMDO)v;umpNu8~>iGk0=d@3U+DmY-|BN%rWU8T&86Z)^7;{9OOt zGt>XTp-0x6-w}*$WUp^$M*Jp2eseGXf4SJ1PkcL&-;S$nVtM}owE33SpHqEUEX9*F06Gg)3!NZeoM8J_f6mKz2d**hvx&FXH0H3?w>YB zIhWnR2UpNJ@BVwf=xkTpV}kGAn{3*^=Gd2WQf$l_XfE7J?lE*Wzdb6wRr-GPaC`oJ z>~`KM=4;zoCErz)zlAr4?DG(coLl~Iu)UUVWJ6$ubL=Ph;0ZckF|+-fn+-PdMSlOW ze5W@4nTR({`BHNA{`TBxiJ=;w4dL&j-L(Be0X7u7cl53H*B;C7p-f=g{%pxTn5^AR zwtY?h3-XDF0H5T4hL>wah5ev>|F}%Ou>X8?yWa-N^+eYE)oAvBs`PX==S;Dh4>AX` Owyl!x*#BFw`u_oJ>f74@ literal 0 HcmV?d00001 diff --git a/examples/SAFIR_genericScanner/test.sh b/examples/SAFIR_genericScanner/test.sh new file mode 100755 index 000000000..517bb6cbf --- /dev/null +++ b/examples/SAFIR_genericScanner/test.sh @@ -0,0 +1,26 @@ +#!/bin/bash + + +lm_path= +project_path= +OSMAPOSL_path= + +# If you have built the STIR installation locally and do not have the methods +# in your PATH variable, then uncomment the path variables and adapt the path +# to your build folder +#build_path=/home//src +#lm_path=${build_path}/listmode_utilities/ +#project_path=${build_path}/utilities/ +#OSMAPOSL_path=${build_path}/iterative/OSMAPOSL/ + +# Creates a projdata file (.hs, .s) out of listmode data +${lm_path}lm_to_projdata lm_to_projdata.par &&\ + +# Creates an image out of the projdata +${project_path}back_project test_back test_generic_implementation_f1g1d0b0.hs template_image.hv &&\ + +# Creates projdata out of an image +${project_path}forward_project test_forward test_back.hv muppet.hs &&\ + +# Use the iterative algorithm OSMAPOSL to get an image out of the projdata +${OSMAPOSL_path}OSMAPOSL OSMAPOSL_QuadraticPrior.par diff --git a/src/IO/InterfileHeader.cxx b/src/IO/InterfileHeader.cxx index 445351d2d..b23f5ff6d 100644 --- a/src/IO/InterfileHeader.cxx +++ b/src/IO/InterfileHeader.cxx @@ -37,6 +37,7 @@ #include #include #include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" +#include "stir/ProjDataInfoGenericNoArcCorr.h" #ifndef STIR_NO_NAMESPACES using std::binary_function; @@ -572,7 +573,7 @@ InterfilePDFSHeader::InterfilePDFSHeader() KeyArgument::ASCII, &scanner_orientation); scanner_geometry = "None"; - add_key("Scanner geometry (BlocksOnCylindrical/Cylindrical)", + add_key("Scanner geometry (BlocksOnCylindrical/Cylindrical/Generic)", KeyArgument::ASCII, &scanner_geometry); axial_distance_between_crystals_in_cm = -1; @@ -591,6 +592,10 @@ InterfilePDFSHeader::InterfilePDFSHeader() add_key("distance between blocks in transaxial direction (cm)", &transaxial_distance_between_blocks_in_cm); // end of new keys for block geometry + //new keys for generic geometry + crystal_map = ""; + add_key("name of crystal map", &crystal_map); + //end of new keys for generic geometry add_key("end scanner parameters", KeyArgument::NONE, &KeyParser::do_nothing); @@ -1336,6 +1341,7 @@ bool InterfilePDFSHeader::post_processing() num_detector_layers, energy_resolution, reference_energy, + crystal_map, scanner_orientation, scanner_geometry, static_cast(axial_distance_between_crystals_in_cm*10.), @@ -1405,7 +1411,7 @@ bool InterfilePDFSHeader::post_processing() } } } - else // if block geometry + else if(scanner_geometry == "BlocksOnCylindrical")// if block geometry { data_info_ptr = new ProjDataInfoBlocksOnCylindricalNoArcCorr ( @@ -1425,6 +1431,26 @@ bool InterfilePDFSHeader::post_processing() data_info_ptr->get_sampling_in_s(Bin(0,0,0,0))/10.); } } + else // if generic geometry + { + data_info_ptr = + new ProjDataInfoGenericNoArcCorr ( + scanner_ptr_from_file, + sorted_num_rings_per_segment, + sorted_min_ring_diff, + sorted_max_ring_diff, + num_views,num_bins); + if (effective_central_bin_size_in_cm>0 && + fabs(effective_central_bin_size_in_cm - + data_info_ptr->get_sampling_in_s(Bin(0,0,0,0))/10.)>.01) + { + warning("Interfile warning: inconsistent effective_central_bin_size_in_cm\n" + "Value in header is %g while I expect %g from the inner ring radius etc\n" + "Ignoring value in header", + effective_central_bin_size_in_cm, + data_info_ptr->get_sampling_in_s(Bin(0,0,0,0))/10.); + } + } //cerr << data_info_ptr->parameter_info() << endl; return false; diff --git a/src/IO/interfile.cxx b/src/IO/interfile.cxx index 7fa231268..9e3088a09 100644 --- a/src/IO/interfile.cxx +++ b/src/IO/interfile.cxx @@ -57,6 +57,7 @@ #include #include #include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" +#include "stir/ProjDataInfoGenericNoArcCorr.h" #ifndef STIR_NO_NAMESPACES @@ -1064,6 +1065,56 @@ write_basic_interfile_PDFS_header(const string& header_file_name, << proj_data_info_ptr->get_sampling_in_s(Bin(0,0,0,0))/10. << endl; }// end of BlocksOnCylindrical scanner + else // generic scanner + { + const ProjDataInfoGeneric* proj_data_info_ptr = + dynamic_cast< const ProjDataInfoGeneric*> (pdfs.get_proj_data_info_ptr()); + + if (proj_data_info_ptr!=NULL) + { + output_header << "minimum ring difference per segment := "; + { + std::vector::const_iterator seg = segment_sequence.begin(); + output_header << "{ " << proj_data_info_ptr->get_min_ring_difference(*seg); + for (seg++; seg != segment_sequence.end(); seg++) + output_header << "," <get_min_ring_difference(*seg); + output_header << "}\n"; + } + + output_header << "maximum ring difference per segment := "; + { + std::vector::const_iterator seg = segment_sequence.begin(); + output_header << "{ " <get_max_ring_difference(*seg); + for (seg++; seg != segment_sequence.end(); seg++) + output_header << "," <get_max_ring_difference(*seg); + output_header << "}\n"; + } + + const Scanner& scanner = *proj_data_info_ptr->get_scanner_ptr(); + if (fabs(proj_data_info_ptr->get_ring_radius()- + scanner.get_effective_ring_radius()) > .1) + warning("write_basic_interfile_PDFS_header: inconsistent effective ring radius:\n" + "\tproj_data_info has %g, scanner has %g.\n" + "\tThis really should not happen and signifies a bug.\n" + "\tYou will have a problem reading this data back in.", + proj_data_info_ptr->get_ring_radius(), + scanner.get_effective_ring_radius()); + if (fabs(proj_data_info_ptr->get_ring_spacing()- + scanner.get_ring_spacing()) > .1) + warning("write_basic_interfile_PDFS_header: inconsistent ring spacing:\n" + "\tproj_data_info has %g, scanner has %g.\n" + "\tThis really should not happen and signifies a bug.\n" + "\tYou will have a problem reading this data back in.", + proj_data_info_ptr->get_ring_spacing(), + scanner.get_ring_spacing()); + + output_header << scanner.parameter_info(); + + output_header << "effective central bin size (cm) := " + << proj_data_info_ptr->get_sampling_in_s(Bin(0,0,0,0))/10. << endl; + + } + } // end generic scanner } diff --git a/src/buildblock/CMakeLists.txt b/src/buildblock/CMakeLists.txt index 63243571b..62e0ae934 100644 --- a/src/buildblock/CMakeLists.txt +++ b/src/buildblock/CMakeLists.txt @@ -77,6 +77,8 @@ set(${dir_LIB_SOURCES} GeometryBlocksOnCylindrical ProjDataInfoBlocksOnCylindrical ProjDataInfoBlocksOnCylindricalNoArcCorr + ProjDataInfoGeneric + ProjDataInfoGenericNoArcCorr ) if (NOT HAVE_SYSTEM_GETOPT) diff --git a/src/buildblock/ProjDataInfoGeneric.cxx b/src/buildblock/ProjDataInfoGeneric.cxx new file mode 100644 index 000000000..8d65e9502 --- /dev/null +++ b/src/buildblock/ProjDataInfoGeneric.cxx @@ -0,0 +1,514 @@ +/* + +*/ +/*! + + \file + \ingroup projdata + + \brief Non-inline implementations of stir::ProjDataInfoGeneric + +*/ + + +#include "stir/ProjDataInfoGeneric.h" +#include "stir/LORCoordinates.h" +#include +#ifdef BOOST_NO_STRINGSTREAM +#include +#else +#include +#endif + +#include "stir/round.h" +#include + +#ifndef STIR_NO_NAMESPACES +using std::min_element; +using std::max_element; +using std::min; +using std::max; +using std::swap; +using std::endl; +#endif + +START_NAMESPACE_STIR + +ProjDataInfoGeneric:: +ProjDataInfoGeneric() +{} + + +ProjDataInfoGeneric:: +ProjDataInfoGeneric(const shared_ptr& scanner_ptr, + const VectorWithOffset& num_axial_pos_per_segment, + const VectorWithOffset& min_ring_diff_v, + const VectorWithOffset& max_ring_diff_v, + const int num_views,const int num_tangential_poss) + :ProjDataInfo(scanner_ptr,num_axial_pos_per_segment, + num_views,num_tangential_poss), + min_ring_diff(min_ring_diff_v), + max_ring_diff(max_ring_diff_v) +{ + + azimuthal_angle_sampling = static_cast(_PI/num_views); + ring_radius.resize(0,0); + ring_radius[0] = get_scanner_ptr()->get_effective_ring_radius(); + ring_spacing= get_scanner_ptr()->get_ring_spacing() ; + + // TODO this info should probably be provided via the constructor, or at + // least by Scanner. + sampling_corresponds_to_physical_rings = + scanner_ptr->get_type() != Scanner::HiDAC; + + + assert(min_ring_diff.get_length() == max_ring_diff.get_length()); + assert(min_ring_diff.get_length() == num_axial_pos_per_segment.get_length()); + + // check min,max ring diff + { + for (int segment_num=get_min_segment_num(); segment_num<=get_max_segment_num(); ++segment_num) + if (min_ring_diff[segment_num]> max_ring_diff[segment_num]) + { + warning("ProjDataInfoGeneric: min_ring_difference %d is larger than max_ring_difference %d for segment %d. " + "Swapping them around", + min_ring_diff[segment_num], max_ring_diff[segment_num], segment_num); + swap(min_ring_diff[segment_num], max_ring_diff[segment_num]); + } + } + + initialise_ring_diff_arrays(); +} + +/* warning In cylindrical geometry m_offset is calculated based on ring_spacing, + then it is used to caculate ax_pos_num_offset and segment_axial_pos_to_ring1_plus_ring2. + For generic geometry, m_offset has been removed and the above mentioned variables are + calculated independant of m_offset.*/ +void +ProjDataInfoGeneric:: +initialise_ring_diff_arrays() const +{ + + // check min,max ring diff + { + // check is necessary here again because of set_min_ring_difference() + // we do not swap here because that would require the min/max_ring_diff arrays to be mutable as well + for (int segment_num=get_min_segment_num(); segment_num<=get_max_segment_num(); ++segment_num) + if (min_ring_diff[segment_num]> max_ring_diff[segment_num]) + { + error("ProjDataInfoGeneric: min_ring_difference %d is larger than " + "max_ring_difference %d for segment %d.", + min_ring_diff[segment_num], max_ring_diff[segment_num], segment_num); + } + } + + // initialise ax_pos_num_offset + if (sampling_corresponds_to_physical_rings) + { + const int num_rings = get_scanner_ptr()->get_num_rings(); + ax_pos_num_offset = + VectorWithOffset(get_min_segment_num(),get_max_segment_num()); + + /* ax_pos_num will be determined by looking at ring1+ring2. + This also works for axially compressed data (i.e. span) as + ring1+ring2 is constant for all ring-pairs combined into 1 + segment,ax_pos. + + Ignoring the difficulties of axial compression for a second, it is clear that + for a given bin, there will be 2 rings as follows: + ring2 = get_m(bin)/ring_spacing + ring_diff/2 + (num_rings-1)/2 + ring1 = get_m(bin)/ring_spacing - ring_diff/2 + (num_rings-1)/2 + This follows from the fact that get_m() returns the z position + in millimeter of the middle of the LOR w.r.t. the middle of the scanner. + The (num_rings-1)/2 shifts the origin such that the first ring has + ring_num==0. + + From the above, it follows that + ring1+ring2=2*get_m(bin)/ring_spacing + (num_rings-1) + Finally, we use the formula for get_m to obtain + ring1+ring2=2*ax_pos_num/get_num_axial_poss_per_ring_inc(segment_num) + -2*m_offset[segment_num]/ring_spacing + (num_rings-1) + Solving this for ax_pos_num: + ax_pos_num = (ring1+ring2-(num_rings-1) + + 2*m_offset[segment_num]/ring_spacing + ) * get_num_axial_poss_per_ring_inc(segment_num)/2 + + We could plug m_offset in to obtain + ax_pos_num = (ring1+ring2-(num_rings-1) + ) * get_num_axial_poss_per_ring_inc(segment_num)/2. + + + (get_max_axial_pos_num(segment_num) + + get_min_axial_pos_num(segment_num) )/2. + this formula is easy to understand, but we don't use it as + at some point somebody might change m_offset + and forget to change this code... + (also, the form above would need float division and then rounding) + */ + for (int segment_num=get_min_segment_num(); segment_num<=get_max_segment_num(); ++segment_num) + { + const float ax_pos_num_offset_float = (num_rings-1) - + (get_max_axial_pos_num(segment_num) + + get_min_axial_pos_num(segment_num)) + /get_num_axial_poss_per_ring_inc(segment_num); + ax_pos_num_offset[segment_num] = round(ax_pos_num_offset_float); + // check that it was integer + if (fabs(ax_pos_num_offset[segment_num] - ax_pos_num_offset_float) > 1E-4) + { + error("ProjDataInfoGeneric: in segment %d, the axial positions\n" + "do not correspond to the usual locations between physical rings.\n" + "This is suspicious and can make things go wrong in STIR, so I abort.\n" + "Check the number of axial positions in this segment.", + segment_num); + } + + if (get_num_axial_poss_per_ring_inc(segment_num)==1) + { + // check that we'll get an integer ax_pos_num, i.e. + // (ring1+ring2 - ax_pos_num_offset) has to be even, for any + // ring1,ring2 in the segment, i.e ring1-ring2 = ring_diff, so + // ring1+ring2 = 2*ring2 + ring_diff + assert(get_min_ring_difference(segment_num) == + get_max_ring_difference(segment_num)); + if ((get_max_ring_difference(segment_num) - + ax_pos_num_offset[segment_num]) % 2 != 0) + warning("ProjDataInfoGeneric: the number of axial positions in " + "segment %d is such that current conventions will place " + "the LORs shifted with respect to the physical rings.", + segment_num); + } + } + } + // initialise ring_diff_to_segment_num + if (sampling_corresponds_to_physical_rings) + { + const int min_ring_difference = + *min_element(min_ring_diff.begin(), min_ring_diff.end()); + const int max_ring_difference = + *max_element(max_ring_diff.begin(), max_ring_diff.end()); + + // set ring_diff_to_segment_num to appropriate size + // in principle, the max ring difference would be scanner.num_rings-1, but + // in case someone is up to strange things, we take the max of this value + // with the max_ring_difference as given in the file + ring_diff_to_segment_num = + VectorWithOffset(min(min_ring_difference, -(get_scanner_ptr()->get_num_rings()-1)), + max(max_ring_difference, get_scanner_ptr()->get_num_rings()-1)); + // first set all to impossible value + // warning: get_segment_num_for_ring_difference relies on the fact that this value + // is larger than get_max_segment_num() + ring_diff_to_segment_num.fill(get_max_segment_num()+1); + + for(int ring_diff=min_ring_difference; ring_diff <= max_ring_difference; ++ring_diff) + { + int segment_num; + for (segment_num=get_min_segment_num(); segment_num<=get_max_segment_num(); ++segment_num) + { + if (ring_diff >= min_ring_diff[segment_num] && + ring_diff <= max_ring_diff[segment_num]) + { + #if 0 + std::cerr << "ring diff " << ring_diff << " stored in s:" << segment_num << std::endl; + #endif + ring_diff_to_segment_num[ring_diff] = segment_num; + break; + } + } + if (segment_num>get_max_segment_num()) + { + warning("ProjDataInfoGeneric: ring difference %d does not belong to a segment", + ring_diff); + } + } + } + // initialise segment_axial_pos_to_ring1_plus_ring2 + if (sampling_corresponds_to_physical_rings) + { + segment_axial_pos_to_ring1_plus_ring2 = + VectorWithOffset >(get_min_segment_num(), get_max_segment_num()); + for (int s_num=get_min_segment_num(); s_num<=get_max_segment_num(); ++s_num) + { + const int min_ax_pos_num = get_min_axial_pos_num(s_num); + const int max_ax_pos_num = get_max_axial_pos_num(s_num); + segment_axial_pos_to_ring1_plus_ring2[s_num].grow(min_ax_pos_num, max_ax_pos_num); + for (int ax_pos_num=min_ax_pos_num; ax_pos_num<=max_ax_pos_num; ++ax_pos_num) + { + // see documentation above for formulas + const float ring1_plus_ring2_float = + (2*ax_pos_num - + get_max_axial_pos_num(s_num) + get_min_axial_pos_num(s_num)) + /get_num_axial_poss_per_ring_inc(s_num) + + get_scanner_ptr()->get_num_rings()-1; + + const int ring1_plus_ring2 = + round(ring1_plus_ring2_float); + // check that it was integer + assert(fabs(ring1_plus_ring2 - ring1_plus_ring2_float) < 1E-4) ; + segment_axial_pos_to_ring1_plus_ring2[s_num][ax_pos_num] = ring1_plus_ring2; + } + } + } + + if (sampling_corresponds_to_physical_rings) + allocate_segment_axial_pos_to_ring_pair(); + + ring_diff_arrays_computed = true; +} + +/*! Default implementation checks common variables. Needs to be overloaded. + */ +bool +ProjDataInfoGeneric:: +blindly_equals(const root_type * const that) const +{ + if (!base_type::blindly_equals(that)) + return false; + + const self_type& proj_data_info = static_cast(*that); + return + this->azimuthal_angle_sampling == proj_data_info.azimuthal_angle_sampling && + this->ring_radius == proj_data_info.ring_radius && + this->sampling_corresponds_to_physical_rings == proj_data_info.sampling_corresponds_to_physical_rings && + this->ring_spacing == proj_data_info.ring_spacing && + this->min_ring_diff == proj_data_info.min_ring_diff && + this->max_ring_diff == proj_data_info.max_ring_diff; +} + +void +ProjDataInfoGeneric:: +get_ring_pair_for_segment_axial_pos_num(int& ring1, + int& ring2, + const int segment_num, + const int axial_pos_num) const +{ + if (!sampling_corresponds_to_physical_rings) + error("ProjDataInfoGeneric::get_ring_pair_for_segment_axial_pos_num does not work for this type of sampled data"); + // can do only span=1 at the moment + if (get_min_ring_difference(segment_num) != get_max_ring_difference(segment_num)) + error("ProjDataInfoGeneric::get_ring_pair_for_segment_axial_pos_num does not work for data with axial compression"); + + if (!ring_diff_arrays_computed) + initialise_ring_diff_arrays(); + + const int ring_diff = get_max_ring_difference(segment_num); + const int ring1_plus_ring2= segment_axial_pos_to_ring1_plus_ring2[segment_num][axial_pos_num]; + + // KT 01/08/2002 swapped rings + ring1 = (ring1_plus_ring2 - ring_diff)/2; + ring2 = (ring1_plus_ring2 + ring_diff)/2; + assert((ring1_plus_ring2 + ring_diff)%2 == 0); + assert((ring1_plus_ring2 - ring_diff)%2 == 0); +} + + +void +ProjDataInfoGeneric:: +set_azimuthal_angle_sampling(const float angle_v) +{ + azimuthal_angle_sampling = angle_v; +} + +//void +//ProjDataInfoGeneric:: +//set_axial_sampling(const float samp_v, int segment_num) +//{axial_sampling = samp_v;} + + +void +ProjDataInfoGeneric:: +set_num_views(const int new_num_views) +{ + const float old_azimuthal_angle_range = + this->get_azimuthal_angle_sampling() * this->get_num_views(); + base_type::set_num_views(new_num_views); + this->azimuthal_angle_sampling = old_azimuthal_angle_range/this->get_num_views(); +} + +void +ProjDataInfoGeneric:: +set_min_ring_difference( int min_ring_diff_v, int segment_num) +{ + ring_diff_arrays_computed = false; + min_ring_diff[segment_num] = min_ring_diff_v; +} + +void +ProjDataInfoGeneric:: +set_max_ring_difference( int max_ring_diff_v, int segment_num) +{ + ring_diff_arrays_computed = false; + max_ring_diff[segment_num] = max_ring_diff_v; +} + +void +ProjDataInfoGeneric:: +set_ring_spacing(float ring_spacing_v) +{ + ring_diff_arrays_computed = false; + ring_spacing = ring_spacing_v; +} + +void +ProjDataInfoGeneric:: +allocate_segment_axial_pos_to_ring_pair() const +{ + segment_axial_pos_to_ring_pair = + VectorWithOffset > > + (get_min_segment_num(), get_max_segment_num()); + + for (int segment_num = get_min_segment_num(); + segment_num <= get_max_segment_num(); + ++segment_num) + { + segment_axial_pos_to_ring_pair[segment_num].grow(get_min_axial_pos_num(segment_num), + get_max_axial_pos_num(segment_num)); + } +} + +void +ProjDataInfoGeneric:: +compute_segment_axial_pos_to_ring_pair(const int segment_num, const int axial_pos_num) const +{ + shared_ptr new_el(new RingNumPairs); + segment_axial_pos_to_ring_pair[segment_num][axial_pos_num] = new_el; + + RingNumPairs& table = + *segment_axial_pos_to_ring_pair[segment_num][axial_pos_num]; + table.reserve(get_max_ring_difference(segment_num) - + get_min_ring_difference(segment_num) + 1); + + /* We compute the lookup-table in a fancy way. + We could just as well have a simple loop over all ring pairs and check + if it belongs to this segment/axial_pos. + The current way is a lot faster though. + */ + const int min_ring_diff = get_min_ring_difference(segment_num); + const int max_ring_diff = get_max_ring_difference(segment_num); + const int num_rings = get_scanner_ptr()->get_num_rings(); + + /* ring1_plus_ring2 is the same for any ring pair that contributes to + this particular segment_num, axial_pos_num. + */ + const int ring1_plus_ring2= + segment_axial_pos_to_ring1_plus_ring2[segment_num][axial_pos_num]; + + /* + The ring_difference increments with 2 as the other ring differences do + not give a ring pair with this axial_position. This is because + ring1_plus_ring2%2 == ring_diff%2 + (which easily follows by plugging in ring1+ring2 and ring1-ring2). + The starting ring_diff is determined such that the above condition + is satisfied. You can check it by noting that the + start_ring_diff%2 + == (min_ring_diff + (min_ring_diff+ring1_plus_ring2)%2)%2 + == (2*min_ring_diff+ring1_plus_ring2)%2 + == ring1_plus_ring2%2 + */ + for(int ring_diff = min_ring_diff + (min_ring_diff+ring1_plus_ring2)%2; + ring_diff <= max_ring_diff; + ring_diff+=2 ) + { + const int ring1 = (ring1_plus_ring2 - ring_diff)/2; + const int ring2 = (ring1_plus_ring2 + ring_diff)/2; + if (ring1<0 || ring2 < 0 || ring1>=num_rings || ring2 >= num_rings) + continue; + assert((ring1_plus_ring2 + ring_diff)%2 == 0); + assert((ring1_plus_ring2 - ring_diff)%2 == 0); + table.push_back(pair(ring1, ring2)); + #ifndef NDEBUG + int check_segment_num = 0, check_axial_pos_num = 0; + assert(get_segment_axial_pos_num_for_ring_pair(check_segment_num, + check_axial_pos_num, + ring1, + ring2) == + Succeeded::yes); + assert(check_segment_num == segment_num); + assert(check_axial_pos_num == axial_pos_num); + #endif + } +} + +void +ProjDataInfoGeneric:: +set_num_axial_poss_per_segment(const VectorWithOffset& num_axial_poss_per_segment) +{ + ProjDataInfo::set_num_axial_poss_per_segment(num_axial_poss_per_segment); + ring_diff_arrays_computed = false; +} + +void +ProjDataInfoGeneric:: +set_min_axial_pos_num(const int min_ax_pos_num, const int segment_num) +{ + ProjDataInfo::set_min_axial_pos_num(min_ax_pos_num, segment_num); + ring_diff_arrays_computed = false; +} + +void ProjDataInfoGeneric:: +set_max_axial_pos_num(const int max_ax_pos_num, const int segment_num) +{ + ProjDataInfo::set_max_axial_pos_num(max_ax_pos_num, segment_num); + ring_diff_arrays_computed = false; +} + +void +ProjDataInfoGeneric:: +reduce_segment_range(const int min_segment_num, const int max_segment_num) +{ + ProjDataInfo::reduce_segment_range(min_segment_num, max_segment_num); + // reduce ring_diff arrays to new valid size + VectorWithOffset new_min_ring_diff(min_segment_num, max_segment_num); + VectorWithOffset new_max_ring_diff(min_segment_num, max_segment_num); + + for (int segment_num = min_segment_num; segment_num<= max_segment_num; ++segment_num) + { + new_min_ring_diff[segment_num] = this->min_ring_diff[segment_num]; + new_max_ring_diff[segment_num] = this->max_ring_diff[segment_num]; + } + + this->min_ring_diff = new_min_ring_diff; + this->max_ring_diff = new_max_ring_diff; + + // make sure other arrays will be updated if/when necessary + this->ring_diff_arrays_computed = false; +} + +//! warning Find lor from cartesian coordinates of detector pair +void +ProjDataInfoGeneric:: +get_LOR(LORInAxialAndNoArcCorrSinogramCoordinates& lor, + const Bin& bin) const +{ + CartesianCoordinate3D _p1; + CartesianCoordinate3D _p2; + find_cartesian_coordinates_of_detection(_p1, _p2, bin); + + LORAs2Points lor_as_2_points(_p1, _p2); + const double R = get_ring_radius(); + lor_as_2_points.change_representation_for_block(lor, R); +} + +std::string +ProjDataInfoGeneric::parameter_info() const +{ + +#ifdef BOOST_NO_STRINGSTREAM + // dangerous for out-of-range, but 'old-style' ostrstream seems to need this + char str[30000]; + ostrstream s(str, 30000); +#else + std::ostringstream s; +#endif + s << ProjDataInfo::parameter_info(); + s << "Azimuthal angle increment (deg): " << get_azimuthal_angle_sampling()*180/_PI << '\n'; + s << "Azimuthal angle extent (deg): " << fabs(get_azimuthal_angle_sampling())*get_num_views()*180/_PI << '\n'; + + s << "ring differences per segment: \n"; + for (int segment_num=get_min_segment_num(); segment_num<=get_max_segment_num(); ++segment_num) + { + s << '(' << min_ring_diff[segment_num] << ',' << max_ring_diff[segment_num] <<')'; + } + s << std::endl; + return s.str(); +} + +END_NAMESPACE_STIR diff --git a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx new file mode 100644 index 000000000..ae2dbe141 --- /dev/null +++ b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx @@ -0,0 +1,438 @@ + +/* + +*/ +/*! + + \file + \ingroup projdata + + \brief Implementation of non-inline functions of class stir::ProjDataInfoGenericNoArcCorr + +*/ + +#include "stir/ProjDataInfoGenericNoArcCorr.h" +#include "stir/Bin.h" +#include "stir/LORCoordinates.h" +#include "stir/round.h" +#include "stir/DetectionPosition.h" +#include "stir/is_null_ptr.h" +#include +#include + +#ifdef BOOST_NO_STRINGSTREAM +#include +#else +#include +#endif + +#ifndef STIR_NO_NAMESPACES +using std::endl; +using std::ends; +#endif + +START_NAMESPACE_STIR +ProjDataInfoGenericNoArcCorr:: +ProjDataInfoGenericNoArcCorr() +{} + +ProjDataInfoGenericNoArcCorr:: +ProjDataInfoGenericNoArcCorr(const shared_ptr scanner_ptr, + const float ring_radius_v, const float angular_increment_v, + const VectorWithOffset& num_axial_pos_per_segment, + const VectorWithOffset& min_ring_diff_v, + const VectorWithOffset& max_ring_diff_v, + const int num_views,const int num_tangential_poss) +: ProjDataInfoGeneric(scanner_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + num_views, num_tangential_poss), + ring_radius(ring_radius_v), + angular_increment(angular_increment_v) +{ + uncompressed_view_tangpos_to_det1det2_initialised = false; + det1det2_to_uncompressed_view_tangpos_initialised = false; +} + +ProjDataInfoGenericNoArcCorr:: +ProjDataInfoGenericNoArcCorr(const shared_ptr scanner_ptr, + const VectorWithOffset& num_axial_pos_per_segment, + const VectorWithOffset& min_ring_diff_v, + const VectorWithOffset& max_ring_diff_v, + const int num_views,const int num_tangential_poss) +: ProjDataInfoGeneric(scanner_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + num_views, num_tangential_poss) +{ + assert(!is_null_ptr(scanner_ptr)); + ring_radius = scanner_ptr->get_effective_ring_radius(); + angular_increment = static_cast(_PI/scanner_ptr->get_num_detectors_per_ring()); + uncompressed_view_tangpos_to_det1det2_initialised = false; + det1det2_to_uncompressed_view_tangpos_initialised = false; +} + + + + +ProjDataInfo* +ProjDataInfoGenericNoArcCorr::clone() const +{ + return static_cast(new ProjDataInfoGenericNoArcCorr(*this)); +} + +bool +ProjDataInfoGenericNoArcCorr:: +operator==(const self_type& that) const +{ + if (!base_type::blindly_equals(&that)) + return false; + return + this->ring_radius == that.ring_radius && + this->angular_increment == that.angular_increment; +} + +bool +ProjDataInfoGenericNoArcCorr:: +blindly_equals(const root_type * const that_ptr) const +{ + assert(dynamic_cast(that_ptr) != 0); + return + this->operator==(static_cast(*that_ptr)); +} + +std::string +ProjDataInfoGenericNoArcCorr::parameter_info() const +{ + + #ifdef BOOST_NO_STRINGSTREAM + // dangerous for out-of-range, but 'old-style' ostrstream seems to need this + char str[50000]; + ostrstream s(str, 50000); + #else + std::ostringstream s; + #endif + s << "ProjDataInfoGenericNoArcCorr := \n"; + s << ProjDataInfoGeneric::parameter_info(); + s << "End :=\n"; + return s.str(); +} + +/* + TODO make compile time assert + + Warning: + this code makes use of an implementation dependent feature: + bit shifting negative ints to the right. + -1 >> 1 should be -1 + -2 >> 1 should be -1 + This is ok on SUNs (gcc, but probably SUNs cc as well), Parsytec (gcc), + Pentium (gcc, VC++) and probably every other system which uses + the 2-complement convention. +*/ + +/*! + Go from sinograms to detectors. + + Because sinograms are not arc-corrected, tang_pos_num corresponds + to an angle as well. Before interleaving we have that + \verbatim + det_angle_1 = LOR_angle + bin_angle + det_angle_2 = LOR_angle + (Pi - bin_angle) + \endverbatim + (Hint: understand this first at LOR_angle=0, then realise that + other LOR_angles follow just by rotation) + + Code gets slightly intricate because: + - angles have to be defined modulo 2 Pi (so num_detectors) + - interleaving +*/ + +//! build look-up table for get_view_tangential_pos_num_for_det_num_pair() +void +ProjDataInfoGenericNoArcCorr:: +initialise_uncompressed_view_tangpos_to_det1det2() const +{ + assert(-1 >> 1 == -1); + assert(-2 >> 1 == -1); + + const int num_detectors = + get_scanner_ptr()->get_num_detectors_per_ring(); + + assert(num_detectors%2 == 0); + // check views range from 0 to Pi + /*! + warning The following assersions are slightly different from cylindrical. + because in the function get_phi(bin), get_lor is called. + */ + assert(fabs(Bin(0,0,0,0).view_num()*get_azimuthal_angle_sampling()) < 1.E-4); + assert(fabs(Bin(0,get_num_views(),0,0).view_num()*get_azimuthal_angle_sampling() - _PI) < 1.E-4); + + const int min_tang_pos_num = -(num_detectors/2)+1; + const int max_tang_pos_num = -(num_detectors/2)+num_detectors; + + if (this->get_min_tangential_pos_num() < min_tang_pos_num || + this->get_max_tangential_pos_num() > max_tang_pos_num) + { + error("The tangential_pos range (%d to %d) for this projection data is too large.\n" + "Maximum supported range is from %d to %d", + this->get_min_tangential_pos_num(), this->get_max_tangential_pos_num(), + min_tang_pos_num, max_tang_pos_num); + } + + uncompressed_view_tangpos_to_det1det2.grow(0,num_detectors/2-1); + for (int v_num=0; v_num<=num_detectors/2-1; ++v_num) + { + uncompressed_view_tangpos_to_det1det2[v_num].grow(min_tang_pos_num, max_tang_pos_num); + + for (int tp_num=min_tang_pos_num; tp_num<=max_tang_pos_num; ++tp_num) + { + /* + adapted from CTI code + Note for implementation: avoid using % with negative numbers + so add num_detectors before doing modulo num_detectors) + */ + uncompressed_view_tangpos_to_det1det2[v_num][tp_num].det1_num = + (v_num + (tp_num >> 1) + num_detectors) % num_detectors; + uncompressed_view_tangpos_to_det1det2[v_num][tp_num].det2_num = + (v_num - ( (tp_num + 1) >> 1 ) + num_detectors/2) % num_detectors; + } + } + uncompressed_view_tangpos_to_det1det2_initialised = true; +} + +void +ProjDataInfoGenericNoArcCorr:: +initialise_det1det2_to_uncompressed_view_tangpos() const +{ + assert(-1 >> 1 == -1); + assert(-2 >> 1 == -1); + + const int num_detectors = + get_scanner_ptr()->get_num_detectors_per_ring(); + + if (num_detectors%2 != 0) + { + error("Number of detectors per ring should be even but is %d", num_detectors); + } + if (this->get_min_view_num() != 0) + { + error("Minimum view number should currently be zero to be able to use get_view_tangential_pos_num_for_det_num_pair()"); + } + + // check views range from 0 to Pi + //! warning The following assersions are slightly different from cylindrical. See the above function + assert(fabs(Bin(0,0,0,0).view_num()*get_azimuthal_angle_sampling()) < 1.E-4); + assert(fabs(Bin(0,get_max_view_num()+1,0,0).view_num()*get_azimuthal_angle_sampling() - _PI) < 1.E-4); + + + //const int min_tang_pos_num = -(num_detectors/2); + //const int max_tang_pos_num = -(num_detectors/2)+num_detectors; + const int max_num_views = num_detectors/2; + + det1det2_to_uncompressed_view_tangpos.grow(0,num_detectors-1); + for (int det1_num=0; det1_num> 1) + num_detectors) % num_detectors; + + /* Now adjust ranges for view_num, tang_pos_num. + The next lines go only wrong in the singular (and irrelevant) case + det_num1 == det_num2 (when tang_pos_num == num_detectors - tang_pos_num) + + We use the combinations of the following 'symmetries' of + (tang_pos_num, view_num) == (tang_pos_num+2*num_views, view_num + num_views) + == (-tang_pos_num, view_num + num_views) + Using the latter interchanges det_num1 and det_num2, and this leaves + the LOR the same in the 2D case. However, in 3D this interchanges the rings + as well. So, we keep track of this in swap_detectors, and return its final + value. + */ + if (view_num < max_num_views) + { + if (tang_pos_num >= max_num_views) + { + tang_pos_num = num_detectors - tang_pos_num; + swap_detectors = 1; + } + else + { + swap_detectors = 0; + } + } + else + { + view_num -= max_num_views; + if (tang_pos_num >= max_num_views) + { + tang_pos_num -= num_detectors; + swap_detectors = 0; + } + else + { + tang_pos_num *= -1; + swap_detectors = 1; + } + } + + det1det2_to_uncompressed_view_tangpos[det1_num][det2_num].view_num = view_num; + det1det2_to_uncompressed_view_tangpos[det1_num][det2_num].tang_pos_num = tang_pos_num; + det1det2_to_uncompressed_view_tangpos[det1_num][det2_num].swap_detectors = swap_detectors==0; + } + } + det1det2_to_uncompressed_view_tangpos_initialised = true; +} + +unsigned int +ProjDataInfoGenericNoArcCorr:: +get_num_det_pos_pairs_for_bin(const Bin& bin) const +{ + return + get_num_ring_pairs_for_segment_axial_pos_num(bin.segment_num(), + bin.axial_pos_num())* + get_view_mashing_factor(); +} + +void +ProjDataInfoGenericNoArcCorr:: +get_all_det_pos_pairs_for_bin(vector >& dps, + const Bin& bin) const +{ + if (!uncompressed_view_tangpos_to_det1det2_initialised) + initialise_uncompressed_view_tangpos_to_det1det2(); + + dps.resize(get_num_det_pos_pairs_for_bin(bin)); + + const ProjDataInfoGeneric::RingNumPairs& ring_pairs = + get_all_ring_pairs_for_segment_axial_pos_num(bin.segment_num(), + bin.axial_pos_num()); + // not sure how to handle mashing with non-zero view offset... + assert(get_min_view_num()==0); + + unsigned int current_dp_num=0; + for (int uncompressed_view_num=bin.view_num()*get_view_mashing_factor(); + uncompressed_view_num<(bin.view_num()+1)*get_view_mashing_factor(); + ++uncompressed_view_num) + { + const int det1_num = + uncompressed_view_tangpos_to_det1det2[uncompressed_view_num][bin.tangential_pos_num()].det1_num; + const int det2_num = + uncompressed_view_tangpos_to_det1det2[uncompressed_view_num][bin.tangential_pos_num()].det2_num; + for (ProjDataInfoGeneric::RingNumPairs::const_iterator rings_iter = ring_pairs.begin(); + rings_iter != ring_pairs.end(); + ++rings_iter) + { + assert(current_dp_num < get_num_det_pos_pairs_for_bin(bin)); + dps[current_dp_num].pos1().tangential_coord() = det1_num; + dps[current_dp_num].pos1().axial_coord() = rings_iter->first; + dps[current_dp_num].pos2().tangential_coord() = det2_num; + dps[current_dp_num].pos2().axial_coord() = rings_iter->second; + ++current_dp_num; + } + } + assert(current_dp_num == get_num_det_pos_pairs_for_bin(bin)); +} + +void +ProjDataInfoGenericNoArcCorr:: +find_cartesian_coordinates_of_detection( + CartesianCoordinate3D& coord_1, + CartesianCoordinate3D& coord_2, + const Bin& bin) const +{ + // find detectors + int det_num_a; + int det_num_b; + int ring_a; + int ring_b; + get_det_pair_for_bin(det_num_a, ring_a, + det_num_b, ring_b, bin); + + // find corresponding cartesian coordinates + find_cartesian_coordinates_given_scanner_coordinates(coord_1,coord_2, + ring_a,ring_b,det_num_a,det_num_b); + return; +} + +void +ProjDataInfoGenericNoArcCorr:: +find_cartesian_coordinates_given_scanner_coordinates(CartesianCoordinate3D& coord_1, + CartesianCoordinate3D& coord_2, + const int Ring_A,const int Ring_B, + const int det1, const int det2) const +{ + assert(0<=det1); + assert(det1get_num_detectors_per_ring()); + assert(0<=det2); + assert(det2get_num_detectors_per_ring()); + + DetectionPosition<> det_pos1; + DetectionPosition<> det_pos2; + det_pos1.tangential_coord() = det1; + det_pos2.tangential_coord() = det2; + det_pos1.axial_coord() = Ring_A; + det_pos2.axial_coord() = Ring_B; + + coord_1 = get_scanner_ptr()->get_coords_from_detpos(det_pos1); + coord_2 = get_scanner_ptr()->get_coords_from_detpos(det_pos2); +} + + +//TODO: Try to get it obsolete (Is still used in one test) +Bin +ProjDataInfoGenericNoArcCorr:: +get_bin(const LOR& lor) const +{ + Bin bin; + + const LORAs2Points & lor_as_2points = dynamic_cast &>(lor); + + CartesianCoordinate3D _p1 = lor_as_2points.p1(); + CartesianCoordinate3D _p2 = lor_as_2points.p2(); + + DetectionPosition<> det_pos1; + DetectionPosition<> det_pos2; + if (crystal_map->find_detection_position_given_cartesian_coordinate(det_pos1, _p1)==Succeeded::no || + crystal_map->find_detection_position_given_cartesian_coordinate(det_pos2, _p2)==Succeeded::no) + { + bin.set_bin_value(-1); + return bin; + } + + DetectionPositionPair<> det_pos_pair; + det_pos_pair.pos1() = det_pos1; + det_pos_pair.pos2() = det_pos2; + + if (get_bin_for_det_pos_pair(bin, det_pos_pair) == Succeeded::yes && + bin.tangential_pos_num() >= get_min_tangential_pos_num() && + bin.tangential_pos_num() <= get_max_tangential_pos_num()) + { + bin.set_bin_value(1); + return bin; + } + else + { + bin.set_bin_value(-1); + return bin; + } +} + + +END_NAMESPACE_STIR diff --git a/src/buildblock/Scanner.cxx b/src/buildblock/Scanner.cxx index 038d1175f..a378b8bed 100644 --- a/src/buildblock/Scanner.cxx +++ b/src/buildblock/Scanner.cxx @@ -39,6 +39,7 @@ #include "stir/Succeeded.h" #include "stir/interfile_keyword_functions.h" #include "stir/info.h" +#include "stir/modulo.h" #include #include #ifdef BOOST_NO_STRINGSTREAM @@ -436,6 +437,7 @@ Scanner::Scanner(Type type_v, const list& list_of_names_v, int num_detector_layers_v, float energy_resolution_v, float reference_energy_v, + const std::string& crystal_map_file_name_v, const string& scanner_orientation_v, const string& scanner_geometry_v, float axial_crystal_spacing_v, @@ -457,6 +459,7 @@ Scanner::Scanner(Type type_v, const list& list_of_names_v, num_detector_layers_v, energy_resolution_v, reference_energy_v, + crystal_map_file_name_v, scanner_orientation_v, scanner_geometry_v, axial_crystal_spacing_v, @@ -480,6 +483,7 @@ Scanner::Scanner(Type type_v, const string& name, int num_detector_layers_v, float energy_resolution_v, float reference_energy_v, + const std::string& crystal_map_file_name_v, const string& scanner_orientation_v, const string& scanner_geometry_v, float axial_crystal_spacing_v, @@ -501,6 +505,7 @@ Scanner::Scanner(Type type_v, const string& name, num_detector_layers_v, energy_resolution_v, reference_energy_v, + crystal_map_file_name_v, scanner_orientation_v, scanner_geometry_v, axial_crystal_spacing_v, @@ -532,6 +537,7 @@ set_params(Type type_v,const list& list_of_names_v, int num_detector_layers_v, float energy_resolution_v, float reference_energy_v, + const std::string& crystal_map_file_name_v, const string& scanner_orientation_v, const string& scanner_geometry_v, float axial_crystal_spacing_v, @@ -553,6 +559,7 @@ set_params(Type type_v,const list& list_of_names_v, num_detector_layers_v, energy_resolution_v, reference_energy_v, + crystal_map_file_name_v, scanner_orientation_v, scanner_geometry_v, axial_crystal_spacing_v, @@ -580,6 +587,7 @@ set_params(Type type_v,const list& list_of_names_v, int num_detector_layers_v, float energy_resolution_v, float reference_energy_v, + const std::string& crystal_map_file_name_v, const string& scanner_orientation_v, const string& scanner_geometry_v, float axial_crystal_spacing_v, @@ -615,7 +623,84 @@ set_params(Type type_v,const list& list_of_names_v, transaxial_crystal_spacing = transaxial_crystal_spacing_v; axial_block_spacing = axial_block_spacing_v; transaxial_block_spacing = transaxial_block_spacing_v; + + crystal_map_file_name = crystal_map_file_name_v; + if (crystal_map_file_name != "") + { + read_detectormap_from_file(crystal_map_file_name); + } +} + +// creates maps to convert between stir and 3d coordinates +void +Scanner:: +read_detectormap_from_file( const std::string& filename ) +{ + std::ifstream myfile(filename.c_str()); + if( !myfile ) + { + std::cerr << "Error opening file " << filename << "." << std::endl; + return; + } + std::string line; + //map containing the crystal map from the input file (safir -> coords) + boost::unordered_map, stir::CartesianCoordinate3D, ihash> coord_map; + // read in the file save the content in a map + while( std::getline( myfile, line)) + { + if( line.size() && line[0] == '#' ) continue; + bool has_layer_index = false; + stir::CartesianCoordinate3D coord; + stir::DetectionPosition<> detpos; + std::vector col; + boost::split(col, line, boost::is_any_of("\t,")); + if( !col.size() ) break; + else if( col.size() == 5 ) has_layer_index = false; + else if( col.size() == 6 ) has_layer_index = true; + coord[1] = atof(col[4+has_layer_index].c_str() ); + coord[2] = atof(col[3+has_layer_index].c_str() ); + coord[3] = atof(col[2+has_layer_index].c_str() ); + + if( !has_layer_index ) detpos.radial_coord() = 0; + else detpos.radial_coord() = atoi(col[2].c_str()); + detpos.axial_coord() = atoi(col[0].c_str()); + detpos.tangential_coord() = atoi(col[1].c_str()); + + coord_map[detpos] = coord; + } + std::vector coords_to_be_sorted; + boost::unordered_map > map_for_sorting_coordinates; + coords_to_be_sorted.reserve(coord_map.size()); + //TODO: change it to be backwards compatible + for(auto it : coord_map) + { + double coord_sorter = it.second[1] * 100 + from_min_pi_plus_pi_to_0_2pi(std::atan2(it.second[3], -it.second[2])); + coords_to_be_sorted.push_back(coord_sorter); + map_for_sorting_coordinates[coord_sorter] = it.first; + } + std::sort(coords_to_be_sorted.begin(), coords_to_be_sorted.end()); + int ring = 0; + int det = 0; + for(std::vector::iterator it = coords_to_be_sorted.begin(); it != coords_to_be_sorted.end();++it) + { + stir::DetectionPosition<> detpos; + detpos.axial_coord() = ring; + detpos.tangential_coord() = det; + detpos.radial_coord() = 0; + input_index_to_stir_index[map_for_sorting_coordinates[*it]] = detpos; + input_index_to_coord[detpos] = coord_map[map_for_sorting_coordinates[*it]]; + det++; + if (det == num_detectors_per_ring) + { + ring++; + det = 0; + if (ring == num_rings && it != --coords_to_be_sorted.end()) + { + stir::error("Detector and RingNumber of the crystal map and ProjData are not the same!"); + } + } + } } @@ -638,7 +723,7 @@ check_consistency() const const int dets_per_ring = get_num_transaxial_blocks() * get_num_transaxial_crystals_per_block(); - if ( dets_per_ring != get_num_detectors_per_ring()) + if ( dets_per_ring != get_num_detectors_per_ring() && scanner_orientation != "Generic") { warning("Scanner %s: inconsistent transaxial block info", this->get_name().c_str()); @@ -891,13 +976,17 @@ Scanner::parameter_info() const << get_num_axial_crystals_per_singles_unit() << '\n' << "Number of crystals per singles unit in transaxial direction := " << get_num_transaxial_crystals_per_singles_unit() << '\n'; + //crystal map + if (crystal_map_file_name != "") + s << "name of crystal map := " + << crystal_map_file_name << '\n'; //block geometry description if (get_scanner_orientation() != "" && get_scanner_geometry() != "") { s << "Scanner orientation (X or Y) := " < #include "stir/unique_ptr.h" #include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" +#include "stir/ProjDataInfoGenericNoArcCorr.h" #ifndef STIR_NO_NAMESPACES using std::ifstream; @@ -111,6 +112,28 @@ static void find_sampling_and_z_size( ? proj_data_info_blk_ptr->get_num_axial_poss(0) : 2*proj_data_info_blk_ptr->get_num_axial_poss(0) - 1; } + else if (const ProjDataInfoGeneric* + proj_data_info_gen_ptr = + dynamic_cast(proj_data_info_ptr)) + { + // the case of Generic data + + z_sampling = proj_data_info_gen_ptr->get_ring_spacing()/2; + + // for 'span>1' case, we take z_size = number of sinograms in segment 0 + // for 'span==1' case, we take 2*num_rings-1 + + // first check if we have segment 0 + assert(proj_data_info_gen_ptr->get_min_segment_num() <= 0); + assert(proj_data_info_gen_ptr->get_max_segment_num() >= 0); + + if (z_size<0) + z_size = + proj_data_info_gen_ptr->get_max_ring_difference(0) > + proj_data_info_gen_ptr->get_min_ring_difference(0) + ? proj_data_info_gen_ptr->get_num_axial_poss(0) + : 2*proj_data_info_gen_ptr->get_num_axial_poss(0) - 1; + } else { // this is any other weird projection data. We just check sampling of segment 0 diff --git a/src/include/stir/IO/InterfileHeader.h b/src/include/stir/IO/InterfileHeader.h index a8943f168..60b71479a 100644 --- a/src/include/stir/IO/InterfileHeader.h +++ b/src/include/stir/IO/InterfileHeader.h @@ -257,6 +257,7 @@ class InterfilePDFSHeader : public InterfileHeader //! new variables for block geometry std::string scanner_orientation; std::string scanner_geometry; + std::string crystal_map; float axial_distance_between_crystals_in_cm; float transaxial_distance_between_crystals_in_cm; float axial_distance_between_blocks_in_cm; diff --git a/src/include/stir/ProjDataInfoGeneric.h b/src/include/stir/ProjDataInfoGeneric.h new file mode 100644 index 000000000..660f61f95 --- /dev/null +++ b/src/include/stir/ProjDataInfoGeneric.h @@ -0,0 +1,302 @@ +/* + +*/ +/*! + + \file + \ingroup projdata + \brief Declaration of class stir::ProjDataInfoGeneric + +*/ +#ifndef __stir_ProjDataInfoGeneric_H__ +#define __stir_ProjDataInfoGeneric_H__ + + +#include "stir/ProjDataInfo.h" +#include +#include + +#ifndef STIR_NO_NAMESPACES +using std::vector; +using std::pair; +#endif + +START_NAMESPACE_STIR + +class Succeeded; +template class CartesianCoordinate3D; + +/*! + \ingroup projdata + \brief projection data info for data corresponding to a + 'Blocks-on-cylindrical' sampling. +*/ + +class ProjDataInfoGeneric: public ProjDataInfo +{ +private: + typedef ProjDataInfo base_type; + typedef ProjDataInfoGeneric self_type; + +public: + //! Type used by get_all_ring_pairs_for_segment_axial_pos_num() + typedef vector > RingNumPairs; + + //! Constructors + ProjDataInfoGeneric(); + //! Constructor given all the necessary information + /*! The min and max ring difference in each segment are passed + as VectorWithOffsets. All three vectors have to have index ranges + from min_segment_num to max_segment_num. + + \warning Most of this library assumes that segment 0 corresponds + to an average ring difference of 0. + */ + ProjDataInfoGeneric(const shared_ptr& scanner_ptr, + const VectorWithOffset& num_axial_poss_per_segment, //index ranges from min_segment_num to max_segment_num + const VectorWithOffset& min_ring_diff, + const VectorWithOffset& max_ring_diff, + const int num_views,const int num_tangential_poss); + + inline virtual float get_tantheta(const Bin&) const; + + inline float get_phi(const Bin&) const; + + inline float get_t(const Bin&) const; + + //! Return z-coordinate of the middle of the LOR + /*! + The 0 of the z-axis is chosen in the middle of the scanner. + */ + inline float get_m(const Bin&) const; + + virtual void + get_LOR(LORInAxialAndNoArcCorrSinogramCoordinates& lor, const Bin& bin) const; + + void set_azimuthal_angle_sampling(const float angle); + + //! set new number of views, covering the same azimuthal angle range + /*! calls ProjDataInfo::set_num_views(), but makes sure that we cover the + same range of angles as before (usually, but not necessarily, 180 degrees) + by adjusting azimuthal_angle_sampling. + */ + virtual void + set_num_views(const int new_num_views); + + //! Get the azimuthal sampling (in radians) + inline float get_azimuthal_angle_sampling() const; + virtual inline float get_sampling_in_t(const Bin&) const; + virtual inline float get_sampling_in_m(const Bin&) const; + + //! Get the axial sampling (e.g in z_direction) + /*! + \warning The implementation of this function currently assumes that the axial + sampling is equal to the ring spacing for non-spanned data + (i.e. no axial compression), while it is half the + ring spacing for spanned data. + */ + inline float get_axial_sampling(int segment_num) const; + + //! Get average ring difference for the given segment + inline float get_average_ring_difference(int segment_num) const; + //! Get minimum ring difference for the given segment + inline int get_min_ring_difference(int segment_num) const; + //! Get maximum ring difference for the given segment + inline int get_max_ring_difference(int segment_num) const; + + //! Set minimum ring difference + void set_min_ring_difference(int min_ring_diff_v, int segment_num); + //! Set maximum ring difference + void set_max_ring_difference(int max_ring_diff_v, int segment_num); + + virtual void set_num_axial_poss_per_segment(const VectorWithOffset& num_axial_poss_per_segment); + virtual void set_min_axial_pos_num(const int min_ax_pos_num, const int segment_num); + virtual void set_max_axial_pos_num(const int max_ax_pos_num, const int segment_num); + virtual void reduce_segment_range(const int min_segment_num, const int max_segment_num); + + //! Set detector ring radius for all views + inline void set_ring_radii_for_all_views(const VectorWithOffset& new_ring_radius); + + //! Get detector ring radius for all views + inline VectorWithOffset get_ring_radii_for_all_views() const; + + //! Get detector ring radius + inline float get_ring_radius() const; + inline float get_ring_radius( const int view_num) const; + + //! Get detector ring spacing + inline float get_ring_spacing() const; + + //! Set detector ring spacing + void set_ring_spacing(float ring_spacing_v); + + //! Get the mashing factor, i.e. how many 'original' views are combined + /*! mashing factor = Ndet/(2Nview) + This gets the result by comparing the number of detectors in the scanner_ptr + with the actual number of views. + \warning In the debug version, it is checked with an assert() that the number of + detectors is an even multiple of the number of views. This is not checked in + the normal version though. + */ + inline int get_view_mashing_factor() const; + + //! Find which segment a particular ring difference belongs to + /*! + \return Succeeded::yes when a corresponding segment was found. + */ + inline Succeeded + get_segment_num_for_ring_difference(int& segment_num, const int ring_diff) const; + + //! Find to which segment and axial position a ring pair contributes + /*! + \a ring1, \a ring2 have to be between 0 and scanner.get_num_rings()-1. + \return Succeeded::yes when a corresponding segment was found. + \warning axial_pos_num returned might be outside the actual range in the proj_data_info. + + \For CTI data with span, this essentially implements a 'michelogram'. + + \warning Current implementation assumes that the axial positions start from 0 for + the first ring-pair in the segment. + + \warning The implementation of this function currently assumes that the axial + sampling is equal to the ring spacing for non-spanned data + (i.e. no axial compression), while it is half the ring spacing for spanned data. + */ + inline Succeeded + get_segment_axial_pos_num_for_ring_pair(int& segment_num, + int& axial_pos_num, + const int ring1, + const int ring2) const; + + //! Find all ring pairs that contribute to a segment and axial position + /*! + \a ring1, \a ring2 will be between 0 and scanner.get_num_rings()-1. + + \warning The implementation of this function currently assumes that the axial + sampling is equal to the ring spacing for non-spanned data + (i.e. no axial compression), while it is half the ring spacing for spanned data. + */ + inline const RingNumPairs& + get_all_ring_pairs_for_segment_axial_pos_num(const int segment_num, + const int axial_pos_num) const; + //! Find the number of ring pairs that contribute to a segment and axial position + /*! + \warning The implementation of this function currently assumes that the axial + sampling is equal to the ring spacing for non-spanned data + (i.e. no axial compression), while it is half the ring spacing for spanned data. + */ + inline unsigned + get_num_ring_pairs_for_segment_axial_pos_num(const int segment_num, + const int axial_pos_num) const; + + //! Find a ring pair that contributes to a segment and axial position + /*! + \a ring1, \a ring2 will be between 0 and scanner.get_num_rings()-1. + + \warning Currently only works when no axial compression is used for the segment (i.e. + min_ring_diff = max_ring_diff). Otherwise, a error() will be called. + + \warning The implementation of this function currently assumes that the axial + sampling is equal to the ring spacing for non-spanned data + (i.e. no axial compression), while it is half the ring spacing for spanned data. + */ + void + get_ring_pair_for_segment_axial_pos_num(int& ring1, + int& ring2, + const int segment_num, + const int axial_pos_num) const; + + virtual std::string parameter_info() const; + +protected: + + //! a variable that is set if the data corresponds to physical rings in the scanner + /*! This is (only) used to prevent get_segment_axial_pos_num_for_ring_pair() et al + to go wild. Indeed, for cases where there's cylindrical sampling, but not + really any physical rings associated to the sampling, those functions will + return invalid information. + + The prime case where this is used is for data corresponding to (nearly) + continuous detectors, such as DHCI systems, or the HiDAC. + + Ideally, this would be done by having a separate class for such systems which + does not contain the ring-difference et al information. This seems to make + the hierarchy too complicated though. + + \bug The value of this variable is currently set by checking if the scanner + is a HiDAC scanner. This needs to be changed. + if scanner is not HiDAC this variable is set to 1 otherwise to 0 (see constructor) + */ + bool sampling_corresponds_to_physical_rings; + +protected: + virtual bool blindly_equals(const root_type * const) const = 0; + +private: + float azimuthal_angle_sampling; + VectorWithOffset ring_radius; + float ring_spacing; + VectorWithOffset min_ring_diff; + VectorWithOffset max_ring_diff; + + /* + Next members have to be mutable as they can be modified by const member + functions. We need this because of the presence of set_min_ring_difference() + which invalidates these precalculated arrays. + If your compiler does not support mutable (and you don't want to upgrade + it to something more sensible), your best bet is to remove the + set_*ring_difference functions, and move the content of + initialise_ring_diff_arrays() to the constructor. (Not recommended!) + */ + + //! This member will signal if the arrays below contain sensible info or not + mutable bool ring_diff_arrays_computed; + + //! This member stores the offsets used in get_m() + /* + //! warning This is not used in block geometry. m is found directly from lors. + mutable VectorWithOffset m_offset; + */ + + //! This member stores the offsets used in get_segment_axial_pos_num_for_ring_pair() + mutable VectorWithOffset ax_pos_num_offset; + + //! This member stores a table converting ring differences to segment numbers + mutable VectorWithOffset ring_diff_to_segment_num; + + //! This member stores a table converting segment/axial_pos to ring1+ring2 + mutable VectorWithOffset > segment_axial_pos_to_ring1_plus_ring2; + + //! This function sets all of the above + void initialise_ring_diff_arrays() const; + + //! This function checks if max_ring_diff is different from min_ring_diff (set to 2). + /*! in case of difference, there are 2 ax_pos per ring, i.e. an ax_pos between each two rings */ + inline int get_num_axial_poss_per_ring_inc(const int segment_num) const; + + //! This member will signal if the array below contain sensible info or not + mutable bool segment_axial_pos_to_ring_pair_allocated; + + //! This member stores a table used by get_all_ring_pairs_for_segment_axial_pos_num() + mutable VectorWithOffset< VectorWithOffset < shared_ptr > > + segment_axial_pos_to_ring_pair; + + //! allocate table + void allocate_segment_axial_pos_to_ring_pair() const; + + //! initialise one element of the above table + void compute_segment_axial_pos_to_ring_pair(const int segment_num, const int axial_pos_num) const; + + //! to be used in get LOR + virtual void find_cartesian_coordinates_of_detection(CartesianCoordinate3D& coord_1, + CartesianCoordinate3D& coord_2, + const Bin& bin) const = 0; + +}; + + +END_NAMESPACE_STIR + +#include "stir/ProjDataInfoGeneric.inl" + +#endif diff --git a/src/include/stir/ProjDataInfoGeneric.inl b/src/include/stir/ProjDataInfoGeneric.inl new file mode 100644 index 000000000..247045e9d --- /dev/null +++ b/src/include/stir/ProjDataInfoGeneric.inl @@ -0,0 +1,259 @@ +/* +TODO copyright and License +*/ + +/*! + + \file + \ingroup projdata + + \brief Implementation of inline functions of class stir::ProjDataInfoGeneric + +*/ + +// for sqrt +#include +#include "stir/Bin.h" +#include "stir/Succeeded.h" +#include "stir/LORCoordinates.h" +#include "stir/is_null_ptr.h" +#include +#include "stir/CartesianCoordinate3D.h" + +START_NAMESPACE_STIR + +//! find phi from correspoding lor +float +ProjDataInfoGeneric::get_phi(const Bin& bin)const +{ + LORInAxialAndNoArcCorrSinogramCoordinates lor; + get_LOR(lor, bin); + if (bin.view_num()==0 && lor.phi()>0.1) + return lor.phi()-_PI; + return lor.phi(); +} + +/*! warning In block geometry m is calculated directly from lor while in + cylindrical geometry m is calculated using m_offset and axial_sampling +*/ +float +ProjDataInfoGeneric::get_m(const Bin& bin) const +{ + LORInAxialAndNoArcCorrSinogramCoordinates lor; + get_LOR(lor, bin); + return (lor.z1() + lor.z2())/2.; +} + +float +ProjDataInfoGeneric::get_t(const Bin& bin) const +{ + return + get_m(bin)*get_costheta(bin); +} + +/* + theta is copolar angle of normal to projection plane with z axis, i.e. copolar angle of lor with z axis. + tan (theta) = dz/sqrt(dx2+dy2) + cylindrical geometry: + delta_z = delta_ring * ring spacing + Block geometry: + delta_z is calculated from lor +*/ +float +ProjDataInfoGeneric::get_tantheta(const Bin& bin) const +{ + LORInAxialAndNoArcCorrSinogramCoordinates lor; + get_LOR(lor, bin); + const float delta_z = lor.z2() - lor.z1(); + if (fabs(delta_z)<0.0001F) + return 0; + const float R=get_ring_radius(bin.view_num()); + assert(R>=fabs(get_s(bin))); + return delta_z/(2*sqrt(square(R)-square(get_s(bin)))); +} + +float +ProjDataInfoGeneric::get_sampling_in_m(const Bin& bin) const +{ + return get_axial_sampling(bin.segment_num()); +} + +/* + warning Theta is not uniform anymore, so sampling in t does not make sense anymore. + Sampling parameters remained unchanged to be consistent with cylindrical version. +*/ +float +ProjDataInfoGeneric::get_sampling_in_t(const Bin& bin) const +{ + return get_axial_sampling(bin.segment_num())*get_costheta(bin); +} + +int +ProjDataInfoGeneric:: +get_num_axial_poss_per_ring_inc(const int segment_num) const +{ + return + max_ring_diff[segment_num] != min_ring_diff[segment_num] ? + 2 : 1; +} + +float +ProjDataInfoGeneric::get_azimuthal_angle_sampling() const +{return azimuthal_angle_sampling;} + +float +ProjDataInfoGeneric::get_axial_sampling(int segment_num) const +{ + return ring_spacing/get_num_axial_poss_per_ring_inc(segment_num); +} + +float +ProjDataInfoGeneric::get_average_ring_difference(int segment_num) const +{ + // KT 05/07/2001 use float division here. + // In any reasonable case, min+max_ring_diff will be even. + // But some day, an unreasonable case will walk in. + return (min_ring_diff[segment_num] + max_ring_diff[segment_num])/2.F; +} + +int +ProjDataInfoGeneric::get_min_ring_difference(int segment_num) const +{ return min_ring_diff[segment_num]; } + +int +ProjDataInfoGeneric::get_max_ring_difference(int segment_num) const +{ return max_ring_diff[segment_num]; } + +float +ProjDataInfoGeneric::get_ring_radius() const +{ + if (this->ring_radius.get_min_index()!=0 || this->ring_radius.get_max_index()!=0) + { + // check if all elements are equal + for (VectorWithOffset::const_iterator iter=this->ring_radius.begin(); iter!= this->ring_radius.end(); ++iter) + { + if (*iter != *this->ring_radius.begin()) + error("get_ring_radius called for non-circular ring"); + } + } + return *this->ring_radius.begin(); +} + +void +ProjDataInfoGeneric::set_ring_radii_for_all_views(const VectorWithOffset& new_ring_radius) +{ + if (new_ring_radius.get_min_index() != this->get_min_view_num() || + new_ring_radius.get_max_index() != this->get_max_view_num()) + { + error("error set_ring_radii_for_all_views: you need to use correct range of view numbers"); + } + + this->ring_radius = new_ring_radius; +} + +VectorWithOffset +ProjDataInfoGeneric::get_ring_radii_for_all_views() const +{ + if (this->ring_radius.get_min_index()==0 && this->ring_radius.get_max_index()==0) + { + VectorWithOffset out(this->get_min_view_num(), this->get_max_view_num()); + out.fill(this->ring_radius[0]); + return out; + } + else + return this->ring_radius; +} + +float +ProjDataInfoGeneric::get_ring_radius( const int view_num) const +{ + if (this->ring_radius.get_min_index()==0 && this->ring_radius.get_max_index()==0) + return ring_radius[0]; + else + return ring_radius[view_num]; +} + +float +ProjDataInfoGeneric::get_ring_spacing() const +{ return ring_spacing;} + +int +ProjDataInfoGeneric:: +get_view_mashing_factor() const +{ + // KT 10/05/2002 new assert + assert(get_scanner_ptr()->get_num_detectors_per_ring() > 0); + // KT 10/05/2002 moved assert here from constructor + assert(get_scanner_ptr()->get_num_detectors_per_ring() % (2*get_num_views()) == 0); + // KT 28/11/2001 do not pre-store anymore as set_num_views would invalidate it + return get_scanner_ptr()->get_num_detectors_per_ring() / (2*get_num_views()); +} + +Succeeded +ProjDataInfoGeneric:: +get_segment_num_for_ring_difference(int& segment_num, const int ring_diff) const +{ + if (!sampling_corresponds_to_physical_rings) + return Succeeded::no; + + // check currently necessary as reduce_segment does not reduce the size of the ring_diff arrays + if (ring_diff > get_max_ring_difference(get_max_segment_num()) || + ring_diff < get_min_ring_difference(get_min_segment_num())) + return Succeeded::no; + + if (!ring_diff_arrays_computed) + initialise_ring_diff_arrays(); + + segment_num = ring_diff_to_segment_num[ring_diff]; + // warning: relies on initialise_ring_diff_arrays to set invalid ring_diff to a too large segment_num + if (segment_num <= get_max_segment_num()) + return Succeeded::yes; + else + return Succeeded::no; +} + +Succeeded +ProjDataInfoGeneric:: +get_segment_axial_pos_num_for_ring_pair(int& segment_num, + int& ax_pos_num, + const int ring1, + const int ring2) const +{ + assert(0<=ring1); + assert(ring1get_num_rings()); + assert(0<=ring2); + assert(ring2get_num_rings()); + + // KT 01/08/2002 swapped rings + if (get_segment_num_for_ring_difference(segment_num, ring2-ring1) == Succeeded::no) + return Succeeded::no; + + // see initialise_ring_diff_arrays() for some info + ax_pos_num = (ring1 + ring2 - ax_pos_num_offset[segment_num])* + get_num_axial_poss_per_ring_inc(segment_num)/2; + return Succeeded::yes; +} + +const ProjDataInfoGeneric::RingNumPairs& +ProjDataInfoGeneric:: +get_all_ring_pairs_for_segment_axial_pos_num(const int segment_num, + const int axial_pos_num) const +{ + if (!ring_diff_arrays_computed) + initialise_ring_diff_arrays(); + if (is_null_ptr(segment_axial_pos_to_ring_pair[segment_num][axial_pos_num])) + compute_segment_axial_pos_to_ring_pair(segment_num, axial_pos_num); + return *segment_axial_pos_to_ring_pair[segment_num][axial_pos_num]; +} + +unsigned +ProjDataInfoGeneric:: +get_num_ring_pairs_for_segment_axial_pos_num(const int segment_num, + const int axial_pos_num) const +{ + return + static_cast( + this->get_all_ring_pairs_for_segment_axial_pos_num(segment_num,axial_pos_num).size()); +} + +END_NAMESPACE_STIR diff --git a/src/include/stir/ProjDataInfoGenericNoArcCorr.h b/src/include/stir/ProjDataInfoGenericNoArcCorr.h new file mode 100644 index 000000000..d9b894e4c --- /dev/null +++ b/src/include/stir/ProjDataInfoGenericNoArcCorr.h @@ -0,0 +1,293 @@ +// +// +/* + */ +/*! + \file + \ingroup projdata + + \brief Declaration of class stir::ProjDataInfoGenericNoArcCorr + +*/ +#ifndef __stir_ProjDataInfoGenericNoArcCorr_H__ +#define __stir_ProjDataInfoGenericNoArcCorr_H__ + + +#include "stir/ProjDataInfoGeneric.h" +#include "stir/GeometryBlocksOnCylindrical.h" +#include "stir/DetectionPositionPair.h" +#include "stir/VectorWithOffset.h" +#include "stir/CartesianCoordinate3D.h" + +START_NAMESPACE_STIR + +class Succeeded; +/*! + \ingroup projdata + \brief Projection data info for data which are not arc-corrected. + + For this class, 'tangential_pos_num' actually indexes an angular coordinate + with a particular angular sampling (usually given by half the angle between + detectors). That is + \code + get_s(Bin(..., tang_pos_num)) == ring_radius * sin(tang_pos_num*angular_increment) + \endcode + + This class also contains some functions specific for (static) full-ring PET + scanners. In this case, it is assumed that for 'raw' data (i.e. no mashing) + sinogram space is 'interleaved': 2 adjacent LOR_angles are + merged to 1 'view', while the corresponding bins are + interleaved: + + \verbatim + before interleaving after interleaving + a00 a01 a02 ... view 0: a00 a10 a01 a11 ... + a10 a11 ... + a20 a21 a22 ... view 1: a20 a30 a21 a31 ... + a30 a31 ... + \endverbatim + This (standard) interleaving is done because for 'odd' LOR_angles there + is no LOR which goes through the origin. + + + \par Interchanging the 2 detectors + + When the ring difference = 0 (i.e. a 2D - or direct - sinogram), + interchanging the 2 detectors does not change the LOR. This is why + (in 2D) one gets away with a full sinogram size of + num_views * 2 * num_views, where the size of 'detector-space' is + twice as large. + However, in 3D, interchanging the detectors, also interchanges the + rings. One has 2 options: + - have 1 sinogram with twice as many views, together with the rings + as 'unordered pair' (i.e. ring_difference is always >0) + - have 2 sinograms of the same size as in 2D, together with the rings + as 'ordered pair' (i.e. ring_difference can be positive and negative). + In STIR, we use the second convention. + + \todo The detector specific functions possibly do not belong in this class. + One can easily imagine a case where the theta,phi,s,t coordinates are as + described, but there is no real correspondence with detectors (for instance, + a rotating system). Maybe they should be moved somewhere else? + */ +class ProjDataInfoGenericNoArcCorr : public ProjDataInfoGeneric +{ +private: + typedef ProjDataInfoGeneric base_type; +#ifdef SWIG + // SWIG needs this typedef to be public + public: +#endif + typedef ProjDataInfoGenericNoArcCorr self_type; + +public: + //! Default constructor (leaves object in ill-defined state) + ProjDataInfoGenericNoArcCorr(); + //! Constructor completely specifying all parameters + /*! \see ProjDataInfoCylindrical class documentation for info on parameters */ + ProjDataInfoGenericNoArcCorr(const shared_ptr scanner_ptr, + const float ring_radius, const float angular_increment, + const VectorWithOffset& num_axial_pos_per_segment, + const VectorWithOffset& min_ring_diff_v, + const VectorWithOffset& max_ring_diff_v, + const int num_views,const int num_tangential_poss); + + //! Constructor which gets \a ring_radius and \a angular_increment from the scanner + /*! \a angular_increment is determined as Pi divided by the number of detectors in a ring. + \todo only suitable for full-ring PET scanners*/ + ProjDataInfoGenericNoArcCorr(const shared_ptr scanner_ptr, + const VectorWithOffset& num_axial_pos_per_segment, + const VectorWithOffset& min_ring_diff_v, + const VectorWithOffset& max_ring_diff_v, + const int num_views,const int num_tangential_poss); + + ProjDataInfo* clone() const; + + bool operator==(const self_type&) const; + + //! Gets s coordinate in mm + /*! \warning + This does \c not take the 'interleaving' into account which is + customarily applied to raw PET data. + */ + inline virtual float get_s(const Bin&) const; + + //! Gets angular increment (in radians) + inline float get_angular_increment() const; + + virtual std::string parameter_info() const; + + //! \name Functions that convert between bins and detection positions + //@{ + //! This gets view_num and tang_pos_num for a particular detector pair + /*! This function makes only sense if the scanner is a full-ring scanner + with discrete detectors and there is no rotation or wobble. + + \arg view runs currently from 0 to num_views-1 + + \arg tang_pos_num is centred around 0, where 0 corresponds + to opposing detectors. The maximum range of tangential positions for any + scanner is -(num_detectors)/2&) const; + + + //! This routine gets the detector pair corresponding to a bin. + /*! + \see get_det_pair_for_view_tangential_pos_num() for + restrictions. In addition, this routine only works for span=1 data, + i.e. no axial compression. + \todo use member template for the coordT type to support continuous detectors. + \warning Will call error() if certain conditions are not met. + */ + inline void + get_det_pos_pair_for_bin(DetectionPositionPair<>&, + const Bin&) const; + + //! This routine returns the number of detector pairs that correspond to a bin + unsigned int + get_num_det_pos_pairs_for_bin(const Bin&) const; + + //! This routine fills a vector with all the detector pairs that correspond to a bin. + /*! + \see ProjDataInfoCylindrical::get_all_ring_pairs_for_segment_axial_pos_num + for restrictions. + \todo It might be possible to return some weight factors in case there is + no many-to-one correspondence between detection positions and bins + (for instance for continuous detectors, or rotating scanners, or + arc-corrected data). + */ + void + get_all_det_pos_pairs_for_bin(vector >&, + const Bin&) const; + + //! This gets Bin coordinates for a particular detector pair + /*! + \return Succeeded::yes when a corresponding segment is found + \see get_view_tangential_pos_num_for_det_num_pair() for restrictions + \obsolete + */ + inline Succeeded + get_bin_for_det_pair(Bin&, + const int det1_num, const int ring1_num, + const int det2_num, const int ring2_num) const; + + + //! This routine gets the detector pair corresponding to a bin. + /*! + \see get_det_pair_for_view_tangential_pos_num() for + restrictions. In addition, this routine only works for span=1 data, + i.e. no axial compression. + \obsolete + */ + inline void + get_det_pair_for_bin( + int& det1_num, int& ring1_num, + int& det2_num, int& ring2_num, + const Bin&) const; + + //@} + + virtual + Bin + get_bin(const LOR&) const; + + + //! \name set of obsolete functions to go between bins<->LORs (will disappear!) + //@{ + /*! \warning These function take a different convention for the axial coordinate + compare to the get_m(), get_LOR() etc. In the current function, the axial coordinate (z) + is zero in the first ring, while for get_m() etc it is zero in the centre of the scanner. + \obsolete + */ + + virtual void find_cartesian_coordinates_of_detection(CartesianCoordinate3D& coord_1, + CartesianCoordinate3D& coord_2, + const Bin& bin) const; + + void find_cartesian_coordinates_given_scanner_coordinates (CartesianCoordinate3D& coord_1, + CartesianCoordinate3D& coord_2, + const int Ring_A,const int Ring_B, + const int det1, const int det2) const; + +private: + + float ring_radius; + float angular_increment; + + // used in get_view_tangential_pos_num_for_det_num_pair() + struct Det1Det2 { int det1_num; int det2_num; }; + mutable VectorWithOffset< VectorWithOffset > uncompressed_view_tangpos_to_det1det2; + mutable bool uncompressed_view_tangpos_to_det1det2_initialised; + //! build look-up table for get_view_tangential_pos_num_for_det_num_pair() + void initialise_uncompressed_view_tangpos_to_det1det2() const; + + // used in get_view_tangential_pos_num_for_det_num_pair() + // we prestore a lookup-table in terms for unmashed view/tangpos + struct ViewTangPosSwap { int view_num; int tang_pos_num; bool swap_detectors; }; + mutable VectorWithOffset< VectorWithOffset > det1det2_to_uncompressed_view_tangpos; + mutable bool det1det2_to_uncompressed_view_tangpos_initialised; + //! build look-up table for get_view_tangential_pos_num_for_det_num_pair() + void initialise_det1det2_to_uncompressed_view_tangpos() const; + + virtual bool blindly_equals(const root_type * const) const; + + shared_ptr crystal_map; + }; + +END_NAMESPACE_STIR + +#include "stir/ProjDataInfoGenericNoArcCorr.inl" + +#endif diff --git a/src/include/stir/ProjDataInfoGenericNoArcCorr.inl b/src/include/stir/ProjDataInfoGenericNoArcCorr.inl new file mode 100644 index 000000000..537039747 --- /dev/null +++ b/src/include/stir/ProjDataInfoGenericNoArcCorr.inl @@ -0,0 +1,143 @@ +/* + +*/ +/*! + + \file + \ingroup projdata + + \brief Implementation of inline functions of class ProjDataInfoGenericNoArcCorr + +*/ + + +#include "stir/Bin.h" +#include "stir/Succeeded.h" +#include "stir/LORCoordinates.h" +#include + +START_NAMESPACE_STIR + +//Parisa: changed +/* + In cylindrical s is calculated using sin(tang_pos*angular_increment) = sin(beta) + Here in Generic it is calculated directly from corresponding lor +*/ +float +ProjDataInfoGenericNoArcCorr:: +get_s(const Bin& bin) const +{ + LORInAxialAndNoArcCorrSinogramCoordinates lor; + get_LOR(lor, bin); + if (bin.view_num()==0 && lor.phi()>0.1) + return -1*ring_radius * sin(lor.beta()); + return ring_radius * sin(lor.beta()); +} + +float +ProjDataInfoGenericNoArcCorr:: +get_angular_increment() const +{ + return angular_increment; +} + +void +ProjDataInfoGenericNoArcCorr:: +get_det_num_pair_for_view_tangential_pos_num( + int& det1_num, + int& det2_num, + const int view_num, + const int tang_pos_num) const +{ + assert(get_view_mashing_factor() == 1); + if (!uncompressed_view_tangpos_to_det1det2_initialised) + initialise_uncompressed_view_tangpos_to_det1det2(); + + det1_num = uncompressed_view_tangpos_to_det1det2[view_num][tang_pos_num].det1_num; + det2_num = uncompressed_view_tangpos_to_det1det2[view_num][tang_pos_num].det2_num; +} + +bool +ProjDataInfoGenericNoArcCorr:: +get_view_tangential_pos_num_for_det_num_pair(int& view_num, + int& tang_pos_num, + const int det1_num, + const int det2_num) const +{ + assert(det1_num!=det2_num); + if (!det1det2_to_uncompressed_view_tangpos_initialised) + initialise_det1det2_to_uncompressed_view_tangpos(); + + view_num = + det1det2_to_uncompressed_view_tangpos[det1_num][det2_num].view_num/get_view_mashing_factor(); + tang_pos_num = + det1det2_to_uncompressed_view_tangpos[det1_num][det2_num].tang_pos_num; + return + det1det2_to_uncompressed_view_tangpos[det1_num][det2_num].swap_detectors; +} + +Succeeded +ProjDataInfoGenericNoArcCorr:: +get_bin_for_det_pair(Bin& bin, + const int det_num1, const int ring_num1, + const int det_num2, const int ring_num2) const +{ + if (get_view_tangential_pos_num_for_det_num_pair(bin.view_num(), bin.tangential_pos_num(), det_num1, det_num2)) + return get_segment_axial_pos_num_for_ring_pair(bin.segment_num(), bin.axial_pos_num(), ring_num1, ring_num2); + else + return get_segment_axial_pos_num_for_ring_pair(bin.segment_num(), bin.axial_pos_num(), ring_num2, ring_num1); +} + +Succeeded +ProjDataInfoGenericNoArcCorr:: +get_bin_for_det_pos_pair(Bin& bin, + const DetectionPositionPair<>& dp) const +{ + return + get_bin_for_det_pair(bin, + dp.pos1().tangential_coord(), + dp.pos1().axial_coord(), + dp.pos2().tangential_coord(), + dp.pos2().axial_coord()); +} + +void +ProjDataInfoGenericNoArcCorr:: +get_det_pair_for_bin( + int& det_num1, int& ring_num1, + int& det_num2, int& ring_num2, + const Bin& bin) const +{ + get_det_num_pair_for_view_tangential_pos_num(det_num1, det_num2, bin.view_num(), bin.tangential_pos_num()); + get_ring_pair_for_segment_axial_pos_num( ring_num1, ring_num2, bin.segment_num(), bin.axial_pos_num()); +} + +void +ProjDataInfoGenericNoArcCorr:: +get_det_pos_pair_for_bin( + DetectionPositionPair<>& dp, + const Bin& bin) const +{ + //lousy work around because types don't match TODO remove! +#if 1 + int t1=dp.pos1().tangential_coord(), + a1=dp.pos1().axial_coord(), + t2=dp.pos2().tangential_coord(), + a2=dp.pos2().axial_coord(); + get_det_pair_for_bin(t1, a1, t2, a2, bin); + dp.pos1().tangential_coord()=t1; + dp.pos1().axial_coord()=a1; + dp.pos2().tangential_coord()=t2; + dp.pos2().axial_coord()=a2; + +#else + + get_det_pair_for_bin(dp.pos1().tangential_coord(), + dp.pos1().axial_coord(), + dp.pos2().tangential_coord(), + dp.pos2().axial_coord(), + bin); +#endif +} + +END_NAMESPACE_STIR diff --git a/src/include/stir/Scanner.h b/src/include/stir/Scanner.h index c622b1a4c..d74b7ebbd 100644 --- a/src/include/stir/Scanner.h +++ b/src/include/stir/Scanner.h @@ -37,8 +37,15 @@ #define __stir_buildblock_SCANNER_H__ #include "stir/DetectionPosition.h" +#include "stir/CartesianCoordinate3D.h" #include #include +#include +#include +#include +#include +#include +#include START_NAMESPACE_STIR @@ -137,6 +144,7 @@ class Scanner int num_detector_layers_v, float energy_resolution_v = -1.0f, float reference_energy_v = -1.0f, + const std::string& crystal_map_file_name = "", const std::string& scanner_orientation_v = "", const std::string& scanner_geometry_v = "", float axial_crystal_spacing_v = -1.0f, @@ -162,6 +170,7 @@ class Scanner int num_detector_layers_v, float energy_resolution_v = -1.0f, float reference_energy_v = -1.0f, + const std::string& crystal_map_file_name = "", const std::string& scanner_orientation_v = "", const std::string& scanner_geometry_v = "", float axial_crystal_spacing_v = -1.0f, @@ -386,6 +395,9 @@ class Scanner // Get the transaxial singles bin coordinate from a singles bin. inline int get_transaxial_singles_unit(int singles_bin_index) const; + // Functions to retrieve the coordinates and the stir_id out of the scanner + inline stir::DetectionPosition<> get_detpos_from_id(const stir::DetectionPosition<> det_pos) const; + inline stir::CartesianCoordinate3D get_coords_from_detpos(const stir::DetectionPosition<> det_pos) const; private: Type type; @@ -435,6 +447,28 @@ class Scanner float transaxial_crystal_spacing; /*! crystal pitch in transaxial direction in mm*/ float axial_block_spacing; /*! block pitch in axial direction in mm*/ float transaxial_block_spacing; /*! block pitch in transaxial direction in mm*/ + + //! + //!\brief map and hash function for saving the coords in generic geometry + //!\author Michael Roethlisberger + struct ihash + : std::unary_function , std::size_t> + { + std::size_t operator()(stir::DetectionPosition<> const& detpos) const + { + std::size_t seed = 0; + boost::hash_combine(seed, detpos.axial_coord()); + boost::hash_combine(seed, detpos.radial_coord()); + boost::hash_combine(seed, detpos.tangential_coord()); + return seed; + } + }; + boost::unordered_map, stir::DetectionPosition<>, ihash> input_index_to_stir_index; + boost::unordered_map, stir::CartesianCoordinate3D, ihash> input_index_to_coord; + std::string crystal_map_file_name; + + // function to create the maps + void read_detectormap_from_file( const std::string& filename ); // ! set all parameters, case where default_num_arccorrected_bins==max_num_non_arccorrected_bins @@ -453,6 +487,7 @@ class Scanner int num_detector_layers_v, float energy_resolution_v = -1.0f, float reference_energy = -1.0f, + const std::string& crystal_map_file_name = "", const std::string& scanner_orientation_v = "", const std::string& scanner_geometry_v = "", float axial_crystal_spacing_v = -1.0f, @@ -477,6 +512,7 @@ class Scanner int num_detector_layers_v, float energy_resolution_v = -1.0f, float reference_energy = -1.0f, + const std::string& crystal_map_file_name = "", const std::string& scanner_orientation_v = "", const std::string& scanner_geometry_v = "", float axial_crystal_spacing_v = -1.0f, diff --git a/src/include/stir/Scanner.inl b/src/include/stir/Scanner.inl index fde3c2078..cb05b74c3 100644 --- a/src/include/stir/Scanner.inl +++ b/src/include/stir/Scanner.inl @@ -445,7 +445,16 @@ Scanner::get_transaxial_singles_unit(int singles_bin_index) const { return(singles_bin_index % get_num_transaxial_singles_units()); } - +// For retrieving the coordinates / detector, ring id from the scanner +stir::DetectionPosition<> +Scanner::get_detpos_from_id(const stir::DetectionPosition<> det_pos) const{ + return input_index_to_stir_index.at(det_pos); +} +// Attention, this uses the stir coordinate, not the one from listmode +stir::CartesianCoordinate3D +Scanner::get_coords_from_detpos(const stir::DetectionPosition<> det_pos) const{ + return input_index_to_coord.at(det_pos); +} END_NAMESPACE_STIR diff --git a/src/include/stir/listmode/CListRecordSAFIR.inl b/src/include/stir/listmode/CListRecordSAFIR.inl index 2d0959c54..0ea38ffc9 100644 --- a/src/include/stir/listmode/CListRecordSAFIR.inl +++ b/src/include/stir/listmode/CListRecordSAFIR.inl @@ -30,6 +30,7 @@ #include "stir/ProjDataInfoCylindricalNoArcCorr.h" #include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" +#include "stir/ProjDataInfoGenericNoArcCorr.h" #include "stir/CartesianCoordinate3D.h" START_NAMESPACE_STIR @@ -70,45 +71,63 @@ get_bin(Bin& bin, const ProjDataInfo& proj_data_info) const <<"\tring2="< c1 = map->get_detector_coordinate(det_pos_pair.pos1()); - stir::CartesianCoordinate3D c2 = map->get_detector_coordinate(det_pos_pair.pos2()); - int det1, det2, ring1, ring2; - - if(proj_data_info.get_scanner_ptr()->get_scanner_geometry() == "Cylindrical" - && bin.get_bin_value()!=-1) + // Case for generic scanner + if(proj_data_info.get_scanner_ptr()->get_scanner_geometry() == "Generic" && bin.get_bin_value() != -1) { - //const ProjDataInfoCylindricalNoArcCorr& proj_data_info_cyl = - // dynamic_cast(proj_data_info); - - LORAs2Points lor; - lor.p1() = c1; - lor.p2() = c2; - bin = proj_data_info.get_bin(lor); + const ProjDataInfoGenericNoArcCorr& proj_data_info_gen = + dynamic_cast(proj_data_info); + //transform det_pos_pair into stir coordinates + DetectionPosition<> pos1 = det_pos_pair.pos1(); + DetectionPosition<> pos2 = det_pos_pair.pos2(); + det_pos_pair.pos1() = proj_data_info_gen.get_scanner_ptr()->get_detpos_from_id(pos1); + det_pos_pair.pos2() = proj_data_info_gen.get_scanner_ptr()->get_detpos_from_id(pos2); + + if (proj_data_info_gen.get_bin_for_det_pos_pair(bin, det_pos_pair) == Succeeded::yes) + bin.set_bin_value(1); + else + bin.set_bin_value(-1); } - else if(proj_data_info.get_scanner_ptr()->get_scanner_geometry() == "BlocksOnCylindrical" - && bin.get_bin_value()!=-1) + else { - const ProjDataInfoBlocksOnCylindricalNoArcCorr& proj_data_info_blk = - dynamic_cast(proj_data_info); - - if (proj_data_info_blk.find_scanner_coordinates_given_cartesian_coordinates(det1, det2, ring1, ring2, c1, c2) == Succeeded::no) - bin.set_bin_value(-1); - else - { - assert(!(ring1<0 || - ring1>=proj_data_info_blk.get_scanner_ptr()->get_num_rings() || - ring2<0 || - ring2>=proj_data_info_blk.get_scanner_ptr()->get_num_rings()) - ); - - if(proj_data_info_blk.get_bin_for_det_pair(bin, det1, ring1, det2, ring2)==Succeeded::yes) - bin.set_bin_value(1); - else - bin.set_bin_value(-1); - } + if(!map) stir::error("Crystal map not set."); + + stir::CartesianCoordinate3D c1 = map->get_detector_coordinate(det_pos_pair.pos1()); + stir::CartesianCoordinate3D c2 = map->get_detector_coordinate(det_pos_pair.pos2()); + int det1, det2, ring1, ring2; + + if(proj_data_info.get_scanner_ptr()->get_scanner_geometry() == "Cylindrical" + && bin.get_bin_value()!=-1) + { + //const ProjDataInfoCylindricalNoArcCorr& proj_data_info_cyl = + // dynamic_cast(proj_data_info); + + LORAs2Points lor; + lor.p1() = c1; + lor.p2() = c2; + bin = proj_data_info.get_bin(lor); + } + else if(proj_data_info.get_scanner_ptr()->get_scanner_geometry() == "BlocksOnCylindrical" + && bin.get_bin_value()!=-1) + { + const ProjDataInfoBlocksOnCylindricalNoArcCorr& proj_data_info_blk = + dynamic_cast(proj_data_info); + + if (proj_data_info_blk.find_scanner_coordinates_given_cartesian_coordinates(det1, det2, ring1, ring2, c1, c2) == Succeeded::no) + bin.set_bin_value(-1); + else + { + assert(!(ring1<0 || + ring1>=proj_data_info_blk.get_scanner_ptr()->get_num_rings() || + ring2<0 || + ring2>=proj_data_info_blk.get_scanner_ptr()->get_num_rings()) + ); + + if(proj_data_info_blk.get_bin_for_det_pair(bin, det1, ring1, det2, ring2)==Succeeded::yes) + bin.set_bin_value(1); + else + bin.set_bin_value(-1); + } + } } } diff --git a/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl b/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl index 59a80f25c..01f28368b 100644 --- a/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl +++ b/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl @@ -35,6 +35,7 @@ #include "stir/ProjDataInfoCylindrical.h" #include "stir/recon_buildblock/SymmetryOperations_PET_CartesianGrid.h" #include "stir/ProjDataInfoBlocksOnCylindrical.h" +#include "stir/ProjDataInfoGeneric.h" START_NAMESPACE_STIR @@ -121,7 +122,29 @@ find_transform_z( } } - return transform_z; + // generic implementation + else + { + ProjDataInfoGeneric* proj_data_info_blk_ptr = + static_cast(proj_data_info_ptr.get()); + + const float delta = proj_data_info_blk_ptr->get_average_ring_difference(segment_num); + + // Find symmetric value in Z by 'mirroring' it around the centre z of the LOR: + // Z+Q = 2*centre_of_LOR_in_image_coordinates == transform_z + { + // first compute it as floating point (although it has to be an int really) + const float transform_z_float = (2*num_planes_per_axial_pos[segment_num]*(axial_pos_num) + + num_planes_per_scanner_ring*delta + + 2*axial_pos_to_z_offset[segment_num]); + // now use rounding to be safe + int transform_z = (int)floor(transform_z_float + 0.5); + assert(fabs(transform_z-transform_z_float) < 10E-4); + + return transform_z; + } + } + return transform_z; } SymmetryOperation* @@ -228,6 +251,11 @@ find_sym_op_bin0( } } + if(proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="Generic") + { + // No symmetry is implemented for generic scanner + return new TrivialSymmetryOperation(); + } } // from symmetries @@ -404,6 +432,11 @@ find_sym_op_general_bin( return new TrivialSymmetryOperation(); } } + if(proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="Generic") + { + // No symmetry is implemented for generic scanner + return new TrivialSymmetryOperation(); + } } @@ -559,7 +592,83 @@ find_basic_bin(int &segment_num, int &view_num, int &axial_pos_num, int &tangent } } } - return change; + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="Generic") + { //same procedure as for the block implementation above + ProjDataInfoGeneric* proj_data_info_gen_ptr = + static_cast(proj_data_info_ptr.get()); + if (do_symmetry_shift_z) + { + int ring1, ring2; + proj_data_info_gen_ptr->get_ring_pair_for_segment_axial_pos_num (ring1, ring2, segment_num, axial_pos_num); + + int axial_crys_diff = ring2 - ring1; + int num_axial_crys_per_block = proj_data_info_ptr->get_scanner_ptr()->get_num_axial_crystals_per_block(); + int axial_blk_diff = ring2/num_axial_crys_per_block - ring1/num_axial_crys_per_block; + + if (axial_crys_diff >=0) + { // seg_num > 0 + if (axial_crys_diff%num_axial_crys_per_block == 0) + { // axial block difference can be only axial_crys_diff/num_axial_crys_per_block. So we only have one type of related bins + // basic bin will be the first lor from the corresponding group + ring1= (ring1/num_axial_crys_per_block)*num_axial_crys_per_block; + ring2= ring1 + axial_crys_diff; + } + else + { /* axial block difference can be axial_crys_diff/num_axial_crys_per_block + or one less. + So we can have two types of of related bins*/ + if (axial_blk_diff == axial_crys_diff/num_axial_crys_per_block) + {// basic bin will be the first lor from the corresponding group + ring1= (ring1/num_axial_crys_per_block)*num_axial_crys_per_block; + ring2= ring1 + axial_crys_diff; + } + else if (axial_blk_diff > axial_crys_diff/num_axial_crys_per_block) + {// basic bin will be the last lor from the corresponding group + ring1= (ring1/num_axial_crys_per_block)*num_axial_crys_per_block + num_axial_crys_per_block-1; + ring2= ring1 + axial_crys_diff; + } + } + } + else if (axial_crys_diff <0) + { // seg_num < 0 + if (abs(axial_crys_diff)%num_axial_crys_per_block == 0) + { // axial block difference can be only axial_crys_diff/num_axial_crys_per_block. So we only have one type of related bins + // basic bin will be the first lor from the corresponding group + ring2= (ring2/num_axial_crys_per_block)*num_axial_crys_per_block; + ring1= ring2 - axial_crys_diff; + } + else + { /* axial block difference can be axial_crys_diff/num_axial_crys_per_block + or one less. + So we can have two types of of related bins*/ + if (abs(axial_blk_diff) == abs(axial_crys_diff)/num_axial_crys_per_block) + {// basic bin will be the first lor from the corresponding group + ring2= (ring2/num_axial_crys_per_block)*num_axial_crys_per_block; + ring1= ring2 - axial_crys_diff; + } + else if (abs(axial_blk_diff) > abs(axial_crys_diff)/num_axial_crys_per_block) + {// basic bin will be the last lor from the corresponding group + ring2= (ring2/num_axial_crys_per_block)*num_axial_crys_per_block + num_axial_crys_per_block-1; + ring1= ring2 - axial_crys_diff; + } + } + } + + int segment_num_temp, axial_pos_num_temp; + proj_data_info_gen_ptr-> + get_segment_axial_pos_num_for_ring_pair(segment_num_temp, axial_pos_num_temp, ring1, ring2); + + if (segment_num_temp != segment_num) + error("segment number shouldn't change in basic bin when implementing only symmetry in z.\n" + "segment_num = %d while segment_num_temp = %d \n", segment_num, segment_num_temp); + else if (axial_pos_num_temp != axial_pos_num) + { + axial_pos_num = axial_pos_num_temp; + change = true; + } + } + } + return change; } bool @@ -646,6 +755,11 @@ num_related_bins(const Bin& b) const } } } + //generic implementation + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="Generic") + { + num = 1; + } return num; } @@ -693,6 +807,24 @@ get_related_bins_factorised(std::vector& ax_tang_poss, const B } } } + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="Generic") + { + for (int axial_pos_num=do_symmetry_shift_z?min_axial_pos_num:b.axial_pos_num(); + axial_pos_num <= (do_symmetry_shift_z?max_axial_pos_num:b.axial_pos_num()); + ++axial_pos_num) + { + if (b.tangential_pos_num() >= min_tangential_pos_num && + b.tangential_pos_num() <= max_tangential_pos_num) + { + Bin basic_bin(b); + find_basic_bin(basic_bin); + Bin bin_temp(b.segment_num(), b.view_num(), axial_pos_num, b.tangential_pos_num()); + find_basic_bin(bin_temp); + if (basic_bin == bin_temp) + ax_tang_poss.push_back(AxTangPosNumbers(axial_pos_num, b.tangential_pos_num())); + } + } + } } diff --git a/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx b/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx index 82f6a1ace..d5040122b 100644 --- a/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx +++ b/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx @@ -181,6 +181,47 @@ find_relation_between_coordinate_systems(int& num_planes_per_scanner_ring, } } +// overloading for generic case +static void +find_relation_between_coordinate_systems(int& num_planes_per_scanner_ring, + VectorWithOffset& num_planes_per_axial_pos, + VectorWithOffset& axial_pos_to_z_offset, + const ProjDataInfoGeneric* proj_data_info_blk_ptr, + const DiscretisedDensityOnCartesianGrid<3,float> * cartesian_grid_info_ptr) +{ + const int min_segment_num = proj_data_info_blk_ptr->get_min_segment_num(); + const int max_segment_num = proj_data_info_blk_ptr->get_max_segment_num(); + + num_planes_per_axial_pos = VectorWithOffset(min_segment_num, max_segment_num); + axial_pos_to_z_offset = VectorWithOffset(min_segment_num, max_segment_num); + + // TODO and WARNING: get_grid_spacing()[1] is z() + const float image_plane_spacing = cartesian_grid_info_ptr->get_grid_spacing()[1]; + + + const float num_planes_per_scanner_ring_float = + proj_data_info_blk_ptr->get_ring_spacing() / image_plane_spacing; + + num_planes_per_scanner_ring = round(num_planes_per_scanner_ring_float); + + + for (int segment_num=min_segment_num; segment_num<=max_segment_num; ++segment_num) + { + const float num_planes_per_axial_pos_float = + proj_data_info_blk_ptr->get_axial_sampling(segment_num)/image_plane_spacing; + + num_planes_per_axial_pos[segment_num] = round(num_planes_per_axial_pos_float); + + const float delta = proj_data_info_blk_ptr->get_average_ring_difference(segment_num); + axial_pos_to_z_offset[segment_num] = + (cartesian_grid_info_ptr->get_max_index() + cartesian_grid_info_ptr->get_min_index())/2.F + - cartesian_grid_info_ptr->get_origin().z()/image_plane_spacing + - (num_planes_per_axial_pos[segment_num] + *(proj_data_info_blk_ptr->get_max_axial_pos_num(segment_num) + + proj_data_info_blk_ptr->get_min_axial_pos_num(segment_num)) + + num_planes_per_scanner_ring*delta)/2; + } +} /*! The DiscretisedDensity pointer has to point to an object of type DiscretisedDensityOnCartesianGrid (or a derived type). @@ -338,6 +379,54 @@ DataSymmetriesForBins_PET_CartesianGrid static_cast(proj_data_info_ptr.get()), cartesian_grid_info_ptr); } + // generic implementation + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="Generic") + { + if (dynamic_cast(proj_data_info_ptr.get()) == NULL) + error("DataSymmetriesForBins_PET_CartesianGrid constructed with wrong type of ProjDataInfo: %s\n" + "(can only handle projection data corresponding to blocks on a cylinder)\n", + typeid(*proj_data_info_ptr).name()); + + const DiscretisedDensityOnCartesianGrid<3,float> * + cartesian_grid_info_ptr = + dynamic_cast *> + (image_info_ptr.get()); + + if (cartesian_grid_info_ptr == NULL) + error("DataSymmetriesForBins_PET_CartesianGrid constructed with wrong type of image info: %s\n", + typeid(*image_info_ptr).name()); + + // WARNING get_grid_spacing()[1] == z + const float z_origin_in_planes = + image_info_ptr->get_origin().z()/cartesian_grid_info_ptr->get_grid_spacing()[1]; + // z_origin_in_planes should be an integer + if (fabs(round(z_origin_in_planes) - z_origin_in_planes) > 1.E-3F) + error("DataSymmetriesForBins_PET_CartesianGrid: the shift in the " + "z-direction of the origin (which is %g) should be a multiple of the plane " + "separation (%g)\n", + image_info_ptr->get_origin().z(), cartesian_grid_info_ptr->get_grid_spacing()[1]); + + if (this->do_symmetry_90degrees_min_phi|| + this->do_symmetry_180degrees_min_phi|| + this->do_symmetry_swap_segment|| + this->do_symmetry_swap_s|| + this->do_symmetry_shift_z) + { + warning("Disabling all symmetries since they are not implemented in generic geometry."); + this->do_symmetry_90degrees_min_phi = + this->do_symmetry_180degrees_min_phi = + this->do_symmetry_swap_segment = + this->do_symmetry_swap_s = + this->do_symmetry_shift_z = false; + } + + find_relation_between_coordinate_systems( + num_planes_per_scanner_ring, + num_planes_per_axial_pos, + axial_pos_to_z_offset, + static_cast(proj_data_info_ptr.get()), + cartesian_grid_info_ptr); + } } diff --git a/src/recon_buildblock/ProjMatrixByBinUsingRayTracing.cxx b/src/recon_buildblock/ProjMatrixByBinUsingRayTracing.cxx index 2e21c5425..2ad1d6b6a 100644 --- a/src/recon_buildblock/ProjMatrixByBinUsingRayTracing.cxx +++ b/src/recon_buildblock/ProjMatrixByBinUsingRayTracing.cxx @@ -60,6 +60,7 @@ #include #include #include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" +#include "stir/ProjDataInfoGenericNoArcCorr.h" #ifndef STIR_NO_NAMESPACE using std::min; @@ -339,7 +340,7 @@ set_up( } } } - else + else if(proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()== "BlocksOnCylindrical") { const ProjDataInfoBlocksOnCylindricalNoArcCorr * proj_data_info_blk_ptr = dynamic_cast(proj_data_info_ptr.get()); @@ -369,6 +370,36 @@ set_up( } } } + else + { + const ProjDataInfoGenericNoArcCorr * proj_data_info_blk_ptr = + dynamic_cast(proj_data_info_ptr.get()); + + if (proj_data_info_blk_ptr== 0) + { + warning("ProjMatrixByBinUsingRayTracing: use_actual_detector_boundaries" + " is reset to false as the projection data should be non-arccorected.\n"); + use_actual_detector_boundaries = false; + } + else + { + bool nocompression = + proj_data_info_blk_ptr->get_view_mashing_factor()==1; + for (int segment_num=proj_data_info_blk_ptr->get_min_segment_num(); + nocompression && segment_num <= proj_data_info_blk_ptr->get_max_segment_num(); + ++segment_num) + nocompression= + proj_data_info_blk_ptr->get_min_ring_difference(segment_num) == + proj_data_info_blk_ptr->get_max_ring_difference(segment_num); + + if (!nocompression) + { + warning("ProjMatrixByBinUsingRayTracing: use_actual_detector_boundaries" + " is reset to false as the projection data as either mashed or uses axial compression\n"); + use_actual_detector_boundaries = false; + } + } + } if (use_actual_detector_boundaries) warning("ProjMatrixByBinUsingRayTracing: use_actual_detector_boundaries==true\n"); @@ -625,7 +656,7 @@ calculate_proj_matrix_elems_for_one_bin( if (fabs(s_in_mm-old_s_in_mm)>proj_data_info_ptr->get_sampling_in_s(bin)*.0001) warning("tangential_pos_num %d old_s_in_mm %g new_s_in_mm %g\n",bin.tangential_pos_num(), old_s_in_mm, s_in_mm); } - else + else if(proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()== "BlocksOnCylindrical") { // can be static_cast later on const ProjDataInfoBlocksOnCylindricalNoArcCorr& proj_data_info_noarccor = @@ -634,6 +665,14 @@ calculate_proj_matrix_elems_for_one_bin( phi = proj_data_info_noarccor.get_phi(bin); s_in_mm = proj_data_info_noarccor.get_s(bin); } + else + { + const ProjDataInfoGenericNoArcCorr& proj_data_info_noarccor = + dynamic_cast(*proj_data_info_ptr); + + phi = proj_data_info_noarccor.get_phi(bin); + s_in_mm = proj_data_info_noarccor.get_s(bin); + } } From b8c92a0165d3f6a29d42215ac4dba49b953b1bc3 Mon Sep 17 00:00:00 2001 From: Michael Roethlisberger Date: Mon, 1 Oct 2018 15:37:11 +0200 Subject: [PATCH 03/81] crystal map only asked if 'Generic' and built-in error for maps --- src/buildblock/Scanner.cxx | 13 +++++++++---- src/include/stir/Scanner.inl | 6 ++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/buildblock/Scanner.cxx b/src/buildblock/Scanner.cxx index a378b8bed..7db649bfc 100644 --- a/src/buildblock/Scanner.cxx +++ b/src/buildblock/Scanner.cxx @@ -1109,14 +1109,19 @@ Scanner* Scanner::ask_parameters() int num_detector_layers = ask_num("Enter number of detector layers per block: ",1,100,1); - - const string crystal_map_file_name = - ask_string("Enter the path to the crystal map: ", ""); + const string ScannerOrientation = ask_string("Enter the scanner orientation, i.e. which axis passes through two opposite blocks ('X' or 'Y')", "Y"); const string ScannerGeometry = ask_string("Enter the scanner geometry ( BlocksOnCylindrical / Cylindrical / Generic ) :", "Cylindrical"); - float AxialCrystalSpacing= + + string crystal_map_file_name = ""; + if (ScannerGeometry == "Generic") { + crystal_map_file_name = + ask_string("Enter the name of the crystal map: ", ""); + } + + float AxialCrystalSpacing= ask_num("Enter crystal spacing in axial direction (in mm): ",0.F,30.F,6.75F); float TransaxialCrystalSpacing= ask_num("Enter crystal spacing in transaxial direction (in mm): ",0.F,30.F,6.75F); diff --git a/src/include/stir/Scanner.inl b/src/include/stir/Scanner.inl index cb05b74c3..f49d3f205 100644 --- a/src/include/stir/Scanner.inl +++ b/src/include/stir/Scanner.inl @@ -448,11 +448,17 @@ Scanner::get_transaxial_singles_unit(int singles_bin_index) const { // For retrieving the coordinates / detector, ring id from the scanner stir::DetectionPosition<> Scanner::get_detpos_from_id(const stir::DetectionPosition<> det_pos) const{ + if (crystal_map_file_name == ""){ + stir::error("Crystal Map not defined!"); + } return input_index_to_stir_index.at(det_pos); } // Attention, this uses the stir coordinate, not the one from listmode stir::CartesianCoordinate3D Scanner::get_coords_from_detpos(const stir::DetectionPosition<> det_pos) const{ + if (crystal_map_file_name == ""){ + stir::error("Crystal Map not defined!"); + } return input_index_to_coord.at(det_pos); } From cf4340e6ab26f8da6a18335f4632666c400ec4a8 Mon Sep 17 00:00:00 2001 From: Michael Roethlisberger Date: Fri, 12 Oct 2018 11:05:25 +0200 Subject: [PATCH 04/81] corrections for issue 1 --- examples/SAFIR_genericScanner/README.txt | 7 ++++--- examples/SAFIR_genericScanner/muppet.hs | 2 +- src/buildblock/Scanner.cxx | 21 +++++++++++++++++++-- src/include/stir/Scanner.h | 6 +++++- src/include/stir/Scanner.inl | 6 ++++++ 5 files changed, 35 insertions(+), 7 deletions(-) diff --git a/examples/SAFIR_genericScanner/README.txt b/examples/SAFIR_genericScanner/README.txt index 7e4eda640..e9b9adba9 100644 --- a/examples/SAFIR_genericScanner/README.txt +++ b/examples/SAFIR_genericScanner/README.txt @@ -23,11 +23,12 @@ found in the online documentation of STIR and another example file can be found in 'example/samples/lm_to_projdata.par'. The projdata template file can be created with the utility -'create_projdata_template'. The new entries for this implementation are 'name of +'create_projdata_template'. The new entries for this implementation are 'Name of crystal map' which defines the name of the file which contains all coordinates of the detectors like the example file 'DualRingPrototype_crystal_map.txt' and -the new 'Scanner geometry' option 'Generic'. For the other entries please check -the online documentation. +it contains as well the new 'Scanner geometry' option 'Generic'. The 'crystal map' +file should be in the same folder as the template file. For the other entries +please check the online documentation. 'forward_project' and 'backproject' are the utilities that convert the sinogram data into an image and vice versa. diff --git a/examples/SAFIR_genericScanner/muppet.hs b/examples/SAFIR_genericScanner/muppet.hs index b72852b5a..9a59a5766 100644 --- a/examples/SAFIR_genericScanner/muppet.hs +++ b/examples/SAFIR_genericScanner/muppet.hs @@ -41,7 +41,7 @@ Number of crystals per block in transaxial direction := 8 Number of detector layers := 1 Number of crystals per singles unit in axial direction := 1 Number of crystals per singles unit in transaxial direction := 1 -name of crystal map := DualRingPrototype_crystal_map.txt +Name of crystal map := DualRingPrototype_crystal_map.txt Scanner orientation (X or Y) := Y Scanner geometry (BlocksOnCylindrical/Cylindrical/Generic) := Generic Distance between crystals in axial direction (cm) := 0.22 diff --git a/src/buildblock/Scanner.cxx b/src/buildblock/Scanner.cxx index 7db649bfc..5609cf045 100644 --- a/src/buildblock/Scanner.cxx +++ b/src/buildblock/Scanner.cxx @@ -863,6 +863,23 @@ check_consistency() const get_transaxial_block_spacing(), get_num_transaxial_blocks_per_bucket(), get_inner_ring_radius()); return Succeeded::no; } + else if (get_scannner_geometry() == "Generic") + { //! Check if the crystal map is correct and given + if (get_crystal_map() == "") + { + warning("No crystal map is provided. The scanner geometry Generic needs it! Please provide one."); + return Succeeded::no; + } + else + { + std::ifstream crystal_map(get_crystal_map()); + if( !crystal_map) + { + warning("No correct crystal map provided. Please check the file name."); + return Succeeded::no; + } + } + } } @@ -978,8 +995,8 @@ Scanner::parameter_info() const << get_num_transaxial_crystals_per_singles_unit() << '\n'; //crystal map if (crystal_map_file_name != "") - s << "name of crystal map := " - << crystal_map_file_name << '\n'; + s << "Name of crystal map := " + << get_crystal_map() << '\n'; //block geometry description if (get_scanner_orientation() != "" && get_scanner_geometry() != "") diff --git a/src/include/stir/Scanner.h b/src/include/stir/Scanner.h index d74b7ebbd..a576c5285 100644 --- a/src/include/stir/Scanner.h +++ b/src/include/stir/Scanner.h @@ -300,7 +300,11 @@ class Scanner //! get block spacing in transaxial direction inline float get_transaxial_block_spacing() const; //@} (end of get block geometry info) - + + //! \name functions to get generic geometry info + //! get scanner orientation + inline std::string get_crystal_map() const; + //@} (end of block/bucket info) //@} (end of get geometrical info) diff --git a/src/include/stir/Scanner.inl b/src/include/stir/Scanner.inl index f49d3f205..728726312 100644 --- a/src/include/stir/Scanner.inl +++ b/src/include/stir/Scanner.inl @@ -266,6 +266,12 @@ Scanner::get_axial_block_spacing() const return axial_block_spacing; } +std::string +Scanner::get_crystal_map() const +{ + return crystal_map_file_name; +} + //************************ set ******************************8 void Scanner::set_type(const Type & new_type) From 616d8c67302dd320a8f72fcf9d314cab0ea777ea Mon Sep 17 00:00:00 2001 From: Michael Roethlisberger Date: Fri, 12 Oct 2018 13:59:24 +0200 Subject: [PATCH 05/81] issue #2 first patch of corrections --- src/IO/InterfileHeader.cxx | 2 +- src/IO/interfile.cxx | 48 +++++----- src/buildblock/Scanner.cxx | 76 +++++++++------- src/include/stir/IO/InterfileHeader.h | 4 +- src/include/stir/Scanner.h | 5 +- src/include/stir/Scanner.inl | 3 +- ...ataSymmetriesForBins_PET_CartesianGrid.cxx | 88 +++++++++---------- .../ProjMatrixByBinUsingRayTracing.cxx | 74 ++++++++-------- 8 files changed, 157 insertions(+), 143 deletions(-) diff --git a/src/IO/InterfileHeader.cxx b/src/IO/InterfileHeader.cxx index b23f5ff6d..9bf847ef8 100644 --- a/src/IO/InterfileHeader.cxx +++ b/src/IO/InterfileHeader.cxx @@ -594,7 +594,7 @@ InterfilePDFSHeader::InterfilePDFSHeader() // end of new keys for block geometry //new keys for generic geometry crystal_map = ""; - add_key("name of crystal map", &crystal_map); + add_key("Name of crystal map", &crystal_map); //end of new keys for generic geometry add_key("end scanner parameters", diff --git a/src/IO/interfile.cxx b/src/IO/interfile.cxx index 9e3088a09..d9a522b3c 100644 --- a/src/IO/interfile.cxx +++ b/src/IO/interfile.cxx @@ -1066,41 +1066,39 @@ write_basic_interfile_PDFS_header(const string& header_file_name, }// end of BlocksOnCylindrical scanner else // generic scanner - { - const ProjDataInfoGeneric* proj_data_info_ptr = + { + const ProjDataInfoGeneric* proj_data_info_ptr = dynamic_cast< const ProjDataInfoGeneric*> (pdfs.get_proj_data_info_ptr()); - if (proj_data_info_ptr!=NULL) - { - output_header << "minimum ring difference per segment := "; - { - std::vector::const_iterator seg = segment_sequence.begin(); - output_header << "{ " << proj_data_info_ptr->get_min_ring_difference(*seg); - for (seg++; seg != segment_sequence.end(); seg++) - output_header << "," <get_min_ring_difference(*seg); - output_header << "}\n"; - } - - output_header << "maximum ring difference per segment := "; - { + if (proj_data_info_ptr!=NULL) + { + output_header << "minimum ring difference per segment := "; + { + std::vector::const_iterator seg = segment_sequence.begin(); + output_header << "{ " << proj_data_info_ptr->get_min_ring_difference(*seg); + for (seg++; seg != segment_sequence.end(); seg++) + output_header << "," <get_min_ring_difference(*seg); + output_header << "}\n"; + } + + output_header << "maximum ring difference per segment := "; + { std::vector::const_iterator seg = segment_sequence.begin(); output_header << "{ " <get_max_ring_difference(*seg); for (seg++; seg != segment_sequence.end(); seg++) output_header << "," <get_max_ring_difference(*seg); output_header << "}\n"; - } + } - const Scanner& scanner = *proj_data_info_ptr->get_scanner_ptr(); - if (fabs(proj_data_info_ptr->get_ring_radius()- - scanner.get_effective_ring_radius()) > .1) + const Scanner& scanner = *proj_data_info_ptr->get_scanner_ptr(); + if (fabs(proj_data_info_ptr->get_ring_radius() - scanner.get_effective_ring_radius()) > .1) warning("write_basic_interfile_PDFS_header: inconsistent effective ring radius:\n" "\tproj_data_info has %g, scanner has %g.\n" "\tThis really should not happen and signifies a bug.\n" "\tYou will have a problem reading this data back in.", proj_data_info_ptr->get_ring_radius(), scanner.get_effective_ring_radius()); - if (fabs(proj_data_info_ptr->get_ring_spacing()- - scanner.get_ring_spacing()) > .1) + if (fabs(proj_data_info_ptr->get_ring_spacing() - scanner.get_ring_spacing()) > .1) warning("write_basic_interfile_PDFS_header: inconsistent ring spacing:\n" "\tproj_data_info has %g, scanner has %g.\n" "\tThis really should not happen and signifies a bug.\n" @@ -1108,13 +1106,13 @@ write_basic_interfile_PDFS_header(const string& header_file_name, proj_data_info_ptr->get_ring_spacing(), scanner.get_ring_spacing()); - output_header << scanner.parameter_info(); + output_header << scanner.parameter_info(); - output_header << "effective central bin size (cm) := " + output_header << "effective central bin size (cm) := " << proj_data_info_ptr->get_sampling_in_s(Bin(0,0,0,0))/10. << endl; - } - } // end generic scanner + } + } // end generic scanner } diff --git a/src/buildblock/Scanner.cxx b/src/buildblock/Scanner.cxx index 5609cf045..7971b9381 100644 --- a/src/buildblock/Scanner.cxx +++ b/src/buildblock/Scanner.cxx @@ -634,45 +634,56 @@ set_params(Type type_v,const list& list_of_names_v, // creates maps to convert between stir and 3d coordinates void Scanner:: -read_detectormap_from_file( const std::string& filename ) +read_detectormap_from_file( const std::string& crystal_map_name ) { - std::ifstream myfile(filename.c_str()); - if( !myfile ) + std::ifstream crystal_map_file(crystal_map_name.c_str()); + if( !crystal_map_file ) { - std::cerr << "Error opening file " << filename << "." << std::endl; + std::cerr << "Error opening file " << crystal_map_name << "." << std::endl; return; } std::string line; //map containing the crystal map from the input file (safir -> coords) boost::unordered_map, stir::CartesianCoordinate3D, ihash> coord_map; // read in the file save the content in a map - while( std::getline( myfile, line)) + while( std::getline( crystal_map_file, line)) { if( line.size() && line[0] == '#' ) continue; bool has_layer_index = false; stir::CartesianCoordinate3D coord; stir::DetectionPosition<> detpos; - std::vector col; - boost::split(col, line, boost::is_any_of("\t,")); - if( !col.size() ) break; - else if( col.size() == 5 ) has_layer_index = false; - else if( col.size() == 6 ) has_layer_index = true; - coord[1] = atof(col[4+has_layer_index].c_str() ); - coord[2] = atof(col[3+has_layer_index].c_str() ); - coord[3] = atof(col[2+has_layer_index].c_str() ); + std::vector entry; + boost::split(entry, line, boost::is_any_of("\t,")); + if( !entry.size() ) break; + else if( entry.size() == 5 ) has_layer_index = false; + else if( entry.size() == 6 ) has_layer_index = true; + coord[1] = atof(entry[4+has_layer_index].c_str() ); + coord[2] = atof(entry[3+has_layer_index].c_str() ); + coord[3] = atof(entry[2+has_layer_index].c_str() ); if( !has_layer_index ) detpos.radial_coord() = 0; - else detpos.radial_coord() = atoi(col[2].c_str()); - detpos.axial_coord() = atoi(col[0].c_str()); - detpos.tangential_coord() = atoi(col[1].c_str()); + else detpos.radial_coord() = atoi(entry[2].c_str()); + detpos.axial_coord() = atoi(entry[0].c_str()); + detpos.tangential_coord() = atoi(entry[1].c_str()); coord_map[detpos] = coord; } + // The detector crystal coordinates are now saved in coord_map the following way: + // (detector#, ring#, 1)[(x,y,z)] + // the detector# and ring# are determined outside of STIR (later given in input) + // In order to fulfill the STIR convention we have to give the coordinates + // detector# and ring# defined by ourself so that the start (0,0) goes to the + // coordinate with the smallest z and smallest y and the detector# is + // counterclockwise rising. + // To achieve this, we assign each coordinate the value 'coord_sorter' which + // is the assigned value of the criteria mentioned above. With it we sort the + // coordinates and fill the to maps 'input_index_to_stir_index' and + // 'stir_index_to_coord'. std::vector coords_to_be_sorted; boost::unordered_map > map_for_sorting_coordinates; coords_to_be_sorted.reserve(coord_map.size()); - //TODO: change it to be backwards compatible + for(auto it : coord_map) { double coord_sorter = it.second[1] * 100 + from_min_pi_plus_pi_to_0_2pi(std::atan2(it.second[3], -it.second[2])); @@ -689,7 +700,7 @@ read_detectormap_from_file( const std::string& filename ) detpos.tangential_coord() = det; detpos.radial_coord() = 0; input_index_to_stir_index[map_for_sorting_coordinates[*it]] = detpos; - input_index_to_coord[detpos] = coord_map[map_for_sorting_coordinates[*it]]; + stir_index_to_coord[detpos] = coord_map[map_for_sorting_coordinates[*it]]; det++; if (det == num_detectors_per_ring) { @@ -723,6 +734,7 @@ check_consistency() const const int dets_per_ring = get_num_transaxial_blocks() * get_num_transaxial_crystals_per_block(); + // exclusion of generic as 'get_num_transaxial_crystals_per_block()' is sometimes false for asymmetric detectors and not important for generic if ( dets_per_ring != get_num_detectors_per_ring() && scanner_orientation != "Generic") { warning("Scanner %s: inconsistent transaxial block info", @@ -741,7 +753,8 @@ check_consistency() const const int blocks_per_ring = get_num_transaxial_buckets() * get_num_transaxial_blocks_per_bucket(); - if ( blocks_per_ring != get_num_transaxial_blocks()) + // exclusion of generic as 'get_num_transaxial_blocks_per_bucket()' is sometimes false for asymmetric detectors and not important for generic + if ( blocks_per_ring != get_num_transaxial_blocks() && scanner_orientation != "Generic") { warning("Scanner %s: inconsistent transaxial block/bucket info", this->get_name().c_str()); @@ -759,7 +772,8 @@ check_consistency() const const int dets_axial = get_num_axial_blocks() * get_num_axial_crystals_per_block(); - if ( dets_axial != get_num_rings()) + // exclusion of generic as 'get_num_axial_crystals_per_block()' is sometimes false for asymmetric detectors and not important for generic + if ( dets_axial != get_num_rings() && scanner_orientation != "Generic") { warning("Scanner %s: inconsistent axial block info", this->get_name().c_str()); @@ -777,7 +791,8 @@ check_consistency() const const int blocks_axial = get_num_axial_buckets() * get_num_axial_blocks_per_bucket(); - if ( blocks_axial != get_num_axial_blocks()) + // exclusion of generic as 'get_num_axial_blocks_per_bucket()' is sometimes false for asymmetric detectors and not important for generic + if ( blocks_axial != get_num_axial_blocks() && scanner_orientation != "Generic") { warning("Scanner %s: inconsistent axial block/bucket info", this->get_name().c_str()); @@ -863,7 +878,7 @@ check_consistency() const get_transaxial_block_spacing(), get_num_transaxial_blocks_per_bucket(), get_inner_ring_radius()); return Succeeded::no; } - else if (get_scannner_geometry() == "Generic") + else if (get_scanner_geometry() == "Generic") { //! Check if the crystal map is correct and given if (get_crystal_map() == "") { @@ -993,12 +1008,11 @@ Scanner::parameter_info() const << get_num_axial_crystals_per_singles_unit() << '\n' << "Number of crystals per singles unit in transaxial direction := " << get_num_transaxial_crystals_per_singles_unit() << '\n'; - //crystal map + + //block and generic geometry description if (crystal_map_file_name != "") s << "Name of crystal map := " << get_crystal_map() << '\n'; - - //block geometry description if (get_scanner_orientation() != "" && get_scanner_geometry() != "") { s << "Scanner orientation (X or Y) := " @@ -1132,12 +1146,6 @@ Scanner* Scanner::ask_parameters() const string ScannerGeometry = ask_string("Enter the scanner geometry ( BlocksOnCylindrical / Cylindrical / Generic ) :", "Cylindrical"); - string crystal_map_file_name = ""; - if (ScannerGeometry == "Generic") { - crystal_map_file_name = - ask_string("Enter the name of the crystal map: ", ""); - } - float AxialCrystalSpacing= ask_num("Enter crystal spacing in axial direction (in mm): ",0.F,30.F,6.75F); float TransaxialCrystalSpacing= @@ -1147,6 +1155,12 @@ Scanner* Scanner::ask_parameters() float TransaxialBlockSpacing= ask_num("Enter block spacing in transaxial direction (in mm): ",0.F,360.F,54.F); + string crystal_map_file_name = ""; + if (ScannerGeometry == "Generic") { + crystal_map_file_name = + ask_string("Enter the name of the crystal map: ", ""); + } + Type type = User_defined_scanner; scanner_ptr = diff --git a/src/include/stir/IO/InterfileHeader.h b/src/include/stir/IO/InterfileHeader.h index 60b71479a..53cb54bb4 100644 --- a/src/include/stir/IO/InterfileHeader.h +++ b/src/include/stir/IO/InterfileHeader.h @@ -257,11 +257,13 @@ class InterfilePDFSHeader : public InterfileHeader //! new variables for block geometry std::string scanner_orientation; std::string scanner_geometry; - std::string crystal_map; float axial_distance_between_crystals_in_cm; float transaxial_distance_between_crystals_in_cm; float axial_distance_between_blocks_in_cm; float transaxial_distance_between_blocks_in_cm; + + //! new variables for generic geometry + std::string crystal_map; // end scanner parameters diff --git a/src/include/stir/Scanner.h b/src/include/stir/Scanner.h index a576c5285..777f287e5 100644 --- a/src/include/stir/Scanner.h +++ b/src/include/stir/Scanner.h @@ -399,8 +399,9 @@ class Scanner // Get the transaxial singles bin coordinate from a singles bin. inline int get_transaxial_singles_unit(int singles_bin_index) const; - // Functions to retrieve the coordinates and the stir_id out of the scanner + // Get the STIR detection position (det#, ring#, layer#) from the input detection position inline stir::DetectionPosition<> get_detpos_from_id(const stir::DetectionPosition<> det_pos) const; + // Get the coordinates (x,y,z) from a crystal detector from the STIR detection position (det#, ring#, layer#) inline stir::CartesianCoordinate3D get_coords_from_detpos(const stir::DetectionPosition<> det_pos) const; private: @@ -468,7 +469,7 @@ class Scanner } }; boost::unordered_map, stir::DetectionPosition<>, ihash> input_index_to_stir_index; - boost::unordered_map, stir::CartesianCoordinate3D, ihash> input_index_to_coord; + boost::unordered_map, stir::CartesianCoordinate3D, ihash> stir_index_to_coord; std::string crystal_map_file_name; // function to create the maps diff --git a/src/include/stir/Scanner.inl b/src/include/stir/Scanner.inl index 728726312..09b77062c 100644 --- a/src/include/stir/Scanner.inl +++ b/src/include/stir/Scanner.inl @@ -459,13 +459,12 @@ Scanner::get_detpos_from_id(const stir::DetectionPosition<> det_pos) const{ } return input_index_to_stir_index.at(det_pos); } -// Attention, this uses the stir coordinate, not the one from listmode stir::CartesianCoordinate3D Scanner::get_coords_from_detpos(const stir::DetectionPosition<> det_pos) const{ if (crystal_map_file_name == ""){ stir::error("Crystal Map not defined!"); } - return input_index_to_coord.at(det_pos); + return stir_index_to_coord.at(det_pos); } END_NAMESPACE_STIR diff --git a/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx b/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx index d5040122b..eb3eec497 100644 --- a/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx +++ b/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx @@ -380,52 +380,52 @@ DataSymmetriesForBins_PET_CartesianGrid cartesian_grid_info_ptr); } // generic implementation - if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="Generic") + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="Generic") + { + if (dynamic_cast(proj_data_info_ptr.get()) == NULL) + error("DataSymmetriesForBins_PET_CartesianGrid constructed with wrong type of ProjDataInfo: %s\n" + "(can only handle projection data corresponding to blocks on a cylinder)\n", + typeid(*proj_data_info_ptr).name()); + + const DiscretisedDensityOnCartesianGrid<3,float> * + cartesian_grid_info_ptr = + dynamic_cast *> + (image_info_ptr.get()); + + if (cartesian_grid_info_ptr == NULL) + error("DataSymmetriesForBins_PET_CartesianGrid constructed with wrong type of image info: %s\n", + typeid(*image_info_ptr).name()); + + // WARNING get_grid_spacing()[1] == z + const float z_origin_in_planes = + image_info_ptr->get_origin().z()/cartesian_grid_info_ptr->get_grid_spacing()[1]; + // z_origin_in_planes should be an integer + if (fabs(round(z_origin_in_planes) - z_origin_in_planes) > 1.E-3F) + error("DataSymmetriesForBins_PET_CartesianGrid: the shift in the " + "z-direction of the origin (which is %g) should be a multiple of the plane " + "separation (%g)\n", + image_info_ptr->get_origin().z(), cartesian_grid_info_ptr->get_grid_spacing()[1]); + + if (this->do_symmetry_90degrees_min_phi|| + this->do_symmetry_180degrees_min_phi|| + this->do_symmetry_swap_segment|| + this->do_symmetry_swap_s|| + this->do_symmetry_shift_z) { - if (dynamic_cast(proj_data_info_ptr.get()) == NULL) - error("DataSymmetriesForBins_PET_CartesianGrid constructed with wrong type of ProjDataInfo: %s\n" - "(can only handle projection data corresponding to blocks on a cylinder)\n", - typeid(*proj_data_info_ptr).name()); + warning("Disabling all symmetries since they are not implemented in generic geometry."); + this->do_symmetry_90degrees_min_phi = + this->do_symmetry_180degrees_min_phi = + this->do_symmetry_swap_segment = + this->do_symmetry_swap_s = + this->do_symmetry_shift_z = false; + } - const DiscretisedDensityOnCartesianGrid<3,float> * - cartesian_grid_info_ptr = - dynamic_cast *> - (image_info_ptr.get()); - - if (cartesian_grid_info_ptr == NULL) - error("DataSymmetriesForBins_PET_CartesianGrid constructed with wrong type of image info: %s\n", - typeid(*image_info_ptr).name()); - - // WARNING get_grid_spacing()[1] == z - const float z_origin_in_planes = - image_info_ptr->get_origin().z()/cartesian_grid_info_ptr->get_grid_spacing()[1]; - // z_origin_in_planes should be an integer - if (fabs(round(z_origin_in_planes) - z_origin_in_planes) > 1.E-3F) - error("DataSymmetriesForBins_PET_CartesianGrid: the shift in the " - "z-direction of the origin (which is %g) should be a multiple of the plane " - "separation (%g)\n", - image_info_ptr->get_origin().z(), cartesian_grid_info_ptr->get_grid_spacing()[1]); - - if (this->do_symmetry_90degrees_min_phi|| - this->do_symmetry_180degrees_min_phi|| - this->do_symmetry_swap_segment|| - this->do_symmetry_swap_s|| - this->do_symmetry_shift_z) - { - warning("Disabling all symmetries since they are not implemented in generic geometry."); - this->do_symmetry_90degrees_min_phi = - this->do_symmetry_180degrees_min_phi = - this->do_symmetry_swap_segment = - this->do_symmetry_swap_s = - this->do_symmetry_shift_z = false; - } - - find_relation_between_coordinate_systems( - num_planes_per_scanner_ring, - num_planes_per_axial_pos, - axial_pos_to_z_offset, - static_cast(proj_data_info_ptr.get()), - cartesian_grid_info_ptr); + find_relation_between_coordinate_systems( + num_planes_per_scanner_ring, + num_planes_per_axial_pos, + axial_pos_to_z_offset, + static_cast(proj_data_info_ptr.get()), + cartesian_grid_info_ptr); } } diff --git a/src/recon_buildblock/ProjMatrixByBinUsingRayTracing.cxx b/src/recon_buildblock/ProjMatrixByBinUsingRayTracing.cxx index 2ad1d6b6a..fd49e2ec1 100644 --- a/src/recon_buildblock/ProjMatrixByBinUsingRayTracing.cxx +++ b/src/recon_buildblock/ProjMatrixByBinUsingRayTracing.cxx @@ -369,44 +369,44 @@ set_up( use_actual_detector_boundaries = false; } } + } + else + { + const ProjDataInfoGenericNoArcCorr * proj_data_info_blk_ptr = + dynamic_cast(proj_data_info_ptr.get()); + + if (proj_data_info_blk_ptr== 0) + { + warning("ProjMatrixByBinUsingRayTracing: use_actual_detector_boundaries" + " is reset to false as the projection data should be non-arccorected.\n"); + use_actual_detector_boundaries = false; } else + { + bool nocompression = + proj_data_info_blk_ptr->get_view_mashing_factor()==1; + for (int segment_num=proj_data_info_blk_ptr->get_min_segment_num(); + nocompression && segment_num <= proj_data_info_blk_ptr->get_max_segment_num(); + ++segment_num) + nocompression= + proj_data_info_blk_ptr->get_min_ring_difference(segment_num) == + proj_data_info_blk_ptr->get_max_ring_difference(segment_num); + + if (!nocompression) { - const ProjDataInfoGenericNoArcCorr * proj_data_info_blk_ptr = - dynamic_cast(proj_data_info_ptr.get()); - - if (proj_data_info_blk_ptr== 0) - { - warning("ProjMatrixByBinUsingRayTracing: use_actual_detector_boundaries" - " is reset to false as the projection data should be non-arccorected.\n"); - use_actual_detector_boundaries = false; - } - else - { - bool nocompression = - proj_data_info_blk_ptr->get_view_mashing_factor()==1; - for (int segment_num=proj_data_info_blk_ptr->get_min_segment_num(); - nocompression && segment_num <= proj_data_info_blk_ptr->get_max_segment_num(); - ++segment_num) - nocompression= - proj_data_info_blk_ptr->get_min_ring_difference(segment_num) == - proj_data_info_blk_ptr->get_max_ring_difference(segment_num); - - if (!nocompression) - { - warning("ProjMatrixByBinUsingRayTracing: use_actual_detector_boundaries" - " is reset to false as the projection data as either mashed or uses axial compression\n"); - use_actual_detector_boundaries = false; - } - } + warning("ProjMatrixByBinUsingRayTracing: use_actual_detector_boundaries" + " is reset to false as the projection data as either mashed or uses axial compression\n"); + use_actual_detector_boundaries = false; } + } + } - if (use_actual_detector_boundaries) + if (use_actual_detector_boundaries) warning("ProjMatrixByBinUsingRayTracing: use_actual_detector_boundaries==true\n"); - } + } -#if 0 + #if 0 // test if our 2D code does not have problems { // currently 2D code relies on the LOR falling in the middle of a voxel (in z-direction) @@ -419,7 +419,7 @@ set_up( "- or an even number of planes and z_origin=(n+1/2)*z_voxel_size\n" "(for some integer n).\n"); } -#endif + #endif this->already_setup = true; @@ -664,14 +664,14 @@ calculate_proj_matrix_elems_for_one_bin( phi = proj_data_info_noarccor.get_phi(bin); s_in_mm = proj_data_info_noarccor.get_s(bin); - } - else + } + else { - const ProjDataInfoGenericNoArcCorr& proj_data_info_noarccor = - dynamic_cast(*proj_data_info_ptr); + const ProjDataInfoGenericNoArcCorr& proj_data_info_noarccor = + dynamic_cast(*proj_data_info_ptr); - phi = proj_data_info_noarccor.get_phi(bin); - s_in_mm = proj_data_info_noarccor.get_s(bin); + phi = proj_data_info_noarccor.get_phi(bin); + s_in_mm = proj_data_info_noarccor.get_s(bin); } } From db527c027d4cf606673b08518461753ec3f919c3 Mon Sep 17 00:00:00 2001 From: Michael Roethlisberger Date: Wed, 7 Nov 2018 12:56:54 +0100 Subject: [PATCH 06/81] Update 2 for issue 2 --- examples/SAFIR_genericScanner/README.txt | 20 ++++++---- src/IO/InterfileHeader.cxx | 4 +- .../ProjDataInfoGenericNoArcCorr.cxx | 4 +- src/buildblock/Scanner.cxx | 38 +++++++++---------- src/include/stir/ProjDataInfoGeneric.h | 4 +- src/include/stir/ProjDataInfoGeneric.inl | 4 +- src/include/stir/Scanner.h | 30 ++++++++------- src/include/stir/Scanner.inl | 6 +-- .../stir/listmode/CListRecordSAFIR.inl | 4 +- 9 files changed, 61 insertions(+), 53 deletions(-) diff --git a/examples/SAFIR_genericScanner/README.txt b/examples/SAFIR_genericScanner/README.txt index e9b9adba9..adb1e078d 100644 --- a/examples/SAFIR_genericScanner/README.txt +++ b/examples/SAFIR_genericScanner/README.txt @@ -1,11 +1,17 @@ -The PET scanner prototype of the SAFIR project does not consists of symmetric -detector blocks. This led to the need for a new scanner geometry in STIR. It -is implemented named generic geometry. In short it uses the coordinates -provided by an external text file instead of the ones computed internally by -STIR itself (based on the symmetry of the detector). +The PET scanner prototype of the SAFIR project has a high requirement for +precision. Therefore the simplification of projecting all detectors on a cylinder +(cylindric geometry) for the reconstruction instead of the real scanner form of +a dodecagon leads to a not sufficient spatial resolution. The block geometry +projects the detectors on a polygon, but the modules have the restriction of +consisting of equally sized blocks. The newest design of SAFIR does not fulfill +this condition. Therefore a third geometry (generic geometry) was implemented. +It computes the position of the detectors used for the reconstruction based on +an external text file and hence is not dependent on the design of the detector. +It requires the availability of the coordinates in an external file and that the +rings consist of an constant number of detectors. -The files in this directory are to test the new implementation, to understand -the changes and as model for later usage. +The files in this directory are to test the new 'generic' implementation, to +understand the changes and as model for later usage. The bash script 'test.sh' runs the methods 'lm_to_projdata', 'backproject', 'forward_project' and 'OSMAPOSL' to test the functionality with the new scanner diff --git a/src/IO/InterfileHeader.cxx b/src/IO/InterfileHeader.cxx index 9bf847ef8..83c8eb13d 100644 --- a/src/IO/InterfileHeader.cxx +++ b/src/IO/InterfileHeader.cxx @@ -1341,13 +1341,13 @@ bool InterfilePDFSHeader::post_processing() num_detector_layers, energy_resolution, reference_energy, - crystal_map, scanner_orientation, scanner_geometry, static_cast(axial_distance_between_crystals_in_cm*10.), static_cast(transaxial_distance_between_crystals_in_cm*10.), static_cast(axial_distance_between_blocks_in_cm*10.), - static_cast(transaxial_distance_between_blocks_in_cm*10.) + static_cast(transaxial_distance_between_blocks_in_cm*10.), + crystal_map )); bool is_consistent = diff --git a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx index ae2dbe141..a28e59f69 100644 --- a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx +++ b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx @@ -390,8 +390,8 @@ find_cartesian_coordinates_given_scanner_coordinates(CartesianCoordinate3Dget_coords_from_detpos(det_pos1); - coord_2 = get_scanner_ptr()->get_coords_from_detpos(det_pos2); + coord_1 = get_scanner_ptr()->get_coords_given_detpos(det_pos1); + coord_2 = get_scanner_ptr()->get_coords_given_detpos(det_pos2); } diff --git a/src/buildblock/Scanner.cxx b/src/buildblock/Scanner.cxx index 7971b9381..3f56ea01d 100644 --- a/src/buildblock/Scanner.cxx +++ b/src/buildblock/Scanner.cxx @@ -437,13 +437,13 @@ Scanner::Scanner(Type type_v, const list& list_of_names_v, int num_detector_layers_v, float energy_resolution_v, float reference_energy_v, - const std::string& crystal_map_file_name_v, const string& scanner_orientation_v, const string& scanner_geometry_v, float axial_crystal_spacing_v, float transaxial_crystal_spacing_v, float axial_block_spacing_v, - float transaxial_block_spacing_v) + float transaxial_block_spacing_v, + const std::string& crystal_map_file_name_v) { set_params(type_v, list_of_names_v, num_rings_v, max_num_non_arccorrected_bins_v, @@ -459,13 +459,13 @@ Scanner::Scanner(Type type_v, const list& list_of_names_v, num_detector_layers_v, energy_resolution_v, reference_energy_v, - crystal_map_file_name_v, scanner_orientation_v, scanner_geometry_v, axial_crystal_spacing_v, transaxial_crystal_spacing_v, axial_block_spacing_v, - transaxial_block_spacing_v); + transaxial_block_spacing_v, + crystal_map_file_name_v); } @@ -483,13 +483,13 @@ Scanner::Scanner(Type type_v, const string& name, int num_detector_layers_v, float energy_resolution_v, float reference_energy_v, - const std::string& crystal_map_file_name_v, const string& scanner_orientation_v, const string& scanner_geometry_v, float axial_crystal_spacing_v, float transaxial_crystal_spacing_v, float axial_block_spacing_v, - float transaxial_block_spacing_v) + float transaxial_block_spacing_v, + const std::string& crystal_map_file_name_v) { set_params(type_v, string_list(name), num_rings_v, max_num_non_arccorrected_bins_v, @@ -505,13 +505,13 @@ Scanner::Scanner(Type type_v, const string& name, num_detector_layers_v, energy_resolution_v, reference_energy_v, - crystal_map_file_name_v, scanner_orientation_v, scanner_geometry_v, axial_crystal_spacing_v, transaxial_crystal_spacing_v, axial_block_spacing_v, - transaxial_block_spacing_v); + transaxial_block_spacing_v, + crystal_map_file_name_v); } @@ -537,13 +537,13 @@ set_params(Type type_v,const list& list_of_names_v, int num_detector_layers_v, float energy_resolution_v, float reference_energy_v, - const std::string& crystal_map_file_name_v, const string& scanner_orientation_v, const string& scanner_geometry_v, float axial_crystal_spacing_v, float transaxial_crystal_spacing_v, float axial_block_spacing_v, - float transaxial_block_spacing_v) + float transaxial_block_spacing_v, + const std::string& crystal_map_file_name_v) { set_params(type_v, list_of_names_v, num_rings_v, max_num_non_arccorrected_bins_v, @@ -559,13 +559,13 @@ set_params(Type type_v,const list& list_of_names_v, num_detector_layers_v, energy_resolution_v, reference_energy_v, - crystal_map_file_name_v, scanner_orientation_v, scanner_geometry_v, axial_crystal_spacing_v, transaxial_crystal_spacing_v, axial_block_spacing_v, - transaxial_block_spacing_v); + transaxial_block_spacing_v, + crystal_map_file_name_v); } @@ -587,13 +587,13 @@ set_params(Type type_v,const list& list_of_names_v, int num_detector_layers_v, float energy_resolution_v, float reference_energy_v, - const std::string& crystal_map_file_name_v, const string& scanner_orientation_v, const string& scanner_geometry_v, float axial_crystal_spacing_v, float transaxial_crystal_spacing_v, float axial_block_spacing_v, - float transaxial_block_spacing_v) + float transaxial_block_spacing_v, + const std::string& crystal_map_file_name_v) { type = type_v; list_of_names = list_of_names_v; @@ -880,14 +880,14 @@ check_consistency() const } else if (get_scanner_geometry() == "Generic") { //! Check if the crystal map is correct and given - if (get_crystal_map() == "") + if (get_crystal_map_file_name() == "") { warning("No crystal map is provided. The scanner geometry Generic needs it! Please provide one."); return Succeeded::no; } else { - std::ifstream crystal_map(get_crystal_map()); + std::ifstream crystal_map(get_crystal_map_file_name()); if( !crystal_map) { warning("No correct crystal map provided. Please check the file name."); @@ -1012,7 +1012,7 @@ Scanner::parameter_info() const //block and generic geometry description if (crystal_map_file_name != "") s << "Name of crystal map := " - << get_crystal_map() << '\n'; + << get_crystal_map_file_name() << '\n'; if (get_scanner_orientation() != "" && get_scanner_geometry() != "") { s << "Scanner orientation (X or Y) := " @@ -1175,13 +1175,13 @@ Scanner* Scanner::ask_parameters() num_detector_layers, EnergyResolution, ReferenceEnergy, - crystal_map_file_name, ScannerOrientation, ScannerGeometry, TransaxialCrystalSpacing, AxialCrystalSpacing, AxialBlockSpacing, - TransaxialBlockSpacing); + TransaxialBlockSpacing, + crystal_map_file_name); if (scanner_ptr->check_consistency()==Succeeded::yes || !ask("Ask questions again?",true)) diff --git a/src/include/stir/ProjDataInfoGeneric.h b/src/include/stir/ProjDataInfoGeneric.h index 660f61f95..f4a904819 100644 --- a/src/include/stir/ProjDataInfoGeneric.h +++ b/src/include/stir/ProjDataInfoGeneric.h @@ -29,7 +29,7 @@ template class CartesianCoordinate3D; /*! \ingroup projdata \brief projection data info for data corresponding to a - 'Blocks-on-cylindrical' sampling. + 'Generic' sampling. */ class ProjDataInfoGeneric: public ProjDataInfo @@ -254,7 +254,7 @@ class ProjDataInfoGeneric: public ProjDataInfo //! This member stores the offsets used in get_m() /* - //! warning This is not used in block geometry. m is found directly from lors. + //! warning This is not used in generic geometry. m is found directly from lors. mutable VectorWithOffset m_offset; */ diff --git a/src/include/stir/ProjDataInfoGeneric.inl b/src/include/stir/ProjDataInfoGeneric.inl index 247045e9d..183c3d7d5 100644 --- a/src/include/stir/ProjDataInfoGeneric.inl +++ b/src/include/stir/ProjDataInfoGeneric.inl @@ -33,7 +33,7 @@ ProjDataInfoGeneric::get_phi(const Bin& bin)const return lor.phi(); } -/*! warning In block geometry m is calculated directly from lor while in +/*! warning In generic geometry m is calculated directly from lor while in cylindrical geometry m is calculated using m_offset and axial_sampling */ float @@ -56,7 +56,7 @@ ProjDataInfoGeneric::get_t(const Bin& bin) const tan (theta) = dz/sqrt(dx2+dy2) cylindrical geometry: delta_z = delta_ring * ring spacing - Block geometry: + generic geometry: delta_z is calculated from lor */ float diff --git a/src/include/stir/Scanner.h b/src/include/stir/Scanner.h index 777f287e5..c57df9a20 100644 --- a/src/include/stir/Scanner.h +++ b/src/include/stir/Scanner.h @@ -144,13 +144,13 @@ class Scanner int num_detector_layers_v, float energy_resolution_v = -1.0f, float reference_energy_v = -1.0f, - const std::string& crystal_map_file_name = "", const std::string& scanner_orientation_v = "", const std::string& scanner_geometry_v = "", float axial_crystal_spacing_v = -1.0f, float transaxial_crystal_spacing_v = -1.0f, float axial_block_spacing_v = -1.0f, - float transaxial_block_spacing_v = -1.0f); + float transaxial_block_spacing_v = -1.0f, + const std::string& crystal_map_file_name = ""); //! constructor ( a single name) /*! size info is in mm @@ -170,13 +170,13 @@ class Scanner int num_detector_layers_v, float energy_resolution_v = -1.0f, float reference_energy_v = -1.0f, - const std::string& crystal_map_file_name = "", const std::string& scanner_orientation_v = "", const std::string& scanner_geometry_v = "", float axial_crystal_spacing_v = -1.0f, float transaxial_crystal_spacing_v = -1.0f, float axial_block_spacing_v = -1.0f, - float transaxial_block_spacing_v = -1.0f); + float transaxial_block_spacing_v = -1.0f, + const std::string& crystal_map_file_name = ""); @@ -302,8 +302,8 @@ class Scanner //@} (end of get block geometry info) //! \name functions to get generic geometry info - //! get scanner orientation - inline std::string get_crystal_map() const; + //! get crystal map file name + inline std::string get_crystal_map_file_name() const; //@} (end of block/bucket info) @@ -399,10 +399,12 @@ class Scanner // Get the transaxial singles bin coordinate from a singles bin. inline int get_transaxial_singles_unit(int singles_bin_index) const; - // Get the STIR detection position (det#, ring#, layer#) from the input detection position - inline stir::DetectionPosition<> get_detpos_from_id(const stir::DetectionPosition<> det_pos) const; - // Get the coordinates (x,y,z) from a crystal detector from the STIR detection position (det#, ring#, layer#) - inline stir::CartesianCoordinate3D get_coords_from_detpos(const stir::DetectionPosition<> det_pos) const; + // Get the STIR detection position (det#, ring#, layer#) given the detection position id in the input crystal map + // used in CListRecordSAFIR.inl for accessing the coordinates + inline stir::DetectionPosition<> get_detpos_given_id(const stir::DetectionPosition<> det_pos) const; + // Get the Cartesian coordinates (x,y,z) given the STIR detection position (det#, ring#, layer#) + // used in ProjInfoDataGenericNoArcCorr.cxx for accessing the coordinates + inline stir::CartesianCoordinate3D get_coords_given_detpos(const stir::DetectionPosition<> det_pos) const; private: Type type; @@ -492,13 +494,13 @@ class Scanner int num_detector_layers_v, float energy_resolution_v = -1.0f, float reference_energy = -1.0f, - const std::string& crystal_map_file_name = "", const std::string& scanner_orientation_v = "", const std::string& scanner_geometry_v = "", float axial_crystal_spacing_v = -1.0f, float transaxial_crystal_spacing_v = -1.0f, float axial_block_spacing_v = -1.0f, - float transaxial_block_spacing_v = -1.0f); + float transaxial_block_spacing_v = -1.0f, + const std::string& crystal_map_file_name = ""); // ! set all parameters void set_params(Type type_v, const std::list& list_of_names_v, @@ -517,13 +519,13 @@ class Scanner int num_detector_layers_v, float energy_resolution_v = -1.0f, float reference_energy = -1.0f, - const std::string& crystal_map_file_name = "", const std::string& scanner_orientation_v = "", const std::string& scanner_geometry_v = "", float axial_crystal_spacing_v = -1.0f, float transaxial_crystal_spacing_v = -1.0f, float axial_block_spacing_v = -1.0f, - float transaxial_block_spacing_v = -1.0f); + float transaxial_block_spacing_v = -1.0f, + const std::string& crystal_map_file_name = ""); }; diff --git a/src/include/stir/Scanner.inl b/src/include/stir/Scanner.inl index 09b77062c..d7d3c44db 100644 --- a/src/include/stir/Scanner.inl +++ b/src/include/stir/Scanner.inl @@ -267,7 +267,7 @@ Scanner::get_axial_block_spacing() const } std::string -Scanner::get_crystal_map() const +Scanner::get_crystal_map_file_name() const { return crystal_map_file_name; } @@ -453,14 +453,14 @@ Scanner::get_transaxial_singles_unit(int singles_bin_index) const { // For retrieving the coordinates / detector, ring id from the scanner stir::DetectionPosition<> -Scanner::get_detpos_from_id(const stir::DetectionPosition<> det_pos) const{ +Scanner::get_detpos_given_id(const stir::DetectionPosition<> det_pos) const{ if (crystal_map_file_name == ""){ stir::error("Crystal Map not defined!"); } return input_index_to_stir_index.at(det_pos); } stir::CartesianCoordinate3D -Scanner::get_coords_from_detpos(const stir::DetectionPosition<> det_pos) const{ +Scanner::get_coords_given_detpos(const stir::DetectionPosition<> det_pos) const{ if (crystal_map_file_name == ""){ stir::error("Crystal Map not defined!"); } diff --git a/src/include/stir/listmode/CListRecordSAFIR.inl b/src/include/stir/listmode/CListRecordSAFIR.inl index 0ea38ffc9..cd08b38ed 100644 --- a/src/include/stir/listmode/CListRecordSAFIR.inl +++ b/src/include/stir/listmode/CListRecordSAFIR.inl @@ -79,8 +79,8 @@ get_bin(Bin& bin, const ProjDataInfo& proj_data_info) const //transform det_pos_pair into stir coordinates DetectionPosition<> pos1 = det_pos_pair.pos1(); DetectionPosition<> pos2 = det_pos_pair.pos2(); - det_pos_pair.pos1() = proj_data_info_gen.get_scanner_ptr()->get_detpos_from_id(pos1); - det_pos_pair.pos2() = proj_data_info_gen.get_scanner_ptr()->get_detpos_from_id(pos2); + det_pos_pair.pos1() = proj_data_info_gen.get_scanner_ptr()->get_detpos_given_id(pos1); + det_pos_pair.pos2() = proj_data_info_gen.get_scanner_ptr()->get_detpos_given_id(pos2); if (proj_data_info_gen.get_bin_for_det_pos_pair(bin, det_pos_pair) == Succeeded::yes) bin.set_bin_value(1); From f7df3b9e463669ec0cffa2a74ae1054a07d4f699 Mon Sep 17 00:00:00 2001 From: Parisa Khateri Date: Fri, 12 Jun 2020 14:18:12 +0200 Subject: [PATCH 07/81] add license --- src/IO/InterfileHeader.cxx | 1 + src/IO/interfile.cxx | 1 + .../GeometryBlocksOnCylindrical.cxx | 12 +++++++++- .../ProjDataInfoBlocksOnCylindrical.cxx | 12 +++++++++- ...ojDataInfoBlocksOnCylindricalNoArcCorr.cxx | 12 +++++++++- src/buildblock/ProjDataInfoGeneric.cxx | 15 +++++++++++++ .../ProjDataInfoGenericNoArcCorr.cxx | 18 +++++++++++++-- src/buildblock/Scanner.cxx | 2 ++ src/buildblock/VoxelsOnCartesianGrid.cxx | 1 + .../stir/GeometryBlocksOnCylindrical.h | 12 +++++++++- .../stir/GeometryBlocksOnCylindrical.inl | 12 +++++++++- src/include/stir/IO/InterfileHeader.h | 1 + src/include/stir/LORCoordinates.h | 1 + src/include/stir/LORCoordinates.inl | 1 + .../stir/ProjDataInfoBlocksOnCylindrical.h | 12 +++++++++- .../stir/ProjDataInfoBlocksOnCylindrical.inl | 12 +++++++++- ...ProjDataInfoBlocksOnCylindricalNoArcCorr.h | 12 +++++++++- ...ojDataInfoBlocksOnCylindricalNoArcCorr.inl | 12 +++++++++- src/include/stir/ProjDataInfoGeneric.h | 17 +++++++++++++- src/include/stir/ProjDataInfoGeneric.inl | 18 +++++++++++++-- .../stir/ProjDataInfoGenericNoArcCorr.h | 22 +++++++++++++++---- .../stir/ProjDataInfoGenericNoArcCorr.inl | 17 +++++++++++++- src/include/stir/Scanner.h | 3 ++- src/include/stir/Scanner.inl | 1 + src/include/stir/listmode/CListRecordSAFIR.h | 1 + .../stir/listmode/CListRecordSAFIR.inl | 1 + ...ataSymmetriesForBins_PET_CartesianGrid.inl | 2 ++ ...ataSymmetriesForBins_PET_CartesianGrid.cxx | 1 + .../ProjMatrixByBinUsingRayTracing.cxx | 1 + 29 files changed, 213 insertions(+), 20 deletions(-) diff --git a/src/IO/InterfileHeader.cxx b/src/IO/InterfileHeader.cxx index 7af18857f..49be7fe98 100644 --- a/src/IO/InterfileHeader.cxx +++ b/src/IO/InterfileHeader.cxx @@ -3,6 +3,7 @@ Copyright (C) 2000 - 2009-04-30, Hammersmith Imanet Ltd Copyright (C) 2011-07-01 - 2012, Kris Thielemans Copyright (C) 2013, 2016, 2018, 2020 University College London + Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics This file is part of STIR. This file is free software; you can redistribute it and/or modify diff --git a/src/IO/interfile.cxx b/src/IO/interfile.cxx index ee26ef849..f7316df7e 100644 --- a/src/IO/interfile.cxx +++ b/src/IO/interfile.cxx @@ -2,6 +2,7 @@ Copyright (C) 2000 PARAPET partners Copyright (C) 2000- 2011, Hammersmith Imanet Ltd Copyright (C) 2013, 2018, University College London + Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics This file is part of STIR. This file is free software; you can redistribute it and/or modify diff --git a/src/buildblock/GeometryBlocksOnCylindrical.cxx b/src/buildblock/GeometryBlocksOnCylindrical.cxx index 4d17ab84e..265873e30 100644 --- a/src/buildblock/GeometryBlocksOnCylindrical.cxx +++ b/src/buildblock/GeometryBlocksOnCylindrical.cxx @@ -1,8 +1,18 @@ /* +Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics -TODO copyright and License +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. */ /*! diff --git a/src/buildblock/ProjDataInfoBlocksOnCylindrical.cxx b/src/buildblock/ProjDataInfoBlocksOnCylindrical.cxx index 856a6361d..60740ffb0 100644 --- a/src/buildblock/ProjDataInfoBlocksOnCylindrical.cxx +++ b/src/buildblock/ProjDataInfoBlocksOnCylindrical.cxx @@ -1,8 +1,18 @@ /* +Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics -TODO copyright and License +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. */ /*! diff --git a/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx b/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx index f6d45a2f8..8c91bd01c 100644 --- a/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx +++ b/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx @@ -1,8 +1,18 @@ /* +Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics -TODO copyright and License +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. */ /*! diff --git a/src/buildblock/ProjDataInfoGeneric.cxx b/src/buildblock/ProjDataInfoGeneric.cxx index 8d65e9502..da9e0a89a 100644 --- a/src/buildblock/ProjDataInfoGeneric.cxx +++ b/src/buildblock/ProjDataInfoGeneric.cxx @@ -1,6 +1,19 @@ /* +Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics +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 @@ -8,6 +21,8 @@ \brief Non-inline implementations of stir::ProjDataInfoGeneric + \author Parisa Khateri + \author Michael Roethlisberger */ diff --git a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx index a28e59f69..839d8bf49 100644 --- a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx +++ b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx @@ -1,14 +1,28 @@ - /* +Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics + +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 projdata \brief Implementation of non-inline functions of class stir::ProjDataInfoGenericNoArcCorr - + + \author Parisa Khateri + \author Michael Roethlisberger */ #include "stir/ProjDataInfoGenericNoArcCorr.h" diff --git a/src/buildblock/Scanner.cxx b/src/buildblock/Scanner.cxx index 2e26019d9..925895c63 100644 --- a/src/buildblock/Scanner.cxx +++ b/src/buildblock/Scanner.cxx @@ -4,6 +4,8 @@ Copyright (C) 2011, Kris Thielemans Copyright (C) 2010-2013, King's College London Copyright (C) 2013-2016,2019, University College London + Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics + This file is part of STIR. This file is free software; you can redistribute it and/or modify diff --git a/src/buildblock/VoxelsOnCartesianGrid.cxx b/src/buildblock/VoxelsOnCartesianGrid.cxx index eeb44009e..3bbfdf94b 100644 --- a/src/buildblock/VoxelsOnCartesianGrid.cxx +++ b/src/buildblock/VoxelsOnCartesianGrid.cxx @@ -4,6 +4,7 @@ Copyright (C) 2000 PARAPET partners Copyright (C) 2000- 2012, Hammersmith Imanet Ltd Copyright (C) 2018- 2019, University College London + Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics This file is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by diff --git a/src/include/stir/GeometryBlocksOnCylindrical.h b/src/include/stir/GeometryBlocksOnCylindrical.h index d6817a18a..992db3470 100644 --- a/src/include/stir/GeometryBlocksOnCylindrical.h +++ b/src/include/stir/GeometryBlocksOnCylindrical.h @@ -1,8 +1,18 @@ /* +Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics -TODO copyright and License +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. */ /*! diff --git a/src/include/stir/GeometryBlocksOnCylindrical.inl b/src/include/stir/GeometryBlocksOnCylindrical.inl index 24e5524cb..c54fb2413 100644 --- a/src/include/stir/GeometryBlocksOnCylindrical.inl +++ b/src/include/stir/GeometryBlocksOnCylindrical.inl @@ -1,8 +1,18 @@ /* +Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics -TODO copyright and License +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. */ /*! diff --git a/src/include/stir/IO/InterfileHeader.h b/src/include/stir/IO/InterfileHeader.h index 8f96403d1..71df0db5c 100644 --- a/src/include/stir/IO/InterfileHeader.h +++ b/src/include/stir/IO/InterfileHeader.h @@ -1,6 +1,7 @@ /* Copyright (C) 2002-2007, Hammersmith Imanet Ltd Copyright (C) 2013, 2016, 2018, 2020 University College London + Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics This file is part of STIR. This file is free software; you can redistribute it and/or modify diff --git a/src/include/stir/LORCoordinates.h b/src/include/stir/LORCoordinates.h index 2499e259a..2c8c33d29 100644 --- a/src/include/stir/LORCoordinates.h +++ b/src/include/stir/LORCoordinates.h @@ -16,6 +16,7 @@ /* Copyright (C) 2004 - 2009-06-22, Hammersmith Imanet Ltd Copyright (C) 2011-07-01 - 2013, Kris Thielemans + Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics This file is part of STIR. This file is free software; you can redistribute it and/or modify diff --git a/src/include/stir/LORCoordinates.inl b/src/include/stir/LORCoordinates.inl index 593498a2f..021cfc2c5 100644 --- a/src/include/stir/LORCoordinates.inl +++ b/src/include/stir/LORCoordinates.inl @@ -12,6 +12,7 @@ */ /* Copyright (C) 2004- 2013, Hammersmith Imanet Ltd + Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics This file is part of STIR. This file is free software; you can redistribute it and/or modify diff --git a/src/include/stir/ProjDataInfoBlocksOnCylindrical.h b/src/include/stir/ProjDataInfoBlocksOnCylindrical.h index 38a276aaa..4af1345a1 100644 --- a/src/include/stir/ProjDataInfoBlocksOnCylindrical.h +++ b/src/include/stir/ProjDataInfoBlocksOnCylindrical.h @@ -1,8 +1,18 @@ /* +Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics -TODO copyright and License +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. */ /*! diff --git a/src/include/stir/ProjDataInfoBlocksOnCylindrical.inl b/src/include/stir/ProjDataInfoBlocksOnCylindrical.inl index e767c77d1..ce749dc7c 100644 --- a/src/include/stir/ProjDataInfoBlocksOnCylindrical.inl +++ b/src/include/stir/ProjDataInfoBlocksOnCylindrical.inl @@ -1,8 +1,18 @@ /* +Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics -TODO copyright and License +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. */ /*! diff --git a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h index 1bdaa06aa..7b10678b9 100644 --- a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h +++ b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h @@ -1,8 +1,18 @@ /* +Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics -TODO copyright and License +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. */ /*! diff --git a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl index 7be9ac510..bc894c707 100644 --- a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl +++ b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl @@ -1,8 +1,18 @@ /* +Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics -TODO copyright and License +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. */ /*! diff --git a/src/include/stir/ProjDataInfoGeneric.h b/src/include/stir/ProjDataInfoGeneric.h index f4a904819..b22b0295b 100644 --- a/src/include/stir/ProjDataInfoGeneric.h +++ b/src/include/stir/ProjDataInfoGeneric.h @@ -1,12 +1,27 @@ /* +Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics +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 projdata \brief Declaration of class stir::ProjDataInfoGeneric - + + \author Parisa Khateri + \author Michael Roethlisberger */ #ifndef __stir_ProjDataInfoGeneric_H__ #define __stir_ProjDataInfoGeneric_H__ diff --git a/src/include/stir/ProjDataInfoGeneric.inl b/src/include/stir/ProjDataInfoGeneric.inl index 183c3d7d5..48ca32fae 100644 --- a/src/include/stir/ProjDataInfoGeneric.inl +++ b/src/include/stir/ProjDataInfoGeneric.inl @@ -1,5 +1,17 @@ /* -TODO copyright and License +Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics + +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. */ /*! @@ -8,7 +20,9 @@ TODO copyright and License \ingroup projdata \brief Implementation of inline functions of class stir::ProjDataInfoGeneric - + + \author Parisa Khateri + \author Michael Roethlisberger */ // for sqrt diff --git a/src/include/stir/ProjDataInfoGenericNoArcCorr.h b/src/include/stir/ProjDataInfoGenericNoArcCorr.h index d9b894e4c..b6ae5440e 100644 --- a/src/include/stir/ProjDataInfoGenericNoArcCorr.h +++ b/src/include/stir/ProjDataInfoGenericNoArcCorr.h @@ -1,13 +1,27 @@ -// -// /* - */ +Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics + +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 projdata \brief Declaration of class stir::ProjDataInfoGenericNoArcCorr - + + \author Parisa Khateri + \author Michael Roethlisberger */ #ifndef __stir_ProjDataInfoGenericNoArcCorr_H__ #define __stir_ProjDataInfoGenericNoArcCorr_H__ diff --git a/src/include/stir/ProjDataInfoGenericNoArcCorr.inl b/src/include/stir/ProjDataInfoGenericNoArcCorr.inl index 537039747..b610637e1 100644 --- a/src/include/stir/ProjDataInfoGenericNoArcCorr.inl +++ b/src/include/stir/ProjDataInfoGenericNoArcCorr.inl @@ -1,13 +1,28 @@ /* +Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics +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 projdata \brief Implementation of inline functions of class ProjDataInfoGenericNoArcCorr - + + \author Parisa Khateri + \author Michael Roethlisberger */ diff --git a/src/include/stir/Scanner.h b/src/include/stir/Scanner.h index 0894f9fed..f504151e7 100644 --- a/src/include/stir/Scanner.h +++ b/src/include/stir/Scanner.h @@ -3,7 +3,8 @@ Copyright (C) 2000-2010, Hammersmith Imanet Ltd Copyright (C) 2011-2013, King's College London Copyright (C) 2016, 2019, UCL - + Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics + This file is part of STIR. This file is free software; you can redistribute it and/or modify diff --git a/src/include/stir/Scanner.inl b/src/include/stir/Scanner.inl index def47718e..0b8e2d027 100644 --- a/src/include/stir/Scanner.inl +++ b/src/include/stir/Scanner.inl @@ -4,6 +4,7 @@ Copyright (C) 2000 PARAPET partners Copyright (C) 2000- 2007, Hammersmith Imanet Ltd Copyright (C) 2016, UCL + Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics This file is part of STIR. This file is free software; you can redistribute it and/or modify diff --git a/src/include/stir/listmode/CListRecordSAFIR.h b/src/include/stir/listmode/CListRecordSAFIR.h index 737a40720..2e48be39e 100644 --- a/src/include/stir/listmode/CListRecordSAFIR.h +++ b/src/include/stir/listmode/CListRecordSAFIR.h @@ -5,6 +5,7 @@ jannis.fischer@cern.ch Copyright 2015 ETH Zurich, Institute of Particle Physics + Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/include/stir/listmode/CListRecordSAFIR.inl b/src/include/stir/listmode/CListRecordSAFIR.inl index cd08b38ed..ad141fe39 100644 --- a/src/include/stir/listmode/CListRecordSAFIR.inl +++ b/src/include/stir/listmode/CListRecordSAFIR.inl @@ -5,6 +5,7 @@ jannis.fischer@cern.ch Copyright 2015 ETH Zurich, Institute of Particle Physics + Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl b/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl index 59f03c075..6bdaac53d 100644 --- a/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl +++ b/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl @@ -14,6 +14,8 @@ /* Copyright (C) 2000 PARAPET partners Copyright (C) 2000- 2009, Hammersmith Imanet Ltd + Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics + This file is part of STIR. This file is free software; you can redistribute it and/or modify diff --git a/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx b/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx index 678c53481..b42298332 100644 --- a/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx +++ b/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx @@ -3,6 +3,7 @@ /* Copyright (C) 2000 PARAPET partners Copyright (C) 2000- 2009, Hammersmith Imanet Ltd + Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics This file is part of STIR. This file is free software; you can redistribute it and/or modify diff --git a/src/recon_buildblock/ProjMatrixByBinUsingRayTracing.cxx b/src/recon_buildblock/ProjMatrixByBinUsingRayTracing.cxx index a96927790..40d81019f 100644 --- a/src/recon_buildblock/ProjMatrixByBinUsingRayTracing.cxx +++ b/src/recon_buildblock/ProjMatrixByBinUsingRayTracing.cxx @@ -2,6 +2,7 @@ Copyright (C) 2000 PARAPET partners Copyright (C) 2000-2011, Hammersmith Imanet Ltd Copyright (C) 2013-2014, University College London + Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics This file is part of STIR. This file is free software; you can redistribute it and/or modify From 7b8d7914c01afb8d584bb3b341a12ca7bdccc5ce Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Mon, 13 Jul 2020 07:03:54 +0000 Subject: [PATCH 08/81] minor clean-up in GeometryBlocksOnCylindrical - moved some inline functions to .cxx. They were either not used frequently or probably too big to be inlined - made some member functions const and pass "bigger" arguments as references - avoid some warnings about double->float conversion The first item resolves a problem with SWIG compilation #604 by working around it. --- .../GeometryBlocksOnCylindrical.cxx | 76 ++++++++++++++++--- .../stir/GeometryBlocksOnCylindrical.h | 14 ++-- .../stir/GeometryBlocksOnCylindrical.inl | 74 +++--------------- 3 files changed, 83 insertions(+), 81 deletions(-) diff --git a/src/buildblock/GeometryBlocksOnCylindrical.cxx b/src/buildblock/GeometryBlocksOnCylindrical.cxx index 265873e30..8ceda4181 100644 --- a/src/buildblock/GeometryBlocksOnCylindrical.cxx +++ b/src/buildblock/GeometryBlocksOnCylindrical.cxx @@ -86,6 +86,58 @@ operator() (const stir::CartesianCoordinate3D& cart_coord1, return 0; } + +stir::Array<2, float> +GeometryBlocksOnCylindrical:: +get_rotation_matrix(float alpha) const +{ + return stir::make_array( + stir::make_1d_array(1.F,0.F,0.F), + stir::make_1d_array(0.F, std::cos(alpha), std::sin(alpha)), + stir::make_1d_array(0.F, -1*std::sin(alpha), std::cos(alpha)) + ); +} + + +Succeeded +GeometryBlocksOnCylindrical:: +find_detection_position_given_cartesian_coordinate(DetectionPosition<>& det_pos, + const CartesianCoordinate3D& cart_coord) const +{ + /*! first round the cartesian coordinates, it might happen that the cart_coord + is not precisely pointing to the center of the crystal and + then the det_pos cannot be found using the map + */ + //rounding cart_coord to 3 decimal place and find det_pos + CartesianCoordinate3D rounded_cart_coord; + rounded_cart_coord.z() = round(cart_coord.z()*1000.0F)/1000.0F; + rounded_cart_coord.y() = round(cart_coord.y()*1000.0F)/1000.0F; + rounded_cart_coord.x() = round(cart_coord.x()*1000.0F)/1000.0F; + if (detection_position_map_given_cartesian_coord_keys_3_decimal.count(rounded_cart_coord)) + { + det_pos = detection_position_map_given_cartesian_coord_keys_3_decimal.at(rounded_cart_coord); + return Succeeded::yes; + } + else + { + //rounding cart_coord to 2 decimal place and find det_pos + rounded_cart_coord.z() = round(cart_coord.z()*100.0F)/100.0F; + rounded_cart_coord.y() = round(cart_coord.y()*100.0f)/100.0F; + rounded_cart_coord.x() = round(cart_coord.x()*100.0F)/100.0F; + if (detection_position_map_given_cartesian_coord_keys_2_decimal.count(rounded_cart_coord)) + { + det_pos = detection_position_map_given_cartesian_coord_keys_2_decimal.at(rounded_cart_coord); + return Succeeded::yes; + } + else + { + warning("cartesian coordinate (x, y, z)=(%f, %f, %f) does not exist in the inner map", + cart_coord.x(), cart_coord.y(), cart_coord.z()); + return Succeeded::no; + } + } +} + void GeometryBlocksOnCylindrical:: build_crystal_maps() @@ -168,14 +220,14 @@ build_crystal_maps() stir::matrix_multiply(rotation_matrix, transformed_coord); // rounding cart_coord to 3 and 2 decimal points then filling maps - cart_coord.z() = (round(cart_coord.z()*1000.0))/1000.0; - cart_coord.y() = (round(cart_coord.y()*1000.0))/1000.0; - cart_coord.x() = (round(cart_coord.x()*1000.0))/1000.0; + cart_coord.z() = (round(cart_coord.z()*1000.0F))/1000.0F; + cart_coord.y() = (round(cart_coord.y()*1000.0F))/1000.0F; + cart_coord.x() = (round(cart_coord.x()*1000.0F))/1000.0F; cartesian_coord_map_given_detection_position_keys[det_pos] = cart_coord; //used to find s, m, phi, theta detection_position_map_given_cartesian_coord_keys_3_decimal[cart_coord] = det_pos; //used to find bin from listmode data - cart_coord.z() = (round(cart_coord.z()*100.0))/100.0; - cart_coord.y() = (round(cart_coord.y()*100.0))/100.0; - cart_coord.x() = (round(cart_coord.x()*100.0))/100.0; + cart_coord.z() = (round(cart_coord.z()*100.0F))/100.0F; + cart_coord.y() = (round(cart_coord.y()*100.0F))/100.0F; + cart_coord.x() = (round(cart_coord.x()*100.0F))/100.0F; detection_position_map_given_cartesian_coord_keys_2_decimal[cart_coord] = det_pos; } } @@ -232,14 +284,14 @@ build_crystal_maps() stir::matrix_multiply(rotation_matrix, transformed_coord); // rounding cart_coord to 3 and 2 decimal points then filling maps - cart_coord.z() = (round(cart_coord.z()*1000.0))/1000.0; - cart_coord.y() = (round(cart_coord.y()*1000.0))/1000.0; - cart_coord.x() = (round(cart_coord.x()*1000.0))/1000.0; + cart_coord.z() = (round(cart_coord.z()*1000.0F))/1000.0F; + cart_coord.y() = (round(cart_coord.y()*1000.0F))/1000.0F; + cart_coord.x() = (round(cart_coord.x()*1000.0F))/1000.0F; cartesian_coord_map_given_detection_position_keys[det_pos] = cart_coord; //used to find s, m, phi, theta detection_position_map_given_cartesian_coord_keys_3_decimal[cart_coord] = det_pos; //used to find bin from listmode data - cart_coord.z() = (round(cart_coord.z()*100.0))/100.0; - cart_coord.y() = (round(cart_coord.y()*100.0))/100.0; - cart_coord.x() = (round(cart_coord.x()*100.0))/100.0; + cart_coord.z() = (round(cart_coord.z()*100.0F))/100.0F; + cart_coord.y() = (round(cart_coord.y()*100.0F))/100.0F; + cart_coord.x() = (round(cart_coord.x()*100.0F))/100.0F; detection_position_map_given_cartesian_coord_keys_2_decimal[cart_coord] = det_pos; } } diff --git a/src/include/stir/GeometryBlocksOnCylindrical.h b/src/include/stir/GeometryBlocksOnCylindrical.h index 992db3470..354ecfcb1 100644 --- a/src/include/stir/GeometryBlocksOnCylindrical.h +++ b/src/include/stir/GeometryBlocksOnCylindrical.h @@ -82,20 +82,20 @@ class GeometryBlocksOnCylindrical }; //! Get rotation matrix for a given angle around z axis - inline stir::Array<2, float> get_rotation_matrix(float alpha); + stir::Array<2, float> get_rotation_matrix(float alpha) const; //! Build crystal map in cartesian coordinate void build_crystal_maps(); //! Get cartesian coordinate for a given detection position inline Succeeded - find_cartesian_coordinate_given_detection_position(CartesianCoordinate3D& , - DetectionPosition<>); + find_cartesian_coordinate_given_detection_position(CartesianCoordinate3D& , + const DetectionPosition<>&) const; - //! Get cartesian coordinate for a given detection position - inline Succeeded - find_detection_position_given_cartesian_coordinate(DetectionPosition<>&, - CartesianCoordinate3D); + //! Get cartesian coordinate for a given detection position + Succeeded + find_detection_position_given_cartesian_coordinate(DetectionPosition<>&, + const CartesianCoordinate3D&) const; //! Get scanner pointer inline const Scanner* get_scanner_ptr() const; diff --git a/src/include/stir/GeometryBlocksOnCylindrical.inl b/src/include/stir/GeometryBlocksOnCylindrical.inl index c54fb2413..699d43d32 100644 --- a/src/include/stir/GeometryBlocksOnCylindrical.inl +++ b/src/include/stir/GeometryBlocksOnCylindrical.inl @@ -27,72 +27,22 @@ limitations under the License. START_NAMESPACE_STIR - -stir::Array<2, float> -GeometryBlocksOnCylindrical:: -get_rotation_matrix(float alpha) -{ - return stir::make_array( - stir::make_1d_array(1.F,0.F,0.F), - stir::make_1d_array(0.F, std::cos(alpha), std::sin(alpha)), - stir::make_1d_array(0.F, -1*std::sin(alpha), std::cos(alpha)) - ); -} - Succeeded GeometryBlocksOnCylindrical:: find_cartesian_coordinate_given_detection_position(CartesianCoordinate3D& cart_coord, - DetectionPosition<> det_pos) -{ - if (cartesian_coord_map_given_detection_position_keys.count(det_pos)) - { - cart_coord = cartesian_coord_map_given_detection_position_keys[det_pos]; - return Succeeded::yes; - } - else - { - warning("detection position with (tangential_coord, axial_coord, radial_coord)=(%d, %d, %d) does not exist in the inner map", - det_pos.tangential_coord(), det_pos.axial_coord(), det_pos.radial_coord()); - return Succeeded::no; - } -} - -Succeeded -GeometryBlocksOnCylindrical:: -find_detection_position_given_cartesian_coordinate(DetectionPosition<>& det_pos, - CartesianCoordinate3D cart_coord) + const DetectionPosition<>& det_pos) const { - /*! first round the cartesian coordinates, it might happen that the cart_coord - is not precisely pointing to the center of the crystal and - then the det_pos cannot be found using the map - */ - //rounding cart_coord to 3 decimal place and find det_pos - cart_coord.z() = (round(cart_coord.z()*1000.0))/1000.0; - cart_coord.y() = (round(cart_coord.y()*1000.0))/1000.0; - cart_coord.x() = (round(cart_coord.x()*1000.0))/1000.0; - if (detection_position_map_given_cartesian_coord_keys_3_decimal.count(cart_coord)) - { - det_pos = detection_position_map_given_cartesian_coord_keys_3_decimal[cart_coord]; - return Succeeded::yes; - } - else - { - //rounding cart_coord to 3 decimal place and find det_pos - cart_coord.z() = (round(cart_coord.z()*100.0))/100.0; - cart_coord.y() = (round(cart_coord.y()*100.0))/100.0; - cart_coord.x() = (round(cart_coord.x()*100.0))/100.0; - if (detection_position_map_given_cartesian_coord_keys_2_decimal.count(cart_coord)) - { - det_pos = detection_position_map_given_cartesian_coord_keys_2_decimal[cart_coord]; - return Succeeded::yes; - } - else - { - warning("cartesian coordinate (x, y, z)=(%f, %f, %f) does not exist in the inner map", - cart_coord.x(), cart_coord.y(), cart_coord.z()); - return Succeeded::no; - } - } + if (cartesian_coord_map_given_detection_position_keys.count(det_pos)) + { + cart_coord = cartesian_coord_map_given_detection_position_keys.at(det_pos); + return Succeeded::yes; + } + else + { + warning("detection position with (tangential_coord, axial_coord, radial_coord)=(%d, %d, %d) does not exist in the inner map", + det_pos.tangential_coord(), det_pos.axial_coord(), det_pos.radial_coord()); + return Succeeded::no; + } } const Scanner* From 38cd06b153e96a3871baab3b866932ed922a7e3c Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Mon, 13 Jul 2020 18:14:08 +0000 Subject: [PATCH 09/81] removed some commented out lines in the SAFIR OSMAPOSL .par file They aren't used, and probably more confusing than anything else. --- .../OSMAPOSL_QuadraticPrior.par | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/examples/SAFIR_genericScanner/OSMAPOSL_QuadraticPrior.par b/examples/SAFIR_genericScanner/OSMAPOSL_QuadraticPrior.par index daceda7aa..d9148d989 100644 --- a/examples/SAFIR_genericScanner/OSMAPOSL_QuadraticPrior.par +++ b/examples/SAFIR_genericScanner/OSMAPOSL_QuadraticPrior.par @@ -86,20 +86,6 @@ save estimates at subiteration intervals:= 3 ; enable this for multiplicative form of OSMAPOSL (see User's Guide) ;MAP model := multiplicative -inter-update filter subiteration interval:= 0 -inter-update filter type := None - -inter-iteration filter subiteration interval:= 0 -inter-iteration filter type := Separable Cartesian Metz - Separable Cartesian Metz Filter Parameters := - x-dir filter FWHM (in mm):= 0.3 - y-dir filter FWHM (in mm):= 0.3 - z-dir filter FWHM (in mm):= 0.3 - x-dir filter Metz power:= 2.0 - y-dir filter Metz power:= 2.0 - z-dir filter Metz power:= 2.0 -END Separable Cartesian Metz Filter Parameters := - post-filter type := None END := From 7f0794a70d9b1f774f86de5878ec605ebaa9c1a7 Mon Sep 17 00:00:00 2001 From: Parisa Khateri Date: Thu, 16 Jul 2020 12:26:52 +0200 Subject: [PATCH 10/81] fix bug: set default scanner geometry as cylindrical to avoid errors when the scanner geometry is not defined in the header --- src/IO/InterfileHeader.cxx | 2 +- src/buildblock/Scanner.cxx | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/IO/InterfileHeader.cxx b/src/IO/InterfileHeader.cxx index 49be7fe98..bafa8bf49 100644 --- a/src/IO/InterfileHeader.cxx +++ b/src/IO/InterfileHeader.cxx @@ -612,7 +612,7 @@ InterfilePDFSHeader::InterfilePDFSHeader() add_key("Scanner orientation (X or Y)", KeyArgument::ASCII, &scanner_orientation); - scanner_geometry = "None"; + scanner_geometry = "Cylindrical"; add_key("Scanner geometry (BlocksOnCylindrical/Cylindrical/Generic)", KeyArgument::ASCII, &scanner_geometry); diff --git a/src/buildblock/Scanner.cxx b/src/buildblock/Scanner.cxx index 11ccab3c1..e6d5a0404 100644 --- a/src/buildblock/Scanner.cxx +++ b/src/buildblock/Scanner.cxx @@ -681,7 +681,12 @@ set_params(Type type_v,const list& list_of_names_v, reference_energy = reference_energy_v; scanner_orientation = scanner_orientation_v; - scanner_geometry = scanner_geometry_v; + + if (scanner_geometry_v == "") + scanner_geometry = "Cylindrical"; + else + scanner_geometry = scanner_geometry_v; + axial_crystal_spacing = axial_crystal_spacing_v; transaxial_crystal_spacing = transaxial_crystal_spacing_v; axial_block_spacing = axial_block_spacing_v; From db65ef28178666308da356811a869d0299269060 Mon Sep 17 00:00:00 2001 From: Parisa Khateri Date: Thu, 16 Jul 2020 13:00:34 +0200 Subject: [PATCH 11/81] add SAFIRDualRingPrototype to the list of defined scanners --- src/buildblock/Scanner.cxx | 32 +++++++++++++++++++++++++++++++- src/include/stir/Scanner.h | 2 +- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/buildblock/Scanner.cxx b/src/buildblock/Scanner.cxx index e6d5a0404..a53d1aaad 100644 --- a/src/buildblock/Scanner.cxx +++ b/src/buildblock/Scanner.cxx @@ -460,7 +460,37 @@ break; 0, 0, 0, 0, 0, 0, 0); break; - + + case SAFIRDualRingPrototype: + set_params(SAFIRDualRingPrototype, string_list("SAFIRDualRingPrototype"), + 16, //num_rings_v + 150, //max_num_non_arccorrected_bins_v, + 150, //default_num_arccorrected_bins_v, + 180, //num_detectors_per_ring_v + 64.05, // inner_ring_radius_v + 5, //average_depth_of_interaction_v + 2.2, //ring_spacing_v + 1.1, //bin_size_v + 0, //intrinsic_tilt_v + 2, //num_axial_blocks_per_bucket_v + 1, //num_transaxial_blocks_per_bucket_v + 8, //num_axial_crystals_per_block_v + 15, //num_transaxial_crystals_per_block_v + 1, //num_axial_crystals_per_singles_unit_v + 1, //num_transaxial_crystals_per_singles_unit_v + 1, //num_detector_layers_v + -1, //energy_resolution_v + -1, //reference_energy_v + "", //scanner_orientation_v + "", //scanner_geometry_v + 2.2, //axial_crystal_spacing_v + 2.2, //transaxial_crystal_spacing_v + 18.1, //axial_block_spacing_v + 33.6, //transaxial_block_spacing_v + ""//crystal_map_file_name_v + ); + break; + case User_defined_scanner: // zlong, 08-04-2004, Userdefined support set_params(User_defined_scanner, string_list("Userdefined"), diff --git a/src/include/stir/Scanner.h b/src/include/stir/Scanner.h index 400fc4caf..687e77c57 100644 --- a/src/include/stir/Scanner.h +++ b/src/include/stir/Scanner.h @@ -127,7 +127,7 @@ class Scanner */ enum Type {E931, E951, E953, E921, E925, E961, E962, E966, E1080, Siemens_mMR,Siemens_mCT, RPT,HiDAC, Advance, DiscoveryLS, DiscoveryST, DiscoverySTE, DiscoveryRX, Discovery600, PETMR_Signa, Discovery690, - HZLR, RATPET, PANDA, HYPERimage, nanoPET, HRRT, Allegro, GeminiTF, User_defined_scanner, + HZLR, RATPET, PANDA, HYPERimage, nanoPET, HRRT, Allegro, GeminiTF, SAFIRDualRingPrototype, User_defined_scanner, Unknown_scanner}; //! constructor that takes scanner type as an input argument From c948ae26dcf64bb9ff3818f8a59a6c0bb8703943 Mon Sep 17 00:00:00 2001 From: Parisa Khateri Date: Thu, 16 Jul 2020 17:05:52 +0200 Subject: [PATCH 12/81] small leftovers from block/generic implementation --- src/buildblock/ProjDataInfo.cxx | 30 ++++++++++++++++++------- src/buildblock/Scanner.cxx | 39 ++++++++++++++++++++++++--------- src/include/stir/Scanner.h | 2 ++ src/include/stir/Scanner.inl | 5 +++++ 4 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/buildblock/ProjDataInfo.cxx b/src/buildblock/ProjDataInfo.cxx index 60d7a36ff..6c60ae657 100644 --- a/src/buildblock/ProjDataInfo.cxx +++ b/src/buildblock/ProjDataInfo.cxx @@ -33,6 +33,8 @@ #include "stir/ProjDataInfo.h" #include "stir/ProjDataInfoCylindricalArcCorr.h" #include "stir/ProjDataInfoCylindricalNoArcCorr.h" +#include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" +#include "stir/ProjDataInfoGenericNoArcCorr.h" #include "stir/Scanner.h" #include "stir/Viewgram.h" #include "stir/Sinogram.h" @@ -421,22 +423,34 @@ ProjDataInfo::ProjDataInfoCTI(const shared_ptr& scanner, const float bin_size = scanner->get_default_bin_size(); - - if (arc_corrected) + if (scanner->get_scanner_geometry() == "BlocksOnCylindrical") + return + new ProjDataInfoBlocksOnCylindricalNoArcCorr(scanner, + num_axial_pos_per_segment, + min_ring_difference, + max_ring_difference, + num_views,num_tangential_poss); + else if (scanner->get_scanner_geometry() == "Generic") + return + new ProjDataInfoGenericNoArcCorr(scanner, + num_axial_pos_per_segment, + min_ring_difference, + max_ring_difference, + num_views,num_tangential_poss); + else if (scanner->get_scanner_geometry() == "Cylindrical" && arc_corrected) return new ProjDataInfoCylindricalArcCorr(scanner,bin_size, num_axial_pos_per_segment, - min_ring_difference, + min_ring_difference, max_ring_difference, num_views,num_tangential_poss); else return new ProjDataInfoCylindricalNoArcCorr(scanner, - num_axial_pos_per_segment, - min_ring_difference, - max_ring_difference, - num_views,num_tangential_poss); - + num_axial_pos_per_segment, + min_ring_difference, + max_ring_difference, + num_views,num_tangential_poss); } unique_ptr diff --git a/src/buildblock/Scanner.cxx b/src/buildblock/Scanner.cxx index a53d1aaad..31ff4bae4 100644 --- a/src/buildblock/Scanner.cxx +++ b/src/buildblock/Scanner.cxx @@ -1001,7 +1001,7 @@ check_consistency() const } if (get_transaxial_block_spacing()*get_num_transaxial_blocks_per_bucket() - < 2*inner_ring_radius*tan(_PI/get_num_transaxial_blocks()/get_num_transaxial_blocks_per_bucket())) + < round (2*inner_ring_radius*tan(_PI/get_num_transaxial_blocks()/get_num_transaxial_blocks_per_bucket())*1000.0)/1000.0) { warning("Scanner %s: inconsistent transaxial spacing:\n" "\ttransaxial_block_spacing %f muliplied by num_transaxial_blocks_per_bucket %d should fit into a polygon that encircles a cylinder with inner_ring_radius %f", @@ -1144,13 +1144,16 @@ Scanner::parameter_info() const if (crystal_map_file_name != "") s << "Name of crystal map := " << get_crystal_map_file_name() << '\n'; - if (get_scanner_orientation() != "" && get_scanner_geometry() != "") + if (get_scanner_geometry() != "") { - s << "Scanner orientation (X or Y) := " - < viewgram_blk = out_proj_data.get_empty_viewgram(out_proj_data.get_min_view_num(),seg); + Viewgram viewgram_cyl = in_pd_ptr->get_empty_viewgram(in_pd_ptr->get_min_view_num(),seg); + + for(int view=in_pdi_ptr->get_min_view_num(); view<=in_pdi_ptr->get_max_view_num();++view) + { + viewgram_blk = out_proj_data.get_empty_viewgram(view,seg); + viewgram_cyl = in_pd_ptr->get_viewgram(view,seg); + + for(int ax=in_pdi_ptr->get_min_axial_pos_num(seg); ax<=in_pdi_ptr->get_max_axial_pos_num(seg);++ax) + { + for(int tang=in_pdi_ptr->get_min_tangential_pos_num(); tang<=in_pdi_ptr->get_max_tangential_pos_num(); ++tang) + { + viewgram_blk[ax][tang] = viewgram_cyl[ax][tang]; + } + } + if (!(out_proj_data.set_viewgram(viewgram_blk)== Succeeded::yes)) + warning("Error set_segment for projdata_symm %d\n", seg); + } + } + + return EXIT_SUCCESS; +} diff --git a/src/utilities/find_counts_in_cylindrical_ROI.cxx b/src/utilities/find_counts_in_cylindrical_ROI.cxx new file mode 100644 index 000000000..296169a1e --- /dev/null +++ b/src/utilities/find_counts_in_cylindrical_ROI.cxx @@ -0,0 +1,231 @@ +/* +Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics + +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 utilities + + \brief Find count statistics in a cylindrical ROI for image and projection data + + \author Parisa Khateri + +*/ +#include "stir/utilities.h" +#include +#include +#include +#include "stir/shared_ptr.h" +#include "stir/ProjData.h" +#include "stir/CartesianCoordinate3D.h" +#include "stir/LORCoordinates.h" +#include "stir/Bin.h" +#include "stir/ProjDataInterfile.h" +#include "stir/IO/interfile.h" +#include +#include "stir/Shape/EllipsoidalCylinder.h" +#include "stir/is_null_ptr.h" +#include "stir/IO/read_from_file.h" +#include "stir/DiscretisedDensity.h" +#include "stir/VoxelsOnCartesianGrid.h" + + +USING_NAMESPACE_STIR + +int main(int argc, char **argv) +{ + bool is_projdata; + CartesianCoordinate3D centre(0,0,0); + + if (argc==5 && !strcmp(argv[4],"s")) + { + is_projdata = 1; + } + else if (argc==5 && !strcmp(argv[4],"v")) + { + is_projdata = 0; + } + else if (argc==8 && !strcmp(argv[4],"v")) + { + is_projdata = 0; + centre.z() = atof(argv[5]); + centre.y() = atof(argv[6]); + centre.x() = atof(argv[7]); + } + else + { + std::cerr<<"\nError: wrong arguments\n" + <<"Usage: "<< argv[0] + <<" input_file cylinder_radius(mm) cylinder_hight(mm) s/v [z(mm) y(mm) x(mm)]\n" + <<"use either s if the input is a projection data or v if the input is an image\n" + <<"option: if the input is an image, the center of ROI can be deterimined." + <<"Note that the center of Cartesian Coordinate is the center of scanner which is the default.\n" + <<"warning: input order matters\n\n"; + return EXIT_FAILURE; + } + + const double R = atof(argv[2]); // cylinder radius + const double h = atof(argv[3]); // cylinder hight + if (R<=0 || h<=0) + { + std::cerr <<"\nError: Radius and hight must be positive and larger than zero\n" + <<"Usage: "<< argv[0] + <<" input_file cylinder_radius(mm) cylinder_hight(mm) [p]\n" + <<"use option p if the input is projection data\n" + <<"warning: input order matters\n\n"; + return EXIT_FAILURE; + } + + if (is_projdata) //if the input is a projection data + { + shared_ptr projdata_ptr = ProjData::read_from_file(argv[1]); + if (is_null_ptr(projdata_ptr)) + { + std::cerr << "Could not read input file\n"; exit(EXIT_FAILURE); + } + + CartesianCoordinate3D c1, c2; + LORInAxialAndNoArcCorrSinogramCoordinates lor; + + double total_count=0; + double min_count=std::numeric_limits::max(); // minimum number of counts per LOR + double max_count=std::numeric_limits::min(); // minimum number of counts per LOR + double mean_count=0; //average number of counts per LOR in the active region + int num_active_LORs=0; //number of LORs which pass through the cylinder + for (int seg =projdata_ptr->get_min_segment_num(); seg <=projdata_ptr->get_max_segment_num(); ++seg) + for (int view =projdata_ptr->get_min_view_num(); view <=projdata_ptr->get_max_view_num(); ++view) + { + Viewgram cylinder_viewgram = projdata_ptr->get_viewgram(view, seg); + for (int ax =projdata_ptr->get_min_axial_pos_num(seg); ax <=projdata_ptr->get_max_axial_pos_num(seg); ++ax) + for (int tang =projdata_ptr->get_min_tangential_pos_num(); tang <=projdata_ptr->get_max_tangential_pos_num(); ++tang) + { + Bin bin(seg, view, ax, tang); + projdata_ptr->get_proj_data_info_sptr()->get_LOR(lor, bin); + LORAs2Points lor_as2points(lor); + LORAs2Points intersection_coords; + if (find_LOR_intersections_with_cylinder(intersection_coords, lor_as2points, R) ==Succeeded::yes) + { //this only succeeds if LOR is intersecting with the infinitely long cylinder + c1 = intersection_coords.p1(); + c2 = intersection_coords.p2(); + if (!( (c1.z()<-h/2. && c2.z()<-h/2.) || (c1.z()>h/2. && c2.z()>h/2.) )) + { + double N_lor = cylinder_viewgram[ax][tang]; //counts seen by this lor + float c12 = sqrt( pow(c1.z()-c2.z(), 2) // length of intersection of lor with the cylinder + + pow(c1.y()-c2.y(), 2) + + pow(c1.x()-c2.x(), 2) ); + if (c12>0.5) // if LOR intersection is lager than 0.5 mm, check the count per LOR + { + total_count+=N_lor; + num_active_LORs+=1; + if (N_lormax_count) max_count=N_lor; + } + } + } + } + } + mean_count=total_count/num_active_LORs; + std::cout<<"num_lor total_count mean_count min_count max_count : " + < > density_ptr + = read_from_file >(argv[1]); + const VoxelsOnCartesianGrid * image_ptr = + dynamic_cast *>(density_ptr.get()); + CartesianCoordinate3D voxel_size = image_ptr->get_voxel_size(); + + if (is_null_ptr(density_ptr)) + { + std::cerr << "Could not read input file\n"; exit(EXIT_FAILURE); + } + + EllipsoidalCylinder cylinder_shape(h,R,R,centre); + + double total_count=0; + double min_count=std::numeric_limits::max(); // minimum number of counts per LOR + double max_count=std::numeric_limits::min(); // minimum number of counts per LOR + double mean_count=0; //average number of counts per LOR in the active region + double STD=0; //standard deviation of counts per LOR in the active region + int num_voxels=0; + DiscretisedDensity<3,float>& image = *density_ptr; + const int min_k_index = image.get_min_index(); + const int max_k_index = image.get_max_index(); + for ( int k = min_k_index; k<= max_k_index; ++k) + { + const int min_j_index = image[k].get_min_index(); + const int max_j_index = image[k].get_max_index(); + for ( int j = min_j_index; j<= max_j_index; ++j) + { + const int min_i_index = image[k][j].get_min_index(); + const int max_i_index = image[k][j].get_max_index(); + for ( int i = min_i_index; i<= max_i_index; ++i) + { + /* + [min_i_index,max_i_index]=[-num_voxel_x/2,num_voxel_x/2] + [min_j_index,max_j_index]=[-num_voxel_y/2,num_voxel_y/2] + [min_k_index,max_k_index]=[0,num_voxel_z-1] + */ + + CartesianCoordinate3D voxel((k-max_k_index/2)*voxel_size.z(), + j*voxel_size.y(), + i*voxel_size.x()); + if (cylinder_shape.is_inside_shape(voxel)) + { + total_count+=image[k][j][i]; + num_voxels++; + if (image[k][j][i]max_count) max_count=image[k][j][i]; + } + } + } + } + mean_count=total_count/num_voxels; + double sum_for_std=0; + for ( int k = min_k_index; k<= max_k_index; ++k) + { + const int min_j_index = image[k].get_min_index(); + const int max_j_index = image[k].get_max_index(); + for ( int j = min_j_index; j<= max_j_index; ++j) + { + const int min_i_index = image[k][j].get_min_index(); + const int max_i_index = image[k][j].get_max_index(); + for ( int i = min_i_index; i<= max_i_index; ++i) + { + /* + [min_i_index,max_i_index]=[-num_voxel_x/2,num_voxel_x/2] + [min_j_index,max_j_index]=[-num_voxel_y/2,num_voxel_y/2] + [min_k_index,max_k_index]=[0,num_voxel_z-1] + */ + + CartesianCoordinate3D voxel((k-max_k_index/2)*voxel_size.z(), + j*voxel_size.y(), + i*voxel_size.x()); + if (cylinder_shape.is_inside_shape(voxel)) + { + sum_for_std+=pow((image[k][j][i] - mean_count),2); + } + } + } + } + STD=sqrt(sum_for_std/total_count); + std::cout<<"num_voxels total_count mean_count STD min_count max_count : " + < +#include +#include +#include "stir/shared_ptr.h" +#include "stir/ProjData.h" +#include "stir/CartesianCoordinate3D.h" +#include "stir/LORCoordinates.h" +#include "stir/Bin.h" +#include "stir/ProjDataInterfile.h" +#include "stir/IO/interfile.h" +#include + +USING_NAMESPACE_STIR + +int main(int argc, char **argv) +{ + if (argc!=4) + { + std::cerr << "Usage: "<< argv[0] + <<" output_file_name_prefix cylider_measured_data cylinder_radius(mm)\n" + <<"only cylinder data are supported. The radius should be the radius of the measured cylinder data.\n" + <<"warning: mind the input order\n"; + return EXIT_FAILURE; + } + + shared_ptr cylinder_projdata_ptr = ProjData::read_from_file(argv[2]); + const std::string output_file_name = argv[1]; + const float R = atof(argv[3]); // cylinder radius (mm) + if (R==0) + { + std::cerr << " Radius must be a float value\n" + <<"Usage: "<< argv[0] + <<" output_file_name_prefix cylider_measured_data cylinder_radius\n" + <<"warning: mind the input order\n"; + return EXIT_FAILURE; + } + + //output file + shared_ptr cylinder_pdi_ptr(cylinder_projdata_ptr->get_proj_data_info_sptr()->clone()); + + ProjDataInterfile output_projdata(cylinder_projdata_ptr->get_exam_info_sptr(), cylinder_pdi_ptr, output_file_name); + write_basic_interfile_PDFS_header(output_file_name, output_projdata); + + CartesianCoordinate3D c1, c2; + LORInAxialAndNoArcCorrSinogramCoordinates lor; + + // first find the average number of counts per LOR + float total_count=0; + float min_count=std::numeric_limits::max(); // minimum number of counts per LOR + float average_count=0; //average number of counts per LOR in the active region + int num_active_LORs=0; //number of LORs which pass through the cylinder + for (int seg =cylinder_projdata_ptr->get_min_segment_num(); seg <=cylinder_projdata_ptr->get_max_segment_num(); ++seg) + for (int view =cylinder_projdata_ptr->get_min_view_num(); view <=cylinder_projdata_ptr->get_max_view_num(); ++view) + { + Viewgram cylinder_viewgram = cylinder_projdata_ptr->get_viewgram(view, seg); + for (int ax =cylinder_projdata_ptr->get_min_axial_pos_num(seg); ax <=cylinder_projdata_ptr->get_max_axial_pos_num(seg); ++ax) + for (int tang =cylinder_projdata_ptr->get_min_tangential_pos_num(); tang <=cylinder_projdata_ptr->get_max_tangential_pos_num(); ++tang) + { + Bin bin(seg, view, ax, tang); + cylinder_projdata_ptr->get_proj_data_info_sptr()->get_LOR(lor, bin); + LORAs2Points lor_as2points(lor); + LORAs2Points intersection_coords; + if (find_LOR_intersections_with_cylinder(intersection_coords, lor_as2points, R) ==Succeeded::yes) + { //this only succeeds if LOR is intersecting with the cylinder + float N_lor = cylinder_viewgram[ax][tang]; //counts seen by this lor + c1 = intersection_coords.p1(); + c2 = intersection_coords.p2(); + float c12 = sqrt( pow(c1.z()-c2.z(), 2) // length of intersection of lor with the cylinder + + pow(c1.y()-c2.y(), 2) + + pow(c1.x()-c2.x(), 2) ); + if (c12>0.5) // if LOR intersection is lager than 0.5 mm, check the count per LOR + { + float N_lor_corrected=N_lor/c12; // corrected for the length + total_count+=N_lor_corrected; + num_active_LORs+=1; + if (N_lor_correctedget_min_segment_num(); seg <=cylinder_projdata_ptr->get_max_segment_num(); ++seg) + for (int view =cylinder_projdata_ptr->get_min_view_num(); view <=cylinder_projdata_ptr->get_max_view_num(); ++view) + { + Viewgram cylinder_viewgram = cylinder_projdata_ptr->get_viewgram(view, seg); + Viewgram out_viewgram = cylinder_projdata_ptr->get_empty_viewgram(view, seg); + for (int ax =cylinder_projdata_ptr->get_min_axial_pos_num(seg); ax <=cylinder_projdata_ptr->get_max_axial_pos_num(seg); ++ax) + for (int tang =cylinder_projdata_ptr->get_min_tangential_pos_num(); tang <=cylinder_projdata_ptr->get_max_tangential_pos_num(); ++tang) + { + Bin bin(seg, view, ax, tang); + cylinder_projdata_ptr->get_proj_data_info_sptr()->get_LOR(lor, bin); + LORAs2Points lor_as2points(lor); + LORAs2Points intersection_coords; + float NF_lor; + if (find_LOR_intersections_with_cylinder(intersection_coords, lor_as2points, R) ==Succeeded::yes) + { //this only succeeds if LOR is intersecting with the cylinder + + /* + for each lor + find_LOR_intersections_with_cylinder => c1 & c2 + c12 = |c1-c2| = sqrt(Δx^2+Δy^2+Δz^2) + N_lor/c12 should be the same for all therefore: + NF_lor= / (N_lor/c12) + */ + + float N_lor = cylinder_viewgram[ax][tang]; //counts seen by this lor + c1 = intersection_coords.p1(); + c2 = intersection_coords.p2(); + float c12 = sqrt( pow(c1.z()-c2.z(), 2) // length of intersection of lor with the cylinder + + pow(c1.y()-c2.y(), 2) + + pow(c1.x()-c2.x(), 2) ); + if (N_lor<1) //if inside the cylinder but the value is too small + { + NF_lor = average_count*c12/min_count; + } + else + NF_lor = average_count*c12/N_lor; + } + else //if out of the cylinder set it to a small value instead of zero, otherwise normalisation gives strange recon image. + { + NF_lor=0.0001; + } + out_viewgram[ax][tang] = NF_lor; + } + output_projdata.set_viewgram(out_viewgram); + } +} diff --git a/src/utilities/find_recovery_coefficients_in_image_quality_phantom_nema_nu4.cxx b/src/utilities/find_recovery_coefficients_in_image_quality_phantom_nema_nu4.cxx new file mode 100644 index 000000000..739bb8cdf --- /dev/null +++ b/src/utilities/find_recovery_coefficients_in_image_quality_phantom_nema_nu4.cxx @@ -0,0 +1,388 @@ +/* +Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics + +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 utilities + + \brief Utility program for calculating recovery coeficient values in the image + quality phantom described in NEMA NU 4. + + \author Parisa Khateri + +*/ + +#include "stir/VoxelsOnCartesianGrid.h" +#include "stir/DiscretisedDensity.h" +#include "stir/Shape/EllipsoidalCylinder.h" +#include "stir/CartesianCoordinate2D.h" +#include "stir/CartesianCoordinate3D.h" + +#include +#include +#include +#include +#include "stir/IO/read_from_file.h" +#include "stir/info.h" +#include "stir/Succeeded.h" + +#ifndef STIR_NO_NAMESPACES +using std::cerr; +using std::endl; +using std::ofstream; +using std::vector; +#endif + +USING_NAMESPACE_STIR + + +class FindRecoveryCoefficient : public ParsingObject +{ +public: + + FindRecoveryCoefficient(const char * const par_filename); + Succeeded compute(); + +private: + typedef ParsingObject base_type; + int min_index_for_average_plane; //min plane number + int max_index_for_average_plane; //max plane number + float mean_uniform_region; + float STD_uniform_region; + int start_z_of_rods; //axial position of start point of the rods + int stop_z_of_rods; //axial position of end point of the rods + std::vector ROIs_x; // order is from the smallest ROI to the largest + std::vector ROIs_y; // order is from the smallest ROI to the largest + std::string input_filename; + std::string output_filename; + + void initialise_keymap(); + void set_defaults(); +}; + +void FindRecoveryCoefficient:: +initialise_keymap() +{ + this->parser.add_start_key("FindRecoveryCoefficient Parameters"); + this->parser.add_key("input filename",&input_filename); + this->parser.add_key("output filename",&output_filename); + this->parser.add_key("minimun index to calculate the average plane", &min_index_for_average_plane); + this->parser.add_key("maximum index to calculate the average plane", &max_index_for_average_plane); + this->parser.add_key("mean value of uniform region", &mean_uniform_region); + this->parser.add_key("STD of uniform region", &STD_uniform_region); + this->parser.add_key("start index of rods (z value)", &start_z_of_rods); + this->parser.add_key("stop index of rods (z value)", &stop_z_of_rods); + this->parser.add_key("x coordinates of center of ROIs (in mm)", &ROIs_x); + this->parser.add_key("y coordinates of center of ROIs (in mm)", &ROIs_y); + this->parser.add_stop_key("END FindRecoveryCoefficient Parameters"); +} + +void FindRecoveryCoefficient:: +set_defaults() +{ + // specify defaults for the parameters in case they are not set. + min_index_for_average_plane= 88; + max_index_for_average_plane= 98; + mean_uniform_region = 0.00569557; + STD_uniform_region = 0.00107216; + start_z_of_rods = 86; + stop_z_of_rods = 102; + ROIs_x = {2.16312,-5.66312,-5.66312,2.16312,7.}; + ROIs_y = {6.6574,4.1145,-4.1145,-6.6574,0.}; + input_filename.resize(0); + output_filename.resize(0); +} + +FindRecoveryCoefficient:: +FindRecoveryCoefficient(const char * const par_filename) +{ + set_defaults(); + if (par_filename!=0) + { + if (parse(par_filename) == false) + exit(EXIT_FAILURE); + } + else + ask_parameters(); +} + + +void +compute_average_plane_in_given_range(VoxelsOnCartesianGrid& average_plane, + const VoxelsOnCartesianGrid& image, + const int& min_index, const int& max_index) +{ + int min_z = image.get_min_z(); + int max_z = image.get_max_z(); + int min_y = image.get_min_y(); + int min_x = image.get_min_x(); + int max_y = image.get_max_y(); + int max_x = image.get_max_x(); + if (min_indexmax_z) + { + error("min_index & max_index are not in the range of provided image!"); + } + + + shared_ptr > sum_planes_ptr(image.get_empty_voxels_on_cartesian_grid()); + VoxelsOnCartesianGrid sum_planes = *sum_planes_ptr; + int num_planes = max_index - min_index + 1; + + for (int z=min_index; z<=max_index; z++) + for(int y=min_y; y<=max_y; y++) + for(int x=min_x; x<=max_x; x++) + { + sum_planes[0][y][x] += image[z][y][x]; + } + for(int y=min_y; y<=max_y; y++) + for(int x=min_x; x<=max_x; x++) + { + average_plane[0][y][x] = sum_planes[0][y][x] / num_planes; + } +} + +void +build_ROIs(vector & ROIs, float length_z, std::vector ROIs_x, std::vector ROIs_y) +{ + //note: the y coordinate is (-1*y) of coordinates in your images. STIR assumes y axis downward. + vector> ROI_centers{CartesianCoordinate2D(ROIs_y[0],ROIs_x[0]), //(y,x) + CartesianCoordinate2D(ROIs_y[1],ROIs_x[1]), + CartesianCoordinate2D(ROIs_y[2],ROIs_x[2]), + CartesianCoordinate2D(ROIs_y[3],ROIs_x[3]), + CartesianCoordinate2D(ROIs_y[4],ROIs_x[4])}; + for (int i = 0; i<5; i++) + { + float d = 2*(i+1); //ROI diameter in mm + float radius_y =d/2.; + float radius_x = d/2.; + CartesianCoordinate3D centre(0, ROI_centers[i].y(), ROI_centers[i].x()); + EllipsoidalCylinder current_ROI(length_z, radius_y, radius_x, centre); + ROIs.push_back(current_ROI); + } +} + + +void +find_max_in_ROI(float &max, CartesianCoordinate3D& max_coord, + const VoxelsOnCartesianGrid& image, + const EllipsoidalCylinder& ROI) +{ + bool change = 0; // to make sure there is any non-zero voxel + const int min_y = image.get_min_y(); + const int min_x = image.get_min_x(); + const int max_y = image.get_max_y(); + const int max_x = image.get_max_x(); + + shared_ptr > + discretised_shape_ptr(image.get_empty_voxels_on_cartesian_grid()); + ROI.construct_volume(*discretised_shape_ptr, Coordinate3D(1,1,1)); // TODO number_samples=1, make it general + VoxelsOnCartesianGrid discretised_shape = *discretised_shape_ptr; + + max = std::numeric_limits::lowest(); + + //iterate only over y , x. because the image is actualy the average plane in which only z=0 is valid + for(int y=min_y; y<=max_y; y++) + for(int x=min_x; x<=max_x; x++) + { + int z=0; + const float weight = discretised_shape[z][y][x]; + if (weight ==0) + continue; + + change = 1; + CartesianCoordinate3D current_index(z,y,x); + const float current_value = image[z][y][x]; + if (current_value>max) + { + max=current_value; + max_coord = current_index; + } + } + if (change ==0) + error("Max was not found in this range. All voxels are zero\n"); +} + +void +find_max_in_all_ROIs(vector& maxs, + vector>& max_coords, + const VoxelsOnCartesianGrid& image, + const vector& ROIs) +{ + for (unsigned int i=0; i max_coord; + find_max_in_ROI(max, max_coord, image, ROIs[i]); + maxs.push_back(max); + max_coords.push_back(max_coord); + } +} + +void +find_mean_STD_along_lineprofile(float & mean, float & STD, + const VoxelsOnCartesianGrid& image, + char direction, int min_index, int max_index, + const CartesianCoordinate3D& pos) +{ + float sum = 0; + float variance = 0; + switch(direction) + { + case 'z': + { + for (int z=min_index; z<=max_index; z++) + { + sum += image[z][pos.y()][pos.x()]; + } + + mean = sum/(max_index-min_index+1); + + for (int z=min_index; z<=max_index; z++) + { + variance += pow((image[z][pos.y()][pos.x()] - mean), 2); + } + + variance /= (max_index-min_index+1); + STD = sqrt(variance); + break; + } + case 'y': + { + for (int y=min_index; y<=max_index; y++) + { + sum += image[pos.z()][y][pos.x()]; + } + + mean = sum/(max_index-min_index+1); + + for (int y=min_index; y<=max_index; y++) + { + variance += pow((image[pos.z()][y][pos.x()] - mean), 2); + } + + variance /= (max_index-min_index+1); + STD = sqrt(variance); + break; + } + case 'x': + { + for (int x=min_index; x<=max_index; x++) + { + sum += image[pos.z()][pos.y()][x]; + } + mean = sum/(max_index-min_index+1); + + for (int x=min_index; x<=max_index; x++) + { + variance += pow((image[pos.z()][pos.y()][x] - mean), 2); + } + + variance /= (max_index-min_index+1); + STD = sqrt(variance); + break; + } + } +} + +Succeeded FindRecoveryCoefficient:: +compute() +{ + shared_ptr > + density(read_from_file >(input_filename)); + + const VoxelsOnCartesianGrid& image = + dynamic_cast&>(*density); + + ofstream out (output_filename +".txt"); + if (!out) + { + warning("Cannot open output file.\n"); + return Succeeded::no; + } + + vector ROI_maxs; + vector> ROI_max_coords; + + // average over 10 middle slices and save the result in one slice + shared_ptr > average_plane_ptr(image.get_empty_voxels_on_cartesian_grid()); + VoxelsOnCartesianGrid average_plane = *average_plane_ptr; + info("Computing average plane"); + compute_average_plane_in_given_range(average_plane, image, + min_index_for_average_plane, max_index_for_average_plane); + info("Done computing average plane"); + + // draw circular ROIs around each rod with D=2 * rod_d then find max in each ROIs + vector ROIs; + float length_z = image.get_voxel_size().z(); + info("Building ROIs"); + build_ROIs(ROIs, length_z, ROIs_x, ROIs_y); + info("Done building ROIs"); + + //find maximum and its corresponding coordinate in ROIs + info("Finding max in ROIs"); + find_max_in_all_ROIs(ROI_maxs, ROI_max_coords, average_plane, ROIs); + info("Done find max in ROIs"); + + // draw line profiles along the rods on the pixel coordinates with max ROI + //find pixel values along line profiles + //determine mean and standard deviation of the valuse. + out<<"Results of recovery coeficient for "< +#include +#include "stir/ProjDataFromStream.h" +#include "stir/Sinogram.h" +#include "stir/IO/read_from_file.h" +#include "stir/SegmentByView.h" +#include "stir/ProjDataInterfile.h" +#include "stir/ProjDataInfo.h" + + + + +#ifndef STIR_NO_NAMESPACES +using std::cerr; +#endif + +USING_NAMESPACE_STIR + +int main(int argc, char *argv[]) +{ + if(argc<6) + { + cerr<<"\tUsage: " << argv[0] << " output_filename input_filename segment_number viewgram/simogram view_number/axial_pos_number\n"; + cerr<<"\tviewgram: to calculate sum projection for a viewgram\n"; + cerr<<"\tsinogram: to calculate sum projection for a sinogram\n"; + exit(EXIT_FAILURE); + } + std::string output_filename=argv[1]; + shared_ptr in_proj_data_ptr = ProjData::read_from_file(argv[2]); + if (strcmp(argv[4], "viewgram")==0) + { + const int segment_num = atoi(argv[3]); + const int view_num = atoi(argv[5]); + shared_ptr pdi_ptr (in_proj_data_ptr->get_proj_data_info_sptr()->clone()); + std::cout<<"[min_tang_pos, max_tang_pos]=["<get_min_view_num()<<", "<get_max_view_num()<<"]\n"; + if (pdi_ptr->get_num_tangential_poss()/2.==0) + std::cout<<"num_tang_pos is even\n"; + + if (segment_num get_min_segment_num() || segment_num > in_proj_data_ptr->get_max_segment_num()) + error("segment_num is out of range!\n"); + if (view_num get_min_view_num() || view_num > in_proj_data_ptr->get_max_view_num()) + error("view_num is out of range!\n"); + + SegmentByView segment_by_view = in_proj_data_ptr->get_segment_by_view(segment_num); + Viewgram view = segment_by_view.get_viewgram(view_num); + std::vector squeezed_view; + squeezed_view.reserve(view.get_num_tangential_poss()); + for (int tang = view.get_min_tangential_pos_num(); + tang <= view.get_max_tangential_pos_num(); + ++tang) + { + float sum_bins = 0; + for (int ax = view.get_min_axial_pos_num(); + ax <= view.get_max_axial_pos_num(); + ++ax) + { + sum_bins+= view[ax][tang]; + } + squeezed_view.push_back(sum_bins); + } + std::ofstream out(output_filename+".txt"); + out<<"Values of the squeezed view for segment_num="< output_iterator(out, "\n"); + std::copy(squeezed_view.begin(), squeezed_view.end(), output_iterator); + return EXIT_SUCCESS; + } + else if (strcmp(argv[4], "sinogram")==0) + { + const int segment_num = atoi(argv[3]); + const int axial_pos_num = atoi(argv[5]); + shared_ptr pdi_ptr (in_proj_data_ptr->get_proj_data_info_sptr()->clone()); + std::cout<<"[min_axial_pos, max_axial_pos]=["<get_min_axial_pos_num(segment_num)<<", "<get_max_axial_pos_num(segment_num)<<"]\n"; + + if (segment_num get_min_segment_num() || segment_num > in_proj_data_ptr->get_max_segment_num()) + error("segment_num is out of range!\n"); + if (axial_pos_num get_min_axial_pos_num(segment_num) || axial_pos_num > in_proj_data_ptr->get_max_axial_pos_num(segment_num)) + error("axial_pos_num is out of range!\n"); + + SegmentBySinogram segment_by_sino = in_proj_data_ptr->get_segment_by_sinogram(segment_num); + Sinogram sino = segment_by_sino.get_sinogram(axial_pos_num); + std::vector squeezed_sino; + squeezed_sino.reserve(sino.get_num_tangential_poss()); + for (int tang = sino.get_min_tangential_pos_num(); + tang <= sino.get_max_tangential_pos_num(); + ++tang) + { + float sum_bins = 0; + for (int view = sino.get_min_view_num(); + view <= sino.get_max_view_num(); + ++view) + { + sum_bins+= sino[view][tang]; + } + squeezed_sino.push_back(sum_bins); + } + std::ofstream out(output_filename+".txt"); + out<<"Values of the squeezed sino for segment_num="< output_iterator(out, "\n"); + std::copy(squeezed_sino.begin(), squeezed_sino.end(), output_iterator); + return EXIT_SUCCESS; + } + else + { + cerr<<"\tYou should determine either 'sinogram' or 'viewgram'\n"; + cerr<<"\tUsage: " << argv[0] << " output_filename input_filename segment_number viewgram/simogram view_number/axial_pos_number\n"; + cerr<<"\tviewgram: to calculate sum projection for a viewgram\n"; + cerr<<"\tsinogram: to calculate sum projection for a sinogram\n"; + exit(EXIT_FAILURE); + } +} diff --git a/src/utilities/separate_true_from_random_scatter_for_necr.cxx b/src/utilities/separate_true_from_random_scatter_for_necr.cxx new file mode 100644 index 000000000..9deaee42b --- /dev/null +++ b/src/utilities/separate_true_from_random_scatter_for_necr.cxx @@ -0,0 +1,301 @@ +/* +Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics + +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 utilities + + \brief This program gets a projection data file of a mouse/rat scatter phantom measured according to NEMA NU 4. + It seperates number of true events from number of random+scatter events. + + \author Parisa Khateri + +*/ + +#include "stir/ProjData.h" +#include "stir/IO/interfile.h" +#include "stir/utilities.h" +#include "stir/Bin.h" +#include +#include +#include +#include "stir/ProjDataFromStream.h" +#include "stir/Sinogram.h" +#include "stir/IO/read_from_file.h" +#include "stir/SegmentByView.h" +#include "stir/ProjDataInterfile.h" +#include "stir/ProjDataInfo.h" +#include + + + +#ifndef STIR_NO_NAMESPACES +using std::cerr; +#endif + +USING_NAMESPACE_STIR + +int find_num_tang_pos_for_FOV(float a, float r, int n) +{ //if r is radius, s is length of arc, a is length of chord, and n is number of azimuthal angles (num_detectors_per_ring) + float theta = asin((a/2.)/r); // the azimuthal angle (in radian) covered by the FOV in cylindrical geometry + float delta = 2*_PI/n;// the azimuthal angle covered by a detector element in cylindrical geometry + return int(round(2*theta/delta)); +} + +float find_length_of_arc_for_FOV(float a, float r) +{ //if r is radius, s is length of arc and a is length of chord + // s = r * theta + // theta = 2* asin(a/2/r) + float theta = 2*asin((a/2.)/r); // the azimuthal angle (in radian) covered by the FOV in cylindrical geometry + float s = r * theta; //length of arc + return s; +} + +int main(int argc, char *argv[]) +{ + if(argc<5) + { + cerr<<"\tUsage: " << argv[0] << " output_filename input_filename phantom_diameter single/all [axial_pos_number]\n"; + cerr<<"\tchoose single to calculate sum of the rows of a single sinogram. axial_pos_number is required for this option.\n"; + cerr<<"\tchoose all to calculate sum of the sum of the rows of all sinograms\n"; + cerr<<"\tthe input projection data should have already been processed by SSRB.\n"; + exit(EXIT_FAILURE); + } + std::string output_filename=argv[1]; + shared_ptr in_proj_data_ptr = ProjData::read_from_file(argv[2]); + const float phantom_diameter = atof(argv[3]); + const int segment_num = 0; + + if (strcmp(argv[4], "single")==0 && argc==6 ) + { + const int axial_pos_num = atoi(argv[5]); + shared_ptr pdi_ptr (in_proj_data_ptr->get_proj_data_info_sptr()->clone()); + + if (axial_pos_num get_min_axial_pos_num(segment_num) || axial_pos_num > in_proj_data_ptr->get_max_axial_pos_num(segment_num)) + error("axial_pos_num is out of range!\n"); + + SegmentBySinogram segment_by_sino = in_proj_data_ptr->get_segment_by_sinogram(segment_num); + Sinogram sino = segment_by_sino.get_sinogram(axial_pos_num); + + //find max index in each row of sinogram and shift + Sinogram shifted_sino = in_proj_data_ptr->get_empty_sinogram(axial_pos_num,segment_num); + for (int view = sino.get_min_view_num(); + view <= sino.get_max_view_num(); + ++view) + { + float max=0; + int max_idx=0; + + //find max index in each row of sinogram + for (int tang = sino.get_min_tangential_pos_num(); + tang <= sino.get_max_tangential_pos_num(); + ++tang) + { + if (max squeezed_sino; + squeezed_sino.reserve(sino.get_num_tangential_poss()); + for (int tang = sino.get_min_tangential_pos_num(); + tang <= sino.get_max_tangential_pos_num(); + ++tang) + { + float sum_bins = 0; + for (int view = sino.get_min_view_num(); + view <= sino.get_max_view_num(); + ++view) + { + sum_bins+= shifted_sino[view][tang]; + } + squeezed_sino.push_back(sum_bins); + } + std::ofstream out(output_filename+".txt"); + out<<"Values of the squeezed sino for segment_num="< output_iterator(out, "\n"); + std::copy(squeezed_sino.begin(), squeezed_sino.end(), output_iterator); + return EXIT_SUCCESS; + + } + if (strcmp(argv[4], "all")==0 && argc==5 ) + { + shared_ptr pdi_ptr (in_proj_data_ptr->get_proj_data_info_sptr()->clone()); + + // keep sinograms out of the loop to avoid reallocations + // initialise to something because there's no default constructor + Sinogram sino = + in_proj_data_ptr->get_empty_sinogram(in_proj_data_ptr->get_min_axial_pos_num(segment_num),segment_num); + Sinogram shifted_sino = + in_proj_data_ptr->get_empty_sinogram(in_proj_data_ptr->get_min_axial_pos_num(segment_num),segment_num); + + std::vector squeezed_sino_all(in_proj_data_ptr->get_num_tangential_poss(), 0.0); + for (int axial_pos_num = pdi_ptr->get_min_axial_pos_num(segment_num); + axial_pos_num <= pdi_ptr->get_max_axial_pos_num(segment_num); + ++axial_pos_num) + { + sino = in_proj_data_ptr->get_sinogram(axial_pos_num, segment_num); + shifted_sino = in_proj_data_ptr->get_empty_sinogram(axial_pos_num,segment_num); + + //find max index in each row of sinogram and shift + for (int view = sino.get_min_view_num(); + view <= sino.get_max_view_num(); + ++view) + { + float max=0; + int max_idx=0; + + //find max index in each row of sinogram + for (int tang = sino.get_min_tangential_pos_num(); + tang <= sino.get_max_tangential_pos_num(); + ++tang) + { + if (maxsino.get_max_tangential_pos_num()) + shifted_tang= tang - sino.get_num_tangential_poss() - max_idx; + assert(shifted_tang>=sino.get_min_tangential_pos_num() && shifted_tang<=sino.get_max_tangential_pos_num()); + shifted_sino[view][shifted_tang]=sino[view][tang]; + } + } + + //sum over the shifted rows in sinograms + for (int tang = sino.get_min_tangential_pos_num(); + tang <= sino.get_max_tangential_pos_num(); + ++tang) + { + float sum_bins = 0; + for (int view = sino.get_min_view_num(); + view <= sino.get_max_view_num(); + ++view) + { + sum_bins+= shifted_sino[view][tang]; + } + assert(tang+sino.get_max_tangential_pos_num()+1>=0 && tang+sino.get_max_tangential_pos_num()+1 output_iterator(out, "\n"); + std::copy(squeezed_sino_all.begin(), squeezed_sino_all.end(), output_iterator); + + //trim sino with 8mm band around the phantom according to NEMA NU4 + int num_tang_pos_for_FOV = find_num_tang_pos_for_FOV(phantom_diameter+16, //16 means 8 mm from each side of the sinogram + pdi_ptr->get_scanner_ptr()->get_inner_ring_radius(), + pdi_ptr->get_scanner_ptr()->get_num_detectors_per_ring()); + int n = sino.get_max_tangential_pos_num() - num_tang_pos_for_FOV; // total number of tang pos to trim + + squeezed_sino_all.erase(squeezed_sino_all.begin(),squeezed_sino_all.begin()+n/2+1); + squeezed_sino_all.erase(squeezed_sino_all.end()-n/2,squeezed_sino_all.end()); + + std::ofstream out_trimmed(output_filename+"_trimmed.txt"); + out_trimmed<<"Values of the squeezed sino for segment_num\n"; + std::ostream_iterator output_iterator_trimmed(out_trimmed, "\n"); + std::copy(squeezed_sino_all.begin(), squeezed_sino_all.end(), output_iterator_trimmed); + + + //find bin value at 7 mm from center according to NEMA NU4 + float length_of_arc_for_14mm = find_length_of_arc_for_FOV(14, //7 mm from center to both sides + pdi_ptr->get_scanner_ptr()->get_inner_ring_radius()); + + int idx_mid = squeezed_sino_all.size()/2; + float delta_unit = length_of_arc_for_14mm/2.2; + // delta_unit-1/2 is number of bins from each side of central bin + float fraction1 = delta_unit/2 - floor(delta_unit/2); + float fraction2 = ceil(delta_unit/2) - delta_unit/2; + + assert(idx_mid-floor(delta_unit/2)>=0 && idx_mid-floor(delta_unit/2)=0 && idx_mid+floor(delta_unit/2)=0 && idx_mid-ceil(delta_unit/2)=0 && idx_mid+ceil(delta_unit/2) +#include +#include "stir/ProjDataFromStream.h" +#include "stir/ProjDataInterfile.h" +#include "stir/ProjDataInfoCylindrical.h" +#include "stir/ProjDataInfoBlocksOnCylindrical.h" +#include "stir/ProjDataInfoGeneric.h" +#include "stir/Sinogram.h" +#include "stir/Bin.h" +#include "stir/round.h" +#include +#include + + +#ifndef STIR_NO_NAMESPACES +using std::string; +using std::cerr; +#endif + +USING_NAMESPACE_STIR + +int main(int argc, char **argv) +{ + int num_tang_poss_to_trim = 0; + if (argc>1 && strcmp(argv[1], "-t")==0) + { + num_tang_poss_to_trim = atoi(argv[2]); + argc -= 2; argv += 2; + } + if (argc > 5 || argc < 3 ) + { + cerr << "Usage:\n" + << argv[0] << " [-t num_tang_poss_to_trim] \\\n" + << "\toutput_filename input_projdata_name \\\n" + << "num_tang_poss_to_trim has to be smaller than the available number\n"; + exit(EXIT_FAILURE); + } + const string output_filename = argv[1]; + shared_ptr in_projdata_ptr = ProjData::read_from_file(argv[2]); + + + if (in_projdata_ptr->get_num_tangential_poss() <= + num_tang_poss_to_trim) + error("trim_projdata: too large number of tangential positions to trim (%d)\n", + num_tang_poss_to_trim); + + if (in_projdata_ptr->get_proj_data_info_sptr()->get_scanner_ptr()->get_scanner_geometry() == + "Cylindrical") + { + const ProjDataInfoCylindrical * const in_projdata_info_cyl_ptr = + dynamic_cast(in_projdata_ptr->get_proj_data_info_sptr()->clone()); + if (in_projdata_info_cyl_ptr== NULL) + { + error("error converting to cylindrical projection data\n"); + } + ProjDataInfoCylindrical * out_projdata_info_cyl_ptr = + dynamic_cast + (in_projdata_info_cyl_ptr->clone()); + + out_projdata_info_cyl_ptr-> + set_num_tangential_poss(in_projdata_info_cyl_ptr->get_num_tangential_poss() - + num_tang_poss_to_trim); + + shared_ptr out_projdata_info_ptr(out_projdata_info_cyl_ptr); + ProjDataInterfile out_projdata(in_projdata_ptr->get_exam_info_sptr(), + out_projdata_info_ptr, output_filename, std::ios::out); + + for (int seg = out_projdata.get_min_segment_num(); + seg <= out_projdata.get_max_segment_num(); + ++seg) + { + // keep sinograms out of the loop to avoid reallocations + // initialise to something because there's no default constructor + Sinogram out_sino = + out_projdata.get_empty_sinogram(out_projdata.get_min_axial_pos_num(seg),seg); + Sinogram in_sino = + in_projdata_ptr->get_empty_sinogram(in_projdata_ptr->get_min_axial_pos_num(seg),seg); + + for (int ax = out_projdata.get_min_axial_pos_num(seg); + ax <= out_projdata.get_max_axial_pos_num(seg); + ++ax ) + { + out_sino= out_projdata.get_empty_sinogram(ax, seg); + in_sino = in_projdata_ptr->get_sinogram(ax, seg); + + { + for (int view=out_projdata.get_min_view_num(); + view <= out_projdata.get_max_view_num(); + ++view) + for (int tang=out_projdata.get_min_tangential_pos_num(); + tang <= out_projdata.get_max_tangential_pos_num(); + ++tang) + out_sino[view][tang] = in_sino[view][tang]; + } + out_projdata.set_sinogram(out_sino); + } + + } + + } + else if (in_projdata_ptr->get_proj_data_info_sptr()->get_scanner_ptr()->get_scanner_geometry() == + "BlocksOnCylindrical") + { + const ProjDataInfoBlocksOnCylindrical * const in_projdata_info_blk_ptr = + dynamic_cast(in_projdata_ptr->get_proj_data_info_sptr()->clone()); + if (in_projdata_info_blk_ptr== NULL) + { + error("error converting to BlocksOnCylindrical projection data\n"); + } + ProjDataInfoBlocksOnCylindrical * out_projdata_info_blk_ptr = + dynamic_cast + (in_projdata_info_blk_ptr->clone()); + + out_projdata_info_blk_ptr-> + set_num_tangential_poss(in_projdata_info_blk_ptr->get_num_tangential_poss() - + num_tang_poss_to_trim); + + shared_ptr out_projdata_info_ptr(out_projdata_info_blk_ptr); + ProjDataInterfile out_projdata(in_projdata_ptr->get_exam_info_sptr(), + out_projdata_info_ptr, output_filename, std::ios::out); + + + for (int seg = out_projdata.get_min_segment_num(); + seg <= out_projdata.get_max_segment_num(); + ++seg) + { + // keep sinograms out of the loop to avoid reallocations + // initialise to something because there's no default constructor + Sinogram out_sino = + out_projdata.get_empty_sinogram(out_projdata.get_min_axial_pos_num(seg),seg); + Sinogram in_sino = + in_projdata_ptr->get_empty_sinogram(in_projdata_ptr->get_min_axial_pos_num(seg),seg); + + for (int ax = out_projdata.get_min_axial_pos_num(seg); + ax <= out_projdata.get_max_axial_pos_num(seg); + ++ax ) + { + out_sino= out_projdata.get_empty_sinogram(ax, seg); + in_sino = in_projdata_ptr->get_sinogram(ax, seg); + + { + for (int view=out_projdata.get_min_view_num(); + view <= out_projdata.get_max_view_num(); + ++view) + for (int tang=out_projdata.get_min_tangential_pos_num(); + tang <= out_projdata.get_max_tangential_pos_num(); + ++tang) + out_sino[view][tang] = in_sino[view][tang]; + } + out_projdata.set_sinogram(out_sino); + } + + } + + + } + else if (in_projdata_ptr->get_proj_data_info_sptr()->get_scanner_ptr()->get_scanner_geometry() == + "Generic") + { + const ProjDataInfoGeneric * const in_projdata_info_gen_ptr = + dynamic_cast(in_projdata_ptr->get_proj_data_info_sptr()->clone()); + if (in_projdata_info_gen_ptr== NULL) + { + error("error converting to Generic projection data\n"); + } + ProjDataInfoGeneric * out_projdata_info_gen_ptr = + dynamic_cast + (in_projdata_info_gen_ptr->clone()); + + out_projdata_info_gen_ptr-> + set_num_tangential_poss(in_projdata_info_gen_ptr->get_num_tangential_poss() - + num_tang_poss_to_trim); + + shared_ptr out_projdata_info_ptr(out_projdata_info_gen_ptr); + ProjDataInterfile out_projdata(in_projdata_ptr->get_exam_info_sptr(), + out_projdata_info_ptr, output_filename, std::ios::out); + + for (int seg = out_projdata.get_min_segment_num(); + seg <= out_projdata.get_max_segment_num(); + ++seg) + { + // keep sinograms out of the loop to avoid reallocations + // initialise to something because there's no default constructor + Sinogram out_sino = + out_projdata.get_empty_sinogram(out_projdata.get_min_axial_pos_num(seg),seg); + Sinogram in_sino = + in_projdata_ptr->get_empty_sinogram(in_projdata_ptr->get_min_axial_pos_num(seg),seg); + + for (int ax = out_projdata.get_min_axial_pos_num(seg); + ax <= out_projdata.get_max_axial_pos_num(seg); + ++ax ) + { + out_sino= out_projdata.get_empty_sinogram(ax, seg); + in_sino = in_projdata_ptr->get_sinogram(ax, seg); + + { + for (int view=out_projdata.get_min_view_num(); + view <= out_projdata.get_max_view_num(); + ++view) + for (int tang=out_projdata.get_min_tangential_pos_num(); + tang <= out_projdata.get_max_tangential_pos_num(); + ++tang) + out_sino[view][tang] = in_sino[view][tang]; + } + out_projdata.set_sinogram(out_sino); + } + + } + + } + else + { + error("error the scanner geometry of projection data is not known\n"); + } + + return EXIT_SUCCESS; +} diff --git a/src/utilities/write_sinogram_to_txt.cxx b/src/utilities/write_sinogram_to_txt.cxx new file mode 100644 index 000000000..e9579498b --- /dev/null +++ b/src/utilities/write_sinogram_to_txt.cxx @@ -0,0 +1,73 @@ +/* write_sinogram_to_txt.cxx + + Extracts sinogram to txt file + Jannis Fischer + jannis.fischer@cern.ch + + Copyright 2015 ETH Zurich, Institute of Particle Physics + + 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. +*/ + +#include "stir/ProjDataFromStream.h" +#include "stir/SegmentByView.h" +#include "stir/SegmentBySinogram.h" +#include "stir/Sinogram.h" +#include "stir/Viewgram.h" + +#include "stir/IO/interfile.h" +#include "stir/shared_ptr.h" + +#include +#include + +USING_NAMESPACE_STIR +using std::cerr; +using std::cout; +using std::endl; +using std::ofstream; +int main( int argc, char** argv ) +{ + if( argc < 4 ) + { + cerr << "Usage: " << argv[0] << " filename.hs axial_position segment_number" << endl; + return EXIT_FAILURE; + } + std::string filename(argv[1]); + + shared_ptr projdata_sptr = ProjData::read_from_file(filename.c_str()); + Sinogram sino = projdata_sptr->get_sinogram(atoi(argv[2]), atoi(argv[3]) ); + + std::string outfilename; + size_t lastdot = filename.find_last_of("."); + if( lastdot == std::string::npos ) outfilename = filename; + else outfilename = filename.substr(0,lastdot); + outfilename.append("_axialposition"); + outfilename.append(argv[2]); + outfilename.append("_segment"); + outfilename.append(argv[3]); + outfilename.append(".csv"); + + ofstream outfile(outfilename.c_str()); + outfile << "#\tsegment=" << sino.get_segment_num() << "\taxial_pos=" << sino.get_axial_pos_num() << endl; + + for( int view = sino.get_min_view_num(); view <= sino.get_max_view_num(); view++ ) { + for( int tpos = sino.get_min_tangential_pos_num(); tpos <= sino.get_max_tangential_pos_num(); tpos++ ){ + outfile << sino[view][tpos] << "\t"; + } + outfile << endl; + } + + outfile.close(); + return EXIT_SUCCESS; +} From a311276a4244156aecbf6635f5bd5bc48e56ed26 Mon Sep 17 00:00:00 2001 From: Parisa Khateri Date: Thu, 16 Jul 2020 17:36:16 +0200 Subject: [PATCH 14/81] small fix --- src/buildblock/Scanner.cxx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/buildblock/Scanner.cxx b/src/buildblock/Scanner.cxx index 31ff4bae4..c8bed03eb 100644 --- a/src/buildblock/Scanner.cxx +++ b/src/buildblock/Scanner.cxx @@ -1229,11 +1229,10 @@ Scanner* Scanner::ask_parameters() ask_string("Enter the scanner geometry ( BlocksOnCylindrical / Cylindrical / Generic ) :", "Cylindrical"); scanner_ptr->set_scanner_geometry(ScannerGeometry); - string CrystalMapFileName = ""; if (ScannerGeometry == "Generic") { - CrystalMapFileName = ask_string("Enter the name of the crystal map: ", ""); - scanner_ptr->set_crystal_map_file_name(CrystalMapFileName); + string CrystalMapFileName = ask_string("Enter the name of the crystal map: ", ""); + scanner_ptr->set_crystal_map_file_name(CrystalMapFileName); scanner_ptr->read_detectormap_from_file(CrystalMapFileName); } From 097977bb67f8f4e4075f0c68d9aa7c1cb6b5053b Mon Sep 17 00:00:00 2001 From: Parisa Khateri Date: Wed, 29 Jul 2020 08:44:52 +0200 Subject: [PATCH 15/81] fix a bug --- src/utilities/find_counts_in_cylindrical_ROI.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/find_counts_in_cylindrical_ROI.cxx b/src/utilities/find_counts_in_cylindrical_ROI.cxx index 296169a1e..7a39ded59 100644 --- a/src/utilities/find_counts_in_cylindrical_ROI.cxx +++ b/src/utilities/find_counts_in_cylindrical_ROI.cxx @@ -223,7 +223,7 @@ int main(int argc, char **argv) } } } - STD=sqrt(sum_for_std/total_count); + STD=sqrt(sum_for_std/num_voxels); std::cout<<"num_voxels total_count mean_count STD min_count max_count : " < Date: Thu, 5 Nov 2020 11:49:46 +0100 Subject: [PATCH 16/81] update test inputs according to the new version In the block/generic geometry new parameters have been added to the interfile header. --- recon_test_pack/run_test_simulate_and_recon.sh | 1 + recon_test_pack/run_test_simulate_and_recon_with_motion.sh | 1 + recon_test_pack/run_test_zoom_image.sh | 1 + 3 files changed, 3 insertions(+) diff --git a/recon_test_pack/run_test_simulate_and_recon.sh b/recon_test_pack/run_test_simulate_and_recon.sh index 8c5a60b8d..0dd5cdfb1 100755 --- a/recon_test_pack/run_test_simulate_and_recon.sh +++ b/recon_test_pack/run_test_simulate_and_recon.sh @@ -87,6 +87,7 @@ echo "=== create template sinogram (DSTE in 3D with max ring diff 2 to save tim template_sino=my_DSTE_3D_rd2_template.hs cat > my_input.txt < my_input.txt < my_input.txt < Date: Thu, 5 Nov 2020 13:41:39 +0100 Subject: [PATCH 17/81] fix a small bug --- src/include/stir/Scanner.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/include/stir/Scanner.h b/src/include/stir/Scanner.h index e825ecd88..23613fe31 100644 --- a/src/include/stir/Scanner.h +++ b/src/include/stir/Scanner.h @@ -127,7 +127,7 @@ class Scanner */ enum Type {E931, E951, E953, E921, E925, E961, E962, E966, E1080, Siemens_mMR,Siemens_mCT, RPT,HiDAC, Advance, DiscoveryLS, DiscoveryST, DiscoverySTE, DiscoveryRX, Discovery600, PETMR_Signa, Discovery690, DiscoveryMI3ring, DiscoveryMI4ring, - HZLR, RATPET, PANDA, HYPERimage, nanoPET, HRRT, Allegro, GeminiTF, , SAFIRDualRingPrototype, User_defined_scanner, + HZLR, RATPET, PANDA, HYPERimage, nanoPET, HRRT, Allegro, GeminiTF, SAFIRDualRingPrototype, User_defined_scanner, Unknown_scanner}; //! constructor that takes scanner type as an input argument From ad37c7d875e41e397c2ee1c8a734904aafd0a66b Mon Sep 17 00:00:00 2001 From: Parisa Khateri Date: Sun, 15 Nov 2020 11:55:06 +0100 Subject: [PATCH 18/81] fix a bug and update generate_input_data for PET_simulation example --- examples/PET_simulation/generate_input_data.sh | 1 + src/IO/InterfileHeader.cxx | 6 +++--- src/include/stir/Scanner.h | 8 ++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/PET_simulation/generate_input_data.sh b/examples/PET_simulation/generate_input_data.sh index aaf479030..877e05f4b 100755 --- a/examples/PET_simulation/generate_input_data.sh +++ b/examples/PET_simulation/generate_input_data.sh @@ -48,6 +48,7 @@ echo "=== create template sinogram (DSTE in 3D with max ring diff 1 to save tim template_sino=my_DSTE_3D_rd1_template.hs cat > my_input.txt < Date: Sun, 15 Nov 2020 14:43:30 +0100 Subject: [PATCH 19/81] added few fix suggested by Kris --- src/IO/interfile.cxx | 6 ++++-- src/include/stir/IO/InterfileHeader.h | 9 ++++++--- src/include/stir/Scanner.h | 2 +- src/include/stir/Scanner.inl | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/IO/interfile.cxx b/src/IO/interfile.cxx index 08cb71c1e..5a00d8370 100644 --- a/src/IO/interfile.cxx +++ b/src/IO/interfile.cxx @@ -1516,8 +1516,10 @@ write_basic_interfile_PDFS_header(const string& header_file_name, output_header << "effective central bin size (cm) := " << proj_data_info_sptr->get_sampling_in_s(Bin(0,0,0,0))/10. << endl; - } - } // end generic scanner + } // end generic scanner + + else error("write_basic_interfile_PDFS_header: Error casting the projdata to one of its geometries: Cylindrical/BlocksOnCylindrical/Genreic"); + } } diff --git a/src/include/stir/IO/InterfileHeader.h b/src/include/stir/IO/InterfileHeader.h index 3363ceb19..f7e091b16 100644 --- a/src/include/stir/IO/InterfileHeader.h +++ b/src/include/stir/IO/InterfileHeader.h @@ -291,17 +291,20 @@ class InterfilePDFSHeader : public InterfileHeader //! Reference energy. float reference_energy; - //! new variables for block geometry + //! \name new variables for block geometry + //@{ std::string scanner_orientation; std::string scanner_geometry; float axial_distance_between_crystals_in_cm; float transaxial_distance_between_crystals_in_cm; float axial_distance_between_blocks_in_cm; float transaxial_distance_between_blocks_in_cm; + //@} - //! new variables for generic geometry + //! \name new variables for generic geometry + //@{ std::string crystal_map; - + //@} // end scanner parameters double effective_central_bin_size_in_cm; diff --git a/src/include/stir/Scanner.h b/src/include/stir/Scanner.h index bd13e79b3..52570ca59 100644 --- a/src/include/stir/Scanner.h +++ b/src/include/stir/Scanner.h @@ -423,7 +423,7 @@ class Scanner // Get the STIR detection position (det#, ring#, layer#) given the detection position id in the input crystal map // used in CListRecordSAFIR.inl for accessing the coordinates - inline stir::DetectionPosition<> get_detpos_given_id(const stir::DetectionPosition<> det_pos) const; + inline stir::DetectionPosition<> get_detpos_given_id(const stir::DetectionPosition<> & det_pos) const; // Get the Cartesian coordinates (x,y,z) given the STIR detection position (det#, ring#, layer#) // used in ProjInfoDataGenericNoArcCorr.cxx for accessing the coordinates inline stir::CartesianCoordinate3D get_coords_given_detpos(const stir::DetectionPosition<> det_pos) const; diff --git a/src/include/stir/Scanner.inl b/src/include/stir/Scanner.inl index 06ccc216c..d528eb8c8 100644 --- a/src/include/stir/Scanner.inl +++ b/src/include/stir/Scanner.inl @@ -465,7 +465,7 @@ Scanner::get_transaxial_singles_unit(int singles_bin_index) const { // For retrieving the coordinates / detector, ring id from the scanner stir::DetectionPosition<> -Scanner::get_detpos_given_id(const stir::DetectionPosition<> det_pos) const{ +Scanner::get_detpos_given_id(const stir::DetectionPosition<> & det_pos) const{ if (crystal_map_file_name == ""){ stir::error("Crystal Map not defined!"); } From d4eb363529f5c88aad3402a816d95f528c500186 Mon Sep 17 00:00:00 2001 From: Parisa Khateri Date: Sun, 15 Nov 2020 16:03:02 +0100 Subject: [PATCH 20/81] update test --- recon_test_pack/simulate_PET_data_for_tests.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/recon_test_pack/simulate_PET_data_for_tests.sh b/recon_test_pack/simulate_PET_data_for_tests.sh index e1b965bd1..bb05afcd6 100755 --- a/recon_test_pack/simulate_PET_data_for_tests.sh +++ b/recon_test_pack/simulate_PET_data_for_tests.sh @@ -47,6 +47,7 @@ echo "=== create template sinogram (DSTE in 3D with max ring diff 2 to save tim template_sino=my_DSTE_3D_rd2_template.hs cat > my_input.txt < Date: Sun, 15 Nov 2020 19:33:25 +0100 Subject: [PATCH 21/81] fix a bug --- src/IO/InterfileHeader.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/IO/InterfileHeader.cxx b/src/IO/InterfileHeader.cxx index e07789a6a..46fe56b0a 100644 --- a/src/IO/InterfileHeader.cxx +++ b/src/IO/InterfileHeader.cxx @@ -635,7 +635,7 @@ InterfilePDFSHeader::InterfilePDFSHeader() add_key("distance between blocks in axial direction (cm)", &axial_distance_between_blocks_in_cm); - transaxial_distance_between_blocks_in_cm = -1; + transaxial_distance_between_blocks_in_cm = -0.1; add_key("distance between blocks in transaxial direction (cm)", &transaxial_distance_between_blocks_in_cm); // end of new keys for block geometry From 642438a3a286ae584e26e08fdf74d26667906b29 Mon Sep 17 00:00:00 2001 From: Parisa Khateri Date: Fri, 19 Feb 2021 14:54:03 +0100 Subject: [PATCH 22/81] fix a bug --- src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx b/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx index 8c91bd01c..221365f6a 100644 --- a/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx +++ b/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx @@ -450,7 +450,7 @@ find_cartesian_coordinates_given_scanner_coordinates(CartesianCoordinate3D Date: Mon, 22 Feb 2021 11:24:20 +0100 Subject: [PATCH 23/81] delete utilities discussed with Kris --- src/utilities/convert_projdata_types.cxx | 105 ------- .../find_counts_in_cylindrical_ROI.cxx | 231 ---------------- src/utilities/trim_projdata.cxx | 257 ------------------ 3 files changed, 593 deletions(-) delete mode 100644 src/utilities/convert_projdata_types.cxx delete mode 100644 src/utilities/find_counts_in_cylindrical_ROI.cxx delete mode 100644 src/utilities/trim_projdata.cxx diff --git a/src/utilities/convert_projdata_types.cxx b/src/utilities/convert_projdata_types.cxx deleted file mode 100644 index 15d0e7246..000000000 --- a/src/utilities/convert_projdata_types.cxx +++ /dev/null @@ -1,105 +0,0 @@ -/* -Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics - -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 utilities - - \brief This program takes a projection data from Cylindrical type and - converts it to either BlocksOnCylindrical or Generic type, and vice versa. - - \author Parisa Khateri - -*/ - -#include "stir/ProjData.h" -#include "stir/IO/interfile.h" -#include "stir/utilities.h" -#include "stir/Bin.h" - -#include -#include -#include "stir/ProjDataFromStream.h" -#include "stir/Viewgram.h" -#include "stir/IO/read_from_file.h" -#include "stir/SegmentByView.h" -#include "stir/ProjDataInterfile.h" -#include "stir/ProjDataInfo.h" -#include "stir/LORCoordinates.h" - -#include "stir/GeometryBlocksOnCylindrical.h" -#include "stir/DetectionPosition.h" -#include "stir/CartesianCoordinate3D.h" -#include "stir/listmode/DetectorCoordinateMapFromFile.h" -#include -#include "stir/CPUTimer.h" -#include "stir/shared_ptr.h" - - -#ifndef STIR_NO_NAMESPACES -using std::cerr; -#endif - -USING_NAMESPACE_STIR - - - -int main(int argc, char *argv[]) -{ - CPUTimer timer0; - - if(argc<4) - { - cerr<<"Usage: " << argv[0] << " output_filename input_filename template_projdata\n"; - exit(EXIT_FAILURE); - } - std::string output_filename=argv[1]; - shared_ptr in_pd_ptr = ProjData::read_from_file(argv[2]); - shared_ptr template_pd_ptr = ProjData::read_from_file(argv[3]); - - shared_ptr in_pdi_ptr(in_pd_ptr->get_proj_data_info_sptr()->clone()); - shared_ptr out_pdi_ptr(template_pd_ptr->get_proj_data_info_sptr()->clone()); - ProjDataInterfile out_proj_data(template_pd_ptr->get_exam_info_sptr(), out_pdi_ptr, output_filename+".hs"); - write_basic_interfile_PDFS_header(output_filename+".hs", out_proj_data); - - assert(in_pdi_ptr->get_min_segment_num()==-1*in_pdi_ptr->get_max_segment_num()); - for (int seg=in_pdi_ptr->get_min_segment_num(); seg<=in_pdi_ptr->get_max_segment_num();++seg) - { - std::cout<<"seg_num = "< viewgram_blk = out_proj_data.get_empty_viewgram(out_proj_data.get_min_view_num(),seg); - Viewgram viewgram_cyl = in_pd_ptr->get_empty_viewgram(in_pd_ptr->get_min_view_num(),seg); - - for(int view=in_pdi_ptr->get_min_view_num(); view<=in_pdi_ptr->get_max_view_num();++view) - { - viewgram_blk = out_proj_data.get_empty_viewgram(view,seg); - viewgram_cyl = in_pd_ptr->get_viewgram(view,seg); - - for(int ax=in_pdi_ptr->get_min_axial_pos_num(seg); ax<=in_pdi_ptr->get_max_axial_pos_num(seg);++ax) - { - for(int tang=in_pdi_ptr->get_min_tangential_pos_num(); tang<=in_pdi_ptr->get_max_tangential_pos_num(); ++tang) - { - viewgram_blk[ax][tang] = viewgram_cyl[ax][tang]; - } - } - if (!(out_proj_data.set_viewgram(viewgram_blk)== Succeeded::yes)) - warning("Error set_segment for projdata_symm %d\n", seg); - } - } - - return EXIT_SUCCESS; -} diff --git a/src/utilities/find_counts_in_cylindrical_ROI.cxx b/src/utilities/find_counts_in_cylindrical_ROI.cxx deleted file mode 100644 index 7a39ded59..000000000 --- a/src/utilities/find_counts_in_cylindrical_ROI.cxx +++ /dev/null @@ -1,231 +0,0 @@ -/* -Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics - -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 utilities - - \brief Find count statistics in a cylindrical ROI for image and projection data - - \author Parisa Khateri - -*/ -#include "stir/utilities.h" -#include -#include -#include -#include "stir/shared_ptr.h" -#include "stir/ProjData.h" -#include "stir/CartesianCoordinate3D.h" -#include "stir/LORCoordinates.h" -#include "stir/Bin.h" -#include "stir/ProjDataInterfile.h" -#include "stir/IO/interfile.h" -#include -#include "stir/Shape/EllipsoidalCylinder.h" -#include "stir/is_null_ptr.h" -#include "stir/IO/read_from_file.h" -#include "stir/DiscretisedDensity.h" -#include "stir/VoxelsOnCartesianGrid.h" - - -USING_NAMESPACE_STIR - -int main(int argc, char **argv) -{ - bool is_projdata; - CartesianCoordinate3D centre(0,0,0); - - if (argc==5 && !strcmp(argv[4],"s")) - { - is_projdata = 1; - } - else if (argc==5 && !strcmp(argv[4],"v")) - { - is_projdata = 0; - } - else if (argc==8 && !strcmp(argv[4],"v")) - { - is_projdata = 0; - centre.z() = atof(argv[5]); - centre.y() = atof(argv[6]); - centre.x() = atof(argv[7]); - } - else - { - std::cerr<<"\nError: wrong arguments\n" - <<"Usage: "<< argv[0] - <<" input_file cylinder_radius(mm) cylinder_hight(mm) s/v [z(mm) y(mm) x(mm)]\n" - <<"use either s if the input is a projection data or v if the input is an image\n" - <<"option: if the input is an image, the center of ROI can be deterimined." - <<"Note that the center of Cartesian Coordinate is the center of scanner which is the default.\n" - <<"warning: input order matters\n\n"; - return EXIT_FAILURE; - } - - const double R = atof(argv[2]); // cylinder radius - const double h = atof(argv[3]); // cylinder hight - if (R<=0 || h<=0) - { - std::cerr <<"\nError: Radius and hight must be positive and larger than zero\n" - <<"Usage: "<< argv[0] - <<" input_file cylinder_radius(mm) cylinder_hight(mm) [p]\n" - <<"use option p if the input is projection data\n" - <<"warning: input order matters\n\n"; - return EXIT_FAILURE; - } - - if (is_projdata) //if the input is a projection data - { - shared_ptr projdata_ptr = ProjData::read_from_file(argv[1]); - if (is_null_ptr(projdata_ptr)) - { - std::cerr << "Could not read input file\n"; exit(EXIT_FAILURE); - } - - CartesianCoordinate3D c1, c2; - LORInAxialAndNoArcCorrSinogramCoordinates lor; - - double total_count=0; - double min_count=std::numeric_limits::max(); // minimum number of counts per LOR - double max_count=std::numeric_limits::min(); // minimum number of counts per LOR - double mean_count=0; //average number of counts per LOR in the active region - int num_active_LORs=0; //number of LORs which pass through the cylinder - for (int seg =projdata_ptr->get_min_segment_num(); seg <=projdata_ptr->get_max_segment_num(); ++seg) - for (int view =projdata_ptr->get_min_view_num(); view <=projdata_ptr->get_max_view_num(); ++view) - { - Viewgram cylinder_viewgram = projdata_ptr->get_viewgram(view, seg); - for (int ax =projdata_ptr->get_min_axial_pos_num(seg); ax <=projdata_ptr->get_max_axial_pos_num(seg); ++ax) - for (int tang =projdata_ptr->get_min_tangential_pos_num(); tang <=projdata_ptr->get_max_tangential_pos_num(); ++tang) - { - Bin bin(seg, view, ax, tang); - projdata_ptr->get_proj_data_info_sptr()->get_LOR(lor, bin); - LORAs2Points lor_as2points(lor); - LORAs2Points intersection_coords; - if (find_LOR_intersections_with_cylinder(intersection_coords, lor_as2points, R) ==Succeeded::yes) - { //this only succeeds if LOR is intersecting with the infinitely long cylinder - c1 = intersection_coords.p1(); - c2 = intersection_coords.p2(); - if (!( (c1.z()<-h/2. && c2.z()<-h/2.) || (c1.z()>h/2. && c2.z()>h/2.) )) - { - double N_lor = cylinder_viewgram[ax][tang]; //counts seen by this lor - float c12 = sqrt( pow(c1.z()-c2.z(), 2) // length of intersection of lor with the cylinder - + pow(c1.y()-c2.y(), 2) - + pow(c1.x()-c2.x(), 2) ); - if (c12>0.5) // if LOR intersection is lager than 0.5 mm, check the count per LOR - { - total_count+=N_lor; - num_active_LORs+=1; - if (N_lormax_count) max_count=N_lor; - } - } - } - } - } - mean_count=total_count/num_active_LORs; - std::cout<<"num_lor total_count mean_count min_count max_count : " - < > density_ptr - = read_from_file >(argv[1]); - const VoxelsOnCartesianGrid * image_ptr = - dynamic_cast *>(density_ptr.get()); - CartesianCoordinate3D voxel_size = image_ptr->get_voxel_size(); - - if (is_null_ptr(density_ptr)) - { - std::cerr << "Could not read input file\n"; exit(EXIT_FAILURE); - } - - EllipsoidalCylinder cylinder_shape(h,R,R,centre); - - double total_count=0; - double min_count=std::numeric_limits::max(); // minimum number of counts per LOR - double max_count=std::numeric_limits::min(); // minimum number of counts per LOR - double mean_count=0; //average number of counts per LOR in the active region - double STD=0; //standard deviation of counts per LOR in the active region - int num_voxels=0; - DiscretisedDensity<3,float>& image = *density_ptr; - const int min_k_index = image.get_min_index(); - const int max_k_index = image.get_max_index(); - for ( int k = min_k_index; k<= max_k_index; ++k) - { - const int min_j_index = image[k].get_min_index(); - const int max_j_index = image[k].get_max_index(); - for ( int j = min_j_index; j<= max_j_index; ++j) - { - const int min_i_index = image[k][j].get_min_index(); - const int max_i_index = image[k][j].get_max_index(); - for ( int i = min_i_index; i<= max_i_index; ++i) - { - /* - [min_i_index,max_i_index]=[-num_voxel_x/2,num_voxel_x/2] - [min_j_index,max_j_index]=[-num_voxel_y/2,num_voxel_y/2] - [min_k_index,max_k_index]=[0,num_voxel_z-1] - */ - - CartesianCoordinate3D voxel((k-max_k_index/2)*voxel_size.z(), - j*voxel_size.y(), - i*voxel_size.x()); - if (cylinder_shape.is_inside_shape(voxel)) - { - total_count+=image[k][j][i]; - num_voxels++; - if (image[k][j][i]max_count) max_count=image[k][j][i]; - } - } - } - } - mean_count=total_count/num_voxels; - double sum_for_std=0; - for ( int k = min_k_index; k<= max_k_index; ++k) - { - const int min_j_index = image[k].get_min_index(); - const int max_j_index = image[k].get_max_index(); - for ( int j = min_j_index; j<= max_j_index; ++j) - { - const int min_i_index = image[k][j].get_min_index(); - const int max_i_index = image[k][j].get_max_index(); - for ( int i = min_i_index; i<= max_i_index; ++i) - { - /* - [min_i_index,max_i_index]=[-num_voxel_x/2,num_voxel_x/2] - [min_j_index,max_j_index]=[-num_voxel_y/2,num_voxel_y/2] - [min_k_index,max_k_index]=[0,num_voxel_z-1] - */ - - CartesianCoordinate3D voxel((k-max_k_index/2)*voxel_size.z(), - j*voxel_size.y(), - i*voxel_size.x()); - if (cylinder_shape.is_inside_shape(voxel)) - { - sum_for_std+=pow((image[k][j][i] - mean_count),2); - } - } - } - } - STD=sqrt(sum_for_std/num_voxels); - std::cout<<"num_voxels total_count mean_count STD min_count max_count : " - < -#include -#include "stir/ProjDataFromStream.h" -#include "stir/ProjDataInterfile.h" -#include "stir/ProjDataInfoCylindrical.h" -#include "stir/ProjDataInfoBlocksOnCylindrical.h" -#include "stir/ProjDataInfoGeneric.h" -#include "stir/Sinogram.h" -#include "stir/Bin.h" -#include "stir/round.h" -#include -#include - - -#ifndef STIR_NO_NAMESPACES -using std::string; -using std::cerr; -#endif - -USING_NAMESPACE_STIR - -int main(int argc, char **argv) -{ - int num_tang_poss_to_trim = 0; - if (argc>1 && strcmp(argv[1], "-t")==0) - { - num_tang_poss_to_trim = atoi(argv[2]); - argc -= 2; argv += 2; - } - if (argc > 5 || argc < 3 ) - { - cerr << "Usage:\n" - << argv[0] << " [-t num_tang_poss_to_trim] \\\n" - << "\toutput_filename input_projdata_name \\\n" - << "num_tang_poss_to_trim has to be smaller than the available number\n"; - exit(EXIT_FAILURE); - } - const string output_filename = argv[1]; - shared_ptr in_projdata_ptr = ProjData::read_from_file(argv[2]); - - - if (in_projdata_ptr->get_num_tangential_poss() <= - num_tang_poss_to_trim) - error("trim_projdata: too large number of tangential positions to trim (%d)\n", - num_tang_poss_to_trim); - - if (in_projdata_ptr->get_proj_data_info_sptr()->get_scanner_ptr()->get_scanner_geometry() == - "Cylindrical") - { - const ProjDataInfoCylindrical * const in_projdata_info_cyl_ptr = - dynamic_cast(in_projdata_ptr->get_proj_data_info_sptr()->clone()); - if (in_projdata_info_cyl_ptr== NULL) - { - error("error converting to cylindrical projection data\n"); - } - ProjDataInfoCylindrical * out_projdata_info_cyl_ptr = - dynamic_cast - (in_projdata_info_cyl_ptr->clone()); - - out_projdata_info_cyl_ptr-> - set_num_tangential_poss(in_projdata_info_cyl_ptr->get_num_tangential_poss() - - num_tang_poss_to_trim); - - shared_ptr out_projdata_info_ptr(out_projdata_info_cyl_ptr); - ProjDataInterfile out_projdata(in_projdata_ptr->get_exam_info_sptr(), - out_projdata_info_ptr, output_filename, std::ios::out); - - for (int seg = out_projdata.get_min_segment_num(); - seg <= out_projdata.get_max_segment_num(); - ++seg) - { - // keep sinograms out of the loop to avoid reallocations - // initialise to something because there's no default constructor - Sinogram out_sino = - out_projdata.get_empty_sinogram(out_projdata.get_min_axial_pos_num(seg),seg); - Sinogram in_sino = - in_projdata_ptr->get_empty_sinogram(in_projdata_ptr->get_min_axial_pos_num(seg),seg); - - for (int ax = out_projdata.get_min_axial_pos_num(seg); - ax <= out_projdata.get_max_axial_pos_num(seg); - ++ax ) - { - out_sino= out_projdata.get_empty_sinogram(ax, seg); - in_sino = in_projdata_ptr->get_sinogram(ax, seg); - - { - for (int view=out_projdata.get_min_view_num(); - view <= out_projdata.get_max_view_num(); - ++view) - for (int tang=out_projdata.get_min_tangential_pos_num(); - tang <= out_projdata.get_max_tangential_pos_num(); - ++tang) - out_sino[view][tang] = in_sino[view][tang]; - } - out_projdata.set_sinogram(out_sino); - } - - } - - } - else if (in_projdata_ptr->get_proj_data_info_sptr()->get_scanner_ptr()->get_scanner_geometry() == - "BlocksOnCylindrical") - { - const ProjDataInfoBlocksOnCylindrical * const in_projdata_info_blk_ptr = - dynamic_cast(in_projdata_ptr->get_proj_data_info_sptr()->clone()); - if (in_projdata_info_blk_ptr== NULL) - { - error("error converting to BlocksOnCylindrical projection data\n"); - } - ProjDataInfoBlocksOnCylindrical * out_projdata_info_blk_ptr = - dynamic_cast - (in_projdata_info_blk_ptr->clone()); - - out_projdata_info_blk_ptr-> - set_num_tangential_poss(in_projdata_info_blk_ptr->get_num_tangential_poss() - - num_tang_poss_to_trim); - - shared_ptr out_projdata_info_ptr(out_projdata_info_blk_ptr); - ProjDataInterfile out_projdata(in_projdata_ptr->get_exam_info_sptr(), - out_projdata_info_ptr, output_filename, std::ios::out); - - - for (int seg = out_projdata.get_min_segment_num(); - seg <= out_projdata.get_max_segment_num(); - ++seg) - { - // keep sinograms out of the loop to avoid reallocations - // initialise to something because there's no default constructor - Sinogram out_sino = - out_projdata.get_empty_sinogram(out_projdata.get_min_axial_pos_num(seg),seg); - Sinogram in_sino = - in_projdata_ptr->get_empty_sinogram(in_projdata_ptr->get_min_axial_pos_num(seg),seg); - - for (int ax = out_projdata.get_min_axial_pos_num(seg); - ax <= out_projdata.get_max_axial_pos_num(seg); - ++ax ) - { - out_sino= out_projdata.get_empty_sinogram(ax, seg); - in_sino = in_projdata_ptr->get_sinogram(ax, seg); - - { - for (int view=out_projdata.get_min_view_num(); - view <= out_projdata.get_max_view_num(); - ++view) - for (int tang=out_projdata.get_min_tangential_pos_num(); - tang <= out_projdata.get_max_tangential_pos_num(); - ++tang) - out_sino[view][tang] = in_sino[view][tang]; - } - out_projdata.set_sinogram(out_sino); - } - - } - - - } - else if (in_projdata_ptr->get_proj_data_info_sptr()->get_scanner_ptr()->get_scanner_geometry() == - "Generic") - { - const ProjDataInfoGeneric * const in_projdata_info_gen_ptr = - dynamic_cast(in_projdata_ptr->get_proj_data_info_sptr()->clone()); - if (in_projdata_info_gen_ptr== NULL) - { - error("error converting to Generic projection data\n"); - } - ProjDataInfoGeneric * out_projdata_info_gen_ptr = - dynamic_cast - (in_projdata_info_gen_ptr->clone()); - - out_projdata_info_gen_ptr-> - set_num_tangential_poss(in_projdata_info_gen_ptr->get_num_tangential_poss() - - num_tang_poss_to_trim); - - shared_ptr out_projdata_info_ptr(out_projdata_info_gen_ptr); - ProjDataInterfile out_projdata(in_projdata_ptr->get_exam_info_sptr(), - out_projdata_info_ptr, output_filename, std::ios::out); - - for (int seg = out_projdata.get_min_segment_num(); - seg <= out_projdata.get_max_segment_num(); - ++seg) - { - // keep sinograms out of the loop to avoid reallocations - // initialise to something because there's no default constructor - Sinogram out_sino = - out_projdata.get_empty_sinogram(out_projdata.get_min_axial_pos_num(seg),seg); - Sinogram in_sino = - in_projdata_ptr->get_empty_sinogram(in_projdata_ptr->get_min_axial_pos_num(seg),seg); - - for (int ax = out_projdata.get_min_axial_pos_num(seg); - ax <= out_projdata.get_max_axial_pos_num(seg); - ++ax ) - { - out_sino= out_projdata.get_empty_sinogram(ax, seg); - in_sino = in_projdata_ptr->get_sinogram(ax, seg); - - { - for (int view=out_projdata.get_min_view_num(); - view <= out_projdata.get_max_view_num(); - ++view) - for (int tang=out_projdata.get_min_tangential_pos_num(); - tang <= out_projdata.get_max_tangential_pos_num(); - ++tang) - out_sino[view][tang] = in_sino[view][tang]; - } - out_projdata.set_sinogram(out_sino); - } - - } - - } - else - { - error("error the scanner geometry of projection data is not known\n"); - } - - return EXIT_SUCCESS; -} From 2c5d2898d0345ba424b1a662ba202d9d3375c7c5 Mon Sep 17 00:00:00 2001 From: Parisa Khateri Date: Mon, 22 Feb 2021 11:35:26 +0100 Subject: [PATCH 24/81] remove utilities name from cmakelist file --- src/utilities/CMakeLists.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/utilities/CMakeLists.txt b/src/utilities/CMakeLists.txt index 6a1e03f95..54e8e7273 100644 --- a/src/utilities/CMakeLists.txt +++ b/src/utilities/CMakeLists.txt @@ -76,12 +76,9 @@ set(${dir_EXE_SOURCES} find_ML_singles_from_delayed find_normfactors_from_cylinder_data find_recovery_coefficients_in_image_quality_phantom_nema_nu4 - trim_projdata - convert_projdata_types write_sinogram_to_txt find_sum_projection_of_viewgram_and_sinogram separate_true_from_random_scatter_for_necr - find_counts_in_cylindrical_ROI ) if (AVW_FOUND) From 9edf75e83baf86af34c272ca1b119fc4cac19086 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Thu, 18 Mar 2021 17:02:21 +0000 Subject: [PATCH 25/81] make ProjDataInfoBlocksOnCylindricalNoArcCorr::get_s safer It was not guaranteed that get_LOR() and get_s() use the same radius, potentially leading to problems. (I did not check if they were the same or not, but now it's guaranteed) --- src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl index bc894c707..adab1cec9 100644 --- a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl +++ b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl @@ -90,8 +90,8 @@ get_s(const Bin& bin) const LORInAxialAndNoArcCorrSinogramCoordinates lor; get_LOR(lor, bin); if (bin.view_num()==0 && lor.phi()>0.1) - return -1*ring_radius * sin(lor.beta()); - return ring_radius * sin(lor.beta()); + return -1*lor.radius() * sin(lor.beta()); + return lor.radius() * sin(lor.beta()); } float From b0e4f4a2959503b6d9ba0cd06d071248fa3a9adc Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Tue, 14 Sep 2021 09:20:35 +0100 Subject: [PATCH 26/81] bux fixed for test_DataSymmetriesForBins_PET_CartesianGrid --- .../DataSymmetriesForBins_PET_CartesianGrid.inl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl b/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl index 6bdaac53d..145a58bf0 100644 --- a/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl +++ b/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl @@ -103,7 +103,7 @@ find_transform_z( } } //block implementaion - if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="BlocksOnCylindrical") + else if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry()=="BlocksOnCylindrical") { const ProjDataInfoBlocksOnCylindrical* proj_data_info_blk_ptr = static_cast(proj_data_info_ptr.get()); From 72929c46ddbd7b94e85583c0dc7193bdfcf10820 Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Wed, 3 Nov 2021 16:28:59 +0000 Subject: [PATCH 27/81] BlockOnCylindrical: 1) crystal_maps z_start is now 0; 2) added recon_test/test_blocks_on_cylindrical_projectors, with test on symmetries; 3) for blocks, get_lor() now uses a STIR existing function instead of the one created by Parisa; 4) added tests to test_proj_data_info:run_Blocks_DOI_test(), run_lor_get_s_test(), run_coordinate_test(), run_coordinate_test_for_realistic_scanner(). --- .../GeometryBlocksOnCylindrical.cxx | 5 +- .../ProjDataInfoBlocksOnCylindrical.cxx | 3 +- src/recon_test/CMakeLists.txt | 1 + .../test_blocks_on_cylindrical_projectors.cxx | 430 +++++++++++++ src/test/test_proj_data_info.cxx | 566 ++++++++++++++++++ 5 files changed, 1000 insertions(+), 5 deletions(-) create mode 100644 src/recon_test/test_blocks_on_cylindrical_projectors.cxx diff --git a/src/buildblock/GeometryBlocksOnCylindrical.cxx b/src/buildblock/GeometryBlocksOnCylindrical.cxx index 8ceda4181..c7a4edf42 100644 --- a/src/buildblock/GeometryBlocksOnCylindrical.cxx +++ b/src/buildblock/GeometryBlocksOnCylindrical.cxx @@ -161,10 +161,7 @@ build_crystal_maps() see start_x*/ //calculate start_point to build the map. - float start_z = -1*( - ((num_axial_blocks-1)/2.)*axial_block_spacing - + ((num_axial_crystals_per_block-1)/2.)*axial_crystal_spacing - ); + float start_z = 0; float start_y = -1*get_scanner_ptr()->get_effective_ring_radius(); float start_x = -1*( ((num_transaxial_blocks_per_bucket-1)/2.)*transaxial_block_spacing diff --git a/src/buildblock/ProjDataInfoBlocksOnCylindrical.cxx b/src/buildblock/ProjDataInfoBlocksOnCylindrical.cxx index 60740ffb0..4ba923e40 100644 --- a/src/buildblock/ProjDataInfoBlocksOnCylindrical.cxx +++ b/src/buildblock/ProjDataInfoBlocksOnCylindrical.cxx @@ -500,7 +500,8 @@ get_LOR(LORInAxialAndNoArcCorrSinogramCoordinates& lor, LORAs2Points lor_as_2_points(_p1, _p2); const double R = get_ring_radius(); - lor_as_2_points.change_representation_for_block(lor, R); + + lor_as_2_points.change_representation(lor, R); } string diff --git a/src/recon_test/CMakeLists.txt b/src/recon_test/CMakeLists.txt index 0e7ff7bc2..442a094d4 100644 --- a/src/recon_test/CMakeLists.txt +++ b/src/recon_test/CMakeLists.txt @@ -27,6 +27,7 @@ set(${dir_SIMPLE_TEST_EXE_SOURCES} test_FBP2D test_FBP3DRP test_priors + test_blocks_on_cylindrical_projectors ) diff --git a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx new file mode 100644 index 000000000..bf02277ba --- /dev/null +++ b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx @@ -0,0 +1,430 @@ +/*! + + \file + \ingroup recontest + + \brief Test program for back projection and forward projection using stir::ProjDataInfoBlockOnCylindrical + + \author Daniel Deidda + +*/ +/* Copyright (C) 2021, National Physical Laboratory + This file is part of STIR. + + This file is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + This file is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + See STIR/LICENSE.txt for details +*/ + +#include "stir/info.h" +#include "stir/ProjDataInMemory.h" +#include "stir/DiscretisedDensity.h" +#include "stir/ProjDataInterfile.h" +#include "stir/recon_buildblock/ProjMatrixElemsForOneBin.h" +#include "stir/recon_buildblock/ProjMatrixByBinUsingRayTracing.h" +#include "stir/ExamInfo.h" +#include "stir/LORCoordinates.h" +#include "stir/ProjDataInfo.h" +#include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" +#include "stir/ProjDataInfoCylindricalNoArcCorr.h" +#include "stir/recon_buildblock/ProjMatrixByBinUsingRayTracing.h" +#include "stir/Sinogram.h" +#include "stir/Viewgram.h" +#include "stir/Succeeded.h" +#include "stir/RunTests.h" +#include "stir/Scanner.h" +#include "stir/copy_fill.h" +#include "stir/IndexRange3D.h" +#include "stir/CPUTimer.h" +#include "stir/Shape/Shape3DWithOrientation.h" +#include "stir/Shape/Ellipsoid.h" +#include "stir/Shape/Box3D.h" +#include "stir/VoxelsOnCartesianGrid.h" +#include "stir/recon_buildblock/ForwardProjectorByBin.h" +#include "stir/recon_buildblock/ForwardProjectorByBinUsingProjMatrixByBin.h" +#include "stir/IO/write_to_file.h" +//#include "stir/Shape/Shape3D.h" + +START_NAMESPACE_STIR + + +/*! + \ingroup test + \brief Test class for Blocks +*/ +class BlocksTests: public RunTests +{ +public: + void run_tests(); +private: + void run_symmetry_test(); + void run_plane_symmetry_test(); +}; + +/*! The following is a test for symmetries: a simulated image is created with a plane at known angles, + * the forward projected sinogram should show the makimum value at the bin corresponding to the angl phi + * equal to the orientation of the plane +*/ +void +BlocksTests::run_plane_symmetry_test(){ + + CartesianCoordinate3D origin (0,0,0); + CartesianCoordinate3D grid_spacing (1.1,2.2,2.2); + float phi1; + float phi2; + const IndexRange<3> + range(Coordinate3D(0,-45,-44), + Coordinate3D(24,44,45)); + VoxelsOnCartesianGrid image(range,origin, grid_spacing); + +// 60 degrees + phi1= 60*_PI/180; + const Array<2,float> direction_vectors= + make_array(make_1d_array(1.F,0.F,0.F), + make_1d_array(0.F,cos(float(_PI)-phi1),sin(float(_PI)-phi1)), + make_1d_array(0.F,-sin(float(_PI)-phi1),cos(float(_PI)-phi1))); + + Ellipsoid + plane(CartesianCoordinate3D(/*edge_z*/25*grid_spacing.z(), + /*edge_y*/91*grid_spacing.y(), + /*edge_x*/5*grid_spacing.x()), + /*centre*/CartesianCoordinate3D((image.get_min_index()+image.get_max_index())/2*grid_spacing.z(), + 0*grid_spacing.y(), + 0), + direction_vectors); + + plane.construct_volume(image, make_coordinate(3,3,3)); + + +// rotate by 30 degrees + phi2=30*_PI/180; + VoxelsOnCartesianGrid image2=image; + const Array<2,float> direction2=make_array(make_1d_array(1.F,0.F,0.F), + make_1d_array(0.F,cos(float(_PI)-phi2),sin(float(_PI)-phi2)), + make_1d_array(0.F,-sin(float(_PI)-phi2),cos(float(_PI)-phi2))); + plane.set_direction_vectors(direction2); + + plane.construct_volume(image2, make_coordinate(3,3,3)); + +// create projadata info + + shared_ptr scannerBlocks_ptr; + scannerBlocks_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + scannerBlocks_ptr->set_num_axial_crystals_per_block(1); + scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing()* + scannerBlocks_ptr->get_num_axial_crystals_per_block()); + scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing()* + scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); +// scannerBlocks_ptr->set_num_transaxial_crystals_per_block(1); + scannerBlocks_ptr->set_num_axial_blocks_per_bucket(1); +// scannerBlocks_ptr->set_num_transaxial_blocks_per_bucket(1); + scannerBlocks_ptr->set_num_rings(1); + + scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); + + + VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + + for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ + min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + if (iget_num_rings()) + num_axial_pos_per_segment[i]=i+1; + else + num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; + } + + shared_ptr proj_data_info_blocks_ptr; + proj_data_info_blocks_ptr.reset( + new ProjDataInfoBlocksOnCylindricalNoArcCorr( + scannerBlocks_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + scannerBlocks_ptr->get_max_num_views(), + scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); +// now forward-project image + + shared_ptr forw_projector_sptr,forw_projector2_sptr; + shared_ptr projdata, projdata2; + shared_ptr > image_sptr(image.clone()); + write_to_file("plane60",*image_sptr); + + shared_ptr > image2_sptr(image2.clone()); + write_to_file("plane30",*image2_sptr); + + shared_ptr PM(new ProjMatrixByBinUsingRayTracing()); + forw_projector_sptr.reset(new ForwardProjectorByBinUsingProjMatrixByBin(PM)); + info(boost::format("Test blocks on Cylindrical: Forward projector used: %1%") % forw_projector_sptr->parameter_info()); + + forw_projector_sptr->set_up(proj_data_info_blocks_ptr, + image_sptr); + + + forw_projector2_sptr.reset(new ForwardProjectorByBinUsingProjMatrixByBin(PM)); + forw_projector2_sptr->set_up(proj_data_info_blocks_ptr, + image2_sptr); + + //-- ExamInfo + shared_ptr exam_info_sptr(new ExamInfo()); + exam_info_sptr->imaging_modality = ImagingModality::PT; + + projdata.reset(new ProjDataInterfile(exam_info_sptr, + proj_data_info_blocks_ptr, + "sino1_from_plane.hs", + std::ios::out | std::ios::trunc | std::ios::in)); + + + forw_projector_sptr->forward_project(*projdata, *image_sptr); + + + projdata2.reset(new ProjDataInterfile(exam_info_sptr, + proj_data_info_blocks_ptr, + "sino2_from_plane.hs", + std::ios::out | std::ios::trunc | std::ios::in)); + + + forw_projector2_sptr->forward_project(*projdata2, *image2_sptr); + + int view1_num = 0, view2_num = 0; + LORInAxialAndNoArcCorrSinogramCoordinates lorB; + for(int i=0;iget_max_view_num();i++){ + Bin bin(0,i,0,0); + proj_data_info_blocks_ptr->get_LOR(lorB,bin); + if(abs(lorB.phi()-phi1)/phi1<=1E-2){ + view1_num=i; + break; + } + } + + for(int i=0;iget_max_view_num();i++){ + Bin bin(0,i,0,0); + proj_data_info_blocks_ptr->get_LOR(lorB,bin); + if(abs(lorB.phi()-phi2)/phi2<=1E-2){ + view2_num=i; + break; + } + } + + float max1 =projdata->get_sinogram(0,0).find_max(); + float max2 =projdata2->get_sinogram(0,0).find_max(); + +// find the tang position with the max value + int tang1_num=0,tang2_num=0; + for(int tang=projdata->get_min_tangential_pos_num();tangget_max_tangential_pos_num();tang++){ + + if((max1-projdata->get_sinogram(0,0).at(view1_num).at(tang))/max1<1E-3) { + tang1_num=tang; + break; + } + } + + for(int tang=projdata2->get_min_tangential_pos_num();tangget_max_tangential_pos_num();tang++){ + + if((max2-projdata2->get_sinogram(0,0).at(view2_num).at(tang))/max2<1E-3) { + tang1_num=tang; + break; + } + } + + float bin1=projdata->get_sinogram(0,0).at(view1_num).at(tang1_num); + float bin2=projdata2->get_sinogram(0,0).at(view2_num).at(tang2_num); + set_tolerance(10E-3); + check_if_equal(bin1, max1,"the value seen in the block at 30 degrees should be the same as the max value of the sinogram"); + check_if_equal(bin2, max2,"the value seen in the block at 60 degrees should be the same as the max value of the sinogram"); +} + +/*! The following is a test for symmetries: a simulated image is created with spherical source in front of each detector block, + * the forward projected sinogram should show the same bin values in simmetric places (in this test a dodecagone scanner is + * used so we have symmetry every 30 degrees. The above is repeated for an image with sources in front of the dodecagone corners. + * the sinogram should have now different values at fixed bin compared the previous image +*/ +void +BlocksTests::run_symmetry_test(){ + + CartesianCoordinate3D origin (0,0,0); + CartesianCoordinate3D grid_spacing (1.1,2.2,2.2); + float theta1=0;float theta2=0; + + const IndexRange<3> + range(Coordinate3D(0,-45,-44), + Coordinate3D(24,44,45)); + VoxelsOnCartesianGrid image(range,origin, grid_spacing); + + const Array<2,float> direction_vectors= + make_array(make_1d_array(1.F,0.F,0.F), + make_1d_array(0.F,cos(theta1),sin(theta1)), + make_1d_array(0.F,-sin(theta1),cos(theta1))); + + Ellipsoid + ellipsoid(CartesianCoordinate3D(/*radius_z*/6*grid_spacing.z(), + /*radius_y*/6*grid_spacing.y(), + /*radius_x*/6*grid_spacing.x()), + /*centre*/CartesianCoordinate3D((image.get_min_index()+image.get_max_index())/2*grid_spacing.z(), + -34*grid_spacing.y(), + 0), + direction_vectors); + + ellipsoid.construct_volume(image, make_coordinate(3,3,3)); + + VoxelsOnCartesianGrid image1=image; + VoxelsOnCartesianGrid image22=image; +// rotate by 30 degrees, this scanner is a dodecagon and there is a 30 degrees angle between consecutive blocks + for(int i=30; i<360; i+=30){ + theta1=i*_PI/180; + + CartesianCoordinate3D origin1 ((image.get_min_index()+image.get_max_index())/2*grid_spacing.z(), + -34*grid_spacing.y()*cos(theta1), + 34*grid_spacing.y()*sin(theta1)); + + ellipsoid.set_origin(origin1); + ellipsoid.construct_volume(image1, make_coordinate(3,3,3)); + image+=image1; +} + shared_ptr > image1_sptr(image.clone()); + write_to_file("image_for",*image1_sptr); + + + image=*image.get_empty_copy(); + for(int i=15; i<360; i+=30){ + theta2=i*_PI/180; + + CartesianCoordinate3D origin2 ((image.get_min_index()+image.get_max_index())/2*grid_spacing.z(), + -34*grid_spacing.y()*cos(theta2), + 34*grid_spacing.y()*sin(theta2)); + + ellipsoid.set_origin(origin2); + ellipsoid.construct_volume(image22, make_coordinate(3,3,3)); + image+=image22; +} + +shared_ptr > image2_sptr(image.clone()); +write_to_file("image_for2",*image2_sptr); + + + +// create projadata info + shared_ptr scannerBlocks_ptr; + scannerBlocks_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + scannerBlocks_ptr->set_num_axial_crystals_per_block(1); + scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing()* + scannerBlocks_ptr->get_num_axial_crystals_per_block()); + scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing()* + scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); +// scannerBlocks_ptr->set_num_transaxial_crystals_per_block(1); + scannerBlocks_ptr->set_num_axial_blocks_per_bucket(1); +// scannerBlocks_ptr->set_num_transaxial_blocks_per_bucket(1); + scannerBlocks_ptr->set_num_rings(1); + + scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); + + + VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + + for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ + min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + if (iget_num_rings()) + num_axial_pos_per_segment[i]=i+1; + else + num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; + } + + shared_ptr proj_data_info_blocks_ptr; + proj_data_info_blocks_ptr.reset( + new ProjDataInfoBlocksOnCylindricalNoArcCorr( + scannerBlocks_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + scannerBlocks_ptr->get_max_num_views(), + scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); + +// now forward-project images + + shared_ptr forw_projector1_sptr, forw_projector2_sptr; + shared_ptr projdata1, projdata2; + shared_ptr PM(new ProjMatrixByBinUsingRayTracing()); + forw_projector1_sptr.reset(new ForwardProjectorByBinUsingProjMatrixByBin(PM)); + info(boost::format("Test blocks on Cylindrical: Forward projector used: %1%") % forw_projector1_sptr->parameter_info()); + + forw_projector1_sptr->set_up(proj_data_info_blocks_ptr, + image1_sptr); + + + forw_projector2_sptr.reset(new ForwardProjectorByBinUsingProjMatrixByBin(PM)); + forw_projector2_sptr->set_up(proj_data_info_blocks_ptr, + image2_sptr); + + //-- ExamInfo + shared_ptr exam_info_sptr(new ExamInfo()); + exam_info_sptr->imaging_modality = ImagingModality::PT; + + projdata1.reset(new ProjDataInterfile(exam_info_sptr, + proj_data_info_blocks_ptr, + "sino1_from_image.hs",std::ios::out | std::ios::trunc | std::ios::in)); + + projdata2.reset(new ProjDataInterfile(exam_info_sptr, + proj_data_info_blocks_ptr, + "sino2_from_image.hs",std::ios::out | std::ios::trunc | std::ios::in)); + + forw_projector1_sptr->forward_project(*projdata1, *image1_sptr); + forw_projector2_sptr->forward_project(*projdata2, *image2_sptr); + int crystals_in_ring=scannerBlocks_ptr->get_num_detectors_per_ring(); + float bin1_0=projdata1->get_sinogram(0,0).at(0/crystals_in_ring*_PI).at(0); + float bin1_90=projdata1->get_sinogram(0,0).at(90/crystals_in_ring*_PI).at(0); + float bin1_30=projdata1->get_sinogram(0,0).at(30/crystals_in_ring*_PI).at(0); + float bin1_60=projdata1->get_sinogram(0,0).at(60/crystals_in_ring*_PI).at(0); + float bin1_150=projdata1->get_sinogram(0,0).at(150/crystals_in_ring*_PI).at(0); + +// values of the asymetric image + float bin2_0=projdata2->get_sinogram(0,0).at(0/crystals_in_ring*_PI).at(0); + float bin2_90=projdata2->get_sinogram(0,0).at(90/crystals_in_ring*_PI).at(0); + float bin2_30=projdata2->get_sinogram(0,0).at(30/crystals_in_ring*_PI).at(0); + float bin2_60=projdata2->get_sinogram(0,0).at(60/crystals_in_ring*_PI).at(0); + float bin2_150=projdata2->get_sinogram(0,0).at(150/crystals_in_ring*_PI).at(0); + + set_tolerance(10E-3); + check_if_equal(bin1_0, bin1_90,"the value seen in the block 0 should be the same as the one at angle 90"); + check_if_equal(bin1_30, bin1_150,"the value seen in the block at angle 30 should be the same as the one at angle 150 "); + check_if_equal(bin1_30, bin1_60,"the value seen in the block at angle 30 should be the same as the one at angle 60"); + + + check(bin1_0!= bin2_0,"the two data have different symmetries, the values should be different"); + check(bin1_30!= bin2_30,"the two data have different symmetries, the values should be different "); + check(bin1_60!= bin2_60,"the two data have different symmetries, the values should be different"); + check(bin1_90!= bin2_90,"the two data have different symmetries, the values should be different"); + check(bin1_30!= bin2_30,"the two data have different symmetries, the values should be different"); + check(bin1_150!= bin2_150,"the two data have different symmetries, the values should be different"); +} + +void +BlocksTests:: +run_tests() +{ + + std::cerr << "-------- Testing Blocks Geometry --------\n"; + run_symmetry_test(); + run_plane_symmetry_test(); +} +END_NAMESPACE_STIR + + +USING_NAMESPACE_STIR + +int main() +{ + BlocksTests tests; + tests.run_tests(); + return tests.main_return_value(); +} diff --git a/src/test/test_proj_data_info.cxx b/src/test/test_proj_data_info.cxx index 9288ba60e..13ace03e0 100644 --- a/src/test/test_proj_data_info.cxx +++ b/src/test/test_proj_data_info.cxx @@ -10,6 +10,7 @@ \author Sanida Mustafovic \author Kris Thielemans \author Palak Wadhwa + \author Daniel Deidda \author PARAPET project */ @@ -18,6 +19,7 @@ Copyright (C) 2000- 2011, Hammersmith Imanet Ltd Copyright (C) 2018, 2021, University College London Copyright (C) 2018, University of Leeds + Copyright (C) 2021, National Physical Laboratory This file is part of STIR. This file is free software; you can redistribute it and/or modify @@ -35,6 +37,10 @@ #include "stir/ProjDataInfoCylindricalArcCorr.h" #include "stir/ProjDataInfoCylindricalNoArcCorr.h" +#include "stir/LORCoordinates.h" +#include "stir/ProjDataInfo.h" +#include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" +#include "stir/ProjDataInfoCylindricalNoArcCorr.h" #include "stir/RunTests.h" #include "stir/Scanner.h" #include "stir/Bin.h" @@ -45,6 +51,7 @@ #include #include #include +#include "stir/CPUTimer.h" #ifndef STIR_NO_NAMESPACES using std::cerr; @@ -100,6 +107,11 @@ class ProjDataInfoTests: public RunTests { protected: void test_generic_proj_data_info(ProjDataInfo& proj_data_info); + + void run_coordinate_test(); + void run_coordinate_test_for_realistic_scanner(); + void run_Blocks_DOI_test(); + void run_lor_get_s_test(); }; void @@ -434,6 +446,550 @@ test_cylindrical_proj_data_info(ProjDataInfoCylindrical& proj_data_info) test_generic_proj_data_info(proj_data_info); } +/*! + The following tests that detection position is affected by the value of DOI +*/ +void +ProjDataInfoTests::run_Blocks_DOI_test() +{ + CPUTimer timer; + shared_ptr scannerBlocks_ptr, scannerBlocksDOI_ptr; + scannerBlocks_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + scannerBlocks_ptr->set_average_depth_of_interaction(0); + scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); + + VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + + for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ + min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + if (iget_num_rings()) + num_axial_pos_per_segment[i]=i+1; + else + num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; + } + + shared_ptr proj_data_info_blocks_doi0_ptr; + proj_data_info_blocks_doi0_ptr.reset( + new ProjDataInfoBlocksOnCylindricalNoArcCorr( + scannerBlocks_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + scannerBlocks_ptr->get_max_num_views(), + scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); + + scannerBlocksDOI_ptr=scannerBlocks_ptr; + scannerBlocksDOI_ptr->set_average_depth_of_interaction(0.1); + shared_ptr proj_data_info_blocks_doi5_ptr; + proj_data_info_blocks_doi5_ptr.reset( + new ProjDataInfoBlocksOnCylindricalNoArcCorr( + scannerBlocksDOI_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + scannerBlocksDOI_ptr->get_max_num_views(), + scannerBlocksDOI_ptr->get_max_num_non_arccorrected_bins())); + + Bin bin; + LORInAxialAndNoArcCorrSinogramCoordinates lor; + + int Bring1, Bring2, Bdet1,Bdet2, BDring1, BDring2, BDdet1, BDdet2; + CartesianCoordinate3D< float> b1,b2,bd1,bd2; + float doi=scannerBlocksDOI_ptr->get_average_depth_of_interaction(); +// timer.reset(); timer.start(); + + for (int seg =proj_data_info_blocks_doi0_ptr->get_min_segment_num(); seg <=proj_data_info_blocks_doi0_ptr->get_max_segment_num(); ++ seg) + for (int ax =proj_data_info_blocks_doi0_ptr->get_min_axial_pos_num(seg); ax <=proj_data_info_blocks_doi0_ptr->get_max_axial_pos_num(seg); ++ax) + for (int view = 0; view <=proj_data_info_blocks_doi0_ptr->get_max_view_num(); view++) + for (int tang =proj_data_info_blocks_doi0_ptr->get_min_tangential_pos_num(); tang <=proj_data_info_blocks_doi0_ptr->get_max_tangential_pos_num(); ++tang) + { + bin.segment_num() = seg; + bin.axial_pos_num() = ax; + bin.view_num() = view; + bin.tangential_pos_num() = tang; + + +// check det_pos instead + proj_data_info_blocks_doi0_ptr->get_det_pair_for_bin(Bdet1, Bring1, Bdet2,Bring2,bin); + proj_data_info_blocks_doi5_ptr->get_det_pair_for_bin(BDdet1, BDring1, BDdet2,BDring2,bin); + + + proj_data_info_blocks_doi0_ptr->get_LOR(lor,bin); + set_tolerance(10E-4); + + check_if_equal(Bdet1,BDdet1, ""); + check_if_equal(Bdet2,BDdet2, ""); + check_if_equal(Bring1,BDring1, ""); + check_if_equal(Bring2,BDring2, ""); + +// checkcartesian coordinates of detectors + proj_data_info_blocks_doi0_ptr->find_cartesian_coordinates_of_detection(b1,b2,bin); + proj_data_info_blocks_doi5_ptr->find_cartesian_coordinates_of_detection(bd1,bd2,bin); + +// set_tolerance(10E-2); + check(b1!=bd1, "detector position should be different with different DOIs"); + check(b2!=bd2, "detector position should be different with different DOIs"); + } + timer.stop(); std::cerr<< "-- CPU Time " << timer.value() << '\n'; + +} + +/*! + The following tests the consistency of coordinates obtained with a cilindrical scanner + and those of a blocks on cylindrical scanner. For this test, a scanner with 1 ring and + 1 detector per block is used. + + In this function, an extra test is performed to check that a roundtrip + transformation: detector_ID->cartesian_coord_detection_pos->detector_ID + provide the same as the starting point +*/ +void +ProjDataInfoTests::run_coordinate_test() +{ + CPUTimer timer; + shared_ptr scannerBlocks_ptr; + scannerBlocks_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + scannerBlocks_ptr->set_num_axial_crystals_per_block(1); + scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing()* + scannerBlocks_ptr->get_num_axial_crystals_per_block()); + scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing()* + scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); + scannerBlocks_ptr->set_num_transaxial_crystals_per_block(1); + scannerBlocks_ptr->set_num_axial_blocks_per_bucket(1); + scannerBlocks_ptr->set_num_transaxial_blocks_per_bucket(1); + scannerBlocks_ptr->set_num_rings(1); + + scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); + + shared_ptr scannerCyl_ptr; + scannerCyl_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + scannerCyl_ptr->set_num_axial_crystals_per_block(1); + scannerCyl_ptr->set_axial_block_spacing(scannerCyl_ptr->get_axial_crystal_spacing()* + scannerCyl_ptr->get_num_axial_crystals_per_block()); + scannerCyl_ptr->set_transaxial_block_spacing(scannerCyl_ptr->get_transaxial_crystal_spacing()* + scannerCyl_ptr->get_num_transaxial_crystals_per_block()); + scannerCyl_ptr->set_num_transaxial_crystals_per_block(1); + scannerCyl_ptr->set_num_axial_blocks_per_bucket(1); + scannerCyl_ptr->set_num_transaxial_blocks_per_bucket(1); + + scannerCyl_ptr->set_num_rings(1); + scannerCyl_ptr->set_scanner_geometry("Cylindrical"); + + VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + + for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ + min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + if (iget_num_rings()) + num_axial_pos_per_segment[i]=i+1; + else + num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; + } + + shared_ptr proj_data_info_blocks_ptr; + proj_data_info_blocks_ptr.reset( + new ProjDataInfoBlocksOnCylindricalNoArcCorr( + scannerBlocks_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + scannerBlocks_ptr->get_max_num_views(), + scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); + + shared_ptr proj_data_info_cyl_ptr; + proj_data_info_cyl_ptr.reset( + new ProjDataInfoCylindricalNoArcCorr( + scannerCyl_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + scannerBlocks_ptr->get_max_num_views(), + scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); + + Bin bin, binRT; + + int Bring1, Bring2, Bdet1,Bdet2, Cring1, Cring2, Cdet1, Cdet2; + int RTring1, RTring2, RTdet1,RTdet2; + CartesianCoordinate3D< float> b1,b2,c1,c2,roundt1, roundt2; +// timer.reset(); timer.start(); + LORInAxialAndNoArcCorrSinogramCoordinates lorB; + LORInAxialAndNoArcCorrSinogramCoordinates lorC; + + LORInAxialAndNoArcCorrSinogramCoordinates lorC1, lorCn; + + for (int seg = proj_data_info_blocks_ptr->get_min_segment_num(); seg <= proj_data_info_blocks_ptr->get_max_segment_num(); ++ seg) + for (int ax = proj_data_info_blocks_ptr->get_min_axial_pos_num(seg); ax <= proj_data_info_blocks_ptr->get_max_axial_pos_num(seg); ++ax) + for (int view = 0; view <= proj_data_info_blocks_ptr->get_max_view_num(); view++) + for (int tang = proj_data_info_blocks_ptr->get_min_tangential_pos_num(); tang <= proj_data_info_blocks_ptr->get_max_tangential_pos_num(); ++tang) + { + bin.segment_num() = seg; + bin.axial_pos_num() = ax; + bin.view_num() = view; + bin.tangential_pos_num() = tang; + + proj_data_info_cyl_ptr->get_LOR(lorC,bin); + proj_data_info_blocks_ptr->get_LOR(lorB,bin); + + const int num_detectors = + proj_data_info_cyl_ptr->get_scanner_ptr()->get_num_detectors_per_ring(); + + int det_num1=0, det_num2=0; + proj_data_info_cyl_ptr-> + get_det_num_pair_for_view_tangential_pos_num(det_num1, + det_num2, + bin.view_num(), + bin.tangential_pos_num()); + + float phi; + phi = static_cast( + (det_num1+det_num2)*_PI/num_detectors-_PI/2 + proj_data_info_cyl_ptr->get_azimuthal_angle_offset() ); + + lorC1 = LORInAxialAndNoArcCorrSinogramCoordinates(lorC.z1(), + lorC.z2(), + phi,//lorC.phi(), + lorC.beta(), + proj_data_info_cyl_ptr->get_ring_radius()); + + const float old_phi=proj_data_info_cyl_ptr->get_phi(bin); + if (fabs(phi-old_phi)>=2*_PI/num_detectors){ + float ang=2*_PI/num_detectors/2; +// warning("view %d old_phi %g new_phi %g\n",bin.view_num(), old_phi, phi); + + lorC1 = LORInAxialAndNoArcCorrSinogramCoordinates(lorC.z2(), + lorC.z1(), + phi,//lorC.phi(), + -lorC.beta(), + proj_data_info_cyl_ptr->get_ring_radius()); + } +// check det_pos instead + proj_data_info_blocks_ptr->get_det_pair_for_bin(Bdet1, Bring1, Bdet2,Bring2,bin); + proj_data_info_cyl_ptr->get_det_pair_for_bin(Cdet1, Cring1, Cdet2,Cring2,bin); + + set_tolerance(10E-4); + + check_if_equal(Bdet1,Cdet1, ""); + check_if_equal(Bdet2,Cdet2, ""); + check_if_equal(Bring1,Cring1, ""); + check_if_equal(Bring2,Cring2, ""); + +// test round trip from detector ID to coordinates and from cordinates to detecto IDs + proj_data_info_blocks_ptr->find_cartesian_coordinates_given_scanner_coordinates(roundt1, roundt2,Bring1, Bring2, Bdet1, Bdet2); + proj_data_info_blocks_ptr->find_bin_given_cartesian_coordinates_of_detection(binRT,roundt1, roundt2); + proj_data_info_blocks_ptr->get_det_pair_for_bin(RTdet1, RTring1, RTdet2,RTring2,bin); + + check_if_equal(Bdet1,RTdet1, "Roundtrip from detector A ID to coordinates and from cordinates to detector A ID"); + check_if_equal(Bdet2,RTdet2, "Roundtrip from detector B ID to coordinates and from cordinates to detector B ID"); + check_if_equal(Bring1,RTring1, "Roundtrip from ring A ID to coordinates and from cordinates to ring A ID"); + check_if_equal(Bring2,RTring2, "Roundtrip from ring B ID to coordinates and from cordinates to ring B ID"); + + +// checkcartesian coordinates of detectors + proj_data_info_cyl_ptr->find_cartesian_coordinates_of_detection(c1,c2,bin); + proj_data_info_blocks_ptr->find_cartesian_coordinates_of_detection(b1,b2,bin); + + check_if_equal(b1,c1, ""); + check_if_equal(b2,c2, ""); + + set_tolerance(10E-3); + + if (abs(lorB.phi() - lorC1.phi()) < tolerance ) + { + + check_if_equal(proj_data_info_blocks_ptr->get_s(bin),proj_data_info_cyl_ptr->get_s(bin), "A get_s() from projdata Cylinder is different from Block on Cylindrical"); + + check_if_equal(lorB.s(),lorC1.s(),"tang_pos="+ std::to_string(tang)+ + " PHI-C="+ std::to_string(lorC1.phi())+ + " PHI-B="+ std::to_string(lorB.phi())+ + " view="+ std::to_string(view)+ + " Atest if BlocksOnCylindrical LOR.s is the same as the LOR produced by Cylindrical");//) + + check_if_equal(lorB.beta(),lorC1.beta(),"tang_pos="+ std::to_string(tang)+ + " ax_pos="+ std::to_string(ax)+ + " segment="+ std::to_string(seg)+ + " view="+ std::to_string(view)+" test if BlocksOnCylindrical LOR.beta is the same as the LOR produced by Cylindrical"); + check_if_equal(lorB.z1(),lorC1.z1(),"tang_pos="+ std::to_string(tang)+ + " ax_pos="+ std::to_string(ax)+ + " segment="+ std::to_string(seg)+ + " view="+ std::to_string(view)+" test if BlocksOnCylindrical LOR.z1 is the same as the LOR produced by Cylindrical"); + check_if_equal(lorB.z2(),lorC1.z2(),"tang_pos="+ std::to_string(tang)+ + " ax_pos="+ std::to_string(ax)+ + " segment="+ std::to_string(seg)+ + " view="+ std::to_string(view)+" test if BlocksOnCylindrical LOR.z2 is the same as the LOR produced by Cylindrical"); + +// TODO: fix problem with interleaving when calculating Phi + } + else if (abs(lorB.phi() - lorC1.phi())+_PI < tolerance || abs(lorB.phi() - lorC1.phi())-_PI < tolerance){ + + + check_if_equal(proj_data_info_blocks_ptr->get_s(bin),proj_data_info_cyl_ptr->get_s(bin), "B get_s() from projdata Cylinder is different from Block on Cylindrical"); + check_if_equal(proj_data_info_blocks_ptr->get_phi(bin), phi-_PI, "B get_phi() from projdata Cylinder is different from Block on Cylindrical"); + + check_if_equal(lorB.s(),-lorC1.s(),"tang_pos="+ std::to_string(tang)+ + " PHYC="+ std::to_string(lorC1.phi())+ + " PHIB="+ std::to_string(lorB.phi())+ + " view="+ std::to_string(view)+ + " Btest if BlocksOnCylindrical LOR.s is the same as the LOR produced by Cylindrical");//) + check_if_equal(lorB.beta(),-lorC1.beta()," test if BlocksOnCylindrical LOR.beta is the same as the LOR produced by Cylindrical"); + check_if_equal(lorB.z1(),lorC1.z1()," test if BlocksOnCylindrical LOR.z1 is the same as the LOR produced by Cylindrical"); + check_if_equal(lorB.z2(),lorC1.z2()," test if BlocksOnCylindrical LOR.z2 is the same as the LOR produced by Cylindrical"); + + } + else{ + check(false, "phi is different"); + } + } + timer.stop(); std::cerr<< "-- CPU Time " << timer.value() << '\n'; + +} + +/*! + The following test is similar to the above but for a scanner that has multiple rings and + multiple detectors per block. +*/ +void +ProjDataInfoTests::run_coordinate_test_for_realistic_scanner() +{ + CPUTimer timer; + shared_ptr scannerBlocks_ptr; + scannerBlocks_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing()* + scannerBlocks_ptr->get_num_axial_crystals_per_block()); + scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing()* + scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); + + scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); + + shared_ptr scannerCyl_ptr; + scannerCyl_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + scannerCyl_ptr->set_axial_block_spacing(scannerCyl_ptr->get_axial_crystal_spacing()* + scannerCyl_ptr->get_num_axial_crystals_per_block()); + scannerCyl_ptr->set_transaxial_block_spacing(scannerCyl_ptr->get_transaxial_crystal_spacing()* + scannerCyl_ptr->get_num_transaxial_crystals_per_block()); + + scannerCyl_ptr->set_scanner_geometry("Cylindrical"); + + VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + + for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ + min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + if (iget_num_rings()) + num_axial_pos_per_segment[i]=i+1; + else + num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; + } + + shared_ptr proj_data_info_blocks_ptr; + proj_data_info_blocks_ptr.reset( + new ProjDataInfoBlocksOnCylindricalNoArcCorr( + scannerBlocks_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + scannerBlocks_ptr->get_max_num_views(), + scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); + + shared_ptr proj_data_info_cyl_ptr; + proj_data_info_cyl_ptr.reset( + new ProjDataInfoCylindricalNoArcCorr( + scannerCyl_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + scannerBlocks_ptr->get_max_num_views(), + scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); + + Bin bin; + + int Bring1, Bring2, Bdet1,Bdet2, Cring1, Cring2, Cdet1, Cdet2; + CartesianCoordinate3D< float> b1,b2,c1,c2; +// timer.reset(); timer.start(); + LORInAxialAndNoArcCorrSinogramCoordinates lorB; + LORInAxialAndNoArcCorrSinogramCoordinates lorC; + + LORInAxialAndNoArcCorrSinogramCoordinates lorC1, lorCn; + + for (int seg = proj_data_info_blocks_ptr->get_min_segment_num(); seg <= proj_data_info_blocks_ptr->get_max_segment_num(); ++ seg) + for (int ax = proj_data_info_blocks_ptr->get_min_axial_pos_num(seg); ax <= proj_data_info_blocks_ptr->get_max_axial_pos_num(seg); ++ax) + for (int view = 0; view <= proj_data_info_blocks_ptr->get_max_view_num(); view++) + for (int tang = proj_data_info_blocks_ptr->get_min_tangential_pos_num(); tang <= proj_data_info_blocks_ptr->get_max_tangential_pos_num(); ++tang) + { + bin.segment_num() = seg; + bin.axial_pos_num() = ax; + bin.view_num() = view; + bin.tangential_pos_num() = tang; + + proj_data_info_cyl_ptr->get_LOR(lorC,bin); + proj_data_info_blocks_ptr->get_LOR(lorB,bin); + + const int num_detectors = + proj_data_info_cyl_ptr->get_scanner_ptr()->get_num_detectors_per_ring(); + + int det_num1=0, det_num2=0; + proj_data_info_cyl_ptr-> + get_det_num_pair_for_view_tangential_pos_num(det_num1, + det_num2, + bin.view_num(), + bin.tangential_pos_num()); + + +// check det_pos instead + proj_data_info_blocks_ptr->get_det_pair_for_bin(Bdet1, Bring1, Bdet2,Bring2,bin); + proj_data_info_cyl_ptr->get_det_pair_for_bin(Cdet1, Cring1, Cdet2,Cring2,bin); + + set_tolerance(10E-4); + + check_if_equal(Bdet1,Cdet1, ""); + check_if_equal(Bdet2,Cdet2, ""); + check_if_equal(Bring1,Cring1, ""); + check_if_equal(Bring2,Cring2, ""); + +// check cartesian coordinates of detectors + proj_data_info_cyl_ptr->find_cartesian_coordinates_of_detection(c1,c2,bin); + proj_data_info_blocks_ptr->find_cartesian_coordinates_of_detection(b1,b2,bin); + + // we expect to be differences of the order of the mm in x and y due to the difference in geometry + set_tolerance(10E-1); + + check_if_equal(b1.z(),c1.z(), " "); + check_if_equal(b2.z(),c2.z(), " "); + check_if_equal(b1.y(),c1.y(), " "); + check_if_equal(b2.y(),c2.y(), " "); + check_if_equal(b1.x(),c1.x(), " "); + check_if_equal(b2.x(),c2.x(), " "); + + } + timer.stop(); std::cerr<< "-- CPU Time " << timer.value() << '\n'; + +} + +/*! + The following tests the function get_s() for the BlockOnCylindrical case. the first test + checks that all lines passing for the center provide s=0. The second test check that + parallel lines are always at the same angle phi, and that the step between consecutive + lines is the same and equal to the one calculate geometrically. +*/ +void +ProjDataInfoTests:: +run_lor_get_s_test(){ + CPUTimer timer; + shared_ptr scannerBlocks_ptr; + scannerBlocks_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing()* + scannerBlocks_ptr->get_num_axial_crystals_per_block()); + scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing()* + scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); + + scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); + + shared_ptr scannerCyl_ptr; + scannerCyl_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); + scannerCyl_ptr->set_axial_block_spacing(scannerCyl_ptr->get_axial_crystal_spacing()* + scannerCyl_ptr->get_num_axial_crystals_per_block()); + scannerCyl_ptr->set_transaxial_block_spacing(scannerCyl_ptr->get_transaxial_crystal_spacing()* + scannerCyl_ptr->get_num_transaxial_crystals_per_block()); + + scannerCyl_ptr->set_scanner_geometry("Cylindrical"); + + VectorWithOffset num_axial_pos_per_segment(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset min_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + VectorWithOffset max_ring_diff_v(scannerBlocks_ptr->get_num_rings()*2-1); + + for (int i=0; i<2*scannerBlocks_ptr->get_num_rings()-1; i++){ + min_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + max_ring_diff_v[i]=-scannerBlocks_ptr->get_num_rings()+1+i; + if (iget_num_rings()) + num_axial_pos_per_segment[i]=i+1; + else + num_axial_pos_per_segment[i]=2*scannerBlocks_ptr->get_num_rings()-i-1; + } + + shared_ptr proj_data_info_blocks_ptr; + proj_data_info_blocks_ptr.reset( + new ProjDataInfoBlocksOnCylindricalNoArcCorr( + scannerBlocks_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + scannerBlocks_ptr->get_max_num_views(), + scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); + + shared_ptr proj_data_info_cyl_ptr; + proj_data_info_cyl_ptr.reset( + new ProjDataInfoCylindricalNoArcCorr( + scannerCyl_ptr, + num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, + scannerBlocks_ptr->get_max_num_views(), + scannerBlocks_ptr->get_max_num_non_arccorrected_bins())); +// select detection position 1 + + LORInAxialAndNoArcCorrSinogramCoordinates lorB; + LORInAxialAndNoArcCorrSinogramCoordinates lorC; + int Cring1, Cring2, Cdet1, Cdet2; + Bin bin; +// Det<> pos1(0,0,0); + set_tolerance(10E-4); + for (int i=0; iget_num_detectors_per_ring();i++){ + + Cring1=0;Cdet1=i; + Cring2=0;Cdet2=scannerCyl_ptr->get_num_detectors_per_ring()/2+Cdet1; + if(Cdet2>=scannerCyl_ptr->get_num_detectors_per_ring()) + Cdet2=Cdet1-scannerCyl_ptr->get_num_detectors_per_ring()/2; + + proj_data_info_cyl_ptr->get_bin_for_det_pair(bin, Cdet1,Cring1,Cdet2,Cring2); + proj_data_info_cyl_ptr->get_LOR(lorC,bin); + + proj_data_info_blocks_ptr->get_bin_for_det_pair(bin, Cdet1,Cring1,Cdet2,Cring2); + proj_data_info_blocks_ptr->get_LOR(lorB,bin); + + check_if_equal(0., lorC.s(),std::to_string(i)+ " Cylinder get_s() should be zero when the LOR passes at the center of the scanner"); + check_if_equal(0., lorB.s(),std::to_string(i)+ " Blocks get_s() should be zero when the LOR passes at the center of the scanner"); + } + +// Check get_s() (for BlocksOnCylindrical) when the line is at a given angle. We consider two blocks at a relative angle of 90 degrees +// the angle covered by the detectors is 120 (each block is 30 degrees and the detector are 4 blocks apart). The following LOR will be +// obtained by increasing detID1 and decreasing detID2 so that they are always parallel. +// + +// Let's calculate the relative ID difference between block1 and block2 in coincidence +// num_det_per_ring:2PI=det_id_diff:PI/2 + int det_id_diff=scannerBlocks_ptr->get_num_detectors_per_ring()*(_PI/2)/(2*_PI); + int Ctb=scannerCyl_ptr->get_num_transaxial_crystals_per_block(); + float crystal_trans_spacing=scannerBlocks_ptr->get_transaxial_crystal_spacing(); + float block_trans_spacing=scannerBlocks_ptr->get_transaxial_block_spacing(); + float prev_s=0; + float prev_phi=0; + for (int i=0; iget_num_transaxial_crystals_per_block();i++){ + + Cring1=0;Cdet1=i+2*Ctb-Ctb/2; + Cring2=0;Cdet2=2*Ctb-Ctb/2+det_id_diff+Ctb-1 -i; + + proj_data_info_blocks_ptr->get_bin_for_det_pair(bin, Cdet1,Cring1,Cdet2,Cring1); + proj_data_info_blocks_ptr->get_LOR(lorB,bin); + float R=block_trans_spacing*(sin(_PI*5/12)+sin(_PI/4)+sin(_PI/12)); + float s=R*cos(_PI/3)+ + crystal_trans_spacing/2*sin(_PI/4)+ + (i)*crystal_trans_spacing*sin(_PI/4); + + float s_step=crystal_trans_spacing*sin(_PI/4); + +// the following fails at the moment +// check_if_equal(s, lorB.s(),std::to_string(i)+ " Blocks get_s() is different"); +// the first value we expect to be different + if (i>0){ + check_if_equal(s_step, lorB.s()-prev_s,std::to_string(i)+ " Blocks get_s() the step is different");//+ +// std::to_string(crystal_trans_spacing*sin(_PI/4)) + " lorB.s() the step is "+std::to_string(lorB.s()-prev_s)+ +// + "phi step is "+std::to_string(lorB.phi()-prev_phi)); + check_if_equal(0.F, lorB.phi()-prev_phi, " Blocks get_phi() should be always the same as we are considering parallel LORs"); + } + prev_s=lorB.s(); + prev_phi=lorB.phi(); + + } +} + + /*! \ingroup test \brief Test class for ProjDataInfoCylindricalArcCorr @@ -450,6 +1006,16 @@ void ProjDataInfoCylindricalArcCorrTests::run_tests() { + + std::cerr << "-------- Testing ProjData Geometry --------\n"; + + std::cerr << "-------- Testing DOI for blocks --------\n"; + run_Blocks_DOI_test(); + std::cerr << "-------- Testing coordinates --------\n"; + run_lor_get_s_test(); + run_coordinate_test(); + run_coordinate_test_for_realistic_scanner(); + cerr << "-------- Testing ProjDataInfoCylindricalArcCorr --------\n"; { // Test on the empty constructor From 7cea90ca52c3aaf89613ca6e1f4a39c284ab77da Mon Sep 17 00:00:00 2001 From: VietAnhDao Date: Tue, 9 Nov 2021 21:13:57 +0000 Subject: [PATCH 28/81] BlocksOnCylindrical updates - get_s and get_tantheta are derived from end-points. - scatter simulation accept BlocksOnCylindrical. - remove X orientation. squashed commit of https://github.com/pkhateri/STIR/pull/2 Co-authored-by: Kris Thielemans --- .../GeometryBlocksOnCylindrical.cxx | 211 +++++++----------- src/buildblock/SSRB.cxx | 10 +- .../stir/ProjDataInfoBlocksOnCylindrical.inl | 15 +- ...ojDataInfoBlocksOnCylindricalNoArcCorr.inl | 42 +++- src/include/stir/scatter/ScatterSimulation.h | 7 +- .../ProjMatrixByBinUsingRayTracing.cxx | 8 +- src/scatter_buildblock/ScatterSimulation.cxx | 36 ++- .../scatter_detection_modelling.cxx | 20 +- src/test/test_ScatterSimulation.cxx | 6 +- 9 files changed, 177 insertions(+), 178 deletions(-) diff --git a/src/buildblock/GeometryBlocksOnCylindrical.cxx b/src/buildblock/GeometryBlocksOnCylindrical.cxx index c7a4edf42..f4f62508e 100644 --- a/src/buildblock/GeometryBlocksOnCylindrical.cxx +++ b/src/buildblock/GeometryBlocksOnCylindrical.cxx @@ -131,9 +131,18 @@ find_detection_position_given_cartesian_coordinate(DetectionPosition<>& det_pos, } else { - warning("cartesian coordinate (x, y, z)=(%f, %f, %f) does not exist in the inner map", + rounded_cart_coord.z() = round(cart_coord.z()*10.0F)/10.0F; + rounded_cart_coord.y() = round(cart_coord.y()*10.0f)/10.0F; + rounded_cart_coord.x() = round(cart_coord.x()*10.0F)/10.0F; + if (detection_position_map_given_cartesian_coord_keys_2_decimal.count(rounded_cart_coord)) + { + det_pos = detection_position_map_given_cartesian_coord_keys_2_decimal.at(rounded_cart_coord); + return Succeeded::yes; + }else{ + warning("cartesian coordinate (x, y, z)=(%f, %f, %f) does not exist in the inner map", cart_coord.x(), cart_coord.y(), cart_coord.z()); - return Succeeded::no; + return Succeeded::no; + } } } } @@ -156,141 +165,75 @@ build_crystal_maps() std::string scanner_orientation = get_scanner_ptr()->get_scanner_orientation(); // check for the scanner orientation - if (scanner_orientation=="Y" || num_transaxial_buckets%4==0 ) - {/*Building starts from a bucket perpendicular to y axis, from its first crystal. + /*Building starts from a bucket perpendicular to y axis, from its first crystal. see start_x*/ //calculate start_point to build the map. - float start_z = 0; - float start_y = -1*get_scanner_ptr()->get_effective_ring_radius(); - float start_x = -1*( - ((num_transaxial_blocks_per_bucket-1)/2.)*transaxial_block_spacing - + ((num_transaxial_crystals_per_block-1)/2.)*transaxial_crystal_spacing - ); //the first crystal in the bucket - stir::CartesianCoordinate3D start_point(start_z, start_y, start_x); - - for (int ax_block_num=0; ax_block_num det_pos(tangential_coord, axial_coord, radial_coord); - - //calculate cartesion coordinate for a given detector - stir::CartesianCoordinate3D transformation_matrix( - ax_block_num*axial_block_spacing + ax_crys_num*axial_crystal_spacing, - 0., - trans_block_num*transaxial_block_spacing + trans_crys_num*transaxial_crystal_spacing); - float alpha = trans_bucket_num*(2*_PI)/num_transaxial_buckets; - - stir::Array<2, float> rotation_matrix = get_rotation_matrix(alpha); - // to match index range of CartesianCoordinate3D, which is 1 to 3 - rotation_matrix.set_min_index(1); - rotation_matrix[1].set_min_index(1); - rotation_matrix[2].set_min_index(1); - rotation_matrix[3].set_min_index(1); - - stir::CartesianCoordinate3D transformed_coord = - start_point + transformation_matrix; - stir::CartesianCoordinate3D cart_coord = - stir::matrix_multiply(rotation_matrix, transformed_coord); - - // rounding cart_coord to 3 and 2 decimal points then filling maps - cart_coord.z() = (round(cart_coord.z()*1000.0F))/1000.0F; - cart_coord.y() = (round(cart_coord.y()*1000.0F))/1000.0F; - cart_coord.x() = (round(cart_coord.x()*1000.0F))/1000.0F; - cartesian_coord_map_given_detection_position_keys[det_pos] = cart_coord; //used to find s, m, phi, theta - detection_position_map_given_cartesian_coord_keys_3_decimal[cart_coord] = det_pos; //used to find bin from listmode data - cart_coord.z() = (round(cart_coord.z()*100.0F))/100.0F; - cart_coord.y() = (round(cart_coord.y()*100.0F))/100.0F; - cart_coord.x() = (round(cart_coord.x()*100.0F))/100.0F; - detection_position_map_given_cartesian_coord_keys_2_decimal[cart_coord] = det_pos; - } - } - - else if (scanner_orientation=="X" ) - {/*Building starts from a bucket perpendicular to x axis, from its first crystal. - see start_y*/ - - //calculate start_point to build the map. - float start_z = -1*( - ((num_axial_blocks-1)/2.)*axial_block_spacing - + ((num_axial_crystals_per_block-1)/2.)*axial_crystal_spacing - ); - float start_x = get_scanner_ptr()->get_effective_ring_radius(); - float start_y = -1*( - ((num_transaxial_blocks_per_bucket-1)/2.)*transaxial_block_spacing - + ((num_transaxial_crystals_per_block-1)/2.)*transaxial_crystal_spacing - ); //the first crystal in the bucket - stir::CartesianCoordinate3D start_point(start_z, start_y, start_x); - - for (int ax_block_num=0; ax_block_num det_pos(tangential_coord, axial_coord, radial_coord); - //calculate cartesion coordinate for a given detector - stir::CartesianCoordinate3D transformation_matrix( - ax_block_num*axial_block_spacing + ax_crys_num*axial_crystal_spacing, - 0., - trans_block_num*transaxial_block_spacing + trans_crys_num*transaxial_crystal_spacing); - - float alpha = (trans_bucket_num - num_transaxial_buckets/4) - *(2*_PI)/num_transaxial_buckets; - - stir::Array<2, float> rotation_matrix = get_rotation_matrix(alpha); - // to match index range of CartesianCoordinate3D, which is 1 to 3 - rotation_matrix.set_min_index(1); - rotation_matrix[1].set_min_index(1); - rotation_matrix[2].set_min_index(1); - rotation_matrix[3].set_min_index(1); - - stir::CartesianCoordinate3D transformed_coord = start_point+transformation_matrix; - stir::CartesianCoordinate3D cart_coord = - stir::matrix_multiply(rotation_matrix, transformed_coord); - - // rounding cart_coord to 3 and 2 decimal points then filling maps - cart_coord.z() = (round(cart_coord.z()*1000.0F))/1000.0F; - cart_coord.y() = (round(cart_coord.y()*1000.0F))/1000.0F; - cart_coord.x() = (round(cart_coord.x()*1000.0F))/1000.0F; - cartesian_coord_map_given_detection_position_keys[det_pos] = cart_coord; //used to find s, m, phi, theta - detection_position_map_given_cartesian_coord_keys_3_decimal[cart_coord] = det_pos; //used to find bin from listmode data - cart_coord.z() = (round(cart_coord.z()*100.0F))/100.0F; - cart_coord.y() = (round(cart_coord.y()*100.0F))/100.0F; - cart_coord.x() = (round(cart_coord.x()*100.0F))/100.0F; - detection_position_map_given_cartesian_coord_keys_2_decimal[cart_coord] = det_pos; - } + float start_z = 0; + float start_y = -1*get_scanner_ptr()->get_effective_ring_radius(); + float start_x = -1*( + ((num_transaxial_blocks_per_bucket-1)/2.)*transaxial_block_spacing + + ((num_transaxial_crystals_per_block-1)/2.)*transaxial_crystal_spacing + ); //the first crystal in the bucket + stir::CartesianCoordinate3D start_point(start_z, start_y, start_x); + + for (int ax_block_num=0; ax_block_num det_pos(tangential_coord, axial_coord, radial_coord); + + //calculate cartesion coordinate for a given detector + stir::CartesianCoordinate3D transformation_matrix( + ax_block_num*axial_block_spacing + ax_crys_num*axial_crystal_spacing, + 0., + trans_block_num*transaxial_block_spacing + trans_crys_num*transaxial_crystal_spacing); + float alpha = trans_bucket_num*(2*_PI)/num_transaxial_buckets; + + stir::Array<2, float> rotation_matrix = get_rotation_matrix(alpha); + // to match index range of CartesianCoordinate3D, which is 1 to 3 + rotation_matrix.set_min_index(1); + rotation_matrix[1].set_min_index(1); + rotation_matrix[2].set_min_index(1); + rotation_matrix[3].set_min_index(1); + + stir::CartesianCoordinate3D transformed_coord = + start_point + transformation_matrix; + stir::CartesianCoordinate3D cart_coord = + stir::matrix_multiply(rotation_matrix, transformed_coord); + + // rounding cart_coord to 3 and 2 decimal points then filling maps + cart_coord.z() = (round(cart_coord.z()*1000.0F))/1000.0F; + cart_coord.y() = (round(cart_coord.y()*1000.0F))/1000.0F; + cart_coord.x() = (round(cart_coord.x()*1000.0F))/1000.0F; + cartesian_coord_map_given_detection_position_keys[det_pos] = cart_coord; //used to find s, m, phi, theta + detection_position_map_given_cartesian_coord_keys_3_decimal[cart_coord] = det_pos; //used to find bin from listmode data + cart_coord.z() = (round(cart_coord.z()*100.0F))/100.0F; + cart_coord.y() = (round(cart_coord.y()*100.0F))/100.0F; + cart_coord.x() = (round(cart_coord.x()*100.0F))/100.0F; + detection_position_map_given_cartesian_coord_keys_2_decimal[cart_coord] = det_pos; } } diff --git a/src/buildblock/SSRB.cxx b/src/buildblock/SSRB.cxx index 1c2ec6fe1..2eb42fa59 100644 --- a/src/buildblock/SSRB.cxx +++ b/src/buildblock/SSRB.cxx @@ -5,15 +5,7 @@ Copyright (C) 2021, University College London This file is part of STIR. - This file is free software; you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation; either version 2.1 of the License, or - (at your option) any later version. - - This file is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Lesser General Public License for more details. + SPDX-License-Identifier: Apache-2.0 See STIR/LICENSE.txt for details */ diff --git a/src/include/stir/ProjDataInfoBlocksOnCylindrical.inl b/src/include/stir/ProjDataInfoBlocksOnCylindrical.inl index ce749dc7c..f92bcc286 100644 --- a/src/include/stir/ProjDataInfoBlocksOnCylindrical.inl +++ b/src/include/stir/ProjDataInfoBlocksOnCylindrical.inl @@ -78,8 +78,6 @@ ProjDataInfoBlocksOnCylindrical::get_m(const Bin& bin) const { LORInAxialAndNoArcCorrSinogramCoordinates lor; get_LOR(lor, bin); - //Parisa to Check - //std::cout<<"seg_ax_m_z1 = "< lor; - get_LOR(lor, bin); - const float delta_z = lor.z2() - lor.z1(); - if (fabs(delta_z)<0.0001F) - return 0; - const float R=get_ring_radius(bin.view_num()); - assert(R>=fabs(get_s(bin))); - return delta_z/(2*sqrt(square(R)-square(get_s(bin)))); + CartesianCoordinate3D _p1; + CartesianCoordinate3D _p2; + find_cartesian_coordinates_of_detection(_p1, _p2, bin); + CartesianCoordinate3D p2_minus_p1 = _p2 - _p1; + return p2_minus_p1.z() / (sqrt(square(p2_minus_p1.x())+square(p2_minus_p1.y()))); } float diff --git a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl index adab1cec9..bc3ee9890 100644 --- a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl +++ b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl @@ -87,11 +87,45 @@ float ProjDataInfoBlocksOnCylindricalNoArcCorr:: get_s(const Bin& bin) const { - LORInAxialAndNoArcCorrSinogramCoordinates lor; + LORInAxialAndNoArcCorrSinogramCoordinates lor; get_LOR(lor, bin); - if (bin.view_num()==0 && lor.phi()>0.1) - return -1*lor.radius() * sin(lor.beta()); - return lor.radius() * sin(lor.beta()); + // REQUIREMENT: Euclidean coordinate of 3 points, a,b and c. + // CALCULATION: // Equation of a line, in parametric form, given two point a, b: p(t) = a + (b-a)*t + // // Let a,b,c be points in 2D and let a,b form a line then shortest distance between c and line ab is: + // // || (p0-p1) - [(p0-p1)*(p2-p1)/||p2-p1||^2]*(p2-p1) || + // p1 = _p1, p2=_p2 and p0=(0,0,0) + // OUTPUT: replace the ring_radius*sin(lor.beta()) with distance from o to ab. + + // Can't access coordinate of detection from this class so we have to recalculate where it is: + + CartesianCoordinate3D _p1; + CartesianCoordinate3D _p2; + find_cartesian_coordinates_of_detection(_p1, _p2, bin); + + // // Get p1-p0 and p2-p1 vector. + CartesianCoordinate3D p2_minus_p1 = _p2 - _p1; + CartesianCoordinate3D p0_minus_p1 = CartesianCoordinate3D(0,0,0) - _p1; + float p0_minus_p1_dot_p2_minus_p1 = p0_minus_p1.x()* p2_minus_p1.x() + p0_minus_p1.y()*p2_minus_p1.y(); + float p2_minus_p1_magitude = square(p2_minus_p1.x()) + square(p2_minus_p1.y()); + float x = 0; + float y = 0; + float sign = sin(lor.beta()); + if (p2_minus_p1_magitude > 0.01) + { + x = p0_minus_p1.x() - (p0_minus_p1_dot_p2_minus_p1/p2_minus_p1_magitude)*p2_minus_p1.x(); + y = p0_minus_p1.y() - (p0_minus_p1_dot_p2_minus_p1/p2_minus_p1_magitude)*p2_minus_p1.y(); + } + else + { + error("get_s(): 2 detection points are too close to each other. This indicates an internal error."); + } + float s = sqrt(square(x) + square(y)); + + if(sign < 0.0){ + return -s; + }else{ + return s; + } } float diff --git a/src/include/stir/scatter/ScatterSimulation.h b/src/include/stir/scatter/ScatterSimulation.h index 9b0d215e3..796869b7b 100644 --- a/src/include/stir/scatter/ScatterSimulation.h +++ b/src/include/stir/scatter/ScatterSimulation.h @@ -34,7 +34,9 @@ #include "stir/RegisteredObject.h" #include "stir/ProjData.h" #include "stir/VoxelsOnCartesianGrid.h" +#include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" #include "stir/ProjDataInfoCylindricalNoArcCorr.h" +#include "stir/ProjDataInfoGenericNoArcCorr.h" START_NAMESPACE_STIR @@ -118,7 +120,7 @@ class ScatterSimulation : public RegisteredObject inline int get_num_scatter_points() const { return static_cast(this->scatt_points_vector.size());} //! Get the template ProjDataInfo - shared_ptr get_template_proj_data_info_sptr() const; + shared_ptr get_template_proj_data_info_sptr() const; //! Get the ExamInfo shared_ptr get_exam_info_sptr() const; @@ -349,8 +351,7 @@ class ScatterSimulation : public RegisteredObject std::string template_proj_data_filename; - shared_ptr proj_data_info_cyl_noarc_cor_sptr; - + shared_ptr proj_data_info_cyl_noarc_cor_sptr; //! \details Exam info extracted from the scanner template shared_ptr template_exam_info_sptr; diff --git a/src/recon_buildblock/ProjMatrixByBinUsingRayTracing.cxx b/src/recon_buildblock/ProjMatrixByBinUsingRayTracing.cxx index 61f5b5794..7f8125409 100644 --- a/src/recon_buildblock/ProjMatrixByBinUsingRayTracing.cxx +++ b/src/recon_buildblock/ProjMatrixByBinUsingRayTracing.cxx @@ -761,9 +761,15 @@ calculate_proj_matrix_elems_for_one_bin( min((min(max_index.x(), -min_index.x())+.45F)*voxel_size.x(), (min(max_index.y(), -min_index.y())+.45F)*voxel_size.y()); #else - const float fovrad_in_mm = + float fovrad_in_mm = min((min(max_index.x(), -min_index.x()))*voxel_size.x(), (min(max_index.y(), -min_index.y()))*voxel_size.y()); + if (proj_data_info_ptr->get_scanner_ptr()->get_scanner_geometry() == "BlocksOnCylindrical") + { + fovrad_in_mm = + min((min(max_index.x(), -min_index.x()) - 5.f) * voxel_size.x(), + (min(max_index.y(), -min_index.y()) - 5.f) * voxel_size.y()); + } #endif if (num_tangential_LORs == 1) diff --git a/src/scatter_buildblock/ScatterSimulation.cxx b/src/scatter_buildblock/ScatterSimulation.cxx index 71bb183cd..596ec44fb 100644 --- a/src/scatter_buildblock/ScatterSimulation.cxx +++ b/src/scatter_buildblock/ScatterSimulation.cxx @@ -111,7 +111,6 @@ process_data() float total_scatter = 0 ; info("ScatterSimulator: Initialization finished ..."); - for (vs_num.segment_num() = this->proj_data_info_cyl_noarc_cor_sptr->get_min_segment_num(); vs_num.segment_num() <= this->proj_data_info_cyl_noarc_cor_sptr->get_max_segment_num(); ++vs_num.segment_num()) @@ -366,8 +365,13 @@ set_up() { CartesianCoordinate3D detector_coord_A, detector_coord_B; // check above statement - this->proj_data_info_cyl_noarc_cor_sptr->find_cartesian_coordinates_of_detection( - detector_coord_A, detector_coord_B, Bin(0, 0, 0, 0)); + if(dynamic_cast (proj_data_info_cyl_noarc_cor_sptr.get())){ + auto ptr = dynamic_cast (proj_data_info_cyl_noarc_cor_sptr.get()); + ptr->find_cartesian_coordinates_of_detection(detector_coord_A, detector_coord_B, Bin(0, 0, 0, 0)); + }else{ + auto ptr = dynamic_cast (proj_data_info_cyl_noarc_cor_sptr.get()); + ptr->find_cartesian_coordinates_of_detection(detector_coord_A, detector_coord_B, Bin(0, 0, 0, 0)); + } assert(detector_coord_A.z() == 0); assert(detector_coord_B.z() == 0); // check that get_m refers to the middle of the scanner @@ -378,11 +382,20 @@ set_up() assert(fabs(m_last + m_first) < m_last * 10E-4); } #endif - this->shift_detector_coordinates_to_origin = + if(dynamic_cast (proj_data_info_cyl_noarc_cor_sptr.get())){ + this->shift_detector_coordinates_to_origin = CartesianCoordinate3D(this->proj_data_info_cyl_noarc_cor_sptr->get_m(Bin(0, 0, 0, 0)), 0, 0); + }else{ + if(dynamic_cast (proj_data_info_cyl_noarc_cor_sptr.get())){ + // align BlocksOnCylindrical scanner ring 0 to z=0. + this->shift_detector_coordinates_to_origin = + CartesianCoordinate3D(this->proj_data_info_cyl_noarc_cor_sptr->get_m(Bin(0, 0, 0, 0)), 0, 0); + } + // align Generic geometry here. + } #if 1 - // checks on image zooming to avvoid getting incorrect results + // checks on image zooming to avoid getting incorrect results { check_z_to_middle_consistent(*this->activity_image_sptr, "activity"); check_z_to_middle_consistent(*this->density_image_sptr, "attenuation"); @@ -710,7 +723,7 @@ set_output_proj_data_sptr(shared_ptr arg) this->output_proj_data_sptr = arg; } -shared_ptr +shared_ptr ScatterSimulation:: get_template_proj_data_info_sptr() const { @@ -739,10 +752,14 @@ void ScatterSimulation::set_template_proj_data_info(const ProjDataInfo& arg) { this->_already_set_up = false; - this->proj_data_info_cyl_noarc_cor_sptr.reset(dynamic_cast(arg.clone())); + this->proj_data_info_cyl_noarc_cor_sptr.reset(dynamic_cast(arg.clone())); - if (is_null_ptr(this->proj_data_info_cyl_noarc_cor_sptr)) - error("ScatterSimulation: Can only handle non-arccorrected data"); + if (is_null_ptr(this->proj_data_info_cyl_noarc_cor_sptr)){ + this->proj_data_info_cyl_noarc_cor_sptr.reset(dynamic_cast(arg.clone())); + if (is_null_ptr(this->proj_data_info_cyl_noarc_cor_sptr)){ + error("ScatterSimulation: Can only handle non-arccorrected data"); + } + } // find final size of detection_points_vector this->total_detectors = @@ -818,7 +835,6 @@ ScatterSimulation::downsample_scanner(int new_num_rings, int new_num_dets) + 5; // add 5 to avoid strange edge-effects, certainly with B-splines new_scanner_sptr->set_max_num_non_arccorrected_bins(round(approx_num_non_arccorrected_bins+.5F)); new_scanner_sptr->set_default_bin_size(new_scanner_sptr->get_effective_ring_radius() * _PI / new_num_dets); // approx new detector size - // Find how much is the delta ring // If the previous projdatainfo had max segment == 1 then should be from SSRB // in ScatterEstimation. Otherwise use the max possible. diff --git a/src/scatter_buildblock/scatter_detection_modelling.cxx b/src/scatter_buildblock/scatter_detection_modelling.cxx index ad6124560..69cc51ae9 100644 --- a/src/scatter_buildblock/scatter_detection_modelling.cxx +++ b/src/scatter_buildblock/scatter_detection_modelling.cxx @@ -29,7 +29,7 @@ */ #include "stir/scatter/ScatterSimulation.h" -#include "stir/ProjDataInfoCylindricalNoArcCorr.h" +#include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" #include "stir/numerics/erf.h" #include "stir/info.h" #include @@ -75,9 +75,21 @@ find_detectors(unsigned& det_num_A, unsigned& det_num_B, const Bin& bin) const error("ScatterSimulation::find_detectors: need to call set_up() first"); #endif CartesianCoordinate3D detector_coord_A, detector_coord_B; - this->proj_data_info_cyl_noarc_cor_sptr-> - find_cartesian_coordinates_of_detection( - detector_coord_A,detector_coord_B,bin); + auto ptr = dynamic_cast (proj_data_info_cyl_noarc_cor_sptr.get()); + if(ptr){ + ptr-> + find_cartesian_coordinates_of_detection( + detector_coord_A,detector_coord_B,bin); + }else{ + auto ptr = dynamic_cast (proj_data_info_cyl_noarc_cor_sptr.get()); + if(ptr){ + ptr-> + find_cartesian_coordinates_of_detection( + detector_coord_A,detector_coord_B,bin); + }else{ + error("wrong type of projection data for scatter simulation"); + } + } det_num_A = this->find_in_detection_points_vector(detector_coord_A + this->shift_detector_coordinates_to_origin); diff --git a/src/test/test_ScatterSimulation.cxx b/src/test/test_ScatterSimulation.cxx index 5110178d6..18129cea5 100644 --- a/src/test/test_ScatterSimulation.cxx +++ b/src/test/test_ScatterSimulation.cxx @@ -32,6 +32,7 @@ #include "stir/Scanner.h" #include "stir/Viewgram.h" #include "stir/Succeeded.h" +#include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h" #include "stir/ProjDataInfoCylindricalNoArcCorr.h" #include "stir/scatter/SingleScatterSimulation.h" #include "stir/zoom.h" @@ -101,9 +102,8 @@ test_downsampling_ProjDataInfo() unique_ptr sss(new SingleScatterSimulation()); sss->set_template_proj_data_info(*original_projdata); - { - auto sss_projdata(sss->get_template_proj_data_info_sptr()); + auto sss_projdata = dynamic_cast(sss->get_template_proj_data_info_sptr().get()); check(*original_projdata == *sss_projdata, "Check the ProjDataInfo has been set correctly."); } @@ -113,7 +113,7 @@ test_downsampling_ProjDataInfo() int down_dets = static_cast(test_scanner->get_num_detectors_per_ring() * 0.5); sss->downsample_scanner(down_rings, down_dets); - auto sss_projdata(sss->get_template_proj_data_info_sptr()); + auto sss_projdata = dynamic_cast(sss->get_template_proj_data_info_sptr().get()); check_if_equal(original_projdata->get_scanner_ptr()->get_num_rings(), 2*sss_projdata->get_scanner_ptr()->get_num_rings(), "Check the number of rings is correct"); check_if_equal(original_projdata->get_scanner_ptr()->get_num_detectors_per_ring(), 2*sss_projdata->get_scanner_ptr()->get_num_detectors_per_ring(), "Check number of detectors per ring."); From 05cd5ae4cec9ebda5c06faf243395a187ded5ad2 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Wed, 10 Nov 2021 20:42:01 +0000 Subject: [PATCH 29/81] remove non-ASCII character in comment this breaks dox2swig --- src/utilities/find_normfactors_from_cylinder_data.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/find_normfactors_from_cylinder_data.cxx b/src/utilities/find_normfactors_from_cylinder_data.cxx index f7d18b638..2164546ac 100644 --- a/src/utilities/find_normfactors_from_cylinder_data.cxx +++ b/src/utilities/find_normfactors_from_cylinder_data.cxx @@ -129,7 +129,7 @@ int main(int argc, char **argv) /* for each lor find_LOR_intersections_with_cylinder => c1 & c2 - c12 = |c1-c2| = sqrt(Δx^2+Δy^2+Δz^2) + c12 = |c1-c2| = sqrt(dx^2+dy^2+dz^2) N_lor/c12 should be the same for all therefore: NF_lor= / (N_lor/c12) */ From d0284d0dab8d680ab549e2d00407eccbf23410c0 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Thu, 11 Nov 2021 23:49:47 +0000 Subject: [PATCH 30/81] update ProjDataInfoGeneric from Blocks* copied changed in the BlocksOnCylindrical classes to the Generic hierarchy --- src/buildblock/ProjDataInfoGeneric.cxx | 3 +- .../ProjDataInfoGenericNoArcCorr.cxx | 15 ++- src/include/stir/ProjDataInfoGeneric.h | 4 + src/include/stir/ProjDataInfoGeneric.inl | 38 +++++-- .../stir/ProjDataInfoGenericNoArcCorr.h | 6 ++ .../stir/ProjDataInfoGenericNoArcCorr.inl | 102 +++++++++++++++--- 6 files changed, 146 insertions(+), 22 deletions(-) diff --git a/src/buildblock/ProjDataInfoGeneric.cxx b/src/buildblock/ProjDataInfoGeneric.cxx index da9e0a89a..b362eb34d 100644 --- a/src/buildblock/ProjDataInfoGeneric.cxx +++ b/src/buildblock/ProjDataInfoGeneric.cxx @@ -499,7 +499,8 @@ get_LOR(LORInAxialAndNoArcCorrSinogramCoordinates& lor, LORAs2Points lor_as_2_points(_p1, _p2); const double R = get_ring_radius(); - lor_as_2_points.change_representation_for_block(lor, R); + + lor_as_2_points.change_representation(lor, R); } std::string diff --git a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx index 839d8bf49..e3014b27c 100644 --- a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx +++ b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx @@ -404,12 +404,24 @@ find_cartesian_coordinates_given_scanner_coordinates(CartesianCoordinate3Dget_coords_given_detpos(det_pos1); coord_2 = get_scanner_ptr()->get_coords_given_detpos(det_pos2); +#else + if (crystal_map->find_cartesian_coordinate_given_detection_position(coord_1, det_pos1)==Succeeded::yes && + crystal_map->find_cartesian_coordinate_given_detection_position(coord_2, det_pos2)==Succeeded::yes) + { + return; + } + else + { + error("couldn't find corresponding cartesian coordinates for given detection positions.\n"); + return; + } +#endif } -//TODO: Try to get it obsolete (Is still used in one test) Bin ProjDataInfoGenericNoArcCorr:: get_bin(const LOR& lor) const @@ -423,6 +435,7 @@ get_bin(const LOR& lor) const DetectionPosition<> det_pos1; DetectionPosition<> det_pos2; + // TODO GENERICvsBLOCK crystal_map should not be used if (crystal_map->find_detection_position_given_cartesian_coordinate(det_pos1, _p1)==Succeeded::no || crystal_map->find_detection_position_given_cartesian_coordinate(det_pos2, _p2)==Succeeded::no) { diff --git a/src/include/stir/ProjDataInfoGeneric.h b/src/include/stir/ProjDataInfoGeneric.h index b22b0295b..6971a8076 100644 --- a/src/include/stir/ProjDataInfoGeneric.h +++ b/src/include/stir/ProjDataInfoGeneric.h @@ -285,6 +285,10 @@ class ProjDataInfoGeneric: public ProjDataInfo //! This function sets all of the above void initialise_ring_diff_arrays() const; + //! This function guarantees that ring_diff_arrays will be set but checks first if was done already + /*! This function is OPENMP thread-safe */ + inline void initialise_ring_diff_arrays_if_not_done_yet() const; + //! This function checks if max_ring_diff is different from min_ring_diff (set to 2). /*! in case of difference, there are 2 ax_pos per ring, i.e. an ax_pos between each two rings */ inline int get_num_axial_poss_per_ring_inc(const int segment_num) const; diff --git a/src/include/stir/ProjDataInfoGeneric.inl b/src/include/stir/ProjDataInfoGeneric.inl index 48ca32fae..9dda5f755 100644 --- a/src/include/stir/ProjDataInfoGeneric.inl +++ b/src/include/stir/ProjDataInfoGeneric.inl @@ -36,6 +36,31 @@ limitations under the License. START_NAMESPACE_STIR +void +ProjDataInfoGeneric:: +initialise_ring_diff_arrays_if_not_done_yet() const +{ + // for efficiency reasons, use "Double-Checked-Locking(DCL) pattern" with OpenMP atomic operation + // OpenMP v3.1 or later required + // thanks to yohjp: http://stackoverflow.com/questions/27975737/how-to-handle-cached-data-structures-with-multi-threading-e-g-openmp +#if defined(STIR_OPENMP) && _OPENMP >=201012 + bool initialised; +#pragma omp atomic read + initialised = ring_diff_arrays_computed; + + if (!initialised) +#endif + { +#if defined(STIR_OPENMP) +#pragma omp critical(PROJDATAINFOCYLINDRICALRINGDIFFARRAY) +#endif + { + if (!ring_diff_arrays_computed) + initialise_ring_diff_arrays(); + } + } +} + //! find phi from correspoding lor float ProjDataInfoGeneric::get_phi(const Bin& bin)const @@ -76,14 +101,11 @@ ProjDataInfoGeneric::get_t(const Bin& bin) const float ProjDataInfoGeneric::get_tantheta(const Bin& bin) const { - LORInAxialAndNoArcCorrSinogramCoordinates lor; - get_LOR(lor, bin); - const float delta_z = lor.z2() - lor.z1(); - if (fabs(delta_z)<0.0001F) - return 0; - const float R=get_ring_radius(bin.view_num()); - assert(R>=fabs(get_s(bin))); - return delta_z/(2*sqrt(square(R)-square(get_s(bin)))); + CartesianCoordinate3D _p1; + CartesianCoordinate3D _p2; + find_cartesian_coordinates_of_detection(_p1, _p2, bin); + CartesianCoordinate3D p2_minus_p1 = _p2 - _p1; + return p2_minus_p1.z() / (sqrt(square(p2_minus_p1.x())+square(p2_minus_p1.y()))); } float diff --git a/src/include/stir/ProjDataInfoGenericNoArcCorr.h b/src/include/stir/ProjDataInfoGenericNoArcCorr.h index b6ae5440e..0d1c01b4e 100644 --- a/src/include/stir/ProjDataInfoGenericNoArcCorr.h +++ b/src/include/stir/ProjDataInfoGenericNoArcCorr.h @@ -295,8 +295,14 @@ class ProjDataInfoGenericNoArcCorr : public ProjDataInfoGeneric //! build look-up table for get_view_tangential_pos_num_for_det_num_pair() void initialise_det1det2_to_uncompressed_view_tangpos() const; + //! build look-up table unless already done before + inline void initialise_uncompressed_view_tangpos_to_det1det2_if_not_done_yet() const; + //! build look-up table unless already done before + inline void initialise_det1det2_to_uncompressed_view_tangpos_if_not_done_yet() const; + virtual bool blindly_equals(const root_type * const) const; + //! \todo Has to be removed shared_ptr crystal_map; }; diff --git a/src/include/stir/ProjDataInfoGenericNoArcCorr.inl b/src/include/stir/ProjDataInfoGenericNoArcCorr.inl index b610637e1..7d06972bf 100644 --- a/src/include/stir/ProjDataInfoGenericNoArcCorr.inl +++ b/src/include/stir/ProjDataInfoGenericNoArcCorr.inl @@ -33,20 +33,100 @@ limitations under the License. START_NAMESPACE_STIR -//Parisa: changed -/* - In cylindrical s is calculated using sin(tang_pos*angular_increment) = sin(beta) - Here in Generic it is calculated directly from corresponding lor +void +ProjDataInfoGenericNoArcCorr:: +initialise_uncompressed_view_tangpos_to_det1det2_if_not_done_yet() const +{ + // for efficiency reasons, use "Double-Checked-Locking(DCL) pattern" with OpenMP atomic operation + // OpenMP v3.1 or later required + // thanks to yohjp: http://stackoverflow.com/questions/27975737/how-to-handle-cached-data-structures-with-multi-threading-e-g-openmp +#if defined(STIR_OPENMP) && _OPENMP >=201012 + bool initialised; +#pragma omp atomic read + initialised = uncompressed_view_tangpos_to_det1det2_initialised; + + if (!initialised) +#endif + { +#if defined(STIR_OPENMP) +#pragma omp critical(PROJDATAINFOCYLINDRICALNOARCCORR_VIEWTANGPOS_TO_DETS) +#endif + { + if (!uncompressed_view_tangpos_to_det1det2_initialised) + initialise_uncompressed_view_tangpos_to_det1det2(); + } + } +} + +void +ProjDataInfoGenericNoArcCorr:: +initialise_det1det2_to_uncompressed_view_tangpos_if_not_done_yet() const +{ + // as above +#if defined(STIR_OPENMP) && _OPENMP >=201012 + bool initialised; +#pragma omp atomic read + initialised = det1det2_to_uncompressed_view_tangpos_initialised; + + if (!initialised) +#endif + { +#if defined(STIR_OPENMP) +#pragma omp critical(PROJDATAINFOCYLINDRICALNOARCCORR_DETS_TO_VIEWTANGPOS) +#endif + { + if (!det1det2_to_uncompressed_view_tangpos_initialised) + initialise_det1det2_to_uncompressed_view_tangpos(); + } + } +} + +/*! warning In cylindrical s is found from bin: sin(beta) = sin(tang_pos*angular_increment) + In block it is calculated directly from corresponding lor */ float ProjDataInfoGenericNoArcCorr:: get_s(const Bin& bin) const { - LORInAxialAndNoArcCorrSinogramCoordinates lor; + LORInAxialAndNoArcCorrSinogramCoordinates lor; get_LOR(lor, bin); - if (bin.view_num()==0 && lor.phi()>0.1) - return -1*ring_radius * sin(lor.beta()); - return ring_radius * sin(lor.beta()); + // REQUIREMENT: Euclidean coordinate of 3 points, a,b and c. + // CALCULATION: // Equation of a line, in parametric form, given two point a, b: p(t) = a + (b-a)*t + // // Let a,b,c be points in 2D and let a,b form a line then shortest distance between c and line ab is: + // // || (p0-p1) - [(p0-p1)*(p2-p1)/||p2-p1||^2]*(p2-p1) || + // p1 = _p1, p2=_p2 and p0=(0,0,0) + // OUTPUT: replace the ring_radius*sin(lor.beta()) with distance from o to ab. + + // Can't access coordinate of detection from this class so we have to recalculate where it is: + + CartesianCoordinate3D _p1; + CartesianCoordinate3D _p2; + find_cartesian_coordinates_of_detection(_p1, _p2, bin); + + // // Get p1-p0 and p2-p1 vector. + CartesianCoordinate3D p2_minus_p1 = _p2 - _p1; + CartesianCoordinate3D p0_minus_p1 = CartesianCoordinate3D(0,0,0) - _p1; + float p0_minus_p1_dot_p2_minus_p1 = p0_minus_p1.x()* p2_minus_p1.x() + p0_minus_p1.y()*p2_minus_p1.y(); + float p2_minus_p1_magitude = square(p2_minus_p1.x()) + square(p2_minus_p1.y()); + float x = 0; + float y = 0; + float sign = sin(lor.beta()); + if (p2_minus_p1_magitude > 0.01) + { + x = p0_minus_p1.x() - (p0_minus_p1_dot_p2_minus_p1/p2_minus_p1_magitude)*p2_minus_p1.x(); + y = p0_minus_p1.y() - (p0_minus_p1_dot_p2_minus_p1/p2_minus_p1_magitude)*p2_minus_p1.y(); + } + else + { + error("get_s(): 2 detection points are too close to each other. This indicates an internal error."); + } + float s = sqrt(square(x) + square(y)); + + if(sign < 0.0){ + return -s; + }else{ + return s; + } } float @@ -65,8 +145,7 @@ get_det_num_pair_for_view_tangential_pos_num( const int tang_pos_num) const { assert(get_view_mashing_factor() == 1); - if (!uncompressed_view_tangpos_to_det1det2_initialised) - initialise_uncompressed_view_tangpos_to_det1det2(); + this->initialise_uncompressed_view_tangpos_to_det1det2_if_not_done_yet(); det1_num = uncompressed_view_tangpos_to_det1det2[view_num][tang_pos_num].det1_num; det2_num = uncompressed_view_tangpos_to_det1det2[view_num][tang_pos_num].det2_num; @@ -80,8 +159,7 @@ get_view_tangential_pos_num_for_det_num_pair(int& view_num, const int det2_num) const { assert(det1_num!=det2_num); - if (!det1det2_to_uncompressed_view_tangpos_initialised) - initialise_det1det2_to_uncompressed_view_tangpos(); + this->initialise_det1det2_to_uncompressed_view_tangpos_if_not_done_yet(); view_num = det1det2_to_uncompressed_view_tangpos[det1_num][det2_num].view_num/get_view_mashing_factor(); From bfdca586ba02e1d3c447349e07d2ac6e3d18017e Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Fri, 12 Nov 2021 00:34:50 +0000 Subject: [PATCH 31/81] made ProjDataInfoBlocksOnCylindrical identical to ProjDataInfoGeneric These 2 classes were the same, except for the name. This commit therefore removed all ProjDataInfoBlocksOnCylindrical implementations and sets the type to be the same. In future, it might be necessary to recreate ProjDataInfoBlocksOnCylindrical with additional functionality (or remove functionality from the Generic case), so the type-name remains. --- src/buildblock/CMakeLists.txt | 1 - .../ProjDataInfoBlocksOnCylindrical.cxx | 531 ------------------ .../stir/ProjDataInfoBlocksOnCylindrical.h | 292 +--------- .../stir/ProjDataInfoBlocksOnCylindrical.inl | 296 ---------- ...ataSymmetriesForBins_PET_CartesianGrid.cxx | 2 + 5 files changed, 4 insertions(+), 1118 deletions(-) delete mode 100644 src/buildblock/ProjDataInfoBlocksOnCylindrical.cxx delete mode 100644 src/include/stir/ProjDataInfoBlocksOnCylindrical.inl diff --git a/src/buildblock/CMakeLists.txt b/src/buildblock/CMakeLists.txt index c6cfe6e6b..580f2dc33 100644 --- a/src/buildblock/CMakeLists.txt +++ b/src/buildblock/CMakeLists.txt @@ -86,7 +86,6 @@ set(${dir_LIB_SOURCES} FilePath.cxx date_time_functions.cxx GeometryBlocksOnCylindrical.cxx - ProjDataInfoBlocksOnCylindrical.cxx ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx ProjDataInfoGeneric.cxx ProjDataInfoGenericNoArcCorr.cxx diff --git a/src/buildblock/ProjDataInfoBlocksOnCylindrical.cxx b/src/buildblock/ProjDataInfoBlocksOnCylindrical.cxx deleted file mode 100644 index 4ba923e40..000000000 --- a/src/buildblock/ProjDataInfoBlocksOnCylindrical.cxx +++ /dev/null @@ -1,531 +0,0 @@ - -/* -Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics - -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 projdata - - \brief Non-inline implementations of stir::ProjDataInfoBlocksOnCylindrical - - \author Parisa Khateri - -*/ -#include "stir/ProjDataInfoBlocksOnCylindrical.h" -#include "stir/LORCoordinates.h" -#include -#ifdef BOOST_NO_STRINGSTREAM -#include -#else -#include -#endif - -#include "stir/round.h" -#include - -#ifndef STIR_NO_NAMESPACES -using std::min_element; -using std::max_element; -using std::min; -using std::max; -using std::swap; -using std::endl; -using std::string; -using std::pair; -using std::vector; -#endif - -START_NAMESPACE_STIR - -ProjDataInfoBlocksOnCylindrical:: -ProjDataInfoBlocksOnCylindrical() -{} - - -ProjDataInfoBlocksOnCylindrical:: -ProjDataInfoBlocksOnCylindrical(const shared_ptr& scanner_ptr, - const VectorWithOffset& num_axial_pos_per_segment, - const VectorWithOffset& min_ring_diff_v, - const VectorWithOffset& max_ring_diff_v, - const int num_views,const int num_tangential_poss) - :ProjDataInfo(scanner_ptr,num_axial_pos_per_segment, - num_views,num_tangential_poss), - min_ring_diff(min_ring_diff_v), - max_ring_diff(max_ring_diff_v) -{ - - azimuthal_angle_sampling = static_cast(_PI/num_views); - ring_radius.resize(0,0); - ring_radius[0] = get_scanner_ptr()->get_effective_ring_radius(); - ring_spacing= get_scanner_ptr()->get_ring_spacing() ; - - // TODO this info should probably be provided via the constructor, or at - // least by Scanner. - sampling_corresponds_to_physical_rings = - scanner_ptr->get_type() != Scanner::HiDAC; - - - assert(min_ring_diff.get_length() == max_ring_diff.get_length()); - assert(min_ring_diff.get_length() == num_axial_pos_per_segment.get_length()); - - // check min,max ring diff - { - for (int segment_num=get_min_segment_num(); segment_num<=get_max_segment_num(); ++segment_num) - if (min_ring_diff[segment_num]> max_ring_diff[segment_num]) - { - warning("ProjDataInfoBlocksOnCylindrical: min_ring_difference %d is larger than max_ring_difference %d for segment %d. " - "Swapping them around", - min_ring_diff[segment_num], max_ring_diff[segment_num], segment_num); - swap(min_ring_diff[segment_num], max_ring_diff[segment_num]); - } - } - - initialise_ring_diff_arrays(); -} - -/* warning In cylindrical geometry m_offset is calculated based on axial_spacing, - then it is used to caculate ax_pos_num_offset and segment_axial_pos_to_ring1_plus_ring2. - For block geometry, m_offset has been removed and the above mentioned variables are - calculated independant of m_offset.*/ -void -ProjDataInfoBlocksOnCylindrical:: -initialise_ring_diff_arrays() const -{ - - // check min,max ring diff - { - // check is necessary here again because of set_min_ring_difference() - // we do not swap here because that would require the min/max_ring_diff arrays to be mutable as well - for (int segment_num=get_min_segment_num(); segment_num<=get_max_segment_num(); ++segment_num) - if (min_ring_diff[segment_num]> max_ring_diff[segment_num]) - { - error("ProjDataInfoBlocksOnCylindrical: min_ring_difference %d is larger than " - "max_ring_difference %d for segment %d.", - min_ring_diff[segment_num], max_ring_diff[segment_num], segment_num); - } - } - - // initialise ax_pos_num_offset - if (sampling_corresponds_to_physical_rings) - { - const int num_rings = get_scanner_ptr()->get_num_rings(); - ax_pos_num_offset = - VectorWithOffset(get_min_segment_num(),get_max_segment_num()); - - /* ax_pos_num will be determined by looking at ring1+ring2. - This also works for axially compressed data (i.e. span) as - ring1+ring2 is constant for all ring-pairs combined into 1 - segment,ax_pos. - - Ignoring the difficulties of axial compression for a second, it is clear that - for a given bin, there will be 2 rings as follows: - ring2 = get_m(bin)/ring_spacing + ring_diff/2 + (num_rings-1)/2 - ring1 = get_m(bin)/ring_spacing - ring_diff/2 + (num_rings-1)/2 - This follows from the fact that get_m() returns the z position - in millimeter of the middle of the LOR w.r.t. the middle of the scanner. - The (num_rings-1)/2 shifts the origin such that the first ring has - ring_num==0. - - From the above, it follows that - ring1+ring2=2*get_m(bin)/ring_spacing + (num_rings-1) - Finally, we use the formula for get_m to obtain - ring1+ring2=2*ax_pos_num/get_num_axial_poss_per_ring_inc(segment_num) - -2*m_offset[segment_num]/ring_spacing + (num_rings-1) - Solving this for ax_pos_num: - ax_pos_num = (ring1+ring2-(num_rings-1) - + 2*m_offset[segment_num]/ring_spacing - ) * get_num_axial_poss_per_ring_inc(segment_num)/2 - - We could plug m_offset in to obtain - ax_pos_num = (ring1+ring2-(num_rings-1) - ) * get_num_axial_poss_per_ring_inc(segment_num)/2. - + - (get_max_axial_pos_num(segment_num) - + get_min_axial_pos_num(segment_num) )/2. - this formula is easy to understand, but we don't use it as - at some point somebody might change m_offset - and forget to change this code... - (also, the form above would need float division and then rounding) - */ - for (int segment_num=get_min_segment_num(); segment_num<=get_max_segment_num(); ++segment_num) - { - const float ax_pos_num_offset_float = (num_rings-1) - - (get_max_axial_pos_num(segment_num) + - get_min_axial_pos_num(segment_num)) - /get_num_axial_poss_per_ring_inc(segment_num); - ax_pos_num_offset[segment_num] = round(ax_pos_num_offset_float); - // check that it was integer - if (fabs(ax_pos_num_offset[segment_num] - ax_pos_num_offset_float) > 1E-4) - { - error("ProjDataInfoBlocksOnCylindrical: in segment %d, the axial positions\n" - "do not correspond to the usual locations between physical rings.\n" - "This is suspicious and can make things go wrong in STIR, so I abort.\n" - "Check the number of axial positions in this segment.", - segment_num); - } - - if (get_num_axial_poss_per_ring_inc(segment_num)==1) - { - // check that we'll get an integer ax_pos_num, i.e. - // (ring1+ring2 - ax_pos_num_offset) has to be even, for any - // ring1,ring2 in the segment, i.e ring1-ring2 = ring_diff, so - // ring1+ring2 = 2*ring2 + ring_diff - assert(get_min_ring_difference(segment_num) == - get_max_ring_difference(segment_num)); - if ((get_max_ring_difference(segment_num) - - ax_pos_num_offset[segment_num]) % 2 != 0) - warning("ProjDataInfoBlocksOnCylindrical: the number of axial positions in " - "segment %d is such that current conventions will place " - "the LORs shifted with respect to the physical rings.", - segment_num); - } - } - } - // initialise ring_diff_to_segment_num - if (sampling_corresponds_to_physical_rings) - { - const int min_ring_difference = - *min_element(min_ring_diff.begin(), min_ring_diff.end()); - const int max_ring_difference = - *max_element(max_ring_diff.begin(), max_ring_diff.end()); - - // set ring_diff_to_segment_num to appropriate size - // in principle, the max ring difference would be scanner.num_rings-1, but - // in case someone is up to strange things, we take the max of this value - // with the max_ring_difference as given in the file - ring_diff_to_segment_num = - VectorWithOffset(min(min_ring_difference, -(get_scanner_ptr()->get_num_rings()-1)), - max(max_ring_difference, get_scanner_ptr()->get_num_rings()-1)); - // first set all to impossible value - // warning: get_segment_num_for_ring_difference relies on the fact that this value - // is larger than get_max_segment_num() - ring_diff_to_segment_num.fill(get_max_segment_num()+1); - - for(int ring_diff=min_ring_difference; ring_diff <= max_ring_difference; ++ring_diff) - { - int segment_num; - for (segment_num=get_min_segment_num(); segment_num<=get_max_segment_num(); ++segment_num) - { - if (ring_diff >= min_ring_diff[segment_num] && - ring_diff <= max_ring_diff[segment_num]) - { -#if 0 - std::cerr << "ring diff " << ring_diff << " stored in s:" << segment_num << std::endl; -#endif - ring_diff_to_segment_num[ring_diff] = segment_num; - break; - } - } - if (segment_num>get_max_segment_num()) - { - warning("ProjDataInfoBlocksOnCylindrical: ring difference %d does not belong to a segment", - ring_diff); - } - } - } - // initialise segment_axial_pos_to_ring1_plus_ring2 - if (sampling_corresponds_to_physical_rings) - { - segment_axial_pos_to_ring1_plus_ring2 = - VectorWithOffset >(get_min_segment_num(), get_max_segment_num()); - for (int s_num=get_min_segment_num(); s_num<=get_max_segment_num(); ++s_num) - { - const int min_ax_pos_num = get_min_axial_pos_num(s_num); - const int max_ax_pos_num = get_max_axial_pos_num(s_num); - segment_axial_pos_to_ring1_plus_ring2[s_num].grow(min_ax_pos_num, max_ax_pos_num); - for (int ax_pos_num=min_ax_pos_num; ax_pos_num<=max_ax_pos_num; ++ax_pos_num) - { - // see documentation above for formulas - const float ring1_plus_ring2_float = - (2*ax_pos_num - - get_max_axial_pos_num(s_num) + get_min_axial_pos_num(s_num)) - /get_num_axial_poss_per_ring_inc(s_num) + - get_scanner_ptr()->get_num_rings()-1; - - const int ring1_plus_ring2 = - round(ring1_plus_ring2_float); - // check that it was integer - assert(fabs(ring1_plus_ring2 - ring1_plus_ring2_float) < 1E-4) ; - segment_axial_pos_to_ring1_plus_ring2[s_num][ax_pos_num] = ring1_plus_ring2; - } - } - } - - if (sampling_corresponds_to_physical_rings) - allocate_segment_axial_pos_to_ring_pair(); - - ring_diff_arrays_computed = true; -} - -/*! Default implementation checks common variables. Needs to be overloaded. - */ -bool -ProjDataInfoBlocksOnCylindrical:: -blindly_equals(const root_type * const that) const -{ - if (!base_type::blindly_equals(that)) - return false; - - const self_type& proj_data_info = static_cast(*that); - return - this->azimuthal_angle_sampling == proj_data_info.azimuthal_angle_sampling && - this->ring_radius == proj_data_info.ring_radius && - this->sampling_corresponds_to_physical_rings == proj_data_info.sampling_corresponds_to_physical_rings && - this->ring_spacing == proj_data_info.ring_spacing && - this->min_ring_diff == proj_data_info.min_ring_diff && - this->max_ring_diff == proj_data_info.max_ring_diff; -} - -void -ProjDataInfoBlocksOnCylindrical:: -get_ring_pair_for_segment_axial_pos_num(int& ring1, - int& ring2, - const int segment_num, - const int axial_pos_num) const -{ - if (!sampling_corresponds_to_physical_rings) - error("ProjDataInfoBlocksOnCylindrical::get_ring_pair_for_segment_axial_pos_num does not work for this type of sampled data"); - // can do only span=1 at the moment - if (get_min_ring_difference(segment_num) != get_max_ring_difference(segment_num)) - error("ProjDataInfoBlocksOnCylindrical::get_ring_pair_for_segment_axial_pos_num does not work for data with axial compression"); - - if (!ring_diff_arrays_computed) - initialise_ring_diff_arrays(); - - const int ring_diff = get_max_ring_difference(segment_num); - const int ring1_plus_ring2= segment_axial_pos_to_ring1_plus_ring2[segment_num][axial_pos_num]; - - // KT 01/08/2002 swapped rings - ring1 = (ring1_plus_ring2 - ring_diff)/2; - ring2 = (ring1_plus_ring2 + ring_diff)/2; - assert((ring1_plus_ring2 + ring_diff)%2 == 0); - assert((ring1_plus_ring2 - ring_diff)%2 == 0); -} - - -void -ProjDataInfoBlocksOnCylindrical:: -set_azimuthal_angle_sampling(const float angle_v) -{ - azimuthal_angle_sampling = angle_v; -} - -//void -//ProjDataInfoBlocksOnCylindrical:: -//set_axial_sampling(const float samp_v, int segment_num) -//{axial_sampling = samp_v;} - - -void -ProjDataInfoBlocksOnCylindrical:: -set_num_views(const int new_num_views) -{ - const float old_azimuthal_angle_range = - this->get_azimuthal_angle_sampling() * this->get_num_views(); - base_type::set_num_views(new_num_views); - this->azimuthal_angle_sampling = old_azimuthal_angle_range/this->get_num_views(); -} - -void -ProjDataInfoBlocksOnCylindrical:: -set_min_ring_difference( int min_ring_diff_v, int segment_num) -{ - ring_diff_arrays_computed = false; - min_ring_diff[segment_num] = min_ring_diff_v; -} - -void -ProjDataInfoBlocksOnCylindrical:: -set_max_ring_difference( int max_ring_diff_v, int segment_num) -{ - ring_diff_arrays_computed = false; - max_ring_diff[segment_num] = max_ring_diff_v; -} - -void -ProjDataInfoBlocksOnCylindrical:: -set_ring_spacing(float ring_spacing_v) -{ - ring_diff_arrays_computed = false; - ring_spacing = ring_spacing_v; -} - -void -ProjDataInfoBlocksOnCylindrical:: -allocate_segment_axial_pos_to_ring_pair() const -{ - segment_axial_pos_to_ring_pair = - VectorWithOffset > > - (get_min_segment_num(), get_max_segment_num()); - - for (int segment_num = get_min_segment_num(); - segment_num <= get_max_segment_num(); - ++segment_num) - { - segment_axial_pos_to_ring_pair[segment_num].grow(get_min_axial_pos_num(segment_num), - get_max_axial_pos_num(segment_num)); - } -} - -void -ProjDataInfoBlocksOnCylindrical:: -compute_segment_axial_pos_to_ring_pair(const int segment_num, const int axial_pos_num) const -{ - shared_ptr new_el(new RingNumPairs); - segment_axial_pos_to_ring_pair[segment_num][axial_pos_num] = new_el; - - RingNumPairs& table = - *segment_axial_pos_to_ring_pair[segment_num][axial_pos_num]; - table.reserve(get_max_ring_difference(segment_num) - - get_min_ring_difference(segment_num) + 1); - - /* We compute the lookup-table in a fancy way. - We could just as well have a simple loop over all ring pairs and check - if it belongs to this segment/axial_pos. - The current way is a lot faster though. - */ - const int min_ring_diff = get_min_ring_difference(segment_num); - const int max_ring_diff = get_max_ring_difference(segment_num); - const int num_rings = get_scanner_ptr()->get_num_rings(); - - /* ring1_plus_ring2 is the same for any ring pair that contributes to - this particular segment_num, axial_pos_num. - */ - const int ring1_plus_ring2= - segment_axial_pos_to_ring1_plus_ring2[segment_num][axial_pos_num]; - - /* - The ring_difference increments with 2 as the other ring differences do - not give a ring pair with this axial_position. This is because - ring1_plus_ring2%2 == ring_diff%2 - (which easily follows by plugging in ring1+ring2 and ring1-ring2). - The starting ring_diff is determined such that the above condition - is satisfied. You can check it by noting that the - start_ring_diff%2 - == (min_ring_diff + (min_ring_diff+ring1_plus_ring2)%2)%2 - == (2*min_ring_diff+ring1_plus_ring2)%2 - == ring1_plus_ring2%2 - */ - for(int ring_diff = min_ring_diff + (min_ring_diff+ring1_plus_ring2)%2; - ring_diff <= max_ring_diff; - ring_diff+=2 ) - { - const int ring1 = (ring1_plus_ring2 - ring_diff)/2; - const int ring2 = (ring1_plus_ring2 + ring_diff)/2; - if (ring1<0 || ring2 < 0 || ring1>=num_rings || ring2 >= num_rings) - continue; - assert((ring1_plus_ring2 + ring_diff)%2 == 0); - assert((ring1_plus_ring2 - ring_diff)%2 == 0); - table.push_back(pair(ring1, ring2)); - #ifndef NDEBUG - int check_segment_num = 0, check_axial_pos_num = 0; - assert(get_segment_axial_pos_num_for_ring_pair(check_segment_num, - check_axial_pos_num, - ring1, - ring2) == - Succeeded::yes); - assert(check_segment_num == segment_num); - assert(check_axial_pos_num == axial_pos_num); - #endif - } -} - -void -ProjDataInfoBlocksOnCylindrical:: -set_num_axial_poss_per_segment(const VectorWithOffset& num_axial_poss_per_segment) -{ - ProjDataInfo::set_num_axial_poss_per_segment(num_axial_poss_per_segment); - ring_diff_arrays_computed = false; -} - -void -ProjDataInfoBlocksOnCylindrical:: -set_min_axial_pos_num(const int min_ax_pos_num, const int segment_num) -{ - ProjDataInfo::set_min_axial_pos_num(min_ax_pos_num, segment_num); - ring_diff_arrays_computed = false; -} - -void ProjDataInfoBlocksOnCylindrical:: -set_max_axial_pos_num(const int max_ax_pos_num, const int segment_num) -{ - ProjDataInfo::set_max_axial_pos_num(max_ax_pos_num, segment_num); - ring_diff_arrays_computed = false; -} - -void -ProjDataInfoBlocksOnCylindrical:: -reduce_segment_range(const int min_segment_num, const int max_segment_num) -{ - ProjDataInfo::reduce_segment_range(min_segment_num, max_segment_num); - // reduce ring_diff arrays to new valid size - VectorWithOffset new_min_ring_diff(min_segment_num, max_segment_num); - VectorWithOffset new_max_ring_diff(min_segment_num, max_segment_num); - - for (int segment_num = min_segment_num; segment_num<= max_segment_num; ++segment_num) - { - new_min_ring_diff[segment_num] = this->min_ring_diff[segment_num]; - new_max_ring_diff[segment_num] = this->max_ring_diff[segment_num]; - } - - this->min_ring_diff = new_min_ring_diff; - this->max_ring_diff = new_max_ring_diff; - - // make sure other arrays will be updated if/when necessary - this->ring_diff_arrays_computed = false; -} - -//! warning Find lor from cartesian coordinates of detector pair -void -ProjDataInfoBlocksOnCylindrical:: -get_LOR(LORInAxialAndNoArcCorrSinogramCoordinates& lor, - const Bin& bin) const -{ - CartesianCoordinate3D _p1; - CartesianCoordinate3D _p2; - find_cartesian_coordinates_of_detection(_p1, _p2, bin); - - LORAs2Points lor_as_2_points(_p1, _p2); - const double R = get_ring_radius(); - - lor_as_2_points.change_representation(lor, R); -} - -string -ProjDataInfoBlocksOnCylindrical::parameter_info() const -{ - -#ifdef BOOST_NO_STRINGSTREAM - // dangerous for out-of-range, but 'old-style' ostrstream seems to need this - char str[30000]; - ostrstream s(str, 30000); -#else - std::ostringstream s; -#endif - s << ProjDataInfo::parameter_info(); - s << "Azimuthal angle increment (deg): " << get_azimuthal_angle_sampling()*180/_PI << '\n'; - s << "Azimuthal angle extent (deg): " << fabs(get_azimuthal_angle_sampling())*get_num_views()*180/_PI << '\n'; - - s << "ring differences per segment: \n"; - for (int segment_num=get_min_segment_num(); segment_num<=get_max_segment_num(); ++segment_num) - { - s << '(' << min_ring_diff[segment_num] << ',' << max_ring_diff[segment_num] <<')'; - } - s << std::endl; - return s.str(); -} - -END_NAMESPACE_STIR diff --git a/src/include/stir/ProjDataInfoBlocksOnCylindrical.h b/src/include/stir/ProjDataInfoBlocksOnCylindrical.h index 4af1345a1..1183bbf40 100644 --- a/src/include/stir/ProjDataInfoBlocksOnCylindrical.h +++ b/src/include/stir/ProjDataInfoBlocksOnCylindrical.h @@ -1,6 +1,5 @@ /* -Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,303 +20,16 @@ limitations under the License. \brief Declaration of class stir::ProjDataInfoBlocksOnCylindrical - \author Parisa Khateri - */ #ifndef __stir_ProjDataInfoBlocksOnCylindrical_H__ #define __stir_ProjDataInfoBlocksOnCylindrical_H__ -#include "stir/ProjDataInfo.h" -#include -#include - -#ifndef STIR_NO_NAMESPACES -using std::vector; -using std::pair; -#endif +#include "stir/ProjDataInfoGeneric.h" START_NAMESPACE_STIR -class Succeeded; -template class CartesianCoordinate3D; - -/*! - \ingroup projdata - \brief projection data info for data corresponding to a - 'Blocks-on-cylindrical' sampling. -*/ - -class ProjDataInfoBlocksOnCylindrical: public ProjDataInfo -{ -private: - typedef ProjDataInfo base_type; - typedef ProjDataInfoBlocksOnCylindrical self_type; - -public: - //! Type used by get_all_ring_pairs_for_segment_axial_pos_num() - typedef vector > RingNumPairs; - - //! Constructors - ProjDataInfoBlocksOnCylindrical(); - //! Constructor given all the necessary information - /*! The min and max ring difference in each segment are passed - as VectorWithOffsets. All three vectors have to have index ranges - from min_segment_num to max_segment_num. - - \warning Most of this library assumes that segment 0 corresponds - to an average ring difference of 0. - */ - ProjDataInfoBlocksOnCylindrical(const shared_ptr& scanner_ptr, - const VectorWithOffset& num_axial_poss_per_segment, //index ranges from min_segment_num to max_segment_num - const VectorWithOffset& min_ring_diff, - const VectorWithOffset& max_ring_diff, - const int num_views,const int num_tangential_poss); - - inline virtual float get_tantheta(const Bin&) const; - - inline float get_phi(const Bin&) const; - - inline float get_t(const Bin&) const; - - //! Return z-coordinate of the middle of the LOR - /*! - The 0 of the z-axis is chosen in the middle of the scanner. - */ - inline float get_m(const Bin&) const; - - virtual void - get_LOR(LORInAxialAndNoArcCorrSinogramCoordinates& lor, const Bin& bin) const; - - void set_azimuthal_angle_sampling(const float angle); - - //! set new number of views, covering the same azimuthal angle range - /*! calls ProjDataInfo::set_num_views(), but makes sure that we cover the - same range of angles as before (usually, but not necessarily, 180 degrees) - by adjusting azimuthal_angle_sampling. - */ - virtual void - set_num_views(const int new_num_views); - - //! Get the azimuthal sampling (in radians) - inline float get_azimuthal_angle_sampling() const; - virtual inline float get_sampling_in_t(const Bin&) const; - virtual inline float get_sampling_in_m(const Bin&) const; - - //! Get the axial sampling (e.g in z_direction) - /*! - \warning The implementation of this function currently assumes that the axial - sampling is equal to the ring spacing for non-spanned data - (i.e. no axial compression), while it is half the - ring spacing for spanned data. - */ - inline float get_axial_sampling(int segment_num) const; - - //! Get average ring difference for the given segment - inline float get_average_ring_difference(int segment_num) const; - //! Get minimum ring difference for the given segment - inline int get_min_ring_difference(int segment_num) const; - //! Get maximum ring difference for the given segment - inline int get_max_ring_difference(int segment_num) const; - - //! Set minimum ring difference - void set_min_ring_difference(int min_ring_diff_v, int segment_num); - //! Set maximum ring difference - void set_max_ring_difference(int max_ring_diff_v, int segment_num); - - virtual void set_num_axial_poss_per_segment(const VectorWithOffset& num_axial_poss_per_segment); - virtual void set_min_axial_pos_num(const int min_ax_pos_num, const int segment_num); - virtual void set_max_axial_pos_num(const int max_ax_pos_num, const int segment_num); - virtual void reduce_segment_range(const int min_segment_num, const int max_segment_num); - - //! Set detector ring radius for all views - inline void set_ring_radii_for_all_views(const VectorWithOffset& new_ring_radius); - - //! Get detector ring radius for all views - inline VectorWithOffset get_ring_radii_for_all_views() const; - - //! Get detector ring radius - inline float get_ring_radius() const; - inline float get_ring_radius( const int view_num) const; - - //! Get detector ring spacing - inline float get_ring_spacing() const; - - //! Set detector ring spacing - void set_ring_spacing(float ring_spacing_v); - - //! Get the mashing factor, i.e. how many 'original' views are combined. - /*! mashing factor = Ndet/(2Nview) - This gets the result by comparing the number of detectors in the scanner_ptr - with the actual number of views. - \warning In the debug version, it is checked with an assert() that the number of - detectors is an even multiple of the number of views. This is not checked in - the normal version though. - */ - inline int get_view_mashing_factor() const; - - //! Find which segment a particular ring difference belongs to - /*! - \return Succeeded::yes when a corresponding segment was found. - */ - inline Succeeded - get_segment_num_for_ring_difference(int& segment_num, const int ring_diff) const; - - //! Find to which segment and axial position a ring pair contributes - /*! - \a ring1, \a ring2 have to be between 0 and scanner.get_num_rings()-1. - \return Succeeded::yes when a corresponding segment was found. - \warning axial_pos_num returned might be outside the actual range in the proj_data_info. - - \For CTI data with span, this essentially implements a 'michelogram'. - - \warning Current implementation assumes that the axial positions start from 0 for - the first ring-pair in the segment. - - \warning The implementation of this function currently assumes that the axial - sampling is equal to the ring spacing for non-spanned data - (i.e. no axial compression), while it is half the ring spacing for spanned data. - */ - inline Succeeded - get_segment_axial_pos_num_for_ring_pair(int& segment_num, - int& axial_pos_num, - const int ring1, - const int ring2) const; - - //! Find all ring pairs that contribute to a segment and axial position - /*! - \a ring1, \a ring2 will be between 0 and scanner.get_num_rings()-1. - - \warning The implementation of this function currently assumes that the axial - sampling is equal to the ring spacing for non-spanned data - (i.e. no axial compression), while it is half the ring spacing for spanned data. - */ - inline const RingNumPairs& - get_all_ring_pairs_for_segment_axial_pos_num(const int segment_num, - const int axial_pos_num) const; - //! Find the number of ring pairs that contribute to a segment and axial position - /*! - \warning The implementation of this function currently assumes that the axial - sampling is equal to the ring spacing for non-spanned data - (i.e. no axial compression), while it is half the ring spacing for spanned data. - */ - inline unsigned - get_num_ring_pairs_for_segment_axial_pos_num(const int segment_num, - const int axial_pos_num) const; - - //! Find a ring pair that contributes to a segment and axial position - /*! - \a ring1, \a ring2 will be between 0 and scanner.get_num_rings()-1. - - \warning Currently only works when no axial compression is used for the segment (i.e. - min_ring_diff = max_ring_diff). Otherwise, a error() will be called. - - \warning The implementation of this function currently assumes that the axial - sampling is equal to the ring spacing for non-spanned data - (i.e. no axial compression), while it is half the ring spacing for spanned data. - */ - void - get_ring_pair_for_segment_axial_pos_num(int& ring1, - int& ring2, - const int segment_num, - const int axial_pos_num) const; - - virtual std::string parameter_info() const; - -protected: - - //! a variable that is set if the data corresponds to physical rings in the scanner - /*! This is (only) used to prevent get_segment_axial_pos_num_for_ring_pair() et al - to go wild. Indeed, for cases where there's cylindrical sampling, but not - really any physical rings associated to the sampling, those functions will - return invalid information. - - The prime case where this is used is for data corresponding to (nearly) - continuous detectors, such as DHCI systems, or the HiDAC. - - Ideally, this would be done by having a separate class for such systems which - does not contain the ring-difference et al information. This seems to make - the hierarchy too complicated though. - - \bug The value of this variable is currently set by checking if the scanner - is a HiDAC scanner. This needs to be changed. - if scanner is not HiDAC this variable is set to 1 otherwise to 0 (see constructor) - */ - bool sampling_corresponds_to_physical_rings; - -protected: - virtual bool blindly_equals(const root_type * const) const = 0; - -private: - float azimuthal_angle_sampling; - VectorWithOffset ring_radius; - float ring_spacing; - VectorWithOffset min_ring_diff; - VectorWithOffset max_ring_diff; - - /* - Next members have to be mutable as they can be modified by const member - functions. We need this because of the presence of set_min_ring_difference() - which invalidates these precalculated arrays. - If your compiler does not support mutable (and you don't want to upgrade - it to something more sensible), your best bet is to remove the - set_*ring_difference functions, and move the content of - initialise_ring_diff_arrays() to the constructor. (Not recommended!) - */ - - //! This member will signal if the arrays below contain sensible info or not - mutable bool ring_diff_arrays_computed; - - //! This member stores the offsets used in get_m() - /* - //! warning This is not used in block geometry. m is found directly from lors. - mutable VectorWithOffset m_offset; - */ - - //! This member stores the offsets used in get_segment_axial_pos_num_for_ring_pair() - mutable VectorWithOffset ax_pos_num_offset; - - //! This member stores a table converting ring differences to segment numbers - mutable VectorWithOffset ring_diff_to_segment_num; - - //! This member stores a table converting segment/axial_pos to ring1+ring2 - mutable VectorWithOffset > segment_axial_pos_to_ring1_plus_ring2; - - //! This function sets all of the above - void initialise_ring_diff_arrays() const; - - //! This function guarantees that ring_diff_arrays will be set but checks first if was done already - /*! This function is OPENMP thread-safe */ - inline void initialise_ring_diff_arrays_if_not_done_yet() const; - - //! This function checks if max_ring_diff is different from min_ring_diff (set to 2). - /*! in case of difference, there are 2 ax_pos per ring, i.e. an ax_pos between each two rings */ - inline int get_num_axial_poss_per_ring_inc(const int segment_num) const; - - //! This member will signal if the array below contain sensible info or not - mutable bool segment_axial_pos_to_ring_pair_allocated; - - //! This member stores a table used by get_all_ring_pairs_for_segment_axial_pos_num() - mutable VectorWithOffset< VectorWithOffset < shared_ptr > > - segment_axial_pos_to_ring_pair; - - //! allocate table - void allocate_segment_axial_pos_to_ring_pair() const; - - //! initialise one element of the above table - void compute_segment_axial_pos_to_ring_pair(const int segment_num, const int axial_pos_num) const; - - //! to be used in get LOR - virtual void - find_cartesian_coordinates_of_detection(CartesianCoordinate3D& coord_1, - CartesianCoordinate3D& coord_2, - const Bin& bin) const = 0; - -}; - +using ProjDataInfoBlocksOnCylindrical = ProjDataInfoGeneric; END_NAMESPACE_STIR - -#include "stir/ProjDataInfoBlocksOnCylindrical.inl" - #endif diff --git a/src/include/stir/ProjDataInfoBlocksOnCylindrical.inl b/src/include/stir/ProjDataInfoBlocksOnCylindrical.inl deleted file mode 100644 index f92bcc286..000000000 --- a/src/include/stir/ProjDataInfoBlocksOnCylindrical.inl +++ /dev/null @@ -1,296 +0,0 @@ - -/* -Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics - -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 projdata - - \brief Implementation of inline functions of class stir::ProjDataInfoBlocksOnCylindrical - - \author Parisa Khateri - -*/ -// for sqrt -#include -#include "stir/Bin.h" -#include "stir/Succeeded.h" -#include "stir/LORCoordinates.h" -#include "stir/is_null_ptr.h" -#include - -START_NAMESPACE_STIR - -void -ProjDataInfoBlocksOnCylindrical:: -initialise_ring_diff_arrays_if_not_done_yet() const -{ - // for efficiency reasons, use "Double-Checked-Locking(DCL) pattern" with OpenMP atomic operation - // OpenMP v3.1 or later required - // thanks to yohjp: http://stackoverflow.com/questions/27975737/how-to-handle-cached-data-structures-with-multi-threading-e-g-openmp -#if defined(STIR_OPENMP) && _OPENMP >=201012 - bool initialised; -#pragma omp atomic read - initialised = ring_diff_arrays_computed; - - if (!initialised) -#endif - { -#if defined(STIR_OPENMP) -#pragma omp critical(PROJDATAINFOCYLINDRICALRINGDIFFARRAY) -#endif - { - if (!ring_diff_arrays_computed) - initialise_ring_diff_arrays(); - } - } -} - -//! find phi from correspoding lor -float -ProjDataInfoBlocksOnCylindrical::get_phi(const Bin& bin)const -{ - LORInAxialAndNoArcCorrSinogramCoordinates lor; - get_LOR(lor, bin); - if (bin.view_num()==0 && lor.phi()>0.1) - return lor.phi()-_PI; - return lor.phi(); -} - -/*! warning In block geometry m is calculated directly from lor while in - cylindrical geometry m is calculated using m_offset and axial_sampling -*/ -float -ProjDataInfoBlocksOnCylindrical::get_m(const Bin& bin) const -{ - LORInAxialAndNoArcCorrSinogramCoordinates lor; - get_LOR(lor, bin); - return (lor.z1() + lor.z2())/2.; -} - -float -ProjDataInfoBlocksOnCylindrical::get_t(const Bin& bin) const -{ - return - get_m(bin)*get_costheta(bin); -} - -/* - theta is copolar angle of normal to projection plane with z axis, i.e. copolar angle of lor with z axis. - tan (theta) = dz/sqrt(dx2+dy2) - cylindrical geometry: - delta_z = delta_ring * ring spacing - Block geometry: - delta_z is calculated from lor -*/ -float -ProjDataInfoBlocksOnCylindrical::get_tantheta(const Bin& bin) const -{ - CartesianCoordinate3D _p1; - CartesianCoordinate3D _p2; - find_cartesian_coordinates_of_detection(_p1, _p2, bin); - CartesianCoordinate3D p2_minus_p1 = _p2 - _p1; - return p2_minus_p1.z() / (sqrt(square(p2_minus_p1.x())+square(p2_minus_p1.y()))); -} - -float -ProjDataInfoBlocksOnCylindrical::get_sampling_in_m(const Bin& bin) const -{ - return get_axial_sampling(bin.segment_num()); -} - -/* - warning Theta is not uniform anymore, so sampling in t does not make sense anymore. - Sampling parameters remained unchanged to be consistent with cylindrical version. -*/ -float -ProjDataInfoBlocksOnCylindrical::get_sampling_in_t(const Bin& bin) const -{ - return get_axial_sampling(bin.segment_num())*get_costheta(bin); -} - -int -ProjDataInfoBlocksOnCylindrical:: -get_num_axial_poss_per_ring_inc(const int segment_num) const -{ - return - max_ring_diff[segment_num] != min_ring_diff[segment_num] ? - 2 : 1; -} - - -float -ProjDataInfoBlocksOnCylindrical::get_azimuthal_angle_sampling() const -{return azimuthal_angle_sampling;} - - -float -ProjDataInfoBlocksOnCylindrical::get_axial_sampling(int segment_num) const -{ - return ring_spacing/get_num_axial_poss_per_ring_inc(segment_num); -} - -float -ProjDataInfoBlocksOnCylindrical::get_average_ring_difference(int segment_num) const -{ - // KT 05/07/2001 use float division here. - // In any reasonable case, min+max_ring_diff will be even. - // But some day, an unreasonable case will walk in. - return (min_ring_diff[segment_num] + max_ring_diff[segment_num])/2.F; -} - -int -ProjDataInfoBlocksOnCylindrical::get_min_ring_difference(int segment_num) const -{ return min_ring_diff[segment_num]; } - -int -ProjDataInfoBlocksOnCylindrical::get_max_ring_difference(int segment_num) const -{ return max_ring_diff[segment_num]; } - -float -ProjDataInfoBlocksOnCylindrical::get_ring_radius() const -{ - if (this->ring_radius.get_min_index()!=0 || this->ring_radius.get_max_index()!=0) - { - // check if all elements are equal - for (VectorWithOffset::const_iterator iter=this->ring_radius.begin(); iter!= this->ring_radius.end(); ++iter) - { - if (*iter != *this->ring_radius.begin()) - error("get_ring_radius called for non-circular ring"); - } - } - return *this->ring_radius.begin(); -} - -void -ProjDataInfoBlocksOnCylindrical::set_ring_radii_for_all_views(const VectorWithOffset& new_ring_radius) -{ - if (new_ring_radius.get_min_index() != this->get_min_view_num() || - new_ring_radius.get_max_index() != this->get_max_view_num()) - { - error("error set_ring_radii_for_all_views: you need to use correct range of view numbers"); - } - - this->ring_radius = new_ring_radius; -} - -VectorWithOffset -ProjDataInfoBlocksOnCylindrical::get_ring_radii_for_all_views() const -{ - if (this->ring_radius.get_min_index()==0 && this->ring_radius.get_max_index()==0) - { - VectorWithOffset out(this->get_min_view_num(), this->get_max_view_num()); - out.fill(this->ring_radius[0]); - return out; - } - else - return this->ring_radius; -} - -float -ProjDataInfoBlocksOnCylindrical::get_ring_radius( const int view_num) const -{ - if (this->ring_radius.get_min_index()==0 && this->ring_radius.get_max_index()==0) - return ring_radius[0]; - else - return ring_radius[view_num]; -} - -float -ProjDataInfoBlocksOnCylindrical::get_ring_spacing() const -{ return ring_spacing;} - -int -ProjDataInfoBlocksOnCylindrical:: -get_view_mashing_factor() const -{ - // KT 10/05/2002 new assert - assert(get_scanner_ptr()->get_num_detectors_per_ring() > 0); - // KT 10/05/2002 moved assert here from constructor - assert(get_scanner_ptr()->get_num_detectors_per_ring() % (2*get_num_views()) == 0); - // KT 28/11/2001 do not pre-store anymore as set_num_views would invalidate it - return get_scanner_ptr()->get_num_detectors_per_ring() / (2*get_num_views()); -} - -Succeeded -ProjDataInfoBlocksOnCylindrical:: -get_segment_num_for_ring_difference(int& segment_num, const int ring_diff) const -{ - if (!sampling_corresponds_to_physical_rings) - return Succeeded::no; - - // check currently necessary as reduce_segment does not reduce the size of the ring_diff arrays - if (ring_diff > get_max_ring_difference(get_max_segment_num()) || - ring_diff < get_min_ring_difference(get_min_segment_num())) - return Succeeded::no; - - if (!ring_diff_arrays_computed) - initialise_ring_diff_arrays(); - - segment_num = ring_diff_to_segment_num[ring_diff]; - // warning: relies on initialise_ring_diff_arrays to set invalid ring_diff to a too large segment_num - if (segment_num <= get_max_segment_num()) - return Succeeded::yes; - else - return Succeeded::no; -} - - -Succeeded -ProjDataInfoBlocksOnCylindrical:: -get_segment_axial_pos_num_for_ring_pair(int& segment_num, - int& ax_pos_num, - const int ring1, - const int ring2) const -{ - assert(0<=ring1); - assert(ring1get_num_rings()); - assert(0<=ring2); - assert(ring2get_num_rings()); - - // KT 01/08/2002 swapped rings - if (get_segment_num_for_ring_difference(segment_num, ring2-ring1) == Succeeded::no) - return Succeeded::no; - - // see initialise_ring_diff_arrays() for some info - ax_pos_num = (ring1 + ring2 - ax_pos_num_offset[segment_num])* - get_num_axial_poss_per_ring_inc(segment_num)/2; - return Succeeded::yes; -} - -const ProjDataInfoBlocksOnCylindrical::RingNumPairs& -ProjDataInfoBlocksOnCylindrical:: -get_all_ring_pairs_for_segment_axial_pos_num(const int segment_num, - const int axial_pos_num) const -{ - if (!ring_diff_arrays_computed) - initialise_ring_diff_arrays(); - if (is_null_ptr(segment_axial_pos_to_ring_pair[segment_num][axial_pos_num])) - compute_segment_axial_pos_to_ring_pair(segment_num, axial_pos_num); - return *segment_axial_pos_to_ring_pair[segment_num][axial_pos_num]; -} - -unsigned -ProjDataInfoBlocksOnCylindrical:: -get_num_ring_pairs_for_segment_axial_pos_num(const int segment_num, - const int axial_pos_num) const -{ - return - static_cast( - this->get_all_ring_pairs_for_segment_axial_pos_num(segment_num,axial_pos_num).size()); -} - -END_NAMESPACE_STIR diff --git a/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx b/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx index 5e30e2f01..e5b234b4c 100644 --- a/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx +++ b/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx @@ -114,6 +114,7 @@ find_relation_between_coordinate_systems(int& num_planes_per_scanner_ring, } } +#if 0 // disabled as currently the same as ProjDataInfoGeneric //overload for block geometry static void find_relation_between_coordinate_systems(int& num_planes_per_scanner_ring, @@ -180,6 +181,7 @@ find_relation_between_coordinate_systems(int& num_planes_per_scanner_ring, + num_planes_per_scanner_ring*delta)/2; } } +#endif // overloading for generic case static void From 66201e6dcd213f98933cf53b2f8d97cd8cb178ed Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Fri, 12 Nov 2021 12:03:56 +0000 Subject: [PATCH 32/81] update copyright/authorship for blocks-code A lot of the code is copy-paste from existing stuff, so I've inserted the original copyright statements. Also added Viet Dao in a few places. --- ...ojDataInfoBlocksOnCylindricalNoArcCorr.cxx | 24 ++++++++---------- src/buildblock/ProjDataInfoGeneric.cxx | 21 ++++++++-------- .../ProjDataInfoGenericNoArcCorr.cxx | 24 ++++++++---------- ...ProjDataInfoBlocksOnCylindricalNoArcCorr.h | 18 ++++++------- ...ojDataInfoBlocksOnCylindricalNoArcCorr.inl | 19 ++++++-------- src/include/stir/ProjDataInfoGeneric.h | 24 +++++++++--------- src/include/stir/ProjDataInfoGeneric.inl | 25 +++++++++++-------- .../stir/ProjDataInfoGenericNoArcCorr.h | 18 ++++++------- .../stir/ProjDataInfoGenericNoArcCorr.inl | 25 ++++++++----------- src/scatter_buildblock/ScatterSimulation.cxx | 2 ++ .../scatter_detection_modelling.cxx | 3 ++- 11 files changed, 97 insertions(+), 106 deletions(-) diff --git a/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx b/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx index 221365f6a..46fa0e59f 100644 --- a/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx +++ b/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx @@ -1,18 +1,14 @@ /* -Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics - -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. + Copyright (C) 2000- 2007-10-08, Hammersmith Imanet Ltd + Copyright (C) 2011-07-01 - 2011, Kris Thielemans + Copyright (C) 2017, ETH Zurich, Institute of Particle Physics and Astrophysics + Copyright (C) 2018, University College London + Copyright (C) 2018, University of Leeds + This file is part of STIR. + + SPDX-License-Identifier: Apache-2.0 + See STIR/LICENSE.txt for details */ /*! @@ -22,6 +18,8 @@ limitations under the License. \brief Non-inline implementations of stir::ProjDataInfoBlocksOnCylindricalNoArcCorr + \author Kris Thielemans + \author Palak Wadhwa \author Parisa Khateri */ diff --git a/src/buildblock/ProjDataInfoGeneric.cxx b/src/buildblock/ProjDataInfoGeneric.cxx index b362eb34d..72b1ade10 100644 --- a/src/buildblock/ProjDataInfoGeneric.cxx +++ b/src/buildblock/ProjDataInfoGeneric.cxx @@ -1,17 +1,15 @@ /* -Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics + Copyright (C) 2000 PARAPET partners + Copyright (C) 2000 - 2009-10-18 Hammersmith Imanet Ltd + Copyright (C) 2011, Kris Thielemans + Copyright (C) 2017 ETH Zurich, Institute of Particle Physics and Astrophysics + Copyright (C) 2013, 2018, 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 + This file is part of STIR. - http://www.apache.org/licenses/LICENSE-2.0 + SPDX-License-Identifier: Apache-2.0 AND License-ref-PARAPET-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. + See STIR/LICENSE.txt for details */ /*! @@ -21,6 +19,9 @@ limitations under the License. \brief Non-inline implementations of stir::ProjDataInfoGeneric + \author Kris Thielemans + \author Sanida Mustafovic + \author PARAPET project \author Parisa Khateri \author Michael Roethlisberger */ diff --git a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx index e3014b27c..30864bed0 100644 --- a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx +++ b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx @@ -1,17 +1,13 @@ /* -Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics - -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. + Copyright (C) 2000- 2007-10-08, Hammersmith Imanet Ltd + Copyright (C) 2011-07-01 - 2011, Kris Thielemans + Copyright (C) 2017 ETH Zurich, Institute of Particle Physics and Astrophysics + Copyright (C) 2018, University College London + Copyright (C) 2018, University of Leeds + This file is part of STIR. + + SPDX-License-Identifier: Apache-2.0 + See STIR/LICENSE.txt for details */ /*! @@ -21,6 +17,8 @@ limitations under the License. \brief Implementation of non-inline functions of class stir::ProjDataInfoGenericNoArcCorr + \author Kris Thielemans + \author Palak Wadhwa \author Parisa Khateri \author Michael Roethlisberger */ diff --git a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h index 7b10678b9..06955ac94 100644 --- a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h +++ b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h @@ -1,18 +1,13 @@ /* -Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics + Copyright (C) 2000- 2011-06-24, Hammersmith Imanet Ltd + Copyright (C) 2011-07-01 - 2011, Kris Thielemans + Copyright (C) 2017, ETH Zurich, Institute of Particle Physics and Astrophysics + This file is part of STIR. -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 + SPDX-License-Identifier: Apache-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. + See STIR/LICENSE.txt for details */ /*! @@ -21,6 +16,7 @@ limitations under the License. \brief Declaration of class stir::ProjDataInfoBlocksOnCylindricalNoArcCorr + \author Kris Thielemans \author Parisa Khateri */ diff --git a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl index bc3ee9890..5ba884ac6 100644 --- a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl +++ b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl @@ -1,18 +1,13 @@ /* -Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics + Copyright (C) 2000- 2005, Hammersmith Imanet Ltd + Copyright (C) 2017, ETH Zurich, Institute of Particle Physics and Astrophysics + Copyright (C) 2021, University of Leeds + This file is part of STIR. -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 + SPDX-License-Identifier: Apache-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. + See STIR/LICENSE.txt for details */ /*! @@ -22,7 +17,9 @@ limitations under the License. \brief Implementation of inline functions of class stir::ProjDataInfoBlocksOnCylindricalNoArcCorr + \author Kris Thielemans \author Parisa Khateri + \author Viet Ahn Dao */ #include "stir/Bin.h" diff --git a/src/include/stir/ProjDataInfoGeneric.h b/src/include/stir/ProjDataInfoGeneric.h index 6971a8076..0109fe02b 100644 --- a/src/include/stir/ProjDataInfoGeneric.h +++ b/src/include/stir/ProjDataInfoGeneric.h @@ -1,19 +1,15 @@ /* -Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics + Copyright (C) 2000 PARAPET partners + Copyright (C) 2000-2009, Hammersmith Imanet Ltd + Copyright (C) 2013, University College London + Copyright (C) 2013, Institute for Bioengineering of Catalonia + Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics + This file is part of STIR. -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 + SPDX-License-Identifier: Apache-2.0 AND License-ref-PARAPET-license - 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. + See STIR/LICENSE.txt for details */ - /*! \file @@ -22,6 +18,10 @@ limitations under the License. \author Parisa Khateri \author Michael Roethlisberger + \author Sanida Mustafovic + \author Kris Thielemans + \author Berta Marti Fuster + \author PARAPET project */ #ifndef __stir_ProjDataInfoGeneric_H__ #define __stir_ProjDataInfoGeneric_H__ diff --git a/src/include/stir/ProjDataInfoGeneric.inl b/src/include/stir/ProjDataInfoGeneric.inl index 9dda5f755..247d4ee16 100644 --- a/src/include/stir/ProjDataInfoGeneric.inl +++ b/src/include/stir/ProjDataInfoGeneric.inl @@ -1,17 +1,16 @@ /* -Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics + Copyright (C) 2000 PARAPET partners + Copyright (C) 2000-2003, Hammersmith Imanet Ltd + Copyright (C) 2013, University College London + Copyright (C) 2013, Institute for Bioengineering of Catalonia + Copyright (C) 2017, ETH Zurich, Institute of Particle Physics and Astrophysics + Copyright (C) 2018, 2021 University of Leeds -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 + This file is part of STIR. - http://www.apache.org/licenses/LICENSE-2.0 + SPDX-License-Identifier: Apache-2.0 AND License-ref-PARAPET-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. + See STIR/LICENSE.txt for details */ /*! @@ -21,8 +20,14 @@ limitations under the License. \brief Implementation of inline functions of class stir::ProjDataInfoGeneric + \author Sanida Mustafovic + \author Kris Thielemans + \author Palak Wadhwa + \author Berta Marti Fuster + \author PARAPET project \author Parisa Khateri \author Michael Roethlisberger + \author Viet Ahn Dao */ // for sqrt diff --git a/src/include/stir/ProjDataInfoGenericNoArcCorr.h b/src/include/stir/ProjDataInfoGenericNoArcCorr.h index 0d1c01b4e..b1fea297a 100644 --- a/src/include/stir/ProjDataInfoGenericNoArcCorr.h +++ b/src/include/stir/ProjDataInfoGenericNoArcCorr.h @@ -1,17 +1,12 @@ /* -Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics + Copyright (C) 2000- 2011-06-24, Hammersmith Imanet Ltd + Copyright (C) 2011-07-01 - 2011, Kris Thielemans + Copyright (C) 2017, ETH Zurich, Institute of Particle Physics and Astrophysics + This file is part of STIR. -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 + SPDX-License-Identifier: Apache-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. + See STIR/LICENSE.txt for details */ /*! @@ -20,6 +15,7 @@ limitations under the License. \brief Declaration of class stir::ProjDataInfoGenericNoArcCorr + \author Kris Thielemans \author Parisa Khateri \author Michael Roethlisberger */ diff --git a/src/include/stir/ProjDataInfoGenericNoArcCorr.inl b/src/include/stir/ProjDataInfoGenericNoArcCorr.inl index 7d06972bf..a05b31b48 100644 --- a/src/include/stir/ProjDataInfoGenericNoArcCorr.inl +++ b/src/include/stir/ProjDataInfoGenericNoArcCorr.inl @@ -1,17 +1,12 @@ /* -Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics + Copyright (C) 2000- 2005, Hammersmith Imanet Ltd + Copyright (C) 2017, ETH Zurich, Institute of Particle Physics and Astrophysics + Copyright (C) 2021, University of Leeds + This file is part of STIR. -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 + SPDX-License-Identifier: Apache-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. + See STIR/LICENSE.txt for details */ /*! @@ -19,10 +14,12 @@ limitations under the License. \file \ingroup projdata - \brief Implementation of inline functions of class ProjDataInfoGenericNoArcCorr + \brief Implementation of inline functions of class stir::ProjDataInfoGenericNoArcCorr - \author Parisa Khateri - \author Michael Roethlisberger + \author Kris Thielemans + \author Parisa Khateri + \author Michael Roethlisberger + \author Viet Ahn Dao */ diff --git a/src/scatter_buildblock/ScatterSimulation.cxx b/src/scatter_buildblock/ScatterSimulation.cxx index 28eb14e69..a3bb4adac 100644 --- a/src/scatter_buildblock/ScatterSimulation.cxx +++ b/src/scatter_buildblock/ScatterSimulation.cxx @@ -2,6 +2,7 @@ Copyright (C) 2004 - 2009 Hammersmith Imanet Ltd Copyright (C) 2013 - 2016, 2019, 2020 University College London Copyright (C) 2018-2019, University of Hull + Copyright (C) 2021, University of Leeds This file is part of STIR. SPDX-License-Identifier: Apache-2.0 @@ -16,6 +17,7 @@ \author Nikos Efthimiou \author Charalampos Tsoumpas \author Kris Thielemans + \author Viet Ahn Dao */ #include "stir/scatter/ScatterSimulation.h" #include "stir/ViewSegmentNumbers.h" diff --git a/src/scatter_buildblock/scatter_detection_modelling.cxx b/src/scatter_buildblock/scatter_detection_modelling.cxx index a44ece939..25edc73f0 100644 --- a/src/scatter_buildblock/scatter_detection_modelling.cxx +++ b/src/scatter_buildblock/scatter_detection_modelling.cxx @@ -2,6 +2,7 @@ // /* Copyright (C) 2004- 2009, Hammersmith Imanet + Copyright (C) 2021, University of Leeds This file is part of STIR. SPDX-License-Identifier: Apache-2.0 @@ -17,7 +18,7 @@ \author Pablo Aguiar \author Nikolaos Dikaios \author Kris Thielemans - + \author Viet Ahn Dao */ #include "stir/scatter/ScatterSimulation.h" From e35385cafa56b4200c2fd6722510facc0739f555 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Fri, 12 Nov 2021 12:22:28 +0000 Subject: [PATCH 33/81] minor updates to blocks code copied some "recent" changes from the cylindrical hierarchy to the generic/blocks one --- ...ojDataInfoBlocksOnCylindricalNoArcCorr.cxx | 14 ++++++++------ .../ProjDataInfoGenericNoArcCorr.cxx | 19 +++++++++++-------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx b/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx index 46fa0e59f..3792ba5f4 100644 --- a/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx +++ b/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx @@ -104,8 +104,8 @@ operator==(const self_type& that) const if (!base_type::blindly_equals(&that)) return false; return - this->ring_radius == that.ring_radius && - this->angular_increment == that.angular_increment; + fabs(this->ring_radius - that.ring_radius) < 0.05F && + fabs(this->angular_increment - that.angular_increment) < 0.05F; } bool @@ -145,6 +145,8 @@ ProjDataInfoBlocksOnCylindricalNoArcCorr::parameter_info() const This is ok on SUNs (gcc, but probably SUNs cc as well), Parsytec (gcc), Pentium (gcc, VC++) and probably every other system which uses the 2-complement convention. + + Update: compile time assert is implemented. */ /*! @@ -170,8 +172,8 @@ void ProjDataInfoBlocksOnCylindricalNoArcCorr:: initialise_uncompressed_view_tangpos_to_det1det2() const { - assert(-1 >> 1 == -1); - assert(-2 >> 1 == -1); + BOOST_STATIC_ASSERT(-1 >> 1 == -1); + BOOST_STATIC_ASSERT(-2 >> 1 == -1); const int num_detectors = get_scanner_ptr()->get_num_detectors_per_ring(); @@ -222,8 +224,8 @@ void ProjDataInfoBlocksOnCylindricalNoArcCorr:: initialise_det1det2_to_uncompressed_view_tangpos() const { - assert(-1 >> 1 == -1); - assert(-2 >> 1 == -1); + BOOST_STATIC_ASSERT(-1 >> 1 == -1); + BOOST_STATIC_ASSERT(-2 >> 1 == -1); const int num_detectors = get_scanner_ptr()->get_num_detectors_per_ring(); diff --git a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx index 30864bed0..4df19592b 100644 --- a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx +++ b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx @@ -38,6 +38,8 @@ #include #endif +#include + #ifndef STIR_NO_NAMESPACES using std::endl; using std::ends; @@ -100,8 +102,8 @@ operator==(const self_type& that) const if (!base_type::blindly_equals(&that)) return false; return - this->ring_radius == that.ring_radius && - this->angular_increment == that.angular_increment; + fabs(this->ring_radius - that.ring_radius) < 0.05F && + fabs(this->angular_increment - that.angular_increment) < 0.05F; } bool @@ -141,6 +143,8 @@ ProjDataInfoGenericNoArcCorr::parameter_info() const This is ok on SUNs (gcc, but probably SUNs cc as well), Parsytec (gcc), Pentium (gcc, VC++) and probably every other system which uses the 2-complement convention. + + Update: compile time assert is implemented. */ /*! @@ -165,8 +169,8 @@ void ProjDataInfoGenericNoArcCorr:: initialise_uncompressed_view_tangpos_to_det1det2() const { - assert(-1 >> 1 == -1); - assert(-2 >> 1 == -1); + BOOST_STATIC_ASSERT(-1 >> 1 == -1); + BOOST_STATIC_ASSERT(-2 >> 1 == -1); const int num_detectors = get_scanner_ptr()->get_num_detectors_per_ring(); @@ -217,8 +221,8 @@ void ProjDataInfoGenericNoArcCorr:: initialise_det1det2_to_uncompressed_view_tangpos() const { - assert(-1 >> 1 == -1); - assert(-2 >> 1 == -1); + BOOST_STATIC_ASSERT(-1 >> 1 == -1); + BOOST_STATIC_ASSERT(-2 >> 1 == -1); const int num_detectors = get_scanner_ptr()->get_num_detectors_per_ring(); @@ -327,8 +331,7 @@ ProjDataInfoGenericNoArcCorr:: get_all_det_pos_pairs_for_bin(vector >& dps, const Bin& bin) const { - if (!uncompressed_view_tangpos_to_det1det2_initialised) - initialise_uncompressed_view_tangpos_to_det1det2(); + this->initialise_uncompressed_view_tangpos_to_det1det2_if_not_done_yet(); dps.resize(get_num_det_pos_pairs_for_bin(bin)); From bb0b5582a17850af308799d438ab62efda8dd6dd Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Fri, 12 Nov 2021 19:15:58 +0000 Subject: [PATCH 34/81] Derive ProjDataInfoBlocksOnCylindricalNoArcCorr from Generic A lot of functionality was identical (copy-paste). I've now left only the functions that actually differ. --- ...ojDataInfoBlocksOnCylindricalNoArcCorr.cxx | 262 +----------------- ...ProjDataInfoBlocksOnCylindricalNoArcCorr.h | 166 +---------- ...ojDataInfoBlocksOnCylindricalNoArcCorr.inl | 199 ------------- .../stir/ProjDataInfoGenericNoArcCorr.h | 6 +- 4 files changed, 16 insertions(+), 617 deletions(-) diff --git a/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx b/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx index 3792ba5f4..9c37908f3 100644 --- a/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx +++ b/src/buildblock/ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx @@ -41,9 +41,6 @@ #ifndef STIR_NO_NAMESPACES using std::endl; using std::ends; -using std::string; -using std::pair; -using std::vector; #endif START_NAMESPACE_STIR @@ -58,15 +55,12 @@ ProjDataInfoBlocksOnCylindricalNoArcCorr(const shared_ptr scanner_ptr, const VectorWithOffset& min_ring_diff_v, const VectorWithOffset& max_ring_diff_v, const int num_views,const int num_tangential_poss) -: ProjDataInfoBlocksOnCylindrical(scanner_ptr, + : ProjDataInfoGenericNoArcCorr(scanner_ptr, + ring_radius_v, angular_increment_v, num_axial_pos_per_segment, min_ring_diff_v, max_ring_diff_v, - num_views, num_tangential_poss), - ring_radius(ring_radius_v), - angular_increment(angular_increment_v) + num_views, num_tangential_poss) { - uncompressed_view_tangpos_to_det1det2_initialised = false; - det1det2_to_uncompressed_view_tangpos_initialised = false; crystal_map.reset(new GeometryBlocksOnCylindrical(scanner_ptr)); } @@ -76,16 +70,12 @@ ProjDataInfoBlocksOnCylindricalNoArcCorr(const shared_ptr scanner_ptr, const VectorWithOffset& min_ring_diff_v, const VectorWithOffset& max_ring_diff_v, const int num_views,const int num_tangential_poss) -: ProjDataInfoBlocksOnCylindrical(scanner_ptr, +: ProjDataInfoGenericNoArcCorr(scanner_ptr, num_axial_pos_per_segment, min_ring_diff_v, max_ring_diff_v, num_views, num_tangential_poss) { assert(!is_null_ptr(scanner_ptr)); - ring_radius = scanner_ptr->get_effective_ring_radius(); - angular_increment = static_cast(_PI/scanner_ptr->get_num_detectors_per_ring()); - uncompressed_view_tangpos_to_det1det2_initialised = false; - det1det2_to_uncompressed_view_tangpos_initialised = false; crystal_map.reset(new GeometryBlocksOnCylindrical(scanner_ptr)); } @@ -103,9 +93,9 @@ operator==(const self_type& that) const { if (!base_type::blindly_equals(&that)) return false; + // TODOBLOCKS check crystal_map return - fabs(this->ring_radius - that.ring_radius) < 0.05F && - fabs(this->angular_increment - that.angular_increment) < 0.05F; + true; } bool @@ -117,7 +107,7 @@ blindly_equals(const root_type * const that_ptr) const this->operator==(static_cast(*that_ptr)); } -string +std::string ProjDataInfoBlocksOnCylindricalNoArcCorr::parameter_info() const { @@ -129,247 +119,11 @@ ProjDataInfoBlocksOnCylindricalNoArcCorr::parameter_info() const std::ostringstream s; #endif s << "ProjDataInfoBlocksOnCylindricalNoArcCorr := \n"; - s << ProjDataInfoBlocksOnCylindrical::parameter_info(); + s << base_type::parameter_info(); s << "End :=\n"; return s.str(); } -/* - TODO make compile time assert - - Warning: - this code makes use of an implementation dependent feature: - bit shifting negative ints to the right. - -1 >> 1 should be -1 - -2 >> 1 should be -1 - This is ok on SUNs (gcc, but probably SUNs cc as well), Parsytec (gcc), - Pentium (gcc, VC++) and probably every other system which uses - the 2-complement convention. - - Update: compile time assert is implemented. -*/ - -/*! - Go from sinograms to detectors. - - Because sinograms are not arc-corrected, tang_pos_num corresponds - to an angle as well. Before interleaving we have that - \verbatim - det_angle_1 = LOR_angle + bin_angle - det_angle_2 = LOR_angle + (Pi - bin_angle) - \endverbatim - (Hint: understand this first at LOR_angle=0, then realise that - other LOR_angles follow just by rotation) - - Code gets slightly intricate because: - - angles have to be defined modulo 2 Pi (so num_detectors) - - interleaving -*/ - -//! build look-up table for get_view_tangential_pos_num_for_det_num_pair() -//Parisa: changed phi assertion -void -ProjDataInfoBlocksOnCylindricalNoArcCorr:: -initialise_uncompressed_view_tangpos_to_det1det2() const -{ - BOOST_STATIC_ASSERT(-1 >> 1 == -1); - BOOST_STATIC_ASSERT(-2 >> 1 == -1); - - const int num_detectors = - get_scanner_ptr()->get_num_detectors_per_ring(); - - assert(num_detectors%2 == 0); - // check views range from 0 to Pi - /*! - warning The following assersions are slightly different from cylindrical. - because in the function get_phi(bin), get_lor is called. - */ - assert(fabs(Bin(0,0,0,0).view_num()*get_azimuthal_angle_sampling()) < 1.E-4); - assert(fabs(Bin(0,get_num_views(),0,0).view_num()*get_azimuthal_angle_sampling() - _PI) < 1.E-4); - - const int min_tang_pos_num = -(num_detectors/2)+1; - const int max_tang_pos_num = -(num_detectors/2)+num_detectors; - - if (this->get_min_tangential_pos_num() < min_tang_pos_num || - this->get_max_tangential_pos_num() > max_tang_pos_num) - { - error("The tangential_pos range (%d to %d) for this projection data is too large.\n" - "Maximum supported range is from %d to %d", - this->get_min_tangential_pos_num(), this->get_max_tangential_pos_num(), - min_tang_pos_num, max_tang_pos_num); - } - - uncompressed_view_tangpos_to_det1det2.grow(0,num_detectors/2-1); - for (int v_num=0; v_num<=num_detectors/2-1; ++v_num) - { - uncompressed_view_tangpos_to_det1det2[v_num].grow(min_tang_pos_num, max_tang_pos_num); - - for (int tp_num=min_tang_pos_num; tp_num<=max_tang_pos_num; ++tp_num) - { - /* - adapted from CTI code - Note for implementation: avoid using % with negative numbers - so add num_detectors before doing modulo num_detectors) - */ - uncompressed_view_tangpos_to_det1det2[v_num][tp_num].det1_num = - (v_num + (tp_num >> 1) + num_detectors) % num_detectors; - uncompressed_view_tangpos_to_det1det2[v_num][tp_num].det2_num = - (v_num - ( (tp_num + 1) >> 1 ) + num_detectors/2) % num_detectors; - } - } - uncompressed_view_tangpos_to_det1det2_initialised = true; -} - -void -ProjDataInfoBlocksOnCylindricalNoArcCorr:: -initialise_det1det2_to_uncompressed_view_tangpos() const -{ - BOOST_STATIC_ASSERT(-1 >> 1 == -1); - BOOST_STATIC_ASSERT(-2 >> 1 == -1); - - const int num_detectors = - get_scanner_ptr()->get_num_detectors_per_ring(); - - if (num_detectors%2 != 0) - { - error("Number of detectors per ring should be even but is %d", num_detectors); - } - if (this->get_min_view_num() != 0) - { - error("Minimum view number should currently be zero to be able to use get_view_tangential_pos_num_for_det_num_pair()"); - } - // check views range from 0 to Pi - - - // check views range from 0 to Pi - //! warning The following assersions are slightly different from cylindrical. See the above function - assert(fabs(Bin(0,0,0,0).view_num()*get_azimuthal_angle_sampling()) < 1.E-4); - assert(fabs(Bin(0,get_max_view_num()+1,0,0).view_num()*get_azimuthal_angle_sampling() - _PI) < 1.E-4); - - - //const int min_tang_pos_num = -(num_detectors/2); - //const int max_tang_pos_num = -(num_detectors/2)+num_detectors; - const int max_num_views = num_detectors/2; - - det1det2_to_uncompressed_view_tangpos.grow(0,num_detectors-1); - for (int det1_num=0; det1_num> 1) + num_detectors) % num_detectors; - - /* Now adjust ranges for view_num, tang_pos_num. - The next lines go only wrong in the singular (and irrelevant) case - det_num1 == det_num2 (when tang_pos_num == num_detectors - tang_pos_num) - - We use the combinations of the following 'symmetries' of - (tang_pos_num, view_num) == (tang_pos_num+2*num_views, view_num + num_views) - == (-tang_pos_num, view_num + num_views) - Using the latter interchanges det_num1 and det_num2, and this leaves - the LOR the same in the 2D case. However, in 3D this interchanges the rings - as well. So, we keep track of this in swap_detectors, and return its final - value. - */ - if (view_num < max_num_views) - { - if (tang_pos_num >= max_num_views) - { - tang_pos_num = num_detectors - tang_pos_num; - swap_detectors = 1; - } - else - { - swap_detectors = 0; - } - } - else - { - view_num -= max_num_views; - if (tang_pos_num >= max_num_views) - { - tang_pos_num -= num_detectors; - swap_detectors = 0; - } - else - { - tang_pos_num *= -1; - swap_detectors = 1; - } - } - - det1det2_to_uncompressed_view_tangpos[det1_num][det2_num].view_num = view_num; - det1det2_to_uncompressed_view_tangpos[det1_num][det2_num].tang_pos_num = tang_pos_num; - det1det2_to_uncompressed_view_tangpos[det1_num][det2_num].swap_detectors = swap_detectors==0; - } - } - det1det2_to_uncompressed_view_tangpos_initialised = true; -} - -unsigned int -ProjDataInfoBlocksOnCylindricalNoArcCorr:: -get_num_det_pos_pairs_for_bin(const Bin& bin) const -{ - return - get_num_ring_pairs_for_segment_axial_pos_num(bin.segment_num(), - bin.axial_pos_num())* - get_view_mashing_factor(); -} - -void -ProjDataInfoBlocksOnCylindricalNoArcCorr:: -get_all_det_pos_pairs_for_bin(vector >& dps, - const Bin& bin) const -{ - this->initialise_uncompressed_view_tangpos_to_det1det2_if_not_done_yet(); - - dps.resize(get_num_det_pos_pairs_for_bin(bin)); - - const ProjDataInfoBlocksOnCylindrical::RingNumPairs& ring_pairs = - get_all_ring_pairs_for_segment_axial_pos_num(bin.segment_num(), - bin.axial_pos_num()); - // not sure how to handle mashing with non-zero view offset... - assert(get_min_view_num()==0); - - unsigned int current_dp_num=0; - for (int uncompressed_view_num=bin.view_num()*get_view_mashing_factor(); - uncompressed_view_num<(bin.view_num()+1)*get_view_mashing_factor(); - ++uncompressed_view_num) - { - const int det1_num = - uncompressed_view_tangpos_to_det1det2[uncompressed_view_num][bin.tangential_pos_num()].det1_num; - const int det2_num = - uncompressed_view_tangpos_to_det1det2[uncompressed_view_num][bin.tangential_pos_num()].det2_num; - for (ProjDataInfoBlocksOnCylindrical::RingNumPairs::const_iterator rings_iter = ring_pairs.begin(); - rings_iter != ring_pairs.end(); - ++rings_iter) - { - assert(current_dp_num < get_num_det_pos_pairs_for_bin(bin)); - dps[current_dp_num].pos1().tangential_coord() = det1_num; - dps[current_dp_num].pos1().axial_coord() = rings_iter->first; - dps[current_dp_num].pos2().tangential_coord() = det2_num; - dps[current_dp_num].pos2().axial_coord() = rings_iter->second; - ++current_dp_num; - } - } - assert(current_dp_num == get_num_det_pos_pairs_for_bin(bin)); -} - //!warning Use crystal map Succeeded ProjDataInfoBlocksOnCylindricalNoArcCorr:: diff --git a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h index 06955ac94..bc3aca2e4 100644 --- a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h +++ b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h @@ -24,6 +24,7 @@ #define __stir_ProjDataInfoBlocksOnCylindricalNoArcCorr_H__ +#include "stir/ProjDataInfoGenericNoArcCorr.h" #include "stir/ProjDataInfoBlocksOnCylindrical.h" #include "stir/GeometryBlocksOnCylindrical.h" #include "stir/DetectionPositionPair.h" @@ -81,10 +82,10 @@ class Succeeded; described, but there is no real correspondence with detectors (for instance, a rotating system). Maybe they should be moved somewhere else? */ -class ProjDataInfoBlocksOnCylindricalNoArcCorr : public ProjDataInfoBlocksOnCylindrical +class ProjDataInfoBlocksOnCylindricalNoArcCorr : public ProjDataInfoGenericNoArcCorr { private: - typedef ProjDataInfoBlocksOnCylindrical base_type; + typedef ProjDataInfoGenericNoArcCorr base_type; #ifdef SWIG // SWIG needs this typedef to be public public: @@ -116,140 +117,8 @@ class ProjDataInfoBlocksOnCylindricalNoArcCorr : public ProjDataInfoBlocksOnCyli bool operator==(const self_type&) const; - //! Gets s coordinate in mm - /*! \warning - This does \c not take the 'interleaving' into account which is - customarily applied to raw PET data. - */ - inline virtual float get_s(const Bin&) const; - - //! Gets angular increment (in radians) - inline float get_angular_increment() const; - virtual std::string parameter_info() const; - //! \name Functions that convert between bins and detection positions - //@{ - //! This gets view_num and tang_pos_num for a particular detector pair - /*! This function makes only sense if the scanner is a full-ring scanner - with discrete detectors and there is no rotation or wobble. - - \arg view runs currently from 0 to num_views-1 - - \arg tang_pos_num is centred around 0, where 0 corresponds - to opposing detectors. The maximum range of tangential positions for any - scanner is -(num_detectors)/2&) const; - - - //! This routine gets the detector pair corresponding to a bin. - /*! - \see get_det_pair_for_view_tangential_pos_num() for - restrictions. In addition, this routine only works for span=1 data, - i.e. no axial compression. - \todo use member template for the coordT type to support continuous detectors. - \warning Will call error() if certain conditions are not met. - */ - inline void - get_det_pos_pair_for_bin(DetectionPositionPair<>&, - const Bin&) const; - - //! This routine returns the number of detector pairs that correspond to a bin - unsigned int - get_num_det_pos_pairs_for_bin(const Bin&) const; - - //! This routine fills a vector with all the detector pairs that correspond to a bin. - /*! - \see ProjDataInfoCylindrical::get_all_ring_pairs_for_segment_axial_pos_num - for restrictions. - \todo It might be possible to return some weight factors in case there is - no many-to-one correspondence between detection positions and bins - (for instance for continuous detectors, or rotating scanners, or - arc-corrected data). - */ - void - get_all_det_pos_pairs_for_bin(vector >&, - const Bin&) const; - - //! This gets Bin coordinates for a particular detector pair - /*! - \return Succeeded::yes when a corresponding segment is found - \see get_view_tangential_pos_num_for_det_num_pair() for restrictions - \obsolete - */ - inline Succeeded - get_bin_for_det_pair(Bin&, - const int det1_num, const int ring1_num, - const int det2_num, const int ring2_num) const; - - - //! This routine gets the detector pair corresponding to a bin. - /*! - \see get_det_pair_for_view_tangential_pos_num() for - restrictions. In addition, this routine only works for span=1 data, - i.e. no axial compression. - \obsolete - */ - inline void - get_det_pair_for_bin( - int& det1_num, int& ring1_num, - int& det2_num, int& ring2_num, - const Bin&) const; - - //@} - virtual Bin get_bin(const LOR&) const; @@ -268,12 +137,12 @@ class ProjDataInfoBlocksOnCylindricalNoArcCorr : public ProjDataInfoBlocksOnCyli virtual void find_cartesian_coordinates_of_detection(CartesianCoordinate3D& coord_1, CartesianCoordinate3D& coord_2, - const Bin& bin) const; + const Bin& bin) const override; void find_cartesian_coordinates_given_scanner_coordinates (CartesianCoordinate3D& coord_1, CartesianCoordinate3D& coord_2, const int Ring_A,const int Ring_B, - const int det1, const int det2) const; + const int det1, const int det2) const override; void find_bin_given_cartesian_coordinates_of_detection(Bin& bin, const CartesianCoordinate3D& coord_1, @@ -282,29 +151,6 @@ class ProjDataInfoBlocksOnCylindricalNoArcCorr : public ProjDataInfoBlocksOnCyli private: - float ring_radius; - float angular_increment; - - // used in get_view_tangential_pos_num_for_det_num_pair() - struct Det1Det2 { int det1_num; int det2_num; }; - mutable VectorWithOffset< VectorWithOffset > uncompressed_view_tangpos_to_det1det2; - mutable bool uncompressed_view_tangpos_to_det1det2_initialised; - //! build look-up table for get_view_tangential_pos_num_for_det_num_pair() - void initialise_uncompressed_view_tangpos_to_det1det2() const; - - // used in get_view_tangential_pos_num_for_det_num_pair() - // we prestore a lookup-table in terms for unmashed view/tangpos - struct ViewTangPosSwap { int view_num; int tang_pos_num; bool swap_detectors; }; - mutable VectorWithOffset< VectorWithOffset > det1det2_to_uncompressed_view_tangpos; - mutable bool det1det2_to_uncompressed_view_tangpos_initialised; - //! build look-up table for get_view_tangential_pos_num_for_det_num_pair() - void initialise_det1det2_to_uncompressed_view_tangpos() const; - - //! build look-up table unless already done before - inline void initialise_uncompressed_view_tangpos_to_det1det2_if_not_done_yet() const; - //! build look-up table unless already done before - inline void initialise_det1det2_to_uncompressed_view_tangpos_if_not_done_yet() const; - virtual bool blindly_equals(const root_type * const) const; //! used to find scanner coordinates given cartesian coordinates and vice versa @@ -314,6 +160,4 @@ class ProjDataInfoBlocksOnCylindricalNoArcCorr : public ProjDataInfoBlocksOnCyli END_NAMESPACE_STIR -#include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl" - #endif diff --git a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl index 5ba884ac6..c58678d47 100644 --- a/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl +++ b/src/include/stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.inl @@ -29,204 +29,5 @@ START_NAMESPACE_STIR -void -ProjDataInfoBlocksOnCylindricalNoArcCorr:: -initialise_uncompressed_view_tangpos_to_det1det2_if_not_done_yet() const -{ - // for efficiency reasons, use "Double-Checked-Locking(DCL) pattern" with OpenMP atomic operation - // OpenMP v3.1 or later required - // thanks to yohjp: http://stackoverflow.com/questions/27975737/how-to-handle-cached-data-structures-with-multi-threading-e-g-openmp -#if defined(STIR_OPENMP) && _OPENMP >=201012 - bool initialised; -#pragma omp atomic read - initialised = uncompressed_view_tangpos_to_det1det2_initialised; - - if (!initialised) -#endif - { -#if defined(STIR_OPENMP) -#pragma omp critical(PROJDATAINFOCYLINDRICALNOARCCORR_VIEWTANGPOS_TO_DETS) -#endif - { - if (!uncompressed_view_tangpos_to_det1det2_initialised) - initialise_uncompressed_view_tangpos_to_det1det2(); - } - } -} - -void -ProjDataInfoBlocksOnCylindricalNoArcCorr:: -initialise_det1det2_to_uncompressed_view_tangpos_if_not_done_yet() const -{ - // as above -#if defined(STIR_OPENMP) && _OPENMP >=201012 - bool initialised; -#pragma omp atomic read - initialised = det1det2_to_uncompressed_view_tangpos_initialised; - - if (!initialised) -#endif - { -#if defined(STIR_OPENMP) -#pragma omp critical(PROJDATAINFOCYLINDRICALNOARCCORR_DETS_TO_VIEWTANGPOS) -#endif - { - if (!det1det2_to_uncompressed_view_tangpos_initialised) - initialise_det1det2_to_uncompressed_view_tangpos(); - } - } -} - -/*! warning In cylindrical s is found from bin: sin(beta) = sin(tang_pos*angular_increment) - In block it is calculated directly from corresponding lor -*/ -float -ProjDataInfoBlocksOnCylindricalNoArcCorr:: -get_s(const Bin& bin) const -{ - LORInAxialAndNoArcCorrSinogramCoordinates lor; - get_LOR(lor, bin); - // REQUIREMENT: Euclidean coordinate of 3 points, a,b and c. - // CALCULATION: // Equation of a line, in parametric form, given two point a, b: p(t) = a + (b-a)*t - // // Let a,b,c be points in 2D and let a,b form a line then shortest distance between c and line ab is: - // // || (p0-p1) - [(p0-p1)*(p2-p1)/||p2-p1||^2]*(p2-p1) || - // p1 = _p1, p2=_p2 and p0=(0,0,0) - // OUTPUT: replace the ring_radius*sin(lor.beta()) with distance from o to ab. - - // Can't access coordinate of detection from this class so we have to recalculate where it is: - - CartesianCoordinate3D _p1; - CartesianCoordinate3D _p2; - find_cartesian_coordinates_of_detection(_p1, _p2, bin); - - // // Get p1-p0 and p2-p1 vector. - CartesianCoordinate3D p2_minus_p1 = _p2 - _p1; - CartesianCoordinate3D p0_minus_p1 = CartesianCoordinate3D(0,0,0) - _p1; - float p0_minus_p1_dot_p2_minus_p1 = p0_minus_p1.x()* p2_minus_p1.x() + p0_minus_p1.y()*p2_minus_p1.y(); - float p2_minus_p1_magitude = square(p2_minus_p1.x()) + square(p2_minus_p1.y()); - float x = 0; - float y = 0; - float sign = sin(lor.beta()); - if (p2_minus_p1_magitude > 0.01) - { - x = p0_minus_p1.x() - (p0_minus_p1_dot_p2_minus_p1/p2_minus_p1_magitude)*p2_minus_p1.x(); - y = p0_minus_p1.y() - (p0_minus_p1_dot_p2_minus_p1/p2_minus_p1_magitude)*p2_minus_p1.y(); - } - else - { - error("get_s(): 2 detection points are too close to each other. This indicates an internal error."); - } - float s = sqrt(square(x) + square(y)); - - if(sign < 0.0){ - return -s; - }else{ - return s; - } -} - -float -ProjDataInfoBlocksOnCylindricalNoArcCorr:: -get_angular_increment() const -{ - return angular_increment; -} - -void -ProjDataInfoBlocksOnCylindricalNoArcCorr:: -get_det_num_pair_for_view_tangential_pos_num( - int& det1_num, - int& det2_num, - const int view_num, - const int tang_pos_num) const -{ - assert(get_view_mashing_factor() == 1); - this->initialise_uncompressed_view_tangpos_to_det1det2_if_not_done_yet(); - - det1_num = uncompressed_view_tangpos_to_det1det2[view_num][tang_pos_num].det1_num; - det2_num = uncompressed_view_tangpos_to_det1det2[view_num][tang_pos_num].det2_num; -} - -bool -ProjDataInfoBlocksOnCylindricalNoArcCorr:: -get_view_tangential_pos_num_for_det_num_pair(int& view_num, - int& tang_pos_num, - const int det1_num, - const int det2_num) const -{ - assert(det1_num!=det2_num); - this->initialise_det1det2_to_uncompressed_view_tangpos_if_not_done_yet(); - - view_num = - det1det2_to_uncompressed_view_tangpos[det1_num][det2_num].view_num/get_view_mashing_factor(); - tang_pos_num = - det1det2_to_uncompressed_view_tangpos[det1_num][det2_num].tang_pos_num; - return - det1det2_to_uncompressed_view_tangpos[det1_num][det2_num].swap_detectors; -} - -Succeeded -ProjDataInfoBlocksOnCylindricalNoArcCorr:: -get_bin_for_det_pair(Bin& bin, - const int det_num1, const int ring_num1, - const int det_num2, const int ring_num2) const -{ - if (get_view_tangential_pos_num_for_det_num_pair(bin.view_num(), bin.tangential_pos_num(), det_num1, det_num2)) - return get_segment_axial_pos_num_for_ring_pair(bin.segment_num(), bin.axial_pos_num(), ring_num1, ring_num2); - else - return get_segment_axial_pos_num_for_ring_pair(bin.segment_num(), bin.axial_pos_num(), ring_num2, ring_num1); -} - -Succeeded -ProjDataInfoBlocksOnCylindricalNoArcCorr:: -get_bin_for_det_pos_pair(Bin& bin, - const DetectionPositionPair<>& dp) const -{ - return - get_bin_for_det_pair(bin, - dp.pos1().tangential_coord(), - dp.pos1().axial_coord(), - dp.pos2().tangential_coord(), - dp.pos2().axial_coord()); -} - -void -ProjDataInfoBlocksOnCylindricalNoArcCorr:: -get_det_pair_for_bin( - int& det_num1, int& ring_num1, - int& det_num2, int& ring_num2, - const Bin& bin) const -{ - get_det_num_pair_for_view_tangential_pos_num(det_num1, det_num2, bin.view_num(), bin.tangential_pos_num()); - get_ring_pair_for_segment_axial_pos_num( ring_num1, ring_num2, bin.segment_num(), bin.axial_pos_num()); -} - -void -ProjDataInfoBlocksOnCylindricalNoArcCorr:: -get_det_pos_pair_for_bin( - DetectionPositionPair<>& dp, - const Bin& bin) const -{ - //lousy work around because types don't match TODO remove! -#if 1 - int t1=dp.pos1().tangential_coord(), - a1=dp.pos1().axial_coord(), - t2=dp.pos2().tangential_coord(), - a2=dp.pos2().axial_coord(); - get_det_pair_for_bin(t1, a1, t2, a2, bin); - dp.pos1().tangential_coord()=t1; - dp.pos1().axial_coord()=a1; - dp.pos2().tangential_coord()=t2; - dp.pos2().axial_coord()=a2; - -#else - - get_det_pair_for_bin(dp.pos1().tangential_coord(), - dp.pos1().axial_coord(), - dp.pos2().tangential_coord(), - dp.pos2().axial_coord(), - bin); -#endif -} END_NAMESPACE_STIR diff --git a/src/include/stir/ProjDataInfoGenericNoArcCorr.h b/src/include/stir/ProjDataInfoGenericNoArcCorr.h index b1fea297a..37dfef76c 100644 --- a/src/include/stir/ProjDataInfoGenericNoArcCorr.h +++ b/src/include/stir/ProjDataInfoGenericNoArcCorr.h @@ -266,7 +266,7 @@ class ProjDataInfoGenericNoArcCorr : public ProjDataInfoGeneric CartesianCoordinate3D& coord_2, const Bin& bin) const; - void find_cartesian_coordinates_given_scanner_coordinates (CartesianCoordinate3D& coord_1, + virtual void find_cartesian_coordinates_given_scanner_coordinates (CartesianCoordinate3D& coord_1, CartesianCoordinate3D& coord_2, const int Ring_A,const int Ring_B, const int det1, const int det2) const; @@ -295,9 +295,9 @@ class ProjDataInfoGenericNoArcCorr : public ProjDataInfoGeneric inline void initialise_uncompressed_view_tangpos_to_det1det2_if_not_done_yet() const; //! build look-up table unless already done before inline void initialise_det1det2_to_uncompressed_view_tangpos_if_not_done_yet() const; - + protected: virtual bool blindly_equals(const root_type * const) const; - + private: //! \todo Has to be removed shared_ptr crystal_map; }; From b85cf697c3de64332b5badbf8319ad9449f8fe40 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Fri, 12 Nov 2021 23:10:42 +0000 Subject: [PATCH 35/81] ProjDataInfoGeneric: update initalisation call bring up-to-date with how it's done in the cylindrical case --- src/include/stir/ProjDataInfoGeneric.inl | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/include/stir/ProjDataInfoGeneric.inl b/src/include/stir/ProjDataInfoGeneric.inl index 247d4ee16..670714698 100644 --- a/src/include/stir/ProjDataInfoGeneric.inl +++ b/src/include/stir/ProjDataInfoGeneric.inl @@ -242,8 +242,7 @@ get_segment_num_for_ring_difference(int& segment_num, const int ring_diff) const ring_diff < get_min_ring_difference(get_min_segment_num())) return Succeeded::no; - if (!ring_diff_arrays_computed) - initialise_ring_diff_arrays(); + this->initialise_ring_diff_arrays_if_not_done_yet(); segment_num = ring_diff_to_segment_num[ring_diff]; // warning: relies on initialise_ring_diff_arrays to set invalid ring_diff to a too large segment_num @@ -280,8 +279,7 @@ ProjDataInfoGeneric:: get_all_ring_pairs_for_segment_axial_pos_num(const int segment_num, const int axial_pos_num) const { - if (!ring_diff_arrays_computed) - initialise_ring_diff_arrays(); + this->initialise_ring_diff_arrays_if_not_done_yet(); if (is_null_ptr(segment_axial_pos_to_ring_pair[segment_num][axial_pos_num])) compute_segment_axial_pos_to_ring_pair(segment_num, axial_pos_num); return *segment_axial_pos_to_ring_pair[segment_num][axial_pos_num]; From b57f472396dc46f75000203250b3d8a0a7ceaa04 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Fri, 12 Nov 2021 23:15:06 +0000 Subject: [PATCH 36/81] gitignore output of test SAFIR_genericScanner/test.sh outputs some files which are not in git --- examples/SAFIR_genericScanner/.gitignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 examples/SAFIR_genericScanner/.gitignore diff --git a/examples/SAFIR_genericScanner/.gitignore b/examples/SAFIR_genericScanner/.gitignore new file mode 100644 index 000000000..29d8de657 --- /dev/null +++ b/examples/SAFIR_genericScanner/.gitignore @@ -0,0 +1,3 @@ +test_generic_implementation* +test_forward* +test_back.*v \ No newline at end of file From 7ec9546ca5b3ff99abe6845f38f580d7a68d4612 Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Tue, 16 Nov 2021 11:43:20 +0000 Subject: [PATCH 37/81] testing Proj.get_s() =lorBlocks.s() --- src/test/test_proj_data_info.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/test_proj_data_info.cxx b/src/test/test_proj_data_info.cxx index 76d8a046e..d6a8ee2d0 100644 --- a/src/test/test_proj_data_info.cxx +++ b/src/test/test_proj_data_info.cxx @@ -688,7 +688,7 @@ ProjDataInfoTests::run_coordinate_test() if (abs(lorB.phi() - lorC1.phi()) < tolerance ) { - check_if_equal(proj_data_info_blocks_ptr->get_s(bin),proj_data_info_cyl_ptr->get_s(bin), "A get_s() from projdata Cylinder is different from Block on Cylindrical"); + check_if_equal(proj_data_info_blocks_ptr->get_s(bin),lorB.s(), "A get_s() from projdata is different from Block on Cylindrical LOR.s()"); check_if_equal(lorB.s(),lorC1.s(),"tang_pos="+ std::to_string(tang)+ " PHI-C="+ std::to_string(lorC1.phi())+ @@ -714,7 +714,7 @@ ProjDataInfoTests::run_coordinate_test() else if (abs(lorB.phi() - lorC1.phi())+_PI < tolerance || abs(lorB.phi() - lorC1.phi())-_PI < tolerance){ - check_if_equal(proj_data_info_blocks_ptr->get_s(bin),proj_data_info_cyl_ptr->get_s(bin), "B get_s() from projdata Cylinder is different from Block on Cylindrical"); + check_if_equal(proj_data_info_blocks_ptr->get_s(bin),lorB.s(), "B get_s() from projdata is different from Block on Cylindrical LOR.s()"); check_if_equal(proj_data_info_blocks_ptr->get_phi(bin), phi-_PI, "B get_phi() from projdata Cylinder is different from Block on Cylindrical"); check_if_equal(lorB.s(),-lorC1.s(),"tang_pos="+ std::to_string(tang)+ From 42bf1e98e1cfb3c7fae131fb8590b439ff07068c Mon Sep 17 00:00:00 2001 From: danieldeidda Date: Mon, 29 Nov 2021 16:37:08 +0000 Subject: [PATCH 38/81] shifting the first detector index to the first crystal in the firts bucket --- .../GeometryBlocksOnCylindrical.cxx | 35 +++++----- src/buildblock/ProjDataInfoGeneric.cxx | 2 +- .../test_blocks_on_cylindrical_projectors.cxx | 34 ++++++---- src/test/test_proj_data_info.cxx | 64 +++++++++++++------ 4 files changed, 85 insertions(+), 50 deletions(-) mode change 100644 => 100755 src/buildblock/GeometryBlocksOnCylindrical.cxx mode change 100644 => 100755 src/buildblock/ProjDataInfoGeneric.cxx mode change 100644 => 100755 src/recon_test/test_blocks_on_cylindrical_projectors.cxx mode change 100644 => 100755 src/test/test_proj_data_info.cxx diff --git a/src/buildblock/GeometryBlocksOnCylindrical.cxx b/src/buildblock/GeometryBlocksOnCylindrical.cxx old mode 100644 new mode 100755 index f4f62508e..45becfe0f --- a/src/buildblock/GeometryBlocksOnCylindrical.cxx +++ b/src/buildblock/GeometryBlocksOnCylindrical.cxx @@ -169,12 +169,22 @@ build_crystal_maps() see start_x*/ //calculate start_point to build the map. + +// estimate the angle covered by half bucket, csi + float csi=_PI/num_transaxial_buckets; + float trans_blocks_gap=transaxial_block_spacing-num_transaxial_crystals_per_block*transaxial_crystal_spacing; + float csi_minus_csiGaps=csi-(csi/transaxial_block_spacing*2)* + (transaxial_crystal_spacing/2+trans_blocks_gap); +// distance between the center of the scannner and the first crystal in the bucket, r=Reffective/cos(csi) + float r=get_scanner_ptr()->get_effective_ring_radius()/cos(csi_minus_csiGaps); + float start_z = 0; float start_y = -1*get_scanner_ptr()->get_effective_ring_radius(); - float start_x = -1*( - ((num_transaxial_blocks_per_bucket-1)/2.)*transaxial_block_spacing - + ((num_transaxial_crystals_per_block-1)/2.)*transaxial_crystal_spacing - ); //the first crystal in the bucket + float start_x = -1*r*sin(csi_minus_csiGaps);//( +// ((num_transaxial_blocks_per_bucket-1)/2.)*transaxial_block_spacing +// + ((num_transaxial_crystals_per_block-1)/2.)*transaxial_crystal_spacing +// ); //the first crystal in the bucket + stir::CartesianCoordinate3D start_point(start_z, start_y, start_x); for (int ax_block_num=0; ax_block_numget_intrinsic_azimuthal_tilt()+ + trans_bucket_num*(2*_PI)/num_transaxial_buckets+csi_minus_csiGaps; stir::Array<2, float> rotation_matrix = get_rotation_matrix(alpha); // to match index range of CartesianCoordinate3D, which is 1 to 3 diff --git a/src/buildblock/ProjDataInfoGeneric.cxx b/src/buildblock/ProjDataInfoGeneric.cxx old mode 100644 new mode 100755 index 72b1ade10..0586fe128 --- a/src/buildblock/ProjDataInfoGeneric.cxx +++ b/src/buildblock/ProjDataInfoGeneric.cxx @@ -499,7 +499,7 @@ get_LOR(LORInAxialAndNoArcCorrSinogramCoordinates& lor, find_cartesian_coordinates_of_detection(_p1, _p2, bin); LORAs2Points lor_as_2_points(_p1, _p2); - const double R = get_ring_radius(); + const double R = sqrt(max(square(_p1.x())+square(_p1.y()), square(_p2.x())+square(_p2.y()))); lor_as_2_points.change_representation(lor, R); } diff --git a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx old mode 100644 new mode 100755 index bf02277ba..d05a241b3 --- a/src/recon_test/test_blocks_on_cylindrical_projectors.cxx +++ b/src/recon_test/test_blocks_on_cylindrical_projectors.cxx @@ -106,13 +106,22 @@ BlocksTests::run_plane_symmetry_test(){ // rotate by 30 degrees phi2=30*_PI/180; - VoxelsOnCartesianGrid image2=image; + VoxelsOnCartesianGrid image2=*image.get_empty_copy(); const Array<2,float> direction2=make_array(make_1d_array(1.F,0.F,0.F), make_1d_array(0.F,cos(float(_PI)-phi2),sin(float(_PI)-phi2)), make_1d_array(0.F,-sin(float(_PI)-phi2),cos(float(_PI)-phi2))); - plane.set_direction_vectors(direction2); + + Ellipsoid + plane2(CartesianCoordinate3D(/*edge_z*/25*grid_spacing.z(), + /*edge_y*/91*grid_spacing.y(), + /*edge_x*/5*grid_spacing.x()), + /*centre*/CartesianCoordinate3D((image.get_min_index()+image.get_max_index())/2*grid_spacing.z(), + 0*grid_spacing.y(), + 0), + direction2); +// plane.set_direction_vectors(direction2); - plane.construct_volume(image2, make_coordinate(3,3,3)); + plane2.construct_volume(image2, make_coordinate(3,3,3)); // create projadata info @@ -196,20 +205,21 @@ BlocksTests::run_plane_symmetry_test(){ forw_projector2_sptr->forward_project(*projdata2, *image2_sptr); int view1_num = 0, view2_num = 0; - LORInAxialAndNoArcCorrSinogramCoordinates lorB; + LORInAxialAndNoArcCorrSinogramCoordinates lorB1; for(int i=0;iget_max_view_num();i++){ Bin bin(0,i,0,0); - proj_data_info_blocks_ptr->get_LOR(lorB,bin); - if(abs(lorB.phi()-phi1)/phi1<=1E-2){ + proj_data_info_blocks_ptr->get_LOR(lorB1,bin); + if(abs(lorB1.phi()-phi1)/phi1<=1E-2){ view1_num=i; break; } } + LORInAxialAndNoArcCorrSinogramCoordinates lorB2; for(int i=0;iget_max_view_num();i++){ Bin bin(0,i,0,0); - proj_data_info_blocks_ptr->get_LOR(lorB,bin); - if(abs(lorB.phi()-phi2)/phi2<=1E-2){ + proj_data_info_blocks_ptr->get_LOR(lorB2,bin); + if(abs(lorB2.phi()-phi2)/phi2<=1E-2){ view2_num=i; break; } @@ -231,16 +241,16 @@ BlocksTests::run_plane_symmetry_test(){ for(int tang=projdata2->get_min_tangential_pos_num();tangget_max_tangential_pos_num();tang++){ if((max2-projdata2->get_sinogram(0,0).at(view2_num).at(tang))/max2<1E-3) { - tang1_num=tang; + tang2_num=tang; break; } } float bin1=projdata->get_sinogram(0,0).at(view1_num).at(tang1_num); float bin2=projdata2->get_sinogram(0,0).at(view2_num).at(tang2_num); - set_tolerance(10E-3); - check_if_equal(bin1, max1,"the value seen in the block at 30 degrees should be the same as the max value of the sinogram"); - check_if_equal(bin2, max2,"the value seen in the block at 60 degrees should be the same as the max value of the sinogram"); + set_tolerance(10E-2); + check_if_equal(bin1, max1,"the value seen in the block at 60 degrees should be the same as the max value of the sinogram"); + check_if_equal(bin2, max2,"the value seen in the block at 30 degrees should be the same as the max value of the sinogram"); } /*! The following is a test for symmetries: a simulated image is created with spherical source in front of each detector block, diff --git a/src/test/test_proj_data_info.cxx b/src/test/test_proj_data_info.cxx old mode 100644 new mode 100755 index d6a8ee2d0..54cb3a1c3 --- a/src/test/test_proj_data_info.cxx +++ b/src/test/test_proj_data_info.cxx @@ -489,7 +489,7 @@ ProjDataInfoTests::run_Blocks_DOI_test() int Bring1, Bring2, Bdet1,Bdet2, BDring1, BDring2, BDdet1, BDdet2; CartesianCoordinate3D< float> b1,b2,bd1,bd2; float doi=scannerBlocksDOI_ptr->get_average_depth_of_interaction(); -// timer.reset(); timer.start(); + timer.reset(); timer.start(); for (int seg =proj_data_info_blocks_doi0_ptr->get_min_segment_num(); seg <=proj_data_info_blocks_doi0_ptr->get_max_segment_num(); ++ seg) for (int ax =proj_data_info_blocks_doi0_ptr->get_min_axial_pos_num(seg); ax <=proj_data_info_blocks_doi0_ptr->get_max_axial_pos_num(seg); ++ax) @@ -545,15 +545,30 @@ ProjDataInfoTests::run_coordinate_test() scannerBlocks_ptr->set_num_axial_crystals_per_block(1); scannerBlocks_ptr->set_axial_block_spacing(scannerBlocks_ptr->get_axial_crystal_spacing()* scannerBlocks_ptr->get_num_axial_crystals_per_block()); - scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing()* - scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); scannerBlocks_ptr->set_num_transaxial_crystals_per_block(1); scannerBlocks_ptr->set_num_axial_blocks_per_bucket(1); scannerBlocks_ptr->set_num_transaxial_blocks_per_bucket(1); + scannerBlocks_ptr->set_transaxial_block_spacing(scannerBlocks_ptr->get_transaxial_crystal_spacing()* + scannerBlocks_ptr->get_num_transaxial_crystals_per_block()); scannerBlocks_ptr->set_num_rings(1); scannerBlocks_ptr->set_scanner_geometry("BlocksOnCylindrical"); + int num_transaxial_blocks_per_bucket = scannerBlocks_ptr->get_num_transaxial_blocks_per_bucket(); + int num_transaxial_crystals_per_block = scannerBlocks_ptr->get_num_transaxial_crystals_per_block(); + int num_transaxial_buckets = scannerBlocks_ptr->get_num_transaxial_blocks()/num_transaxial_blocks_per_bucket; + float transaxial_block_spacing = scannerBlocks_ptr->get_transaxial_block_spacing(); + float transaxial_crystal_spacing = scannerBlocks_ptr->get_transaxial_crystal_spacing(); +// estimate the angle covered by a bucket, alpha + + float csi=_PI/num_transaxial_buckets; + float trans_blocks_gap=transaxial_block_spacing-num_transaxial_crystals_per_block*transaxial_crystal_spacing; + float csi_minus_csiGaps=csi-(csi/transaxial_block_spacing/2)* + (transaxial_crystal_spacing/2+trans_blocks_gap); + + float dx=scannerBlocks_ptr->get_effective_ring_radius()*sin(csi_minus_csiGaps); + float dy=scannerBlocks_ptr->get_effective_ring_radius()-scannerBlocks_ptr->get_effective_ring_radius()*cos(csi_minus_csiGaps); + shared_ptr scannerCyl_ptr; scannerCyl_ptr.reset(new Scanner (Scanner::SAFIRDualRingPrototype)); scannerCyl_ptr->set_num_axial_crystals_per_block(1); @@ -604,7 +619,7 @@ ProjDataInfoTests::run_coordinate_test() int Bring1, Bring2, Bdet1,Bdet2, Cring1, Cring2, Cdet1, Cdet2; int RTring1, RTring2, RTdet1,RTdet2; CartesianCoordinate3D< float> b1,b2,c1,c2,roundt1, roundt2; -// timer.reset(); timer.start(); + timer.reset(); timer.start(); LORInAxialAndNoArcCorrSinogramCoordinates lorB; LORInAxialAndNoArcCorrSinogramCoordinates lorC; @@ -796,7 +811,14 @@ ProjDataInfoTests::run_coordinate_test_for_realistic_scanner() int Bring1, Bring2, Bdet1,Bdet2, Cring1, Cring2, Cdet1, Cdet2; CartesianCoordinate3D< float> b1,b2,c1,c2; -// timer.reset(); timer.start(); + + // estimate the angle covered by half bucket, csi + float csi=_PI/scannerBlocks_ptr->get_num_transaxial_buckets(); + // distance between the center of the scannner and the first crystal in the bucket, r=Reffective/cos(csi) + float r=scannerBlocks_ptr->get_effective_ring_radius()/cos(csi); + float max_tolerance=abs(scannerBlocks_ptr->get_effective_ring_radius()-r); + + timer.reset(); timer.start(); LORInAxialAndNoArcCorrSinogramCoordinates lorB; LORInAxialAndNoArcCorrSinogramCoordinates lorC; @@ -842,14 +864,15 @@ ProjDataInfoTests::run_coordinate_test_for_realistic_scanner() proj_data_info_blocks_ptr->find_cartesian_coordinates_of_detection(b1,b2,bin); // we expect to be differences of the order of the mm in x and y due to the difference in geometry - set_tolerance(10E-1); - check_if_equal(b1.z(),c1.z(), " "); - check_if_equal(b2.z(),c2.z(), " "); - check_if_equal(b1.y(),c1.y(), " "); - check_if_equal(b2.y(),c2.y(), " "); - check_if_equal(b1.x(),c1.x(), " "); - check_if_equal(b2.x(),c2.x(), " "); + set_tolerance(max_tolerance); + + check_if_equal(b1.z(),c1.z(), " checking cartesian coordinate z1"); + check_if_equal(b2.z(),c2.z(), " checking cartesian coordinate z2"); + check_if_equal(b1.y(),c1.y(), " checking cartesian coordinate y1"); + check_if_equal(b2.y(),c2.y(), " checking cartesian coordinate y2"); + check_if_equal(b1.x(),c1.x(), " checking cartesian coordinate x1"); + check_if_equal(b2.x(),c2.x(), " checking cartesian coordinate x2"); } timer.stop(); std::cerr<< "-- CPU Time " << timer.value() << '\n'; @@ -948,31 +971,30 @@ run_lor_get_s_test(){ // num_det_per_ring:2PI=det_id_diff:PI/2 int det_id_diff=scannerBlocks_ptr->get_num_detectors_per_ring()*(_PI/2)/(2*_PI); int Ctb=scannerCyl_ptr->get_num_transaxial_crystals_per_block(); - float crystal_trans_spacing=scannerBlocks_ptr->get_transaxial_crystal_spacing(); + float transaxial_crystal_spacing=scannerBlocks_ptr->get_transaxial_crystal_spacing(); float block_trans_spacing=scannerBlocks_ptr->get_transaxial_block_spacing(); float prev_s=0; float prev_phi=0; for (int i=0; iget_num_transaxial_crystals_per_block();i++){ - Cring1=0;Cdet1=i+2*Ctb-Ctb/2; - Cring2=0;Cdet2=2*Ctb-Ctb/2+det_id_diff+Ctb-1 -i; + Cring1=0;Cdet1=i+2*Ctb; + Cring2=0;Cdet2=2*Ctb+det_id_diff+Ctb-1 -i; proj_data_info_blocks_ptr->get_bin_for_det_pair(bin, Cdet1,Cring1,Cdet2,Cring1); proj_data_info_blocks_ptr->get_LOR(lorB,bin); float R=block_trans_spacing*(sin(_PI*5/12)+sin(_PI/4)+sin(_PI/12)); float s=R*cos(_PI/3)+ - crystal_trans_spacing/2*sin(_PI/4)+ - (i)*crystal_trans_spacing*sin(_PI/4); + transaxial_crystal_spacing/2*sin(_PI/4)+ + (i)*transaxial_crystal_spacing*sin(_PI/4); - float s_step=crystal_trans_spacing*sin(_PI/4); + float s_step=transaxial_crystal_spacing*sin(_PI/4); // the following fails at the moment // check_if_equal(s, lorB.s(),std::to_string(i)+ " Blocks get_s() is different"); // the first value we expect to be different + set_tolerance(10E-3); if (i>0){ - check_if_equal(s_step, lorB.s()-prev_s,std::to_string(i)+ " Blocks get_s() the step is different");//+ -// std::to_string(crystal_trans_spacing*sin(_PI/4)) + " lorB.s() the step is "+std::to_string(lorB.s()-prev_s)+ -// + "phi step is "+std::to_string(lorB.phi()-prev_phi)); + check_if_equal(s_step, lorB.s()-prev_s,std::to_string(i)+ " Blocks get_s() the step is different"); check_if_equal(0.F, lorB.phi()-prev_phi, " Blocks get_phi() should be always the same as we are considering parallel LORs"); } prev_s=lorB.s(); From 503cc560159194a346f322cf9ac375b083bd8fe0 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Mon, 29 Nov 2021 22:25:06 +0000 Subject: [PATCH 39/81] derive ProjDataInfoGeneric from ProjDataInfoCylindrical Most of the code in Generic case in Cylindrical was the same. The Generic case assumed a 2D system anyway. So I've cut out a lot of the duplication by deriving Generical from Cylindrical. In fact, it should be the other way around, but that requires more changes elsewhere. This is left for later. At present, I just "delete" some member functions in Generic to make clear that they shouldn't be called (they still can be of course, so this isn't very safe). Added one noew function axial_sampling_is_uniform() to ProjDataInfoCylindrical (and hence all Generic and Block versions). At present, this is still set to true. It is used by the DataSymmetries to disable z-shift symmetry. --- src/IO/interfile.cxx | 18 +- src/buildblock/ProjDataInfoGeneric.cxx | 414 +----------------- .../ProjDataInfoGenericNoArcCorr.cxx | 15 +- src/include/stir/ProjDataInfoCylindrical.h | 6 +- src/include/stir/ProjDataInfoGeneric.h | 258 ++--------- src/include/stir/ProjDataInfoGeneric.inl | 196 +-------- .../stir/ProjDataInfoGenericNoArcCorr.h | 2 +- ...ataSymmetriesForBins_PET_CartesianGrid.cxx | 19 +- 8 files changed, 85 insertions(+), 843 deletions(-) diff --git a/src/IO/interfile.cxx b/src/IO/interfile.cxx index ec5599651..2dbe67b2f 100644 --- a/src/IO/interfile.cxx +++ b/src/IO/interfile.cxx @@ -1441,7 +1441,8 @@ write_basic_interfile_PDFS_header(const string& header_file_name, } const Scanner& scanner = *proj_data_info_sptr->get_scanner_ptr(); - if (fabs(proj_data_info_sptr->get_ring_radius()- +#if 0 // KT commented out. currently no get_ring_radius() anymore + if (fabs(proj_data_info_sptr->get_ring_radius()- scanner.get_effective_ring_radius()) > .1) warning("write_basic_interfile_PDFS_header: inconsistent effective ring radius:\n" "\tproj_data_info has %g, scanner has %g.\n" @@ -1449,6 +1450,7 @@ write_basic_interfile_PDFS_header(const string& header_file_name, "\tYou will have a problem reading this data back in.", proj_data_info_sptr->get_ring_radius(), scanner.get_effective_ring_radius()); +#endif if (fabs(proj_data_info_sptr->get_ring_spacing()- scanner.get_ring_spacing()) > .1) warning("write_basic_interfile_PDFS_header: inconsistent ring spacing:\n" @@ -1490,20 +1492,6 @@ write_basic_interfile_PDFS_header(const string& header_file_name, } const Scanner& scanner = *proj_data_info_sptr->get_scanner_ptr(); - if (fabs(proj_data_info_sptr->get_ring_radius() - scanner.get_effective_ring_radius()) > .1) - warning("write_basic_interfile_PDFS_header: inconsistent effective ring radius:\n" - "\tproj_data_info has %g, scanner has %g.\n" - "\tThis really should not happen and signifies a bug.\n" - "\tYou will have a problem reading this data back in.", - proj_data_info_sptr->get_ring_radius(), - scanner.get_effective_ring_radius()); - if (fabs(proj_data_info_sptr->get_ring_spacing() - scanner.get_ring_spacing()) > .1) - warning("write_basic_interfile_PDFS_header: inconsistent ring spacing:\n" - "\tproj_data_info has %g, scanner has %g.\n" - "\tThis really should not happen and signifies a bug.\n" - "\tYou will have a problem reading this data back in.", - proj_data_info_sptr->get_ring_spacing(), - scanner.get_ring_spacing()); output_header << scanner.parameter_info(); diff --git a/src/buildblock/ProjDataInfoGeneric.cxx b/src/buildblock/ProjDataInfoGeneric.cxx index 0586fe128..404d29dc0 100755 --- a/src/buildblock/ProjDataInfoGeneric.cxx +++ b/src/buildblock/ProjDataInfoGeneric.cxx @@ -61,215 +61,12 @@ ProjDataInfoGeneric(const shared_ptr& scanner_ptr, const VectorWithOffset& min_ring_diff_v, const VectorWithOffset& max_ring_diff_v, const int num_views,const int num_tangential_poss) - :ProjDataInfo(scanner_ptr,num_axial_pos_per_segment, - num_views,num_tangential_poss), - min_ring_diff(min_ring_diff_v), - max_ring_diff(max_ring_diff_v) + :ProjDataInfoCylindrical(scanner_ptr,num_axial_pos_per_segment, + min_ring_diff_v, max_ring_diff_v, num_views,num_tangential_poss) { - - azimuthal_angle_sampling = static_cast(_PI/num_views); - ring_radius.resize(0,0); - ring_radius[0] = get_scanner_ptr()->get_effective_ring_radius(); - ring_spacing= get_scanner_ptr()->get_ring_spacing() ; - - // TODO this info should probably be provided via the constructor, or at - // least by Scanner. - sampling_corresponds_to_physical_rings = - scanner_ptr->get_type() != Scanner::HiDAC; - - - assert(min_ring_diff.get_length() == max_ring_diff.get_length()); - assert(min_ring_diff.get_length() == num_axial_pos_per_segment.get_length()); - - // check min,max ring diff - { - for (int segment_num=get_min_segment_num(); segment_num<=get_max_segment_num(); ++segment_num) - if (min_ring_diff[segment_num]> max_ring_diff[segment_num]) - { - warning("ProjDataInfoGeneric: min_ring_difference %d is larger than max_ring_difference %d for segment %d. " - "Swapping them around", - min_ring_diff[segment_num], max_ring_diff[segment_num], segment_num); - swap(min_ring_diff[segment_num], max_ring_diff[segment_num]); - } - } - - initialise_ring_diff_arrays(); -} - -/* warning In cylindrical geometry m_offset is calculated based on ring_spacing, - then it is used to caculate ax_pos_num_offset and segment_axial_pos_to_ring1_plus_ring2. - For generic geometry, m_offset has been removed and the above mentioned variables are - calculated independant of m_offset.*/ -void -ProjDataInfoGeneric:: -initialise_ring_diff_arrays() const -{ - - // check min,max ring diff - { - // check is necessary here again because of set_min_ring_difference() - // we do not swap here because that would require the min/max_ring_diff arrays to be mutable as well - for (int segment_num=get_min_segment_num(); segment_num<=get_max_segment_num(); ++segment_num) - if (min_ring_diff[segment_num]> max_ring_diff[segment_num]) - { - error("ProjDataInfoGeneric: min_ring_difference %d is larger than " - "max_ring_difference %d for segment %d.", - min_ring_diff[segment_num], max_ring_diff[segment_num], segment_num); - } - } - - // initialise ax_pos_num_offset - if (sampling_corresponds_to_physical_rings) - { - const int num_rings = get_scanner_ptr()->get_num_rings(); - ax_pos_num_offset = - VectorWithOffset(get_min_segment_num(),get_max_segment_num()); - - /* ax_pos_num will be determined by looking at ring1+ring2. - This also works for axially compressed data (i.e. span) as - ring1+ring2 is constant for all ring-pairs combined into 1 - segment,ax_pos. - - Ignoring the difficulties of axial compression for a second, it is clear that - for a given bin, there will be 2 rings as follows: - ring2 = get_m(bin)/ring_spacing + ring_diff/2 + (num_rings-1)/2 - ring1 = get_m(bin)/ring_spacing - ring_diff/2 + (num_rings-1)/2 - This follows from the fact that get_m() returns the z position - in millimeter of the middle of the LOR w.r.t. the middle of the scanner. - The (num_rings-1)/2 shifts the origin such that the first ring has - ring_num==0. - - From the above, it follows that - ring1+ring2=2*get_m(bin)/ring_spacing + (num_rings-1) - Finally, we use the formula for get_m to obtain - ring1+ring2=2*ax_pos_num/get_num_axial_poss_per_ring_inc(segment_num) - -2*m_offset[segment_num]/ring_spacing + (num_rings-1) - Solving this for ax_pos_num: - ax_pos_num = (ring1+ring2-(num_rings-1) - + 2*m_offset[segment_num]/ring_spacing - ) * get_num_axial_poss_per_ring_inc(segment_num)/2 - - We could plug m_offset in to obtain - ax_pos_num = (ring1+ring2-(num_rings-1) - ) * get_num_axial_poss_per_ring_inc(segment_num)/2. - + - (get_max_axial_pos_num(segment_num) - + get_min_axial_pos_num(segment_num) )/2. - this formula is easy to understand, but we don't use it as - at some point somebody might change m_offset - and forget to change this code... - (also, the form above would need float division and then rounding) - */ - for (int segment_num=get_min_segment_num(); segment_num<=get_max_segment_num(); ++segment_num) - { - const float ax_pos_num_offset_float = (num_rings-1) - - (get_max_axial_pos_num(segment_num) + - get_min_axial_pos_num(segment_num)) - /get_num_axial_poss_per_ring_inc(segment_num); - ax_pos_num_offset[segment_num] = round(ax_pos_num_offset_float); - // check that it was integer - if (fabs(ax_pos_num_offset[segment_num] - ax_pos_num_offset_float) > 1E-4) - { - error("ProjDataInfoGeneric: in segment %d, the axial positions\n" - "do not correspond to the usual locations between physical rings.\n" - "This is suspicious and can make things go wrong in STIR, so I abort.\n" - "Check the number of axial positions in this segment.", - segment_num); - } - - if (get_num_axial_poss_per_ring_inc(segment_num)==1) - { - // check that we'll get an integer ax_pos_num, i.e. - // (ring1+ring2 - ax_pos_num_offset) has to be even, for any - // ring1,ring2 in the segment, i.e ring1-ring2 = ring_diff, so - // ring1+ring2 = 2*ring2 + ring_diff - assert(get_min_ring_difference(segment_num) == - get_max_ring_difference(segment_num)); - if ((get_max_ring_difference(segment_num) - - ax_pos_num_offset[segment_num]) % 2 != 0) - warning("ProjDataInfoGeneric: the number of axial positions in " - "segment %d is such that current conventions will place " - "the LORs shifted with respect to the physical rings.", - segment_num); - } - } - } - // initialise ring_diff_to_segment_num - if (sampling_corresponds_to_physical_rings) - { - const int min_ring_difference = - *min_element(min_ring_diff.begin(), min_ring_diff.end()); - const int max_ring_difference = - *max_element(max_ring_diff.begin(), max_ring_diff.end()); - - // set ring_diff_to_segment_num to appropriate size - // in principle, the max ring difference would be scanner.num_rings-1, but - // in case someone is up to strange things, we take the max of this value - // with the max_ring_difference as given in the file - ring_diff_to_segment_num = - VectorWithOffset(min(min_ring_difference, -(get_scanner_ptr()->get_num_rings()-1)), - max(max_ring_difference, get_scanner_ptr()->get_num_rings()-1)); - // first set all to impossible value - // warning: get_segment_num_for_ring_difference relies on the fact that this value - // is larger than get_max_segment_num() - ring_diff_to_segment_num.fill(get_max_segment_num()+1); - - for(int ring_diff=min_ring_difference; ring_diff <= max_ring_difference; ++ring_diff) - { - int segment_num; - for (segment_num=get_min_segment_num(); segment_num<=get_max_segment_num(); ++segment_num) - { - if (ring_diff >= min_ring_diff[segment_num] && - ring_diff <= max_ring_diff[segment_num]) - { - #if 0 - std::cerr << "ring diff " << ring_diff << " stored in s:" << segment_num << std::endl; - #endif - ring_diff_to_segment_num[ring_diff] = segment_num; - break; - } - } - if (segment_num>get_max_segment_num()) - { - warning("ProjDataInfoGeneric: ring difference %d does not belong to a segment", - ring_diff); - } - } - } - // initialise segment_axial_pos_to_ring1_plus_ring2 - if (sampling_corresponds_to_physical_rings) - { - segment_axial_pos_to_ring1_plus_ring2 = - VectorWithOffset >(get_min_segment_num(), get_max_segment_num()); - for (int s_num=get_min_segment_num(); s_num<=get_max_segment_num(); ++s_num) - { - const int min_ax_pos_num = get_min_axial_pos_num(s_num); - const int max_ax_pos_num = get_max_axial_pos_num(s_num); - segment_axial_pos_to_ring1_plus_ring2[s_num].grow(min_ax_pos_num, max_ax_pos_num); - for (int ax_pos_num=min_ax_pos_num; ax_pos_num<=max_ax_pos_num; ++ax_pos_num) - { - // see documentation above for formulas - const float ring1_plus_ring2_float = - (2*ax_pos_num - - get_max_axial_pos_num(s_num) + get_min_axial_pos_num(s_num)) - /get_num_axial_poss_per_ring_inc(s_num) + - get_scanner_ptr()->get_num_rings()-1; - - const int ring1_plus_ring2 = - round(ring1_plus_ring2_float); - // check that it was integer - assert(fabs(ring1_plus_ring2 - ring1_plus_ring2_float) < 1E-4) ; - segment_axial_pos_to_ring1_plus_ring2[s_num][ax_pos_num] = ring1_plus_ring2; - } - } - } - - if (sampling_corresponds_to_physical_rings) - allocate_segment_axial_pos_to_ring_pair(); - - ring_diff_arrays_computed = true; } +#if 0 /*! Default implementation checks common variables. Needs to be overloaded. */ bool @@ -281,80 +78,18 @@ blindly_equals(const root_type * const that) const const self_type& proj_data_info = static_cast(*that); return - this->azimuthal_angle_sampling == proj_data_info.azimuthal_angle_sampling && - this->ring_radius == proj_data_info.ring_radius && - this->sampling_corresponds_to_physical_rings == proj_data_info.sampling_corresponds_to_physical_rings && - this->ring_spacing == proj_data_info.ring_spacing && - this->min_ring_diff == proj_data_info.min_ring_diff && - this->max_ring_diff == proj_data_info.max_ring_diff; -} - -void -ProjDataInfoGeneric:: -get_ring_pair_for_segment_axial_pos_num(int& ring1, - int& ring2, - const int segment_num, - const int axial_pos_num) const -{ - if (!sampling_corresponds_to_physical_rings) - error("ProjDataInfoGeneric::get_ring_pair_for_segment_axial_pos_num does not work for this type of sampled data"); - // can do only span=1 at the moment - if (get_min_ring_difference(segment_num) != get_max_ring_difference(segment_num)) - error("ProjDataInfoGeneric::get_ring_pair_for_segment_axial_pos_num does not work for data with axial compression"); - - if (!ring_diff_arrays_computed) - initialise_ring_diff_arrays(); - - const int ring_diff = get_max_ring_difference(segment_num); - const int ring1_plus_ring2= segment_axial_pos_to_ring1_plus_ring2[segment_num][axial_pos_num]; - - // KT 01/08/2002 swapped rings - ring1 = (ring1_plus_ring2 - ring_diff)/2; - ring2 = (ring1_plus_ring2 + ring_diff)/2; - assert((ring1_plus_ring2 + ring_diff)%2 == 0); - assert((ring1_plus_ring2 - ring_diff)%2 == 0); -} - - -void -ProjDataInfoGeneric:: -set_azimuthal_angle_sampling(const float angle_v) -{ - azimuthal_angle_sampling = angle_v; + true; } - -//void -//ProjDataInfoGeneric:: -//set_axial_sampling(const float samp_v, int segment_num) -//{axial_sampling = samp_v;} - +#endif void ProjDataInfoGeneric:: set_num_views(const int new_num_views) { - const float old_azimuthal_angle_range = - this->get_azimuthal_angle_sampling() * this->get_num_views(); - base_type::set_num_views(new_num_views); - this->azimuthal_angle_sampling = old_azimuthal_angle_range/this->get_num_views(); + if (new_num_views != get_num_views()) + error("ProjDataInfoGeneric::set_num_views not supported"); } - -void -ProjDataInfoGeneric:: -set_min_ring_difference( int min_ring_diff_v, int segment_num) -{ - ring_diff_arrays_computed = false; - min_ring_diff[segment_num] = min_ring_diff_v; -} - -void -ProjDataInfoGeneric:: -set_max_ring_difference( int max_ring_diff_v, int segment_num) -{ - ring_diff_arrays_computed = false; - max_ring_diff[segment_num] = max_ring_diff_v; -} - +#if 0 // TODOBLOCK void ProjDataInfoGeneric:: set_ring_spacing(float ring_spacing_v) @@ -362,131 +97,7 @@ set_ring_spacing(float ring_spacing_v) ring_diff_arrays_computed = false; ring_spacing = ring_spacing_v; } - -void -ProjDataInfoGeneric:: -allocate_segment_axial_pos_to_ring_pair() const -{ - segment_axial_pos_to_ring_pair = - VectorWithOffset > > - (get_min_segment_num(), get_max_segment_num()); - - for (int segment_num = get_min_segment_num(); - segment_num <= get_max_segment_num(); - ++segment_num) - { - segment_axial_pos_to_ring_pair[segment_num].grow(get_min_axial_pos_num(segment_num), - get_max_axial_pos_num(segment_num)); - } -} - -void -ProjDataInfoGeneric:: -compute_segment_axial_pos_to_ring_pair(const int segment_num, const int axial_pos_num) const -{ - shared_ptr new_el(new RingNumPairs); - segment_axial_pos_to_ring_pair[segment_num][axial_pos_num] = new_el; - - RingNumPairs& table = - *segment_axial_pos_to_ring_pair[segment_num][axial_pos_num]; - table.reserve(get_max_ring_difference(segment_num) - - get_min_ring_difference(segment_num) + 1); - - /* We compute the lookup-table in a fancy way. - We could just as well have a simple loop over all ring pairs and check - if it belongs to this segment/axial_pos. - The current way is a lot faster though. - */ - const int min_ring_diff = get_min_ring_difference(segment_num); - const int max_ring_diff = get_max_ring_difference(segment_num); - const int num_rings = get_scanner_ptr()->get_num_rings(); - - /* ring1_plus_ring2 is the same for any ring pair that contributes to - this particular segment_num, axial_pos_num. - */ - const int ring1_plus_ring2= - segment_axial_pos_to_ring1_plus_ring2[segment_num][axial_pos_num]; - - /* - The ring_difference increments with 2 as the other ring differences do - not give a ring pair with this axial_position. This is because - ring1_plus_ring2%2 == ring_diff%2 - (which easily follows by plugging in ring1+ring2 and ring1-ring2). - The starting ring_diff is determined such that the above condition - is satisfied. You can check it by noting that the - start_ring_diff%2 - == (min_ring_diff + (min_ring_diff+ring1_plus_ring2)%2)%2 - == (2*min_ring_diff+ring1_plus_ring2)%2 - == ring1_plus_ring2%2 - */ - for(int ring_diff = min_ring_diff + (min_ring_diff+ring1_plus_ring2)%2; - ring_diff <= max_ring_diff; - ring_diff+=2 ) - { - const int ring1 = (ring1_plus_ring2 - ring_diff)/2; - const int ring2 = (ring1_plus_ring2 + ring_diff)/2; - if (ring1<0 || ring2 < 0 || ring1>=num_rings || ring2 >= num_rings) - continue; - assert((ring1_plus_ring2 + ring_diff)%2 == 0); - assert((ring1_plus_ring2 - ring_diff)%2 == 0); - table.push_back(pair(ring1, ring2)); - #ifndef NDEBUG - int check_segment_num = 0, check_axial_pos_num = 0; - assert(get_segment_axial_pos_num_for_ring_pair(check_segment_num, - check_axial_pos_num, - ring1, - ring2) == - Succeeded::yes); - assert(check_segment_num == segment_num); - assert(check_axial_pos_num == axial_pos_num); - #endif - } -} - -void -ProjDataInfoGeneric:: -set_num_axial_poss_per_segment(const VectorWithOffset& num_axial_poss_per_segment) -{ - ProjDataInfo::set_num_axial_poss_per_segment(num_axial_poss_per_segment); - ring_diff_arrays_computed = false; -} - -void -ProjDataInfoGeneric:: -set_min_axial_pos_num(const int min_ax_pos_num, const int segment_num) -{ - ProjDataInfo::set_min_axial_pos_num(min_ax_pos_num, segment_num); - ring_diff_arrays_computed = false; -} - -void ProjDataInfoGeneric:: -set_max_axial_pos_num(const int max_ax_pos_num, const int segment_num) -{ - ProjDataInfo::set_max_axial_pos_num(max_ax_pos_num, segment_num); - ring_diff_arrays_computed = false; -} - -void -ProjDataInfoGeneric:: -reduce_segment_range(const int min_segment_num, const int max_segment_num) -{ - ProjDataInfo::reduce_segment_range(min_segment_num, max_segment_num); - // reduce ring_diff arrays to new valid size - VectorWithOffset new_min_ring_diff(min_segment_num, max_segment_num); - VectorWithOffset new_max_ring_diff(min_segment_num, max_segment_num); - - for (int segment_num = min_segment_num; segment_num<= max_segment_num; ++segment_num) - { - new_min_ring_diff[segment_num] = this->min_ring_diff[segment_num]; - new_max_ring_diff[segment_num] = this->max_ring_diff[segment_num]; - } - - this->min_ring_diff = new_min_ring_diff; - this->max_ring_diff = new_max_ring_diff; - - // make sure other arrays will be updated if/when necessary - this->ring_diff_arrays_computed = false; -} +#endif //! warning Find lor from cartesian coordinates of detector pair void @@ -516,13 +127,14 @@ ProjDataInfoGeneric::parameter_info() const std::ostringstream s; #endif s << ProjDataInfo::parameter_info(); - s << "Azimuthal angle increment (deg): " << get_azimuthal_angle_sampling()*180/_PI << '\n'; - s << "Azimuthal angle extent (deg): " << fabs(get_azimuthal_angle_sampling())*get_num_views()*180/_PI << '\n'; + // TODOBLOCK Cylindrical has the following which doesn't make sense for Generic, so repeat code + //s << "Azimuthal angle increment (deg): " << get_azimuthal_angle_sampling()*180/_PI << '\n'; + //s << "Azimuthal angle extent (deg): " << fabs(get_azimuthal_angle_sampling())*get_num_views()*180/_PI << '\n'; s << "ring differences per segment: \n"; for (int segment_num=get_min_segment_num(); segment_num<=get_max_segment_num(); ++segment_num) { - s << '(' << min_ring_diff[segment_num] << ',' << max_ring_diff[segment_num] <<')'; + s << '(' << get_min_ring_difference(segment_num) << ',' << get_max_ring_difference(segment_num) <<')'; } s << std::endl; return s.str(); diff --git a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx index 4df19592b..3b671f871 100644 --- a/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx +++ b/src/buildblock/ProjDataInfoGenericNoArcCorr.cxx @@ -174,15 +174,7 @@ initialise_uncompressed_view_tangpos_to_det1det2() const const int num_detectors = get_scanner_ptr()->get_num_detectors_per_ring(); - assert(num_detectors%2 == 0); - // check views range from 0 to Pi - /*! - warning The following assersions are slightly different from cylindrical. - because in the function get_phi(bin), get_lor is called. - */ - assert(fabs(Bin(0,0,0,0).view_num()*get_azimuthal_angle_sampling()) < 1.E-4); - assert(fabs(Bin(0,get_num_views(),0,0).view_num()*get_azimuthal_angle_sampling() - _PI) < 1.E-4); const int min_tang_pos_num = -(num_detectors/2)+1; const int max_tang_pos_num = -(num_detectors/2)+num_detectors; @@ -236,11 +228,6 @@ initialise_det1det2_to_uncompressed_view_tangpos() const error("Minimum view number should currently be zero to be able to use get_view_tangential_pos_num_for_det_num_pair()"); } - // check views range from 0 to Pi - //! warning The following assersions are slightly different from cylindrical. See the above function - assert(fabs(Bin(0,0,0,0).view_num()*get_azimuthal_angle_sampling()) < 1.E-4); - assert(fabs(Bin(0,get_max_view_num()+1,0,0).view_num()*get_azimuthal_angle_sampling() - _PI) < 1.E-4); - //const int min_tang_pos_num = -(num_detectors/2); //const int max_tang_pos_num = -(num_detectors/2)+num_detectors; @@ -328,7 +315,7 @@ get_num_det_pos_pairs_for_bin(const Bin& bin) const void ProjDataInfoGenericNoArcCorr:: -get_all_det_pos_pairs_for_bin(vector >& dps, +get_all_det_pos_pairs_for_bin(std::vector >& dps, const Bin& bin) const { this->initialise_uncompressed_view_tangpos_to_det1det2_if_not_done_yet(); diff --git a/src/include/stir/ProjDataInfoCylindrical.h b/src/include/stir/ProjDataInfoCylindrical.h index 73c1c0d97..b43656151 100644 --- a/src/include/stir/ProjDataInfoCylindrical.h +++ b/src/include/stir/ProjDataInfoCylindrical.h @@ -122,7 +122,11 @@ class ProjDataInfoCylindrical: public ProjDataInfo (i.e. no axial compression), while it is half the ring spacing for spanned data. */ - inline float get_axial_sampling(int segment_num) const; + virtual inline float get_axial_sampling(int segment_num) const; + //! Return if axial sampling makes sense + /*! could be \c false for block/generic cases */ + virtual inline bool axial_sampling_is_uniform() const + { return true; } //! Get average ring difference for the given segment inline float get_average_ring_difference(int segment_num) const; diff --git a/src/include/stir/ProjDataInfoGeneric.h b/src/include/stir/ProjDataInfoGeneric.h index 0109fe02b..e7dc1bb9b 100644 --- a/src/include/stir/ProjDataInfoGeneric.h +++ b/src/include/stir/ProjDataInfoGeneric.h @@ -1,12 +1,9 @@ /* - Copyright (C) 2000 PARAPET partners - Copyright (C) 2000-2009, Hammersmith Imanet Ltd - Copyright (C) 2013, University College London - Copyright (C) 2013, Institute for Bioengineering of Catalonia + Copyright (C) 2011, University College London Copyright 2017 ETH Zurich, Institute of Particle Physics and Astrophysics This file is part of STIR. - SPDX-License-Identifier: Apache-2.0 AND License-ref-PARAPET-license + SPDX-License-Identifier: Apache-2.0 See STIR/LICENSE.txt for details */ @@ -18,23 +15,13 @@ \author Parisa Khateri \author Michael Roethlisberger - \author Sanida Mustafovic \author Kris Thielemans - \author Berta Marti Fuster - \author PARAPET project */ #ifndef __stir_ProjDataInfoGeneric_H__ #define __stir_ProjDataInfoGeneric_H__ -#include "stir/ProjDataInfo.h" -#include -#include - -#ifndef STIR_NO_NAMESPACES -using std::vector; -using std::pair; -#endif +#include "stir/ProjDataInfoCylindrical.h" START_NAMESPACE_STIR @@ -43,11 +30,24 @@ template class CartesianCoordinate3D; /*! \ingroup projdata - \brief projection data info for data corresponding to a + \brief projection data info for data corresponding to 'Generic' sampling. + + This isn't completely generic as it assumes that there is "axial" (or "ring") + coordinate and a "transaxial" (or "crystal"). However, their spacing can + be arbitrary. + + Currently this class is derived from ProjDataInfoCylindrical. Arguably it + should be the other way around. However, this would break backwards + compatibility dramatically, and break other pull-requests in progress. + We will leave this for later. At present, we just \c delete some member functions + that do not make sense in the Generic case. There are a few ones left which might + be removed in future, but are currently still used. + + \todo change hierarchy order, i.e. derive from ProjDataInfoCylindrical from ProjDataInfoGeneric. */ -class ProjDataInfoGeneric: public ProjDataInfo +class ProjDataInfoGeneric: public ProjDataInfoCylindrical { private: typedef ProjDataInfo base_type; @@ -55,7 +55,7 @@ class ProjDataInfoGeneric: public ProjDataInfo public: //! Type used by get_all_ring_pairs_for_segment_axial_pos_num() - typedef vector > RingNumPairs; + typedef std::vector > RingNumPairs; //! Constructors ProjDataInfoGeneric(); @@ -88,224 +88,32 @@ class ProjDataInfoGeneric: public ProjDataInfo virtual void get_LOR(LORInAxialAndNoArcCorrSinogramCoordinates& lor, const Bin& bin) const; - void set_azimuthal_angle_sampling(const float angle); + void set_azimuthal_angle_offset(const float angle) = delete; + void set_azimuthal_angle_sampling(const float angle) = delete; - //! set new number of views, covering the same azimuthal angle range - /*! calls ProjDataInfo::set_num_views(), but makes sure that we cover the - same range of angles as before (usually, but not necessarily, 180 degrees) - by adjusting azimuthal_angle_sampling. - */ + //! set new number of views (currently calls error() unless nothing changes) virtual void set_num_views(const int new_num_views); - //! Get the azimuthal sampling (in radians) - inline float get_azimuthal_angle_sampling() const; - virtual inline float get_sampling_in_t(const Bin&) const; - virtual inline float get_sampling_in_m(const Bin&) const; - - //! Get the axial sampling (e.g in z_direction) - /*! - \warning The implementation of this function currently assumes that the axial - sampling is equal to the ring spacing for non-spanned data - (i.e. no axial compression), while it is half the - ring spacing for spanned data. - */ - inline float get_axial_sampling(int segment_num) const; - - //! Get average ring difference for the given segment - inline float get_average_ring_difference(int segment_num) const; - //! Get minimum ring difference for the given segment - inline int get_min_ring_difference(int segment_num) const; - //! Get maximum ring difference for the given segment - inline int get_max_ring_difference(int segment_num) const; - - //! Set minimum ring difference - void set_min_ring_difference(int min_ring_diff_v, int segment_num); - //! Set maximum ring difference - void set_max_ring_difference(int max_ring_diff_v, int segment_num); + float get_azimuthal_angle_sampling() const = delete; + float get_azimuthal_angle_offset() const = delete; + float get_ring_radius() const = delete; + void set_ring_radii_for_all_views(const VectorWithOffset& new_ring_radius) = delete; + VectorWithOffset get_ring_radii_for_all_views() const = delete; - virtual void set_num_axial_poss_per_segment(const VectorWithOffset& num_axial_poss_per_segment); - virtual void set_min_axial_pos_num(const int min_ax_pos_num, const int segment_num); - virtual void set_max_axial_pos_num(const int max_ax_pos_num, const int segment_num); - virtual void reduce_segment_range(const int min_segment_num, const int max_segment_num); - - //! Set detector ring radius for all views - inline void set_ring_radii_for_all_views(const VectorWithOffset& new_ring_radius); - - //! Get detector ring radius for all views - inline VectorWithOffset get_ring_radii_for_all_views() const; - - //! Get detector ring radius - inline float get_ring_radius() const; - inline float get_ring_radius( const int view_num) const; - - //! Get detector ring spacing + //! return an average ring-spacing (from Scanner) + //TODOBLOCK what does this mean in a generic case? inline float get_ring_spacing() const; - //! Set detector ring spacing - void set_ring_spacing(float ring_spacing_v); - - //! Get the mashing factor, i.e. how many 'original' views are combined - /*! mashing factor = Ndet/(2Nview) - This gets the result by comparing the number of detectors in the scanner_ptr - with the actual number of views. - \warning In the debug version, it is checked with an assert() that the number of - detectors is an even multiple of the number of views. This is not checked in - the normal version though. - */ - inline int get_view_mashing_factor() const; - - //! Find which segment a particular ring difference belongs to - /*! - \return Succeeded::yes when a corresponding segment was found. - */ - inline Succeeded - get_segment_num_for_ring_difference(int& segment_num, const int ring_diff) const; - - //! Find to which segment and axial position a ring pair contributes - /*! - \a ring1, \a ring2 have to be between 0 and scanner.get_num_rings()-1. - \return Succeeded::yes when a corresponding segment was found. - \warning axial_pos_num returned might be outside the actual range in the proj_data_info. - - \For CTI data with span, this essentially implements a 'michelogram'. - - \warning Current implementation assumes that the axial positions start from 0 for - the first ring-pair in the segment. - - \warning The implementation of this function currently assumes that the axial - sampling is equal to the ring spacing for non-spanned data - (i.e. no axial compression), while it is half the ring spacing for spanned data. - */ - inline Succeeded - get_segment_axial_pos_num_for_ring_pair(int& segment_num, - int& axial_pos_num, - const int ring1, - const int ring2) const; - - //! Find all ring pairs that contribute to a segment and axial position - /*! - \a ring1, \a ring2 will be between 0 and scanner.get_num_rings()-1. - - \warning The implementation of this function currently assumes that the axial - sampling is equal to the ring spacing for non-spanned data - (i.e. no axial compression), while it is half the ring spacing for spanned data. - */ - inline const RingNumPairs& - get_all_ring_pairs_for_segment_axial_pos_num(const int segment_num, - const int axial_pos_num) const; - //! Find the number of ring pairs that contribute to a segment and axial position - /*! - \warning The implementation of this function currently assumes that the axial - sampling is equal to the ring spacing for non-spanned data - (i.e. no axial compression), while it is half the ring spacing for spanned data. - */ - inline unsigned - get_num_ring_pairs_for_segment_axial_pos_num(const int segment_num, - const int axial_pos_num) const; - - //! Find a ring pair that contributes to a segment and axial position - /*! - \a ring1, \a ring2 will be between 0 and scanner.get_num_rings()-1. - - \warning Currently only works when no axial compression is used for the segment (i.e. - min_ring_diff = max_ring_diff). Otherwise, a error() will be called. + virtual inline float get_sampling_in_t(const Bin&) const; + virtual inline float get_sampling_in_m(const Bin&) const; - \warning The implementation of this function currently assumes that the axial - sampling is equal to the ring spacing for non-spanned data - (i.e. no axial compression), while it is half the ring spacing for spanned data. - */ - void - get_ring_pair_for_segment_axial_pos_num(int& ring1, - int& ring2, - const int segment_num, - const int axial_pos_num) const; + inline float get_axial_sampling(int segment_num) const override; + inline bool axial_sampling_is_uniform() const override; virtual std::string parameter_info() const; -protected: - - //! a variable that is set if the data corresponds to physical rings in the scanner - /*! This is (only) used to prevent get_segment_axial_pos_num_for_ring_pair() et al - to go wild. Indeed, for cases where there's cylindrical sampling, but not - really any physical rings associated to the sampling, those functions will - return invalid information. - - The prime case where this is used is for data corresponding to (nearly) - continuous detectors, such as DHCI systems, or the HiDAC. - - Ideally, this would be done by having a separate class for such systems which - does not contain the ring-difference et al information. This seems to make - the hierarchy too complicated though. - - \bug The value of this variable is currently set by checking if the scanner - is a HiDAC scanner. This needs to be changed. - if scanner is not HiDAC this variable is set to 1 otherwise to 0 (see constructor) - */ - bool sampling_corresponds_to_physical_rings; - -protected: - virtual bool blindly_equals(const root_type * const) const = 0; - private: - float azimuthal_angle_sampling; - VectorWithOffset ring_radius; - float ring_spacing; - VectorWithOffset min_ring_diff; - VectorWithOffset max_ring_diff; - - /* - Next members have to be mutable as they can be modified by const member - functions. We need this because of the presence of set_min_ring_difference() - which invalidates these precalculated arrays. - If your compiler does not support mutable (and you don't want to upgrade - it to something more sensible), your best bet is to remove the - set_*ring_difference functions, and move the content of - initialise_ring_diff_arrays() to the constructor. (Not recommended!) - */ - - //! This member will signal if the arrays below contain sensible info or not - mutable bool ring_diff_arrays_computed; - - //! This member stores the offsets used in get_m() - /* - //! warning This is not used in generic geometry. m is found directly from lors. - mutable VectorWithOffset m_offset; - */ - - //! This member stores the offsets used in get_segment_axial_pos_num_for_ring_pair() - mutable VectorWithOffset ax_pos_num_offset; - - //! This member stores a table converting ring differences to segment numbers - mutable VectorWithOffset ring_diff_to_segment_num; - - //! This member stores a table converting segment/axial_pos to ring1+ring2 - mutable VectorWithOffset > segment_axial_pos_to_ring1_plus_ring2; - - //! This function sets all of the above - void initialise_ring_diff_arrays() const; - - //! This function guarantees that ring_diff_arrays will be set but checks first if was done already - /*! This function is OPENMP thread-safe */ - inline void initialise_ring_diff_arrays_if_not_done_yet() const; - - //! This function checks if max_ring_diff is different from min_ring_diff (set to 2). - /*! in case of difference, there are 2 ax_pos per ring, i.e. an ax_pos between each two rings */ - inline int get_num_axial_poss_per_ring_inc(const int segment_num) const; - - //! This member will signal if the array below contain sensible info or not - mutable bool segment_axial_pos_to_ring_pair_allocated; - - //! This member stores a table used by get_all_ring_pairs_for_segment_axial_pos_num() - mutable VectorWithOffset< VectorWithOffset < shared_ptr > > - segment_axial_pos_to_ring_pair; - - //! allocate table - void allocate_segment_axial_pos_to_ring_pair() const; - - //! initialise one element of the above table - void compute_segment_axial_pos_to_ring_pair(const int segment_num, const int axial_pos_num) const; - //! to be used in get LOR virtual void find_cartesian_coordinates_of_detection(CartesianCoordinate3D& coord_1, CartesianCoordinate3D& coord_2, diff --git a/src/include/stir/ProjDataInfoGeneric.inl b/src/include/stir/ProjDataInfoGeneric.inl index 670714698..acdc1e5bd 100644 --- a/src/include/stir/ProjDataInfoGeneric.inl +++ b/src/include/stir/ProjDataInfoGeneric.inl @@ -41,30 +41,6 @@ START_NAMESPACE_STIR -void -ProjDataInfoGeneric:: -initialise_ring_diff_arrays_if_not_done_yet() const -{ - // for efficiency reasons, use "Double-Checked-Locking(DCL) pattern" with OpenMP atomic operation - // OpenMP v3.1 or later required - // thanks to yohjp: http://stackoverflow.com/questions/27975737/how-to-handle-cached-data-structures-with-multi-threading-e-g-openmp -#if defined(STIR_OPENMP) && _OPENMP >=201012 - bool initialised; -#pragma omp atomic read - initialised = ring_diff_arrays_computed; - - if (!initialised) -#endif - { -#if defined(STIR_OPENMP) -#pragma omp critical(PROJDATAINFOCYLINDRICALRINGDIFFARRAY) -#endif - { - if (!ring_diff_arrays_computed) - initialise_ring_diff_arrays(); - } - } -} //! find phi from correspoding lor float @@ -116,183 +92,35 @@ ProjDataInfoGeneric::get_tantheta(const Bin& bin) const float ProjDataInfoGeneric::get_sampling_in_m(const Bin& bin) const { - return get_axial_sampling(bin.segment_num()); + // TODOBLOCK + return get_scanner_ptr()->get_ring_spacing()/2; // TODO currently restricted to span=1 get_num_axial_poss_per_ring_inc(segment_num); + //return get_axial_sampling(bin.segment_num()); } -/* - warning Theta is not uniform anymore, so sampling in t does not make sense anymore. - Sampling parameters remained unchanged to be consistent with cylindrical version. -*/ float ProjDataInfoGeneric::get_sampling_in_t(const Bin& bin) const { - return get_axial_sampling(bin.segment_num())*get_costheta(bin); + // TODOBLOCK + return get_scanner_ptr()->get_ring_spacing()/2*get_costheta(bin); // TODO currently restricted to span=1 get_num_axial_poss_per_ring_inc(segment_num); + //return get_axial_sampling(bin.segment_num())*get_costheta(bin); } -int -ProjDataInfoGeneric:: -get_num_axial_poss_per_ring_inc(const int segment_num) const -{ - return - max_ring_diff[segment_num] != min_ring_diff[segment_num] ? - 2 : 1; -} - -float -ProjDataInfoGeneric::get_azimuthal_angle_sampling() const -{return azimuthal_angle_sampling;} - float ProjDataInfoGeneric::get_axial_sampling(int segment_num) const { - return ring_spacing/get_num_axial_poss_per_ring_inc(segment_num); -} - -float -ProjDataInfoGeneric::get_average_ring_difference(int segment_num) const -{ - // KT 05/07/2001 use float division here. - // In any reasonable case, min+max_ring_diff will be even. - // But some day, an unreasonable case will walk in. - return (min_ring_diff[segment_num] + max_ring_diff[segment_num])/2.F; -} - -int -ProjDataInfoGeneric::get_min_ring_difference(int segment_num) const -{ return min_ring_diff[segment_num]; } - -int -ProjDataInfoGeneric::get_max_ring_difference(int segment_num) const -{ return max_ring_diff[segment_num]; } - -float -ProjDataInfoGeneric::get_ring_radius() const -{ - if (this->ring_radius.get_min_index()!=0 || this->ring_radius.get_max_index()!=0) - { - // check if all elements are equal - for (VectorWithOffset::const_iterator iter=this->ring_radius.begin(); iter!= this->ring_radius.end(); ++iter) - { - if (*iter != *this->ring_radius.begin()) - error("get_ring_radius called for non-circular ring"); - } - } - return *this->ring_radius.begin(); -} - -void -ProjDataInfoGeneric::set_ring_radii_for_all_views(const VectorWithOffset& new_ring_radius) -{ - if (new_ring_radius.get_min_index() != this->get_min_view_num() || - new_ring_radius.get_max_index() != this->get_max_view_num()) - { - error("error set_ring_radii_for_all_views: you need to use correct range of view numbers"); - } - - this->ring_radius = new_ring_radius; + // TODOBLOCK should check sampling + return get_ring_spacing()/2; // TODO currently restricted to span=1 get_num_axial_poss_per_ring_inc(segment_num); } -VectorWithOffset -ProjDataInfoGeneric::get_ring_radii_for_all_views() const +bool ProjDataInfoGeneric::axial_sampling_is_uniform() const { - if (this->ring_radius.get_min_index()==0 && this->ring_radius.get_max_index()==0) - { - VectorWithOffset out(this->get_min_view_num(), this->get_max_view_num()); - out.fill(this->ring_radius[0]); - return out; - } - else - return this->ring_radius; -} - -float -ProjDataInfoGeneric::get_ring_radius( const int view_num) const -{ - if (this->ring_radius.get_min_index()==0 && this->ring_radius.get_max_index()==0) - return ring_radius[0]; - else - return ring_radius[view_num]; + // TODOBLOCK should check sampling + return true; } float ProjDataInfoGeneric::get_ring_spacing() const -{ return ring_spacing;} - -int -ProjDataInfoGeneric:: -get_view_mashing_factor() const -{ - // KT 10/05/2002 new assert - assert(get_scanner_ptr()->get_num_detectors_per_ring() > 0); - // KT 10/05/2002 moved assert here from constructor - assert(get_scanner_ptr()->get_num_detectors_per_ring() % (2*get_num_views()) == 0); - // KT 28/11/2001 do not pre-store anymore as set_num_views would invalidate it - return get_scanner_ptr()->get_num_detectors_per_ring() / (2*get_num_views()); -} - -Succeeded -ProjDataInfoGeneric:: -get_segment_num_for_ring_difference(int& segment_num, const int ring_diff) const -{ - if (!sampling_corresponds_to_physical_rings) - return Succeeded::no; - - // check currently necessary as reduce_segment does not reduce the size of the ring_diff arrays - if (ring_diff > get_max_ring_difference(get_max_segment_num()) || - ring_diff < get_min_ring_difference(get_min_segment_num())) - return Succeeded::no; - - this->initialise_ring_diff_arrays_if_not_done_yet(); - - segment_num = ring_diff_to_segment_num[ring_diff]; - // warning: relies on initialise_ring_diff_arrays to set invalid ring_diff to a too large segment_num - if (segment_num <= get_max_segment_num()) - return Succeeded::yes; - else - return Succeeded::no; -} - -Succeeded -ProjDataInfoGeneric:: -get_segment_axial_pos_num_for_ring_pair(int& segment_num, - int& ax_pos_num, - const int ring1, - const int ring2) const -{ - assert(0<=ring1); - assert(ring1get_num_rings()); - assert(0<=ring2); - assert(ring2get_num_rings()); - - // KT 01/08/2002 swapped rings - if (get_segment_num_for_ring_difference(segment_num, ring2-ring1) == Succeeded::no) - return Succeeded::no; - - // see initialise_ring_diff_arrays() for some info - ax_pos_num = (ring1 + ring2 - ax_pos_num_offset[segment_num])* - get_num_axial_poss_per_ring_inc(segment_num)/2; - return Succeeded::yes; -} +{ return get_scanner_ptr()->get_ring_spacing();} -const ProjDataInfoGeneric::RingNumPairs& -ProjDataInfoGeneric:: -get_all_ring_pairs_for_segment_axial_pos_num(const int segment_num, - const int axial_pos_num) const -{ - this->initialise_ring_diff_arrays_if_not_done_yet(); - if (is_null_ptr(segment_axial_pos_to_ring_pair[segment_num][axial_pos_num])) - compute_segment_axial_pos_to_ring_pair(segment_num, axial_pos_num); - return *segment_axial_pos_to_ring_pair[segment_num][axial_pos_num]; -} - -unsigned -ProjDataInfoGeneric:: -get_num_ring_pairs_for_segment_axial_pos_num(const int segment_num, - const int axial_pos_num) const -{ - return - static_cast( - this->get_all_ring_pairs_for_segment_axial_pos_num(segment_num,axial_pos_num).size()); -} END_NAMESPACE_STIR diff --git a/src/include/stir/ProjDataInfoGenericNoArcCorr.h b/src/include/stir/ProjDataInfoGenericNoArcCorr.h index 37dfef76c..118e62108 100644 --- a/src/include/stir/ProjDataInfoGenericNoArcCorr.h +++ b/src/include/stir/ProjDataInfoGenericNoArcCorr.h @@ -219,7 +219,7 @@ class ProjDataInfoGenericNoArcCorr : public ProjDataInfoGeneric arc-corrected data). */ void - get_all_det_pos_pairs_for_bin(vector >&, + get_all_det_pos_pairs_for_bin(std::vector >&, const Bin&) const; //! This gets Bin coordinates for a particular detector pair diff --git a/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx b/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx index e5b234b4c..cc8af5f1f 100644 --- a/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx +++ b/src/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.cxx @@ -183,6 +183,7 @@ find_relation_between_coordinate_systems(int& num_planes_per_scanner_ring, } #endif +#if 0 // disabled as now derived from Cylindrical // overloading for generic case static void find_relation_between_coordinate_systems(int& num_planes_per_scanner_ring, @@ -224,6 +225,7 @@ find_relation_between_coordinate_systems(int& num_planes_per_scanner_ring, + num_planes_per_scanner_ring*delta)/2; } } +#endif /*! The DiscretisedDensity pointer has to point to an object of type DiscretisedDensityOnCartesianGrid (or a derived type). @@ -380,12 +382,18 @@ DataSymmetriesForBins_PET_CartesianGrid this->do_symmetry_swap_segment = this->do_symmetry_swap_s = false; } + if (!dynamic_cast(proj_data_info_ptr.get())->axial_sampling_is_uniform()) + { + this->do_symmetry_shift_z = false; + this->do_symmetry_swap_segment = false; + } + // TODOBLOCK. should probably not call next function for non-uniform sampling find_relation_between_coordinate_systems( num_planes_per_scanner_ring, num_planes_per_axial_pos, axial_pos_to_z_offset, - static_cast(proj_data_info_ptr.get()), + dynamic_cast(proj_data_info_ptr.get()), cartesian_grid_info_ptr); } // generic implementation @@ -429,11 +437,18 @@ DataSymmetriesForBins_PET_CartesianGrid this->do_symmetry_shift_z = false; } + if (!dynamic_cast(proj_data_info_ptr.get())->axial_sampling_is_uniform()) + { + this->do_symmetry_shift_z = false; + this->do_symmetry_swap_segment = false; + } + + // TODOBLOCK. should probably not call next function for non-uniform sampling find_relation_between_coordinate_systems( num_planes_per_scanner_ring, num_planes_per_axial_pos, axial_pos_to_z_offset, - static_cast(proj_data_info_ptr.get()), + dynamic_cast(proj_data_info_ptr.get()), cartesian_grid_info_ptr); } } From 319d753ec800b912431794b383e9411d54992589 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Fri, 3 Dec 2021 10:37:24 +0000 Subject: [PATCH 40/81] [SWIG] ProjDataInfo changes - add generic/block proj_data_info - rename ProjDataInfo.ProjDataInfoCTI to ProjDataInfo.construct_proj_data_info, which now returns an object of the derived class. (Warning: the renaming breaks backwards compatibility) --- documentation/release_5.0.htm | 6 ++ src/swig/factory_shared.i | 95 +++++++++++++++++++++++++ src/swig/stir.i | 60 +++++++++++----- src/swig/test/python/test_buildblock.py | 16 +++-- 4 files changed, 157 insertions(+), 20 deletions(-) create mode 100644 src/swig/factory_shared.i diff --git a/documentation/release_5.0.htm b/documentation/release_5.0.htm index c329704ab..53c603134 100644 --- a/documentation/release_5.0.htm +++ b/documentation/release_5.0.htm @@ -69,9 +69,15 @@