Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master' into fix/xa_pulse_seq_…
Browse files Browse the repository at this point in the history
…name
  • Loading branch information
bpinsard committed Sep 10, 2024
2 parents 49c5783 + a0a3635 commit 687b1fc
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 53 deletions.
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>`_

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.
77 changes: 41 additions & 36 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 @@ -402,32 +402,34 @@ def infotodict(
run_label: Optional[str] = None # run-
dcm_image_iod_spec: Optional[str] = None
skip_derived = False
for s in seqinfo:
for curr_seqinfo in seqinfo:
# XXX: skip derived sequences, we don't store them to avoid polluting
# the directory, unless it is the motion corrected ones
# (will get _rec-moco suffix)
if skip_derived and s.is_derived and not s.is_motion_corrected:
skipped.append(s.series_id)
lgr.debug("Ignoring derived data %s", s.series_id)
if skip_derived and curr_seqinfo.is_derived and not curr_seqinfo.is_motion_corrected:
skipped.append(curr_seqinfo.series_id)
lgr.debug("Ignoring derived data %s", curr_seqinfo.series_id)
continue

# possibly apply present formatting in the series_description or protocol name
for f in "series_description", "protocol_name":
s = s._replace(**{f: getattr(s, f).format(**s._asdict())})
curr_seqinfo = curr_seqinfo._replace(
**{f: getattr(curr_seqinfo, f).format(**curr_seqinfo._asdict())}
)

template = None
suffix = ""
# seq = []

# figure out type of image from s.image_info -- just for checking ATM
# figure out type of image from curr_seqinfo.image_info -- just for checking ATM
# since we primarily rely on encoded in the protocol name information
prev_dcm_image_iod_spec = dcm_image_iod_spec
if len(s.image_type) > 2:
if len(curr_seqinfo.image_type) > 2:
# https://dicom.innolitics.com/ciods/cr-image/general-image/00080008
# 0 - ORIGINAL/DERIVED
# 1 - PRIMARY/SECONDARY
# 3 - Image IOD specific specialization (optional)
dcm_image_iod_spec = s.image_type[2]
dcm_image_iod_spec = curr_seqinfo.image_type[2]
image_type_datatype = {
# Note: P and M are too generic to make a decision here, could be
# for different datatypes (bold, fmap, etc)
Expand All @@ -443,7 +445,7 @@ def infotodict(

series_info = {} # For please lintian and its friends
for sfield in series_spec_fields:
svalue = getattr(s, sfield)
svalue = getattr(curr_seqinfo, sfield)
series_info = parse_series_spec(svalue)
if series_info: # looks like a valid spec - we are done
series_spec = svalue
Expand All @@ -454,10 +456,10 @@ def infotodict(
if not series_info:
series_spec = None # we cannot know better
lgr.warning(
"Could not determine the series name by looking at " "%s fields",
"Could not determine the series name by looking at %s fields",
", ".join(series_spec_fields),
)
skipped_unknown.append(s.series_id)
skipped_unknown.append(curr_seqinfo.series_id)
continue

if dcm_image_iod_spec and dcm_image_iod_spec.startswith("MIP"):
Expand All @@ -476,14 +478,14 @@ def infotodict(
series_spec,
)

# if s.is_derived:
# if curr_seqinfo.is_derived:
# # Let's for now stash those close to original images
# # TODO: we might want a separate tree for all of this!?
# # so more of a parameter to the create_key
# #datatype += '/derivative'
# # just keep it lower case and without special characters
# # XXXX what for???
# #seq.append(s.series_description.lower())
# #seq.append(curr_seqinfo.series_description.lower())
# prefix = os.path.join('derivatives', 'scanner')
# else:
# prefix = ''
Expand All @@ -493,14 +495,14 @@ def infotodict(
# Figure out the datatype_suffix (BIDS _suffix)
#
# If none was provided -- let's deduce it from the information we find:
# analyze s.protocol_name (series_id is based on it) for full name mapping etc
# analyze curr_seqinfo.protocol_name (series_id is based on it) for full name mapping etc
if not datatype_suffix:
if datatype == "func":
if "_pace_" in series_spec:
datatype_suffix = "pace" # or should it be part of seq-
elif "P" in s.image_type:
elif "P" in curr_seqinfo.image_type:
datatype_suffix = "phase"
elif "M" in s.image_type:
elif "M" in curr_seqinfo.image_type:
datatype_suffix = "bold"
else:
# assume bold by default
Expand All @@ -526,7 +528,7 @@ def infotodict(
# since they are complementary files produced along-side with original
# ones.
#
if s.series_description.endswith("_SBRef"):
if curr_seqinfo.series_description.endswith("_SBRef"):
datatype_suffix = "sbref"

if not datatype_suffix:
Expand All @@ -550,7 +552,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([s]) == "9d148e2a05f782273f6343507733309d":
if get_study_hash([curr_seqinfo]) == "9d148e2a05f782273f6343507733309d":
current_run += 1
else:
raise RuntimeError(
Expand Down Expand Up @@ -583,10 +585,10 @@ def infotodict(
run_label = None

# yoh: had a wrong assumption
# if s.is_motion_corrected:
# assert s.is_derived, "Motion corrected images must be 'derived'"
# if curr_seqinfo.is_motion_corrected:
# assert curr_seqinfo.is_derived, "Motion corrected images must be 'derived'"

if s.is_motion_corrected and "rec-" in series_info.get("bids", ""):
if curr_seqinfo.is_motion_corrected and "rec-" in series_info.get("bids", ""):
raise NotImplementedError(
"want to add _rec-moco but there is _rec- already"
)
Expand All @@ -611,7 +613,7 @@ def from_series_info(name: str) -> Optional[str]:
from_series_info("acq"),
# But we want to add an indicator in case it was motion corrected
# in the magnet. ref sample /2017/01/03/qa
None if not s.is_motion_corrected else "rec-moco",
None if not curr_seqinfo.is_motion_corrected else "rec-moco",
from_series_info("dir"),
series_info.get("bids"),
run_label,
Expand All @@ -621,7 +623,7 @@ def from_series_info(name: str) -> Optional[str]:
suffix = "_".join(filter(bool, filename_suffix_parts)) # type: ignore[arg-type]

# # .series_description in case of
# sdesc = s.study_description
# sdesc = curr_seqinfo.study_description
# # temporary aliases for those phantoms which we already collected
# # so we rename them into this
# #MAPPING
Expand All @@ -638,13 +640,16 @@ def from_series_info(name: str) -> Optional[str]:
# https://github.com/nipy/heudiconv/issues/145
outtype: tuple[str, ...]
if (
"_Scout" in s.series_description
"_Scout" in curr_seqinfo.series_description
or (
datatype == "anat"
and datatype_suffix
and datatype_suffix.startswith("scout")
)
or (s.series_description.lower() == s.protocol_name.lower() + "_setter")
or (
curr_seqinfo.series_description.lower()
== curr_seqinfo.protocol_name.lower() + "_setter"
)
):
outtype = ("dicom",)
else:
Expand All @@ -654,7 +659,7 @@ def from_series_info(name: str) -> Optional[str]:
# we wanted ordered dict for consistent demarcation of dups
if template not in info:
info[template] = []
info[template].append(s.series_id)
info[template].append(curr_seqinfo.series_id)

if skipped:
lgr.info("Skipped %d sequences: %s" % (len(skipped), skipped))
Expand Down Expand Up @@ -762,7 +767,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

0 comments on commit 687b1fc

Please sign in to comment.