Skip to content

Commit 75a4c82

Browse files
authored
Merge pull request #344 from stilley2/master
Add --bids notop flag to prevent writing top level bids files
2 parents 254b383 + 92856e3 commit 75a4c82

File tree

5 files changed

+89
-34
lines changed

5 files changed

+89
-34
lines changed

docs/usage.rst

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ DICOMs as an independent ``heudiconv`` execution.
4343
The first script aggregates the DICOM directories and submits them to
4444
``run_heudiconv.sh`` with SLURM as a job array.
4545

46+
If using bids, the ``notop`` bids option suppresses creation of
47+
top-level files in the bids directory (e.g.,
48+
``dataset_description.json``) to avoid possible race conditions.
49+
These files may be generated later with ``populate_templates.sh``
50+
below (except for ``participants.tsv``, which must be create
51+
manually).
52+
4653
.. code:: shell
4754
4855
#!/bin/bash
@@ -76,7 +83,22 @@ The second script processes a DICOM directory with ``heudiconv`` using the built
7683
echo Submitted directory: ${DCMDIR}
7784
7885
IMG="/singularity-images/heudiconv-0.5.4-dev.sif"
79-
CMD="singularity run -B ${DCMDIR}:/dicoms:ro -B ${OUTDIR}:/output -e ${IMG} --files /dicoms/ -o /output -f reproin -c dcm2niix -b --minmeta -l ."
86+
CMD="singularity run -B ${DCMDIR}:/dicoms:ro -B ${OUTDIR}:/output -e ${IMG} --files /dicoms/ -o /output -f reproin -c dcm2niix -b notop --minmeta -l ."
87+
88+
printf "Command:\n${CMD}\n"
89+
${CMD}
90+
echo "Successful process"
91+
92+
This script creates the top-level bids files (e.g.,
93+
``dataset_description.json``)
94+
95+
..code:: shell
96+
#!/bin/bash
97+
set -eu
98+
99+
OUTDIR=${1}
100+
IMG="/singularity-images/heudiconv-0.5.4-dev.sif"
101+
CMD="singularity run -B ${OUTDIR}:/output -e ${IMG} --files /output -f reproin --command populate-templates"
80102

81103
printf "Command:\n${CMD}\n"
82104
${CMD}

heudiconv/cli/run.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ def main(argv=None):
112112
random.seed(args.random_seed)
113113
import numpy
114114
numpy.random.seed(args.random_seed)
115+
# Ensure only supported bids options are passed
115116
if args.debug:
116117
lgr.setLevel(logging.DEBUG)
117118
# Should be possible but only with a single subject -- will be used to
@@ -181,8 +182,16 @@ def get_parser():
181182
parser.add_argument('-ss', '--ses', dest='session', default=None,
182183
help='session for longitudinal study_sessions, default '
183184
'is none')
184-
parser.add_argument('-b', '--bids', action='store_true',
185-
help='flag for output into BIDS structure')
185+
parser.add_argument('-b', '--bids', nargs='*',
186+
metavar=('BIDSOPTION1', 'BIDSOPTION2'),
187+
choices=['notop'],
188+
dest='bids_options',
189+
help='flag for output into BIDS structure. Can also '
190+
'take bids specific options, e.g., --bids notop.'
191+
'The only currently supported options is'
192+
'"notop", which skips creation of top-level bids '
193+
'files. This is useful when running in batch mode to '
194+
'prevent possible race conditions.')
186195
parser.add_argument('--overwrite', action='store_true', default=False,
187196
help='flag to allow overwriting existing converted files')
188197
parser.add_argument('--datalad', action='store_true',
@@ -302,7 +311,8 @@ def process_args(args):
302311
from ..external.dlad import prepare_datalad
303312
dlad_sid = sid if not anon_sid else anon_sid
304313
dl_msg = prepare_datalad(anon_study_outdir, anon_outdir, dlad_sid,
305-
session, seqinfo, dicoms, args.bids)
314+
session, seqinfo, dicoms,
315+
args.bids_options)
306316

307317
lgr.info("PROCESSING STARTS: {0}".format(
308318
str(dict(subject=sid, outdir=study_outdir, session=session))))
@@ -316,7 +326,7 @@ def process_args(args):
316326
anon_outdir=anon_study_outdir,
317327
with_prov=args.with_prov,
318328
ses=session,
319-
bids=args.bids,
329+
bids_options=args.bids_options,
320330
seqinfo=seqinfo,
321331
min_meta=args.minmeta,
322332
overwrite=args.overwrite,
@@ -333,7 +343,7 @@ def process_args(args):
333343
# also in batch mode might fail since we have no locking ATM
334344
# and theoretically no need actually to save entire study
335345
# we just need that
336-
add_to_datalad(outdir, study_outdir, msg, args.bids)
346+
add_to_datalad(outdir, study_outdir, msg, args.bids_options)
337347

338348
# if args.bids:
339349
# # Let's populate BIDS templates for folks to take care about

heudiconv/convert.py

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def conversion_info(subject, outdir, info, filegroup, ses):
7979

8080

8181
def prep_conversion(sid, dicoms, outdir, heuristic, converter, anon_sid,
82-
anon_outdir, with_prov, ses, bids, seqinfo, min_meta,
82+
anon_outdir, with_prov, ses, bids_options, seqinfo, min_meta,
8383
overwrite, dcmconfig):
8484
if dicoms:
8585
lgr.info("Processing %d dicoms", len(dicoms))
@@ -88,7 +88,7 @@ def prep_conversion(sid, dicoms, outdir, heuristic, converter, anon_sid,
8888
else:
8989
raise ValueError("neither dicoms nor seqinfo dict was provided")
9090

91-
if bids:
91+
if bids_options is not None:
9292
if not sid:
9393
raise ValueError(
9494
"BIDS requires alphanumeric subject ID. Got an empty value")
@@ -102,7 +102,7 @@ def prep_conversion(sid, dicoms, outdir, heuristic, converter, anon_sid,
102102

103103
# Generate heudiconv info folder
104104
idir = op.join(outdir, '.heudiconv', anon_sid)
105-
if bids and ses:
105+
if bids_options is not None and ses:
106106
idir = op.join(idir, 'ses-%s' % str(ses))
107107
if anon_outdir == outdir:
108108
idir = op.join(idir, 'info')
@@ -177,7 +177,7 @@ def prep_conversion(sid, dicoms, outdir, heuristic, converter, anon_sid,
177177
write_config(edit_file, info)
178178
save_json(filegroup_file, filegroup)
179179

180-
if bids:
180+
if bids_options is not None:
181181
# the other portion of the path would mimic BIDS layout
182182
# so we don't need to worry here about sub, ses at all
183183
tdir = anon_outdir
@@ -192,7 +192,7 @@ def prep_conversion(sid, dicoms, outdir, heuristic, converter, anon_sid,
192192
scaninfo_suffix=getattr(heuristic, 'scaninfo_suffix', '.json'),
193193
custom_callable=getattr(heuristic, 'custom_callable', None),
194194
with_prov=with_prov,
195-
bids=bids,
195+
bids_options=bids_options,
196196
outdir=tdir,
197197
min_meta=min_meta,
198198
overwrite=overwrite,
@@ -201,7 +201,7 @@ def prep_conversion(sid, dicoms, outdir, heuristic, converter, anon_sid,
201201
for item_dicoms in filegroup.values():
202202
clear_temp_dicoms(item_dicoms)
203203

204-
if bids:
204+
if bids_options is not None and 'notop' not in bids_options:
205205
if seqinfo:
206206
keys = list(seqinfo)
207207
add_participant_record(anon_outdir,
@@ -213,7 +213,7 @@ def prep_conversion(sid, dicoms, outdir, heuristic, converter, anon_sid,
213213

214214

215215
def convert(items, converter, scaninfo_suffix, custom_callable, with_prov,
216-
bids, outdir, min_meta, overwrite, symlink=True, prov_file=None,
216+
bids_options, outdir, min_meta, overwrite, symlink=True, prov_file=None,
217217
dcmconfig=None):
218218
"""Perform actual conversion (calls to converter etc) given info from
219219
heuristic's `infotodict`
@@ -256,7 +256,7 @@ def convert(items, converter, scaninfo_suffix, custom_callable, with_prov,
256256
# We want to create this dir only if we are converting it to nifti,
257257
# or if we're using BIDS
258258
dicom_only = outtypes == ('dicom',)
259-
if not(dicom_only and bids) and not op.exists(prefix_dirname):
259+
if not(dicom_only and (bids_options is not None)) and not op.exists(prefix_dirname):
260260
os.makedirs(prefix_dirname)
261261

262262
for outtype in outtypes:
@@ -265,7 +265,7 @@ def convert(items, converter, scaninfo_suffix, custom_callable, with_prov,
265265
lgr.debug("Includes the following dicoms: %s", item_dicoms)
266266

267267
if outtype == 'dicom':
268-
convert_dicom(item_dicoms, bids, prefix,
268+
convert_dicom(item_dicoms, bids_options, prefix,
269269
outdir, tempdirs, symlink, overwrite)
270270
elif outtype in ['nii', 'nii.gz']:
271271
assert converter == 'dcm2niix', ('Invalid converter '
@@ -279,16 +279,16 @@ def convert(items, converter, scaninfo_suffix, custom_callable, with_prov,
279279

280280
# run conversion through nipype
281281
res, prov_file = nipype_convert(item_dicoms, prefix, with_prov,
282-
bids, tmpdir, dcmconfig)
282+
bids_options, tmpdir, dcmconfig)
283283

284-
bids_outfiles = save_converted_files(res, item_dicoms, bids,
284+
bids_outfiles = save_converted_files(res, item_dicoms, bids_options,
285285
outtype, prefix,
286286
outname_bids,
287287
overwrite=overwrite)
288288

289289
# save acquisition time information if it's BIDS
290290
# at this point we still have acquisition date
291-
if bids:
291+
if bids_options is not None:
292292
save_scans_key(item, bids_outfiles)
293293
# Fix up and unify BIDS files
294294
tuneup_bids_json_files(bids_outfiles)
@@ -310,7 +310,7 @@ def convert(items, converter, scaninfo_suffix, custom_callable, with_prov,
310310
elif not bids_outfiles:
311311
lgr.debug("No BIDS files were produced, nothing to embed to then")
312312
elif outname:
313-
embed_metadata_from_dicoms(bids, item_dicoms, outname, outname_bids,
313+
embed_metadata_from_dicoms(bids_options, item_dicoms, outname, outname_bids,
314314
prov_file, scaninfo, tempdirs, with_prov,
315315
min_meta)
316316
if scaninfo and op.exists(scaninfo):
@@ -326,16 +326,17 @@ def convert(items, converter, scaninfo_suffix, custom_callable, with_prov,
326326
custom_callable(*item)
327327

328328

329-
def convert_dicom(item_dicoms, bids, prefix,
329+
def convert_dicom(item_dicoms, bids_options, prefix,
330330
outdir, tempdirs, symlink, overwrite):
331331
"""Save DICOMs as output (default is by symbolic link)
332332
333333
Parameters
334334
----------
335335
item_dicoms : list of filenames
336336
DICOMs to save
337-
bids : bool
338-
Save to BIDS format
337+
bids_options : list or None
338+
If not None then save to BIDS format. List may be empty
339+
or contain bids specific options
339340
prefix : string
340341
Conversion outname
341342
outdir : string
@@ -352,7 +353,7 @@ def convert_dicom(item_dicoms, bids, prefix,
352353
-------
353354
None
354355
"""
355-
if bids:
356+
if bids_options is not None:
356357
# mimic the same hierarchy location as the prefix
357358
# although it could all have been done probably
358359
# within heuristic really
@@ -383,7 +384,7 @@ def convert_dicom(item_dicoms, bids, prefix,
383384
shutil.copyfile(filename, outfile)
384385

385386

386-
def nipype_convert(item_dicoms, prefix, with_prov, bids, tmpdir, dcmconfig=None):
387+
def nipype_convert(item_dicoms, prefix, with_prov, bids_options, tmpdir, dcmconfig=None):
387388
"""
388389
Converts DICOMs grouped from heuristic using Nipype's Dcm2niix interface.
389390
@@ -395,8 +396,9 @@ def nipype_convert(item_dicoms, prefix, with_prov, bids, tmpdir, dcmconfig=None)
395396
Heuristic output path
396397
with_prov : Bool
397398
Store provenance information
398-
bids : Bool
399-
Output BIDS sidecar JSONs
399+
bids_options : List or None
400+
If not None then output BIDS sidecar JSONs
401+
List may contain bids specific options
400402
tmpdir : Directory
401403
Conversion working directory
402404
dcmconfig : File (optional)
@@ -425,7 +427,7 @@ def nipype_convert(item_dicoms, prefix, with_prov, bids, tmpdir, dcmconfig=None)
425427
convertnode.inputs.terminal_output = 'allatonce'
426428
else:
427429
convertnode.terminal_output = 'allatonce'
428-
convertnode.inputs.bids_format = bids
430+
convertnode.inputs.bids_format = bids_options is not None
429431
eg = convertnode.run()
430432

431433
# prov information
@@ -439,7 +441,7 @@ def nipype_convert(item_dicoms, prefix, with_prov, bids, tmpdir, dcmconfig=None)
439441
return eg, prov_file
440442

441443

442-
def save_converted_files(res, item_dicoms, bids, outtype, prefix, outname_bids, overwrite):
444+
def save_converted_files(res, item_dicoms, bids_options, outtype, prefix, outname_bids, overwrite):
443445
"""Copy converted files from tempdir to output directory.
444446
Will rename files if necessary.
445447
@@ -449,8 +451,9 @@ def save_converted_files(res, item_dicoms, bids, outtype, prefix, outname_bids,
449451
Nipype conversion Node with results
450452
item_dicoms: list of filenames
451453
DICOMs converted
452-
bids : bool
453-
Option to save to BIDS
454+
bids : list or None
455+
If not list save to BIDS
456+
List may contain bids specific options
454457
prefix : string
455458
456459
Returns
@@ -481,7 +484,7 @@ def save_converted_files(res, item_dicoms, bids, outtype, prefix, outname_bids,
481484
# dwi etc which might spit out multiple files
482485

483486
suffixes = ([str(i+1) for i in range(len(res_files))]
484-
if bids else None)
487+
if (bids_options is not None) else None)
485488

486489
if not suffixes:
487490
lgr.warning("Following series files likely have "

heudiconv/dicoms.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -429,15 +429,15 @@ def embed_nifti(dcmfiles, niftifile, infofile, bids_info, min_meta):
429429
return niftifile, infofile
430430

431431

432-
def embed_metadata_from_dicoms(bids, item_dicoms, outname, outname_bids,
432+
def embed_metadata_from_dicoms(bids_options, item_dicoms, outname, outname_bids,
433433
prov_file, scaninfo, tempdirs, with_prov,
434434
min_meta):
435435
"""
436436
Enhance sidecar information file with more information from DICOMs
437437
438438
Parameters
439439
----------
440-
bids
440+
bids_options
441441
item_dicoms
442442
outname
443443
outname_bids
@@ -466,7 +466,7 @@ def embed_metadata_from_dicoms(bids, item_dicoms, outname, outname_bids,
466466
embedfunc.inputs.niftifile = op.abspath(outname)
467467
embedfunc.inputs.infofile = op.abspath(scaninfo)
468468
embedfunc.inputs.min_meta = min_meta
469-
embedfunc.inputs.bids_info = load_json(op.abspath(outname_bids)) if bids else None
469+
embedfunc.inputs.bids_info = load_json(op.abspath(outname_bids)) if (bids_options is not None) else None
470470
embedfunc.base_dir = tmpdir
471471
cwd = os.getcwd()
472472

heudiconv/tests/test_heuristics.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,23 @@ def test_scout_conversion(tmpdir):
150150
'ses-localizer/anat/sub-phantom1sid1_ses-localizer_scout.dicom.tgz'
151151
)
152152
)
153+
154+
155+
@pytest.mark.parametrize(
156+
'bidsoptions', [
157+
['notop'], [],
158+
])
159+
def test_notop(tmpdir, bidsoptions):
160+
tmppath = tmpdir.strpath
161+
args = (
162+
"-f reproin --files %s"
163+
% (TESTS_DATA_PATH)
164+
).split(' ') + ['-o', tmppath] + ['-b'] + bidsoptions
165+
runner(args)
166+
167+
assert op.exists(pjoin(tmppath, 'Halchenko/Yarik/950_bids_test4'))
168+
for fname in ['CHANGES', 'dataset_description.json', 'participants.tsv', 'README']:
169+
if 'notop' in bidsoptions:
170+
assert not op.exists(pjoin(tmppath, 'Halchenko/Yarik/950_bids_test4', fname))
171+
else:
172+
assert op.exists(pjoin(tmppath, 'Halchenko/Yarik/950_bids_test4', fname))

0 commit comments

Comments
 (0)