It is now clear that the intention was to use each field map acquisition just once. To make this more apparent in the BIDS layout, and enable Heudiconv's "CustomAcquisitionLabel" strategy for matching IntendedFor, we switch from using run- labels to acq- labels. The label will be either the corresponding task label (if intended for func) or the corresponding acquisition label (for dwi). We still keep the ImagingVolume and Shims options: we'd rather not assign IntendedFor than assign a mismatched one. It seems that the conditional which handles protocol name with no explicit field map numbering was left over from the initial draft (it seems that protocols from Aachen, Frankfurt, and Mannheim always use run-(1/2/3) while Mannheim has a naming strategy altogether incompatible with this heuristic), but the conditional is kept (and updated) just in case.
154 lines
5.8 KiB
Python
154 lines
5.8 KiB
Python
from itertools import product
|
|
import re
|
|
from typing import Optional
|
|
from heudiconv.utils import SeqInfo
|
|
|
|
# https://heudiconv.readthedocs.io/en/latest/heuristics.html#populate-intended-for-opts
|
|
POPULATE_INTENDED_FOR_OPTS = {
|
|
"matching_parameters": ["CustomAcquisitionLabel", "ImagingVolume", "Shims"],
|
|
"criterion": "Closest",
|
|
}
|
|
|
|
|
|
def create_key(
|
|
template: Optional[str],
|
|
outtype: tuple[str, ...] = ("nii.gz",),
|
|
annotation_classes: None = None,
|
|
) -> tuple[str, tuple[str, ...], None]:
|
|
if template is None or not template:
|
|
raise ValueError("Template must be a valid format string")
|
|
return (template, outtype, annotation_classes)
|
|
|
|
|
|
def infotodict(
|
|
seqinfo: list[SeqInfo],
|
|
) -> dict[tuple[str, tuple[str, ...], None], list[str]]:
|
|
"""Heuristic evaluator for determining which runs belong where
|
|
|
|
allowed template fields - follow python string module:
|
|
|
|
item: index within category
|
|
subject: participant id
|
|
seqitem: run number during scanning
|
|
subindex: sub index within group
|
|
"""
|
|
|
|
# regular expressions to match protocol names of each type (named groups capture detail)
|
|
anat_regex = re.compile(r"anat(?!_acq-scout)(_acq-.*)?_(?P<suffix>T[12]w)")
|
|
func_regex = re.compile(r"task-(?P<task>rest(vid[an])?)")
|
|
fmap_regex = re.compile(r"fmap(_se_|_dir-)(?P<dir>ap|pa)(.*_run-(?P<run>\d))?")
|
|
dwi_regex = re.compile(r"dwi_(acq-)?(?P<label>[a-zA-Z0-9]+)")
|
|
|
|
# keys for the mapping
|
|
t1w = create_key("{bids_subject_session_dir}/anat/{bids_subject_session_prefix}_T1w")
|
|
t2w = create_key("{bids_subject_session_dir}/anat/{bids_subject_session_prefix}_T2w")
|
|
rest = create_key("{bids_subject_session_dir}/func/{bids_subject_session_prefix}_task-rest_bold")
|
|
restvida = create_key("{bids_subject_session_dir}/func/{bids_subject_session_prefix}_task-restvida_bold")
|
|
restvidn = create_key("{bids_subject_session_dir}/func/{bids_subject_session_prefix}_task-restvidn_bold")
|
|
dwi_b1200 = create_key("{bids_subject_session_dir}/dwi/{bids_subject_session_prefix}_acq-b1200_dir-PA_dwi")
|
|
ref_b1200 = create_key("{bids_subject_session_dir}/fmap/{bids_subject_session_prefix}_acq-b1200_dir-AP_epi")
|
|
dwi_mshell = create_key("{bids_subject_session_dir}/dwi/{bids_subject_session_prefix}_acq-mshell_dir-PA_dwi")
|
|
ref_mshell = create_key("{bids_subject_session_dir}/fmap/{bids_subject_session_prefix}_acq-mshell_dir-AP_epi")
|
|
|
|
# generate fieldmap keys with fixed acq labels for explicitly declared runs
|
|
run_to_task = {"1": "rest", "2": "restvidn", "3": "restvida"} # that was the intention
|
|
fmap_explicit_keys = {
|
|
(dirlabel, tasklabel): create_key(
|
|
f"{{bids_subject_session_dir}}/fmap/{{bids_subject_session_prefix}}_acq-{tasklabel}_dir-{dirlabel}_epi"
|
|
)
|
|
for dirlabel, tasklabel in product(("ap", "pa"), ("rest", "restvidn", "restvida"))
|
|
}
|
|
|
|
info: dict[tuple[str, tuple[str, ...], None], list[str]] = {
|
|
t1w: [],
|
|
t2w: [],
|
|
rest: [],
|
|
restvida: [],
|
|
restvidn: [],
|
|
dwi_b1200: [],
|
|
ref_b1200: [],
|
|
dwi_mshell: [],
|
|
ref_mshell: [],
|
|
}
|
|
|
|
# also include the generated keys
|
|
info.update({k: [] for k in fmap_explicit_keys.values()})
|
|
|
|
# keep track of last task (potentially for field maps)
|
|
last_task = None
|
|
|
|
for s in seqinfo:
|
|
"""
|
|
The namedtuple `s` contains the following fields:
|
|
|
|
* total_files_till_now
|
|
* example_dcm_file
|
|
* series_id
|
|
* dcm_dir_name
|
|
* unspecified2
|
|
* unspecified3
|
|
* dim1
|
|
* dim2
|
|
* dim3
|
|
* dim4
|
|
* TR
|
|
* TE
|
|
* protocol_name
|
|
* is_motion_corrected
|
|
* is_derived
|
|
* patient_id
|
|
* study_description
|
|
* referring_physician_name
|
|
* series_description
|
|
* image_type
|
|
"""
|
|
|
|
if (m := anat_regex.search(s.protocol_name)) is not None:
|
|
if m.group("suffix") == "T1w":
|
|
info[t1w].append(s.series_id)
|
|
elif m.group("suffix") == "T2w":
|
|
info[t2w].append(s.series_id)
|
|
|
|
elif (m := func_regex.search(s.protocol_name)) is not None:
|
|
if m.group("task") == "rest":
|
|
info[rest].append(s.series_id)
|
|
if m.group("task") == "restvida":
|
|
info[restvida].append(s.series_id)
|
|
if m.group("task") == "restvidn":
|
|
info[restvidn].append(s.series_id)
|
|
last_task = m.group("task")
|
|
|
|
elif (m := dwi_regex.search(s.protocol_name)) is not None:
|
|
if "b1200" in m.group("label") and not s.is_derived:
|
|
info[dwi_b1200].append(s.series_id)
|
|
elif "b0ref" in m.group("label"):
|
|
info[ref_b1200].append(s.series_id)
|
|
elif "mshell" in m.group("label") and not s.is_derived:
|
|
# need to look outside the matched group
|
|
if "ref" in s.protocol_name:
|
|
info[ref_mshell].append(s.series_id)
|
|
else:
|
|
info[dwi_mshell].append(s.series_id)
|
|
|
|
elif (m := fmap_regex.search(s.protocol_name)) is not None:
|
|
if m.group("run") is None:
|
|
# no explicit run numbering
|
|
target_task = last_task if last_task is not None else "unknown"
|
|
else:
|
|
# explicit run numbering
|
|
target_task = run_to_task.get(m.group("run"), "unknown")
|
|
key = fmap_explicit_keys.get((m.group("dir"), target_task))
|
|
if key is None:
|
|
# unexpected label combination, ignore
|
|
continue
|
|
info[key].append(s.series_id)
|
|
|
|
# deduplicate
|
|
removed = []
|
|
for key, sid_list in info.items():
|
|
if len(sid_list) > 1:
|
|
# all other should be unique, keep last in case of repetitions
|
|
removed.extend(sid_list[:-1])
|
|
del sid_list[:-1]
|
|
|
|
return info
|