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\"
diff --git a/documentation/release_5.0.htm b/documentation/release_5.0.htm
index 86e3613a9..3ed9befe6 100644
--- a/documentation/release_5.0.htm
+++ b/documentation/release_5.0.htm
@@ -15,6 +15,17 @@
Overall summary
- At least C++-11 is now required. We are not aware of any problems with more recent versions of C++.
+ - Initial support for PET scanners with block detectors or even generic location of detectors (less tested feature), taking into account
+ appropriate geometry. This is currently kept independent of the cylindrical scanner modelling used normally, but this will be change
+ in a future version. See PR #577. This work is described in
+ P. Khateri, J. Fischer, W. Lustermann, C. Tsoumpas, and G. Dissertori,
+ Implementation of cylindrical PET scanners with block detector geometry in STIR,
+ EJNMMI Physics, vol. 6, no. 1, p. 15, Jul. 2019, doi: 10.1186/s40658-019-0248-9.
+ V. Dao, E. Mikhaylova, M. L. Ahnen, J. Fischer , K. Thielemans, C. Tsoumpas,
+ Image Reconstruction for PET Scanners With Polygonal Prism Geometry Using STIR, proc. IEEE MIC 2021
+ This code was initially developed by Parisa Khateri and Michael Roethlisberger (ETH), and further improved and tested by Viet Dao (Leeds),
+ Daniel Deidda (NPL) and Kris Thielemans (UCL and ASC) with funding by the University of Leeds and Positrigo AG.
+
- View Offset Support enabled for the PET scanners, contributed by Palak Wadhwa (Leeds Univ), allowing fixing rotation of reconstructed images, see PR 181.
- Maximum Likelihood estimation of normalisation factors now includes estimation of geometric factors in 3D, see PR 619.
@@ -80,9 +91,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
-
@@ -100,6 +117,20 @@
Bug fixes
New functionality
+ 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
@@ -308,6 +345,8 @@ New functionality
KeyParser::parameter_info()
now outputs vectorised keys as well
+DetectionPosition
now has all comparison operators.
+
LOR
classes now no longer require phi at input to be between 0 and pi,
nor psi to be between 0 and 2 pi. They will standardise the input automatically.
diff --git a/examples/PET_simulation/generate_input_data.sh b/examples/PET_simulation/generate_input_data.sh
index ba3f5c4e8..9d792704e 100755
--- a/examples/PET_simulation/generate_input_data.sh
+++ b/examples/PET_simulation/generate_input_data.sh
@@ -40,6 +40,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 </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/examples/python/plot_scanner_LORs.py b/examples/python/plot_scanner_LORs.py
new file mode 100644
index 000000000..31ab67d6e
--- /dev/null
+++ b/examples/python/plot_scanner_LORs.py
@@ -0,0 +1,229 @@
+# 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('plot_scanner_LORs.py')
+# In ipython, you can use
+# %run plot_scanner_LORs.py
+
+# Copyright 2021 University College London
+# Copyright 2021 National Phyisical Laboratory
+
+# Author Daniel Deidda
+# Author Kris Thielemans
+
+# This file is part of STIR.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+# See STIR/LICENSE.txt for details
+
+
+#%% imports
+
+import stir
+import stirextra
+import pylab
+import numpy
+import math
+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(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()
+NaBl=scanner.get_num_axial_blocks()
+NtBu=scanner.get_num_transaxial_blocks()/scanner.get_num_transaxial_blocks_per_bucket()
+aBl_s=scanner.get_axial_block_spacing()
+tBl_s=scanner.get_transaxial_block_spacing()
+tC_s=scanner.get_transaxial_crystal_spacing()
+r=scanner.get_effective_ring_radius()
+
+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))
+max_r_diff=stir.IntVectorWithOffset((2*Nr-1))
+num_ax_pps=stir.IntVectorWithOffset((2*Nr-1))
+
+csi=math.pi/NtBu
+tBl_gap=tBl_s-NtCpBl*tC_s
+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_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
+ max_r_diff[i]=-Nr+1+i
+ if itB_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()),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*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.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();
+
+# # %% 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();
+# 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)
+
+# plt.close()
+
+#%% Plot 2D ZY LORs
+ax=plt.axes()
+plt.xlim([0, NaBl*aBl_s])
+plt.ylim([-rmax, rmax])
+ax.set_xlabel('Z ')
+ax.set_ylabel('Y ')
+
+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()),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.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()
+
+
+#%% Plot 3D LORs
+ax=plt.axes(projection='3d')
+ax.set_xlim([-rmax, rmax])
+ax.set_ylim([-rmax, rmax])
+ax.set_zlim([0, NaBl*aBl_s])
+ax.set_xlabel('X ')
+ax.set_ylabel('Y ')
+ax.set_zlabel('Z ')
+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()),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.gca().invert_yaxis()
+plt.savefig('3dLOR.png', format='png', dpi=300)
+plt.show()
\ No newline at end of file
diff --git a/recon_test_pack/run_test_simulate_and_recon.sh b/recon_test_pack/run_test_simulate_and_recon.sh
index ec2f8b7b1..d955c5814 100755
--- a/recon_test_pack/run_test_simulate_and_recon.sh
+++ b/recon_test_pack/run_test_simulate_and_recon.sh
@@ -70,6 +70,7 @@ LC_ALL=C
export LC_ALL
./simulate_PET_data_for_tests.sh
+
if [ $? -ne 0 ]; then
echo "Error running simulation"
exit 1
diff --git a/recon_test_pack/run_test_simulate_and_recon_with_motion.sh b/recon_test_pack/run_test_simulate_and_recon_with_motion.sh
index e6f925e1f..61fda5434 100755
--- a/recon_test_pack/run_test_simulate_and_recon_with_motion.sh
+++ b/recon_test_pack/run_test_simulate_and_recon_with_motion.sh
@@ -117,6 +117,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 <
#include
+#include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h"
+#include "stir/ProjDataInfoGenericNoArcCorr.h"
#ifndef STIR_NO_NAMESPACES
using std::binary_function;
@@ -614,6 +618,36 @@ 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);
+
+ axial_distance_between_crystals_in_cm = -0.1;
+ add_key("distance between crystals in axial direction (cm)",
+ &axial_distance_between_crystals_in_cm);
+
+ transaxial_distance_between_crystals_in_cm = -0.1;
+ add_key("distance between crystals in transaxial direction (cm)",
+ &transaxial_distance_between_crystals_in_cm);
+
+ axial_distance_between_blocks_in_cm = -0.1;
+ add_key("distance between blocks in axial direction (cm)",
+ &axial_distance_between_blocks_in_cm);
+
+ 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
+ //new keys for generic geometry
+ crystal_map = "";
+ add_key("Name of crystal map", &crystal_map);
+ //end of new keys for generic geometry
+
ignore_key("end scanner parameters");
effective_central_bin_size_in_cm = -1;
@@ -1104,7 +1138,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())
@@ -1240,6 +1285,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)
@@ -1278,7 +1354,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
@@ -1303,7 +1388,15 @@ 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.),
+ crystal_map
+ ));
bool is_consistent =
scanner_ptr_from_file->check_consistency() == Succeeded::yes;
@@ -1321,7 +1414,8 @@ bool InterfilePDFSHeader::post_processing()
-
+ if (scanner_geometry == "Cylindrical")
+ {
if (is_arccorrected)
{
if (effective_central_bin_size_in_cm <= 0)
@@ -1364,7 +1458,48 @@ bool InterfilePDFSHeader::post_processing()
data_info_sptr->get_sampling_in_s(Bin(0,0,0,0))/10.);
}
}
- //cerr << data_info_ptr->parameter_info() << endl;
+ }
+ else if(scanner_geometry == "BlocksOnCylindrical")// if block geometry
+ {
+ data_info_sptr.reset(
+ 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_sptr->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_sptr->get_sampling_in_s(Bin(0,0,0,0))/10.);
+ }
+ }
+ else // if generic geometry
+ {
+ data_info_sptr.reset(
+ 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_sptr->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_sptr->get_sampling_in_s(Bin(0,0,0,0))/10.);
+ }
+ }
+ //cerr << data_info_sptr->parameter_info() << endl;
// Set the bed position
data_info_sptr->set_bed_position_horizontal(bed_position_horizontal);
diff --git a/src/IO/interfile.cxx b/src/IO/interfile.cxx
index f0cd4c8de..2dbe67b2f 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.
SPDX-License-Identifier: Apache-2.0 AND License-ref-PARAPET-license
@@ -18,6 +19,7 @@
\author Sanida Mustafovic
\author PARAPET project
\author Richard Brown
+ \author Parisa Khateri
*/
// Pretty horrible implementations at the moment...
@@ -51,6 +53,9 @@
#include
#include
#include
+#include "stir/ProjDataInfoBlocksOnCylindricalNoArcCorr.h"
+#include "stir/ProjDataInfoGenericNoArcCorr.h"
+
#ifndef STIR_NO_NAMESPACES
using std::cerr;
@@ -1410,7 +1415,93 @@ write_basic_interfile_PDFS_header(const string& header_file_name,
} // end of cylindrical scanner
else
{
- // TODO something here
+ // !author Parisa Khateri
+ const shared_ptr proj_data_info_sptr =
+ dynamic_pointer_cast(pdfs.get_proj_data_info_sptr());
+
+ if (proj_data_info_sptr!=NULL)
+ {
+ // BlocksOncylindrical scanners
+ output_header << "minimum ring difference per segment := ";
+ {
+ std::vector::const_iterator seg = segment_sequence.begin();
+ output_header << "{ " << proj_data_info_sptr->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_sptr->get_scanner_ptr();
+#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"
+ "\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());
+#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"
+ "\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();
+
+ output_header << "effective central bin size (cm) := "
+ << proj_data_info_sptr->get_sampling_in_s(Bin(0,0,0,0))/10. << endl;
+
+ }// end of BlocksOnCylindrical scanner
+ else // generic scanner
+ {
+ const shared_ptr proj_data_info_sptr =
+ dynamic_pointer_cast(pdfs.get_proj_data_info_sptr());
+
+ if (proj_data_info_sptr!=NULL)
+ {
+ output_header << "minimum ring difference per segment := ";
+ {
+ std::vector::const_iterator seg = segment_sequence.begin();
+ output_header << "{ " << proj_data_info_sptr->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_sptr->get_scanner_ptr();
+
+ output_header << scanner.parameter_info();
+
+ 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
+
+ else error("write_basic_interfile_PDFS_header: Error casting the projdata to one of its geometries: Cylindrical/BlocksOnCylindrical/Genreic");
+ }
}
diff --git a/src/buildblock/CMakeLists.txt b/src/buildblock/CMakeLists.txt
index 2a5196748..0e4e647f9 100644
--- a/src/buildblock/CMakeLists.txt
+++ b/src/buildblock/CMakeLists.txt
@@ -88,6 +88,12 @@ set(${dir_LIB_SOURCES}
GeneralisedPoissonNoiseGenerator.cxx
FilePath.cxx
date_time_functions.cxx
+ DetectorCoordinateMap.cxx
+ GeometryBlocksOnCylindrical.cxx
+ ProjDataInfoBlocksOnCylindricalNoArcCorr.cxx
+ ProjDataInfoGeneric.cxx
+ ProjDataInfoGenericNoArcCorr.cxx
+ date_time_functions.cxx
)
if (HAVE_HDF5)
list(APPEND ${dir_LIB_SOURCES}
diff --git a/src/buildblock/DetectorCoordinateMap.cxx b/src/buildblock/DetectorCoordinateMap.cxx
new file mode 100644
index 000000000..cf8e5daab
--- /dev/null
+++ b/src/buildblock/DetectorCoordinateMap.cxx
@@ -0,0 +1,209 @@
+/*
+ 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
+
+ See STIR/LICENSE.txt for details
+*/
+/*!
+
+ \file
+ \ingroup buildblock
+ \brief Implementation of class stir::DetectorCoordinateMap
+
+ \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
+
+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 + "'");
+ }
+
+ det_pos_to_coord_type coord_map;
+ std::string line;
+ 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] = static_cast(atof(col[4+has_layer_index].c_str() ));
+ coord[2] = static_cast(atof(col[3+has_layer_index].c_str() ));
+ coord[3] = static_cast(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;
+ }
+ 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)
+ {
+#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;
+ 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)
+ {
+ ++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);
+}
+
+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
new file mode 100755
index 000000000..455992fe6
--- /dev/null
+++ b/src/buildblock/GeometryBlocksOnCylindrical.cxx
@@ -0,0 +1,157 @@
+
+/*
+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::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