diff --git a/heudiconv/convert.py b/heudiconv/convert.py index aecc70dc..c1fb1e39 100644 --- a/heudiconv/convert.py +++ b/heudiconv/convert.py @@ -523,6 +523,34 @@ def update_uncombined_name( return filename +def update_multiorient_name( + metadata: dict[str, Any], + filename: str, +) -> str: + if "acq-" in filename: + lgr.warning( + "Not embedding multi-orientation information as prefix already uses acq- parameter." + ) + return filename + iop = metadata.get("ImageOrientationPatientDICOM") + iop = [round(x) for x in iop] + cross_prod = [ + iop[1] * iop[5] - iop[2] * iop[4], + iop[2] * iop[3] - iop[0] * iop[5], + iop[0] * iop[4] - iop[1] * iop[3], + ] + cross_prod = [abs(x) for x in cross_prod] + slice_orient = ["sagittal", "coronal", "axial"][cross_prod.index(1)] + bids_pairs = filename.split("_") + # acq needs to be inserted right after sub- or ses- + ses_or_sub_idx = sum( + [bids_pair.split("-")[0] in ["sub", "ses"] for bids_pair in bids_pairs] + ) + bids_pairs.insert(ses_or_sub_idx, "acq-%s" % slice_orient) + filename = "_".join(bids_pairs) + return filename + + def convert( items: list[tuple[str, tuple[str, ...], list[str]]], converter: str, @@ -953,6 +981,7 @@ def save_converted_files( echo_times: set[float] = set() channel_names: set[str] = set() image_types: set[str] = set() + iops: set[str] = set() for metadata in bids_metas: if not metadata: continue @@ -968,6 +997,12 @@ def save_converted_files( image_types.update(metadata["ImageType"]) except KeyError: pass + try: + iops.add(str(metadata["ImageOrientationPatientDICOM"])) + except KeyError: + pass + + print(iops) is_multiecho = ( len(set(filter(bool, echo_times))) > 1 @@ -978,6 +1013,7 @@ def save_converted_files( is_complex = ( "M" in image_types and "P" in image_types ) # Determine if data are complex (magnitude + phase) + is_multiorient = len(iops) > 1 echo_times_lst = sorted(echo_times) # also converts to list channel_names_lst = sorted(channel_names) # also converts to list @@ -1008,6 +1044,11 @@ def save_converted_files( bids_meta, this_prefix_basename, channel_names_lst ) + if is_multiorient: + this_prefix_basename = update_multiorient_name( + bids_meta, this_prefix_basename + ) + # Fallback option: # If we have failed to modify this_prefix_basename, because it didn't fall # into any of the options above, just add the suffix at the end: diff --git a/heudiconv/tests/test_convert.py b/heudiconv/tests/test_convert.py index ec63f4e2..0e39c269 100644 --- a/heudiconv/tests/test_convert.py +++ b/heudiconv/tests/test_convert.py @@ -17,6 +17,7 @@ bvals_are_zero, update_complex_name, update_multiecho_name, + update_multiorient_name, update_uncombined_name, ) from heudiconv.utils import load_heuristic @@ -143,6 +144,18 @@ def test_update_uncombined_name() -> None: update_uncombined_name(metadata, base_fn, set(channel_names)) # type: ignore[arg-type] +def test_update_multiorient_name() -> None: + """Unit testing for heudiconv.convert.update_multiorient_name(), which updates + filenames with the acq field if appropriate. + """ + # Standard name update + base_fn = "sub-X_ses-Y_task-Z_run-01_bold" + metadata = {"ImageOrientationPatientDICOM": [0, 1, 0, 0, 0, -1]} + out_fn_true = "sub-X_ses-Y_acq-sagittal_task-Z_run-01_bold" + out_fn_test = update_multiorient_name(metadata, base_fn) + assert out_fn_test == out_fn_true + + def test_b0dwi_for_fmap(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None: """Make sure we raise a warning when .bvec and .bval files are present but the modality is not dwi.