-
Notifications
You must be signed in to change notification settings - Fork 95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Prepare for origin shift 2020 #618
base: master
Are you sure you want to change the base?
Conversation
Last commit (6da72d8) ties to re-centre the FOV for backwards compatibility, and to make sure the data symmetries test runs. No dice though unforunately, and we fail even more tests. Before:
After:
So there's an issue with my logic on that realignment I assume. At least we've now rebased against master and have successfully caught up to before, plus I have made a little progress in being able to actually compile everything with |
Just posting a link to the last commit attempting to preserve back-compat before I revert, following discussion yesterday that it may not be a significant issue with 5.0 coming up. |
6da72d8
to
5b962db
Compare
https://github.com/UCL/STIR/pull/618/files#diff-b4411dfe3254536bd1a46bbe37b4ff0cL106-L113 I'm struggling to make sense of what this is doing (
^ This is easy its the middle of the image in index coordinates
Here gets tricky. What exactly is axial pos, a virual ring in the micheologram? So
I assume that last code segment is trying to encode the length of the segment coverage somehow.
^ This part I'm confused with as well, I'm having troubles working out exactly how I should interpret delta, but I guess this is some sort of offset for the segment
^ But why are we taking the average of these two? (It is in the format of My goal is to be able to replace this all with a function in ProjDataInfo::get_point_on_lor_in_gantry_coordinates
(const float s_in_mm, const float m_in_mm, const float a_in_mm,
const float cphi, const float sphi, const float tantheta) const
{
return CartesianCoordinate3D<float>(m_in_mm - a_in_mm*tantheta, // z
s_in_mm*sphi - a_in_mm*cphi, // y
s_in_mm*cphi + a_in_mm*sphi); // x
} Which was called by this helper function in CartesianCoordinate3D<float>
get_point_on_lor_in_index_coordinates
(const float s_in_mm, const float m_in_mm, const float a_in_mm,
const float cphi, const float sphi, const float tantheta,
const DiscretisedDensity<3, float>& density_info,
const ProjDataInfo& proj_data_info)
{
return density_info.get_index_coordinates_for_physical_coordinates
(proj_data_info.get_physical_coordinates_for_gantry_coordinates
(proj_data_info.get_point_on_lor_in_gantry_coordinates
(s_in_mm, m_in_mm, a_in_mm, cphi, sphi, tantheta)));
} |
I should add, the values of the offsets were a little perplexing.
or
i.e., constant except for non-negative segments. |
yes. they happen to start from 0, and therefore go to
so, Anyway, all to be replaced with |
Thanks, I'm still puzzled as to why +/- segments don't have the same z offset though 🤔 |
I was thinking today that perhaps symmetries are a sinogram-space thing, and tried replacing:
(i.e., the middle of the image) with
(i.e., the middle of the rings) or
(i.e., the middle of segment 0) Which both pass more tests than before, but fail on "z voxel size 2 mtimes larger than usual" But I think I realise the error in my ways, this really does have to be tied with the real voxel locations as the symmetries are inherantly a voxel-to-lor mapping property. |
I feel like it should be like this: const float middle_of_scanner_in_index_coords_z =
cartesian_grid_info_ptr->get_index_coordinates_for_physical_coordinates(
proj_data_info_cyl_ptr->get_gantry_isocentre_in_physical_coordinates()
)[1];
// ...
axial_pos_to_z_offset[segment_num] =
// (cartesian_grid_info_ptr->get_max_index() + cartesian_grid_info_ptr->get_min_index())/2.F
// - cartesian_grid_info_ptr->get_origin().z()/image_plane_spacing
middle_of_scanner_in_index_coords_z
-
(num_planes_per_axial_pos[segment_num]
*(proj_data_info_cyl_ptr->get_max_axial_pos_num(segment_num)
+ proj_data_info_cyl_ptr->get_min_axial_pos_num(segment_num))
+ num_planes_per_scanner_ring*delta)/2; But I've done something wrong as |
The purpose of this code is written here
Sadly, it doesn't say what it means with z . It seems a convention introduced in Matthias Egger's old "on-the-fly" projectors. He had good drawings in his thesis, but I don't have access to that now (could be in my office). Note that this part of the formula works out for span=1 as num_rings-1-abs(delta)+delta .
Interestingly, in STIR/src/include/stir/recon_buildblock/DataSymmetriesForBins_PET_CartesianGrid.inl Lines 88 to 90 in 5b962db
the puzzling delta term gets added back in again, such that it actually disappears I believe, which makes a tiny bit of sense to me.
In any case, I believe we could reproduce the code as // KT doubts that the offset code is correct unless the following holds
assert(min_axial_pos_num==0);
Bin first_bin(segment_num, min_axial_pos_num,0,0);
axial_pos_to_z_offset[segment_num] =
cartesian_grid_info_ptr->get_index_coordinates_for_physical_coordinates(
proj_data_info_cyl_ptr->get_gantry_isocentre_in_physical_coordinates() +
CartesianCoordinate3D(get_m(first_bin) - ring_spacing*delta/2, 0,0)
)[1]; I think I've got that right, but not sure! By the way, STIR/src/buildblock/ProjDataInfoCylindrical.cxx Lines 134 to 137 in 5b962db
|
Great, I had to slightly modify that to:
(convert the CC from gantry to physical, and should be axial sampling, not ring spacing). Doesn't quite solve my problems though, but unfortunately I've had an afternoon disruption and I've had to run away from this 👎 More details to come... |
I can see the convert, but I don't understand the use of |
So, using the following I actually match the ProjMatrixByBinUsingRayTracing implementation successfully and all symmetries match and all tests pass: const float middle_of_scanner = (num_rings - 1) * proj_data_info_cyl_ptr->get_ring_spacing() / 2;
axial_pos_to_z_offset[segment_num] =
// cartesian_grid_info_ptr->get_index_coordinates_for_physical_coordinates(
// CartesianCoordinate3D<float>( middle_of_scanner, 0, 0)
// )[1]
(middle_of_scanner
- cartesian_grid_info_ptr->get_origin().z())/image_plane_spacing
// middle_of_scanner_in_index_coords_z
-
(num_planes_per_axial_pos[segment_num]
*(proj_data_info_cyl_ptr->get_max_axial_pos_num(segment_num)
+ proj_data_info_cyl_ptr->get_min_axial_pos_num(segment_num))
+ num_planes_per_scanner_ring*delta)/2; So we at least have a working reference implementation. For the first test, this gives an `axial_pos_to_z_offset' vector like: {10, 8, 6, 4, 2, 0, 0, 0, 0, 0, 0}. Yours: axial_pos_to_z_offset[segment_num] =
cartesian_grid_info_ptr->get_index_coordinates_for_physical_coordinates(
proj_data_info_cyl_ptr->get_gantry_isocentre_in_physical_coordinates() +
proj_data_info_cyl_ptr->get_physical_coordinates_for_gantry_coordinates(
CartesianCoordinate3D<float>(
proj_data_info_cyl_ptr->get_m(first_bin)
- ring_spacing*delta/2,
0,0)
)
)[1]; The problem with your proposed solution is that the I realised there's no need to add the gantry isocentre in, this is done by get_physical_coordinates_for_gantry_coordinates(): axial_pos_to_z_offset[segment_num] =
cartesian_grid_info_ptr->get_index_coordinates_for_physical_coordinates(
// proj_data_info_cyl_ptr->get_gantry_isocentre_in_physical_coordinates() +
proj_data_info_cyl_ptr->get_physical_coordinates_for_gantry_coordinates(
CartesianCoordinate3D<float>(
proj_data_info_cyl_ptr->get_m(first_bin)
- ring_spacing*delta/2,
0,0)
)
)[1]; {9.44444, 7.55556, 5.66667, 3.77778, 1.88889, 0, 0.111111, 0.222222, 0.333333, 0.444444, 0.555556} I am not sure why, but for whatever reason it just seems to work better with the axial_sampling instead of ring_spacing. I agree this is not a good reason to use it, we need to work out why but I'm stumped. axial_pos_to_z_offset[segment_num] =
cartesian_grid_info_ptr->get_index_coordinates_for_physical_coordinates(
proj_data_info_cyl_ptr->get_physical_coordinates_for_gantry_coordinates(
CartesianCoordinate3D<float>(
proj_data_info_cyl_ptr->get_m(first_bin)
- proj_data_info_cyl_ptr->get_axial_sampling(segment_num)*delta/2,
0,0)
)
)[1]; Gives the correct {10, 8, 6, 4, 2, 0, 0, 0, 0, 0, 0} I runs through all the tests until it gets to testing span=3 (I assume the others are span=1), and fails here. Obvious evidence that you are right and it should be ring_spacing not axial_sampling as it seems to only work for span=1. But I'm not sure why these are different for span=1. I naively might have thought that axial sampling and ring spacing would be the same without any compression. Or that it might be a half. But instead, for the initial tests I see I need to look a little more into exactly what axial sampling is, but any tips as to why they may differ will really help. Ash |
🤦 Problem solved. New problems. The FBP3DRP tests fail on your test, as min axial position isn't 0: I assume, though, that really we the i.e. // KT doubts that the offset code is correct unless the following holds
// if (proj_data_info_cyl_ptr->get_min_axial_pos_num(segment_num) != 0) {
// error(boost::format("min axial pos num is nonzero: %s") % proj_data_info_cyl_ptr->get_min_axial_pos_num(segment_num));
// }
// Bin first_bin(segment_num, proj_data_info_cyl_ptr->get_min_axial_pos_num(segment_num), 0, 0);
// get a bin (any bin) at axial position 0
Bin first_bin(segment_num, 0, 0, 0);
const float delta = proj_data_info_cyl_ptr->get_average_ring_difference(segment_num);
const float segment_offset_in_z_gantry_coords =
proj_data_info_cyl_ptr->get_m(first_bin) - ring_spacing*delta/2;
axial_pos_to_z_offset[segment_num] =
cartesian_grid_info_ptr->get_index_coordinates_for_physical_coordinates(
proj_data_info_cyl_ptr->get_physical_coordinates_for_gantry_coordinates(
CartesianCoordinate3D<float>(segment_offset_in_z_gantry_coords, 0, 0)
)
)[1]; |
That certainly seems to pass the tests, but it would be good to add a test to test_DataStmmetriesForBins with a sinogram like this (I have no idea really what it means to have a non-zero minimum axial position). Is there an easy way to construct one? Perhaps easier to incorporate such a test into FBP3DRP with and without symmetries I guess. |
I ended up testing by making all reconstruction tests do their initial forward projection for reconstruction with symmetries turned off (they they are turned on, the default, for their reconstruction, confirming consistency). This passed including for FBP3DRP, would this be of interest to include in this PR? |
good news that this works. The 3DRP mod makes sense. It is of course a bit worrying as we are now uncovering a fuzzy convention that It would be good to see how It would definitely be good to add an extended range of |
Extending for missing data is here STIR/src/analytic/FBP3DRP/FBP3DRPReconstruction.cxx Lines 541 to 544 in eaa7987
the By the way, there's some more horrible stuff that around here that will need checking |
I don't think it's necessarily arbitrary, axial_pos=0 should correspond to the location of the first ring, when considering segment 0. I'll have to think of some pithy description to apply to all segments, but I guess its something like "The z offset (in index coordinates) from ring 0 to the start of the first LOR", but the definition of the start of the LOR is a little confusing. Its a bit unfortunate that axial pos 0 is the first ring, might be simpler if axial pos 0 was the centre of the rings, to be consistent with our new convention that gantry coordinates are zeroed to the centre of the gantry. But that's a lot more work. |
Okay, I can see the issue here that this solution does not consider the actual image location at all, it assumes we had an image that starts at the first ring and ends at the last, axially (the transaxial image components are inconsequential). |
I've pushed the "don't use symmetries in recon tests" and FBP3DRP rmin/rmax commits, feedback welcome |
Since I've modified the PMBBURT and none of the other tests are currently failing - does this mean that all other areas of code that assume an image is auto-centred in the gantry are not benchmarked against the main projector? Should we implement failing tests before progressing to the other ORIGINTODO tagged code points? What about the experimental code? |
I didn't really think we'd go into FBP3DRP corrections here 😉 It does make sense that the numbers of added sinograms would be symmetric of course. I didn't check your proposed formula. I guess you'd have to check what happens when reconstruction a span=1 case as well to see if anything changes.
well... The 3DRP implementation tried to be careful when I gave it motion-corrected proj-data (with LOR-repositioning). As far as I recall, those could be larger than the original scanner, although I cannot immediately find that in the |
hmmm. there's probably no tests comparing projectors. I think we could retire the on-the-fly Harder is and there's some others as well... However, I don't really understand what you mean when you that "you've modified PMBURT, so the other tests should fail" (I'm paraphrasing of course) |
I wasn't sure if we should try and address all the "ORIGINTODO" tags in this PR, so that all of STIR's coordinate system definitions remain constant. It seems rather odd to accept this PR, and have the other parts of the code do something different? (Although I'm sure there will inevitably be remaining bugs) Otherwise, I'm able to revert that commit, pop it in another PR, and we can handle separate elements in separate PRs, then just merge them all around the same time.
Sorry, it was a bit throwaway. I just mean that I've currently got this branch pointing at a STIR which is internally-inconsistent and half-baked, yet all the tests are passing. It might be a good idea to add some failing tests to make sure we've actually fixed everything when we're done. |
that's not why i meant! I agree we should aim for keeping things consistent. with "FBP3DRP corrections", I meant fixing/changing how many rmin/rmax we have (that should be in a different PR I guess). But if you're confident, I'm happy with it. I think we'll have to accept that not all projectors are consistent. It's very bad, but very hard to prevent. In fact, the SPECTUB projector isn't compatible with the PET ones. And the NiftyPET projector is almost certainly not compatible either regarding choice of origin (I'd hope the other ones are currently ok).
ok. it is definitely a good idea to add tests for new desired behaviour. I wasn't aware you started already on the path of breaking things. In fact, I would have thought that we're aiming for not breaking any tests with "standard" images/data, so the current situation looked good to me. |
I've reviewed these changes now. I'm happy with the change in // find axial_pos of projection of centre of first plane
CartesianCoordinated3D<float> gantry_coordinates = proj_data_info.get_gantry_coordinates_for_physical(density.get_physical_coordinates(make_coordinate(min_plane,0,0));
float gantry_z = gantry_coordinates .z();
// shift to LOR at edge of image
gantry_z -= lor_overhang_in_num_virtual_rings;
// now go back to axial_pos somehow. maybe like this
float min_axial_pos = (gantry_z - proj_data_info.get_m(Bin(seg,0,0,0))) / proj_dat_info.get_m_spacing(seg);
rmin = static_cast<int>(floor(min_axial_pos)); or something like that. Once we do that, it seems even easier to just do this with the corners of the image. no longer necessary to compute overhang, i.e. we just can do proj_data_info.get_gantry_coordinates_for_physical(density.get_physical_coordinates(min_index); or even better (allowing for x/y offsets) do this with all 4 corners of the first plane. No more thinking, just call some functions... Also, it seems to me that we need to go between |
Currently just tests RT matrix fwd and back consistency.
…ly-centred images.
using a different convention called cartesian coordinates, and changed to gantry coordinates
de31f09
to
886c95f
Compare
@KrisThielemans Getting close now! I think all the places needing ticking off are done. I've fixed as many of the tests as I've been able to, including finding a bug in downsample_scanner. I think I'll need some help with the last two: The ScatterSimulation tests are failing - It seems one of the previously failing tests no longer fails - but I don't think that means I've actually solved the previous problem because the checks still fail.
The zoom_image tests are also failing. Related to #254
Actually I can't quite work out for myself why my changes have impacted this. I think it crops up around here https://github.com/UCL/STIR/blob/master/src/buildblock/zoom.cxx#L264-L266 in |
Supersedes #260, I rebased and didn't want to overwrite just yet.