Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master' into reproin-part
Browse files Browse the repository at this point in the history
  • Loading branch information
tsalo committed Aug 8, 2024
2 parents 0ca3021 + a0a3635 commit 59bb2ec
Show file tree
Hide file tree
Showing 10 changed files with 57 additions and 32 deletions.
3 changes: 2 additions & 1 deletion .zenodo.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,6 @@
],
"access_right": "open",
"license": "Apache-2.0",
"upload_type": "software"
"upload_type": "software",
"title": "HeuDiConv — flexible DICOM conversion into structured directory layouts"
}
4 changes: 4 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

`a heuristic-centric DICOM converter`

.. image:: https://joss.theoj.org/papers/10.21105/joss.05839/status.svg
:target: https://doi.org/10.21105/joss.05839
:alt: JOSS Paper

.. image:: https://img.shields.io/badge/docker-nipy/heudiconv:latest-brightgreen.svg?logo=docker&style=flat
:target: https://hub.docker.com/r/nipy/heudiconv/tags/
:alt: Our Docker image
Expand Down
3 changes: 1 addition & 2 deletions docs/container.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Using heudiconv in a Container

If heudiconv is :ref:`installed via a Docker container <install_container>`, you
can run the commands in the following format::

docker run nipy/heudiconv:latest [heudiconv options]

So a user running via container would check the version with this command::
Expand Down Expand Up @@ -46,4 +46,3 @@ We typically recommend users make use of the following flags to Docker and Podma

* ``-it`` Interactive terminal
* ``--rm`` Remove the changes to the container when it completes

1 change: 0 additions & 1 deletion docs/custom-heuristic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -316,4 +316,3 @@ Suppose you want to use the values in the field ``image_type``? It is not a num
Note that this differs from testing for a string because you cannot test for any substring (e.g., 'TEST' would not work). String tests will not work on a tuple datatype.

.. Note:: *image_type* is described in the `DICOM specification <https://dicom.innolitics.com/ciods/mr-image/general-image/00080008>`_

3 changes: 3 additions & 0 deletions docs/heuristics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ The parameters that can be specified and the allowed options are defined in ``bi
- the corresponding modality image ``_acq-`` label for modalities other than ``func``
(e.g. ``_acq-XYZ42`` for ``dwi`` images)
- the corresponding image ``_task-`` label for the ``func`` modality (e.g. ``_task-XYZ42``)
* ``'PlainAcquisitionLabel'``: similar to ``'CustomAcquisitionLabel'``, but does not change
behavior for ``func`` modality and always bases decision on the ``_acq-`` label. Helps in
cases when there are multiple tasks and a shared ``fmap`` for some of them.
* ``'Force'``: forces ``heudiconv`` to consider any ``fmaps`` in the session to be
suitable for any image, no matter what the imaging parameters are.

Expand Down
1 change: 0 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,3 @@ Contents
commandline
container
api

26 changes: 13 additions & 13 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ Quickstart

This tutorial is based on `Dianne Patterson's University of Arizona tutorials <https://neuroimaging-core-docs.readthedocs.io/en/latest/pages/heudiconv.html#lesson-3-reproin-py>`_

This guide assumes you have already :ref:`installed heudiconv and dcm2niix <install_local>` and
This guide assumes you have already :ref:`installed heudiconv and dcm2niix <install_local>` and
demonstrates how to use the heudiconv tool with a provided `heuristic.py` to convert DICOMS into the BIDS data structure.

.. _prepare_dataset:

Prepare Dataset
***************

Download and unzip `sub-219_dicom.zip <https://datasets.datalad.org/?dir=/repronim/heudiconv-tutorial-example/>`_.
Download and unzip `sub-219_dicom.zip <https://datasets.datalad.org/?dir=/repronim/heudiconv-tutorial-example/>`_.

We will be working from a directory called MRIS. Under the MRIS directory is the *dicom* subdirectory: Under the subject number *219* the session *itbs* is nested. Each dicom sequence folder is nested under the session::

Expand All @@ -29,7 +29,7 @@ We will be working from a directory called MRIS. Under the MRIS directory is the
├── field_mapping_21
└── restingstate_18
Nifti
└── code
└── code
└── heuristic1.py

Basic Conversion
Expand Down Expand Up @@ -57,36 +57,36 @@ Run the following command::

Output
******

The *Nifti* directory will contain a bids-compliant subject directory::


└── sub-219
└── ses-itbs
├── anat
├── dwi
├── fmap
└── func

The following required BIDS text files are also created in the Nifti directory. Details for filling in these skeleton text files can be found under `tabular files <https://bids-specification.readthedocs.io/en/stable/02-common-principles.html#tabular-files>`_ in the BIDS specification::

CHANGES
README
dataset_description.json
participants.json
participants.tsv
task-rest_bold.json

Validation
**********

Ensure that everything is according to spec by using `bids validator <https://bids-standard.github.io/bids-validator/>`_
Ensure that everything is according to spec by using `bids validator <https://bids-standard.github.io/bids-validator/>`_

Click `Choose File` and then select the *Nifti* directory. There should be no errors (though there are a couple of warnings).

.. Note:: Your files are not uploaded to the BIDS validator, so there are no privacy concerns!
Next

Next
****

In the following sections, you will modify *heuristic.py* yourself so you can test different options and understand how to work with your own data.
5 changes: 5 additions & 0 deletions heudiconv/bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class BIDSError(Exception):
"ImagingVolume",
"ModalityAcquisitionLabel",
"CustomAcquisitionLabel",
"PlainAcquisitionLabel",
"Force",
]
# Key info returned by get_key_info_for_fmap_assignment when
Expand Down Expand Up @@ -755,6 +756,10 @@ def get_key_info_for_fmap_assignment(
custom_label = BIDSFile.parse(op.basename(json_file))["acq"]
# Get the custom acquisition label, acq_label is None if no custom field found
key_info = [custom_label]
elif matching_parameter == "PlainAcquisitionLabel":
# always base the decision on <acq> label
plain_label = BIDSFile.parse(op.basename(json_file))["acq"]
key_info = [plain_label]
elif matching_parameter == "Force":
# We want to force the matching, so just return some string
# regardless of the image
Expand Down
25 changes: 11 additions & 14 deletions heudiconv/heuristics/reproin.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,8 @@ def fix_canceled_runs(seqinfo: list[SeqInfo]) -> list[SeqInfo]:
"""Function that adds cancelme_ to known bad runs which were forgotten"""
if not fix_accession2run:
return seqinfo # nothing to do
for i, s in enumerate(seqinfo):
accession_number = s.accession_number
for i, curr_seqinfo in enumerate(seqinfo):
accession_number = curr_seqinfo.accession_number
if accession_number and accession_number in fix_accession2run:
lgr.info(
"Considering some runs possibly marked to be "
Expand All @@ -292,12 +292,12 @@ def fix_canceled_runs(seqinfo: list[SeqInfo]) -> list[SeqInfo]:
# a single accession, but left as is for now
badruns = fix_accession2run[accession_number]
badruns_pattern = "|".join(badruns)
if re.match(badruns_pattern, s.series_id):
lgr.info("Fixing bad run {0}".format(s.series_id))
if re.match(badruns_pattern, curr_seqinfo.series_id):
lgr.info("Fixing bad run {0}".format(curr_seqinfo.series_id))
fixedkwargs = dict()
for key in series_spec_fields:
fixedkwargs[key] = "cancelme_" + getattr(s, key)
seqinfo[i] = s._replace(**fixedkwargs)
fixedkwargs[key] = "cancelme_" + getattr(curr_seqinfo, key)
seqinfo[i] = curr_seqinfo._replace(**fixedkwargs)
return seqinfo


Expand Down Expand Up @@ -341,19 +341,19 @@ def _apply_substitutions(
seqinfo: list[SeqInfo], substitutions: list[tuple[str, str]], subs_scope: str
) -> None:
lgr.info("Considering %s substitutions", subs_scope)
for i, s in enumerate(seqinfo):
for i, curr_seqinfo in enumerate(seqinfo):
fixed_kwargs = dict()
# need to replace both protocol_name series_description
for key in series_spec_fields:
oldvalue = value = getattr(s, key)
oldvalue = value = getattr(curr_seqinfo, key)
# replace all I need to replace
for substring, replacement in substitutions:
value = re.sub(substring, replacement, value)
if oldvalue != value:
lgr.info(" %s: %r -> %r", key, oldvalue, value)
fixed_kwargs[key] = value
# namedtuples are immutable
seqinfo[i] = s._replace(**fixed_kwargs)
seqinfo[i] = curr_seqinfo._replace(**fixed_kwargs)


def fix_seqinfo(seqinfo: list[SeqInfo]) -> list[SeqInfo]:
Expand Down Expand Up @@ -588,10 +588,7 @@ def infotodict(
# XXX if we have a known earlier study, we need to always
# increase the run counter for phasediff because magnitudes
# were not acquired
if (
get_study_hash([curr_seqinfo])
== "9d148e2a05f782273f6343507733309d"
):
if get_study_hash([curr_seqinfo]) == "9d148e2a05f782273f6343507733309d":
current_run += 1
else:
raise RuntimeError(
Expand Down Expand Up @@ -808,7 +805,7 @@ def infotoids(seqinfos: Iterable[SeqInfo], outdir: str) -> dict[str, Optional[st

# So -- use `outdir` and locator etc to see if for a given locator/subject
# and possible ses+ in the sequence names, so we would provide a sequence
# So might need to go through parse_series_spec(s.protocol_name)
# So might need to go through parse_series_spec(curr_seqinfo.protocol_name)
# to figure out presence of sessions.
ses_markers: list[str] = []

Expand Down
18 changes: 18 additions & 0 deletions heudiconv/tests/test_bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,24 @@ def test_get_key_info_for_fmap_assignment(
json_name, matching_parameter="CustomAcquisitionLabel"
)

# 7) matching_parameter = 'PlainAcquisitionLabel'
A_LABEL = gen_rand_label(label_size, label_seed)
for d in ["fmap", "func", "dwi", "anat"]:
(tmp_path / d).mkdir(parents=True, exist_ok=True)

for dirname, fname, expected_key_info in [
("fmap", f"sub-foo_acq-{A_LABEL}_epi.json", A_LABEL),
("func", f"sub-foo_task-foo_acq-{A_LABEL}_bold.json", A_LABEL),
("func", f"sub-foo_task-bar_acq-{A_LABEL}_bold.json", A_LABEL),
("dwi", f"sub-foo_acq-{A_LABEL}_dwi.json", A_LABEL),
("anat", f"sub-foo_acq-{A_LABEL}_T1w.json", A_LABEL),
]:
json_name = op.join(tmp_path, dirname, fname)
save_json(json_name, {SHIM_KEY: A_SHIM})
assert [expected_key_info] == get_key_info_for_fmap_assignment(
json_name, matching_parameter="PlainAcquisitionLabel"
)

# Finally: invalid matching_parameters:
assert (
get_key_info_for_fmap_assignment(json_name, matching_parameter="Invalid") == []
Expand Down

0 comments on commit 59bb2ec

Please sign in to comment.