From 34f2bfd4340d1109cb3f29d310d1791eb2df4247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Szczepanik?= Date: Mon, 11 May 2026 12:26:00 +0200 Subject: [PATCH 1/2] Fix DWI fieldmap handling The heuristic did not handle DICOM series named "dwi_acq-(...)b0ref" correctly - it placed them as "_sbref" into dwi/, while they should have been "_epi" in func. These have an opposite phase encoding direction to the main dwi acquisitions, and are intended for field map correction. This change assigns the correct folder and suffix to these files, and adds dir-PA / dir-AP to the dwi and reference acquisitions. As a result, QSIPrep should pick these up as field maps and apply susceptibility distorsion correction. --- code/heuristic-q01.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/code/heuristic-q01.py b/code/heuristic-q01.py index 47b27fb..7869199 100644 --- a/code/heuristic-q01.py +++ b/code/heuristic-q01.py @@ -47,10 +47,10 @@ def infotodict( restvidn = create_key("{bids_subject_session_dir}/func/{bids_subject_session_prefix}_task-restvidn_bold") fmap_ap = create_key("{bids_subject_session_dir}/fmap/{bids_subject_session_prefix}_dir-ap_run-{item}_epi") fmap_pa = create_key("{bids_subject_session_dir}/fmap/{bids_subject_session_prefix}_dir-pa_run-{item}_epi") - dwi_b1200 = create_key("{bids_subject_session_dir}/dwi/{bids_subject_session_prefix}_acq-b1200_dwi") - ref_b1200 = create_key("{bids_subject_session_dir}/dwi/{bids_subject_session_prefix}_acq-b1200_sbref") - dwi_mshell = create_key("{bids_subject_session_dir}/dwi/{bids_subject_session_prefix}_acq-mshell_dwi") - ref_mshell = create_key("{bids_subject_session_dir}/dwi/{bids_subject_session_prefix}_acq-mshell_sbref") + 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 run labels for explicitly declared runs fmap_explicit_keys = { -- 2.52.0 From d4b97c3519608463d436db9821f62521f577bd08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Szczepanik?= Date: Wed, 3 Jun 2026 20:53:56 +0200 Subject: [PATCH 2/2] Switch field map labelling to use acq instead of run 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. --- code/heuristic-q01.py | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/code/heuristic-q01.py b/code/heuristic-q01.py index 7869199..8bf4359 100644 --- a/code/heuristic-q01.py +++ b/code/heuristic-q01.py @@ -5,7 +5,7 @@ from heudiconv.utils import SeqInfo # https://heudiconv.readthedocs.io/en/latest/heuristics.html#populate-intended-for-opts POPULATE_INTENDED_FOR_OPTS = { - "matching_parameters": ["ImagingVolume", "Shims"], + "matching_parameters": ["CustomAcquisitionLabel", "ImagingVolume", "Shims"], "criterion": "Closest", } @@ -45,19 +45,18 @@ def infotodict( 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") - fmap_ap = create_key("{bids_subject_session_dir}/fmap/{bids_subject_session_prefix}_dir-ap_run-{item}_epi") - fmap_pa = create_key("{bids_subject_session_dir}/fmap/{bids_subject_session_prefix}_dir-pa_run-{item}_epi") 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 run labels for explicitly declared runs + # 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, runlabel): create_key( - f"{{bids_subject_session_dir}}/fmap/{{bids_subject_session_prefix}}_dir-{dirlabel}_run-{runlabel}_epi" + (dirlabel, tasklabel): create_key( + f"{{bids_subject_session_dir}}/fmap/{{bids_subject_session_prefix}}_acq-{tasklabel}_dir-{dirlabel}_epi" ) - for dirlabel, runlabel in product(("ap", "pa"), ("1", "2", "3")) + for dirlabel, tasklabel in product(("ap", "pa"), ("rest", "restvidn", "restvida")) } info: dict[tuple[str, tuple[str, ...], None], list[str]] = { @@ -66,8 +65,6 @@ def infotodict( rest: [], restvida: [], restvidn: [], - fmap_ap: [], - fmap_pa: [], dwi_b1200: [], ref_b1200: [], dwi_mshell: [], @@ -77,6 +74,9 @@ def infotodict( # 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: @@ -116,6 +116,7 @@ def infotodict( 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: @@ -131,25 +132,20 @@ def infotodict( elif (m := fmap_regex.search(s.protocol_name)) is not None: if m.group("run") is None: - # implicit run numbering - if m.group("dir") == "ap": - info[fmap_ap].append(s.series_id) - elif m.group("dir") == "pa": - info[fmap_pa].append(s.series_id) + # no explicit run numbering + target_task = last_task if last_task is not None else "unknown" else: # explicit run numbering - key = fmap_explicit_keys.get((m.group("dir"), m.group("run"))) - if key is None: - # unexpected label combination, ignore - continue - info[key].append(s.series_id) + 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 key not in {fmap_ap, fmap_pa}: - # we allow (expect) run identifiers in field maps - continue if len(sid_list) > 1: # all other should be unique, keep last in case of repetitions removed.extend(sid_list[:-1]) -- 2.52.0