Skip to content

Commit

Permalink
Added test for transaxial upsampling using interpolate_projdata and a…
Browse files Browse the repository at this point in the history
… couple of fixes for edge cases.
  • Loading branch information
Markus Jehl committed Jul 16, 2024
1 parent 5a1704d commit 312b2b0
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 87 deletions.
30 changes: 15 additions & 15 deletions src/buildblock/interpolate_projdata.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -272,23 +272,21 @@ interpolate_blocks_on_cylindrical_projdata(ProjData& proj_data_out, const ProjDa
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])
{

// 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.");
}

// translate index on output to coordinate
auto bin
= Bin(0 /* segment */, index_out[2] /* view */,
Expand Down Expand Up @@ -372,8 +370,10 @@ interpolate_blocks_on_cylindrical_projdata(ProjData& proj_data_out, const ProjDa
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] = axial_floor;
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);
Expand All @@ -395,7 +395,7 @@ interpolate_blocks_on_cylindrical_projdata(ProjData& proj_data_out, const ProjDa
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] = axial_floor;
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]
Expand Down Expand Up @@ -427,7 +427,7 @@ interpolate_blocks_on_cylindrical_projdata(ProjData& proj_data_out, const ProjDa
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] = axial_floor;
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]
Expand All @@ -444,7 +444,7 @@ interpolate_blocks_on_cylindrical_projdata(ProjData& proj_data_out, const ProjDa
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 bilinear interpolation
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;
Expand All @@ -467,7 +467,7 @@ interpolate_blocks_on_cylindrical_projdata(ProjData& proj_data_out, const ProjDa
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] = axial_floor;
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)
Expand Down
8 changes: 6 additions & 2 deletions src/scatter_buildblock/ScatterSimulation.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,10 @@ ScatterSimulation::downsample_scanner(int new_num_rings, int new_num_dets)
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())
Expand All @@ -866,7 +870,7 @@ ScatterSimulation::downsample_scanner(int new_num_rings, int 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_num_rings);
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)
Expand All @@ -881,7 +885,7 @@ ScatterSimulation::downsample_scanner(int new_num_rings, int new_num_dets)
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);
new_scanner_sptr->set_transaxial_block_spacing(new_transaxial_dets_per_bucket * new_det_spacing * block_spacing_factor);
}
else
{
Expand Down
137 changes: 67 additions & 70 deletions src/test/test_interpolate_projdata.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -690,20 +690,20 @@ InterpolationTests::scatter_interpolation_test_blocks_downsampled()

// define the original scanner and a downsampled one, as it would be used for scatter simulation
auto scanner = Scanner(Scanner::User_defined_scanner,
"NeuroLF_10mm",
256,
48,
180,
180,
134,
6.99,
"Some_BlocksOnCylindrical_Scanner",
96,
30,
int(150 * 96 / 192),
int(150 * 96 / 192),
127,
6.5,
3.313,
1.6565,
4.156,
-3.1091819,
5,
3,
6,
4,
8,
8,
1,
1,
1,
Expand All @@ -714,24 +714,24 @@ InterpolationTests::scatter_interpolation_test_blocks_downsampled()
500,
"BlocksOnCylindrical",
3.313,
3.313,
27.36,
27.36);
7.0,
20.0,
29.0);
auto downsampled_scanner = Scanner(Scanner::User_defined_scanner,
"NeuroLF_10mm",
128,
"Some_Downsampled_BlocksOnCylindrical_Scanner",
64,
8,
91,
180,
int(150 * 64 / 192 + 1),
int(150 * 64 / 192 + 1),
127,
6.99,
22.8559,
3.46,
6.5,
16.652,
6.234,
-3.1091819,
1,
1,
8,
16,
8,
1,
1,
1,
Expand All @@ -741,67 +741,64 @@ InterpolationTests::scatter_interpolation_test_blocks_downsampled()
0,
500,
"BlocksOnCylindrical",
22.8559,
7.01807,
182.848,
112.290);
13.795,
15.4286,
112.0,
125.0);

auto proj_data_info = shared_ptr<ProjDataInfo>(
std::move(ProjDataInfo::construct_proj_data_info(std::make_shared<Scanner>(scanner), 1, 47, 128, 180, false)));
auto downsampled_proj_data_info = shared_ptr<ProjDataInfo>(
std::move(ProjDataInfo::construct_proj_data_info(std::make_shared<Scanner>(downsampled_scanner), 1, 0, 64, 91, false)));
auto proj_data_info = shared_ptr<ProjDataInfo>(std::move(
ProjDataInfo::construct_proj_data_info(std::make_shared<Scanner>(scanner), 1, 29, 48, int(150 * 96 / 192), false)));
auto downsampled_proj_data_info = shared_ptr<ProjDataInfo>(std::move(ProjDataInfo::construct_proj_data_info(
std::make_shared<Scanner>(downsampled_scanner), 1, 0, 32, int(150 * 64 / 192 + 1), false)));

auto proj_data = ProjDataInMemory(std::make_shared<ExamInfo>(exam_info), proj_data_info);
// auto downsampled_proj_data = ProjDataInMemory(std::make_shared<ExamInfo>(exam_info), downsampled_proj_data_info);

// // define a cylinder precisely in the middle of the FOV, such that symmetry can be used for validation
// auto emission_map = VoxelsOnCartesianGrid<float>(*proj_data_info, 1);
// auto cyl_map = VoxelsOnCartesianGrid<float>(*proj_data_info, 1);
// auto cylinder = EllipsoidalCylinder(40, 40, 20, CartesianCoordinate3D<float>(90, 100, 0));
// cylinder.construct_volume(cyl_map, CartesianCoordinate3D<int>(1, 1, 1));
// auto box = Box3D(20, 20, 20, CartesianCoordinate3D<float>(40, -20, 70));
// box.construct_volume(emission_map, CartesianCoordinate3D<int>(1, 1, 1));
// emission_map += cyl_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<ProjMatrixByBinUsingRayTracing>(pm));
// forw_proj.set_up(proj_data_info, std::make_shared<VoxelsOnCartesianGrid<float>>(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<float>(*downsampled_proj_data_info, 1);
// cyl_map = VoxelsOnCartesianGrid<float>(*downsampled_proj_data_info, 1);
// cylinder.construct_volume(cyl_map, CartesianCoordinate3D<int>(1, 1, 1));
// box.construct_volume(emission_map, CartesianCoordinate3D<int>(1, 1, 1));
// emission_map += cyl_map;
// forw_proj.set_up(downsampled_proj_data_info, std::make_shared<VoxelsOnCartesianGrid<float>>(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("downsampled_sino_asym.hs");
// full_size_model_sino.write_to_file("full_size_sino_asym.hs");

auto downsampled_model_sino = std::make_shared<ProjDataInMemory>(
*(ProjDataInMemory::read_from_file("/workspace/stir-public/test_data/downsampled_sino_neurolf.hs")));
auto downsampled_proj_data = ProjDataInMemory(std::make_shared<ExamInfo>(exam_info), downsampled_proj_data_info);

// define a cylinder precisely in the middle of the FOV, such that symmetry can be used for validation
auto emission_map = VoxelsOnCartesianGrid<float>(*proj_data_info, 1);
auto cyl_map = VoxelsOnCartesianGrid<float>(*proj_data_info, 1);
auto cylinder = EllipsoidalCylinder(40, 40, 20, CartesianCoordinate3D<float>(80, 100, 0));
cylinder.construct_volume(cyl_map, CartesianCoordinate3D<int>(1, 1, 1));
auto box = Box3D(20, 20, 20, CartesianCoordinate3D<float>(30, -20, 70));
box.construct_volume(emission_map, CartesianCoordinate3D<int>(1, 1, 1));
emission_map += cyl_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<ProjMatrixByBinUsingRayTracing>(pm));
forw_proj.set_up(proj_data_info, std::make_shared<VoxelsOnCartesianGrid<float>>(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<float>(*downsampled_proj_data_info, 1);
cyl_map = VoxelsOnCartesianGrid<float>(*downsampled_proj_data_info, 1);
cylinder.construct_volume(cyl_map, CartesianCoordinate3D<int>(1, 1, 1));
box.construct_volume(emission_map, CartesianCoordinate3D<int>(1, 1, 1));
emission_map += cyl_map;
forw_proj.set_up(downsampled_proj_data_info, std::make_shared<VoxelsOnCartesianGrid<float>>(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);
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("interpolated_sino_asym.hs");
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), 2);
compare_segment_shape(full_size_model_sino.get_segment_by_sinogram(0), interpolated_proj_data.get_segment_by_sinogram(0), 3);
}

void
Expand Down

0 comments on commit 312b2b0

Please sign in to comment.