From 0de7374249c9bb2e4f66c7a26fffd96355bc4bcd Mon Sep 17 00:00:00 2001 From: dPys Date: Tue, 24 Mar 2020 15:37:41 -0500 Subject: [PATCH 01/22] [ENH] Add nibabel-based split and merge interfaces per https://github.com/nipreps/dmriprep/pull/77 --- niworkflows/interfaces/nibabel.py | 66 ++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index 9364636279b..72784dbdc71 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -7,7 +7,7 @@ from nipype.utils.filemanip import fname_presuffix from nipype.interfaces.base import ( traits, TraitedSpec, BaseInterfaceInputSpec, File, - SimpleInterface + SimpleInterface, OutputMultiPath, InputMultiPath ) IFLOGGER = logging.getLogger('nipype.interface') @@ -89,3 +89,67 @@ def _run_interface(self, runtime): maskimg.to_filename(self._results['out_mask']) return runtime + +class _Save4Dto3DInputSpec(BaseInterfaceInputSpec): + in_file = File(exists=True, mandatory=True, desc='input 4d image') + + +class _Save4Dto3DOutputSpec(TraitedSpec): + out_files = OutputMultiPath(File(exists=True), + desc='output list of 3d images') + + +class Save4Dto3D(SimpleInterface): + """Split a 4D dataset along the last dimension + into multiple 3D volumes.""" + + input_spec = _Save4Dto3DInputSpec + output_spec = _Save4Dto3DOutputSpec + + def _run_interface(self, runtime): + filenii = nb.load(self.inputs.in_file) + if len(filenii.shape) != 4: + raise RuntimeError('Input image (%s) is not 4D.' % filenii) + + files_3d = nb.four_to_three(filenii) + out_files = [] + for i, file_3d in enumerate(files_3d): + out_file = fname_presuffix(in_file, suffix="_tmp_{}".format(i)) + file_3d.to_filename(out_file) + out_files.append(out_file) + + self._results['out_files'] = out_files + return runtime + + +class _Save3Dto4DInputSpec(BaseInterfaceInputSpec): + in_files = InputMultiPath(File(exists=True, mandatory=True, + desc='input list of 3d images')) + + +class _Save3Dto4DOutputSpec(TraitedSpec): + out_file = File(exists=True, desc='output list of 3d images') + + +class Save3Dto4D(SimpleInterface): + """Merge a list of 3D volumes along the last dimension into a single + 4D image.""" + + input_spec = _Save4Dto3DInputSpec + output_spec = _Save4Dto3DOutputSpec + + def _run_interface(self, runtime): + nii_list = [] + for i, f in enumerate(self.inputs.in_files): + filenii = nb.load(f) + filenii = nb.squeeze_image(filenii) + if len(filenii.shape) != 3: + raise RuntimeError('Input image (%s) is not 3D.' % f) + else: + nii_list.append(filenii) + img_4d = nb.funcs.concat_images(nii_list) + out_file = fname_presuffix(self.inputs.in_files[0], suffix="_merged") + img_4d.to_filename(out_file) + + self._results['out_file'] = out_file + return runtime From bfcd29c253539c11976b3beca3d2153c3f48fd8a Mon Sep 17 00:00:00 2001 From: Derek Pisner <16432683+dPys@users.noreply.github.com> Date: Tue, 24 Mar 2020 16:56:17 -0500 Subject: [PATCH 02/22] Update niworkflows/interfaces/nibabel.py Co-Authored-By: Chris Markiewicz --- niworkflows/interfaces/nibabel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index 72784dbdc71..f0bfc303c99 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -99,7 +99,7 @@ class _Save4Dto3DOutputSpec(TraitedSpec): desc='output list of 3d images') -class Save4Dto3D(SimpleInterface): +class FourToThree(SimpleInterface): """Split a 4D dataset along the last dimension into multiple 3D volumes.""" From 30ddf03b11be49aa0e47ed68c5986aedfac82083 Mon Sep 17 00:00:00 2001 From: Derek Pisner <16432683+dPys@users.noreply.github.com> Date: Tue, 24 Mar 2020 16:56:25 -0500 Subject: [PATCH 03/22] Update niworkflows/interfaces/nibabel.py Co-Authored-By: Chris Markiewicz --- niworkflows/interfaces/nibabel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index f0bfc303c99..38bf4eeef1e 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -140,7 +140,7 @@ class Save3Dto4D(SimpleInterface): def _run_interface(self, runtime): nii_list = [] - for i, f in enumerate(self.inputs.in_files): + for f in self.inputs.in_files: filenii = nb.load(f) filenii = nb.squeeze_image(filenii) if len(filenii.shape) != 3: From 5e3c93c00c497ce99795a59176e6da382ca8ed11 Mon Sep 17 00:00:00 2001 From: Derek Pisner <16432683+dPys@users.noreply.github.com> Date: Tue, 24 Mar 2020 16:56:40 -0500 Subject: [PATCH 04/22] Update niworkflows/interfaces/nibabel.py Co-Authored-By: Chris Markiewicz --- niworkflows/interfaces/nibabel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index 38bf4eeef1e..881a4f5ea31 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -7,7 +7,7 @@ from nipype.utils.filemanip import fname_presuffix from nipype.interfaces.base import ( traits, TraitedSpec, BaseInterfaceInputSpec, File, - SimpleInterface, OutputMultiPath, InputMultiPath + SimpleInterface, OutputMultiObject, InputMultiObject ) IFLOGGER = logging.getLogger('nipype.interface') From 40f6de1a1580d7e44ff33fe5b3cbeb640e42e847 Mon Sep 17 00:00:00 2001 From: Derek Pisner <16432683+dPys@users.noreply.github.com> Date: Tue, 24 Mar 2020 16:56:48 -0500 Subject: [PATCH 05/22] Update niworkflows/interfaces/nibabel.py Co-Authored-By: Chris Markiewicz --- niworkflows/interfaces/nibabel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index 881a4f5ea31..675b993b7db 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -90,6 +90,7 @@ def _run_interface(self, runtime): return runtime + class _Save4Dto3DInputSpec(BaseInterfaceInputSpec): in_file = File(exists=True, mandatory=True, desc='input 4d image') From f3572e66044c31952cfbf14922a5ab8c5a849902 Mon Sep 17 00:00:00 2001 From: dPys Date: Tue, 24 Mar 2020 17:04:04 -0500 Subject: [PATCH 06/22] Chage naming to ConcatImages, fix in/out spec namings --- niworkflows/interfaces/nibabel.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index 675b993b7db..7710663d9dc 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -91,11 +91,11 @@ def _run_interface(self, runtime): return runtime -class _Save4Dto3DInputSpec(BaseInterfaceInputSpec): +class _FourToThreeInputSpec(BaseInterfaceInputSpec): in_file = File(exists=True, mandatory=True, desc='input 4d image') -class _Save4Dto3DOutputSpec(TraitedSpec): +class _FourToThreeOutputSpec(TraitedSpec): out_files = OutputMultiPath(File(exists=True), desc='output list of 3d images') @@ -104,8 +104,8 @@ class FourToThree(SimpleInterface): """Split a 4D dataset along the last dimension into multiple 3D volumes.""" - input_spec = _Save4Dto3DInputSpec - output_spec = _Save4Dto3DOutputSpec + input_spec = _FourToThreeInputSpec + output_spec = _FourToThreeOutputSpec def _run_interface(self, runtime): filenii = nb.load(self.inputs.in_file) @@ -123,21 +123,21 @@ def _run_interface(self, runtime): return runtime -class _Save3Dto4DInputSpec(BaseInterfaceInputSpec): +class _ConcatImagesInputSpec(BaseInterfaceInputSpec): in_files = InputMultiPath(File(exists=True, mandatory=True, desc='input list of 3d images')) -class _Save3Dto4DOutputSpec(TraitedSpec): +class _ConcatImagesOutputSpec(TraitedSpec): out_file = File(exists=True, desc='output list of 3d images') -class Save3Dto4D(SimpleInterface): +class ConcatImages(SimpleInterface): """Merge a list of 3D volumes along the last dimension into a single 4D image.""" - input_spec = _Save4Dto3DInputSpec - output_spec = _Save4Dto3DOutputSpec + input_spec = _ConcatImagesInputSpec + output_spec = _ConcatImagesOutputSpec def _run_interface(self, runtime): nii_list = [] From 49ff6bd731534877b59bb31fb5c89cb7d42be05c Mon Sep 17 00:00:00 2001 From: Derek Pisner <16432683+dPys@users.noreply.github.com> Date: Tue, 31 Mar 2020 15:38:19 -0500 Subject: [PATCH 07/22] Update niworkflows/interfaces/nibabel.py Co-Authored-By: Chris Markiewicz --- niworkflows/interfaces/nibabel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index 7710663d9dc..6e826d775f6 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -96,7 +96,7 @@ class _FourToThreeInputSpec(BaseInterfaceInputSpec): class _FourToThreeOutputSpec(TraitedSpec): - out_files = OutputMultiPath(File(exists=True), + out_files = OutputMultiObject(File(exists=True), desc='output list of 3d images') From 87f1cb374b7f4bbd001f170390af16ddf654a2a0 Mon Sep 17 00:00:00 2001 From: Derek Pisner <16432683+dPys@users.noreply.github.com> Date: Tue, 31 Mar 2020 15:38:24 -0500 Subject: [PATCH 08/22] Update niworkflows/interfaces/nibabel.py Co-Authored-By: Chris Markiewicz --- niworkflows/interfaces/nibabel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index 6e826d775f6..4baed0ddf83 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -124,7 +124,7 @@ def _run_interface(self, runtime): class _ConcatImagesInputSpec(BaseInterfaceInputSpec): - in_files = InputMultiPath(File(exists=True, mandatory=True, + in_files = InputMultiObject(File(exists=True, mandatory=True, desc='input list of 3d images')) From b4fcdb71252d59293149b9ad790b6b963fe037d5 Mon Sep 17 00:00:00 2001 From: dPys Date: Tue, 31 Mar 2020 16:07:10 -0500 Subject: [PATCH 09/22] rename ConcatImages to MergeSeries, correct typo in description of outputs --- niworkflows/interfaces/nibabel.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index 4baed0ddf83..fe1ff3bfd37 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -102,7 +102,7 @@ class _FourToThreeOutputSpec(TraitedSpec): class FourToThree(SimpleInterface): """Split a 4D dataset along the last dimension - into multiple 3D volumes.""" + into a series of 3D volumes.""" input_spec = _FourToThreeInputSpec output_spec = _FourToThreeOutputSpec @@ -123,21 +123,21 @@ def _run_interface(self, runtime): return runtime -class _ConcatImagesInputSpec(BaseInterfaceInputSpec): +class _MergeSeriesInputSpec(BaseInterfaceInputSpec): in_files = InputMultiObject(File(exists=True, mandatory=True, desc='input list of 3d images')) -class _ConcatImagesOutputSpec(TraitedSpec): - out_file = File(exists=True, desc='output list of 3d images') +class _MergeSeriesOutputSpec(TraitedSpec): + out_file = File(exists=True, desc='output 4d image') -class ConcatImages(SimpleInterface): - """Merge a list of 3D volumes along the last dimension into a single +class MergeSeries(SimpleInterface): + """Merge a series of 3D volumes along the last dimension into a single 4D image.""" - input_spec = _ConcatImagesInputSpec - output_spec = _ConcatImagesOutputSpec + input_spec = _MergeSeriesInputSpec + output_spec = _MergeSeriesOutputSpec def _run_interface(self, runtime): nii_list = [] From f51f510a5ea6c7228866715fb171cf4bbb05a494 Mon Sep 17 00:00:00 2001 From: Derek Pisner <16432683+dPys@users.noreply.github.com> Date: Sat, 4 Apr 2020 23:19:43 -0500 Subject: [PATCH 10/22] Update niworkflows/interfaces/nibabel.py Co-Authored-By: Oscar Esteban --- niworkflows/interfaces/nibabel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index fe1ff3bfd37..90a6bafa602 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -115,7 +115,7 @@ def _run_interface(self, runtime): files_3d = nb.four_to_three(filenii) out_files = [] for i, file_3d in enumerate(files_3d): - out_file = fname_presuffix(in_file, suffix="_tmp_{}".format(i)) + out_file = fname_presuffix(in_file, suffix=f"_idx-{i:03}") file_3d.to_filename(out_file) out_files.append(out_file) From 9aa065584e3198b56f309d381768e5f91f877870 Mon Sep 17 00:00:00 2001 From: Derek Pisner <16432683+dPys@users.noreply.github.com> Date: Sat, 4 Apr 2020 23:19:53 -0500 Subject: [PATCH 11/22] Update niworkflows/interfaces/nibabel.py Co-Authored-By: Oscar Esteban --- niworkflows/interfaces/nibabel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index 90a6bafa602..0a076fa4bb3 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -113,7 +113,7 @@ def _run_interface(self, runtime): raise RuntimeError('Input image (%s) is not 4D.' % filenii) files_3d = nb.four_to_three(filenii) - out_files = [] + self._results['out_files'] = [] for i, file_3d in enumerate(files_3d): out_file = fname_presuffix(in_file, suffix=f"_idx-{i:03}") file_3d.to_filename(out_file) From 5a31b01dba9f32d8716cd10f574497be76370c1e Mon Sep 17 00:00:00 2001 From: Derek Pisner <16432683+dPys@users.noreply.github.com> Date: Sat, 4 Apr 2020 23:20:22 -0500 Subject: [PATCH 12/22] Update niworkflows/interfaces/nibabel.py Co-Authored-By: Oscar Esteban --- niworkflows/interfaces/nibabel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index 0a076fa4bb3..1c9faa0d722 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -125,7 +125,7 @@ def _run_interface(self, runtime): class _MergeSeriesInputSpec(BaseInterfaceInputSpec): in_files = InputMultiObject(File(exists=True, mandatory=True, - desc='input list of 3d images')) + desc='input list of 3d images')) class _MergeSeriesOutputSpec(TraitedSpec): From 53db81aba7f4e6ea31127beee5530eecb23e6fda Mon Sep 17 00:00:00 2001 From: Derek Pisner <16432683+dPys@users.noreply.github.com> Date: Sat, 4 Apr 2020 23:20:44 -0500 Subject: [PATCH 13/22] Update niworkflows/interfaces/nibabel.py Co-Authored-By: Oscar Esteban --- niworkflows/interfaces/nibabel.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index 1c9faa0d722..a2b22f61719 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -133,8 +133,7 @@ class _MergeSeriesOutputSpec(TraitedSpec): class MergeSeries(SimpleInterface): - """Merge a series of 3D volumes along the last dimension into a single - 4D image.""" + """Merge a series of 3D volumes along the last dimension into a single 4D image.""" input_spec = _MergeSeriesInputSpec output_spec = _MergeSeriesOutputSpec From 1c27f2048878074a19d28b5a79d21ea5f41206a2 Mon Sep 17 00:00:00 2001 From: Derek Pisner <16432683+dPys@users.noreply.github.com> Date: Sat, 4 Apr 2020 23:22:11 -0500 Subject: [PATCH 14/22] Update niworkflows/interfaces/nibabel.py Co-Authored-By: Oscar Esteban --- niworkflows/interfaces/nibabel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index a2b22f61719..707c4ffb36c 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -100,7 +100,7 @@ class _FourToThreeOutputSpec(TraitedSpec): desc='output list of 3d images') -class FourToThree(SimpleInterface): +class SplitSeries(SimpleInterface): """Split a 4D dataset along the last dimension into a series of 3D volumes.""" From 05724d57e1e062422f7e8cfb7b9f788c18cd6d25 Mon Sep 17 00:00:00 2001 From: Derek Pisner <16432683+dPys@users.noreply.github.com> Date: Sat, 4 Apr 2020 23:30:07 -0500 Subject: [PATCH 15/22] Update niworkflows/interfaces/nibabel.py Co-Authored-By: Oscar Esteban --- niworkflows/interfaces/nibabel.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index 707c4ffb36c..721f041ceb3 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -117,9 +117,8 @@ def _run_interface(self, runtime): for i, file_3d in enumerate(files_3d): out_file = fname_presuffix(in_file, suffix=f"_idx-{i:03}") file_3d.to_filename(out_file) - out_files.append(out_file) + self._results['out_files'].append(out_file) - self._results['out_files'] = out_files return runtime From 7263d03c546c721a086f7cdead9f56cdb8cac099 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Wed, 8 Apr 2020 12:27:47 -0700 Subject: [PATCH 16/22] Apply suggestions from code review [skip ci] --- niworkflows/interfaces/nibabel.py | 32 +++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index 721f041ceb3..fcf9c0c7fd1 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -93,6 +93,7 @@ def _run_interface(self, runtime): class _FourToThreeInputSpec(BaseInterfaceInputSpec): in_file = File(exists=True, mandatory=True, desc='input 4d image') + accept_3D = traits.Bool(False, usedefault=True, desc='do not fail if a 3D volume is passed in') class _FourToThreeOutputSpec(TraitedSpec): @@ -108,14 +109,25 @@ class SplitSeries(SimpleInterface): output_spec = _FourToThreeOutputSpec def _run_interface(self, runtime): - filenii = nb.load(self.inputs.in_file) - if len(filenii.shape) != 4: - raise RuntimeError('Input image (%s) is not 4D.' % filenii) + filenii = nb.squeeze_image(nb.load(self.inputs.in_file)) + ndim = filenii.dataobj.ndim + if ndim != 4: + if self.inputs.accept_3D and ndim == 3: + out_file = str( + Path(fname_presuffix(self.inputs.in_file, suffix=f"_idx-000")).absolute() + ) + self._results['out_files'] = out_file + filenii.to_filename(out_file) + return runtime + raise RuntimeError(f"Input image image is {ndim}D.") files_3d = nb.four_to_three(filenii) self._results['out_files'] = [] + in_file = self.inputs.in_file for i, file_3d in enumerate(files_3d): - out_file = fname_presuffix(in_file, suffix=f"_idx-{i:03}") + out_file = str( + Path(fname_presuffix(in_file, suffix=f"_idx-{i:03}")).absolute() + ) file_3d.to_filename(out_file) self._results['out_files'].append(out_file) @@ -125,6 +137,7 @@ def _run_interface(self, runtime): class _MergeSeriesInputSpec(BaseInterfaceInputSpec): in_files = InputMultiObject(File(exists=True, mandatory=True, desc='input list of 3d images')) + allow_4D = traits.Bool(True, usedefault=True, desc='whether 4D images are allowed to be concatenated') class _MergeSeriesOutputSpec(TraitedSpec): @@ -142,11 +155,14 @@ def _run_interface(self, runtime): for f in self.inputs.in_files: filenii = nb.load(f) filenii = nb.squeeze_image(filenii) - if len(filenii.shape) != 3: - raise RuntimeError('Input image (%s) is not 3D.' % f) - else: + if filenii.dataobj.ndim == 3: nii_list.append(filenii) - img_4d = nb.funcs.concat_images(nii_list) + elif self.inputs.allow_4D and filenii.dataobj.ndim == 4: + nii_list += nb.four_to_three(filenii) + else: + raise ValueError("Input image has an incorrect number of dimensions" + f" ({filenii.dataobj.ndim}).") + img_4d = nb.concat_images(nii_list) out_file = fname_presuffix(self.inputs.in_files[0], suffix="_merged") img_4d.to_filename(out_file) From 03ebb6d7064aae298da2e048ce292ab54ab7d3a9 Mon Sep 17 00:00:00 2001 From: oesteban Date: Wed, 8 Apr 2020 12:59:40 -0700 Subject: [PATCH 17/22] fix: added few bugfixes and regression tests --- niworkflows/interfaces/nibabel.py | 25 ++++--- niworkflows/interfaces/tests/test_nibabel.py | 78 +++++++++++++++++++- 2 files changed, 92 insertions(+), 11 deletions(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index fcf9c0c7fd1..206cae2e3fd 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -1,6 +1,7 @@ # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- # vi: set ft=python sts=4 ts=4 sw=4 et: """Nibabel-based interfaces.""" +from pathlib import Path import numpy as np import nibabel as nb from nipype import logging @@ -98,12 +99,11 @@ class _FourToThreeInputSpec(BaseInterfaceInputSpec): class _FourToThreeOutputSpec(TraitedSpec): out_files = OutputMultiObject(File(exists=True), - desc='output list of 3d images') + desc='output list of 3d images') class SplitSeries(SimpleInterface): - """Split a 4D dataset along the last dimension - into a series of 3D volumes.""" + """Split a 4D dataset along the last dimension into a series of 3D volumes.""" input_spec = _FourToThreeInputSpec output_spec = _FourToThreeOutputSpec @@ -119,7 +119,8 @@ def _run_interface(self, runtime): self._results['out_files'] = out_file filenii.to_filename(out_file) return runtime - raise RuntimeError(f"Input image image is {ndim}D.") + raise RuntimeError( + f"Input image image is {ndim}D ({'x'.join(['%d' % s for s in filenii.shape])}).") files_3d = nb.four_to_three(filenii) self._results['out_files'] = [] @@ -137,7 +138,8 @@ def _run_interface(self, runtime): class _MergeSeriesInputSpec(BaseInterfaceInputSpec): in_files = InputMultiObject(File(exists=True, mandatory=True, desc='input list of 3d images')) - allow_4D = traits.Bool(True, usedefault=True, desc='whether 4D images are allowed to be concatenated') + allow_4D = traits.Bool(True, usedefault=True, + desc='whether 4D images are allowed to be concatenated') class _MergeSeriesOutputSpec(TraitedSpec): @@ -153,15 +155,18 @@ class MergeSeries(SimpleInterface): def _run_interface(self, runtime): nii_list = [] for f in self.inputs.in_files: - filenii = nb.load(f) - filenii = nb.squeeze_image(filenii) - if filenii.dataobj.ndim == 3: + filenii = nb.squeeze_image(nb.load(f)) + ndim = filenii.dataobj.ndim + if ndim == 3: nii_list.append(filenii) - elif self.inputs.allow_4D and filenii.dataobj.ndim == 4: + continue + elif self.inputs.allow_4D and ndim == 4: nii_list += nb.four_to_three(filenii) + continue else: raise ValueError("Input image has an incorrect number of dimensions" - f" ({filenii.dataobj.ndim}).") + f" ({ndim}).") + img_4d = nb.concat_images(nii_list) out_file = fname_presuffix(self.inputs.in_files[0], suffix="_merged") img_4d.to_filename(out_file) diff --git a/niworkflows/interfaces/tests/test_nibabel.py b/niworkflows/interfaces/tests/test_nibabel.py index abdab406fcf..30aed36cff8 100644 --- a/niworkflows/interfaces/tests/test_nibabel.py +++ b/niworkflows/interfaces/tests/test_nibabel.py @@ -4,7 +4,7 @@ import nibabel as nb import pytest -from ..nibabel import Binarize, ApplyMask +from ..nibabel import Binarize, ApplyMask, SplitSeries, MergeSeries def test_Binarize(tmp_path): @@ -69,3 +69,79 @@ def test_ApplyMask(tmp_path): ApplyMask(in_file=str(in_file), in_mask=str(in_mask), threshold=0.4).run() with pytest.raises(ValueError): ApplyMask(in_file=str(in_file4d), in_mask=str(in_mask), threshold=0.4).run() + + +def test_SplitSeries(tmp_path): + """Test 4-to-3 NIfTI split interface.""" + os.chdir(str(tmp_path)) + + # Test the 4D + data = np.ones((20, 20, 20, 15), dtype=float) + in_file = tmp_path / 'input4D.nii.gz' + nb.Nifti1Image(data, np.eye(4), None).to_filename(str(in_file)) + + split = SplitSeries(in_file=str(in_file)).run() + assert len(split.outputs.out_files) == 15 + + # Test the 3D + data = np.ones((20, 20, 20), dtype=float) + in_file = tmp_path / 'input3D.nii.gz' + nb.Nifti1Image(data, np.eye(4), None).to_filename(str(in_file)) + + with pytest.raises(RuntimeError): + SplitSeries(in_file=str(in_file)).run() + + split = SplitSeries(in_file=str(in_file), accept_3D=True).run() + assert isinstance(split.outputs.out_files, str) + + # Test the 3D + data = np.ones((20, 20, 20, 1), dtype=float) + in_file = tmp_path / 'input3D.nii.gz' + nb.Nifti1Image(data, np.eye(4), None).to_filename(str(in_file)) + + with pytest.raises(RuntimeError): + SplitSeries(in_file=str(in_file)).run() + + split = SplitSeries(in_file=str(in_file), accept_3D=True).run() + assert isinstance(split.outputs.out_files, str) + + # Test the 5D + data = np.ones((20, 20, 20, 2, 2), dtype=float) + in_file = tmp_path / 'input5D.nii.gz' + nb.Nifti1Image(data, np.eye(4), None).to_filename(str(in_file)) + + with pytest.raises(RuntimeError): + SplitSeries(in_file=str(in_file)).run() + + with pytest.raises(RuntimeError): + SplitSeries(in_file=str(in_file), accept_3D=True).run() + + # Test splitting ANTs warpfields + data = np.ones((20, 20, 20, 1, 3), dtype=float) + in_file = tmp_path / 'warpfield.nii.gz' + nb.Nifti1Image(data, np.eye(4), None).to_filename(str(in_file)) + + split = SplitSeries(in_file=str(in_file)).run() + assert len(split.outputs.out_files) == 3 + +def test_MergeSeries(tmp_path): + """Test 3-to-4 NIfTI concatenation interface.""" + os.chdir(str(tmp_path)) + + data = np.ones((20, 20, 20), dtype=float) + in_file = tmp_path / 'input3D.nii.gz' + nb.Nifti1Image(data, np.eye(4), None).to_filename(str(in_file)) + + merge = MergeSeries(in_files=[str(in_file)] * 5).run() + assert nb.load(merge.outputs.out_file).dataobj.shape == (20, 20, 20, 5) + + in_4D = tmp_path / 'input4D.nii.gz' + nb.Nifti1Image( + np.ones((20, 20, 20, 4), dtype=float), np.eye(4), None + ).to_filename(str(in_4D)) + + merge = MergeSeries(in_files=[str(in_file)] + [str(in_4D)]).run() + assert nb.load(merge.outputs.out_file).dataobj.shape == (20, 20, 20, 5) + + with pytest.raises(ValueError): + MergeSeries(in_files=[str(in_file)] + [str(in_4D)], allow_4D=False).run() From 9446d47ffecb69d51e65316447bbd7453afab359 Mon Sep 17 00:00:00 2001 From: oesteban Date: Wed, 8 Apr 2020 15:05:10 -0700 Subject: [PATCH 18/22] fix: squeeze image with np.squeeze / change input name for consistency --- niworkflows/interfaces/nibabel.py | 11 ++++++++--- niworkflows/interfaces/tests/test_nibabel.py | 7 ++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index 206cae2e3fd..8da8f428bd3 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -94,7 +94,7 @@ def _run_interface(self, runtime): class _FourToThreeInputSpec(BaseInterfaceInputSpec): in_file = File(exists=True, mandatory=True, desc='input 4d image') - accept_3D = traits.Bool(False, usedefault=True, desc='do not fail if a 3D volume is passed in') + allow_3D = traits.Bool(False, usedefault=True, desc='do not fail if a 3D volume is passed in') class _FourToThreeOutputSpec(TraitedSpec): @@ -110,9 +110,12 @@ class SplitSeries(SimpleInterface): def _run_interface(self, runtime): filenii = nb.squeeze_image(nb.load(self.inputs.in_file)) + filenii = filenii.__class__( + np.squeeze(filenii.dataobj), filenii.affine, filenii.header + ) ndim = filenii.dataobj.ndim if ndim != 4: - if self.inputs.accept_3D and ndim == 3: + if self.inputs.allow_3D and ndim == 3: out_file = str( Path(fname_presuffix(self.inputs.in_file, suffix=f"_idx-000")).absolute() ) @@ -120,7 +123,9 @@ def _run_interface(self, runtime): filenii.to_filename(out_file) return runtime raise RuntimeError( - f"Input image image is {ndim}D ({'x'.join(['%d' % s for s in filenii.shape])}).") + f"Input image <{self.inputs.in_file}> is {ndim}D " + f"({'x'.join(['%d' % s for s in filenii.shape])})." + ) files_3d = nb.four_to_three(filenii) self._results['out_files'] = [] diff --git a/niworkflows/interfaces/tests/test_nibabel.py b/niworkflows/interfaces/tests/test_nibabel.py index 30aed36cff8..f137ea6192d 100644 --- a/niworkflows/interfaces/tests/test_nibabel.py +++ b/niworkflows/interfaces/tests/test_nibabel.py @@ -91,7 +91,7 @@ def test_SplitSeries(tmp_path): with pytest.raises(RuntimeError): SplitSeries(in_file=str(in_file)).run() - split = SplitSeries(in_file=str(in_file), accept_3D=True).run() + split = SplitSeries(in_file=str(in_file), allow_3D=True).run() assert isinstance(split.outputs.out_files, str) # Test the 3D @@ -102,7 +102,7 @@ def test_SplitSeries(tmp_path): with pytest.raises(RuntimeError): SplitSeries(in_file=str(in_file)).run() - split = SplitSeries(in_file=str(in_file), accept_3D=True).run() + split = SplitSeries(in_file=str(in_file), allow_3D=True).run() assert isinstance(split.outputs.out_files, str) # Test the 5D @@ -114,7 +114,7 @@ def test_SplitSeries(tmp_path): SplitSeries(in_file=str(in_file)).run() with pytest.raises(RuntimeError): - SplitSeries(in_file=str(in_file), accept_3D=True).run() + SplitSeries(in_file=str(in_file), allow_3D=True).run() # Test splitting ANTs warpfields data = np.ones((20, 20, 20, 1, 3), dtype=float) @@ -124,6 +124,7 @@ def test_SplitSeries(tmp_path): split = SplitSeries(in_file=str(in_file)).run() assert len(split.outputs.out_files) == 3 + def test_MergeSeries(tmp_path): """Test 3-to-4 NIfTI concatenation interface.""" os.chdir(str(tmp_path)) From 0ae53513f4e5dc9913d63fdb3b53eae2046cf32f Mon Sep 17 00:00:00 2001 From: oesteban Date: Wed, 8 Apr 2020 15:08:23 -0700 Subject: [PATCH 19/22] sty(black): standardize formatting a bit --- niworkflows/interfaces/nibabel.py | 95 +++++++++++--------- niworkflows/interfaces/tests/test_nibabel.py | 66 ++++++++------ 2 files changed, 91 insertions(+), 70 deletions(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index 8da8f428bd3..59bf14c12e0 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -7,22 +7,28 @@ from nipype import logging from nipype.utils.filemanip import fname_presuffix from nipype.interfaces.base import ( - traits, TraitedSpec, BaseInterfaceInputSpec, File, - SimpleInterface, OutputMultiObject, InputMultiObject + traits, + TraitedSpec, + BaseInterfaceInputSpec, + File, + SimpleInterface, + OutputMultiObject, + InputMultiObject, ) -IFLOGGER = logging.getLogger('nipype.interface') +IFLOGGER = logging.getLogger("nipype.interface") class _ApplyMaskInputSpec(BaseInterfaceInputSpec): - in_file = File(exists=True, mandatory=True, desc='an image') - in_mask = File(exists=True, mandatory=True, desc='a mask') - threshold = traits.Float(0.5, usedefault=True, - desc='a threshold to the mask, if it is nonbinary') + in_file = File(exists=True, mandatory=True, desc="an image") + in_mask = File(exists=True, mandatory=True, desc="a mask") + threshold = traits.Float( + 0.5, usedefault=True, desc="a threshold to the mask, if it is nonbinary" + ) class _ApplyMaskOutputSpec(TraitedSpec): - out_file = File(exists=True, desc='masked file') + out_file = File(exists=True, desc="masked file") class ApplyMask(SimpleInterface): @@ -36,8 +42,9 @@ def _run_interface(self, runtime): msknii = nb.load(self.inputs.in_mask) msk = msknii.get_fdata() > self.inputs.threshold - self._results['out_file'] = fname_presuffix( - self.inputs.in_file, suffix='_masked', newpath=runtime.cwd) + self._results["out_file"] = fname_presuffix( + self.inputs.in_file, suffix="_masked", newpath=runtime.cwd + ) if img.dataobj.shape[:3] != msk.shape: raise ValueError("Image and mask sizes do not match.") @@ -49,19 +56,18 @@ def _run_interface(self, runtime): msk = msk[..., np.newaxis] masked = img.__class__(img.dataobj * msk, None, img.header) - masked.to_filename(self._results['out_file']) + masked.to_filename(self._results["out_file"]) return runtime class _BinarizeInputSpec(BaseInterfaceInputSpec): - in_file = File(exists=True, mandatory=True, desc='input image') - thresh_low = traits.Float(mandatory=True, - desc='non-inclusive lower threshold') + in_file = File(exists=True, mandatory=True, desc="input image") + thresh_low = traits.Float(mandatory=True, desc="non-inclusive lower threshold") class _BinarizeOutputSpec(TraitedSpec): - out_file = File(exists=True, desc='masked file') - out_mask = File(exists=True, desc='output mask') + out_file = File(exists=True, desc="masked file") + out_mask = File(exists=True, desc="output mask") class Binarize(SimpleInterface): @@ -73,33 +79,35 @@ class Binarize(SimpleInterface): def _run_interface(self, runtime): img = nb.load(self.inputs.in_file) - self._results['out_file'] = fname_presuffix( - self.inputs.in_file, suffix='_masked', newpath=runtime.cwd) - self._results['out_mask'] = fname_presuffix( - self.inputs.in_file, suffix='_mask', newpath=runtime.cwd) + self._results["out_file"] = fname_presuffix( + self.inputs.in_file, suffix="_masked", newpath=runtime.cwd + ) + self._results["out_mask"] = fname_presuffix( + self.inputs.in_file, suffix="_mask", newpath=runtime.cwd + ) data = img.get_fdata() mask = data > self.inputs.thresh_low data[~mask] = 0.0 masked = img.__class__(data, img.affine, img.header) - masked.to_filename(self._results['out_file']) + masked.to_filename(self._results["out_file"]) - img.header.set_data_dtype('uint8') - maskimg = img.__class__(mask.astype('uint8'), img.affine, - img.header) - maskimg.to_filename(self._results['out_mask']) + img.header.set_data_dtype("uint8") + maskimg = img.__class__(mask.astype("uint8"), img.affine, img.header) + maskimg.to_filename(self._results["out_mask"]) return runtime class _FourToThreeInputSpec(BaseInterfaceInputSpec): - in_file = File(exists=True, mandatory=True, desc='input 4d image') - allow_3D = traits.Bool(False, usedefault=True, desc='do not fail if a 3D volume is passed in') + in_file = File(exists=True, mandatory=True, desc="input 4d image") + allow_3D = traits.Bool( + False, usedefault=True, desc="do not fail if a 3D volume is passed in" + ) class _FourToThreeOutputSpec(TraitedSpec): - out_files = OutputMultiObject(File(exists=True), - desc='output list of 3d images') + out_files = OutputMultiObject(File(exists=True), desc="output list of 3d images") class SplitSeries(SimpleInterface): @@ -117,9 +125,11 @@ def _run_interface(self, runtime): if ndim != 4: if self.inputs.allow_3D and ndim == 3: out_file = str( - Path(fname_presuffix(self.inputs.in_file, suffix=f"_idx-000")).absolute() + Path( + fname_presuffix(self.inputs.in_file, suffix=f"_idx-000") + ).absolute() ) - self._results['out_files'] = out_file + self._results["out_files"] = out_file filenii.to_filename(out_file) return runtime raise RuntimeError( @@ -128,27 +138,29 @@ def _run_interface(self, runtime): ) files_3d = nb.four_to_three(filenii) - self._results['out_files'] = [] + self._results["out_files"] = [] in_file = self.inputs.in_file for i, file_3d in enumerate(files_3d): out_file = str( Path(fname_presuffix(in_file, suffix=f"_idx-{i:03}")).absolute() ) file_3d.to_filename(out_file) - self._results['out_files'].append(out_file) + self._results["out_files"].append(out_file) return runtime class _MergeSeriesInputSpec(BaseInterfaceInputSpec): - in_files = InputMultiObject(File(exists=True, mandatory=True, - desc='input list of 3d images')) - allow_4D = traits.Bool(True, usedefault=True, - desc='whether 4D images are allowed to be concatenated') + in_files = InputMultiObject( + File(exists=True, mandatory=True, desc="input list of 3d images") + ) + allow_4D = traits.Bool( + True, usedefault=True, desc="whether 4D images are allowed to be concatenated" + ) class _MergeSeriesOutputSpec(TraitedSpec): - out_file = File(exists=True, desc='output 4d image') + out_file = File(exists=True, desc="output 4d image") class MergeSeries(SimpleInterface): @@ -169,12 +181,13 @@ def _run_interface(self, runtime): nii_list += nb.four_to_three(filenii) continue else: - raise ValueError("Input image has an incorrect number of dimensions" - f" ({ndim}).") + raise ValueError( + "Input image has an incorrect number of dimensions" f" ({ndim})." + ) img_4d = nb.concat_images(nii_list) out_file = fname_presuffix(self.inputs.in_files[0], suffix="_merged") img_4d.to_filename(out_file) - self._results['out_file'] = out_file + self._results["out_file"] = out_file return runtime diff --git a/niworkflows/interfaces/tests/test_nibabel.py b/niworkflows/interfaces/tests/test_nibabel.py index f137ea6192d..adc049a0ad3 100644 --- a/niworkflows/interfaces/tests/test_nibabel.py +++ b/niworkflows/interfaces/tests/test_nibabel.py @@ -14,10 +14,10 @@ def test_Binarize(tmp_path): mask = np.zeros((20, 20, 20), dtype=bool) mask[5:15, 5:15, 5:15] = bool - data = np.zeros_like(mask, dtype='float32') + data = np.zeros_like(mask, dtype="float32") data[mask] = np.random.gamma(2, size=mask.sum()) - in_file = tmp_path / 'input.nii.gz' + in_file = tmp_path / "input.nii.gz" nb.Nifti1Image(data, np.eye(4), None).to_filename(str(in_file)) binif = Binarize(thresh_low=0.0, in_file=str(in_file)).run() @@ -36,28 +36,32 @@ def test_ApplyMask(tmp_path): mask[8:11, 8:11, 8:11] = 1.0 # Test the 3D - in_file = tmp_path / 'input3D.nii.gz' + in_file = tmp_path / "input3D.nii.gz" nb.Nifti1Image(data, np.eye(4), None).to_filename(str(in_file)) - in_mask = tmp_path / 'mask.nii.gz' + in_mask = tmp_path / "mask.nii.gz" nb.Nifti1Image(mask, np.eye(4), None).to_filename(str(in_mask)) masked1 = ApplyMask(in_file=str(in_file), in_mask=str(in_mask), threshold=0.4).run() - assert nb.load(masked1.outputs.out_file).get_fdata().sum() == 5**3 + assert nb.load(masked1.outputs.out_file).get_fdata().sum() == 5 ** 3 masked1 = ApplyMask(in_file=str(in_file), in_mask=str(in_mask), threshold=0.6).run() - assert nb.load(masked1.outputs.out_file).get_fdata().sum() == 3**3 + assert nb.load(masked1.outputs.out_file).get_fdata().sum() == 3 ** 3 data4d = np.stack((data, 2 * data, 3 * data), axis=-1) # Test the 4D case - in_file4d = tmp_path / 'input4D.nii.gz' + in_file4d = tmp_path / "input4D.nii.gz" nb.Nifti1Image(data4d, np.eye(4), None).to_filename(str(in_file4d)) - masked1 = ApplyMask(in_file=str(in_file4d), in_mask=str(in_mask), threshold=0.4).run() - assert nb.load(masked1.outputs.out_file).get_fdata().sum() == 5**3 * 6 + masked1 = ApplyMask( + in_file=str(in_file4d), in_mask=str(in_mask), threshold=0.4 + ).run() + assert nb.load(masked1.outputs.out_file).get_fdata().sum() == 5 ** 3 * 6 - masked1 = ApplyMask(in_file=str(in_file4d), in_mask=str(in_mask), threshold=0.6).run() - assert nb.load(masked1.outputs.out_file).get_fdata().sum() == 3**3 * 6 + masked1 = ApplyMask( + in_file=str(in_file4d), in_mask=str(in_mask), threshold=0.6 + ).run() + assert nb.load(masked1.outputs.out_file).get_fdata().sum() == 3 ** 3 * 6 # Test errors nb.Nifti1Image(mask, 2 * np.eye(4), None).to_filename(str(in_mask)) @@ -77,16 +81,17 @@ def test_SplitSeries(tmp_path): # Test the 4D data = np.ones((20, 20, 20, 15), dtype=float) - in_file = tmp_path / 'input4D.nii.gz' + in_file = tmp_path / "input4D.nii.gz" nb.Nifti1Image(data, np.eye(4), None).to_filename(str(in_file)) split = SplitSeries(in_file=str(in_file)).run() assert len(split.outputs.out_files) == 15 # Test the 3D - data = np.ones((20, 20, 20), dtype=float) - in_file = tmp_path / 'input3D.nii.gz' - nb.Nifti1Image(data, np.eye(4), None).to_filename(str(in_file)) + in_file = tmp_path / "input3D.nii.gz" + nb.Nifti1Image(np.ones((20, 20, 20), dtype=float), np.eye(4), None).to_filename( + str(in_file) + ) with pytest.raises(RuntimeError): SplitSeries(in_file=str(in_file)).run() @@ -95,9 +100,10 @@ def test_SplitSeries(tmp_path): assert isinstance(split.outputs.out_files, str) # Test the 3D - data = np.ones((20, 20, 20, 1), dtype=float) - in_file = tmp_path / 'input3D.nii.gz' - nb.Nifti1Image(data, np.eye(4), None).to_filename(str(in_file)) + in_file = tmp_path / "input3D.nii.gz" + nb.Nifti1Image(np.ones((20, 20, 20, 1), dtype=float), np.eye(4), None).to_filename( + str(in_file) + ) with pytest.raises(RuntimeError): SplitSeries(in_file=str(in_file)).run() @@ -106,9 +112,10 @@ def test_SplitSeries(tmp_path): assert isinstance(split.outputs.out_files, str) # Test the 5D - data = np.ones((20, 20, 20, 2, 2), dtype=float) - in_file = tmp_path / 'input5D.nii.gz' - nb.Nifti1Image(data, np.eye(4), None).to_filename(str(in_file)) + in_file = tmp_path / "input5D.nii.gz" + nb.Nifti1Image( + np.ones((20, 20, 20, 2, 2), dtype=float), np.eye(4), None + ).to_filename(str(in_file)) with pytest.raises(RuntimeError): SplitSeries(in_file=str(in_file)).run() @@ -118,7 +125,7 @@ def test_SplitSeries(tmp_path): # Test splitting ANTs warpfields data = np.ones((20, 20, 20, 1, 3), dtype=float) - in_file = tmp_path / 'warpfield.nii.gz' + in_file = tmp_path / "warpfield.nii.gz" nb.Nifti1Image(data, np.eye(4), None).to_filename(str(in_file)) split = SplitSeries(in_file=str(in_file)).run() @@ -129,17 +136,18 @@ def test_MergeSeries(tmp_path): """Test 3-to-4 NIfTI concatenation interface.""" os.chdir(str(tmp_path)) - data = np.ones((20, 20, 20), dtype=float) - in_file = tmp_path / 'input3D.nii.gz' - nb.Nifti1Image(data, np.eye(4), None).to_filename(str(in_file)) + in_file = tmp_path / "input3D.nii.gz" + nb.Nifti1Image(np.ones((20, 20, 20), dtype=float), np.eye(4), None).to_filename( + str(in_file) + ) merge = MergeSeries(in_files=[str(in_file)] * 5).run() assert nb.load(merge.outputs.out_file).dataobj.shape == (20, 20, 20, 5) - in_4D = tmp_path / 'input4D.nii.gz' - nb.Nifti1Image( - np.ones((20, 20, 20, 4), dtype=float), np.eye(4), None - ).to_filename(str(in_4D)) + in_4D = tmp_path / "input4D.nii.gz" + nb.Nifti1Image(np.ones((20, 20, 20, 4), dtype=float), np.eye(4), None).to_filename( + str(in_4D) + ) merge = MergeSeries(in_files=[str(in_file)] + [str(in_4D)]).run() assert nb.load(merge.outputs.out_file).dataobj.shape == (20, 20, 20, 5) From fc4235159bcbe2ad422cb4c3967bc7aaa51b820a Mon Sep 17 00:00:00 2001 From: oesteban Date: Wed, 8 Apr 2020 16:11:23 -0700 Subject: [PATCH 20/22] enh: make i/o specs of SplitSeries more consistent [skip ci] --- niworkflows/interfaces/nibabel.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index 59bf14c12e0..f7361b7ccf0 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -99,22 +99,22 @@ def _run_interface(self, runtime): return runtime -class _FourToThreeInputSpec(BaseInterfaceInputSpec): +class _SplitSeriesInputSpec(BaseInterfaceInputSpec): in_file = File(exists=True, mandatory=True, desc="input 4d image") allow_3D = traits.Bool( False, usedefault=True, desc="do not fail if a 3D volume is passed in" ) -class _FourToThreeOutputSpec(TraitedSpec): +class _SplitSeriesOutputSpec(TraitedSpec): out_files = OutputMultiObject(File(exists=True), desc="output list of 3d images") class SplitSeries(SimpleInterface): """Split a 4D dataset along the last dimension into a series of 3D volumes.""" - input_spec = _FourToThreeInputSpec - output_spec = _FourToThreeOutputSpec + input_spec = _SplitSeriesInputSpec + output_spec = _SplitSeriesOutputSpec def _run_interface(self, runtime): filenii = nb.squeeze_image(nb.load(self.inputs.in_file)) From 1d12dd36142e246bd8125acf06fa8840ed3c5910 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Wed, 8 Apr 2020 20:51:23 -0700 Subject: [PATCH 21/22] Update niworkflows/interfaces/tests/test_nibabel.py [skip ci] Co-Authored-By: Chris Markiewicz --- niworkflows/interfaces/tests/test_nibabel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/niworkflows/interfaces/tests/test_nibabel.py b/niworkflows/interfaces/tests/test_nibabel.py index adc049a0ad3..47a63a5df37 100644 --- a/niworkflows/interfaces/tests/test_nibabel.py +++ b/niworkflows/interfaces/tests/test_nibabel.py @@ -77,7 +77,7 @@ def test_ApplyMask(tmp_path): def test_SplitSeries(tmp_path): """Test 4-to-3 NIfTI split interface.""" - os.chdir(str(tmp_path)) + os.chdir(tmp_path) # Test the 4D data = np.ones((20, 20, 20, 15), dtype=float) From d65754651fbfb702b0add0bf081ce04574a68955 Mon Sep 17 00:00:00 2001 From: oesteban Date: Fri, 10 Apr 2020 15:46:21 -0700 Subject: [PATCH 22/22] fix: apply review comments from @effigies, add parameterized tests --- niworkflows/interfaces/nibabel.py | 35 +++------ niworkflows/interfaces/tests/test_nibabel.py | 78 +++++++------------- 2 files changed, 35 insertions(+), 78 deletions(-) diff --git a/niworkflows/interfaces/nibabel.py b/niworkflows/interfaces/nibabel.py index f7361b7ccf0..9c660d1d4c5 100644 --- a/niworkflows/interfaces/nibabel.py +++ b/niworkflows/interfaces/nibabel.py @@ -101,9 +101,6 @@ def _run_interface(self, runtime): class _SplitSeriesInputSpec(BaseInterfaceInputSpec): in_file = File(exists=True, mandatory=True, desc="input 4d image") - allow_3D = traits.Bool( - False, usedefault=True, desc="do not fail if a 3D volume is passed in" - ) class _SplitSeriesOutputSpec(TraitedSpec): @@ -117,34 +114,20 @@ class SplitSeries(SimpleInterface): output_spec = _SplitSeriesOutputSpec def _run_interface(self, runtime): - filenii = nb.squeeze_image(nb.load(self.inputs.in_file)) - filenii = filenii.__class__( - np.squeeze(filenii.dataobj), filenii.affine, filenii.header - ) - ndim = filenii.dataobj.ndim - if ndim != 4: - if self.inputs.allow_3D and ndim == 3: - out_file = str( - Path( - fname_presuffix(self.inputs.in_file, suffix=f"_idx-000") - ).absolute() - ) - self._results["out_files"] = out_file - filenii.to_filename(out_file) - return runtime - raise RuntimeError( - f"Input image <{self.inputs.in_file}> is {ndim}D " - f"({'x'.join(['%d' % s for s in filenii.shape])})." - ) + in_file = self.inputs.in_file + img = nb.load(in_file) + extra_dims = tuple(dim for dim in img.shape[3:] if dim > 1) or (1,) + if len(extra_dims) != 1: + raise ValueError(f"Invalid shape {'x'.join(str(s) for s in img.shape)}") + img = img.__class__(img.dataobj.reshape(img.shape[:3] + extra_dims), + img.affine, img.header) - files_3d = nb.four_to_three(filenii) self._results["out_files"] = [] - in_file = self.inputs.in_file - for i, file_3d in enumerate(files_3d): + for i, img_3d in enumerate(nb.four_to_three(img)): out_file = str( Path(fname_presuffix(in_file, suffix=f"_idx-{i:03}")).absolute() ) - file_3d.to_filename(out_file) + img_3d.to_filename(out_file) self._results["out_files"].append(out_file) return runtime diff --git a/niworkflows/interfaces/tests/test_nibabel.py b/niworkflows/interfaces/tests/test_nibabel.py index 47a63a5df37..3ab8c4ffe47 100644 --- a/niworkflows/interfaces/tests/test_nibabel.py +++ b/niworkflows/interfaces/tests/test_nibabel.py @@ -75,61 +75,35 @@ def test_ApplyMask(tmp_path): ApplyMask(in_file=str(in_file4d), in_mask=str(in_mask), threshold=0.4).run() -def test_SplitSeries(tmp_path): +@pytest.mark.parametrize("shape,exp_n", [ + ((20, 20, 20, 15), 15), + ((20, 20, 20), 1), + ((20, 20, 20, 1), 1), + ((20, 20, 20, 1, 3), 3), + ((20, 20, 20, 3, 1), 3), + ((20, 20, 20, 1, 3, 3), -1), + ((20, 1, 20, 15), 15), + ((20, 1, 20), 1), + ((20, 1, 20, 1), 1), + ((20, 1, 20, 1, 3), 3), + ((20, 1, 20, 3, 1), 3), + ((20, 1, 20, 1, 3, 3), -1), +]) +def test_SplitSeries(tmp_path, shape, exp_n): """Test 4-to-3 NIfTI split interface.""" os.chdir(tmp_path) - # Test the 4D - data = np.ones((20, 20, 20, 15), dtype=float) - in_file = tmp_path / "input4D.nii.gz" - nb.Nifti1Image(data, np.eye(4), None).to_filename(str(in_file)) - - split = SplitSeries(in_file=str(in_file)).run() - assert len(split.outputs.out_files) == 15 - - # Test the 3D - in_file = tmp_path / "input3D.nii.gz" - nb.Nifti1Image(np.ones((20, 20, 20), dtype=float), np.eye(4), None).to_filename( - str(in_file) - ) - - with pytest.raises(RuntimeError): - SplitSeries(in_file=str(in_file)).run() - - split = SplitSeries(in_file=str(in_file), allow_3D=True).run() - assert isinstance(split.outputs.out_files, str) - - # Test the 3D - in_file = tmp_path / "input3D.nii.gz" - nb.Nifti1Image(np.ones((20, 20, 20, 1), dtype=float), np.eye(4), None).to_filename( - str(in_file) - ) - - with pytest.raises(RuntimeError): - SplitSeries(in_file=str(in_file)).run() - - split = SplitSeries(in_file=str(in_file), allow_3D=True).run() - assert isinstance(split.outputs.out_files, str) - - # Test the 5D - in_file = tmp_path / "input5D.nii.gz" - nb.Nifti1Image( - np.ones((20, 20, 20, 2, 2), dtype=float), np.eye(4), None - ).to_filename(str(in_file)) - - with pytest.raises(RuntimeError): - SplitSeries(in_file=str(in_file)).run() - - with pytest.raises(RuntimeError): - SplitSeries(in_file=str(in_file), allow_3D=True).run() - - # Test splitting ANTs warpfields - data = np.ones((20, 20, 20, 1, 3), dtype=float) - in_file = tmp_path / "warpfield.nii.gz" - nb.Nifti1Image(data, np.eye(4), None).to_filename(str(in_file)) - - split = SplitSeries(in_file=str(in_file)).run() - assert len(split.outputs.out_files) == 3 + in_file = str(tmp_path / "input.nii.gz") + nb.Nifti1Image(np.ones(shape, dtype=float), np.eye(4), None).to_filename(in_file) + + _interface = SplitSeries(in_file=in_file) + if exp_n > 0: + split = _interface.run() + n = int(isinstance(split.outputs.out_files, str)) or len(split.outputs.out_files) + assert n == exp_n + else: + with pytest.raises(ValueError): + _interface.run() def test_MergeSeries(tmp_path):