Skip to content

Commit a13773d

Browse files
committed
updated dcm2niix for latest syntax
1 parent 78f9c73 commit a13773d

File tree

1 file changed

+123
-57
lines changed

1 file changed

+123
-57
lines changed

pydra/tasks/dcm2niix/utils.py

Lines changed: 123 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,43 @@
11
import attrs
22
from pathlib import Path
3+
import typing as ty
34
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
58
from pydra.design import shell
69

710

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:
921
"""Attempting to handle the different suffixes that are appended to filenames
1022
created by Dcm2niix (see https://github.com/rordenlab/dcm2niix/blob/master/FILENAMING.md)
1123
"""
1224

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+
)
1430
fpath = fpath.absolute()
1531

1632
# 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:
1937
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)
2139
]
22-
if len(neighbours) == 1 and file_postfix is attrs.NOTHING:
40+
if len(neighbours) == 1:
2341
fpath = neighbours[0]
2442
else:
2543
raise ValueError(
@@ -29,33 +47,51 @@ def out_file_path(out_dir, filename, file_postfix, ext):
2947
"list of postfixes that dcm2niix produces and provide an appropriate "
3048
"postfix, or set postfix to None to ignore matching a single file and use "
3149
"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)
3352
)
3453
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]
3665

37-
return fpath
3866

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
3974

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"
4575

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
4783

4884

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:
5088
# 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
5692

5793

58-
def dcm2niix_out_files(out_dir, filename):
94+
def dcm2niix_out_files(out_dir: Path, filename: str) -> list[str]:
5995
return [
6096
str(p.absolute())
6197
for p in Path(out_dir).iterdir()
@@ -83,7 +119,8 @@ class Dcm2Niix(ShellDef["Dcm2Niix.Outputs"]):
83119
position=-1,
84120
help=("The directory containing the DICOMs to be converted"),
85121
)
86-
out_dir: Path = shell.arg(
122+
out_dir: Path | None = shell.arg(
123+
default=None,
87124
argstr="-o '{out_dir}'",
88125
help="output directory",
89126
)
@@ -92,7 +129,8 @@ class Dcm2Niix(ShellDef["Dcm2Niix.Outputs"]):
92129
help="The output name for the file",
93130
default="out_file",
94131
)
95-
file_postfix: str = shell.arg(
132+
file_postfix: str | None = shell.arg(
133+
default=None,
96134
help=(
97135
"The postfix appended to the output filename. Used to select which "
98136
"of the disambiguated nifti files created by dcm2niix to return "
@@ -104,134 +142,160 @@ class Dcm2Niix(ShellDef["Dcm2Niix.Outputs"]):
104142
"output files returned in 'out_files' instead."
105143
),
106144
)
107-
compress: str = shell.arg(
145+
compress: str | None = shell.arg(
146+
default=None,
108147
argstr="-z {compress}",
109148
allowed_values=("y", "o", "i", "n", "3"),
110149
help=(
111150
"gz compress images [y=pigz, o=optimal pigz, "
112151
"i=internal:miniz, n=no, 3=no,3D]"
113152
),
114153
)
115-
compression_level: int = shell.arg(
154+
compression_level: int | None = shell.arg(
155+
default=None,
116156
argstr="-{compression_level}",
117157
allowed_values=tuple(range(1, 10)),
118158
help="gz compression level ",
119159
)
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+
)
121163
bids: str = shell.arg(
164+
default="y",
122165
argstr="-b {bids}",
123166
allowed_values=("y", "n", "o"),
124167
help="BIDS sidecar [o=only: no NIfTI]",
125168
)
126-
anonymize_bids: str = shell.arg(
169+
anonymize_bids: str | None = shell.arg(
170+
default=None,
127171
argstr="-ba {anonymize_bids}",
128172
allowed_values=("y", "n"),
129173
help="anonymize BIDS ",
130174
)
131175
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 "
133177
)
134-
search_depth: int = shell.arg(
178+
search_depth: int | None = shell.arg(
179+
default=None,
135180
argstr="-d {search_depth}",
136181
help=(
137182
"directory search depth. Convert DICOMs in " "sub-folders of " "in_folder? "
138183
),
139184
)
140-
export_nrrd: str = shell.arg(
185+
export_nrrd: str | None = shell.arg(
186+
default=None,
141187
argstr="-e {export_nrrd}",
142188
allowed_values=("y", "n"),
143189
help="export as NRRD instead of NIfTI ",
144190
)
145-
generate_defaults: str = shell.arg(
191+
generate_defaults: str | None = shell.arg(
192+
default=None,
146193
argstr="-g {generate_defaults}",
147194
allowed_values=("y", "n", "o", "i"),
148195
help=(
149196
"generate defaults file [o=only: reset and write "
150197
"defaults; i=ignore: reset defaults]"
151198
),
152199
)
153-
ignore_derived: str = shell.arg(
200+
ignore_derived: str | None = shell.arg(
201+
default=None,
154202
argstr="-i {ignore_derived}",
155203
allowed_values=("y", "n"),
156204
help="ignore derived, localizer and 2D images ",
157205
)
158-
losslessly_scale: str = shell.arg(
206+
losslessly_scale: str | None = shell.arg(
207+
default=None,
159208
argstr="-l {losslessly_scale}",
160209
allowed_values=("y", "n", "o"),
161210
help=(
162211
"losslessly scale 16-bit integers to use dynamic range "
163212
"[yes=scale, no=no, but uint16->int16, o=original]"
164213
),
165214
)
166-
merge_2d: int = shell.arg(
215+
merge_2d: int | None = shell.arg(
216+
default=None,
167217
argstr="-m {merge_2d}",
168218
allowed_values=("y", "n", "0", "1", "2"),
169219
help=(
170220
"merge 2D slices from same series regardless of echo, "
171221
"exposure, etc. no, yes, auto"
172222
),
173223
)
174-
only: int = shell.arg(
224+
only: int | None = shell.arg(
225+
default=None,
175226
argstr="-n {only}",
176227
help=("only convert this series CRC number - can be used up " "to 16 times"),
177228
)
178-
philips_scaling: str = shell.arg(
229+
philips_scaling: str | None = shell.arg(
230+
default=None,
179231
argstr="-p {philips_scaling}",
180232
help="Philips precise float (not display) scaling",
181233
)
182-
rename_instead: str = shell.arg(
234+
rename_instead: str | None = shell.arg(
235+
default=None,
183236
argstr="-r {rename_instead}",
184237
allowed_values=("y", "n"),
185238
help="rename instead of convert DICOMs ",
186239
)
187-
single_file_mode: str = shell.arg(
240+
single_file_mode: str | None = shell.arg(
241+
default=None,
188242
argstr="-s {single_file_mode}",
189243
allowed_values=("y", "n"),
190244
help=("single file mode, do not convert other images in " "folder "),
191245
)
192-
private_text_notes: str = shell.arg(
246+
private_text_notes: str | None = shell.arg(
247+
default=None,
193248
argstr="-t {private_text_notes}",
194249
allowed_values=("y", "n"),
195250
help=("text notes includes private patient details"),
196251
)
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,
199257
argstr="-v {verbose}",
200258
allowed_values=("y", "n", "0", "1", "2"),
201259
help="verbose no, yes, logorrheic",
202260
)
203-
name_conflicts: int = shell.arg(
261+
name_conflicts: int | None = shell.arg(
262+
default=None,
204263
argstr="-w {name_conflicts}",
205264
allowed_values=tuple(range(3)),
206265
help=(
207266
"write behavior for name conflicts "
208267
"[0=skip duplicates, 1=overwrite, 2=add suffix]"
209268
),
210269
)
211-
crop_3d: str = shell.arg(
270+
crop_3d: str | None = shell.arg(
271+
default=None,
212272
argstr="-x {crop_3d}",
213273
allowed_values=("y", "n", "i"),
214274
help=(
215275
"crop 3D acquisitions (use ignore to neither crop nor "
216276
"rotate 3D acquistions)"
217277
),
218278
)
219-
big_endian: str = shell.arg(
279+
big_endian: str | None = shell.arg(
280+
default=None,
220281
argstr="--big-endian {big_endian}",
221282
allowed_values=("y", "n", "o"),
222283
help="byte order [y=big-end, n=little-end, o=optimal/native]",
223284
)
224-
progress: str = shell.arg(
285+
progress: str | None = shell.arg(
286+
default=None,
225287
argstr="--progress {progress}",
226288
allowed_values=("y", "n"),
227289
help="Slicer format progress information ",
228290
)
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")
232296

233297
class Outputs(ShellOutputs):
234-
out_file: File = shell.out(
298+
out_file: Nifti1 | NiftiGz = shell.out(
235299
help=(
236300
"output NIfTI image. If multiple nifti files are created (e.g. for "
237301
"different echoes), then the 'file_postfix' input can be provided to "
@@ -241,18 +305,20 @@ class Outputs(ShellOutputs):
241305
),
242306
callable=dcm2niix_out_file,
243307
)
244-
out_json: File = shell.out(
308+
out_json: Json | None = shell.out(
245309
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'
247311
callable=dcm2niix_out_json,
248312
)
249-
out_bval: File = shell.outarg(
313+
out_bval: Bval | None = shell.out(
250314
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,
252317
)
253-
out_bvec: File = shell.outarg(
318+
out_bvec: Bvec | None = shell.out(
254319
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,
256322
)
257323
out_files: list[File] = shell.out(
258324
help=(

0 commit comments

Comments
 (0)