Skip to content

Commit 7c9d0f0

Browse files
authored
ENH: Add interface for reorienting images (#229)
* ENH: Add interface for reorienting images * FIX: Ensure CIFTI subcortical is in LAS orientation * FIX: Remove redundant template fetching
1 parent d352a21 commit 7c9d0f0

File tree

3 files changed

+140
-6
lines changed

3 files changed

+140
-6
lines changed

nibabies/interfaces/nibabel.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from pathlib import Path
2+
3+
from nipype.interfaces.base import (
4+
BaseInterfaceInputSpec,
5+
File,
6+
SimpleInterface,
7+
TraitedSpec,
8+
traits,
9+
)
10+
11+
12+
class ReorientImageInputSpec(BaseInterfaceInputSpec):
13+
in_file = File(exists=True, mandatory=True, desc="Moving file")
14+
target_file = File(
15+
exists=True, xor=["target_orientation"], desc="Reference file to reorient to"
16+
)
17+
target_orientation = traits.Str(
18+
xor=["target_file"], desc="Axis codes of coordinate system to reorient to"
19+
)
20+
21+
22+
class ReorientImageOutputSpec(TraitedSpec):
23+
out_file = File(desc="Reoriented file")
24+
25+
26+
class ReorientImage(SimpleInterface):
27+
input_spec = ReorientImageInputSpec
28+
output_spec = ReorientImageOutputSpec
29+
30+
def _run_interface(self, runtime):
31+
self._results["out_file"] = reorient_image(
32+
self.inputs.in_file,
33+
target_file=self.inputs.target_file,
34+
target_ornt=self.inputs.target_orientation,
35+
)
36+
return runtime
37+
38+
39+
def reorient_image(
40+
in_file: str, *, target_file: str = None, target_ornt: str = None, newpath: str = None
41+
) -> str:
42+
"""
43+
Reorient an image.
44+
45+
New orientation targets can be either another image, or a string representation of the
46+
orientation axis.
47+
48+
Parameters
49+
----------
50+
in_file : Image to be reoriented
51+
target_file : Reference image of desired orientation
52+
target_ornt : Orientation denoted by the first letter of each axis (i.e., "RAS", "LPI")
53+
"""
54+
import nibabel as nb
55+
56+
img = nb.load(in_file)
57+
img_axcodes = nb.aff2axcodes(img.affine)
58+
in_ornt = nb.orientations.axcodes2ornt(img_axcodes)
59+
60+
if target_file:
61+
target_img = nb.load(target_file)
62+
target_ornt = nb.aff2axcodes(target_img.affine)
63+
64+
out_ornt = nb.orientations.axcodes2ornt(target_ornt)
65+
ornt_xfm = nb.orientations.ornt_transform(in_ornt, out_ornt)
66+
reoriented = img.as_reoriented(ornt_xfm)
67+
68+
if newpath is None:
69+
newpath = Path()
70+
out_file = str((Path(newpath) / "reoriented.nii.gz").absolute())
71+
reoriented.to_filename(out_file)
72+
return out_file
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from pathlib import Path
2+
from uuid import uuid4
3+
4+
import nibabel as nb
5+
import numpy as np
6+
import pytest
7+
8+
from ..nibabel import ReorientImage
9+
10+
11+
def create_save_img(ornt: str):
12+
data = np.random.rand(2, 2, 2)
13+
img = nb.Nifti1Image(data, affine=np.eye(4))
14+
# img will alway be in RAS at the start
15+
ras = nb.orientations.axcodes2ornt("RAS")
16+
if ornt != 'RAS':
17+
new = nb.orientations.axcodes2ornt(ornt)
18+
xfm = nb.orientations.ornt_transform(ras, new)
19+
img = img.as_reoriented(xfm)
20+
out_file = f'{uuid4()}.nii.gz'
21+
img.to_filename(out_file)
22+
return out_file
23+
24+
25+
@pytest.mark.parametrize(
26+
"in_ornt,out_ornt",
27+
[
28+
("RAS", "RAS"),
29+
("RAS", "LAS"),
30+
("LAS", "RAS"),
31+
("RAS", "RPI"),
32+
("LPI", "RAS"),
33+
],
34+
)
35+
def test_reorient_image(tmpdir, in_ornt, out_ornt):
36+
tmpdir.chdir()
37+
38+
in_file = create_save_img(ornt=in_ornt)
39+
in_img = nb.load(in_file)
40+
assert ''.join(nb.aff2axcodes(in_img.affine)) == in_ornt
41+
42+
# test string representation
43+
res = ReorientImage(in_file=in_file, target_orientation=out_ornt).run()
44+
out_file = res.outputs.out_file
45+
out_img = nb.load(out_file)
46+
assert ''.join(nb.aff2axcodes(out_img.affine)) == out_ornt
47+
Path(out_file).unlink()
48+
49+
# test with target file
50+
target_file = create_save_img(ornt=out_ornt)
51+
target_img = nb.load(target_file)
52+
assert ''.join(nb.aff2axcodes(target_img.affine)) == out_ornt
53+
res = ReorientImage(in_file=in_file, target_file=target_file).run()
54+
out_file = res.outputs.out_file
55+
out_img = nb.load(out_file)
56+
assert ''.join(nb.aff2axcodes(out_img.affine)) == out_ornt
57+
58+
# cleanup
59+
for f in (in_file, target_file, out_file):
60+
Path(f).unlink()

nibabies/workflows/bold/resampling.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,7 @@ def init_bold_grayords_wf(grayord_density, mem_gb, repetition_time, name="bold_g
640640
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
641641
from niworkflows.interfaces.utility import KeySelect
642642

643+
from ...interfaces.nibabel import ReorientImage
643644
from ...interfaces.workbench import CiftiCreateDenseTimeseries
644645

645646
workflow = Workflow(name=name)
@@ -732,10 +733,10 @@ def init_bold_grayords_wf(grayord_density, mem_gb, repetition_time, name="bold_g
732733
name="split_surfaces",
733734
)
734735

736+
reorient_data = pe.Node(ReorientImage(target_orientation="LAS"), name="reorient_data")
737+
reorient_labels = reorient_data.clone(name="reorient_labels")
738+
735739
gen_cifti = pe.Node(CiftiCreateDenseTimeseries(timestep=repetition_time), name="gen_cifti")
736-
gen_cifti.inputs.volume_structure_labels = str(
737-
tf.api.get("MNI152NLin6Asym", resolution=2, atlas="HCP", suffix="dseg")
738-
)
739740
gen_cifti.inputs.roi_left = tf.api.get(
740741
"fsLR", density=fslr_density, hemi="L", desc="nomedialwall", suffix="dparc"
741742
)
@@ -750,9 +751,10 @@ def init_bold_grayords_wf(grayord_density, mem_gb, repetition_time, name="bold_g
750751

751752
# fmt: off
752753
workflow.connect([
753-
(inputnode, gen_cifti, [
754-
('subcortical_volume', 'volume_data'),
755-
('subcortical_labels', 'volume_structure_labels')]),
754+
(inputnode, reorient_data, [("subcortical_volume", "in_file")]),
755+
(inputnode, reorient_labels, [("subcortical_labels", "in_file")]),
756+
(reorient_data, gen_cifti, [("out_file", "volume_data")]),
757+
(reorient_labels, gen_cifti, [("out_file", "volume_structure_labels")]),
756758
(inputnode, select_fs_surf, [('surf_files', 'surf_files'),
757759
('surf_refs', 'keys')]),
758760
(select_fs_surf, resample, [('surf_files', 'in_file')]),

0 commit comments

Comments
 (0)