Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/source/transforms/preprocessing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,26 +68,36 @@ Spatial

.. currentmodule:: torchio.transforms


:class:`CropOrPad`
~~~~~~~~~~~~~~~~~~

.. autoclass:: CropOrPad
:show-inheritance:
:members: _get_six_bounds_parameters


:class:`ToOrientation`
~~~~~~~~~~~~~~~~~~~~~~

.. autoclass:: ToOrientation
:show-inheritance:


:class:`ToCanonical`
~~~~~~~~~~~~~~~~~~~~

.. autoclass:: ToCanonical
:show-inheritance:


:class:`Transpose`
~~~~~~~~~~~~~~~~~~

.. autoclass:: Transpose
:show-inheritance:


:class:`Resample`
~~~~~~~~~~~~~~~~~

Expand Down
14 changes: 10 additions & 4 deletions src/torchio/data/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def __repr__(self):
[
f'shape: {self.shape}',
f'spacing: {self.get_spacing_string()}',
f'orientation: {"".join(self.orientation)}+',
f'orientation: {self.orientation_str}+',
]
)
if self._loaded:
Expand Down Expand Up @@ -253,6 +253,7 @@ def set_data(self, tensor: TypeData):
tensor: 4D tensor with dimensions :math:`(C, W, H, D)`.
"""
self[DATA] = self._parse_tensor(tensor, none_ok=False)
self._loaded = True

@property
def tensor(self) -> torch.Tensor:
Expand Down Expand Up @@ -324,7 +325,12 @@ def width(self) -> int:
@property
def orientation(self) -> tuple[str, str, str]:
"""Orientation codes."""
return nib.aff2axcodes(self.affine)
return nib.orientations.aff2axcodes(self.affine)

@property
def orientation_str(self) -> str:
"""Orientation as a string."""
return ''.join(self.orientation)

@property
def direction(self) -> TypeDirection3D:
Expand All @@ -339,7 +345,7 @@ def spacing(self) -> tuple[float, float, float]:
"""Voxel spacing in mm."""
_, spacing = get_rotation_and_spacing_from_affine(self.affine)
sx, sy, sz = spacing
return sx, sy, sz
return float(sx), float(sy), float(sz)

@property
def origin(self) -> tuple[float, float, float]:
Expand Down Expand Up @@ -743,7 +749,7 @@ def to_gif(
)

def to_ras(self) -> Image:
if self.orientation != tuple('RAS'):
if self.orientation_str != 'RAS':
from ..transforms.preprocessing.spatial.to_canonical import ToCanonical

return ToCanonical()(self)
Expand Down
2 changes: 2 additions & 0 deletions src/torchio/transforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
from .preprocessing import To
from .preprocessing import ToCanonical
from .preprocessing import ToOrientation
from .preprocessing import Transpose
from .preprocessing import ZNormalization
from .preprocessing.intensity.histogram_standardization import train_histogram
from .preprocessing.label.label_transform import LabelTransform
Expand Down Expand Up @@ -105,6 +106,7 @@
'To',
'ToCanonical',
'ToOrientation',
'Transpose',
'ZNormalization',
'HistogramStandardization',
'RescaleIntensity',
Expand Down
5 changes: 2 additions & 3 deletions src/torchio/transforms/augmentation/spatial/random_flip.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,15 @@ class Flip(SpatialTransform):
def __init__(self, axes, **kwargs):
super().__init__(**kwargs)
self.axes = _parse_axes(axes)
self.args_names = ('axes',)
self.args_names = ['axes']

def apply_transform(self, subject: Subject) -> Subject:
axes = _ensure_axes_indices(subject, self.axes)
for image in self.get_images(subject):
_flip_image(image, axes)
return subject

@staticmethod
def is_invertible():
def is_invertible(self):
return True

def inverse(self):
Expand Down
2 changes: 2 additions & 0 deletions src/torchio/transforms/preprocessing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .spatial.resize import Resize
from .spatial.to_canonical import ToCanonical
from .spatial.to_orientation import ToOrientation
from .spatial.transpose import Transpose

__all__ = [
'Pad',
Expand All @@ -27,6 +28,7 @@
'Resample',
'ToCanonical',
'ToOrientation',
'Transpose',
'CropOrPad',
'CopyAffine',
'EnsureShapeMultiple',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def __init__(self, orientation: str = 'RAS', **kwargs):

def apply_transform(self, subject: Subject) -> Subject:
for image in subject.get_images(intensity_only=False):
current_orientation = ''.join(nib.aff2axcodes(image.affine))
current_orientation = ''.join(nib.orientations.aff2axcodes(image.affine))

# If the image is already in the target orientation, skip it
if current_orientation == self.orientation:
Expand All @@ -80,7 +80,7 @@ def apply_transform(self, subject: Subject) -> Subject:
# NIfTI images should have channels in 5th dimension
array = rearrange(image.numpy(), 'C W H D -> W H D 1 C')

nii = nib.Nifti1Image(array, image.affine)
nii = nib.nifti1.Nifti1Image(array, image.affine)

# Compute transform from current orientation to target orientation
current_orientation = orientations.io_orientation(nii.affine)
Expand Down
38 changes: 38 additions & 0 deletions src/torchio/transforms/preprocessing/spatial/transpose.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from ....data.subject import Subject
from ...spatial_transform import SpatialTransform
from .to_orientation import ToOrientation


class Transpose(SpatialTransform):
"""Swap the first and last spatial dimensions of the image.

The spatial metadata is updated accordingly, so the world coordinates of
all voxels in the input and output spaces match.

Example:

>>> import torchio as tio
>>> image = tio.datasets.FPG().t1
>>> image
ScalarImage(shape: (1, 256, 256, 176); spacing: (1.00, 1.00, 1.00); orientation: PIR+; path: "/home/fernando/.cache/torchio/fpg/t1.nii.gz")
>>> transpose = tio.Transpose()
>>> transposed = transpose(image)
>>> transposed
ScalarImage(shape: (1, 176, 256, 256); spacing: (1.00, 1.00, 1.00); orientation: RIP+; dtype: torch.IntTensor; memory: 44.0 MiB)
"""

def apply_transform(self, subject: Subject) -> Subject:
for image in self.get_images(subject):
old_orientation = image.orientation_str
new_orientation = old_orientation[::-1]
transform = ToOrientation(new_orientation)
transposed = transform(image)
image.set_data(transposed.data)
image.affine = transposed.affine
return subject

def is_invertible(self):
return True

def inverse(self):
return self
21 changes: 21 additions & 0 deletions tests/transforms/preprocessing/test_transpose.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import SimpleITK as sitk

import torchio as tio

from ...utils import TorchioTestCase


class TestTranspose(TorchioTestCase):
def test_transpose(self):
transform = tio.Transpose()
image = tio.ScalarImage(self.get_image_path('image'))
transformed = transform(image)
sitk_image = sitk.GetImageFromArray(image.numpy()[0])
from_sitk = tio.ScalarImage.from_sitk(sitk_image)
self.assert_tensor_equal(transformed.data, from_sitk.data)

def test_orientation_reversed(self):
transform = tio.Transpose()
image = tio.ScalarImage(self.get_image_path('image'))
transformed = transform(image)
self.assertEqual(transformed.orientation_str, image.orientation_str[::-1])
Loading