1
1
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
2
2
# vi: set ft=python sts=4 ts=4 sw=4 et:
3
3
"""Nibabel-based interfaces."""
4
+ from pathlib import Path
4
5
import numpy as np
5
6
import nibabel as nb
6
7
from nipype import logging
7
8
from nipype .utils .filemanip import fname_presuffix
8
9
from nipype .interfaces .base import (
9
- traits , TraitedSpec , BaseInterfaceInputSpec , File ,
10
- SimpleInterface
10
+ traits ,
11
+ TraitedSpec ,
12
+ BaseInterfaceInputSpec ,
13
+ File ,
14
+ SimpleInterface ,
15
+ OutputMultiObject ,
16
+ InputMultiObject ,
11
17
)
12
18
13
- IFLOGGER = logging .getLogger (' nipype.interface' )
19
+ IFLOGGER = logging .getLogger (" nipype.interface" )
14
20
15
21
16
22
class _ApplyMaskInputSpec (BaseInterfaceInputSpec ):
17
- in_file = File (exists = True , mandatory = True , desc = 'an image' )
18
- in_mask = File (exists = True , mandatory = True , desc = 'a mask' )
19
- threshold = traits .Float (0.5 , usedefault = True ,
20
- desc = 'a threshold to the mask, if it is nonbinary' )
23
+ in_file = File (exists = True , mandatory = True , desc = "an image" )
24
+ in_mask = File (exists = True , mandatory = True , desc = "a mask" )
25
+ threshold = traits .Float (
26
+ 0.5 , usedefault = True , desc = "a threshold to the mask, if it is nonbinary"
27
+ )
21
28
22
29
23
30
class _ApplyMaskOutputSpec (TraitedSpec ):
24
- out_file = File (exists = True , desc = ' masked file' )
31
+ out_file = File (exists = True , desc = " masked file" )
25
32
26
33
27
34
class ApplyMask (SimpleInterface ):
@@ -35,8 +42,9 @@ def _run_interface(self, runtime):
35
42
msknii = nb .load (self .inputs .in_mask )
36
43
msk = msknii .get_fdata () > self .inputs .threshold
37
44
38
- self ._results ['out_file' ] = fname_presuffix (
39
- self .inputs .in_file , suffix = '_masked' , newpath = runtime .cwd )
45
+ self ._results ["out_file" ] = fname_presuffix (
46
+ self .inputs .in_file , suffix = "_masked" , newpath = runtime .cwd
47
+ )
40
48
41
49
if img .dataobj .shape [:3 ] != msk .shape :
42
50
raise ValueError ("Image and mask sizes do not match." )
@@ -48,19 +56,18 @@ def _run_interface(self, runtime):
48
56
msk = msk [..., np .newaxis ]
49
57
50
58
masked = img .__class__ (img .dataobj * msk , None , img .header )
51
- masked .to_filename (self ._results [' out_file' ])
59
+ masked .to_filename (self ._results [" out_file" ])
52
60
return runtime
53
61
54
62
55
63
class _BinarizeInputSpec (BaseInterfaceInputSpec ):
56
- in_file = File (exists = True , mandatory = True , desc = 'input image' )
57
- thresh_low = traits .Float (mandatory = True ,
58
- desc = 'non-inclusive lower threshold' )
64
+ in_file = File (exists = True , mandatory = True , desc = "input image" )
65
+ thresh_low = traits .Float (mandatory = True , desc = "non-inclusive lower threshold" )
59
66
60
67
61
68
class _BinarizeOutputSpec (TraitedSpec ):
62
- out_file = File (exists = True , desc = ' masked file' )
63
- out_mask = File (exists = True , desc = ' output mask' )
69
+ out_file = File (exists = True , desc = " masked file" )
70
+ out_mask = File (exists = True , desc = " output mask" )
64
71
65
72
66
73
class Binarize (SimpleInterface ):
@@ -72,20 +79,98 @@ class Binarize(SimpleInterface):
72
79
def _run_interface (self , runtime ):
73
80
img = nb .load (self .inputs .in_file )
74
81
75
- self ._results ['out_file' ] = fname_presuffix (
76
- self .inputs .in_file , suffix = '_masked' , newpath = runtime .cwd )
77
- self ._results ['out_mask' ] = fname_presuffix (
78
- self .inputs .in_file , suffix = '_mask' , newpath = runtime .cwd )
82
+ self ._results ["out_file" ] = fname_presuffix (
83
+ self .inputs .in_file , suffix = "_masked" , newpath = runtime .cwd
84
+ )
85
+ self ._results ["out_mask" ] = fname_presuffix (
86
+ self .inputs .in_file , suffix = "_mask" , newpath = runtime .cwd
87
+ )
79
88
80
89
data = img .get_fdata ()
81
90
mask = data > self .inputs .thresh_low
82
91
data [~ mask ] = 0.0
83
92
masked = img .__class__ (data , img .affine , img .header )
84
- masked .to_filename (self ._results [' out_file' ])
93
+ masked .to_filename (self ._results [" out_file" ])
85
94
86
- img .header .set_data_dtype ('uint8' )
87
- maskimg = img .__class__ (mask .astype ('uint8' ), img .affine ,
88
- img .header )
89
- maskimg .to_filename (self ._results ['out_mask' ])
95
+ img .header .set_data_dtype ("uint8" )
96
+ maskimg = img .__class__ (mask .astype ("uint8" ), img .affine , img .header )
97
+ maskimg .to_filename (self ._results ["out_mask" ])
90
98
91
99
return runtime
100
+
101
+
102
+ class _SplitSeriesInputSpec (BaseInterfaceInputSpec ):
103
+ in_file = File (exists = True , mandatory = True , desc = "input 4d image" )
104
+
105
+
106
+ class _SplitSeriesOutputSpec (TraitedSpec ):
107
+ out_files = OutputMultiObject (File (exists = True ), desc = "output list of 3d images" )
108
+
109
+
110
+ class SplitSeries (SimpleInterface ):
111
+ """Split a 4D dataset along the last dimension into a series of 3D volumes."""
112
+
113
+ input_spec = _SplitSeriesInputSpec
114
+ output_spec = _SplitSeriesOutputSpec
115
+
116
+ def _run_interface (self , runtime ):
117
+ in_file = self .inputs .in_file
118
+ img = nb .load (in_file )
119
+ extra_dims = tuple (dim for dim in img .shape [3 :] if dim > 1 ) or (1 ,)
120
+ if len (extra_dims ) != 1 :
121
+ raise ValueError (f"Invalid shape { 'x' .join (str (s ) for s in img .shape )} " )
122
+ img = img .__class__ (img .dataobj .reshape (img .shape [:3 ] + extra_dims ),
123
+ img .affine , img .header )
124
+
125
+ self ._results ["out_files" ] = []
126
+ for i , img_3d in enumerate (nb .four_to_three (img )):
127
+ out_file = str (
128
+ Path (fname_presuffix (in_file , suffix = f"_idx-{ i :03} " )).absolute ()
129
+ )
130
+ img_3d .to_filename (out_file )
131
+ self ._results ["out_files" ].append (out_file )
132
+
133
+ return runtime
134
+
135
+
136
+ class _MergeSeriesInputSpec (BaseInterfaceInputSpec ):
137
+ in_files = InputMultiObject (
138
+ File (exists = True , mandatory = True , desc = "input list of 3d images" )
139
+ )
140
+ allow_4D = traits .Bool (
141
+ True , usedefault = True , desc = "whether 4D images are allowed to be concatenated"
142
+ )
143
+
144
+
145
+ class _MergeSeriesOutputSpec (TraitedSpec ):
146
+ out_file = File (exists = True , desc = "output 4d image" )
147
+
148
+
149
+ class MergeSeries (SimpleInterface ):
150
+ """Merge a series of 3D volumes along the last dimension into a single 4D image."""
151
+
152
+ input_spec = _MergeSeriesInputSpec
153
+ output_spec = _MergeSeriesOutputSpec
154
+
155
+ def _run_interface (self , runtime ):
156
+ nii_list = []
157
+ for f in self .inputs .in_files :
158
+ filenii = nb .squeeze_image (nb .load (f ))
159
+ ndim = filenii .dataobj .ndim
160
+ if ndim == 3 :
161
+ nii_list .append (filenii )
162
+ continue
163
+ elif self .inputs .allow_4D and ndim == 4 :
164
+ nii_list += nb .four_to_three (filenii )
165
+ continue
166
+ else :
167
+ raise ValueError (
168
+ "Input image has an incorrect number of dimensions" f" ({ ndim } )."
169
+ )
170
+
171
+ img_4d = nb .concat_images (nii_list )
172
+ out_file = fname_presuffix (self .inputs .in_files [0 ], suffix = "_merged" )
173
+ img_4d .to_filename (out_file )
174
+
175
+ self ._results ["out_file" ] = out_file
176
+ return runtime
0 commit comments