Skip to content

Commit f47b637

Browse files
committed
Merge branch 'master' of https://github.com/nipy/heudiconv into multiecho
2 parents 621a7c4 + b9b5e0d commit f47b637

30 files changed

+396
-209
lines changed

.coveragerc

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
[run]
2-
include = tests/*
3-
heudiconv/*
2+
include = heudiconv/*
43
setup.py

.gitignore

100644100755
File mode changed.

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ install:
3737
- git config --global user.name "Travis Almighty"
3838

3939
script:
40-
- coverage run `which py.test` -s -v tests heudiconv/heuristics/*.py
40+
- coverage run `which py.test` -s -v heudiconv
4141

4242
after_success:
4343
- codecov

CHANGELOG.md

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,45 @@ All notable changes to this project will be documented (for humans) in this file
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7-
## [0.5.2] - Date
7+
## [0.5.3] - Date
88

99
TODO Summary
1010

1111
### Added
12-
### Changed
1312

14-
- Reproin heuristic: `__dup` indices would now be assigned incrementally
15-
individually per each sequence, so there is a chance to properly treat
16-
associate for multi-file (e.g. `fmap`) sequences
13+
### Changed
1714

1815
### Deprecated
16+
1917
### Fixed
18+
2019
### Removed
20+
2121
### Security
2222

23+
## [0.5.2] - 2019-01-04
24+
25+
A variety of bugfixes
26+
27+
### Changed
28+
- Reproin heuristic: `__dup` indices would now be assigned incrementally
29+
individually per each sequence, so there is a chance to properly treat
30+
associate for multi-file (e.g. `fmap`) sequences
31+
- Reproin heuristic: also split StudyDescription by space not only by ^
32+
- `tests/` moved under `heudiconv/tests` to ease maintenance and facilitate
33+
testing of an installed heudiconv
34+
- Protocol name will also be accessed from private Siemens
35+
csa.tProtocolName header field if not present in public one
36+
- nipype>=0.12.0 is required now
37+
38+
### Fixed
39+
- Multiple files produced by dcm2niix are first sorted to guarantee
40+
correct order e.g. of magnitude files in fieldmaps, which otherwise
41+
resulted in incorrect according to BIDS ordering of them
42+
- Aggregated top level .json files now would contain only the fields
43+
with the same values from all scanned files. In prior versions,
44+
those files were not regenerated after an initial conversion
45+
- Unicode handling in anonimization scripts
2346

2447
## [0.5.1] - 2018-07-05
2548
Bugfix release

Dockerfile

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
# Generated by Neurodocker version 0.4.1-28-g83dbc15
2-
# Timestamp: 2018-11-01 22:00:14 UTC
1+
# Generated by Neurodocker version 0.4.2-3-gf7055a1
2+
# Timestamp: 2018-11-13 22:04:04 UTC
33
#
44
# Thank you for using Neurodocker. If you discover any issues
55
# or ways to improve this software, please submit an issue or
@@ -70,6 +70,7 @@ RUN apt-get update -qq \
7070
liblzma-dev \
7171
libc-dev \
7272
git-annex-standalone \
73+
netbase \
7374
&& apt-get clean \
7475
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
7576

@@ -89,15 +90,16 @@ RUN export PATH="/opt/miniconda-latest/bin:$PATH" \
8990
&& conda config --system --set show_channel_urls true \
9091
&& sync && conda clean -tipsy && sync \
9192
&& conda install -y -q --name base \
92-
python=3.6 \
93-
traits>=4.6.0 \
94-
scipy \
95-
numpy \
96-
nomkl \
93+
'python=3.6' \
94+
'traits>=4.6.0' \
95+
'scipy' \
96+
'numpy' \
97+
'pandas' \
98+
'nomkl' \
9799
&& sync && conda clean -tipsy && sync \
98100
&& bash -c "source activate base \
99101
&& pip install --no-cache-dir --editable \
100-
/src/heudiconv[all]" \
102+
'/src/heudiconv[all]'" \
101103
&& rm -rf ~/.cache/pip/* \
102104
&& sync
103105

@@ -125,7 +127,8 @@ RUN echo '{ \
125127
\n "pigz", \
126128
\n "liblzma-dev", \
127129
\n "libc-dev", \
128-
\n "git-annex-standalone" \
130+
\n "git-annex-standalone", \
131+
\n "netbase" \
129132
\n ] \
130133
\n ], \
131134
\n [ \
@@ -144,6 +147,7 @@ RUN echo '{ \
144147
\n "traits>=4.6.0", \
145148
\n "scipy", \
146149
\n "numpy", \
150+
\n "pandas", \
147151
\n "nomkl" \
148152
\n ], \
149153
\n "pip_install": [ \

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright [2014-2018] [Heudiconv developers]
1+
Copyright [2014-2019] [Heudiconv developers]
22

33
Licensed under the Apache License, Version 2.0 (the "License");
44
you may not use this file except in compliance with the License.

heudiconv/bids.py

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,37 @@ def populate_bids_templates(path, defaults={}):
7474
"TODO: Provide description for the dataset -- basic details about the "
7575
"study, possibly pointing to pre-registration (if public or embargoed)")
7676

77+
populate_aggregated_jsons(path)
78+
79+
80+
def populate_aggregated_jsons(path):
81+
"""Aggregate across the entire BIDS dataset .json's into top level .json's
82+
83+
Top level .json files would contain only the fields which are
84+
common to all subject[/session]/type/*_modality.json's.
85+
86+
ATM aggregating only for *_task*_bold.json files. Only the task- and
87+
OPTIONAL _acq- field is retained within the aggregated filename. The other
88+
BIDS _key-value pairs are "aggregated over".
89+
90+
Parameters
91+
----------
92+
path: str
93+
Path to the top of the BIDS dataset
94+
"""
7795
# TODO: collect all task- .json files for func files to
7896
tasks = {}
7997
# way too many -- let's just collect all which are the same!
8098
# FIELDS_TO_TRACK = {'RepetitionTime', 'FlipAngle', 'EchoTime',
8199
# 'Manufacturer', 'SliceTiming', ''}
82100
for fpath in find_files('.*_task-.*\_bold\.json', topdir=path,
83-
exclude_vcs=True, exclude="/\.(datalad|heudiconv)/"):
101+
exclude_vcs=True,
102+
exclude="/\.(datalad|heudiconv)/"):
103+
#
104+
# According to BIDS spec I think both _task AND _acq (may be more?
105+
# _rec, _dir, ...?) should be retained?
106+
# TODO: if we are to fix it, then old ones (without _acq) should be
107+
# removed first
84108
task = re.sub('.*_(task-[^_\.]*(_acq-[^_\.]*)?)_.*', r'\1', fpath)
85109
json_ = load_json(fpath)
86110
if task not in tasks:
@@ -115,18 +139,36 @@ def populate_bids_templates(path, defaults={}):
115139
if not op.lexists(events_file):
116140
lgr.debug("Generating %s", events_file)
117141
with open(events_file, 'w') as f:
118-
f.write("onset\tduration\ttrial_type\tresponse_time\tstim_file\tTODO -- fill in rows and add more tab-separated columns if desired")
142+
f.write(
143+
"onset\tduration\ttrial_type\tresponse_time\tstim_file"
144+
"\tTODO -- fill in rows and add more tab-separated "
145+
"columns if desired")
119146
# extract tasks files stubs
120147
for task_acq, fields in tasks.items():
121148
task_file = op.join(path, task_acq + '_bold.json')
122-
# do not touch any existing thing, it may be precious
123-
if not op.lexists(task_file):
124-
lgr.debug("Generating %s", task_file)
125-
fields["TaskName"] = ("TODO: full task name for %s" %
126-
task_acq.split('_')[0].split('-')[1])
127-
fields["CogAtlasID"] = "TODO"
128-
with open(task_file, 'w') as f:
129-
f.write(json_dumps_pretty(fields, indent=2, sort_keys=True))
149+
# Since we are pulling all unique fields we have to possibly
150+
# rewrite this file to guarantee consistency.
151+
# See https://github.com/nipy/heudiconv/issues/277 for a usecase/bug
152+
# when we didn't touch existing one.
153+
# But the fields we enter (TaskName and CogAtlasID) might need need
154+
# to be populated from the file if it already exists
155+
placeholders = {
156+
"TaskName": ("TODO: full task name for %s" %
157+
task_acq.split('_')[0].split('-')[1]),
158+
"CogAtlasID": "TODO",
159+
}
160+
if op.lexists(task_file):
161+
j = load_json(task_file)
162+
# Retain possibly modified placeholder fields
163+
for f in placeholders:
164+
if f in j:
165+
placeholders[f] = j[f]
166+
act = "Regenerating"
167+
else:
168+
act = "Generating"
169+
lgr.debug("%s %s", act, task_file)
170+
fields.update(placeholders)
171+
save_json(task_file, fields, indent=2, sort_keys=True, pretty=True)
130172

131173

132174
def tuneup_bids_json_files(json_files):

heudiconv/convert.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def prep_conversion(sid, dicoms, outdir, heuristic, converter, anon_sid,
101101
anon_outdir = outdir
102102

103103
# Generate heudiconv info folder
104-
idir = op.join(outdir, '.heudiconv', sid)
104+
idir = op.join(outdir, '.heudiconv', anon_sid)
105105
if bids and ses:
106106
idir = op.join(idir, 'ses-%s' % str(ses))
107107
if anon_outdir == outdir:
@@ -458,6 +458,7 @@ def save_converted_files(res, item_dicoms, bids, outtype, prefix, outname_bids,
458458
safe_copyfile(res.outputs.bvals, outname_bvals, overwrite)
459459

460460
if isinstance(res_files, list):
461+
res_files = sorted(res_files)
461462
# we should provide specific handling for fmap,
462463
# dwi etc which might spit out multiple files
463464

@@ -473,7 +474,7 @@ def save_converted_files(res, item_dicoms, bids, outtype, prefix, outname_bids,
473474

474475
# Also copy BIDS files although they might need to
475476
# be merged/postprocessed later
476-
bids_files = (res.outputs.bids
477+
bids_files = sorted(res.outputs.bids
477478
if len(res.outputs.bids) == len(res_files)
478479
else [None] * len(res_files))
479480

heudiconv/dicoms.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import logging
55
from collections import OrderedDict
66
import tarfile
7-
7+
from nibabel.nicom import csareader
88
from heudiconv.external.pydicom import dcm
99

1010
from .utils import SeqInfo, load_json, set_readonly
@@ -73,6 +73,15 @@ def group_dicoms_into_seqinfos(files, file_filter, dcmfilter, grouping):
7373
lgr.info("File {} is missing any StudyInstanceUID".format(filename))
7474
file_studyUID = None
7575

76+
# Workaround for protocol name in private siemens csa header
77+
try:
78+
mw.dcm_data.ProtocolName
79+
except AttributeError:
80+
if not getattr(mw.dcm_data, 'ProtocolName', '').strip():
81+
mw.dcm_data.ProtocolName = parse_private_csa_header(
82+
mw.dcm_data, 'ProtocolName', 'tProtocolName'
83+
) if mw.is_csa else ''
84+
7685
try:
7786
series_id = (int(mw.dcm_data.SeriesNumber),
7887
mw.dcm_data.ProtocolName)
@@ -208,7 +217,7 @@ def group_dicoms_into_seqinfos(files, file_filter, dcmfilter, grouping):
208217
dcminfo.get('PatientID'),
209218
dcminfo.get('StudyDescription'),
210219
refphys,
211-
dcminfo.get('SeriesDescription'),
220+
series_desc, # We try to set this further up.
212221
sequence_name,
213222
image_type,
214223
accession_number,
@@ -232,7 +241,7 @@ def group_dicoms_into_seqinfos(files, file_filter, dcmfilter, grouping):
232241
lgr.debug("%30s %30s %27s %27s %5s nref=%-2d nsrc=%-2d %s" % (
233242
key,
234243
info.series_id,
235-
dcminfo.SeriesDescription,
244+
series_desc,
236245
dcminfo.ProtocolName,
237246
info.is_derived,
238247
len(dcminfo.get('ReferencedImageSequence', '')),
@@ -483,3 +492,37 @@ def embed_metadata_from_dicoms(bids, item_dicoms, outname, outname_bids,
483492
except Exception as exc:
484493
lgr.error("Embedding failed: %s", str(exc))
485494
os.chdir(cwd)
495+
496+
def parse_private_csa_header(dcm_data, public_attr, private_attr, default=None):
497+
"""
498+
Parses CSA header in cases where value is not defined publicly
499+
500+
Parameters
501+
----------
502+
dcm_data : pydicom Dataset object
503+
DICOM metadata
504+
public_attr : string
505+
non-private DICOM attribute
506+
private_attr : string
507+
private DICOM attribute
508+
default (optional)
509+
default value if private_attr not found
510+
511+
Returns
512+
-------
513+
val (default: empty string)
514+
private attribute value or default
515+
"""
516+
# TODO: provide mapping to private_attr from public_attr
517+
from nibabel.nicom import csareader
518+
import dcmstack.extract as dsextract
519+
try:
520+
# TODO: test with attr besides ProtocolName
521+
csastr = csareader.get_csa_header(dcm_data, 'series')['tags']['MrPhoenixProtocol']['items'][0]
522+
csastr = csastr.replace("### ASCCONV BEGIN", "### ASCCONV BEGIN ### ")
523+
parsedhdr = dsextract.parse_phoenix_prot('MrPhoenixProtocol', csastr)
524+
val = parsedhdr[private_attr].replace(' ', '')
525+
except Exception as e:
526+
lgr.debug("Failed to parse CSA header: %s", str(e))
527+
val = default if default else ''
528+
return val

0 commit comments

Comments
 (0)