Source code for AFQ.api.bundle_dict

import logging
from collections.abc import Mapping, MutableMapping

import nibabel as nib
import numpy as np
from dipy.io.streamline import load_tractogram

import AFQ.data.fetch as afd
import AFQ.utils.volume as auv
from AFQ.definitions.utils import find_file
from AFQ.tasks.utils import get_fname, str_to_desc

logger = logging.getLogger("AFQ")
logger.setLevel(logging.INFO)


__all__ = [
    "BundleDict",
    "default_bd",
    "reco_bd",
    "callosal_bd",
    "cerebellar_bd",
    "baby_bd",
]


def do_preprocessing():
    raise NotImplementedError


def append_l_r(bundle_list, no_lr_list):
    new_bundle_list = []
    for bundle in bundle_list:
        if bundle in no_lr_list:
            new_bundle_list.append(bundle)
        else:
            new_bundle_list.append(bundle + "_L")
            new_bundle_list.append(bundle + "_R")
    return new_bundle_list


RECO_UNIQUE = [
    "CCMid",
    "CC_ForcepsMajor",
    "CC_ForcepsMinor",
    "MCP",
    "AC",
    "PC",
    "SCP",
    "V",
    "CC",
    "F_L_R",
]

RECO_BUNDLES_16 = [
    "CST",
    "C",
    "F",
    "UF",
    "MCP",
    "AF",
    "CCMid",
    "CC_ForcepsMajor",
    "CC_ForcepsMinor",
    "IFOF",
]
RECO_BUNDLES_16 = append_l_r(RECO_BUNDLES_16, RECO_UNIQUE)

RECO_BUNDLES_80 = [
    "AC",
    "AF",
    "AR",
    "AST",
    "C",
    "CB",
    "CC_ForcepsMajor",
    "CC_ForcepsMinor",
    "CC",
    "CCMid",
    "CNII",
    "CNIII",
    "CNIV",
    "CNV",
    "CNVII",
    "CNVIII",
    "CS",
    "CST",
    "CT",
    "CTT",
    "DLF",
    "EMC",
    "F_L_R",
    "FPT",
    "ICP",
    "IFOF",
    "ILF",
    "LL",
    "MCP",
    "MdLF",
    "ML",
    "MLF",
    "OPT",
    "OR",
    "PC",
    "PPT",
    "RST",
    "SCP",
    "SLF",
    "STT",
    "TPT",
    "UF",
    "V",
    "VOF",
]
RECO_BUNDLES_80 = append_l_r(RECO_BUNDLES_80, RECO_UNIQUE)
# See: https://www.cmu.edu/dietrich/psychology/cognitiveaxon/documents/yeh_etal_2018.pdf  # noqa


DIPY_GH = "https://github.com/dipy/dipy/blob/master/dipy/"


def OR_bd():
    or_rois = afd.read_or_templates()

    return BundleDict(
        {
            "Left Optic Radiation": {
                "include": [or_rois["left_OR_1"], or_rois["left_OR_2"]],
                "exclude": [
                    or_rois["left_OP_MNI"],
                    or_rois["left_TP_MNI"],
                    or_rois["left_pos_thal_MNI"],
                ],
                "start": or_rois["left_thal_MNI"],
                "end": or_rois["left_V1_MNI"],
                "cross_midline": False,
            },
            "Right Optic Radiation": {
                "include": [or_rois["right_OR_1"], or_rois["right_OR_2"]],
                "exclude": [
                    or_rois["right_OP_MNI"],
                    or_rois["right_TP_MNI"],
                    or_rois["right_pos_thal_MNI"],
                ],
                "start": or_rois["right_thal_MNI"],
                "end": or_rois["right_V1_MNI"],
                "cross_midline": False,
            },
        },
        citations={"Caffarra2021"},
    )


[docs] def default_bd(): templates = afd.read_templates(as_img=False) templates["ARC_roi1_L"] = templates["SLF_roi1_L"] templates["ARC_roi1_R"] = templates["SLF_roi1_R"] templates["ARC_roi2_L"] = templates["SLFt_roi2_L"] templates["ARC_roi2_R"] = templates["SLFt_roi2_R"] return OR_bd() + BundleDict( { "Left Anterior Thalamic": { "cross_midline": False, "include": [templates["ATR_roi1_L"], templates["ATR_roi2_L"]], "exclude": [], "space": "template", "prob_map": templates["ATR_L_prob_map"], "start": templates["ATR_L_start"], "end": templates["ATR_L_end"], "length": {"min_len": 30}, }, "Right Anterior Thalamic": { "cross_midline": False, "include": [templates["ATR_roi1_R"], templates["ATR_roi2_R"]], "exclude": [], "space": "template", "prob_map": templates["ATR_R_prob_map"], "start": templates["ATR_R_start"], "end": templates["ATR_R_end"], "length": {"min_len": 30}, }, "Left Cingulum Cingulate": { "cross_midline": False, "include": [templates["CGC_roi2_L"], templates["CGC_roi1_L"]], "exclude": [], "space": "template", "prob_map": templates["CGC_L_prob_map"], "end": templates["CGC_L_start"], "length": {"min_len": 30}, }, "Right Cingulum Cingulate": { "cross_midline": False, "include": [templates["CGC_roi2_R"], templates["CGC_roi1_R"]], "exclude": [], "space": "template", "prob_map": templates["CGC_R_prob_map"], "end": templates["CGC_R_start"], "length": {"min_len": 30}, }, "Left Corticospinal": { "cross_midline": False, "include": [templates["CST_roi2_L"], templates["CST_roi1_L"]], "exclude": [], "space": "template", "prob_map": templates["CST_L_prob_map"], "end": templates["CST_L_start"], "length": {"min_len": 40}, }, "Right Corticospinal": { "cross_midline": False, "include": [templates["CST_roi2_R"], templates["CST_roi1_R"]], "exclude": [], "space": "template", "prob_map": templates["CST_R_prob_map"], "end": templates["CST_R_start"], "length": {"min_len": 40}, }, "Left Inferior Fronto-occipital": { "cross_midline": False, "include": [templates["IFO_roi2_L"], templates["IFO_roi1_L"]], "exclude": [templates["ARC_roi1_L"], templates["CGC_roi1_L"]], "space": "template", "prob_map": templates["IFO_L_prob_map"], "end": templates["IFO_L_start"], "start": templates["IFO_L_end"], "length": {"min_len": 80}, }, "Right Inferior Fronto-occipital": { "cross_midline": False, "include": [templates["IFO_roi2_R"], templates["IFO_roi1_R"]], "exclude": [templates["ARC_roi1_R"], templates["CGC_roi1_R"]], "space": "template", "prob_map": templates["IFO_R_prob_map"], "end": templates["IFO_R_start"], "start": templates["IFO_R_end"], "length": {"min_len": 80}, }, "Left Inferior Longitudinal": { "cross_midline": False, "include": [templates["ILF_roi2_L"], templates["ILF_roi1_L"]], "exclude": [], "space": "template", "prob_map": templates["ILF_L_prob_map"], "start": templates["ILF_L_end"], "end": templates["ILF_L_start"], "length": {"min_len": 40}, }, "Right Inferior Longitudinal": { "cross_midline": False, "include": [templates["ILF_roi2_R"], templates["ILF_roi1_R"]], "exclude": [], "space": "template", "prob_map": templates["ILF_R_prob_map"], "start": templates["ILF_R_end"], "end": templates["ILF_R_start"], "length": {"min_len": 40}, }, "Left Arcuate": { "cross_midline": False, "include": [templates["SLF_roi1_L"], templates["SLFt_roi2_L"]], "exclude": [templates["IFO_roi1_L"]], "space": "template", "prob_map": templates["ARC_L_prob_map"], "start": templates["ARC_L_start"], "end": templates["ARC_L_end"], "length": {"min_len": 40}, }, "Right Arcuate": { "cross_midline": False, "include": [templates["SLF_roi1_R"], templates["SLFt_roi2_R"]], "exclude": [templates["IFO_roi1_R"]], "space": "template", "prob_map": templates["ARC_R_prob_map"], "start": templates["ARC_R_start"], "end": templates["ARC_R_end"], "length": {"min_len": 40}, }, "Left Uncinate": { "cross_midline": False, "include": [templates["UNC_roi2_L"], templates["UNC_roi1_L"]], "exclude": [], "space": "template", "prob_map": templates["UNC_L_prob_map"], "start": templates["UNC_L_end"], "end": templates["UNC_L_start"], }, "Right Uncinate": { "cross_midline": False, "include": [templates["UNC_roi2_R"], templates["UNC_roi1_R"]], "exclude": [], "space": "template", "prob_map": templates["UNC_R_prob_map"], "start": templates["UNC_R_end"], "end": templates["UNC_R_start"], }, "Left Posterior Arcuate": { "cross_midline": False, "include": [templates["SLFt_roi2_L"]], "exclude": [ templates["SLF_roi1_L"], templates["IFO_roi1_L"], templates["pARC_xroi1_L"], templates["TPSPL_roi1_L"], ], "space": "template", "start": templates["pARC_L_start"], "end": templates["pARC_L_end"], "Left Arcuate": {"overlap": 30}, "Left Optic Radiation": {"core": "Right"}, "length": {"min_len": 30}, "primary_axis": "I/S", }, "Right Posterior Arcuate": { "cross_midline": False, "include": [templates["SLFt_roi2_R"]], "exclude": [ templates["SLF_roi1_R"], templates["IFO_roi1_R"], templates["pARC_xroi1_R"], templates["TPSPL_roi1_R"], ], "space": "template", "start": templates["pARC_R_start"], "end": templates["pARC_R_end"], "Right Arcuate": {"overlap": 30}, "Right Optic Radiation": {"core": "Left"}, "length": {"min_len": 30}, "primary_axis": "I/S", }, "Left Temporo-parietal": { "cross_midline": False, "include": [ templates["SLFt_roi2_L"], templates["TPSPL_roi1_L"], ], "exclude": [ templates["SLF_roi1_L"], templates["IFO_roi1_L"], templates["pARC_xroi1_L"], ], "space": "template", "start": templates["pARC_L_start"], "end": templates["pARC_L_end"], "Left Arcuate": {"overlap": 20}, "Left Optic Radiation": {"core": "Right"}, "length": {"min_len": 30}, "primary_axis": "I/S", }, "Right Temporo-parietal": { "cross_midline": False, "include": [ templates["SLFt_roi2_R"], templates["TPSPL_roi1_R"], ], "exclude": [ templates["SLF_roi1_R"], templates["IFO_roi1_R"], templates["pARC_xroi1_R"], ], "space": "template", "start": templates["pARC_R_start"], "end": templates["pARC_R_end"], "Right Arcuate": {"overlap": 20}, "Right Optic Radiation": {"core": "Left"}, "length": {"min_len": 30}, "primary_axis": "I/S", }, "Left Vertical Occipital": { "cross_midline": False, "space": "template", "end": templates["VOF_L_end"], "start": templates["VOF_L_start"], "include": [templates["VOF_roi1_L"], templates["VOF_roi2_L"]], "exclude": [ templates["VOF_xroi2_L"], templates["Cerebellar_Hemi_L"], templates["pARC_xroi1_L"], ], "Left Posterior Arcuate": { "node_thresh": 20, "project": "L/R", "core": "Anterior", }, "length": {"min_len": 30}, "endpoint_dists": {"min_dist": 25}, "mahal": {"clean_rounds": 0}, "primary_axis": "I/S", "ORG_spectral_subbundles": SpectralSubbundleDict( { "Left V1V3": { "cluster_IDs": [78], "Left Optic Radiation": { "core": "Anterior", "consideration": "closest", }, }, "Left Posterior Vertical Occipital": { "cluster_IDs": [72, 83], "Left Optic Radiation": { "project": "I/S", "core": "Right", "consideration": 10.0, }, }, "Left Anterior Vertical Occipital": { "cluster_IDs": [7, 18, 21, 25], "Left Optic Radiation": { "project": "I/S", "core": "Right", "consideration": 10.0, }, }, }, criteria_for_all={ "orient_mahal": { "distance_threshold": 4, "length_threshold": 2, "clean_rounds": 5, "remove_lengths": "short", }, "mahal": { "distance_threshold": 4, "length_threshold": 2, "clean_rounds": 5, "remove_lengths": "short", }, }, ), }, "Right Vertical Occipital": { "cross_midline": False, "space": "template", "end": templates["VOF_R_end"], "start": templates["VOF_R_start"], "include": [templates["VOF_roi1_R"], templates["VOF_roi2_R"]], "exclude": [ templates["VOF_xroi2_R"], templates["Cerebellar_Hemi_R"], templates["pARC_xroi1_R"], ], "Right Posterior Arcuate": { "node_thresh": 20, "project": "L/R", "core": "Anterior", }, "length": {"min_len": 30}, "endpoint_dists": {"min_dist": 25}, "mahal": {"clean_rounds": 0}, "primary_axis": "I/S", "ORG_spectral_subbundles": SpectralSubbundleDict( { "Right V1V3": { "cluster_IDs": [78], "Right Optic Radiation": { "core": "Anterior", "consideration": "closest", }, }, "Right Posterior Vertical Occipital": { "cluster_IDs": [72, 83], "Right Optic Radiation": { "project": "I/S", "core": "Left", "consideration": 10.0, }, }, "Right Anterior Vertical Occipital": { "cluster_IDs": [7, 18, 21, 25], "Right Optic Radiation": { "project": "I/S", "core": "Left", "consideration": 10.0, }, }, }, criteria_for_all={ "orient_mahal": { "distance_threshold": 4, "length_threshold": 2, "clean_rounds": 5, "remove_lengths": "short", }, "mahal": { "distance_threshold": 4, "length_threshold": 2, "clean_rounds": 5, "remove_lengths": "short", }, }, ), }, }, citations={ "Yeatman2012", "takemura2016major", "Tzourio-Mazoyer2002", "zhang2018anatomically", "Hua2008", }, )
def mdlf_bd(): """ Work in Progress. """ templates = afd.read_templates(as_img=False) return default_bd() + BundleDict( { "Left Middle Longitudinal": { "cross_midline": False, "start": templates["Temporal_Sup_L"], "end": templates["MdLF_L_end"], "exclude": [templates["SLF_roi1_L"]], "space": "template", "Left Inferior Longitudinal": {"node_thresh": 20}, "length": {"min_len": 50}, }, "Right Middle Longitudinal": { "cross_midline": False, "start": templates["Temporal_Sup_R"], "end": templates["MdLF_R_end"], "exclude": [templates["SLF_roi1_R"]], "space": "template", "Right Inferior Longitudinal": {"node_thresh": 20}, "length": {"min_len": 50}, }, }, citations={ "wang2013rethinking", }, ) def slf_bd(): templates = afd.read_slf_templates(as_img=False) templates_afq = afd.read_templates(as_img=False) templates["Frontal_Lobe_L"] = templates_afq["ATR_L_start"] templates["Frontal_Lobe_R"] = templates_afq["ATR_R_start"] return BundleDict( { "Left Superior Longitudinal I": { "include": [templates["SFgL"], templates["PaL"]], "exclude": [templates["SLFt_roi2_L"]], "start": templates["Frontal_Lobe_L"], "cross_midline": False, "Left Cingulum Cingulate": { "node_thresh": 20, }, }, "Left Superior Longitudinal II": { "include": [templates["MFgL"], templates["PaL"]], "exclude": [templates["SLFt_roi2_L"]], "start": templates["Frontal_Lobe_L"], "cross_midline": False, "Left Cingulum Cingulate": { "node_thresh": 20, }, }, "Left Superior Longitudinal III": { "include": [templates["PrgL"], templates["PaL"]], "exclude": [templates["SLFt_roi2_L"]], "start": templates["Frontal_Lobe_L"], "cross_midline": False, "Left Cingulum Cingulate": { "node_thresh": 20, }, }, "Right Superior Longitudinal I": { "include": [templates["SFgR"], templates["PaR"]], "exclude": [templates["SLFt_roi2_R"]], "start": templates["Frontal_Lobe_R"], "cross_midline": False, "Right Cingulum Cingulate": { "node_thresh": 20, }, }, "Right Superior Longitudinal II": { "include": [templates["MFgR"], templates["PaR"]], "exclude": [templates["SLFt_roi2_R"]], "start": templates["Frontal_Lobe_R"], "cross_midline": False, "Right Cingulum Cingulate": { "node_thresh": 20, }, }, "Right Superior Longitudinal III": { "include": [templates["PrgR"], templates["PaR"]], "exclude": [templates["SLFt_roi2_R"]], "start": templates["Frontal_Lobe_R"], "cross_midline": False, "Right Cingulum Cingulate": { "node_thresh": 20, }, }, }, citations={"Sagi2024"}, criteria_for_all={ "mahal": { "distance_threshold": 3, "length_threshold": 3, "clean_rounds": 5, "remove_lengths": "both", }, }, ) def forceps_bd(): templates = afd.read_templates(as_img=False) callosal_templates = afd.read_callosum_templates(as_img=False) return BundleDict( { "Forceps Minor": { "cross_midline": True, "include": [ templates["FA_L"], callosal_templates["Callosum_midsag"], templates["FA_R"], ], "exclude": [], "space": "template", "prob_map": templates["FA_prob_map"], "start": templates["FA_start"], "end": templates["FA_end"], }, "Forceps Major": { "cross_midline": True, "include": [ templates["FP_L"], callosal_templates["Callosum_midsag"], templates["FP_R"], ], "exclude": [], "space": "template", "prob_map": templates["FP_prob_map"], "start": templates["FP_start"], "end": templates["FP_end"], }, } )
[docs] def baby_bd(): # Pediatric bundles differ from adult bundles: # - A third ROI has been introduced for curvy tracts: # ARC, ATR, CGC, IFO, and UCI # - ILF posterior ROI has been split into two # to separate ILF and mdLF # - Addition of pAF and VOF ROIs # - SLF ROIs are restricted to parietal cortex templates = afd.read_pediatric_templates() # pediatric probability maps prob_map_order = [ "ATR_L", "ATR_R", "CST_L", "CST_R", "CGC_L", "CGC_R", "MdLF_L", "MdLF_R", "FP", "FA", "IFO_L", "IFO_R", "ILF_L", "ILF_R", "SLF_L", "SLF_R", "UNC_L", "UNC_R", "ARC_L", "ARC_R", ] prob_maps = templates["UNCNeo_JHU_tracts_prob-for-babyAFQ"] prob_map_data = prob_maps.get_fdata() for bundle_name in prob_map_order: templates[bundle_name + "_prob_map"] = nib.Nifti1Image( prob_map_data[..., prob_map_order.index(bundle_name)], prob_maps.affine ) # reuse probability map from ILF templates["MdLF_L_prob_map"] = templates["ILF_L_prob_map"] templates["MdLF_R_prob_map"] = templates["ILF_R_prob_map"] return BundleDict( { "Left Anterior Thalamic": { "cross_midline": False, "include": [ templates["ATR_roi3_L"], templates["ATR_roi2_L"], templates["ATR_roi1_L"], ], "exclude": [], "space": "template", "prob_map": templates["ATR_L_prob_map"], "mahal": {"distance_threshold": 4}, }, "Right Anterior Thalamic": { "cross_midline": False, "include": [ templates["ATR_roi3_R"], templates["ATR_roi2_R"], templates["ATR_roi1_R"], ], "exclude": [], "space": "template", "prob_map": templates["ATR_R_prob_map"], "mahal": {"distance_threshold": 4}, }, "Left Cingulum Cingulate": { "cross_midline": False, "include": [ templates["CGC_roi3_L"], templates["CGC_roi2_L"], templates["CGC_roi1_L"], ], "exclude": [], "space": "template", "prob_map": templates["CGC_L_prob_map"], "mahal": {"distance_threshold": 4}, }, "Right Cingulum Cingulate": { "cross_midline": False, "include": [ templates["CGC_roi3_R"], templates["CGC_roi2_R"], templates["CGC_roi1_R"], ], "exclude": [], "space": "template", "prob_map": templates["CGC_R_prob_map"], "mahal": {"distance_threshold": 4}, }, "Left Corticospinal": { "cross_midline": False, "include": [templates["CST_roi2_L"], templates["CST_roi1_L"]], "exclude": [], "space": "template", "prob_map": templates["CST_L_prob_map"], "mahal": {"distance_threshold": 4}, }, "Right Corticospinal": { "cross_midline": False, "include": [templates["CST_roi2_R"], templates["CST_roi1_R"]], "exclude": [], "space": "template", "prob_map": templates["CST_R_prob_map"], "mahal": {"distance_threshold": 4}, }, "Left Inferior Fronto-occipital": { "cross_midline": False, "include": [ templates["IFO_roi3_L"], templates["IFO_roi2_L"], templates["IFO_roi1_L"], ], "exclude": [], "space": "template", "prob_map": templates["IFO_L_prob_map"], "mahal": {"distance_threshold": 4}, }, "Right Inferior Fronto-occipital": { "cross_midline": False, "include": [ templates["IFO_roi3_R"], templates["IFO_roi2_R"], templates["IFO_roi1_R"], ], "exclude": [], "space": "template", "prob_map": templates["IFO_R_prob_map"], "mahal": {"distance_threshold": 4}, }, "Left Inferior Longitudinal": { "cross_midline": False, "include": [templates["ILF_roi2_L"], templates["ILF_roi1_L"]], "exclude": [], "space": "template", "prob_map": templates["ILF_L_prob_map"], "mahal": {"distance_threshold": 4}, }, "Right Inferior Longitudinal": { "cross_midline": False, "include": [templates["ILF_roi2_R"], templates["ILF_roi1_R"]], "exclude": [], "space": "template", "prob_map": templates["ILF_R_prob_map"], "mahal": {"distance_threshold": 4}, }, "Left Middle Longitudinal": { "cross_midline": False, "include": [templates["MdLF_roi2_L"], templates["MdLF_roi1_L"]], "exclude": [], "space": "template", "prob_map": templates["MdLF_L_prob_map"], "mahal": {"distance_threshold": 4}, }, "Right Middle Longitudinal": { "cross_midline": False, "include": [templates["MdLF_roi2_R"], templates["MdLF_roi1_R"]], "exclude": [], "space": "template", "prob_map": templates["MdLF_R_prob_map"], "mahal": {"distance_threshold": 4}, }, "Left Superior Longitudinal": { "cross_midline": False, "include": [templates["SLF_roi1_L"], templates["SLF_roi2_L"]], "exclude": [templates["SLFt_roi2_L"]], "space": "template", "prob_map": templates["SLF_L_prob_map"], "mahal": {"distance_threshold": 4}, }, "Right Superior Longitudinal": { "cross_midline": False, "include": [templates["SLF_roi1_R"], templates["SLF_roi2_R"]], "exclude": [templates["SLFt_roi2_R"]], "space": "template", "prob_map": templates["SLF_R_prob_map"], "mahal": {"distance_threshold": 4}, }, "Left Arcuate": { "cross_midline": False, "include": [ templates["ARC_roi1_L"], templates["ARC_roi2_L"], templates["ARC_roi3_L"], ], "exclude": [], "space": "template", "prob_map": templates["ARC_L_prob_map"], "mahal": {"distance_threshold": 4}, }, "Right Arcuate": { "cross_midline": False, "include": [ templates["ARC_roi1_R"], templates["ARC_roi2_R"], templates["ARC_roi3_R"], ], "exclude": [], "space": "template", "prob_map": templates["ARC_R_prob_map"], "mahal": {"distance_threshold": 4}, }, "Left Uncinate": { "cross_midline": False, "include": [ templates["UNC_roi3_L"], templates["UNC_roi2_L"], templates["UNC_roi1_L"], ], "exclude": [], "space": "template", "prob_map": templates["UNC_L_prob_map"], "mahal": {"distance_threshold": 4}, }, "Right Uncinate": { "cross_midline": False, "include": [ templates["UNC_roi3_R"], templates["UNC_roi2_R"], templates["UNC_roi1_R"], ], "exclude": [], "space": "template", "prob_map": templates["UNC_R_prob_map"], "mahal": {"distance_threshold": 4}, }, "Forceps Minor": { "cross_midline": True, "include": [ templates["FA_R"], templates["mid-sagittal"], templates["FA_L"], ], "exclude": [], "space": "template", "prob_map": templates["FA_prob_map"], "mahal": {"distance_threshold": 4}, }, "Forceps Major": { "cross_midline": True, "include": [ templates["FP_R"], templates["mid-sagittal"], templates["FP_L"], ], "exclude": [], "space": "template", "prob_map": templates["FP_prob_map"], "mahal": {"distance_threshold": 4}, }, "Left Optic Radiation": { "include": [templates["OR_left_roi3"]], "start": templates["OR_leftThal"], "end": templates["OR_leftV1"], "cross_midline": False, "mahal": {"distance_threshold": 4}, }, "Right Optic Radiation": { "include": [templates["OR_right_roi3"]], "start": templates["OR_rightThal"], "end": templates["OR_rightV1"], "cross_midline": False, "mahal": {"distance_threshold": 4}, }, "Left Posterior Arcuate": { "include": [templates["SLFt_roi2_L"]], "exclude": [templates["SLF_roi1_L"]], "start": templates["pARC_L_start"], "end": templates["VOF_box_small_L"], "primary_axis": "I/S", "primary_axis_percentage": 40, "cross_midline": False, "mahal": {"distance_threshold": 4}, }, "Right Posterior Arcuate": { "include": [templates["SLFt_roi2_R"]], "exclude": [templates["SLF_roi1_R"]], "start": templates["pARC_R_start"], "end": templates["VOF_box_small_R"], "primary_axis": "I/S", "primary_axis_percentage": 40, "cross_midline": False, "mahal": {"distance_threshold": 4}, }, "Left Vertical Occipital": { "start": templates["VOF_L_start"], "end": templates["VOF_box_small_L"], "primary_axis": "I/S", "primary_axis_percentage": 40, "cross_midline": False, "mahal": {"distance_threshold": 4}, }, "Right Vertical Occipital": { "start": templates["VOF_R_start"], "end": templates["VOF_box_small_R"], "primary_axis": "I/S", "primary_axis_percentage": 40, "cross_midline": False, "mahal": {"distance_threshold": 4}, }, }, resample_to=afd.read_pediatric_templates()["UNCNeo-withCerebellum-for-babyAFQ"], citations={"Grotheer2022", "grotheer2023human"}, )
[docs] def callosal_bd(): templates = afd.read_templates(as_img=False) callosal_templates = afd.read_callosum_templates(as_img=False) return BundleDict( { "Callosum Anterior Frontal": { "cross_midline": True, "include": [ callosal_templates["R_AntFrontal"], callosal_templates["Callosum_midsag"], callosal_templates["L_AntFrontal"], ], }, "Callosum Motor": { "cross_midline": True, "include": [ callosal_templates["R_Motor"], callosal_templates["Callosum_midsag"], callosal_templates["L_Motor"], ], }, "Callosum Occipital": { "cross_midline": True, "include": [ callosal_templates["R_Occipital"], callosal_templates["Callosum_midsag"], callosal_templates["L_Occipital"], ], }, "Callosum Orbital": { "cross_midline": True, "include": [ callosal_templates["R_Orbital"], callosal_templates["Callosum_midsag"], callosal_templates["L_Orbital"], ], }, "Callosum Posterior Parietal": { "cross_midline": True, "include": [ callosal_templates["R_PostParietal"], callosal_templates["Callosum_midsag"], callosal_templates["L_PostParietal"], ], }, "Callosum Superior Frontal": { "cross_midline": True, "include": [ callosal_templates["R_SupFrontal"], callosal_templates["Callosum_midsag"], callosal_templates["L_SupFrontal"], ], }, "Callosum Superior Parietal": { "cross_midline": True, "include": [ callosal_templates["R_SupParietal"], callosal_templates["Callosum_midsag"], callosal_templates["L_SupParietal"], ], }, "Callosum Temporal": { "cross_midline": True, "include": [ callosal_templates["R_Temporal"], callosal_templates["Callosum_midsag"], callosal_templates["L_Temporal"], ], }, }, citations={"Dougherty2007"}, criteria_for_all={ "exclude": [templates["CST_roi1_L"], templates["CST_roi1_R"]], "space": "template", }, )
[docs] def reco_bd(n_bundles): """ n_bundles: int Selects between 16 or 80 bundle atlas """ templates = afd.read_hcp_atlas(n_bundles, as_file=True) return BundleDict( templates, citations={"yeh2018population", "Garyfallidis2018"}, )
[docs] def cerebellar_bd(): cp_rois = afd.read_cp_templates() return BundleDict( { "Left Inferior Cerebellar Peduncle": { "include": [ cp_rois["ICP_L_inferior_prob"], cp_rois["ICP_L_superior_prob"], ], "cross_midline": False, }, "Right Inferior Cerebellar Peduncle": { "include": [ cp_rois["ICP_R_inferior_prob"], cp_rois["ICP_R_superior_prob"], ], "cross_midline": False, }, "Left Middle Cerebellar Peduncle": { "include": [ cp_rois["MCP_L_inferior_prob"], cp_rois["MCP_R_superior_prob"], ], "exclude": [ cp_rois["SCP_L_inter_prob"], ], "cross_midline": True, }, "Right Middle Cerebellar Peduncle": { "include": [ cp_rois["MCP_R_inferior_prob"], cp_rois["MCP_L_superior_prob"], ], "exclude": [ cp_rois["SCP_R_inter_prob"], ], "cross_midline": True, }, "Left Superior Cerebellar Peduncle": { "include": [ cp_rois["SCP_L_inferior_prob"], cp_rois["SCP_L_inter_prob"], cp_rois["SCP_R_superior_prob"], ], "exclude": [ cp_rois["SCP_L_superior_prob"], ], "cross_midline": True, }, "Right Superior Cerebellar Peduncle": { "include": [ cp_rois["SCP_R_inferior_prob"], cp_rois["SCP_R_inter_prob"], cp_rois["SCP_L_superior_prob"], ], "exclude": [ cp_rois["SCP_R_superior_prob"], ], "cross_midline": True, }, }, citations={"Jossinger2022"}, )
class _BundleEntry(Mapping): """Describes how to recognize a single bundle, immutable""" def __init__(self, data): self._data = data def __getitem__(self, key): return self._data[key] def __len__(self): return len(self._data) def __iter__(self): return iter(self._data) def __setitem__(self, key, value): raise RuntimeError( ( "You cannot modify the properties of a bundle's definition. " "To modify a bundle's definition, replace that bundle's entry " "in the BundleDict." ) )
[docs] class BundleDict(MutableMapping): """ Create a bundle dictionary, needed for the segmentation. Parameters ---------- bundle_info : dict, A dictionary defining custom bundles. See `Defining Custom Bundle Dictionaries` in the `usage` section of pyAFQ's documentation for details. resample_to : Nifti1Image or bool, optional If there are bundles in bundle_info with the 'space' attribute set to 'template', or with no 'space' attribute, their images (all ROIs and probability maps) will be resampled to the affine and shape of this image. If None, the MNI template will be used. If False, no resampling will be done. Default: None resample_subject_to : Nifti1Image or bool, optional If there are bundles in bundle_info with the 'space' attribute set to 'subject', their images (all ROIs and probability maps) will be resampled to the affine and shape of this image. If True, resamples to DWI. Be careful if you use this, that this is the correct choice. If False, no resampling will be done. Default: False keep_in_memory : bool, optional Whether, once loaded, all ROIs and probability maps will stay loaded in memory within this object. By default, ROIs are loaded into memory on demand and no references to ROIs are kept, other than their paths. The default 18 bundles use ~6GB when all loaded. Default: False citations: set, optional A set of citations (in BibTeX format) relevant to the bundle definitions provided. Default: None criteria_for_all: dict, optional A dictionary of criteria that should be applied to all bundles. For example, you might want to set cleaning parameters for all bundles. Applied immediately after instantiation and does not affect newly added bundles. Default: None Examples -------- # import OR ROIs and create a custom bundle dict # from them import AFQ.data.fetch as afd or_rois = afd.read_or_templates() bundles = BundleDict({ "L_OR": { "include": [ or_rois["left_OR_1"], # these can be paths to Nifti files or_rois["left_OR_2"]], # or they can Nifti images "exclude": [ or_rois["left_OP_MNI"], or_rois["left_TP_MNI"], or_rois["left_pos_thal_MNI"]], "start": or_rois['left_thal_MNI'], "end": or_rois['left_V1_MNI'], "cross_midline": False, }, "R_OR": { "include": [ or_rois["right_OR_1"], or_rois["right_OR_2"]], "exclude": [ or_rois["right_OP_MNI"], or_rois["right_TP_MNI"], or_rois["right_pos_thal_MNI"]], "start": or_rois['right_thal_MNI'], "end": or_rois['right_V1_MNI'], "cross_midline": False } }) Note ---- The order of include ROIs may affect your results. This is because ROI order within the include list determines the orientation of the streamlines (from the first to the last) and the clipping of streamlines when `clip_edges` is used, because the streamlines are clipped to between the first and last ROIs. """ def __init__( self, bundle_info, resample_to=None, resample_subject_to=False, keep_in_memory=False, citations=None, criteria_for_all=None, ): if not (isinstance(bundle_info, dict)): raise TypeError( (f"bundle_info must be a dict, currently a {type(bundle_info)}") ) if resample_to is None: resample_to = afd.read_mni_template()
[docs] self.resample_to = resample_to
[docs] self.resample_subject_to = resample_subject_to
[docs] self.keep_in_memory = keep_in_memory
[docs] self.citations = citations
if self.citations is None: self.citations = set()
[docs] self._dict = {}
[docs] self.bundle_names = []
for key, item in bundle_info.items(): if criteria_for_all is not None: for criterion, value in criteria_for_all.items(): if criterion not in item: item[criterion] = value self.__setitem__(key, item)
[docs] self.logger = logging.getLogger("AFQ")
if ( "Forceps Major" in self.bundle_names and "Callosum Occipital" in self.bundle_names ): self.logger.info( ( "Forceps Major and Callosum Occipital bundles" " are co-located, and AFQ" " assigns each streamline to only one bundle." " Only Callosum Occipital will be used." ) ) del self["Forceps Major"] if ( "Forceps Minor" in self.bundle_names and "Callosum Orbital" in self.bundle_names ): self.logger.info( ( "Forceps Minor and Callosum Orbital bundles" " are co-located, and AFQ" " assigns each streamline to only one bundle." " Only Callosum Orbital will be used." ) ) del self["Forceps Minor"] if ( "Forceps Minor" in self.bundle_names and "Callosum Anterior Frontal" in self.bundle_names ): self.logger.info( ( "Forceps Minor and Callosum Anterior Frontal bundles" " are co-located, and AFQ" " assigns each streamline to only one bundle." " Only Callosum Anterior Frontal will be used." ) ) del self["Forceps Minor"]
[docs] def __print__(self): print(self._dict)
[docs] def _use_bids_info(self, roi_or_sl, bids_layout, bids_path, subject, session): if isinstance(roi_or_sl, dict) and "roi" not in roi_or_sl: suffix = roi_or_sl.get("suffix", "dwi") roi_or_sl = find_file( bids_layout, bids_path, roi_or_sl, suffix, session, subject ) return roi_or_sl else: return roi_or_sl
[docs] def _cond_load(self, roi_or_sl, resample_to): """ Load ROI or streamline if not already loaded """ if isinstance(roi_or_sl, dict): space = roi_or_sl.get("space", None) roi_or_sl = roi_or_sl.get("roi", None) if roi_or_sl is None or space is None: raise ValueError( ( f"Unclear ROI definition for {roi_or_sl}. " "See 'Defining Custom Bundle Dictionaries' " "in the documentation for details." ) ) if space == "template": resample_to = self.resample_to elif space == "subject": resample_to = self.resample_subject_to if resample_to is False: raise ValueError( ( "When using mixed ROI bundle definitions, " "and subject space ROIs, " "resample_subject_to cannot be False." ) ) else: raise ValueError( ( f"Unknown space {space} for ROI definition {roi_or_sl}. " "See 'Defining Custom Bundle Dictionaries' " "in the documentation for details." ) ) logger.debug(f"Loading ROI or streamlines: {roi_or_sl}") logger.debug(f"Loading ROI or streamlines from space: {resample_to}") if isinstance(roi_or_sl, str): if ".nii" in roi_or_sl: return afd.read_resample_roi(roi_or_sl, resample_to=resample_to) else: return load_tractogram( roi_or_sl, "same", bbox_valid_check=False ).streamlines elif isinstance(roi_or_sl, nib.Nifti1Image): return afd.read_resample_roi(roi_or_sl, resample_to=resample_to) else: return roi_or_sl
[docs] def get_b_info(self, b_name): return self._dict[b_name]
[docs] def relax_cleaning(self, delta_distance=1, delta_length=1): """ This can be useful for PTT """ cleaner_keys = ["mahal", "isolation_forest", "orient_mahal"] for b_name in self.bundle_names: bundle_data = self._dict[b_name] for key in cleaner_keys: if key in bundle_data: dt = bundle_data[key].get("distance_threshold", 0) if dt != 0: bundle_data[key]["distance_threshold"] = dt + delta_distance lt = bundle_data[key].get("length_threshold", 0) if lt != 0: bundle_data[key]["length_threshold"] = lt + delta_length if "ORG_spectral_subbundles" in bundle_data: bundle_data["ORG_spectral_subbundles"].relax_cleaning( delta_distance, delta_length )
[docs] def __getitem__(self, key): if isinstance(key, tuple) or isinstance(key, list): # Generates a copy of this BundleDict with only the bundle names # from the tuple/list new_bd = {} for b_name in key: if b_name in self._dict: new_bd[b_name] = self._dict[b_name] else: raise ValueError(f"{b_name} is not in this BundleDict") return self.__class__( new_bd, resample_to=self.resample_to, resample_subject_to=self.resample_subject_to, keep_in_memory=self.keep_in_memory, ) else: if not self.keep_in_memory: _item = self._dict[key].copy() _res = self._cond_load_bundle(key, dry_run=True) if _res is not None: _item.update(_res) _item = _BundleEntry(_item) else: if "loaded" not in self._dict[key] or not self._dict[key]["loaded"]: self._cond_load_bundle(key) self._dict[key]["loaded"] = True if ( "resampled" not in self._dict[key] or not self._dict[key]["resampled"] ): self._resample_roi(key) _item = _BundleEntry(self._dict[key].copy()) return _item
[docs] def __setitem__(self, key, item): self._dict[key] = item if key not in self.bundle_names: self.bundle_names.append(key)
[docs] def __len__(self): return len(self.bundle_names)
[docs] def __delitem__(self, key): if key not in self._dict and key not in self.bundle_names: raise KeyError(f"{key} not found") if key in self._dict: del self._dict[key] else: raise RuntimeError( (f"{key} not found in internal dictionary, but found in bundle_names") ) if key in self.bundle_names: self.bundle_names.remove(key) else: raise RuntimeError( (f"{key} not found in bundle_names, but found in internal dictionary") )
[docs] def __iter__(self): return iter(self._dict)
[docs] def copy(self): """ Generates a copy of this BundleDict where the internal dictionary is a copy of this BundleDict's internal dictionary. Useful if you want to add or remove bundles from a copy of a BundleDict. Returns --------- bundle_dict : BundleDict Euclidean norms of vectors. """ return self.__class__( self._dict.copy(), resample_to=self.resample_to, resample_subject_to=self.resample_subject_to, keep_in_memory=self.keep_in_memory, )
[docs] def apply_to_rois(self, b_name, *args, **kwargs): """ See: AFQ.api.bundle_dict.apply_to_roi_dict Parameters ---------- b_name : name bundle name of bundle whose ROIs will be transformed. """ return apply_to_roi_dict(self._dict[b_name], *args, **kwargs)
[docs] def _cond_load_bundle(self, b_name, dry_run=False): """ Given a bundle name, resample all ROIs and prob maps into either template or subject space for that bundle, depending on its "space" attribute. Parameters ---------- b_name : str Name of the bundle to be resampled. """ if self.is_bundle_in_template(b_name): resample_to = self.resample_to else: resample_to = self.resample_subject_to return self.apply_to_rois( b_name, self._cond_load, resample_to, dry_run=dry_run, apply_to_recobundles=True, )
[docs] def is_bundle_in_template(self, bundle_name): return ( "space" not in self._dict[bundle_name] or self._dict[bundle_name]["space"] == "template" or self._dict[bundle_name]["space"] == "mixed" )
[docs] def _roi_transform_helper(self, roi_or_sl, mapping, new_img): roi_or_sl = self._cond_load(roi_or_sl, self.resample_to) if isinstance(roi_or_sl, nib.Nifti1Image): if ( np.allclose(roi_or_sl.affine, new_img.affine) and roi_or_sl.shape == new_img.shape[:3] ): # This is the case of a mixed bundle definition, where # some ROIs need transformed and others do not return roi_or_sl fdata = roi_or_sl.get_fdata() if len(np.unique(fdata)) <= 2: boolean_ = True else: boolean_ = False warped_img = auv.transform_roi(fdata, mapping, boolean_) warped_img = nib.Nifti1Image(warped_img, new_img.affine) return warped_img else: return roi_or_sl
[docs] def transform_rois( self, bundle_name, mapping, new_img, base_fname=None, to_space="subject", apply_to_recobundles=False, ): """ Get the bundle definition with transformed ROIs for a given bundle into a given subject space using a given mapping. Will only run on bundles which are in template space, otherwise will just return the bundle definition without transformation. Parameters ---------- bundle_name : str Name of the bundle to be transformed. mapping : DiffeomorphicMap object A mapping between DWI space and a template. new_img : Nifti1Image Image of space transformed into. base_fname : str, optional Base file path to construct file path from. Additional BIDS descriptors will be added to this file path. If None, no file paths returned. to_space : str, optional Name for space for exported ROIs. Only used if base_fname is not None. Default: "subject" apply_to_recobundles : bool, optional Whether to apply the transformation to recobundles TRKs as well. Default: False Returns ------- If base_fname is None, a dictionary where keys are the roi type and values are the transformed ROIs. Otherwise, a list of file names and a list of transformed ROIs are returned. """ if self.is_bundle_in_template(bundle_name): transformed_rois = self.apply_to_rois( bundle_name, self._roi_transform_helper, mapping, new_img, dry_run=True, apply_to_recobundles=apply_to_recobundles, ) else: transformed_rois = self.apply_to_rois( bundle_name, self._cond_load, self.resample_subject_to, dry_run=True, apply_to_recobundles=apply_to_recobundles, ) if base_fname is not None: fnames = [] list_of_rois_to_save = [] for roi_type, rois in transformed_rois.items(): if roi_type == "prob_map": suffix = "probseg" roi_type_name = "" else: suffix = "mask" roi_type_name = ( roi_type.lower() .replace(" ", "") .replace("_", "") .replace("-", "") ) roi_type_name = roi_type_name[0].upper() + roi_type_name[1:] if not isinstance(rois, list): rois = [rois] for ii, roi in enumerate(rois): desc = f"{str_to_desc(bundle_name)}{roi_type_name}" if roi_type in ["include", "exclude"]: desc = f"{desc}{ii}" fname = get_fname( base_fname, f"_space-{to_space}_desc-{desc}_{suffix}.nii.gz", "ROIs", ) fnames.append(fname) list_of_rois_to_save.append(roi) return list_of_rois_to_save, fnames else: return transformed_rois
[docs] def __add__(self, other): for resample in ["resample_to", "resample_subject_to"]: if getattr(self, resample) == getattr(other, resample): pass elif ( not getattr(self, resample) or not getattr(other, resample) or getattr(self, resample) is None or getattr(other, resample) is None ): if getattr(self, resample) != getattr(other, resample): raise ValueError( ( f"Adding BundleDicts where {resample} do not match." f"{resample}'s are {getattr(self, resample)} and " f"{getattr(other, resample)}" ) ) else: if not np.allclose( getattr(self, resample).affine, getattr(other, resample).affine ): raise ValueError( ( f"Adding BundleDicts where {resample} affines" f" do not match. {resample} affines are" f"{getattr(self, resample).affine} and " f"{getattr(other, resample).affine}" ) ) if not np.allclose( getattr(self, resample).header["dim"], getattr(other, resample).header["dim"], ): raise ValueError( ( f"Adding BundleDicts where {resample} dimensions" f" do not match. {resample} dimensions are" f"{getattr(self, resample).header['dim']} and " f"{getattr(other, resample).header['dim']}" ) ) return self.__class__( {**self._dict, **other._dict}, self.resample_to, self.resample_subject_to, self.keep_in_memory, self.citations | other.citations, )
class SpectralSubbundleDict(BundleDict): """ A BundleDict where each bundle is defined as a spectral subbundle of a larger bundle. See `Defining Custom Bundle Dictionaries` in the documentation for details. """ def __init__( self, bundle_info, resample_to=None, resample_subject_to=False, keep_in_memory=False, citations=None, remove_cluster_IDs=None, criteria_for_all=None, ): super().__init__( bundle_info, resample_to, resample_subject_to, keep_in_memory, citations ) if remove_cluster_IDs is None: remove_cluster_IDs = [] self.remove_cluster_IDs = remove_cluster_IDs self.cluster_IDs = [] for b_name, b_info in bundle_info.items(): if "cluster_IDs" not in b_info: raise ValueError( ( f"Bundle {b_name} does not have cluster_IDs. " "All bundles in a SpectralSubbundleDict must have cluster_IDs." ) ) self.cluster_IDs.extend(b_info["cluster_IDs"]) if criteria_for_all is not None: for criterion, value in criteria_for_all.items(): if criterion not in b_info: b_info[criterion] = value self.all_cluster_IDs = self.remove_cluster_IDs + self.cluster_IDs def apply_to_roi_dict( dict_, func, *args, dry_run=False, apply_to_recobundles=False, apply_to_prob_map=True, **kwargs, ): """ Applies some transformation to all ROIs (include, exclude, end, start) and the prob_map in a given bundle. Parameters ---------- dict_: dict dict describing the bundle using pyAFQ's format. An entry in a BundleDict. func : function function whose first argument must be a Nifti1Image and which returns a Nifti1Image dry_run : bool Whether to actually apply changes returned by `func` to the ROIs. If has_return is False, dry_run is not used. apply_to_recobundles : bool, optional Whether to apply the transformation to recobundles TRKs as well. Default: False apply_to_prob_map : bool, optional Whether to apply the transformation to the prob_map. Default: True *args : Additional arguments for func **kwargs Optional arguments for func Returns ------- A dictionary where keys are the roi type and values are the transformed ROIs. """ return_vals = {} roi_types = ["include", "exclude", "start", "end"] if apply_to_prob_map: roi_types.append("prob_map") for roi_type in roi_types: if roi_type in dict_: if roi_type in ["start", "end", "prob_map"]: return_vals[roi_type] = func(dict_[roi_type], *args, **kwargs) else: changed_rois = [] for _roi in dict_[roi_type]: changed_rois.append(func(_roi, *args, **kwargs)) return_vals[roi_type] = changed_rois if apply_to_recobundles and "recobundles" in dict_: return_vals["recobundles"] = {} for sl_type in ["sl", "centroid"]: return_vals["recobundles"][sl_type] = func( dict_["recobundles"][sl_type], *args, **kwargs ) if not dry_run: for roi_type, roi in return_vals.items(): dict_[roi_type] = roi return return_vals