Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENH, REF] Add uncombined support and modularize multi-file renamers #424

Merged
merged 13 commits into from
May 13, 2020
Merged
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,11 @@ TODO Summary
[#434]: https://github.com/nipy/heudiconv/issues/434
[#436]: https://github.com/nipy/heudiconv/issues/436
[#437]: https://github.com/nipy/heudiconv/issues/437
[#425]: https://github.com/nipy/heudiconv/issues/425
[#420]: https://github.com/nipy/heudiconv/issues/420
[#425]: https://github.com/nipy/heudiconv/issues/425
[#430]: https://github.com/nipy/heudiconv/issues/430
[#432]: https://github.com/nipy/heudiconv/issues/432
[#434]: https://github.com/nipy/heudiconv/issues/434
[#436]: https://github.com/nipy/heudiconv/issues/436
[#437]: https://github.com/nipy/heudiconv/issues/437
16 changes: 8 additions & 8 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,32 @@ Installation

Local
=====
Released versions of HeuDiConv are available on `PyPI <https://pypi.org/project/heudiconv/>`_
and `conda <https://github.com/conda-forge/heudiconv-feedstock#installing-heudiconv>`_.
Released versions of HeuDiConv are available on `PyPI <https://pypi.org/project/heudiconv/>`_
and `conda <https://github.com/conda-forge/heudiconv-feedstock#installing-heudiconv>`_.
If installing through ``PyPI``, eg::

pip install heudiconv[all]

Manual installation of `dcm2niix <https://github.com/rordenlab/dcm2niix#install>`_
Manual installation of `dcm2niix <https://github.com/rordenlab/dcm2niix#install>`_
is required.

On Debian-based systems we recommend using `NeuroDebian <http://neuro.debian.net>`_
On Debian-based systems we recommend using `NeuroDebian <http://neuro.debian.net>`_
which provides the `heudiconv package <http://neuro.debian.net/pkgs/heudiconv.html>`_.


Docker
======
If `Docker <https://docs.docker.com/install/>`_ is available on your system, you
can visit `our page on Docker Hub <https://hub.docker.com/r/nipy/heudiconv/tags>`_
If `Docker <https://docs.docker.com/install/>`_ is available on your system, you
can visit `our page on Docker Hub <https://hub.docker.com/r/nipy/heudiconv/tags>`_
to view available releases. To pull the latest release, run::

$ docker pull nipy/heudiconv:0.8.0


Singularity
===========
If `Singularity <https://www.sylabs.io/singularity/>`_ is available on your system,
you can use it to pull and convert our Docker images! For example, to pull and
If `Singularity <https://www.sylabs.io/singularity/>`_ is available on your system,
you can use it to pull and convert our Docker images! For example, to pull and
build the latest release, you can run::

$ singularity pull docker://nipy/heudiconv:0.8.0
245 changes: 178 additions & 67 deletions heudiconv/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def conversion_info(subject, outdir, info, filegroup, ses):


def prep_conversion(sid, dicoms, outdir, heuristic, converter, anon_sid,
anon_outdir, with_prov, ses, bids_options, seqinfo,
anon_outdir, with_prov, ses, bids_options, seqinfo,
min_meta, overwrite, dcmconfig, grouping):
if dicoms:
lgr.info("Processing %d dicoms", len(dicoms))
Expand Down Expand Up @@ -233,6 +233,157 @@ def prep_conversion(sid, dicoms, outdir, heuristic, converter, anon_sid,
getattr(heuristic, 'DEFAULT_FIELDS', {}))


def update_complex_name(metadata, filename, suffix):
"""
Insert `_rec-<magnitude|phase>` entity into filename if data are from a
sequence with magnitude/phase part.

Parameters
----------
metadata : dict
Scan metadata dictionary from BIDS sidecar file.
filename : str
Incoming filename
suffix : str
An index used for cases where a single scan produces multiple files,
but the differences between those files are unknown.

Returns
-------
filename : str
Updated filename with rec entity added in appropriate position.
"""
# Some scans separate magnitude/phase differently
unsupported_types = ['_bold', '_phase',
'_magnitude', '_magnitude1', '_magnitude2',
'_phasediff', '_phase1', '_phase2']
if any(ut in filename for ut in unsupported_types):
return filename

# Check to see if it is magnitude or phase part:
if 'M' in metadata.get('ImageType'):
mag_or_phase = 'magnitude'
elif 'P' in metadata.get('ImageType'):
mag_or_phase = 'phase'
else:
mag_or_phase = suffix

# Determine scan suffix
filetype = '_' + filename.split('_')[-1]

# Insert rec label
if not ('_rec-%s' % mag_or_phase) in filename:
# If "_rec-" is specified, prepend the 'mag_or_phase' value.
if '_rec-' in filename:
raise BIDSError(
"Reconstruction label for images will be automatically set, "
"remove from heuristic"
)

# Insert it **before** the following string(s), whichever appears first.
for label in ['_dir', '_run', '_mod', '_echo', '_recording', '_proc', '_space', filetype]:
if (label == filetype) or (label in filename):
filename = filename.replace(
label, "_rec-%s%s" % (mag_or_phase, label)
)
break

return filename


def update_multiecho_name(metadata, filename, echo_times):
"""
Insert `_echo-<num>` entity into filename if data are from a multi-echo
sequence.

Parameters
----------
metadata : dict
Scan metadata dictionary from BIDS sidecar file.
filename : str
Incoming filename
echo_times : list
List of all echo times from scan. Used to determine the echo *number*
(i.e., index) if field is missing from metadata.

Returns
-------
filename : str
Updated filename with echo entity added, if appropriate.
"""
# Field maps separate echoes differently
unsupported_types = [
'_magnitude', '_magnitude1', '_magnitude2',
'_phasediff', '_phase1', '_phase2', '_fieldmap'
]
if any(ut in filename for ut in unsupported_types):
return filename

# Get the EchoNumber from json file info. If not present, use EchoTime
if 'EchoNumber' in metadata.keys():
echo_number = metadata['EchoNumber']
else:
echo_number = echo_times.index(metadata['EchoTime']) + 1

# Determine scan suffix
filetype = '_' + filename.split('_')[-1]

# Insert it **before** the following string(s), whichever appears first.
for label in ['_recording', '_proc', '_space', filetype]:
if (label == filetype) or (label in filename):
filename = filename.replace(
label, "_echo-%s%s" % (echo_number, label)
)
break

return filename


def update_uncombined_name(metadata, filename, channel_names):
"""
Insert `_ch-<num>` entity into filename if data are from a sequence
with "save uncombined".

Parameters
----------
metadata : dict
Scan metadata dictionary from BIDS sidecar file.
filename : str
Incoming filename
channel_names : list
List of all channel names from scan. Used to determine the channel
*number* (i.e., index) if field is missing from metadata.

Returns
-------
filename : str
Updated filename with ch entity added, if appropriate.
"""
# In case any scan types separate channels differently
unsupported_types = []
if any(ut in filename for ut in unsupported_types):
return filename

# Determine the channel number
channel_number = ''.join([c for c in metadata['CoilString'] if c.isdigit()])
if not channel_number:
channel_number = channel_names.index(metadata['CoilString']) + 1
channel_number = str(channel_number).zfill(2)

# Determine scan suffix
filetype = '_' + filename.split('_')[-1]

# Insert it **before** the following string(s), whichever appears first.
# Choosing to put channel near the end since it's not in the specification yet.
for label in ['_recording', '_proc', '_space', filetype]:
if (label == filetype) or (label in filename):
filename = filename.replace(
label, "_ch-%s%s" % (channel_number, label)
)
break
return filename


def convert(items, converter, scaninfo_suffix, custom_callable, with_prov,
bids_options, outdir, min_meta, overwrite, symlink=True, prov_file=None,
dcmconfig=None):
Expand Down Expand Up @@ -534,14 +685,17 @@ def save_converted_files(res, item_dicoms, bids_options, outtype, prefix, outnam
# series. To do that, the most straightforward way is to read the
# echo times for all bids_files and see if they are all the same or not.

# Check for varying echo times
echo_times = sorted(list(set(
b.get('EchoTime', nan)
for b in bids_metas
if b
)))

is_multiecho = len(echo_times) > 1
# Collect some metadata across all images
echo_times, channel_names, image_types = set(), set(), set()
for metadata in bids_metas:
if not metadata:
continue
echo_times.add(metadata.get('EchoTime', nan))
channel_names.add(metadata.get('CoilString', nan))
image_types.update(metadata.get('ImageType', [nan]))
is_multiecho = len(set(filter(bool, echo_times))) > 1 # Check for varying echo times
is_uncombined = len(set(filter(bool, channel_names))) > 1 # Check for uncombined data
is_complex = 'M' in image_types and 'P' in image_types # Determine if data are complex (magnitude + phase)

### Loop through the bids_files, set the output name and save files
for fl, suffix, bids_file, bids_meta in zip(res_files, suffixes, bids_files, bids_metas):
Expand All @@ -552,65 +706,22 @@ def save_converted_files(res, item_dicoms, bids_options, outtype, prefix, outnam
# and we don't want to modify it for all the bids_files):
this_prefix_basename = prefix_basename

# _sbref sequences reconstructing magnitude and phase generate
# two NIfTI files IN THE SAME SERIES, so we cannot just add
# the suffix, if we want to be bids compliant:
if bids_meta and this_prefix_basename.endswith('_sbref') \
and len(suffixes) > len(echo_times):
if len(suffixes) != len(echo_times)*2:
lgr.warning(
"Got %d suffixes for %d echo times, which isn't "
"multiple of two as if it was magnitude + phase pairs",
len(suffixes), len(echo_times)
# Update name for certain criteria
if bids_file:
if is_multiecho:
this_prefix_basename = update_multiecho_name(
bids_meta, this_prefix_basename, echo_times
)

if is_complex:
this_prefix_basename = update_complex_name(
bids_meta, this_prefix_basename, suffix
)

if is_uncombined:
this_prefix_basename = update_uncombined_name(
bids_meta, this_prefix_basename, channel_names
)
# Check to see if it is magnitude or phase reconstruction:
if 'M' in bids_meta.get('ImageType'):
mag_or_phase = 'magnitude'
elif 'P' in bids_meta.get('ImageType'):
mag_or_phase = 'phase'
else:
mag_or_phase = suffix

# Insert reconstruction label
if not ("_rec-%s" % mag_or_phase) in this_prefix_basename:

# If "_rec-" is specified, prepend the 'mag_or_phase' value.
if ('_rec-' in this_prefix_basename):
raise BIDSError(
"Reconstruction label for multi-echo single-band"
" reference images will be automatically set, remove"
" from heuristic"
)

# If not, insert "_rec-" + 'mag_or_phase' into the prefix_basename
# **before** "_run", "_echo" or "_sbref", whichever appears first:
for label in ['_run', '_echo', '_sbref']:
if (label in this_prefix_basename):
this_prefix_basename = this_prefix_basename.replace(
label, "_rec-%s%s" % (mag_or_phase, label)
)
break

# Now check if this run is multi-echo
# (Note: it can be _sbref and multiecho, so don't use "elif"):
# For multi-echo sequences, we have to specify the echo number in
# the file name:
if bids_meta and is_multiecho:
# Get the EchoNumber from json file info. If not present, use EchoTime
if 'EchoNumber' in bids_meta:
echo_number = bids_meta['EchoNumber']
else:
echo_number = echo_times.index(bids_meta['EchoTime']) + 1

supported_multiecho = ['_bold', '_phase', '_epi', '_sbref', '_T1w', '_PDT2']
# Now, decide where to insert it.
# Insert it **before** the following string(s), whichever appears first.
for imgtype in supported_multiecho:
if (imgtype in this_prefix_basename):
this_prefix_basename = this_prefix_basename.replace(
imgtype, "_echo-%d%s" % (echo_number, imgtype)
)
break

# Fallback option:
# If we have failed to modify this_prefix_basename, because it didn't fall
Expand Down
Loading