Skip to content

Commit 803982c

Browse files
authored
Merge pull request #2498 from mgxd/fix/dcm2niix
Fix/dcm2niix
2 parents 95e81c9 + 41c45b9 commit 803982c

File tree

3 files changed

+150
-49
lines changed

3 files changed

+150
-49
lines changed

Diff for: nipype/interfaces/dcm2nii.py

+80-44
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,19 @@
1010

1111
from ..utils.filemanip import split_filename
1212
from .base import (CommandLine, CommandLineInputSpec, InputMultiPath, traits,
13-
TraitedSpec, OutputMultiPath, isdefined, File, Directory)
13+
TraitedSpec, OutputMultiPath, isdefined, File, Directory,
14+
PackageInfo)
15+
16+
17+
class Info(PackageInfo):
18+
"""Handle dcm2niix version information"""
19+
20+
version_cmd = 'dcm2niix'
21+
22+
@staticmethod
23+
def parse_version(raw_info):
24+
m = re.search(r'version (\S+)', raw_info)
25+
return m.groups()[0] if m else None
1426

1527

1628
class Dcm2niiInputSpec(CommandLineInputSpec):
@@ -251,6 +263,8 @@ class Dcm2niixInputSpec(CommandLineInputSpec):
251263
argstr="%s",
252264
position=-1,
253265
copyfile=False,
266+
deprecated='1.0.2',
267+
new_name='source_dir',
254268
mandatory=True,
255269
xor=['source_dir'])
256270
source_dir = Directory(
@@ -260,16 +274,28 @@ class Dcm2niixInputSpec(CommandLineInputSpec):
260274
mandatory=True,
261275
xor=['source_names'])
262276
out_filename = traits.Str(
263-
'%t%p', argstr="-f %s", usedefault=True, desc="Output filename")
277+
argstr="-f %s",
278+
desc="Output filename")
264279
output_dir = Directory(
265-
exists=True, argstr='-o %s', genfile=True, desc="Output directory")
280+
".",
281+
usedefault=True,
282+
exists=True,
283+
argstr='-o %s',
284+
desc="Output directory")
266285
bids_format = traits.Bool(
267-
True, argstr='-b', usedefault=True, desc="Create a BIDS sidecar file")
286+
True,
287+
argstr='-b',
288+
usedefault=True,
289+
desc="Create a BIDS sidecar file")
290+
anon_bids = traits.Bool(
291+
argstr='-ba',
292+
requires=["bids_format"],
293+
desc="Anonymize BIDS")
268294
compress = traits.Enum(
269-
'i', ['y', 'i', 'n'],
295+
'y', 'i', 'n', '3',
270296
argstr='-z %s',
271297
usedefault=True,
272-
desc="Gzip compress images - [y=pigz, i=internal, n=no]")
298+
desc="Gzip compress images - [y=pigz, i=internal, n=no, 3=no,3D]")
273299
merge_imgs = traits.Bool(
274300
False,
275301
argstr='-m',
@@ -279,16 +305,39 @@ class Dcm2niixInputSpec(CommandLineInputSpec):
279305
False,
280306
argstr='-s',
281307
usedefault=True,
282-
desc="Convert only one image (filename as last input")
308+
desc="Single file mode")
283309
verbose = traits.Bool(
284-
False, argstr='-v', usedefault=True, desc="Verbose output")
310+
False,
311+
argstr='-v',
312+
usedefault=True,
313+
desc="Verbose output")
285314
crop = traits.Bool(
286-
False, argstr='-x', usedefault=True, desc="Crop 3D T1 acquisitions")
315+
False,
316+
argstr='-x',
317+
usedefault=True,
318+
desc="Crop 3D T1 acquisitions")
287319
has_private = traits.Bool(
288320
False,
289321
argstr='-t',
290322
usedefault=True,
291323
desc="Flag if text notes includes private patient details")
324+
compression = traits.Enum(
325+
1, 2, 3, 4, 5, 6, 7, 8, 9,
326+
argstr='-%d',
327+
desc="Gz compression level (1=fastest, 9=smallest)")
328+
comment = traits.Str(
329+
argstr='-c %s',
330+
desc="Comment stored as NIfTI aux_file")
331+
ignore_deriv = traits.Bool(
332+
argstr='-i',
333+
desc="Ignore derived, localizer and 2D images")
334+
series_numbers = InputMultiPath(
335+
traits.Str(),
336+
argstr='-n %s...',
337+
desc="Selectively convert by series number - can be used up to 16 times")
338+
philips_float = traits.Bool(
339+
argstr='-p',
340+
desc="Philips precise float (not display) scaling")
292341

293342

294343
class Dcm2niixOutputSpec(TraitedSpec):
@@ -306,27 +355,26 @@ class Dcm2niix(CommandLine):
306355
307356
>>> from nipype.interfaces.dcm2nii import Dcm2niix
308357
>>> converter = Dcm2niix()
309-
>>> converter.inputs.source_names = ['functional_1.dcm', 'functional_2.dcm']
310-
>>> converter.inputs.compress = 'i'
311-
>>> converter.inputs.single_file = True
312-
>>> converter.inputs.output_dir = '.'
313-
>>> converter.cmdline # doctest: +SKIP
314-
'dcm2niix -b y -z i -x n -t n -m n -f %t%p -o . -s y -v n functional_1.dcm'
315-
316-
>>> flags = '-'.join([val.strip() + ' ' for val in sorted(' '.join(converter.cmdline.split()[1:-1]).split('-'))])
317-
>>> flags
318-
' -b y -f %t%p -m n -o . -s y -t n -v n -x n -z i '
358+
>>> converter.inputs.source_dir = 'dicomdir'
359+
>>> converter.inputs.compression = 5
360+
>>> converter.inputs.output_dir = 'ds005'
361+
>>> converter.cmdline
362+
'dcm2niix -b y -z y -5 -x n -t n -m n -o ds005 -s n -v n dicomdir'
363+
>>> converter.run() # doctest: +SKIP
319364
"""
320365

321366
input_spec = Dcm2niixInputSpec
322367
output_spec = Dcm2niixOutputSpec
323368
_cmd = 'dcm2niix'
324369

370+
@property
371+
def version(self):
372+
return Info.version()
373+
325374
def _format_arg(self, opt, spec, val):
326-
if opt in [
327-
'bids_format', 'merge_imgs', 'single_file', 'verbose', 'crop',
328-
'has_private'
329-
]:
375+
bools = ['bids_format', 'merge_imgs', 'single_file', 'verbose', 'crop',
376+
'has_private', 'anon_bids', 'ignore_deriv', 'philips_float']
377+
if opt in bools:
330378
spec = deepcopy(spec)
331379
if val:
332380
spec.argstr += ' y'
@@ -338,14 +386,16 @@ def _format_arg(self, opt, spec, val):
338386
return super(Dcm2niix, self)._format_arg(opt, spec, val)
339387

340388
def _run_interface(self, runtime):
341-
new_runtime = super(Dcm2niix, self)._run_interface(runtime)
389+
# may use return code 1 despite conversion
390+
runtime = super(Dcm2niix, self)._run_interface(
391+
runtime, correct_return_codes=(0, 1, ))
342392
if self.inputs.bids_format:
343393
(self.output_files, self.bvecs, self.bvals,
344-
self.bids) = self._parse_stdout(new_runtime.stdout)
394+
self.bids) = self._parse_stdout(runtime.stdout)
345395
else:
346396
(self.output_files, self.bvecs, self.bvals) = self._parse_stdout(
347-
new_runtime.stdout)
348-
return new_runtime
397+
runtime.stdout)
398+
return runtime
349399

350400
def _parse_stdout(self, stdout):
351401
files = []
@@ -359,11 +409,7 @@ def _parse_stdout(self, stdout):
359409
out_file = None
360410
if line.startswith("Convert "): # output
361411
fname = str(re.search('\S+/\S+', line).group(0))
362-
if isdefined(self.inputs.output_dir):
363-
output_dir = self.inputs.output_dir
364-
else:
365-
output_dir = self._gen_filename('output_dir')
366-
out_file = os.path.abspath(os.path.join(output_dir, fname))
412+
out_file = os.path.abspath(fname)
367413
# extract bvals
368414
if find_b:
369415
bvecs.append(out_file + ".bvec")
@@ -372,16 +418,11 @@ def _parse_stdout(self, stdout):
372418
# next scan will have bvals/bvecs
373419
elif 'DTI gradients' in line or 'DTI gradient directions' in line or 'DTI vectors' in line:
374420
find_b = True
375-
else:
376-
pass
377421
if out_file:
378-
if self.inputs.compress == 'n':
379-
files.append(out_file + ".nii")
380-
else:
381-
files.append(out_file + ".nii.gz")
422+
ext = '.nii' if self.inputs.compress == 'n' else '.nii.gz'
423+
files.append(out_file + ext)
382424
if self.inputs.bids_format:
383425
bids.append(out_file + ".json")
384-
continue
385426
skip = False
386427
# just return what was done
387428
if not bids:
@@ -397,8 +438,3 @@ def _list_outputs(self):
397438
if self.inputs.bids_format:
398439
outputs['bids'] = self.bids
399440
return outputs
400-
401-
def _gen_filename(self, name):
402-
if name == 'output_dir':
403-
return os.getcwd()
404-
return None

Diff for: nipype/interfaces/tests/test_auto_Dcm2niix.py

+13-5
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,21 @@
55

66
def test_Dcm2niix_inputs():
77
input_map = dict(
8+
anon_bids=dict(
9+
argstr='-ba',
10+
requires=['bids_format'],
11+
),
812
args=dict(argstr='%s', ),
913
bids_format=dict(
1014
argstr='-b',
1115
usedefault=True,
1216
),
17+
comment=dict(argstr='-c %s', ),
1318
compress=dict(
1419
argstr='-z %s',
1520
usedefault=True,
1621
),
22+
compression=dict(argstr='-%d', ),
1723
crop=dict(
1824
argstr='-x',
1925
usedefault=True,
@@ -26,6 +32,7 @@ def test_Dcm2niix_inputs():
2632
argstr='-t',
2733
usedefault=True,
2834
),
35+
ignore_deriv=dict(argstr='-i', ),
2936
ignore_exception=dict(
3037
deprecated='1.0.0',
3138
nohash=True,
@@ -35,14 +42,13 @@ def test_Dcm2niix_inputs():
3542
argstr='-m',
3643
usedefault=True,
3744
),
38-
out_filename=dict(
39-
argstr='-f %s',
40-
usedefault=True,
41-
),
45+
out_filename=dict(argstr='-f %s', ),
4246
output_dir=dict(
4347
argstr='-o %s',
44-
genfile=True,
48+
usedefault=True,
4549
),
50+
philips_float=dict(argstr='-p', ),
51+
series_numbers=dict(argstr='-n %s...', ),
4652
single_file=dict(
4753
argstr='-s',
4854
usedefault=True,
@@ -56,7 +62,9 @@ def test_Dcm2niix_inputs():
5662
source_names=dict(
5763
argstr='%s',
5864
copyfile=False,
65+
deprecated='1.0.2',
5966
mandatory=True,
67+
new_name='source_dir',
6068
position=-1,
6169
xor=['source_dir'],
6270
),

Diff for: nipype/interfaces/tests/test_dcm2nii.py

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import os
2+
import pytest
3+
import shutil
4+
5+
from nipype.interfaces.dcm2nii import Dcm2niix
6+
no_dcm2niix = not bool(Dcm2niix().version)
7+
no_datalad = False
8+
try:
9+
from datalad import api # to pull and grab data
10+
from datalad.support.exceptions import IncompleteResultsError
11+
except ImportError:
12+
no_datalad = True
13+
14+
DICOM_DIR = 'http://datasets-tests.datalad.org/dicoms/dcm2niix-tests'
15+
16+
17+
def fetch_data(tmpdir, dicoms):
18+
"""Fetches some test DICOMs using datalad"""
19+
data = os.path.join(tmpdir, 'data')
20+
api.install(path=data, source=DICOM_DIR)
21+
data = os.path.join(data, dicoms)
22+
api.get(path=data)
23+
return data
24+
25+
@pytest.mark.skipif(no_datalad, reason="Datalad required")
26+
@pytest.mark.skipif(no_dcm2niix, reason="Dcm2niix required")
27+
def test_dcm2niix_dwi(tmpdir):
28+
tmpdir.chdir()
29+
try:
30+
datadir = fetch_data(tmpdir.strpath, 'Siemens_Sag_DTI_20160825_145811')
31+
except IncompleteResultsError as exc:
32+
pytest.skip("Failed to fetch test data: %s" % str(exc))
33+
34+
def assert_dwi(eg, bids):
35+
"Some assertions we will make"
36+
assert eg.outputs.converted_files
37+
assert eg.outputs.bvals
38+
assert eg.outputs.bvecs
39+
outputs = [y for x,y in eg.outputs.get().items()]
40+
if bids:
41+
# ensure all outputs are of equal lengths
42+
assert len(set(map(len, outputs))) == 1
43+
else:
44+
assert not eg2.outputs.bids
45+
46+
dcm = Dcm2niix()
47+
dcm.inputs.source_dir = datadir
48+
dcm.inputs.out_filename = '%u%z'
49+
eg1 = dcm.run()
50+
assert_dwi(eg1, True)
51+
52+
# now run specifying output directory and removing BIDS option
53+
outdir = tmpdir.mkdir('conversion').strpath
54+
dcm.inputs.output_dir = outdir
55+
dcm.inputs.bids_format = False
56+
eg2 = dcm.run()
57+
assert_dwi(eg2, False)

0 commit comments

Comments
 (0)