Skip to content

Commit 959bed7

Browse files
authored
Merge pull request #736 from mgxd/enh/reorient-image
ENH: ReorientImage interface
2 parents c202fa8 + 39f0fd1 commit 959bed7

File tree

3 files changed

+134
-67
lines changed

3 files changed

+134
-67
lines changed

niworkflows/interfaces/cifti.py

Lines changed: 4 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
from nibabel import cifti2 as ci
3030
import numpy as np
3131
from nilearn.image import resample_to_img
32-
3332
from nipype.utils.filemanip import split_filename
3433
from nipype.interfaces.base import (
3534
BaseInterfaceInputSpec,
@@ -41,6 +40,8 @@
4140
)
4241
import templateflow.api as tf
4342

43+
from niworkflows.interfaces.nibabel import reorient_image
44+
4445
CIFTI_SURFACES = ("fsaverage5", "fsaverage6", "fsLR")
4546
CIFTI_VOLUMES = ("MNI152NLin2009cAsym", "MNI152NLin6Asym")
4647
CIFTI_STRUCT_WITH_LABELS = { # CITFI structures with corresponding labels
@@ -356,8 +357,8 @@ def _create_cifti_image(
356357
bold_img = resample_to_img(bold_img, label_img)
357358

358359
# ensure images match HCP orientation (LAS)
359-
bold_img = _reorient_image(bold_img, orientation="LAS")
360-
label_img = _reorient_image(label_img, orientation="LAS")
360+
bold_img = reorient_image(bold_img, target_ornt="LAS")
361+
label_img = reorient_image(label_img, target_ornt="LAS")
361362

362363
bold_data = bold_img.get_fdata(dtype="float32")
363364
timepoints = bold_img.shape[3]
@@ -466,66 +467,3 @@ def _create_cifti_image(
466467
out_file = "{}.dtseries.nii".format(split_filename(bold_file)[1])
467468
ci.save(img, out_file)
468469
return Path.cwd() / out_file
469-
470-
471-
def _reorient_image(img, *, target_img=None, orientation=None):
472-
"""
473-
Coerce an image to a target orientation.
474-
475-
.. note::
476-
Only RAS -> LAS conversion is currently supported
477-
478-
Parameters
479-
----------
480-
img : :obj:`SpatialImage`
481-
image to be reoriented
482-
target_img : :obj:`SpatialImage`, optional
483-
target in desired orientation
484-
orientation : :obj:`str` or :obj:`tuple`, optional
485-
desired orientation, if no target image is provided
486-
487-
.. testsetup::
488-
>>> img = nb.load(Path(test_data) / 'testSpatialNormalizationRPTMovingWarpedImage.nii.gz')
489-
>>> las_img = img.as_reoriented([[0, -1], [1, 1], [2, 1]])
490-
491-
Examples
492-
--------
493-
>>> nimg = _reorient_image(img, target_img=img)
494-
>>> nb.aff2axcodes(nimg.affine)
495-
('R', 'A', 'S')
496-
497-
>>> nimg = _reorient_image(img, target_img=las_img)
498-
>>> nb.aff2axcodes(nimg.affine)
499-
('L', 'A', 'S')
500-
501-
>>> nimg = _reorient_image(img, orientation='LAS')
502-
>>> nb.aff2axcodes(nimg.affine)
503-
('L', 'A', 'S')
504-
505-
>>> _reorient_image(img, orientation='LPI')
506-
Traceback (most recent call last):
507-
...
508-
NotImplementedError: Cannot reorient ...
509-
510-
>>> _reorient_image(img)
511-
Traceback (most recent call last):
512-
...
513-
RuntimeError: No orientation ...
514-
515-
"""
516-
orient0 = nb.aff2axcodes(img.affine)
517-
if target_img is not None:
518-
orient1 = nb.aff2axcodes(target_img.affine)
519-
elif orientation is not None:
520-
orient1 = tuple(orientation)
521-
else:
522-
raise RuntimeError("No orientation to reorient to!")
523-
524-
if orient0 == orient1: # already in desired orientation
525-
return img
526-
elif orient0 == tuple("RAS") and orient1 == tuple("LAS"): # RAS -> LAS
527-
return img.as_reoriented([[0, -1], [1, 1], [2, 1]])
528-
else:
529-
raise NotImplementedError(
530-
"Cannot reorient {0} to {1}.".format(orient0, orient1)
531-
)

niworkflows/interfaces/nibabel.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
# https://www.nipreps.org/community/licensing/
2222
#
2323
"""Nibabel-based interfaces."""
24+
from pathlib import Path
25+
2426
import numpy as np
2527
import nibabel as nb
2628
from nipype import logging
@@ -494,6 +496,79 @@ def _run_interface(self, runtime):
494496
return runtime
495497

496498

499+
class ReorientImageInputSpec(BaseInterfaceInputSpec):
500+
in_file = File(exists=True, mandatory=True, desc="Moving file")
501+
target_file = File(
502+
exists=True, xor=["target_orientation"], desc="Reference file to reorient to"
503+
)
504+
target_orientation = traits.Str(
505+
xor=["target_file"], desc="Axis codes of coordinate system to reorient to"
506+
)
507+
508+
509+
class ReorientImageOutputSpec(TraitedSpec):
510+
out_file = File(desc="Reoriented file")
511+
512+
513+
class ReorientImage(SimpleInterface):
514+
input_spec = ReorientImageInputSpec
515+
output_spec = ReorientImageOutputSpec
516+
517+
def _run_interface(self, runtime):
518+
self._results["out_file"] = reorient_file(
519+
self.inputs.in_file,
520+
target_file=self.inputs.target_file,
521+
target_ornt=self.inputs.target_orientation,
522+
)
523+
return runtime
524+
525+
526+
def reorient_file(
527+
in_file: str, *, target_file: str = None, target_ornt: str = None, newpath: str = None,
528+
) -> str:
529+
"""
530+
Reorient an image.
531+
532+
New orientation targets can be either another image, or a string representation of the
533+
orientation axis.
534+
535+
Parameters
536+
----------
537+
in_file : Image to be reoriented
538+
target_file : Reference image of desired orientation
539+
target_ornt : Orientation denoted by the first letter of each axis (i.e., "RAS", "LPI")
540+
"""
541+
import nibabel as nb
542+
543+
img = nb.load(in_file)
544+
if not target_file and not target_ornt:
545+
raise TypeError("No target orientation or file is specified.")
546+
547+
if target_file:
548+
target_img = nb.load(target_file)
549+
target_ornt = nb.aff2axcodes(target_img.affine)
550+
551+
reoriented = reorient_image(img, target_ornt)
552+
553+
if newpath is None:
554+
newpath = Path()
555+
out_file = str((Path(newpath) / "reoriented.nii.gz").absolute())
556+
reoriented.to_filename(out_file)
557+
return out_file
558+
559+
560+
def reorient_image(img: nb.spatialimages.SpatialImage, target_ornt: str):
561+
"""Reorient an image in memory."""
562+
import nibabel as nb
563+
564+
img_axcodes = nb.aff2axcodes(img.affine)
565+
in_ornt = nb.orientations.axcodes2ornt(img_axcodes)
566+
out_ornt = nb.orientations.axcodes2ornt(target_ornt)
567+
ornt_xfm = nb.orientations.ornt_transform(in_ornt, out_ornt)
568+
r_img = img.as_reoriented(ornt_xfm)
569+
return r_img
570+
571+
497572
def _gen_reference(
498573
fixed_image,
499574
moving_image,

niworkflows/interfaces/tests/test_nibabel.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
import nibabel as nb
3030
import pytest
3131

32-
from ..nibabel import Binarize, ApplyMask, SplitSeries, MergeSeries, MergeROIs, MapLabels
32+
from ..nibabel import (
33+
Binarize, ApplyMask, SplitSeries, MergeSeries, MergeROIs, MapLabels, ReorientImage
34+
)
3335

3436

3537
@pytest.fixture
@@ -280,3 +282,55 @@ def test_map_labels(tmpdir, data, mapping, tojson, expected):
280282
Path(in_file).unlink()
281283
if tojson:
282284
Path(map_file).unlink()
285+
286+
287+
def create_save_img(ornt: str):
288+
data = np.random.rand(2, 2, 2)
289+
img = nb.Nifti1Image(data, affine=np.eye(4))
290+
# img will alway be in RAS at the start
291+
ras = nb.orientations.axcodes2ornt("RAS")
292+
if ornt != 'RAS':
293+
new = nb.orientations.axcodes2ornt(ornt)
294+
xfm = nb.orientations.ornt_transform(ras, new)
295+
img = img.as_reoriented(xfm)
296+
out_file = f'{uuid.uuid4()}.nii.gz'
297+
img.to_filename(out_file)
298+
return out_file
299+
300+
301+
@pytest.mark.parametrize(
302+
"in_ornt,out_ornt",
303+
[
304+
("RAS", "RAS"),
305+
("RAS", "LAS"),
306+
("LAS", "RAS"),
307+
("RAS", "RPI"),
308+
("LPI", "RAS"),
309+
],
310+
)
311+
def test_reorient_image(tmpdir, in_ornt, out_ornt):
312+
tmpdir.chdir()
313+
314+
in_file = create_save_img(ornt=in_ornt)
315+
in_img = nb.load(in_file)
316+
assert ''.join(nb.aff2axcodes(in_img.affine)) == in_ornt
317+
318+
# test string representation
319+
res = ReorientImage(in_file=in_file, target_orientation=out_ornt).run()
320+
out_file = res.outputs.out_file
321+
out_img = nb.load(out_file)
322+
assert ''.join(nb.aff2axcodes(out_img.affine)) == out_ornt
323+
Path(out_file).unlink()
324+
325+
# test with target file
326+
target_file = create_save_img(ornt=out_ornt)
327+
target_img = nb.load(target_file)
328+
assert ''.join(nb.aff2axcodes(target_img.affine)) == out_ornt
329+
res = ReorientImage(in_file=in_file, target_file=target_file).run()
330+
out_file = res.outputs.out_file
331+
out_img = nb.load(out_file)
332+
assert ''.join(nb.aff2axcodes(out_img.affine)) == out_ornt
333+
334+
# cleanup
335+
for f in (in_file, target_file, out_file):
336+
Path(f).unlink()

0 commit comments

Comments
 (0)