Skip to content

Commit 79a1e83

Browse files
Merge pull request #1375 from robbietuk/axial_bucket_projection_issue
Throw errors if incorrect/unsupported blocks scanner. See #1374
2 parents bc56456 + 1bb2af0 commit 79a1e83

File tree

7 files changed

+257
-22
lines changed

7 files changed

+257
-22
lines changed

documentation/release_6.1.htm

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,10 @@ <H2>What's new for developers (aside from what should be obvious
8585

8686
<h3>Backward incompatibities</h3>
8787
<ul>
88-
<li>
89-
</li>
88+
<li>
89+
Additional checks on <code>GeometryBlocksOnCylindrical</code> scanner configuration, which may lead to an
90+
error being raised, while previously the code silently proceeded.
91+
</li>
9092
</ul>
9193

9294
<h3>New functionality</h3>

src/buildblock/GeometryBlocksOnCylindrical.cxx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,14 @@ limitations under the License.
3939
#include <iostream>
4040
#include <fstream>
4141
#include <iomanip>
42+
#include <boost/format.hpp>
4243

4344
START_NAMESPACE_STIR
4445

4546
GeometryBlocksOnCylindrical::GeometryBlocksOnCylindrical(const Scanner& scanner)
4647
{
48+
if (scanner.check_consistency() == Succeeded::no)
49+
error("Error in GeometryBlocksOnCylindrical: scanner configuration not accepted. Please check warnings.");
4750
build_crystal_maps(scanner);
4851
}
4952

@@ -61,11 +64,10 @@ GeometryBlocksOnCylindrical::build_crystal_maps(const Scanner& scanner)
6164
// local variables to describe scanner
6265
int num_axial_crystals_per_block = scanner.get_num_axial_crystals_per_block();
6366
int num_transaxial_crystals_per_block = scanner.get_num_transaxial_crystals_per_block();
64-
int num_axial_blocks = scanner.get_num_axial_blocks();
6567
int num_transaxial_blocks_per_bucket = scanner.get_num_transaxial_blocks_per_bucket();
6668
int num_axial_blocks_per_bucket = scanner.get_num_axial_blocks_per_bucket();
67-
int num_transaxial_buckets = scanner.get_num_transaxial_blocks() / num_transaxial_blocks_per_bucket;
68-
int num_axial_buckets = scanner.get_num_axial_blocks() / num_axial_blocks_per_bucket;
69+
int num_transaxial_buckets = scanner.get_num_transaxial_buckets();
70+
int num_axial_buckets = scanner.get_num_axial_buckets();
6971
int num_detectors_per_ring = scanner.get_num_detectors_per_ring();
7072
float axial_block_spacing = scanner.get_axial_block_spacing();
7173
float transaxial_block_spacing = scanner.get_transaxial_block_spacing();
@@ -100,7 +102,7 @@ GeometryBlocksOnCylindrical::build_crystal_maps(const Scanner& scanner)
100102
stir::CartesianCoordinate3D<float> start_point(start_z, start_y, start_x);
101103

102104
for (int ax_bucket_num = 0; ax_bucket_num < num_axial_buckets; ++ax_bucket_num)
103-
for (int ax_block_num = 0; ax_block_num < num_axial_blocks; ++ax_block_num)
105+
for (int ax_block_num = 0; ax_block_num < num_axial_blocks_per_bucket; ++ax_block_num)
104106
for (int ax_crys_num = 0; ax_crys_num < num_axial_crystals_per_block; ++ax_crys_num)
105107
for (int trans_bucket_num = 0; trans_bucket_num < num_transaxial_buckets; ++trans_bucket_num)
106108
for (int trans_block_num = 0; trans_block_num < num_transaxial_blocks_per_bucket; ++trans_block_num)

src/buildblock/Scanner.cxx

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
#include <iostream>
4343
#include <algorithm>
4444
#include <sstream>
45+
#include <boost/format.hpp>
4546
#include "stir/warning.h"
4647
#include "stir/error.h"
4748

@@ -1751,6 +1752,23 @@ Scanner::check_consistency() const
17511752

17521753
if (get_scanner_geometry() == "BlocksOnCylindrical")
17531754
{ //! Check consistency of axial and transaxial spacing for block geometry
1755+
if (get_num_axial_buckets() != 1)
1756+
{
1757+
warning(boost::format("BlocksOnCylindrical num_axial_buckets (%d) is greater than 1. This is not supported yet. "
1758+
"Consider multiplying the number of axial_blocks_per_bucket by %d.")
1759+
% get_num_axial_buckets() % get_num_axial_buckets());
1760+
return Succeeded::no;
1761+
}
1762+
{ // Assert that each block contains an equal number of axial crystals
1763+
if (get_num_rings() % get_num_axial_crystals_per_bucket() != 0)
1764+
{
1765+
warning(boost::format("Error in GeometryBlocksOnCylindrical: number of rings (%d) is not a multiple of the "
1766+
"get_num_axial_crystals_per_bucket "
1767+
"(%d) = num_axial_crystals_per_block (%d) * num_axial_blocks_per_bucket (%d)")
1768+
% get_num_rings() % get_num_axial_crystals_per_bucket() % get_num_axial_crystals_per_block()
1769+
% get_num_axial_blocks_per_bucket());
1770+
}
1771+
}
17541772
if (get_axial_crystal_spacing() * get_num_axial_crystals_per_block() > get_axial_block_spacing())
17551773
{
17561774
warning(
@@ -1786,22 +1804,22 @@ Scanner::check_consistency() const
17861804
get_inner_ring_radius());
17871805
return Succeeded::no;
17881806
}
1789-
else if (get_scanner_geometry() == "Generic")
1790-
{ //! Check if the crystal map is correct and given
1791-
if (get_crystal_map_file_name() == "")
1807+
}
1808+
else if (get_scanner_geometry() == "Generic")
1809+
{ //! Check if the crystal map is correct and given
1810+
if (get_crystal_map_file_name().empty())
1811+
{
1812+
warning("No crystal map is provided. The scanner geometry Generic needs it! Please provide one.");
1813+
return Succeeded::no;
1814+
}
1815+
else
1816+
{
1817+
std::ifstream crystal_map(get_crystal_map_file_name());
1818+
if (!crystal_map)
17921819
{
1793-
warning("No crystal map is provided. The scanner geometry Generic needs it! Please provide one.");
1820+
warning("No correct crystal map provided. Please check the file name.");
17941821
return Succeeded::no;
17951822
}
1796-
else
1797-
{
1798-
std::ifstream crystal_map(get_crystal_map_file_name());
1799-
if (!crystal_map)
1800-
{
1801-
warning("No correct crystal map provided. Please check the file name.");
1802-
return Succeeded::no;
1803-
}
1804-
}
18051823
}
18061824
}
18071825

src/recon_test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ set(${dir_SIMPLE_TEST_EXE_SOURCES}
2626
test_FBP3DRP.cxx
2727
test_priors.cxx
2828
test_blocks_on_cylindrical_projectors.cxx
29+
test_geometry_blocks_on_cylindrical.cxx
2930
)
3031

3132

src/recon_test/test_blocks_on_cylindrical_projectors.cxx

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
\brief Test program for back projection and forward projection using stir::ProjDataInfoBlockOnCylindrical
77
88
\author Daniel Deidda
9+
\author Robert Twyman
910
1011
*/
1112
/* Copyright (C) 2021-2022, National Physical Laboratory
13+
Copyright (C) 2024, Prescient Imaging
1214
This file is part of STIR.
1315
1416
SPDX-License-Identifier: Apache-2.0
@@ -52,8 +54,6 @@
5254
#endif
5355
#include "stir/recon_buildblock/BackProjectorByBinUsingProjMatrixByBin.h"
5456
#include "stir/IO/write_to_file.h"
55-
#include "stir/VoxelsOnCartesianGrid.h"
56-
//#include "stir/Shape/Shape3D.h"
5757
#include <cmath>
5858

5959
START_NAMESPACE_STIR
@@ -80,6 +80,7 @@ class BlocksTests : public RunTests
8080
void run_map_orientation_test(ForwardProjectorByBin& forw_projector1, ForwardProjectorByBin& forw_projector2);
8181
void run_projection_test(ForwardProjectorByBin& forw_projector1, ForwardProjectorByBin& forw_projector2);
8282
void run_intersection_with_cylinder_test();
83+
void run_back_projection_test_with_axial_buckets(BackProjectorByBin& back_projector);
8384
};
8485

8586
/*! The following is a function to allow a projdata_info BlocksOnCylindrical to be created from the scanner. */
@@ -871,6 +872,64 @@ BlocksTests::run_intersection_with_cylinder_test()
871872
}
872873
}
873874

875+
void
876+
BlocksTests::run_back_projection_test_with_axial_buckets(BackProjectorByBin& back_projector)
877+
878+
{
879+
int num_buckets = 1;
880+
auto scanner_sptr = std::make_shared<Scanner>(Scanner::SAFIRDualRingPrototype);
881+
{ // create geometry
882+
scanner_sptr->set_average_depth_of_interaction(5);
883+
scanner_sptr->set_num_axial_crystals_per_block(1);
884+
scanner_sptr->set_axial_block_spacing(scanner_sptr->get_axial_crystal_spacing()
885+
* scanner_sptr->get_num_axial_crystals_per_block());
886+
scanner_sptr->set_transaxial_block_spacing(scanner_sptr->get_transaxial_crystal_spacing()
887+
* scanner_sptr->get_num_transaxial_crystals_per_block());
888+
scanner_sptr->set_num_axial_blocks_per_bucket(2);
889+
scanner_sptr->set_num_rings(scanner_sptr->get_num_axial_crystals_per_bucket() * num_buckets);
890+
scanner_sptr->set_scanner_geometry("BlocksOnCylindrical");
891+
scanner_sptr->set_up();
892+
}
893+
894+
auto projdata_info_sptr = set_direct_projdata_info<ProjDataInfoBlocksOnCylindricalNoArcCorr>(scanner_sptr, 1);
895+
auto exam_info_sptr = std::make_shared<ExamInfo>(ImagingModality::PT);
896+
auto projdata_sptr = std::make_shared<ProjDataInMemory>(exam_info_sptr, projdata_info_sptr);
897+
898+
auto origin = CartesianCoordinate3D<float>(0, 0, 0);
899+
auto volume_dimensions = CartesianCoordinate3D<int>(-1, -1, -1);
900+
auto volume_sptr
901+
= std::make_shared<VoxelsOnCartesianGrid<float>>(exam_info_sptr, *projdata_info_sptr, 1, origin, volume_dimensions);
902+
903+
// Now run the test
904+
volume_sptr->fill(0.0);
905+
projdata_sptr->fill(1.0);
906+
907+
back_projector.set_up(projdata_info_sptr, volume_sptr);
908+
back_projector.back_project(*volume_sptr, *projdata_sptr, 0, projdata_info_sptr->get_num_views());
909+
910+
bool test_ok = true;
911+
std::ostringstream oss;
912+
oss << "BlocksTests::run_back_projection_test_with_axial_buckets: Central voxel values are:" << std::endl;
913+
914+
auto centre_axial_values = std::vector<float>(volume_sptr->get_z_size());
915+
for (int z = volume_sptr->get_min_z(); z <= volume_sptr->get_max_z(); z++)
916+
{
917+
centre_axial_values[z] = (*volume_sptr)[z][0][0];
918+
oss << "\tz = " << z << "/" << volume_sptr->get_max_z() << " is " << centre_axial_values[z] << std::endl;
919+
if (test_ok)
920+
{
921+
test_ok = check(centre_axial_values[z] > 0,
922+
"BlocksTests::run_back_projection_test_with_axial_buckets: Central voxel value <= 0");
923+
everything_ok = everything_ok && test_ok;
924+
}
925+
}
926+
927+
if (test_ok)
928+
return;
929+
// Something went wrong, log the error
930+
std::cerr << oss.str();
931+
}
932+
874933
void
875934
BlocksTests::run_tests()
876935
{
@@ -905,6 +964,8 @@ BlocksTests::run_tests()
905964
print_time("map orientation test took: ");
906965
run_intersection_with_cylinder_test();
907966
print_time("intersection with cylinder test took: ");
967+
run_back_projection_test_with_axial_buckets(back_projector);
968+
print_time("back projection test with axial buckets took: ");
908969

909970
#ifdef STIR_WITH_Parallelproj_PROJECTOR
910971
// run the same tests with parallelproj, if available
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*!
2+
3+
\file
4+
\ingroup recontest
5+
6+
\brief Test program to ensure the axial coordinates of blocks on cylindrical are monotonic with axial indices
7+
8+
\author Robert Twyman
9+
10+
Copyright (C) 2024, Prescient Imaging
11+
This file is part of STIR.
12+
13+
SPDX-License-Identifier: Apache-2.0
14+
15+
See STIR/LICENSE.txt for details
16+
*/
17+
18+
#include "stir/recon_buildblock/ProjMatrixByBinUsingRayTracing.h"
19+
#include "stir/ExamInfo.h"
20+
#include "stir/Verbosity.h"
21+
#include "stir/LORCoordinates.h"
22+
#include "stir/ProjDataInfoGenericNoArcCorr.h"
23+
#include "stir/Succeeded.h"
24+
#include "stir/RunTests.h"
25+
#include "stir/Scanner.h"
26+
#include "stir/HighResWallClockTimer.h"
27+
#include "stir/GeometryBlocksOnCylindrical.h"
28+
#include "stir/IO/write_to_file.h"
29+
#include <cmath>
30+
31+
START_NAMESPACE_STIR
32+
33+
/*!
34+
\ingroup test
35+
\brief Test class for BlocksOnCylindrical geometry
36+
*/
37+
class GeometryBlocksOnCylindricalTests : public RunTests
38+
{
39+
public:
40+
void run_tests() override;
41+
42+
private:
43+
/*! \brief Tests multiple axial blocks/bucket configurations to ensure the detector map's axial indices and coordinates
44+
* are monotonic
45+
*/
46+
void run_monotonic_axial_coordinates_in_detector_map_test();
47+
//! Tests the axial indices and coordinates are monotonic in the detector map
48+
static Succeeded monotonic_axial_coordinates_in_detector_map_test(const shared_ptr<Scanner>& scanner_sptr);
49+
};
50+
51+
void
52+
GeometryBlocksOnCylindricalTests::run_monotonic_axial_coordinates_in_detector_map_test()
53+
{
54+
auto scanner_sptr = std::make_shared<Scanner>(Scanner::SAFIRDualRingPrototype);
55+
scanner_sptr->set_scanner_geometry("BlocksOnCylindrical");
56+
scanner_sptr->set_transaxial_block_spacing(scanner_sptr->get_transaxial_crystal_spacing()
57+
* scanner_sptr->get_num_transaxial_crystals_per_block());
58+
int num_axial_buckets = 1; // TODO add for loop when support is added
59+
60+
for (int num_axial_crystals_per_blocks = 1; num_axial_crystals_per_blocks < 3; ++num_axial_crystals_per_blocks)
61+
for (int num_axial_blocks_per_bucket = 1; num_axial_blocks_per_bucket < 3; ++num_axial_blocks_per_bucket)
62+
{
63+
scanner_sptr->set_num_axial_crystals_per_block(num_axial_crystals_per_blocks);
64+
scanner_sptr->set_num_axial_blocks_per_bucket(num_axial_blocks_per_bucket);
65+
scanner_sptr->set_num_rings(scanner_sptr->get_num_axial_crystals_per_bucket() * num_axial_buckets);
66+
scanner_sptr->set_axial_block_spacing(scanner_sptr->get_axial_crystal_spacing()
67+
* (scanner_sptr->get_num_axial_crystals_per_block() + 0.5));
68+
69+
if (monotonic_axial_coordinates_in_detector_map_test(scanner_sptr) == Succeeded::no)
70+
{
71+
warning(boost::format("Monothonic axial coordinates test failed for:\n"
72+
"\taxial_crystal_per_block =\t%1%\n"
73+
"\taxial_blocks_per_bucket =\t%2%\n"
74+
"\tnum_axial_buckets =\t\t\t%3%")
75+
% num_axial_crystals_per_blocks % num_axial_blocks_per_bucket % num_axial_buckets);
76+
everything_ok = false;
77+
return;
78+
}
79+
}
80+
}
81+
82+
Succeeded
83+
GeometryBlocksOnCylindricalTests::monotonic_axial_coordinates_in_detector_map_test(const shared_ptr<Scanner>& scanner_sptr)
84+
{
85+
if (scanner_sptr->get_scanner_geometry() != "BlocksOnCylindrical")
86+
{
87+
warning("monotonic_axial_coordinates_in_detector_map_test is only for the BlocksOnCylindrical geometry");
88+
return Succeeded::no;
89+
}
90+
91+
shared_ptr<DetectorCoordinateMap> detector_map_sptr;
92+
try
93+
{
94+
detector_map_sptr.reset(new GeometryBlocksOnCylindrical(*scanner_sptr));
95+
}
96+
catch (const std::runtime_error& e)
97+
{
98+
warning(boost::format("Caught runtime_error while creating GeometryBlocksOnCylindrical: %1%\n"
99+
"Failing the test.")
100+
% e.what());
101+
return Succeeded::no;
102+
}
103+
104+
unsigned min_axial_pos = 0;
105+
float prev_min_axial_coord = -std::numeric_limits<float>::max();
106+
107+
for (unsigned axial_idx = 0; axial_idx < detector_map_sptr->get_num_axial_coords(); ++axial_idx)
108+
for (unsigned tangential_idx = 0; tangential_idx < detector_map_sptr->get_num_tangential_coords(); ++tangential_idx)
109+
for (unsigned radial_idx = 0; radial_idx < detector_map_sptr->get_num_radial_coords(); ++radial_idx)
110+
{
111+
const DetectionPosition<> det_pos = DetectionPosition<>(tangential_idx, axial_idx, radial_idx);
112+
CartesianCoordinate3D<float> coord = detector_map_sptr->get_coordinate_for_det_pos(det_pos);
113+
if (coord.z() > prev_min_axial_coord)
114+
{
115+
min_axial_pos = axial_idx;
116+
prev_min_axial_coord = coord.z();
117+
}
118+
else if (coord.z() < prev_min_axial_coord)
119+
{
120+
float delta = coord.z() - prev_min_axial_coord;
121+
warning(boost::format("Axial Coordinates are not monotonic.\n"
122+
"Next axial index =\t\t%1%, Next axial coord (mm) =\t\t%2% (%3%)\n"
123+
"Previous axial index =\t%4%, Previous axial coord (mm) =\t%5%")
124+
% axial_idx % coord.z() % delta % min_axial_pos % prev_min_axial_coord);
125+
return Succeeded::no;
126+
}
127+
}
128+
129+
return Succeeded::yes;
130+
}
131+
132+
void
133+
GeometryBlocksOnCylindricalTests::run_tests()
134+
{
135+
HighResWallClockTimer timer;
136+
timer.start();
137+
run_monotonic_axial_coordinates_in_detector_map_test();
138+
timer.stop();
139+
}
140+
END_NAMESPACE_STIR
141+
142+
USING_NAMESPACE_STIR
143+
144+
int
145+
main()
146+
{
147+
Verbosity::set(1);
148+
GeometryBlocksOnCylindricalTests tests;
149+
tests.run_tests();
150+
return tests.main_return_value();
151+
}

src/test/test_interpolate_projdata.cxx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ InterpolationTests::scatter_interpolation_test_blocks_asymmetric()
496496
"BlocksOnCylindrical",
497497
10.0,
498498
16.0,
499-
60.0,
499+
120.0,
500500
96.0);
501501

502502
auto proj_data_info = shared_ptr<ProjDataInfo>(

0 commit comments

Comments
 (0)