Skip to content

Commit 7422960

Browse files
committed
Simplifying example
1 parent b2903da commit 7422960

20 files changed

+507
-339
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{('sub-001/anat/sub-001_run-1_T1w', ('nii.gz',), None): ['401-anat-T1w']}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{('sub-001/anat/sub-001_run-1_T1w', ('nii.gz',), None): ['401-anat-T1w']}
Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
import logging
2+
import lzma
3+
from simplejson import loads as json_loads
4+
from os import environ
5+
6+
lgr = logging.getLogger(__name__)
7+
8+
9+
# map the various guesses to the cannonical labels
10+
modality_label_map = {
11+
't1': 'T1w',
12+
't1w': 'T1w',
13+
't2': 'T2w',
14+
't2w': 'T2w',
15+
't1rho': 'T1rho',
16+
't1map': 'T1map',
17+
't2map': 'T2map',
18+
't2star': 'T2star',
19+
'flair': 'FLAIR',
20+
'flash': 'FLASH',
21+
'pd': 'PD',
22+
'pdmap': 'PDmap',
23+
'pdt2': 'PDT2',
24+
'inplanet1': 'inplaneT1',
25+
'inplanet2': 'inplaneT2',
26+
}
27+
28+
# map the cannonical modality labels to data_type labels
29+
datatype_labels_map = {
30+
'bold': 'func',
31+
'sbref': 'func',
32+
33+
'T1w': 'anat',
34+
'T2w': 'anat',
35+
'T1rho': 'anat',
36+
'T1map': 'anat',
37+
'T2map': 'anat',
38+
'T2star': 'anat',
39+
'FLAIR': 'anat',
40+
'FLASH': 'anat',
41+
'PD': 'anat',
42+
'PDmap': 'anat',
43+
'PDT2': 'anat',
44+
'inplaneT1': 'anat',
45+
'inplaneT2': 'anat',
46+
'angio': 'anat',
47+
48+
'swi': 'swi',
49+
'dwi': 'dwi',
50+
51+
'phasediff': 'fmap',
52+
'phase1': 'fmap',
53+
'phase2': 'fmap',
54+
'magnitude1': 'fmap',
55+
'magnitude2': 'fmap',
56+
'fieldmap': 'fmap',
57+
58+
'epi': 'fmap', # TODO?
59+
}
60+
61+
# map specification keys to BIDS abbreviation used in paths
62+
spec2bids_map = {
63+
'subject': "sub",
64+
'anon-subject': "sub",
65+
'bids-session': "ses",
66+
'bids-task': "task",
67+
'bids-run': "run",
68+
'bids-modality': "mod",
69+
'bids-acquisition': "acq",
70+
'bids-scan': "scan",
71+
'bids-contrast-enhancement': "ce",
72+
'bids-reconstruction-algorithm': "rec",
73+
'bids-echo': "echo",
74+
'bids-direction': "dir",
75+
76+
# SWI Extension:
77+
'bids-part': "part",
78+
'bids-coil': "coil",
79+
80+
}
81+
82+
83+
def get_specval(spec, key):
84+
return spec[key]['value']
85+
86+
87+
def has_specval(spec, key):
88+
return key in spec and 'value' in spec[key] and spec[key]['value']
89+
90+
91+
# Snippet from https://github.com/datalad/datalad to avoid depending on it for
92+
# just one function:
93+
def LZMAFile(*args, **kwargs):
94+
"""A little decorator to overcome a bug in lzma
95+
96+
A unique to yoh and some others bug with pyliblzma
97+
calling dir() helps to avoid AttributeError __exit__
98+
see https://bugs.launchpad.net/pyliblzma/+bug/1219296
99+
"""
100+
lzmafile = lzma.LZMAFile(*args, **kwargs)
101+
dir(lzmafile)
102+
return lzmafile
103+
104+
105+
def loads(s, *args, **kwargs):
106+
"""Helper to log actual value which failed to be parsed"""
107+
try:
108+
return json_loads(s, *args, **kwargs)
109+
except:
110+
lgr.error(
111+
"Failed to load content from %r with args=%r kwargs=%r"
112+
% (s, args, kwargs)
113+
)
114+
raise
115+
116+
117+
def load_stream(fname, compressed=False):
118+
119+
_open = LZMAFile if compressed else open
120+
with _open(fname, mode='r') as f:
121+
for line in f:
122+
yield loads(line)
123+
124+
# END datalad Snippet
125+
126+
127+
def create_key(template, outtype=('nii.gz',), annotation_classes=None):
128+
if template is None or not template:
129+
raise ValueError('Template must be a valid format string')
130+
131+
return template, outtype, annotation_classes
132+
133+
134+
class SpecLoader(object):
135+
"""
136+
Persistent object to hold the study specification and not read the JSON on
137+
each invocation of `infotodict`. Module level attribute for the spec itself
138+
doesn't work, since the env variable isn't necessarily available at first
139+
import.
140+
"""
141+
142+
def __init__(self):
143+
self._spec = None
144+
# get chosen subject id (orig or anon) from env var
145+
self.subject = environ.get('HIRNI_SPEC2BIDS_SUBJECT')
146+
147+
def get_study_spec(self):
148+
if self._spec is None:
149+
filename = environ.get('HIRNI_STUDY_SPEC')
150+
if filename:
151+
self._spec = [d for d in load_stream(filename)
152+
if d['type'] == 'dicomseries']
153+
else:
154+
# TODO: Just raise or try a default location first?
155+
raise ValueError("No study specification provided. "
156+
"Set environment variable HIRNI_STUDY_SPEC "
157+
"to do so.")
158+
return self._spec
159+
160+
161+
_spec = SpecLoader()
162+
163+
164+
def validate_spec(spec):
165+
166+
if not spec:
167+
raise ValueError("Image series specification is empty.")
168+
169+
tags = spec.get('tags', None)
170+
if tags and 'hirni-dicom-converter-ignore' in tags:
171+
lgr.debug("Skip series %s (marked 'ignore' in spec)", spec['uid'])
172+
return False
173+
174+
# mandatory keys for any spec dict (not only dicomseries)
175+
for k in spec.keys():
176+
# automatically managed keys with no subdict:
177+
# TODO: Where to define this list?
178+
# TODO: Test whether those are actually present!
179+
if k in ['type', 'location', 'uid', 'dataset-id',
180+
'dataset-refcommit', 'procedures', 'tags']:
181+
continue
182+
if 'value' not in spec[k]:
183+
lgr.warning("DICOM series specification (UID: {uid}) has no value "
184+
"for key '{key}'.".format(uid=spec['uid'], key=k))
185+
return False
186+
187+
if spec['type'] != 'dicomseries':
188+
lgr.warning("Specification not of type 'dicomseries'.")
189+
return False
190+
191+
if 'uid' not in spec.keys() or not spec['uid']:
192+
lgr.warning("Missing image series UID.")
193+
return False
194+
195+
for var in ('bids-modality',):
196+
if not has_specval(spec, var):
197+
lgr.warning("Missing specification value for key '%s'", var)
198+
return False
199+
200+
return True
201+
202+
203+
# TODO: can be removed, whenever nipy/heudiconv #197 is solved
204+
def infotoids(seqinfos, outdir):
205+
return {'locator': None,
206+
'session': None,
207+
'subject': None}
208+
209+
210+
def infotodict(seqinfo): # pragma: no cover
211+
"""Heuristic evaluator for determining which runs belong where
212+
213+
allowed template fields - follow python string module:
214+
215+
item: index within category
216+
subject: participant id
217+
seqitem: run number during scanning
218+
subindex: sub index within group
219+
"""
220+
221+
info = dict()
222+
for idx, s in enumerate(seqinfo):
223+
224+
# find in spec:
225+
candidates = [series for series in _spec.get_study_spec()
226+
if str(s.series_uid) == series['uid']]
227+
if not candidates:
228+
raise ValueError("Found no match for seqinfo: %s" % str(s))
229+
if len(candidates) != 1:
230+
raise ValueError("Found %s match(es) for series UID %s" %
231+
(len(candidates), s.uid))
232+
series_spec = candidates[0]
233+
234+
if not validate_spec(series_spec):
235+
lgr.debug("Series invalid (%s). Skip.", str(s.series_uid))
236+
continue
237+
238+
dirname = filename = "sub-{}".format(_spec.subject)
239+
# session
240+
if has_specval(series_spec, 'bids-session'):
241+
ses = get_specval(series_spec, 'bids-session')
242+
dirname += "/ses-{}".format(ses)
243+
filename += "_ses-{}".format(ses)
244+
245+
# data type
246+
modality = get_specval(series_spec, 'bids-modality')
247+
# make cannonical if possible
248+
modality = modality_label_map.get(modality, modality)
249+
# apply fixed mapping from modality -> data_type
250+
data_type = datatype_labels_map[modality]
251+
252+
dirname += "/{}".format(data_type)
253+
254+
# TODO: Once special cases (like when to use '_mod-' prefix for modality
255+
# are clear, integrate data type selection with spec_key list and
256+
# thereby reduce code duplication further
257+
258+
if data_type == 'func':
259+
# func/sub-<participant_label>[_ses-<session_label>]
260+
# _task-<task_label>[_acq-<label>][_rec-<label>][_run-<index>][_echo-<index>]_<modality_label>.nii[.gz]
261+
262+
for spec_key in ['bids-task', 'bids-acquisition',
263+
'bids-reconstruction_algorithm', 'bids-run',
264+
'bids-echo']:
265+
if has_specval(series_spec, spec_key):
266+
filename += "_{}-{}".format(
267+
spec2bids_map[spec_key],
268+
get_specval(series_spec, spec_key))
269+
270+
filename += "_{}".format(modality)
271+
272+
if data_type == 'anat':
273+
# anat/sub-<participant_label>[_ses-<session_label>]
274+
# [_acq-<label>][_ce-<label>][_rec-<label>][_run-<index>][_mod-<label>]_<modality_label>.nii[.gz]
275+
276+
for spec_key in ['bids-acquisition',
277+
'bids-contrast_enhancement',
278+
'bids-reconstruction_algorithm',
279+
'bids-run']:
280+
if has_specval(series_spec, spec_key):
281+
filename += "_{}-{}".format(
282+
spec2bids_map[spec_key],
283+
get_specval(series_spec, spec_key))
284+
285+
# TODO: [_mod-<label>] (modality if defaced, right?)
286+
# => simple bool 'defaced' in spec or is there more to it?
287+
288+
filename += "_{}".format(modality)
289+
290+
if data_type == 'dwi':
291+
# dwi/sub-<participant_label>[_ses-<session_label>]
292+
# [_acq-<label>][_run-<index>]_dwi.nii[.gz]
293+
294+
for spec_key in ['bids-acquisition',
295+
'bids-run']:
296+
if has_specval(series_spec, spec_key):
297+
filename += "_{}-{}".format(
298+
spec2bids_map[spec_key],
299+
get_specval(series_spec, spec_key))
300+
301+
# TODO: Double check: Is this always correct?
302+
filename += "_dwi"
303+
304+
if data_type == 'swi':
305+
# BIDS-Extension:
306+
# https://docs.google.com/document/d/1kyw9mGgacNqeMbp4xZet3RnDhcMmf4_BmRgKaOkO2Sc
307+
# swi/sub-<participant_label>[_ses-<session_label>]
308+
# [_acq-<label>][_rec-<label>]_part-<phase|mag>[_coil-<index>][_echo-<index>][_run-<index>]_GRE.nii[.gz]
309+
310+
for spec_key in ['bids-acquisition',
311+
'bids-reconstruction_algorithm',
312+
'bids-part',
313+
'bids-coil',
314+
'bids-echo',
315+
'bids-run',
316+
]:
317+
if has_specval(series_spec, spec_key):
318+
filename += "_{}-{}".format(
319+
spec2bids_map[spec_key],
320+
get_specval(series_spec, spec_key))
321+
322+
filename += "_GRE"
323+
324+
if data_type == 'fmap':
325+
# Case 1: Phase difference image and at least one magnitude image
326+
# sub-<participant_label>/[ses-<session_label>/]
327+
# [_acq-<label>][_dir-<dir_label>][_run-<run_index>]_<modality_label>.nii[.gz]
328+
329+
# Note/TODO: fmap modalities:
330+
# _phasediff
331+
# _magnitude1
332+
# _magnitude2
333+
# _phase1
334+
# _phase2
335+
# _magnitude
336+
# _fieldmap
337+
# _epi
338+
339+
for spec_key in ['bids-acquisition',
340+
'bids-direction',
341+
'bids-run']:
342+
if has_specval(series_spec, spec_key):
343+
filename += "_{}-{}".format(
344+
spec2bids_map[spec_key],
345+
get_specval(series_spec, spec_key))
346+
347+
filename += "_{}".format(modality)
348+
349+
key = create_key(dirname + '/' + filename)
350+
if key not in info:
351+
info[key] = []
352+
353+
info[key].append(s[2])
354+
355+
return info

examples/heudiconv/README

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
TODO: Provide description for the dataset -- basic details about the study, possibly pointing to pre-registration (if public or embargoed)

0 commit comments

Comments
 (0)