Skip to content

Automatically shrink reconstruction volume #2221

Open
hrobarts wants to merge 17 commits intomasterfrom
shrink_volume
Open

Automatically shrink reconstruction volume #2221
hrobarts wants to merge 17 commits intomasterfrom
shrink_volume

Conversation

@hrobarts
Copy link
Contributor

@hrobarts hrobarts commented Oct 10, 2025

Changes

Code to create a reduced reconstruction volume from an AcquisitionData object
If using method='manual'

  • Creates an image geometry with limits specified with the limits argument. If a dimension is not included in the limits dictionary, the full size of the axis is used
    If using method='otsu or threshold
  • Performs a binned reconstruction
  • Finds the maximum pixel intensity along each direction
  • Finds pixels above the threshold or a threshold found with an Otsu filter
  • Identified limits between sample and background where no pixels above the threshold are found
    If preview=True
  • Plots the limits on the maximum intensity reconstruction
    kwargs
  • buffer applies a buffer around the automatically calculated limits
  • mask_radius applies a mask to the reconstruction before finding limits
  • otsu_classes changes number of material classes to assume in the Otsu thresholding
  • min_component_size specified a minimum number of connected components in pixels to consider when choosing the limits - to avoid a small amount of noise changing the limits
vs = VolumeShrinker(data, recon_backend='astra')
ig_reduced = vs.run(limits={'horizontal_x':(10, 150), 'vertical':(5, 50)}, preview=True)
image
vs = VolumeShrinker(data, recon_backend='astra')
ig_reduced = vs.run(method='threshold', preview=True, threshold=0.9, buffer=10)
image

If debug logging is enabled, we plot a histogram with the threshold and reduced box
image

vs = VolumeShrinker(data, recon_backend='astra')
ig_reduced = vs.run(method='otsu', preview=True, otsu_classes=2, buffer=10, min_component_size=10)
image

Testing you performed

Demo TomographicImaging/CIL-Demos#283

I think it makes sense for this code to be in utilities and possibly be called by AcquisitionData.get_ImageGeometry() however when I did this I had trouble with circular imports. Maybe it should be moved to utilities under framework?

Related issues/links

May close #1998

Checklist

  • I have performed a self-review of my code
  • I have added docstrings in line with the guidance in the developer guide
  • I have updated the relevant documentation
  • I have implemented unit tests that cover any new or modified functionality
  • CHANGELOG.md has been updated with any functionality change
  • Request review from all relevant developers

@hrobarts hrobarts linked an issue Oct 29, 2025 that may be closed by this pull request
@hrobarts hrobarts self-assigned this Nov 3, 2025
@hrobarts
Copy link
Contributor Author

hrobarts commented Dec 2, 2025

For discussion:

Could this be a method on image_data

  • The user would have to reconstruct their data themselves before calling the get reduced image geometry method
  • Once they have reconstructed the full thing they might not be interested in reducing the volume
  • If they bin the data they will have to convert the binned reduced geometry to their unbinned reduced geometry

Could this be a method on acquisition_geometry

  • The method needs access to the data so would have to pass the data as an argument?
  • Similar to the ag.get_ImageGeometry() method we could have ag.get_reduced_ImageGeometry(data)

Could this be a method on acqusition_data

  • Makes the most sense, but confusing get_ImageGeometry is a method on acquisition_geometry and this would be a method on acqusition_data. Could allow both?

@hrobarts
Copy link
Contributor Author

hrobarts commented Dec 2, 2025

What should the name be

  • get_reduced_ImageGeometry()
  • get_optimal_ImageGeometry() - makes more sense if we also do rotation at some point
  • get_shrunk_ImageGeometry()
    or
    get_ImageGeometry() with argument method=auto (or rotate in future?) or enums like in labels

@hrobarts hrobarts marked this pull request as ready for review February 25, 2026 09:04
Copy link
Member

@lauramurgatroyd lauramurgatroyd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @hrobarts
The shrinker tool is looking very useful!
I had some questions about the structure, plus some docstring and unit test suggestions, and maybe some more checking of valid parameters. Also wasn't sure about the mask parameter in the 'manual' case

'vertical':(bounds['vertical'][0], bounds['vertical'][1], 1)})(ig_unbinned)
return ig

def get_recon(self, mask_radius):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be great to have docstrings on all the methods

Comment on lines +44 to +45
recon_backend : {'tigre', 'astra'}
The backend to use for the reconstruction
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should mention that it uses the cil plugin, not the recon class.
Is this because the recon class doesn't yet support astra FDK?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I guess, this is just the same functionality as the COR corrector



class TestVolumeShrinker(unittest.TestCase):

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be good to have a test with the mask parameter used

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kind of test are you thinking? Testing the impact of the mask on the reduce_reconstruction_volume function? At the moment I'm only testing this on a very small test recon and I'm not sure how to make a mask predictably change the behaviour. I don't think this is something I have time to do but happy to add to a to-do list!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or do you mean test the kwargs are correctly initialised when we pass them to run?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes I meant the impact on the reduce_reconstruction_volume

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess if we had a circular mask that we know makes the region smaller than the automatic region on each dimension


bounds = vs.reduce_reconstruction_volume(self.test_recon, binning=1, method='threshold', kwargs={'threshold':0.1})
for dim in ['horizontal_x', 'horizontal_y', 'vertical']:
self.assertEqual(bounds[dim], (2,7))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably good to have a test where they aren't the same in each dimension? If not too difficult to achieve

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm added an asymmetrical volume

fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(8, 2.5))

axes[0].imshow(arr, cmap=plt.cm.gray)
axes[0].set_title('Original')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be useful to add to the titles the direction and that its the max value (like in the other plots)

@lauramurgatroyd
Copy link
Member

You will need to add the volume shrinker to this file in the documentation:
https://github.com/TomographicImaging/CIL/blob/master/docs/source/utilities.rst

hrobarts and others added 4 commits February 27, 2026 08:34
Co-authored-by: Laura Murgatroyd <60604372+lauramurgatroyd@users.noreply.github.com>
Signed-off-by: Hannah Robarts <77114597+hrobarts@users.noreply.github.com>
Copy link
Contributor Author

@hrobarts hrobarts left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @lauramurgatroyd thank you for your review! I think I've addressed all the comments now.

Comment on lines +81 to +82
If using 'threshold' method, specify the intensity threshold
between sample and background. Default is None.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it fails confusingly, I've added a check now!

Comment on lines +88 to +90
mask_radius: float, optional
Radius of circular mask to apply on the reconstructed volume, before
automatically cropping the reconstruction volume. Default is None.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have updated to say it's used for the cropping and in preview

Comment on lines +125 to +127
if method.lower() == 'manual':
mask_radius = kwargs.pop('mask_radius', None)
recon, binning = self.get_recon(mask_radius=mask_radius)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's useful for the preview as well. If you had data that created a region of interest sample with a ring in the recon then it might be useful to use a mask to check how your manual limits look on the volume. I've updated the docstring to say the mask is also used for preview.



class TestVolumeShrinker(unittest.TestCase):

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kind of test are you thinking? Testing the impact of the mask on the reduce_reconstruction_volume function? At the moment I'm only testing this on a very small test recon and I'm not sure how to make a mask predictably change the behaviour. I don't think this is something I have time to do but happy to add to a to-do list!



class TestVolumeShrinker(unittest.TestCase):

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or do you mean test the kwargs are correctly initialised when we pass them to run?

Comment on lines +44 to +45
recon_backend : {'tigre', 'astra'}
The backend to use for the reconstruction
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I guess, this is just the same functionality as the COR corrector

>>> cil_log_level = logging.getLogger()
>>> cil_log_level.setLevel(logging.DEBUG)
>>> vs = VolumeShrinker(data, recon_backend='astra')
>>> ig_reduced = vs.run(method='threshold' threshold=0.9)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing a ','



class TestVolumeShrinker(unittest.TestCase):

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes I meant the impact on the reduce_reconstruction_volume

Comment on lines +88 to +91
mask_radius: float, optional
Radius of circular mask to apply on the reconstructed volume, before
automatically cropping the reconstruction volume or displaying with
preview. Default is None.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should say here or under method that the mask_radius is not used except for the preview, for the manual case



class TestVolumeShrinker(unittest.TestCase):

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess if we had a circular mask that we know makes the region smaller than the automatic region on each dimension

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add tool for auto-cropping Image Geometry

2 participants