1
1
import attrs
2
2
from pathlib import Path
3
+ import typing as ty
3
4
from pydra .engine .specs import ShellDef , ShellOutputs
4
- from fileformats .generic import File , Directory
5
+ from fileformats .generic import File , Directory , FileSet
6
+ from fileformats .application import Json
7
+ from fileformats .medimage import Nifti1 , NiftiGz , Bvec , Bval
5
8
from pydra .design import shell
6
9
7
10
8
- def out_file_path (out_dir , filename , file_postfix , ext ):
11
+ FS = ty .TypeVar ("FS" , bound = FileSet )
12
+
13
+
14
+ def get_out_file (
15
+ out_dir : Path ,
16
+ fileformat : ty .Type [FS ],
17
+ filename : str ,
18
+ file_postfix : str | None ,
19
+ required : bool = False ,
20
+ ) -> FS | None :
9
21
"""Attempting to handle the different suffixes that are appended to filenames
10
22
created by Dcm2niix (see https://github.com/rordenlab/dcm2niix/blob/master/FILENAMING.md)
11
23
"""
12
24
13
- fpath = Path (out_dir ) / (filename + (file_postfix if file_postfix else "" ) + ext )
25
+ assert fileformat .ext , f"File format { fileformat } does not have an extension"
26
+
27
+ fpath = Path (out_dir ) / (
28
+ filename + (file_postfix if file_postfix else "" ) + fileformat .ext
29
+ )
14
30
fpath = fpath .absolute ()
15
31
16
32
# Check to see if multiple echos exist in the DICOM dataset
17
- if not fpath .exists ():
18
- if file_postfix is not None : # NB: doesn't match attrs.NOTHING
33
+ if fpath .exists ():
34
+ fileset = fileformat (fpath )
35
+ else :
36
+ if required :
19
37
neighbours = [
20
- str ( p ) for p in fpath .parent .iterdir () if p .name .endswith (ext )
38
+ p for p in fpath .parent .iterdir () if p .name .endswith (fileformat . ext )
21
39
]
22
- if len (neighbours ) == 1 and file_postfix is attrs . NOTHING :
40
+ if len (neighbours ) == 1 :
23
41
fpath = neighbours [0 ]
24
42
else :
25
43
raise ValueError (
@@ -29,33 +47,51 @@ def out_file_path(out_dir, filename, file_postfix, ext):
29
47
"list of postfixes that dcm2niix produces and provide an appropriate "
30
48
"postfix, or set postfix to None to ignore matching a single file and use "
31
49
"the list returned in 'out_files' instead. Found the following files "
32
- "with matching extensions:\n " + "\n " .join (neighbours )
50
+ "with matching extensions:\n "
51
+ + "\n " .join (str (p ) for p in neighbours )
33
52
)
34
53
else :
35
- fpath = attrs .NOTHING # Did not find output path and
54
+ fileset = None # Did not find output path and
55
+ return fileset
56
+
57
+
58
+ def dcm2niix_out_file (
59
+ out_dir : Path , filename : str , file_postfix : str , compress : str
60
+ ) -> Nifti1 | NiftiGz :
61
+ fileformat : ty .Type [Nifti1 | NiftiGz ] = (
62
+ NiftiGz if compress in ("y" , "o" , "i" ) else Nifti1
63
+ )
64
+ return get_out_file (out_dir , fileformat , filename , file_postfix , True ) # type: ignore[return-value]
36
65
37
- return fpath
38
66
67
+ def dcm2niix_out_json (
68
+ out_dir : Path , filename : str , file_postfix : str , bids : str
69
+ ) -> Json | None :
70
+ # Append echo number of NIfTI echo to select is provided
71
+ if bids in ("y" , "o" ):
72
+ return get_out_file (out_dir , Json , filename , file_postfix )
73
+ return None
39
74
40
- def dcm2niix_out_file (out_dir , filename , file_postfix , compress ):
41
- ext = ".nii"
42
- # If compressed, append the zip extension
43
- if compress in ("y" , "o" , "i" ):
44
- ext += ".gz"
45
75
46
- return out_file_path (out_dir , filename , file_postfix , ext )
76
+ def dcm2niix_out_bvec (
77
+ out_dir : Path , filename : str , file_postfix : str , bids : str
78
+ ) -> Bvec | None :
79
+ # Append echo number of NIfTI echo to select is provided
80
+ if bids in ("y" , "o" ):
81
+ return get_out_file (out_dir , Bvec , filename , file_postfix )
82
+ return None
47
83
48
84
49
- def dcm2niix_out_json (out_dir , filename , file_postfix , bids ):
85
+ def dcm2niix_out_bval (
86
+ out_dir : Path , filename : str , file_postfix : str , bids : str
87
+ ) -> Bval | None :
50
88
# Append echo number of NIfTI echo to select is provided
51
- if bids is attrs .NOTHING or bids in ("y" , "o" ):
52
- fpath = out_file_path (out_dir , filename , file_postfix , ".json" )
53
- else :
54
- fpath = attrs .NOTHING
55
- return fpath
89
+ if bids in ("y" , "o" ):
90
+ return get_out_file (out_dir , Bval , filename , file_postfix )
91
+ return None
56
92
57
93
58
- def dcm2niix_out_files (out_dir , filename ) :
94
+ def dcm2niix_out_files (out_dir : Path , filename : str ) -> list [ str ] :
59
95
return [
60
96
str (p .absolute ())
61
97
for p in Path (out_dir ).iterdir ()
@@ -83,7 +119,8 @@ class Dcm2Niix(ShellDef["Dcm2Niix.Outputs"]):
83
119
position = - 1 ,
84
120
help = ("The directory containing the DICOMs to be converted" ),
85
121
)
86
- out_dir : Path = shell .arg (
122
+ out_dir : Path | None = shell .arg (
123
+ default = None ,
87
124
argstr = "-o '{out_dir}'" ,
88
125
help = "output directory" ,
89
126
)
@@ -92,7 +129,8 @@ class Dcm2Niix(ShellDef["Dcm2Niix.Outputs"]):
92
129
help = "The output name for the file" ,
93
130
default = "out_file" ,
94
131
)
95
- file_postfix : str = shell .arg (
132
+ file_postfix : str | None = shell .arg (
133
+ default = None ,
96
134
help = (
97
135
"The postfix appended to the output filename. Used to select which "
98
136
"of the disambiguated nifti files created by dcm2niix to return "
@@ -104,134 +142,160 @@ class Dcm2Niix(ShellDef["Dcm2Niix.Outputs"]):
104
142
"output files returned in 'out_files' instead."
105
143
),
106
144
)
107
- compress : str = shell .arg (
145
+ compress : str | None = shell .arg (
146
+ default = None ,
108
147
argstr = "-z {compress}" ,
109
148
allowed_values = ("y" , "o" , "i" , "n" , "3" ),
110
149
help = (
111
150
"gz compress images [y=pigz, o=optimal pigz, "
112
151
"i=internal:miniz, n=no, 3=no,3D]"
113
152
),
114
153
)
115
- compression_level : int = shell .arg (
154
+ compression_level : int | None = shell .arg (
155
+ default = None ,
116
156
argstr = "-{compression_level}" ,
117
157
allowed_values = tuple (range (1 , 10 )),
118
158
help = "gz compression level " ,
119
159
)
120
- adjacent : str = shell .arg (argstr = "-a {adjacent}" , help = "adjacent DICOMs " )
160
+ adjacent : str | None = shell .arg (
161
+ default = None , argstr = "-a {adjacent}" , help = "adjacent DICOMs "
162
+ )
121
163
bids : str = shell .arg (
164
+ default = "y" ,
122
165
argstr = "-b {bids}" ,
123
166
allowed_values = ("y" , "n" , "o" ),
124
167
help = "BIDS sidecar [o=only: no NIfTI]" ,
125
168
)
126
- anonymize_bids : str = shell .arg (
169
+ anonymize_bids : str | None = shell .arg (
170
+ default = None ,
127
171
argstr = "-ba {anonymize_bids}" ,
128
172
allowed_values = ("y" , "n" ),
129
173
help = "anonymize BIDS " ,
130
174
)
131
175
store_comments : bool = shell .arg (
132
- argstr = "-c" , help = "comment stored in NIfTI aux_file "
176
+ default = False , argstr = "-c" , help = "comment stored in NIfTI aux_file "
133
177
)
134
- search_depth : int = shell .arg (
178
+ search_depth : int | None = shell .arg (
179
+ default = None ,
135
180
argstr = "-d {search_depth}" ,
136
181
help = (
137
182
"directory search depth. Convert DICOMs in " "sub-folders of " "in_folder? "
138
183
),
139
184
)
140
- export_nrrd : str = shell .arg (
185
+ export_nrrd : str | None = shell .arg (
186
+ default = None ,
141
187
argstr = "-e {export_nrrd}" ,
142
188
allowed_values = ("y" , "n" ),
143
189
help = "export as NRRD instead of NIfTI " ,
144
190
)
145
- generate_defaults : str = shell .arg (
191
+ generate_defaults : str | None = shell .arg (
192
+ default = None ,
146
193
argstr = "-g {generate_defaults}" ,
147
194
allowed_values = ("y" , "n" , "o" , "i" ),
148
195
help = (
149
196
"generate defaults file [o=only: reset and write "
150
197
"defaults; i=ignore: reset defaults]"
151
198
),
152
199
)
153
- ignore_derived : str = shell .arg (
200
+ ignore_derived : str | None = shell .arg (
201
+ default = None ,
154
202
argstr = "-i {ignore_derived}" ,
155
203
allowed_values = ("y" , "n" ),
156
204
help = "ignore derived, localizer and 2D images " ,
157
205
)
158
- losslessly_scale : str = shell .arg (
206
+ losslessly_scale : str | None = shell .arg (
207
+ default = None ,
159
208
argstr = "-l {losslessly_scale}" ,
160
209
allowed_values = ("y" , "n" , "o" ),
161
210
help = (
162
211
"losslessly scale 16-bit integers to use dynamic range "
163
212
"[yes=scale, no=no, but uint16->int16, o=original]"
164
213
),
165
214
)
166
- merge_2d : int = shell .arg (
215
+ merge_2d : int | None = shell .arg (
216
+ default = None ,
167
217
argstr = "-m {merge_2d}" ,
168
218
allowed_values = ("y" , "n" , "0" , "1" , "2" ),
169
219
help = (
170
220
"merge 2D slices from same series regardless of echo, "
171
221
"exposure, etc. no, yes, auto"
172
222
),
173
223
)
174
- only : int = shell .arg (
224
+ only : int | None = shell .arg (
225
+ default = None ,
175
226
argstr = "-n {only}" ,
176
227
help = ("only convert this series CRC number - can be used up " "to 16 times" ),
177
228
)
178
- philips_scaling : str = shell .arg (
229
+ philips_scaling : str | None = shell .arg (
230
+ default = None ,
179
231
argstr = "-p {philips_scaling}" ,
180
232
help = "Philips precise float (not display) scaling" ,
181
233
)
182
- rename_instead : str = shell .arg (
234
+ rename_instead : str | None = shell .arg (
235
+ default = None ,
183
236
argstr = "-r {rename_instead}" ,
184
237
allowed_values = ("y" , "n" ),
185
238
help = "rename instead of convert DICOMs " ,
186
239
)
187
- single_file_mode : str = shell .arg (
240
+ single_file_mode : str | None = shell .arg (
241
+ default = None ,
188
242
argstr = "-s {single_file_mode}" ,
189
243
allowed_values = ("y" , "n" ),
190
244
help = ("single file mode, do not convert other images in " "folder " ),
191
245
)
192
- private_text_notes : str = shell .arg (
246
+ private_text_notes : str | None = shell .arg (
247
+ default = None ,
193
248
argstr = "-t {private_text_notes}" ,
194
249
allowed_values = ("y" , "n" ),
195
250
help = ("text notes includes private patient details" ),
196
251
)
197
- up_to_date_check : bool = shell .arg (argstr = "-u" , help = "up-to-date check" )
198
- verbose : str = shell .arg (
252
+ up_to_date_check : bool = shell .arg (
253
+ default = False , argstr = "-u" , help = "up-to-date check"
254
+ )
255
+ verbose : str | None = shell .arg (
256
+ default = None ,
199
257
argstr = "-v {verbose}" ,
200
258
allowed_values = ("y" , "n" , "0" , "1" , "2" ),
201
259
help = "verbose no, yes, logorrheic" ,
202
260
)
203
- name_conflicts : int = shell .arg (
261
+ name_conflicts : int | None = shell .arg (
262
+ default = None ,
204
263
argstr = "-w {name_conflicts}" ,
205
264
allowed_values = tuple (range (3 )),
206
265
help = (
207
266
"write behavior for name conflicts "
208
267
"[0=skip duplicates, 1=overwrite, 2=add suffix]"
209
268
),
210
269
)
211
- crop_3d : str = shell .arg (
270
+ crop_3d : str | None = shell .arg (
271
+ default = None ,
212
272
argstr = "-x {crop_3d}" ,
213
273
allowed_values = ("y" , "n" , "i" ),
214
274
help = (
215
275
"crop 3D acquisitions (use ignore to neither crop nor "
216
276
"rotate 3D acquistions)"
217
277
),
218
278
)
219
- big_endian : str = shell .arg (
279
+ big_endian : str | None = shell .arg (
280
+ default = None ,
220
281
argstr = "--big-endian {big_endian}" ,
221
282
allowed_values = ("y" , "n" , "o" ),
222
283
help = "byte order [y=big-end, n=little-end, o=optimal/native]" ,
223
284
)
224
- progress : str = shell .arg (
285
+ progress : str | None = shell .arg (
286
+ default = None ,
225
287
argstr = "--progress {progress}" ,
226
288
allowed_values = ("y" , "n" ),
227
289
help = "Slicer format progress information " ,
228
290
)
229
- terse : bool = shell .arg (argstr = "--terse" , help = "omit filename post-fixes " )
230
- version : bool = shell .arg (argstr = "--version" , help = "report version" )
231
- xml : bool = shell .arg (argstr = "--xml" , help = "Slicer format features" )
291
+ terse : bool = shell .arg (
292
+ default = False , argstr = "--terse" , help = "omit filename post-fixes "
293
+ )
294
+ version : bool = shell .arg (default = False , argstr = "--version" , help = "report version" )
295
+ xml : bool = shell .arg (default = False , argstr = "--xml" , help = "Slicer format features" )
232
296
233
297
class Outputs (ShellOutputs ):
234
- out_file : File = shell .out (
298
+ out_file : Nifti1 | NiftiGz = shell .out (
235
299
help = (
236
300
"output NIfTI image. If multiple nifti files are created (e.g. for "
237
301
"different echoes), then the 'file_postfix' input can be provided to "
@@ -241,18 +305,20 @@ class Outputs(ShellOutputs):
241
305
),
242
306
callable = dcm2niix_out_file ,
243
307
)
244
- out_json : File = shell .out (
308
+ out_json : Json | None = shell .out (
245
309
help = "output BIDS side-car JSON corresponding to 'out_file'" ,
246
- # requires=[("bids", 'y' )], FIXME: should be either 'y' or 'o'
310
+ # requires=[("bids", "y" )], # FIXME: should be either 'y' or 'o'
247
311
callable = dcm2niix_out_json ,
248
312
)
249
- out_bval : File = shell .outarg (
313
+ out_bval : Bval | None = shell .out (
250
314
help = "output dMRI b-values in FSL format" ,
251
- path_template = "{out_dir}/{filename}.bval" ,
315
+ # requires=[("bids", "y")], # FIXME: should be either 'y' or 'o'
316
+ callable = dcm2niix_out_bval ,
252
317
)
253
- out_bvec : File = shell .outarg (
318
+ out_bvec : Bvec | None = shell .out (
254
319
help = "output dMRI b-bectors in FSL format" ,
255
- path_template = "{out_dir}/{filename}.bvec" ,
320
+ # requires=[("bids", "y")], # FIXME: should be either 'y' or 'o'
321
+ callable = dcm2niix_out_bvec ,
256
322
)
257
323
out_files : list [File ] = shell .out (
258
324
help = (
0 commit comments