Skip to content

Commit f71fc22

Browse files
committed
Merge remote-tracking branch 'maloney/dcmstack'
2 parents 29b09af + fe16163 commit f71fc22

File tree

1 file changed

+377
-0
lines changed

1 file changed

+377
-0
lines changed

nipype/interfaces/dcmstack.py

Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
from __future__ import absolute_import
2+
import os, string
3+
from os import path
4+
from glob import glob
5+
from nipype.interfaces.base import (TraitedSpec,
6+
DynamicTraitedSpec,
7+
InputMultiPath,
8+
File,
9+
Directory,
10+
traits,
11+
BaseInterface,
12+
)
13+
import nibabel as nb
14+
from nipype.interfaces.traits_extension import isdefined, Undefined
15+
16+
have_dcmstack = True
17+
try:
18+
import dicom
19+
import dcmstack
20+
from dcmstack.dcmmeta import NiftiWrapper
21+
except ImportError:
22+
have_dcmstack = False
23+
24+
def sanitize_path_comp(path_comp):
25+
result = []
26+
for char in path_comp:
27+
if not char in string.letters + string.digits + '-_.':
28+
result.append('_')
29+
else:
30+
result.append(char)
31+
return ''.join(result)
32+
33+
class NiftiGeneratorBaseInputSpec(TraitedSpec):
34+
out_format = traits.Str(desc="String which can be formatted with "
35+
"meta data to create the output filename(s)")
36+
out_ext = traits.Str('.nii.gz',
37+
usedefault=True,
38+
desc="Determines output file type")
39+
40+
class NiftiGeneratorBase(BaseInterface):
41+
'''Base class for interfaces that produce Nifti files, potentially with
42+
embeded meta data.'''
43+
def _get_out_path(self, meta):
44+
'''Return the output path for the gernerated Nifti.'''
45+
if self.inputs.out_format:
46+
out_fmt = self.inputs.out_format
47+
else:
48+
#If no out_format is specified, use a sane default that will work
49+
#with the provided meta data.
50+
out_fmt = []
51+
if 'SeriesNumber' in meta:
52+
out_fmt.append('%(SeriesNumber)03d')
53+
if 'ProtocolName' in meta:
54+
out_fmt.append('%(ProtocolName)s')
55+
elif 'SeriesDescription' in meta:
56+
out_fmt.append('%(SeriesDescription)s')
57+
else:
58+
out_fmt.append('sequence')
59+
out_fmt = '-'.join(out_fmt)
60+
out_fn = (out_fmt % meta) + self.inputs.out_ext
61+
out_fn = sanitize_path_comp(out_fn)
62+
return path.join(os.getcwd(), out_fn)
63+
64+
class DcmStackInputSpec(NiftiGeneratorBaseInputSpec):
65+
dicom_files = traits.Either(InputMultiPath(File(exists=True)),
66+
Directory(exists=True),
67+
traits.Str(),
68+
mandatory=True)
69+
embed_meta = traits.Bool(desc="Embed DICOM meta data into result")
70+
exclude_regexes = traits.List(desc="Meta data to exclude, suplementing "
71+
"any default exclude filters")
72+
include_regexes = traits.List(desc="Meta data to include, overriding any "
73+
"exclude filters")
74+
75+
class DcmStackOutputSpec(TraitedSpec):
76+
out_file = traits.File(exists=True)
77+
78+
class DcmStack(NiftiGeneratorBase):
79+
'''Create one Nifti file from a set of DICOM files. Can optionally embed
80+
meta data.
81+
82+
Example
83+
-------
84+
85+
>>> from nipype.interfaces.dcmstack import DcmStack
86+
>>> stacker = DcmStack()
87+
>>> stacker.inputs.dicom_files = 'path/to/series/'
88+
>>> stacker.run()
89+
>>> result.outputs.out_file
90+
'/path/to/cwd/sequence.nii.gz'
91+
'''
92+
input_spec = DcmStackInputSpec
93+
output_spec = DcmStackOutputSpec
94+
95+
def _get_filelist(self, trait_input):
96+
if isinstance(trait_input, str):
97+
if path.isdir(trait_input):
98+
return glob(path.join(trait_input, '*.dcm'))
99+
else:
100+
return glob(trait_input)
101+
102+
return trait_input
103+
104+
def _run_interface(self, runtime):
105+
src_paths = self._get_filelist(self.inputs.dicom_files)
106+
include_regexes = dcmstack.default_key_incl_res
107+
if isdefined(self.inputs.include_regexes):
108+
include_regexes += self.inputs.include_regexes
109+
exclude_regexes = dcmstack.default_key_excl_res
110+
if isdefined(self.inputs.exclude_regexes):
111+
exclude_regexes += self.inputs.exclude_regexes
112+
meta_filter = dcmstack.make_key_regex_filter(exclude_regexes,
113+
include_regexes)
114+
stack = dcmstack.DicomStack(meta_filter=meta_filter)
115+
for src_path in src_paths:
116+
src_dcm = dicom.read_file(src_path, force=True)
117+
stack.add_dcm(src_dcm)
118+
nii = stack.to_nifti(embed_meta=True)
119+
nw = NiftiWrapper(nii)
120+
self.out_path = \
121+
self._get_out_path(nw.meta_ext.get_class_dict(('global', 'const')))
122+
if not self.inputs.embed_meta:
123+
nw.remove_extension()
124+
nb.save(nii, self.out_path)
125+
return runtime
126+
127+
def _list_outputs(self):
128+
outputs = self._outputs().get()
129+
outputs["out_file"] = self.out_path
130+
return outputs
131+
132+
class GroupAndStackOutputSpec(TraitedSpec):
133+
out_list = traits.List(desc="List of output nifti files")
134+
135+
class GroupAndStack(DcmStack):
136+
'''Create (potentially) multiple Nifti files for a set of DICOM files.
137+
'''
138+
input_spec = DcmStackInputSpec
139+
output_spec = GroupAndStackOutputSpec
140+
141+
def _run_interface(self, runtime):
142+
src_paths = self._get_filelist(self.inputs.dicom_files)
143+
stacks = dcmstack.parse_and_stack(src_paths)
144+
145+
self.out_list = []
146+
for key, stack in stacks.iteritems():
147+
nw = NiftiWrapper(stack.to_nifti(embed_meta=True))
148+
const_meta = nw.meta_ext.get_class_dict(('global', 'const'))
149+
out_path = self._get_out_path(const_meta)
150+
if not self.inputs.embed_meta:
151+
nw.remove_extension()
152+
nb.save(nw.nii_img, out_path)
153+
self.out_list.append(out_path)
154+
155+
return runtime
156+
157+
def _list_outputs(self):
158+
outputs = self._outputs().get()
159+
outputs["out_list"] = self.out_list
160+
return outputs
161+
162+
class LookupMetaInputSpec(TraitedSpec):
163+
in_file = File(mandatory=True,
164+
exists=True,
165+
desc='The input Nifti file')
166+
meta_keys = traits.Either(traits.List(),
167+
traits.Dict(),
168+
mandatory=True,
169+
desc=("List of meta data keys to lookup, or a "
170+
"dict where keys specify the meta data keys to "
171+
"lookup and the values specify the output names")
172+
)
173+
174+
class LookupMeta(BaseInterface):
175+
'''Lookup meta data values from a Nifti with embeded meta data.
176+
177+
Example
178+
-------
179+
180+
>>> from nipype.interfaces import dcmstack
181+
>>> lookup = dcmstack.LookupMeta()
182+
>>> lookup.inputs.in_file = 'input.nii.gz'
183+
>>> lookup.inputs.meta_keys = {'RepetitionTime' : 'TR',
184+
'EchoTime' : 'TE'
185+
}
186+
>>> result = lookup.run()
187+
>>> result.outputs.TR
188+
9500.0
189+
>>> result.outputs.TE
190+
95.0
191+
'''
192+
input_spec = LookupMetaInputSpec
193+
output_spec = DynamicTraitedSpec
194+
195+
def _make_name_map(self):
196+
if isinstance(self.inputs.meta_keys, list):
197+
self._meta_keys = {}
198+
for key in self.inputs.meta_keys:
199+
self._meta_keys[key] = key
200+
else:
201+
self._meta_keys = self.inputs.meta_keys
202+
203+
def _outputs(self):
204+
self._make_name_map()
205+
outputs = super(LookupMeta, self)._outputs()
206+
undefined_traits = {}
207+
for out_name in self._meta_keys.values():
208+
outputs.add_trait(out_name, traits.Any)
209+
undefined_traits[out_name] = Undefined
210+
outputs.trait_set(trait_change_notify=False, **undefined_traits)
211+
#Not sure why this is needed
212+
for out_name in self._meta_keys.values():
213+
_ = getattr(outputs, out_name)
214+
return outputs
215+
216+
def _run_interface(self, runtime):
217+
#If the 'meta_keys' input is a list, covert it to a dict
218+
self._make_name_map()
219+
nw = NiftiWrapper.from_filename(self.inputs.in_file)
220+
self.result = {}
221+
for meta_key, out_name in self._meta_keys.iteritems():
222+
self.result[out_name] = nw.meta_ext.get_values(meta_key)
223+
224+
return runtime
225+
226+
def _list_outputs(self):
227+
outputs = self._outputs().get()
228+
outputs.update(self.result)
229+
return outputs
230+
231+
class CopyMetaInputSpec(TraitedSpec):
232+
src_file = traits.File(mandatory=True,
233+
exists=True,
234+
)
235+
dest_file = traits.File(mandatory=True,
236+
exists=True)
237+
include_classes = traits.List(desc="List of specific meta data "
238+
"classifications to include. If not "
239+
"specified include everything.")
240+
exclude_classes = traits.List(desc="List of meta data "
241+
"classifications to exclude")
242+
243+
class CopyMetaOutputSpec(TraitedSpec):
244+
dest_file = traits.File(exists=True)
245+
246+
class CopyMeta(BaseInterface):
247+
'''Copy meta data from one Nifti file to another. Useful for preserving
248+
meta data after some processing steps.'''
249+
input_spec = CopyMetaInputSpec
250+
output_spec = CopyMetaOutputSpec
251+
252+
def _run_interface(self, runtime):
253+
src = NiftiWrapper.from_filename(self.inputs.src_file)
254+
dest_nii = nb.load(self.inputs.dest_file)
255+
dest = NiftiWrapper(dest_nii, make_empty=True)
256+
classes = src.meta_ext.get_valid_classes()
257+
if self.inputs.include_classes:
258+
classes = [cls
259+
for cls in classes
260+
if cls in self.inputs.include_classes
261+
]
262+
if self.inputs.exclude_classes:
263+
classes = [cls
264+
for cls in classes
265+
if not cls in self.inputs.exclude_classes
266+
]
267+
268+
for cls in classes:
269+
src_dict = src.meta_ext.get_class_dict(cls)
270+
dest_dict = dest.meta_ext.get_class_dict(cls)
271+
dest_dict.update(src_dict)
272+
273+
self.out_path = path.join(os.getcwd(),
274+
path.basename(self.inputs.dest_file))
275+
dest.to_filename(self.out_path)
276+
277+
return runtime
278+
279+
def _list_outputs(self):
280+
outputs = self._outputs().get()
281+
outputs['dest_file'] = self.out_path
282+
return outputs
283+
284+
class MergeNiftiInputSpec(NiftiGeneratorBaseInputSpec):
285+
in_files = traits.List(mandatory=True,
286+
desc="List of Nifti files to merge")
287+
sort_order = traits.Either(traits.Str(),
288+
traits.List(),
289+
desc="One or more meta data keys to "
290+
"sort files by.")
291+
merge_dim = traits.Int(desc="Dimension to merge along. If not "
292+
"specified, the last singular or "
293+
"non-existant dimension is used.")
294+
295+
class MergeNiftiOutputSpec(TraitedSpec):
296+
out_file = traits.File(exists=True,
297+
desc="Merged Nifti file")
298+
299+
def make_key_func(meta_keys, index=None):
300+
def key_func(src_nii):
301+
result = [src_nii.get_meta(key, index) for key in meta_keys]
302+
return result
303+
304+
return key_func
305+
306+
class MergeNifti(NiftiGeneratorBase):
307+
'''Merge multiple Nifti files into one. Merges together meta data
308+
extensions as well.'''
309+
input_spec = MergeNiftiInputSpec
310+
output_spec = MergeNiftiOutputSpec
311+
312+
def _run_interface(self, runtime):
313+
niis = [nb.load(fn)
314+
for fn in self.inputs.in_files
315+
]
316+
nws = [NiftiWrapper(nii, make_empty=True)
317+
for nii in niis
318+
]
319+
if self.inputs.sort_order:
320+
sort_order = self.inputs.sort_order
321+
if isinstance(sort_order, str):
322+
sort_order = [sort_order]
323+
nws.sort(key=make_key_func(sort_order))
324+
if self.inputs.merge_dim == traits.Undefined:
325+
merge_dim = None
326+
else:
327+
merge_dim = self.inputs.merge_dim
328+
merged = NiftiWrapper.from_sequence(nws, merge_dim)
329+
const_meta = merged.meta_ext.get_class_dict(('global', 'const'))
330+
self.out_path = self._get_out_path(const_meta)
331+
nb.save(merged.nii_img, self.out_path)
332+
return runtime
333+
334+
def _list_outputs(self):
335+
outputs = self._outputs().get()
336+
outputs['out_file'] = self.out_path
337+
return outputs
338+
339+
class SplitNiftiInputSpec(NiftiGeneratorBaseInputSpec):
340+
in_file = traits.File(exists=True,
341+
mandatory=True,
342+
desc="Nifti file to split")
343+
split_dim = traits.Int(desc="Dimension to split along. If not "
344+
"specified, the last dimension is used.")
345+
346+
class SplitNiftiOutputSpec(TraitedSpec):
347+
out_list = traits.List(exists=True,
348+
desc="Split Nifti files")
349+
350+
class SplitNifti(NiftiGeneratorBase):
351+
'''Split one Nifti file into many along the specified dimension. Each
352+
result has an updated meta data extension as well.'''
353+
input_spec = SplitNiftiInputSpec
354+
output_spec = SplitNiftiOutputSpec
355+
356+
def _run_interface(self, runtime):
357+
self.out_list = []
358+
nii = nb.load(self.inputs.in_file)
359+
nw = NiftiWrapper(nii, make_empty=True)
360+
split_dim = None
361+
if self.inputs.split_dim == traits.Undefined:
362+
split_dim = None
363+
else:
364+
split_dim = self.inputs.split_dim
365+
for split_nw in nw.split(split_dim):
366+
const_meta = split_nw.meta_ext.get_class_dict(('global', 'const'))
367+
out_path = self._get_out_path(const_meta)
368+
nb.save(split_nw.nii_img, out_path)
369+
self.out_list.append(out_path)
370+
371+
return runtime
372+
373+
def _list_outputs(self):
374+
outputs = self._outputs().get()
375+
outputs['out_list'] = self.out_list
376+
return outputs
377+

0 commit comments

Comments
 (0)