Skip to content

Commit a1394a6

Browse files
committed
RF: Move slicing machinery to SpatialFirstSlicer class
1 parent b9201cf commit a1394a6

File tree

1 file changed

+87
-83
lines changed

1 file changed

+87
-83
lines changed

nibabel/spatialimages.py

Lines changed: 87 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -322,24 +322,95 @@ class ImageDataError(Exception):
322322
pass
323323

324324

325-
class SpatialImage(DataobjImage):
326-
''' Template class for volumetric (3D/4D) images '''
327-
header_class = SpatialHeader
325+
class SpatialFirstSlicer(object):
326+
''' Slicing interface that returns a new image with an updated affine
327+
328+
Checks that an image's first three axes are spatial
329+
'''
330+
def __init__(self, img):
331+
from .imageclasses import spatial_axes_first
332+
if not spatial_axes_first(img):
333+
raise ValueError("Cannot predict position of spatial axes for "
334+
"Image type " + img.__class__.__name__)
335+
self.img = img
336+
337+
def __getitem__(self, slicer):
338+
try:
339+
slicer = self.check_slicing(slicer)
340+
except ValueError as err:
341+
raise IndexError(*err.args)
342+
dataobj = self.img.dataobj[slicer]
343+
affine = self.slice_affine(slicer)
344+
return self.img.__class__(dataobj.copy(), affine, self.img.header)
345+
346+
def check_slicing(self, slicer, return_spatial=False):
347+
''' Canonicalize slicers and check for scalar indices in spatial dims
348+
349+
Parameters
350+
----------
351+
slicer : object
352+
something that can be used to slice an array as in
353+
``arr[sliceobj]``
354+
return_spatial : bool
355+
return only slices along spatial dimensions (x, y, z)
328356
329-
class Slicer(object):
330-
''' Slicing interface that returns a new image with an updated affine
357+
Returns
358+
-------
359+
slicer : object
360+
Validated slicer object that will slice image's `dataobj`
361+
without collapsing spatial dimensions
331362
'''
332-
def __init__(self, img):
333-
self.img = img
363+
slicer = canonical_slicers(slicer, self.img.shape)
364+
spatial_slices = slicer[:3]
365+
for subslicer in spatial_slices:
366+
if subslicer is None:
367+
raise IndexError("New axis not permitted in spatial dimensions")
368+
elif isinstance(subslicer, int):
369+
raise IndexError("Scalar indices disallowed in spatial dimensions; "
370+
"Use `[x]` or `x:x+1`.")
371+
return spatial_slices if return_spatial else slicer
334372

335-
def __getitem__(self, slicer):
336-
try:
337-
slicer = self.img._check_slicing(slicer)
338-
except ValueError as err:
339-
raise IndexError(*err.args)
340-
dataobj = self.img.dataobj[slicer]
341-
affine = self.img._slice_affine(slicer)
342-
return self.img.__class__(dataobj.copy(), affine, self.img.header)
373+
def slice_affine(self, slicer):
374+
""" Retrieve affine for current image, if sliced by a given index
375+
376+
Applies scaling if down-sampling is applied, and adjusts the intercept
377+
to account for any cropping.
378+
379+
Parameters
380+
----------
381+
slicer : object
382+
something that can be used to slice an array as in
383+
``arr[sliceobj]``
384+
385+
Returns
386+
-------
387+
affine : (4,4) ndarray
388+
Affine with updated scale and intercept
389+
"""
390+
slicer = self.check_slicing(slicer, return_spatial=True)
391+
392+
# Transform:
393+
# sx 0 0 tx
394+
# 0 sy 0 ty
395+
# 0 0 sz tz
396+
# 0 0 0 1
397+
transform = np.eye(4, dtype=int)
398+
399+
for i, subslicer in enumerate(slicer):
400+
if isinstance(subslicer, slice):
401+
if subslicer.step == 0:
402+
raise ValueError("slice step cannot be 0")
403+
transform[i, i] = subslicer.step if subslicer.step is not None else 1
404+
transform[i, 3] = subslicer.start or 0
405+
# If slicer is None, nothing to do
406+
407+
return self.img.affine.dot(transform)
408+
409+
410+
class SpatialImage(DataobjImage):
411+
''' Template class for volumetric (3D/4D) images '''
412+
header_class = SpatialHeader
413+
ImageSlicer = SpatialFirstSlicer
343414

344415
def __init__(self, dataobj, affine, header=None,
345416
extra=None, file_map=None):
@@ -477,69 +548,6 @@ def from_image(klass, img):
477548
klass.header_class.from_header(img.header),
478549
extra=img.extra.copy())
479550

480-
def _check_slicing(self, slicer, return_spatial=False):
481-
''' Canonicalize slicers and check for scalar indices in spatial dims
482-
483-
Parameters
484-
----------
485-
slicer : object
486-
something that can be used to slice an array as in
487-
``arr[sliceobj]``
488-
return_spatial : bool
489-
return only slices along spatial dimensions (x, y, z)
490-
491-
Returns
492-
-------
493-
slicer : object
494-
Validated slicer object that will slice image's `dataobj`
495-
without collapsing spatial dimensions
496-
'''
497-
slicer = canonical_slicers(slicer, self.shape)
498-
spatial_slices = slicer[:3]
499-
for subslicer in spatial_slices:
500-
if subslicer is None:
501-
raise IndexError("New axis not permitted in spatial dimensions")
502-
elif isinstance(subslicer, int):
503-
raise IndexError("Scalar indices disallowed in spatial dimensions; "
504-
"Use `[x]` or `x:x+1`.")
505-
return spatial_slices if return_spatial else slicer
506-
507-
def _slice_affine(self, slicer):
508-
""" Retrieve affine for current image, if sliced by a given index
509-
510-
Applies scaling if down-sampling is applied, and adjusts the intercept
511-
to account for any cropping.
512-
513-
Parameters
514-
----------
515-
slicer : object
516-
something that can be used to slice an array as in
517-
``arr[sliceobj]``
518-
519-
Returns
520-
-------
521-
affine : (4,4) ndarray
522-
Affine with updated scale and intercept
523-
"""
524-
slicer = self._check_slicing(slicer, return_spatial=True)
525-
526-
# Transform:
527-
# sx 0 0 tx
528-
# 0 sy 0 ty
529-
# 0 0 sz tz
530-
# 0 0 0 1
531-
transform = np.eye(4, dtype=int)
532-
533-
for i, subslicer in enumerate(slicer):
534-
if isinstance(subslicer, slice):
535-
if subslicer.step == 0:
536-
raise ValueError("slice step cannot be 0")
537-
transform[i, i] = subslicer.step if subslicer.step is not None else 1
538-
transform[i, 3] = subslicer.start or 0
539-
# If slicer is None, nothing to do
540-
541-
return self.affine.dot(transform)
542-
543551
@property
544552
def slicer(self):
545553
""" Slicer object that returns cropped and subsampled images
@@ -558,11 +566,7 @@ def slicer(self):
558566
559567
.. _aliasing: https://en.wikipedia.org/wiki/Aliasing
560568
"""
561-
from .imageclasses import spatial_axes_first
562-
if not spatial_axes_first(self):
563-
raise ValueError("Cannot predict position of spatial axes for "
564-
"Image type " + self.__class__.__name__)
565-
return self.Slicer(self)
569+
return self.ImageSlicer(self)
566570

567571

568572
def __getitem__(self, idx):

0 commit comments

Comments
 (0)