Skip to content

Commit

Permalink
Merge pull request #879 from mgxd/add/t2w-template-dim
Browse files Browse the repository at this point in the history
ADD: Make template dimensions support T2w as well
  • Loading branch information
mgxd authored Jul 25, 2024
2 parents 1dc0327 + 785c40f commit a744a53
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 7 deletions.
27 changes: 20 additions & 7 deletions niworkflows/interfaces/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ def _run_interface(self, runtime):

CONFORMATION_TEMPLATE = """\t\t<h3 class="elem-title">Anatomical Conformation</h3>
\t\t<ul class="elem-desc">
\t\t\t<li>Input T1w images: {n_t1w}</li>
\t\t\t<li>Input {anat} images: {n_anat}</li>
\t\t\t<li>Output orientation: RAS</li>
\t\t\t<li>Output dimensions: {dims}</li>
\t\t\t<li>Output voxel size: {zooms}</li>
Expand All @@ -378,8 +378,15 @@ def _run_interface(self, runtime):


class _TemplateDimensionsInputSpec(BaseInterfaceInputSpec):
anat_type = traits.Enum("T1w", "T2w", usedefault=True, desc="Anatomical image type")
anat_list = InputMultiObject(
File(exists=True), xor=["t1w_list"], desc="input anatomical images"
)
t1w_list = InputMultiObject(
File(exists=True), mandatory=True, desc="input T1w images"
File(exists=True),
xor=["anat_list"],
deprecated="1.14.0",
new_name="anat_list",
)
max_scale = traits.Float(
3.0, usedefault=True, desc="Maximum scaling factor in images to accept"
Expand All @@ -388,6 +395,7 @@ class _TemplateDimensionsInputSpec(BaseInterfaceInputSpec):

class _TemplateDimensionsOutputSpec(TraitedSpec):
t1w_valid_list = OutputMultiObject(exists=True, desc="valid T1w images")
anat_valid_list = OutputMultiObject(exists=True, desc="valid anatomical images")
target_zooms = traits.Tuple(
traits.Float, traits.Float, traits.Float, desc="Target zoom information"
)
Expand All @@ -399,8 +407,8 @@ class _TemplateDimensionsOutputSpec(TraitedSpec):

class TemplateDimensions(SimpleInterface):
"""
Finds template target dimensions for a series of T1w images, filtering low-resolution images,
if necessary.
Finds template target dimensions for a series of anatomical images, filtering low-resolution
images, if necessary.
Along each axis, the minimum voxel size (zoom) and the maximum number of voxels (shape) are
found across images.
Expand All @@ -426,7 +434,8 @@ def _generate_segment(self, discards, dims, zooms):
)
zoom_fmt = "{:.02g}mm x {:.02g}mm x {:.02g}mm".format(*zooms)
return CONFORMATION_TEMPLATE.format(
n_t1w=len(self.inputs.t1w_list),
anat=self.inputs.anat_type,
n_anat=len(self.inputs.anat_list),
dims="x".join(map(str, dims)),
zooms=zoom_fmt,
n_discards=len(discards),
Expand All @@ -435,7 +444,10 @@ def _generate_segment(self, discards, dims, zooms):

def _run_interface(self, runtime):
# Load images, orient as RAS, collect shape and zoom data
in_names = np.array(self.inputs.t1w_list)
if not self.inputs.anat_list: # Deprecate: 1.14.0
self.inputs.anat_list = self.inputs.t1w_list

in_names = np.array(self.inputs.anat_list)
orig_imgs = np.vectorize(nb.load)(in_names)
reoriented = np.vectorize(nb.as_closest_canonical)(orig_imgs)
all_zooms = np.array([img.header.get_zooms()[:3] for img in reoriented])
Expand All @@ -452,7 +464,8 @@ def _run_interface(self, runtime):

# Ignore dropped images
valid_fnames = np.atleast_1d(in_names[valid]).tolist()
self._results["t1w_valid_list"] = valid_fnames
self._results["anat_valid_list"] = valid_fnames
self._results["t1w_valid_list"] = valid_fnames # Deprecate: 1.14.0

# Set target shape information
target_zooms = all_zooms[valid].min(axis=0)
Expand Down
36 changes: 36 additions & 0 deletions niworkflows/interfaces/tests/test_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#
"""Test images module."""
import time
from pathlib import Path
import numpy as np
import nibabel as nb
from nipype.pipeline import engine as pe
Expand Down Expand Up @@ -179,3 +180,38 @@ def test_RobustAverage(tmpdir, shape):

assert out_file.shape == (10, 10, 10)
assert np.allclose(out_file.get_fdata(), 1.0)


def test_TemplateDimensions(tmp_path):
"""Exercise the various types of inputs."""
shapes = [
(10, 10, 10),
(11, 11, 11),
]
zooms = [
(1, 1, 1),
(0.9, 0.9, 0.9),
]

for i, (shape, zoom) in enumerate(zip(shapes, zooms)):
img = nb.Nifti1Image(np.ones(shape, dtype="float32"), np.eye(4))
img.header.set_zooms(zoom)
img.to_filename(tmp_path / f"test{i}.nii")

anat_list = [str(tmp_path / f"test{i}.nii") for i in range(2)]
td = im.TemplateDimensions(anat_list=anat_list)
res = td.run()

report = Path(res.outputs.out_report).read_text()
assert "Input T1w images: 2" in report
assert "Output dimensions: 11x11x11" in report
assert "Output voxel size: 0.9mm x 0.9mm x 0.9mm" in report
assert "Discarded images: 0" in report

assert res.outputs.t1w_valid_list == anat_list
assert res.outputs.anat_valid_list == anat_list
assert np.allclose(res.outputs.target_zooms, (0.9, 0.9, 0.9))
assert res.outputs.target_shape == (11, 11, 11)

with pytest.warns(UserWarning, match="t1w_list .* is deprecated"):
im.TemplateDimensions(t1w_list=anat_list)

0 comments on commit a744a53

Please sign in to comment.