diff --git a/documentation/release_6.3.htm b/documentation/release_6.3.htm
new file mode 100644
index 000000000..fc818f8db
--- /dev/null
+++ b/documentation/release_6.3.htm
@@ -0,0 +1,66 @@
+
+
+
+ Summary of changes in STIR release 6.3
+
+
+
+ Summary of changes in STIR release 6.3
+
+
+Overall summary
+
+
+Patch release info
+
+
+ Summary for end users (also to be read by developers)
+
+
+New functionality
+
+ -
+
ScatterSimulation
can now downsample the scanner transaxially (crystals per ring) for BlocksOnCylindrical
,
+ scanners, which speeds up ScatterEstimation
considerably. By default, downsampling the detectors per reading
+ is disabled for backwards compatibility.
+ PR #1291
+
+
+
+
+Changed functionality
+
+
+Bug fixes
+
+
+Build system
+
+
+Known problems
+ See our issue tracker.
+
+
+What is new for developers (aside from what should be obvious from the above):
+
+
+Changed functionality
+
+
+Bug fixes
+
+
+Other code changes
+
+
+Test changes
+
+
+C++ tests
+
+
+recon_test_pack
+
+
+
+
diff --git a/src/buildblock/extend_projdata.cxx b/src/buildblock/extend_projdata.cxx
index 1ecd11c15..59fa14288 100644
--- a/src/buildblock/extend_projdata.cxx
+++ b/src/buildblock/extend_projdata.cxx
@@ -93,7 +93,7 @@ extend_segment(const SegmentBySinogram& segment,
if (extend_without_wrapping)
{
out[axial_pos][min_dim[2] + view_edge] = out[axial_pos][min_dim[2] + view_extension];
- out[axial_pos][max_dim[2] - view_extension] = out[axial_pos][max_dim[2] - view_extension];
+ out[axial_pos][max_dim[2] - view_edge] = out[axial_pos][max_dim[2] - view_extension];
}
else if (flip_views)
{
diff --git a/src/buildblock/interpolate_projdata.cxx b/src/buildblock/interpolate_projdata.cxx
index 060e8aa35..8e0eee6a8 100644
--- a/src/buildblock/interpolate_projdata.cxx
+++ b/src/buildblock/interpolate_projdata.cxx
@@ -23,6 +23,7 @@
//#include "stir/display.h"
#include "stir/ProjDataInfo.h"
#include "stir/ProjDataInfoCylindricalNoArcCorr.h"
+#include "stir/ProjDataInfoGenericNoArcCorr.h"
#include "stir/IndexRange.h"
#include "stir/BasicCoordinate.h"
#include "stir/Sinogram.h"
@@ -191,6 +192,10 @@ interpolate_projdata(ProjData& proj_data_out,
error("interpolate_projdata needs both projection to be of a scanner with the same ring radius");
}
+ // handle BlocksOnCylindrical interpolation manually
+ if (proj_data_in_info.get_scanner_sptr()->get_scanner_geometry() != "Cylindrical")
+ return interpolate_blocks_on_cylindrical_projdata(proj_data_out, proj_data_in, remove_interleaving);
+
// initialise interpolator
BSpline::BSplinesRegularGrid<3, float, float> proj_data_interpolator(these_types);
for (int k = proj_data_out_info.get_min_tof_pos_num(); k <= proj_data_out_info.get_max_tof_pos_num(); ++k)
@@ -200,96 +205,319 @@ interpolate_projdata(ProjData& proj_data_out,
proj_data_in.get_segment_by_sinogram(0, k))
: proj_data_in.get_segment_by_sinogram(0, k);
+ // for Cylindrical, spacing is regular in all directions, which makes mapping trivial
std::function(const BasicCoordinate<3, int>&)> index_converter;
- if (proj_data_in_info.get_scanner_sptr()->get_scanner_geometry() == "Cylindrical")
- { // for Cylindrical, spacing is regular in all directions, which makes mapping trivial
- // especially in view direction, extending by 5 leads to much smaller artifacts
- proj_data_interpolator.set_coef(extend_segment(segment, 5, 5, 5));
-
- BasicCoordinate<3, double> offset, step;
- // out_index * step + offset = in_index
- const float in_sampling_m = proj_data_in_info.get_sampling_in_m(Bin(0, 0, 0, 0));
- const float out_sampling_m = proj_data_out_info.get_sampling_in_m(Bin(0, 0, 0, 0));
- // offset in 'in' index units
- offset[1] = (proj_data_out_info.get_m(Bin(0, 0, 0, 0)) - proj_data_in_info.get_m(Bin(0, 0, 0, 0))) / in_sampling_m;
- step[1] = out_sampling_m / in_sampling_m;
-
- const float in_sampling_phi = (proj_data_in_info.get_phi(Bin(0, 1, 0, 0)) - proj_data_in_info.get_phi(Bin(0, 0, 0, 0)))
- / (remove_interleaving ? 2 : 1);
- const float out_sampling_phi
- = proj_data_out_info.get_phi(Bin(0, 1, 0, 0)) - proj_data_out_info.get_phi(Bin(0, 0, 0, 0));
- offset[2]
- = (proj_data_out_info.get_phi(Bin(0, 0, 0, 0)) - proj_data_in_info.get_phi(Bin(0, 0, 0, 0))) / in_sampling_phi;
- step[2] = out_sampling_phi / in_sampling_phi;
-
- const float in_sampling_s = proj_data_in_info.get_sampling_in_s(Bin(0, 0, 0, 0));
- const float out_sampling_s = proj_data_out_info.get_sampling_in_s(Bin(0, 0, 0, 0));
- offset[3] = (proj_data_out_info.get_s(Bin(0, 0, 0, 0)) - proj_data_in_info.get_s(Bin(0, 0, 0, 0))) / in_sampling_s;
- step[3] = out_sampling_s / in_sampling_s;
-
- // define a function to translate indices in the output proj data to indices in input proj data
- index_converter
- = [&proj_data_out_info, offset, step](const BasicCoordinate<3, int>& index_out) -> BasicCoordinate<3, double> {
- // translate to indices in input proj data
- BasicCoordinate<3, double> index_in;
- for (auto dim = 1; dim <= 3; dim++)
- index_in[dim] = index_out[dim] * step[dim] + offset[dim];
-
- return index_in;
- };
- }
- else
- { // for BlocksOnCylindrical, views and tangential positions are not subsampled and can be mapped 1:1
- if (proj_data_in_info.get_num_tangential_poss() != proj_data_out_info.get_num_tangential_poss())
- {
- error("Interpolation of BlocksOnCylindrical scanners assumes that number of tangential positions "
- "is the same in the downsampled scanner.");
- }
- if (proj_data_in_info.get_num_views() != proj_data_out_info.get_num_views())
- {
- error("Interpolation of BlocksOnCylindrical scanners assumes that number of views "
- "is the same in the downsampled scanner.");
- }
+ // especially in view direction, extending by 5 leads to much smaller artifacts
+ proj_data_interpolator.set_coef(extend_segment(segment, 5, 5, 5));
+
+ BasicCoordinate<3, double> offset, step;
+ // out_index * step + offset = in_index
+ const float in_sampling_m = proj_data_in_info.get_sampling_in_m(Bin(0, 0, 0, 0));
+ const float out_sampling_m = proj_data_out_info.get_sampling_in_m(Bin(0, 0, 0, 0));
+ // offset in 'in' index units
+ offset[1] = (proj_data_out_info.get_m(Bin(0, 0, 0, 0)) - proj_data_in_info.get_m(Bin(0, 0, 0, 0))) / in_sampling_m;
+ step[1] = out_sampling_m / in_sampling_m;
+
+ const float in_sampling_phi = (proj_data_in_info.get_phi(Bin(0, 1, 0, 0)) - proj_data_in_info.get_phi(Bin(0, 0, 0, 0)))
+ / (remove_interleaving ? 2 : 1);
+ const float out_sampling_phi = proj_data_out_info.get_phi(Bin(0, 1, 0, 0)) - proj_data_out_info.get_phi(Bin(0, 0, 0, 0));
+ offset[2] = (proj_data_out_info.get_phi(Bin(0, 0, 0, 0)) - proj_data_in_info.get_phi(Bin(0, 0, 0, 0))) / in_sampling_phi;
+ step[2] = out_sampling_phi / in_sampling_phi;
+
+ const float in_sampling_s = proj_data_in_info.get_sampling_in_s(Bin(0, 0, 0, 0));
+ const float out_sampling_s = proj_data_out_info.get_sampling_in_s(Bin(0, 0, 0, 0));
+ offset[3] = (proj_data_out_info.get_s(Bin(0, 0, 0, 0)) - proj_data_in_info.get_s(Bin(0, 0, 0, 0))) / in_sampling_s;
+ step[3] = out_sampling_s / in_sampling_s;
+
+ // define a function to translate indices in the output proj data to indices in input proj data
+ index_converter
+ = [&proj_data_out_info, offset, step](const BasicCoordinate<3, int>& index_out) -> BasicCoordinate<3, double> {
+ // translate to indices in input proj data
+ BasicCoordinate<3, double> index_in;
+ for (auto dim = 1; dim <= 3; dim++)
+ index_in[dim] = index_out[dim] * step[dim] + offset[dim];
+
+ return index_in;
+ };
- // only extending in axial direction - an extension of 2 was found to be sufficient
- proj_data_interpolator.set_coef(extend_segment(segment, 0, 2, 0));
+ SegmentBySinogram sino_3D_out = proj_data_out.get_empty_segment_by_sinogram(0, false, k);
+ sample_function_using_index_converter(sino_3D_out, proj_data_interpolator, index_converter);
- auto m_offset = proj_data_in_info.get_m(Bin(0, 0, 0, 0));
- auto m_sampling = proj_data_in_info.get_sampling_in_m(Bin(0, 0, 0, 0));
+ if (proj_data_out.set_segment(sino_3D_out) == Succeeded::no)
+ return Succeeded::no;
+ }
+ return Succeeded::yes;
+}
- // confirm that proj_data_in has equidistant sampling in m
- for (auto axial_pos = proj_data_in_info.get_min_axial_pos_num(0);
- axial_pos <= proj_data_in_info.get_max_axial_pos_num(0);
- axial_pos++)
- {
- if (abs(m_sampling - proj_data_in_info.get_sampling_in_m(Bin(0, 0, axial_pos, 0))) > 1E-4)
- error("input projdata to interpolate_projdata are not equidistantly sampled in m.");
- }
+//! This function interpolates BlocksOnCylindrical proj data taking bucket intersections and gaps into account.
+/*! The above proj data interpolation function works well for cylindrical scanners where there are no large discontinuities.
+For BlocksOnCylindrical scanners, the sudden change of angle from one bucket to the next and the gaps between blocks and buckets
+lead to interpolation artifacts if not taken into account. Therefore, this function does the interpolation in physical space
+rather than directly on the proj data bin values. For each bin in proj_data_out (the full size proj data), we find the four
+closest LORs in the downsampled proj_data_in. These are then weighted using bilinear interpolation based on the crystal positions
+of the two endpoints of the LOR.
+*/
+Succeeded
+interpolate_blocks_on_cylindrical_projdata(ProjData& proj_data_out, const ProjData& proj_data_in, bool remove_interleaving)
+{
+ const ProjDataInfo& proj_data_in_info = *proj_data_in.get_proj_data_info_sptr();
+ const ProjDataInfo& proj_data_out_info = *proj_data_out.get_proj_data_info_sptr();
- // define a function to translate indices in the output proj data to indices in input proj data
- index_converter = [&proj_data_out_info, m_offset, m_sampling](
- const BasicCoordinate<3, int>& index_out) -> BasicCoordinate<3, double> {
- // translate index on output to coordinate
- auto bin
- = Bin(0 /* segment */, index_out[2] /* view */, index_out[1] /* axial pos */, index_out[3] /* tangential pos */);
- auto out_m = proj_data_out_info.get_m(bin);
-
- // translate to indices in input proj data
- BasicCoordinate<3, double> index_in;
- index_in[1] = (out_m - m_offset) / m_sampling;
- index_in[2] = index_out[2];
- index_in[3] = index_out[3];
-
- return index_in;
- };
- }
+ auto m_offset = proj_data_in_info.get_m(Bin(0, 0, 0, 0));
+ auto m_sampling = proj_data_in_info.get_sampling_in_m(Bin(0, 0, 0, 0));
+ for (int k = proj_data_out_info.get_min_tof_pos_num(); k <= proj_data_out_info.get_max_tof_pos_num(); ++k)
+ {
+ SegmentBySinogram segment
+ = remove_interleaving ? make_non_interleaved_segment(*(make_non_interleaved_proj_data_info(proj_data_in_info)),
+ proj_data_in.get_segment_by_sinogram(0, k))
+ : proj_data_in.get_segment_by_sinogram(0, k);
SegmentBySinogram sino_3D_out = proj_data_out.get_empty_segment_by_sinogram(0, false, k);
- sample_function_using_index_converter(sino_3D_out, proj_data_interpolator, index_converter);
+ BasicCoordinate<3, int> min_out, max_out;
+ IndexRange<3> out_range = sino_3D_out.get_index_range();
+ if (!out_range.get_regular_range(min_out, max_out))
+ warning("Output must be regular range!");
+
+ // confirm that proj_data_in has equidistant sampling in m
+ for (auto axial_pos = proj_data_in_info.get_min_axial_pos_num(0); axial_pos <= proj_data_in_info.get_max_axial_pos_num(0);
+ axial_pos++)
+ {
+ if (abs(m_sampling - proj_data_in_info.get_sampling_in_m(Bin(0, 0, axial_pos, 0))) > 1E-4)
+ error("input projdata to interpolate_projdata are not equidistantly sampled in m.");
+ }
+
+ BasicCoordinate<3, int> index_out;
+ for (index_out[1] = min_out[1]; index_out[1] <= max_out[1]; ++index_out[1])
+ {
+ for (index_out[2] = min_out[2]; index_out[2] <= max_out[2]; ++index_out[2])
+ {
+ for (index_out[3] = min_out[3]; index_out[3] <= max_out[3]; ++index_out[3])
+ {
+ // translate index on output to coordinate
+ auto bin
+ = Bin(0 /* segment */, index_out[2] /* view */,
+ index_out[1] /* axial pos */, index_out[3] /* tangential pos
+ */);
+ auto out_m = proj_data_out_info.get_m(bin);
+ double axial_idx = (out_m - m_offset) / m_sampling;
+ int axial_floor = std::floor(axial_idx);
+ int axial_ceil = std::ceil(axial_idx);
+ if (axial_floor == axial_ceil)
+ {
+ if (axial_floor == 0)
+ axial_ceil++;
+ else
+ axial_floor--;
+ }
+
+ // find the two crystals for this bin and on which buckets (modules) they are
+ int det1_num_out, det2_num_out;
+ const auto proj_data_out_info_ptr = dynamic_cast(&proj_data_out_info);
+ proj_data_out_info_ptr->get_det_num_pair_for_view_tangential_pos_num(det1_num_out,
+ det2_num_out,
+ index_out[2], /*view*/
+ index_out[3] /* tangential pos */);
+ const int dets_per_module_out = proj_data_out_info.get_scanner_sptr()->get_num_transaxial_crystals_per_bucket();
+ const int det1_module = det1_num_out / dets_per_module_out;
+ const int det2_module = det2_num_out / dets_per_module_out;
+
+ // translate the crystal position on each module from the full size scanner to the downsampled scanner
+ const auto proj_data_in_info_ptr = dynamic_cast(&proj_data_in_info);
+ const int dets_per_module_in
+ = proj_data_in_info_ptr->get_scanner_sptr()->get_num_transaxial_crystals_per_bucket();
+ const int crystal1_out_module_idx = det1_num_out % dets_per_module_out;
+ const double crystal1_out_module_pos
+ = std::floor(static_cast(crystal1_out_module_idx)
+ / proj_data_out_info_ptr->get_scanner_sptr()->get_num_transaxial_crystals_per_block())
+ * proj_data_out_info_ptr->get_scanner_sptr()->get_transaxial_block_spacing()
+ + static_cast(
+ crystal1_out_module_idx
+ % proj_data_out_info_ptr->get_scanner_sptr()->get_num_transaxial_crystals_per_block())
+ * proj_data_out_info_ptr->get_scanner_sptr()->get_transaxial_crystal_spacing();
+ const double crystal1_num_in
+ = det1_module * dets_per_module_in
+ + crystal1_out_module_pos / proj_data_in_info.get_scanner_sptr()->get_transaxial_crystal_spacing();
+ const int crystal2_out_module_idx = det2_num_out % dets_per_module_out;
+ const double crystal2_out_module_pos
+ = std::floor(static_cast(crystal2_out_module_idx)
+ / proj_data_out_info_ptr->get_scanner_sptr()->get_num_transaxial_crystals_per_block())
+ * proj_data_out_info_ptr->get_scanner_sptr()->get_transaxial_block_spacing()
+ + static_cast(
+ crystal2_out_module_idx
+ % proj_data_out_info_ptr->get_scanner_sptr()->get_num_transaxial_crystals_per_block())
+ * proj_data_out_info_ptr->get_scanner_sptr()->get_transaxial_crystal_spacing();
+ const double crystal2_num_in
+ = det2_module * dets_per_module_in
+ + crystal2_out_module_pos / proj_data_in_info.get_scanner_sptr()->get_transaxial_crystal_spacing();
+ const auto crystal1_num_in_floor
+ = std::max(static_cast(std::floor(crystal1_num_in)), det1_module * dets_per_module_in);
+ const auto crystal1_num_in_ceil
+ = std::min(static_cast(std::ceil(crystal1_num_in)), (det1_module + 1) * dets_per_module_in - 1);
+ const auto crystal2_num_in_floor
+ = std::max(static_cast(std::floor(crystal2_num_in)), det2_module * dets_per_module_in);
+ const auto crystal2_num_in_ceil
+ = std::min(static_cast(std::ceil(crystal2_num_in)), (det2_module + 1) * dets_per_module_in - 1);
+
+ // translate to indices in input proj data
+ BasicCoordinate<3, int> index_in;
+
+ // in this case we can skip parts of the interpolation
+ if (crystal1_num_in_floor == crystal1_num_in_ceil)
+ {
+ if (crystal2_num_in_floor == crystal2_num_in_ceil)
+ {
+ int one_view, one_tang;
+ proj_data_in_info_ptr->get_view_tangential_pos_num_for_det_num_pair(
+ one_view, one_tang, crystal1_num_in_floor, crystal2_num_in_floor);
+ one_tang = std::min(std::max(proj_data_in_info_ptr->get_min_tangential_pos_num(), one_tang),
+ proj_data_in_info_ptr->get_max_tangential_pos_num());
+
+ index_in[1] = std::max(axial_floor, proj_data_in_info_ptr->get_min_axial_pos_num(0));
+ index_in[2] = one_view;
+ index_in[3] = one_tang;
+ sino_3D_out[index_out] += segment[index_in] * (axial_ceil - axial_idx);
+
+ index_in[1] = std::min(axial_ceil, proj_data_in_info_ptr->get_max_axial_pos_num(0));
+ sino_3D_out[index_out] += segment[index_in] * (axial_idx - axial_floor);
+ }
+ else
+ {
+ int ff_view, fc_view;
+ int ff_tang, fc_tang;
+ proj_data_in_info_ptr->get_view_tangential_pos_num_for_det_num_pair(
+ ff_view, ff_tang, crystal1_num_in_floor, crystal2_num_in_floor);
+ proj_data_in_info_ptr->get_view_tangential_pos_num_for_det_num_pair(
+ fc_view, fc_tang, crystal1_num_in_floor, crystal2_num_in_ceil);
+
+ ff_tang = std::min(std::max(proj_data_in_info_ptr->get_min_tangential_pos_num(), ff_tang),
+ proj_data_in_info_ptr->get_max_tangential_pos_num());
+ fc_tang = std::min(std::max(proj_data_in_info_ptr->get_min_tangential_pos_num(), fc_tang),
+ proj_data_in_info_ptr->get_max_tangential_pos_num());
+
+ index_in[1] = std::max(axial_floor, proj_data_in_info_ptr->get_min_axial_pos_num(0));
+ index_in[2] = ff_view;
+ index_in[3] = ff_tang;
+ sino_3D_out[index_out]
+ += segment[index_in] * (axial_ceil - axial_idx) * (crystal2_num_in_ceil - crystal2_num_in);
+ index_in[2] = fc_view;
+ index_in[3] = fc_tang;
+ sino_3D_out[index_out]
+ += segment[index_in] * (axial_ceil - axial_idx) * (crystal2_num_in - crystal2_num_in_floor);
+ index_in[1] = std::min(axial_ceil, proj_data_in_info_ptr->get_max_axial_pos_num(0));
+ sino_3D_out[index_out]
+ += segment[index_in] * (axial_idx - axial_floor) * (crystal2_num_in - crystal2_num_in_floor);
+ index_in[2] = ff_view;
+ index_in[3] = ff_tang;
+ sino_3D_out[index_out]
+ += segment[index_in] * (axial_idx - axial_floor) * (crystal2_num_in_ceil - crystal2_num_in);
+ }
+ }
+ else if (crystal2_num_in_floor == crystal2_num_in_ceil)
+ {
+ int ff_view, cf_view;
+ int ff_tang, cf_tang;
+ proj_data_in_info_ptr->get_view_tangential_pos_num_for_det_num_pair(
+ ff_view, ff_tang, crystal1_num_in_floor, crystal2_num_in_floor);
+ proj_data_in_info_ptr->get_view_tangential_pos_num_for_det_num_pair(
+ cf_view, cf_tang, crystal1_num_in_ceil, crystal2_num_in_floor);
+
+ ff_tang = std::min(std::max(proj_data_in_info_ptr->get_min_tangential_pos_num(), ff_tang),
+ proj_data_in_info_ptr->get_max_tangential_pos_num());
+ cf_tang = std::min(std::max(proj_data_in_info_ptr->get_min_tangential_pos_num(), cf_tang),
+ proj_data_in_info_ptr->get_max_tangential_pos_num());
+
+ index_in[1] = std::max(axial_floor, proj_data_in_info_ptr->get_min_axial_pos_num(0));
+ index_in[2] = ff_view;
+ index_in[3] = ff_tang;
+ sino_3D_out[index_out]
+ += segment[index_in] * (axial_ceil - axial_idx) * (crystal1_num_in_ceil - crystal1_num_in);
+ index_in[2] = cf_view;
+ index_in[3] = cf_tang;
+ sino_3D_out[index_out]
+ += segment[index_in] * (axial_ceil - axial_idx) * (crystal1_num_in - crystal1_num_in_floor);
+ index_in[1] = std::min(axial_ceil, proj_data_in_info_ptr->get_max_axial_pos_num(0));
+ sino_3D_out[index_out]
+ += segment[index_in] * (axial_idx - axial_floor) * (crystal1_num_in - crystal1_num_in_floor);
+ index_in[2] = ff_view;
+ index_in[3] = ff_tang;
+ sino_3D_out[index_out]
+ += segment[index_in] * (axial_idx - axial_floor) * (crystal1_num_in_ceil - crystal1_num_in);
+ }
+ else // in this case we need to do a trilinear interpolation
+ {
+ int ff_view, fc_view, cf_view, cc_view;
+ int ff_tang, fc_tang, cf_tang, cc_tang;
+ proj_data_in_info_ptr->get_view_tangential_pos_num_for_det_num_pair(
+ ff_view, ff_tang, crystal1_num_in_floor, crystal2_num_in_floor);
+ proj_data_in_info_ptr->get_view_tangential_pos_num_for_det_num_pair(
+ fc_view, fc_tang, crystal1_num_in_floor, crystal2_num_in_ceil);
+ proj_data_in_info_ptr->get_view_tangential_pos_num_for_det_num_pair(
+ cf_view, cf_tang, crystal1_num_in_ceil, crystal2_num_in_floor);
+ proj_data_in_info_ptr->get_view_tangential_pos_num_for_det_num_pair(
+ cc_view, cc_tang, crystal1_num_in_ceil, crystal2_num_in_ceil);
+
+ // TODO: why can we get positions out that are not even in the proj data?!
+ ff_tang = std::min(std::max(proj_data_in_info_ptr->get_min_tangential_pos_num(), ff_tang),
+ proj_data_in_info_ptr->get_max_tangential_pos_num());
+ fc_tang = std::min(std::max(proj_data_in_info_ptr->get_min_tangential_pos_num(), fc_tang),
+ proj_data_in_info_ptr->get_max_tangential_pos_num());
+ cf_tang = std::min(std::max(proj_data_in_info_ptr->get_min_tangential_pos_num(), cf_tang),
+ proj_data_in_info_ptr->get_max_tangential_pos_num());
+ cc_tang = std::min(std::max(proj_data_in_info_ptr->get_min_tangential_pos_num(), cc_tang),
+ proj_data_in_info_ptr->get_max_tangential_pos_num());
+
+ index_in[1] = std::max(axial_floor, proj_data_in_info_ptr->get_min_axial_pos_num(0));
+ index_in[2] = ff_view;
+ index_in[3] = ff_tang;
+ sino_3D_out[index_out] += segment[index_in] * (axial_ceil - axial_idx)
+ * (crystal1_num_in_ceil - crystal1_num_in)
+ * (crystal2_num_in_ceil - crystal2_num_in);
+ index_in[2] = fc_view;
+ index_in[3] = fc_tang;
+ sino_3D_out[index_out] += segment[index_in] * (axial_ceil - axial_idx)
+ * (crystal1_num_in_ceil - crystal1_num_in)
+ * (crystal2_num_in - crystal2_num_in_floor);
+ index_in[2] = cc_view;
+ index_in[3] = cc_tang;
+ sino_3D_out[index_out] += segment[index_in] * (axial_ceil - axial_idx)
+ * (crystal1_num_in - crystal1_num_in_floor)
+ * (crystal2_num_in - crystal2_num_in_floor);
+ index_in[2] = cf_view;
+ index_in[3] = cf_tang;
+ sino_3D_out[index_out] += segment[index_in] * (axial_ceil - axial_idx)
+ * (crystal1_num_in - crystal1_num_in_floor)
+ * (crystal2_num_in_ceil - crystal2_num_in);
+
+ index_in[1] = std::min(axial_ceil, proj_data_in_info_ptr->get_max_axial_pos_num(0));
+ index_in[2] = ff_view;
+ index_in[3] = ff_tang;
+ sino_3D_out[index_out] += segment[index_in] * (axial_idx - axial_floor)
+ * (crystal1_num_in_ceil - crystal1_num_in)
+ * (crystal2_num_in_ceil - crystal2_num_in);
+ index_in[2] = fc_view;
+ index_in[3] = fc_tang;
+ sino_3D_out[index_out] += segment[index_in] * (axial_idx - axial_floor)
+ * (crystal1_num_in_ceil - crystal1_num_in)
+ * (crystal2_num_in - crystal2_num_in_floor);
+ index_in[2] = cc_view;
+ index_in[3] = cc_tang;
+ sino_3D_out[index_out] += segment[index_in] * (axial_idx - axial_floor)
+ * (crystal1_num_in - crystal1_num_in_floor)
+ * (crystal2_num_in - crystal2_num_in_floor);
+ index_in[2] = cf_view;
+ index_in[3] = cf_tang;
+ sino_3D_out[index_out] += segment[index_in] * (axial_idx - axial_floor)
+ * (crystal1_num_in - crystal1_num_in_floor)
+ * (crystal2_num_in_ceil - crystal2_num_in);
+ }
+ }
+ }
+ }
if (proj_data_out.set_segment(sino_3D_out) == Succeeded::no)
return Succeeded::no;
}
+
return Succeeded::yes;
}
diff --git a/src/include/stir/interpolate_projdata.h b/src/include/stir/interpolate_projdata.h
index 43889ca8a..0fe97060c 100644
--- a/src/include/stir/interpolate_projdata.h
+++ b/src/include/stir/interpolate_projdata.h
@@ -59,6 +59,8 @@ Succeeded interpolate_projdata(ProjData& proj_data_out,
const ProjData& proj_data_in,
const BasicCoordinate<3, BSpline::BSplineType>& these_types,
const bool remove_interleaving);
+Succeeded
+interpolate_blocks_on_cylindrical_projdata(ProjData& proj_data_out, const ProjData& proj_data_in, bool remove_interleaving);
//@}
END_NAMESPACE_STIR
diff --git a/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl b/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl
index 16f92a2e7..661744d50 100644
--- a/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl
+++ b/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl
@@ -216,6 +216,7 @@ DataSymmetriesForBins_PET_CartesianGrid::find_sym_op_bin0(int segment_num, int v
// No symmetry is implemented for generic scanner
return new TrivialSymmetryOperation();
}
+ return new TrivialSymmetryOperation();
}
// from symmetries
@@ -390,6 +391,8 @@ DataSymmetriesForBins_PET_CartesianGrid::find_sym_op_general_bin(int s, int segm
// No symmetry is implemented for generic scanner
return new TrivialSymmetryOperation();
}
+
+ return new TrivialSymmetryOperation();
}
bool
diff --git a/src/include/stir/scatter/ScatterEstimation.h b/src/include/stir/scatter/ScatterEstimation.h
index 38df94df4..785cd3b25 100644
--- a/src/include/stir/scatter/ScatterEstimation.h
+++ b/src/include/stir/scatter/ScatterEstimation.h
@@ -174,6 +174,12 @@ class ScatterEstimation : public ParsingObject
inline void set_num_iterations(int);
+ inline unsigned int get_half_filter_width() const;
+ inline void set_half_filter_width(unsigned int);
+
+ inline void
+ set_downsample_scanner(bool downsample_scanner, int downsampled_number_of_rings = -1, int downsampled_detectors_per_ring = -1);
+
void set_output_scatter_estimate_prefix(const std::string&);
void set_export_scatter_estimates_of_each_iteration(bool);
@@ -382,6 +388,9 @@ class ScatterEstimation : public ParsingObject
float min_scale_value;
bool downsample_scanner_bool;
+ int downsampled_number_of_rings;
+ int downsampled_detectors_per_ring;
+
//!
unsigned int half_filter_width;
diff --git a/src/include/stir/scatter/ScatterEstimation.inl b/src/include/stir/scatter/ScatterEstimation.inl
index 73336a24d..dc8bd9f2b 100644
--- a/src/include/stir/scatter/ScatterEstimation.inl
+++ b/src/include/stir/scatter/ScatterEstimation.inl
@@ -66,4 +66,26 @@ ScatterEstimation::set_num_iterations(int arg)
this->num_scatter_iterations = arg;
}
+unsigned int
+ScatterEstimation::get_half_filter_width() const
+{
+ return this->half_filter_width;
+}
+
+void
+ScatterEstimation::set_half_filter_width(unsigned int arg)
+{
+ this->half_filter_width = arg;
+}
+
+void
+ScatterEstimation::set_downsample_scanner(bool downsample_scanner,
+ int downsampled_number_of_rings,
+ int downsampled_detectors_per_ring)
+{
+ this->downsample_scanner_bool = downsample_scanner;
+ this->downsampled_number_of_rings = downsampled_number_of_rings;
+ this->downsampled_detectors_per_ring = downsampled_detectors_per_ring;
+}
+
END_NAMESPACE_STIR
diff --git a/src/scatter_buildblock/ScatterEstimation.cxx b/src/scatter_buildblock/ScatterEstimation.cxx
index 3ceb4cf1d..07772ae99 100644
--- a/src/scatter_buildblock/ScatterEstimation.cxx
+++ b/src/scatter_buildblock/ScatterEstimation.cxx
@@ -23,6 +23,7 @@
#include "stir/recon_buildblock/ChainedBinNormalisation.h"
#include "stir/ProjDataInterfile.h"
#include "stir/ProjDataInMemory.h"
+#include "stir/inverse_SSRB.h"
#include "stir/ExamInfo.h"
#include "stir/ProjDataInfo.h"
#include "stir/ProjDataInfoCylindricalNoArcCorr.h"
@@ -77,6 +78,8 @@ ScatterEstimation::set_defaults()
this->override_scanner_template = true;
this->override_density_image = true;
this->downsample_scanner_bool = true;
+ this->downsampled_number_of_rings = -1;
+ this->downsampled_detectors_per_ring = -1;
this->remove_interleaving = true;
this->atten_image_filename = "";
this->atten_coeff_filename = "";
@@ -124,6 +127,8 @@ ScatterEstimation::initialise_keymap()
this->parser.add_parsing_key("Scatter Simulation type", &this->scatter_simulation_sptr);
this->parser.add_key("scatter simulation parameter filename", &this->scatter_sim_par_filename);
this->parser.add_key("use scanner downsampling in scatter simulation", &this->downsample_scanner_bool);
+ this->parser.add_key("override number of downsampled rings", &this->downsampled_number_of_rings);
+ this->parser.add_key("override number of downsampled detectors per ring", &this->downsampled_detectors_per_ring);
this->parser.add_key("override attenuation image", &this->override_density_image);
this->parser.add_key("override scanner template", &this->override_scanner_template);
@@ -597,7 +602,7 @@ ScatterEstimation::set_up()
}
if (this->downsample_scanner_bool)
- this->scatter_simulation_sptr->downsample_scanner();
+ this->scatter_simulation_sptr->downsample_scanner(this->downsampled_number_of_rings, this->downsampled_detectors_per_ring);
// Check if Load a mask proj_data
@@ -834,7 +839,7 @@ ScatterEstimation::process_data()
float local_min_scale_value = 0.5f;
float local_max_scale_value = 0.5f;
- stir::BSpline::BSplineType spline_type = stir::BSpline::quadratic;
+ stir::BSpline::BSplineType spline_type = stir::BSpline::linear;
// This has been set to 2D or 3D in the set_up()
shared_ptr unscaled_est_projdata_sptr(
@@ -1026,16 +1031,13 @@ ScatterEstimation::process_data()
shared_ptr normalisation_factors_3d_sptr
= this->get_normalisation_object_sptr(this->multiplicative_binnorm_sptr);
- upsample_and_fit_scatter_estimate(*scatter_estimate_sptr,
- *this->input_projdata_sptr,
- *temp_projdata,
- *normalisation_factors_3d_sptr,
- *this->input_projdata_sptr,
- 1.0f,
- 1.0f,
- 1,
- spline_type,
- false);
+ ProjDataInMemory interpolated_scatter(this->input_projdata_sptr->get_exam_info_sptr(),
+ this->input_projdata_sptr->get_proj_data_info_sptr()->create_shared_clone());
+ inverse_SSRB(interpolated_scatter, *temp_projdata);
+ normalisation_factors_3d_sptr->set_up(this->input_projdata_sptr->get_exam_info_sptr(),
+ this->input_projdata_sptr->get_proj_data_info_sptr()->create_shared_clone());
+ normalisation_factors_3d_sptr->undo(interpolated_scatter);
+ scatter_estimate_sptr->fill(interpolated_scatter);
}
else
{
diff --git a/src/scatter_buildblock/ScatterSimulation.cxx b/src/scatter_buildblock/ScatterSimulation.cxx
index 8411b3f53..522178b83 100644
--- a/src/scatter_buildblock/ScatterSimulation.cxx
+++ b/src/scatter_buildblock/ScatterSimulation.cxx
@@ -841,19 +841,51 @@ ScatterSimulation::downsample_scanner(int new_num_rings, int new_num_dets)
float approx_num_non_arccorrected_bins;
if (new_scanner_sptr->get_scanner_geometry() != "Cylindrical")
{
- new_num_dets = this->proj_data_info_sptr->get_scanner_ptr()->get_num_detectors_per_ring();
- approx_num_non_arccorrected_bins = this->proj_data_info_sptr->get_num_tangential_poss();
+ if (new_num_dets <= 0)
+ { // by default, do not downsample the detectors per ring for BlocksOnCylindrical
+ new_num_dets = this->proj_data_info_sptr->get_scanner_ptr()->get_num_detectors_per_ring();
+ }
+ // STIR does not like block spacings that are smaller than number of crystals times crystal spacing,
+ // therefore add a 1% extension on top for the downsampled scanner, to avoid running into floating point issues
+ const float block_spacing_factor = 1.01;
+
+ // extend the bins by a small amount to avoid edge-effects, at the expense of longer computation time
+ approx_num_non_arccorrected_bins = ceil(this->proj_data_info_sptr->get_num_tangential_poss() * float(new_num_dets)
+ / old_scanner_ptr->get_num_detectors_per_ring())
+ + 1;
// preserve the length of the scanner, but place crystals equidistantly
float scanner_length = old_scanner_ptr->get_axial_length();
float new_ring_spacing = scanner_length / (new_num_rings - 1);
-
new_scanner_sptr->set_num_axial_blocks_per_bucket(1);
+ new_scanner_sptr->set_num_transaxial_blocks_per_bucket(1);
+
new_scanner_sptr->set_num_rings(new_num_rings);
+ new_scanner_sptr->set_num_detectors_per_ring(new_num_dets);
+
new_scanner_sptr->set_axial_crystal_spacing(new_ring_spacing);
new_scanner_sptr->set_ring_spacing(new_ring_spacing);
new_scanner_sptr->set_num_axial_crystals_per_block(new_num_rings);
- new_scanner_sptr->set_axial_block_spacing(new_ring_spacing * new_scanner_sptr->get_num_axial_crystals_per_block());
+ new_scanner_sptr->set_axial_block_spacing(new_ring_spacing * new_num_rings * block_spacing_factor);
+
+ float transaxial_bucket_width
+ = old_scanner_ptr->get_transaxial_block_spacing() * (old_scanner_ptr->get_num_transaxial_blocks_per_bucket() - 1)
+ + old_scanner_ptr->get_transaxial_crystal_spacing() * (old_scanner_ptr->get_num_transaxial_crystals_per_block() - 1);
+
+ int num_trans_buckets = old_scanner_ptr->get_num_transaxial_buckets();
+ // get a new number of detectors that is a multiple of the number of buckets to preserve scanner shape
+ new_scanner_sptr->set_num_detectors_per_ring(new_num_dets);
+ int new_transaxial_dets_per_bucket = new_num_dets / num_trans_buckets;
+ float new_det_spacing = transaxial_bucket_width / (new_transaxial_dets_per_bucket - 1);
+
+ new_scanner_sptr->set_num_transaxial_blocks_per_bucket(1);
+ new_scanner_sptr->set_num_transaxial_crystals_per_block(new_transaxial_dets_per_bucket);
+ new_scanner_sptr->set_transaxial_crystal_spacing(new_det_spacing);
+ new_scanner_sptr->set_transaxial_block_spacing(new_transaxial_dets_per_bucket * new_det_spacing * block_spacing_factor);
+ // avoid problems with Scanner checks by setting singles_units to 1 crystal
+ // (only used for dead-time processing in ECAY norm)
+ new_scanner_sptr->set_num_axial_crystals_per_singles_unit(1);
+ new_scanner_sptr->set_num_transaxial_crystals_per_singles_unit(1);
}
else
{
diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt
index 6b7ae72fd..e111be65b 100644
--- a/src/test/CMakeLists.txt
+++ b/src/test/CMakeLists.txt
@@ -74,7 +74,7 @@ set(buildblock_simple_tests
include(stir_test_exe_targets)
foreach(source ${buildblock_simple_tests})
- create_stir_test(${source} "buildblock;IO;buildblock;numerics_buildblock;display;IO;recon_buildblock;Shape_buildblock" "")
+ create_stir_test(${source} "buildblock;IO;buildblock;numerics_buildblock;display;IO;recon_buildblock;Shape_buildblock;scatter_buildblock" "")
endforeach()
#
diff --git a/src/test/test_interpolate_projdata.cxx b/src/test/test_interpolate_projdata.cxx
index 29c554ce7..3153678b3 100644
--- a/src/test/test_interpolate_projdata.cxx
+++ b/src/test/test_interpolate_projdata.cxx
@@ -41,6 +41,7 @@
#include "stir/Shape/Box3D.h"
#include "stir/recon_buildblock/ProjMatrixByBinUsingRayTracing.h"
#include "stir/recon_buildblock/ForwardProjectorByBinUsingProjMatrixByBin.h"
+#include "stir/scatter/SingleScatterSimulation.h"
#include "stir/RunTests.h"
@@ -56,6 +57,8 @@ class InterpolationTests : public RunTests
void scatter_interpolation_test_cyl();
void scatter_interpolation_test_blocks_asymmetric();
void scatter_interpolation_test_cyl_asymmetric();
+ void scatter_interpolation_test_blocks_downsampled();
+ void transaxial_upsampling_interpolation_test_blocks();
void check_symmetry(const SegmentBySinogram& segment);
void compare_segment(const SegmentBySinogram& segment1, const SegmentBySinogram& segment2, float maxDiff);
@@ -675,6 +678,280 @@ InterpolationTests::scatter_interpolation_test_cyl_asymmetric()
compare_segment_shape(full_size_model_sino.get_segment_by_sinogram(0), interpolated_proj_data.get_segment_by_sinogram(0), 2);
}
+void
+InterpolationTests::scatter_interpolation_test_blocks_downsampled()
+{
+ info("Performing downampled interpolation test for BlocksOnCylindrical scanner");
+ auto time_frame_def = TimeFrameDefinitions();
+ time_frame_def.set_num_time_frames(1);
+ time_frame_def.set_time_frame(1, 0, 1e9);
+ auto exam_info = ExamInfo();
+ exam_info.set_high_energy_thres(650);
+ exam_info.set_low_energy_thres(425);
+ exam_info.set_time_frame_definitions(time_frame_def);
+
+ // define the original scanner and a downsampled one, as it would be used for scatter simulation
+ auto scanner = Scanner(Scanner::User_defined_scanner,
+ "Some_BlocksOnCylindrical_Scanner",
+ 96,
+ 30,
+ int(150 * 96 / 192),
+ int(150 * 96 / 192),
+ 127,
+ 6.5,
+ 3.313,
+ 4.156,
+ -3.1091819,
+ 5,
+ 3,
+ 6,
+ 4,
+ 1,
+ 1,
+ 1,
+ 0.14,
+ 511,
+ 1,
+ 0,
+ 500,
+ "BlocksOnCylindrical",
+ 3.313,
+ 7.0,
+ 20.0,
+ 29.0);
+ auto downsampled_scanner = Scanner(Scanner::User_defined_scanner,
+ "Some_Downsampled_BlocksOnCylindrical_Scanner",
+ 64,
+ 8,
+ int(150 * 64 / 192 + 1),
+ int(150 * 64 / 192 + 1),
+ 127,
+ 6.5,
+ 16.652,
+ 6.234,
+ -3.1091819,
+ 1,
+ 1,
+ 8,
+ 8,
+ 1,
+ 1,
+ 1,
+ 0.17,
+ 511,
+ 1,
+ 0,
+ 500,
+ "BlocksOnCylindrical",
+ 13.795,
+ 15.4286,
+ 112.0,
+ 125.0);
+
+ auto proj_data_info = shared_ptr(std::move(
+ ProjDataInfo::construct_proj_data_info(std::make_shared(scanner), 1, 29, 48, int(150 * 96 / 192), false)));
+ auto downsampled_proj_data_info = shared_ptr(std::move(ProjDataInfo::construct_proj_data_info(
+ std::make_shared(downsampled_scanner), 1, 0, 32, int(150 * 64 / 192 + 1), false)));
+
+ auto proj_data = ProjDataInMemory(std::make_shared(exam_info), proj_data_info);
+ auto downsampled_proj_data = ProjDataInMemory(std::make_shared(exam_info), downsampled_proj_data_info);
+
+ // define a cylinder and a box that are off-centre, such that the shapes in the sinogram can be compared
+ auto emission_map = VoxelsOnCartesianGrid(*proj_data_info, 1);
+ auto cyl_map = VoxelsOnCartesianGrid(*proj_data_info, 1);
+ auto cylinder = EllipsoidalCylinder(40, 40, 20, CartesianCoordinate3D(80, 100, 0));
+ cylinder.construct_volume(cyl_map, CartesianCoordinate3D(1, 1, 1));
+ auto box = Box3D(20, 20, 20, CartesianCoordinate3D(30, -20, 70));
+ box.construct_volume(emission_map, CartesianCoordinate3D(1, 1, 1));
+ emission_map += cyl_map;
+
+ // project the emission map onto the full-scale scanner proj data
+ auto pm = ProjMatrixByBinUsingRayTracing();
+ pm.set_use_actual_detector_boundaries(true);
+ pm.enable_cache(false);
+ auto forw_proj = ForwardProjectorByBinUsingProjMatrixByBin(std::make_shared(pm));
+ forw_proj.set_up(proj_data_info, std::make_shared>(emission_map));
+ auto full_size_model_sino = ProjDataInMemory(proj_data);
+ full_size_model_sino.fill(0);
+ forw_proj.forward_project(full_size_model_sino, emission_map);
+
+ // also project onto the downsampled scanner
+ emission_map = VoxelsOnCartesianGrid(*downsampled_proj_data_info, 1);
+ cyl_map = VoxelsOnCartesianGrid(*downsampled_proj_data_info, 1);
+ cylinder.construct_volume(cyl_map, CartesianCoordinate3D(1, 1, 1));
+ box.construct_volume(emission_map, CartesianCoordinate3D(1, 1, 1));
+ emission_map += cyl_map;
+ forw_proj.set_up(downsampled_proj_data_info, std::make_shared>(emission_map));
+ auto downsampled_model_sino = ProjDataInMemory(downsampled_proj_data);
+ downsampled_model_sino.fill(0);
+ forw_proj.forward_project(downsampled_model_sino, emission_map);
+
+ // write the proj data to file
+ downsampled_model_sino.write_to_file("transaxially_downsampled_sino.hs");
+ full_size_model_sino.write_to_file("transaxially_full_size_sino.hs");
+
+ // interpolate the downsampled proj data to the original scanner size and fill in oblique sinograms
+ auto interpolated_direct_proj_data = ProjDataInMemory(proj_data);
+ interpolate_projdata(interpolated_direct_proj_data, downsampled_model_sino, BSpline::linear, false);
+ auto interpolated_proj_data = ProjDataInMemory(proj_data);
+ inverse_SSRB(interpolated_proj_data, interpolated_direct_proj_data);
+
+ // write the proj data to file
+ interpolated_proj_data.write_to_file("transaxially_interpolated_sino.hs");
+
+ // compare to ground truth
+ compare_segment_shape(full_size_model_sino.get_segment_by_sinogram(0), interpolated_proj_data.get_segment_by_sinogram(0), 3);
+}
+
+void
+InterpolationTests::transaxial_upsampling_interpolation_test_blocks()
+{
+ info("Performing transaxial downampled interpolation test for BlocksOnCylindrical scanner");
+ auto time_frame_def = TimeFrameDefinitions();
+ time_frame_def.set_num_time_frames(1);
+ time_frame_def.set_time_frame(1, 0, 1e9);
+ auto exam_info = ExamInfo();
+ exam_info.set_high_energy_thres(650);
+ exam_info.set_low_energy_thres(425);
+ exam_info.set_time_frame_definitions(time_frame_def);
+
+ // define the original scanner and a downsampled one, as it would be used for scatter simulation
+ auto scanner = Scanner(Scanner::User_defined_scanner,
+ "Some_BlocksOnCylindrical_Scanner",
+ 96,
+ 3,
+ 60,
+ 60,
+ 127,
+ 6.5,
+ 3.313,
+ 1.65,
+ -3.1091819,
+ 1,
+ 3,
+ 3,
+ 4,
+ 1,
+ 1,
+ 1,
+ 0.14,
+ 511,
+ 1,
+ 0,
+ 500,
+ "BlocksOnCylindrical",
+ 3.313,
+ 7.0,
+ 20.0,
+ 29.0);
+ auto proj_data_info = shared_ptr(
+ std::move(ProjDataInfo::construct_proj_data_info(std::make_shared(scanner), 1, 0, 48, 60, false)));
+
+ // use the code in scatter simulation to downsample the scanner
+ auto scatter_simulation = SingleScatterSimulation();
+ scatter_simulation.set_template_proj_data_info(*proj_data_info);
+ scatter_simulation.set_exam_info(exam_info);
+ scatter_simulation.downsample_scanner(-1, 96 / 4); // number of detectors per ring reduced by factor of four
+ auto downsampled_proj_data_info = scatter_simulation.get_template_proj_data_info_sptr();
+
+ auto proj_data = ProjDataInMemory(std::make_shared(exam_info), proj_data_info);
+ auto downsampled_proj_data = ProjDataInMemory(std::make_shared(exam_info), downsampled_proj_data_info);
+
+ // define a cylinder precisely in the middle of the FOV
+ auto emission_map = VoxelsOnCartesianGrid(*downsampled_proj_data_info, 1);
+ make_symmetric_object(emission_map);
+
+ // project the cylinder onto the full-scale scanner proj data
+ auto pm = ProjMatrixByBinUsingRayTracing();
+ pm.set_use_actual_detector_boundaries(true);
+ pm.enable_cache(false);
+ auto forw_proj = ForwardProjectorByBinUsingProjMatrixByBin(std::make_shared(pm));
+ forw_proj.set_up(proj_data_info, std::make_shared>(emission_map));
+ auto full_size_model_sino = ProjDataInMemory(proj_data);
+ full_size_model_sino.fill(0);
+ forw_proj.forward_project(full_size_model_sino, emission_map);
+
+ // also project onto the downsampled scanner
+ emission_map = VoxelsOnCartesianGrid(*downsampled_proj_data_info, 1);
+ make_symmetric_object(emission_map);
+ forw_proj.set_up(downsampled_proj_data_info, std::make_shared>(emission_map));
+ auto downsampled_model_sino = ProjDataInMemory(downsampled_proj_data);
+ downsampled_model_sino.fill(0);
+ forw_proj.forward_project(downsampled_model_sino, emission_map);
+
+ // write the proj data to file
+ downsampled_model_sino.write_to_file("transaxially_downsampled_sino_for_LOR.hs");
+
+ // interpolate the downsampled proj data to the original scanner size and fill in oblique sinograms
+ auto interpolated_direct_proj_data = ProjDataInMemory(proj_data);
+ interpolate_projdata(interpolated_direct_proj_data, downsampled_model_sino, BSpline::linear, false);
+ auto interpolated_proj_data = ProjDataInMemory(proj_data);
+ inverse_SSRB(interpolated_proj_data, interpolated_direct_proj_data);
+
+ // write the proj data to file
+ interpolated_proj_data.write_to_file("transaxially_interpolated_sino_for_LOR.hs");
+
+ // Identify the bins which should be identical between the downsampled and the interpolated sinogram:
+ // Each module has 96 / 8 = 12 crystal in the full size scanner, organised in 3 blocks of 4 crystals, while
+ // the downsampled scanner has 3 crystals per module. The idea is that the centre of the outer two
+ // is in exactly the same position than the centre of the first and last crystal in the full size scanner.
+ SegmentBySinogram sinogram_downsampled = downsampled_proj_data.get_empty_segment_by_sinogram(0, false, 0);
+ SegmentBySinogram sinogram_full_size = proj_data.get_empty_segment_by_sinogram(0, false, 0);
+ const auto pdi_downsampled
+ = dynamic_cast(&(*downsampled_proj_data.get_proj_data_info_sptr()));
+ const auto pdi_full_size = dynamic_cast(&(*sinogram_full_size.get_proj_data_info_sptr()));
+
+ int tested_LORs = 0;
+ for (int det1_downsampled = 0; det1_downsampled < 3 * 8; det1_downsampled++)
+ {
+ if (det1_downsampled % 3 == 1)
+ continue; // skip the central crystal of each module
+ for (int det2_downsampled = 0; det2_downsampled < 3 * 8; det2_downsampled++)
+ {
+ if (det2_downsampled % 3 == 1 || det1_downsampled == det2_downsampled)
+ continue; // skip the central crystal of each module
+ if (det1_downsampled / 3 == det2_downsampled / 3)
+ continue; // skip the LORs that lie on the same module
+
+ int view_ds, tang_pos_ds;
+ pdi_downsampled->get_view_tangential_pos_num_for_det_num_pair(view_ds, tang_pos_ds, det1_downsampled, det2_downsampled);
+ BasicCoordinate<3, int> index_downsampled;
+ index_downsampled[1] = 1; // looking at central slice
+ index_downsampled[2] = view_ds;
+ index_downsampled[3] = tang_pos_ds;
+
+ if (tang_pos_ds < pdi_downsampled->get_min_tangential_pos_num()
+ || tang_pos_ds > pdi_downsampled->get_max_tangential_pos_num())
+ continue;
+
+ int view_fs, tang_pos_fs;
+ pdi_full_size->get_view_tangential_pos_num_for_det_num_pair(
+ view_fs,
+ tang_pos_fs,
+ (det1_downsampled / 3) * 12 + ((det1_downsampled % 3) / 2) * 11,
+ (det2_downsampled / 3) * 12 + ((det2_downsampled % 3) / 2) * 11);
+
+ BasicCoordinate<3, int> index_full_size;
+ index_full_size[1] = 1; // looking at central slice
+ index_full_size[2] = view_fs;
+ index_full_size[3] = tang_pos_fs;
+
+ if (tang_pos_fs < pdi_full_size->get_min_tangential_pos_num()
+ || tang_pos_fs > pdi_full_size->get_max_tangential_pos_num())
+ continue;
+
+ // confirm that the difference is smaller than an empirically found value
+ check_if_less(std::abs(sinogram_downsampled[index_downsampled] - sinogram_full_size[index_full_size]),
+ 0.01,
+ "difference between sinogram bin is larger than expected");
+
+ tested_LORs++;
+ }
+ }
+
+ info(boost::format("A total of %1% LORs were compared between the downsampled and the interpolated sinogram.") % tested_LORs);
+}
+
void
InterpolationTests::run_tests()
{
@@ -682,6 +959,8 @@ InterpolationTests::run_tests()
scatter_interpolation_test_cyl();
scatter_interpolation_test_blocks_asymmetric();
scatter_interpolation_test_cyl_asymmetric();
+ scatter_interpolation_test_blocks_downsampled();
+ transaxial_upsampling_interpolation_test_blocks();
}
END_NAMESPACE_STIR