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()
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 __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