-
Notifications
You must be signed in to change notification settings - Fork 260
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
ENH: Add image slicing #550
Merged
Merged
Changes from 35 commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
2516b4f
ENH: Add funcs.crop_image
effigies f031494
ENH: crop_image to bounds, adding margin
effigies 008255a
STY: Do not capture exception in variable
effigies a6af773
ENH: Permit cropping with negative indices, give more specific errors
effigies 5c11174
TEST: Initial crop_image test
effigies 9b7a35c
ENH: Enable SpatialImage.{slice,__getitem__}
effigies 0c37637
ENH: Enable minimally-fancy indexing
effigies 00f6ed1
RF: Move to img.slicer approach
effigies ee0c2b4
RF: Simplify affine transform
effigies 8c113d4
REVERT fileslice modifications
effigies 469376b
ENH: Add SpatialImage._spatial_dims slicer
effigies cfa9fdd
RF: Move slicer validation into image
effigies 9bd4d0e
RF: Remove crop_image (for now)
effigies c686a5a
FIX: Type check, _check_slicing arguments
effigies dfdbbb7
ENH: Handle new axes in Minc
effigies 2d8c7b5
TEST: Image slicing
effigies 0718701
Translate ValueErrors to IndexErrors when used
effigies b3b4a22
TEST: Handle MGHFormat (max: 4D) correctly
effigies 67efd1c
TEST: Test single-element list indices
effigies 526d81c
RF: All SpatialImages are makeable
effigies fcf55b5
TEST: Check step = 0 case
effigies 370d0e1
ENH: Drop fancy indexing
effigies 2d6547c
TEST: Test single-slice and fancy indexing raise errors
effigies e809636
DOC: Changelog entry
effigies 07bbb25
TEST: Do not use deprecated _header_data
effigies 1e15c98
DOC: Link to aliasing wiki article [skip ci]
effigies fbd9e6e
ENH: Improve exception message
effigies f57bfd5
RF: Specify spatial dims in each class
effigies 8a6150c
TEST: Check for _spatial_dims
effigies ea053c8
TEST: Minor fixes
effigies 6f6e38a
RF: Switch to spatial_axes_first
effigies b9201cf
RF/TEST: Purge _spatial_dims and related tests
effigies a1394a6
RF: Move slicing machinery to SpatialFirstSlicer class
effigies d4341af
ENH: Raise IndexError on empty slices
effigies 44e8697
TEST: Update slice_affine test, check zero slices
effigies 919b71a
DOC: Some clarifying comments in SpatialFirstSlicer
effigies File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -140,6 +140,7 @@ | |
from .filebasedimages import ImageFileError # flake8: noqa; for back-compat | ||
from .viewers import OrthoSlicer3D | ||
from .volumeutils import shape_zoom_affine | ||
from .fileslice import canonical_slicers | ||
from .deprecated import deprecate_with_version | ||
from .orientations import apply_orientation, inv_ornt_aff | ||
|
||
|
@@ -321,9 +322,99 @@ class ImageDataError(Exception): | |
pass | ||
|
||
|
||
class SpatialFirstSlicer(object): | ||
''' Slicing interface that returns a new image with an updated affine | ||
|
||
Checks that an image's first three axes are spatial | ||
''' | ||
def __init__(self, img): | ||
from .imageclasses import spatial_axes_first | ||
if not spatial_axes_first(img): | ||
raise ValueError("Cannot predict position of spatial axes for " | ||
"Image type " + img.__class__.__name__) | ||
self.img = img | ||
|
||
def __getitem__(self, slicer): | ||
try: | ||
slicer = self.check_slicing(slicer) | ||
except ValueError as err: | ||
raise IndexError(*err.args) | ||
|
||
dataobj = self.img.dataobj[slicer] | ||
if any(dim == 0 for dim in dataobj.shape): | ||
raise IndexError("Empty slice requested") | ||
|
||
affine = self.slice_affine(slicer) | ||
return self.img.__class__(dataobj.copy(), affine, self.img.header) | ||
|
||
def check_slicing(self, slicer, return_spatial=False): | ||
''' Canonicalize slicers and check for scalar indices in spatial dims | ||
|
||
Parameters | ||
---------- | ||
slicer : object | ||
something that can be used to slice an array as in | ||
``arr[sliceobj]`` | ||
return_spatial : bool | ||
return only slices along spatial dimensions (x, y, z) | ||
|
||
Returns | ||
------- | ||
slicer : object | ||
Validated slicer object that will slice image's `dataobj` | ||
without collapsing spatial dimensions | ||
''' | ||
slicer = canonical_slicers(slicer, self.img.shape) | ||
spatial_slices = slicer[:3] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe worth a comment here about this line being specific to spatial-first images. Or generalize somehow to allow overriding and working out spatial slices from the image. |
||
for subslicer in spatial_slices: | ||
if subslicer is None: | ||
raise IndexError("New axis not permitted in spatial dimensions") | ||
elif isinstance(subslicer, int): | ||
raise IndexError("Scalar indices disallowed in spatial dimensions; " | ||
"Use `[x]` or `x:x+1`.") | ||
return spatial_slices if return_spatial else slicer | ||
|
||
def slice_affine(self, slicer): | ||
""" Retrieve affine for current image, if sliced by a given index | ||
|
||
Applies scaling if down-sampling is applied, and adjusts the intercept | ||
to account for any cropping. | ||
|
||
Parameters | ||
---------- | ||
slicer : object | ||
something that can be used to slice an array as in | ||
``arr[sliceobj]`` | ||
|
||
Returns | ||
------- | ||
affine : (4,4) ndarray | ||
Affine with updated scale and intercept | ||
""" | ||
slicer = self.check_slicing(slicer, return_spatial=True) | ||
|
||
# Transform: | ||
# sx 0 0 tx | ||
# 0 sy 0 ty | ||
# 0 0 sz tz | ||
# 0 0 0 1 | ||
transform = np.eye(4, dtype=int) | ||
|
||
for i, subslicer in enumerate(slicer): | ||
if isinstance(subslicer, slice): | ||
if subslicer.step == 0: | ||
raise ValueError("slice step cannot be 0") | ||
transform[i, i] = subslicer.step if subslicer.step is not None else 1 | ||
transform[i, 3] = subslicer.start or 0 | ||
# If slicer is None, nothing to do | ||
|
||
return self.img.affine.dot(transform) | ||
|
||
|
||
class SpatialImage(DataobjImage): | ||
''' Template class for volumetric (3D/4D) images ''' | ||
header_class = SpatialHeader | ||
ImageSlicer = SpatialFirstSlicer | ||
|
||
def __init__(self, dataobj, affine, header=None, | ||
extra=None, file_map=None): | ||
|
@@ -461,12 +552,38 @@ def from_image(klass, img): | |
klass.header_class.from_header(img.header), | ||
extra=img.extra.copy()) | ||
|
||
@property | ||
def slicer(self): | ||
""" Slicer object that returns cropped and subsampled images | ||
|
||
The image is resliced in the current orientation; no rotation or | ||
resampling is performed, and no attempt is made to filter the image | ||
to avoid `aliasing`_. | ||
|
||
The affine matrix is updated with the new intercept (and scales, if | ||
down-sampling is used), so that all values are found at the same RAS | ||
locations. | ||
|
||
Slicing may include non-spatial dimensions. | ||
However, this method does not currently adjust the repetition time in | ||
the image header. | ||
|
||
.. _aliasing: https://en.wikipedia.org/wiki/Aliasing | ||
""" | ||
return self.ImageSlicer(self) | ||
|
||
|
||
def __getitem__(self, idx): | ||
''' No slicing or dictionary interface for images | ||
|
||
Use the slicer attribute to perform cropping and subsampling at your | ||
own risk. | ||
''' | ||
raise TypeError("Cannot slice image objects; consider slicing image " | ||
"array data with `img.dataobj[slice]` or " | ||
"`img.get_data()[slice]`") | ||
raise TypeError( | ||
"Cannot slice image objects; consider using `img.slicer[slice]` " | ||
"to generate a sliced image (see documentation for caveats) or " | ||
"slicing image array data with `img.dataobj[slice]` or " | ||
"`img.get_data()[slice]`") | ||
|
||
def orthoview(self): | ||
"""Plot the image using OrthoSlicer3D | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this go at the top of the file now? Or would that make a circular import? If so, maybe worth a comment.