Source code for AFQ.tasks.tissue

import logging

import immlib
import nibabel as nib
import numpy as np
from dipy.align import resample
from dipy.core.gradients import unique_bvals_tolerance
from dipy.data import get_sphere
from dipy.reconst.mcsd import (
    mask_for_response_msmt,
    multi_shell_fiber_response,
    response_from_mask_msmt,
)

from AFQ.definitions.image import PVEImage, PVEImages
from AFQ.models.asym_filtering import (
    compute_asymmetry_index,
    compute_nufid_asym,
    compute_odd_power_map,
    unified_filtering,
)
from AFQ.models.msmt import MultiShellDeconvModel
from AFQ.models.QBallTP import anisotropic_power
from AFQ.models.wmgm_interface import fit_wm_gm_interface
from AFQ.nn.brainchop import pve_from_subcortex
from AFQ.nn.multiaxial import extract_pve
from AFQ.nn.synthseg import pve_from_synthseg
from AFQ.tasks.decorators import as_file, as_fit_deriv, as_img
from AFQ.tasks.utils import with_name

[docs] logger = logging.getLogger("AFQ")
@immlib.calc("wm_gm_interface") @as_file(suffix="_desc-wmgmi_mask.nii.gz")
[docs] def wm_gm_interface(pve_internal, data_imap): """ full path to a nifti file containing the white matter/gray matter interface """ PVE_img = nib.load(pve_internal) b0_img = nib.load(data_imap["b0"]) wmgmi_img = fit_wm_gm_interface(PVE_img, b0_img) return wmgmi_img, dict(FromPVE=pve_internal)
@immlib.calc("pve_internal") @as_file(suffix="_desc-pve_probseg.nii.gz")
[docs] def pve_internal(structural_imap, pve="synthseg"): """ WM+GM+CSF segmentation Parameters ---------- pve : str or PVEImage, optional Method to use for PVE estimation. Can be a string defining a built-in method from neural networks, or a Definition object to import the PVE. Importing a PVE from software like Freesurfer or FSL FAST is recommended if they are available. The built-in methods are "synthseg" or "multiaxial+brainchop". "synthseg" uses SynthSeg2 [1] to get the PVE. "multiaxial+brainchop" uses MultiAxial [2] and BrainChop [3] segmentations to get the PVE. Note this requires downloading the pre-trained multi-axial model which is licensed with Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International. Default: "synthseg" """ if isinstance(pve, str): if pve == "synthseg": logger.warning( ( "Using SynthSeg2 for PVE estimation. " "This may use considerable memory resources. " ) ) synthseg_seg = nib.load(structural_imap["synthseg_model"]) PVE = pve_from_synthseg(synthseg_seg.get_fdata()) return nib.Nifti1Image(PVE, synthseg_seg.affine), dict( SynthsegParcellation=structural_imap["synthseg_model"], labels=["csf", "gm", "wm"], ) elif pve == "multiaxial+brainchop": logger.warning( ( "Using MultiAxial+BrainChop for PVE estimation. " "MultiAxial requires downloading " "the pre-trained multi-axial model which is licensed with " "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 " "International." ) ) t1_subcortex_img = nib.load(structural_imap["t1_subcortex"]) mx_model_img = nib.load(structural_imap["mx_model"]) PVE_brainchop = pve_from_subcortex(t1_subcortex_img.get_fdata()).astype( np.float32 ) PVE_multiaxial = extract_pve(mx_model_img).get_fdata().astype(np.float32) # Use predictions from both to get final estimates PVE = (PVE_brainchop + PVE_multiaxial) / 2 return nib.Nifti1Image(PVE, t1_subcortex_img.affine), dict( SubCortexParcellation=structural_imap["t1_subcortex"], MultiAxialSegmentation=structural_imap["mx_model"], labels=["csf", "gm", "wm"], ) raise ValueError( "pve must be a PVEImage, PVEImages, 'synthseg', or 'multiaxial+brainchop'" )
@immlib.calc("msmtcsd_params", "msmtcsd_gm", "msmtcsd_csf") @as_file( suffix=[ "_model-msmtcsd_param-wm_dwimap.nii.gz", "_model-msmtcsd_param-gm_dwimap.nii.gz", "_model-msmtcsd_param-csf_dwimap.nii.gz", ], subfolder="models", ) @as_img
[docs] def msmt_params( structural_imap, data_imap, pve_internal, citations, msmt_sh_order=8, msmt_fa_thr=0.7, ): """ full path to a nifti file containing parameters for the MSMT CSD white matter fit, full path to a nifti file containing parameters for the MSMT CSD gray matter fit, full path to a nifti file containing parameters for the MSMT CSD cerebrospinal fluid fit Parameters ---------- msmt_sh_order : int, optional. Spherical harmonic order to use for the MSMT CSD fit. Default: 8 msmt_fa_thr : float, optional. The threshold on the FA used to calculate the multi shell auto response. Can be useful to reduce for baby subjects. Default: 0.7 References ---------- .. [1] B. Jeurissen, J.-D. Tournier, T. Dhollander, A. Connelly, and J. Sijbers. Multi-tissue constrained spherical deconvolution for improved analysis of multi-shell diffusion MRI data. NeuroImage, 103 (2014), pp. 411–426 """ citations.add("jeurissen2014multi") mask = nib.load(data_imap["brain_mask"]).get_fdata() pve_img = nib.load(pve_internal) pve_data = pve_img.get_fdata() csf = resample( moving=pve_data[..., 0], static=data_imap["data"][..., 0], moving_affine=pve_img.affine, static_affine=data_imap["dwi_affine"], ).get_fdata() gm = resample( moving=pve_data[..., 1], static=data_imap["data"][..., 0], moving_affine=pve_img.affine, static_affine=data_imap["dwi_affine"], ).get_fdata() wm = resample( moving=pve_data[..., 2], static=data_imap["data"][..., 0], moving_affine=pve_img.affine, static_affine=data_imap["dwi_affine"], ).get_fdata() mask_wm, mask_gm, mask_csf = mask_for_response_msmt( data_imap["gtab"], data_imap["data"], roi_radii=10, wm_fa_thr=msmt_fa_thr, gm_fa_thr=0.3, csf_fa_thr=0.15, gm_md_thr=0.001, csf_md_thr=0.0032, ) mask_wm *= wm > 0.5 mask_gm *= gm > 0.5 mask_csf *= csf > 0.5 response_wm, response_gm, response_csf = response_from_mask_msmt( data_imap["gtab"], data_imap["data"], mask_wm, mask_gm, mask_csf ) ubvals = unique_bvals_tolerance(data_imap["gtab"].bvals) response_mcsd = multi_shell_fiber_response( msmt_sh_order, ubvals, response_wm, response_gm, response_csf ) mcsd_model = MultiShellDeconvModel(data_imap["gtab"], response_mcsd) logger.info("Fitting Multi-Shell CSD model...") mcsd_fit = mcsd_model.fit(data_imap["data"], mask) def _get_meta(desc, sh_order, response): return dict( Model=dict( Description=( "Multi-Shell Multi-Tissue (MSMT) " "Constrained Spherical Deconvolution (CSD)" ), URL="https://mrtrix.readthedocs.io/en/latest/constrained_spherical_deconvolution/multi_shell_multi_tissue_csd.html", ), Description=desc, NonNegativity="constrained", OrientationEncoding=dict( EncodingAxis=3, Reference="xyz", SphericalHarmonicBasis="descoteaux", SphericalHarmonicDegree=sh_order, Type="sh", ), ResponseFunction=dict(Coefficients=response, Type="eigen"), ) meta_wm = _get_meta( "White matter", msmt_sh_order, response_wm[0].tolist(), ) meta_wm["ParameterURL"] = ( "http://www.sciencedirect.com/science/article/pii/S1053811911012092" ) meta_gm = _get_meta( "Gray matter", 0, response_gm[0].tolist(), ) meta_csf = _get_meta( "Cerebro-spinal fluid", 0, response_csf[0].tolist(), ) return [ (mcsd_fit.shm_coeff, meta_wm), (mcsd_fit.volume_fractions[1], meta_gm), (mcsd_fit.volume_fractions[0], meta_csf), ]
@immlib.calc("msmt_apm") @as_file(suffix="_model-msmtcsd_param-apm_dwimap.nii.gz", subfolder="models") @as_fit_deriv("MSMTCSD")
[docs] def msmt_apm(msmtcsd_params): """ full path to a nifti file containing the anisotropic power map """ sh_coeff = nib.load(msmtcsd_params).get_fdata() pmap = anisotropic_power(sh_coeff) return pmap, {"Description": "Anisotropic Power Map"}
@immlib.calc("msmt_aodf_params") @as_file(suffix="_model-msmtcsd_param-aodf_dwimap.nii.gz", subfolder="models") @as_img
[docs] def msmt_aodf(msmtcsd_params, structural_imap, tracking_params, citations): """ full path to a nifti file containing MSMT CSD ODFs filtered by unified filtering [1] References ---------- [1] Poirier and Descoteaux, 2024, "A Unified Filtering Method for Estimating Asymmetric Orientation Distribution Functions", Neuroimage, https://doi.org/10.1016/j.neuroimage.2024.120516 """ citations.add("poirier2024unified") citations.add("renauld2026tractography") sh_coeff = nib.load(msmtcsd_params).get_fdata() logger.info("Applying unified filtering to generate asymmetric MSMT CSD ODFs...") aodf = unified_filtering( sh_coeff, get_sphere(name=tracking_params["sphere"]), n_threads=structural_imap["n_threads"], low_mem=structural_imap["low_mem"], ) return aodf, dict( Model=dict( Description=( "ODFs for Asymmetric MSMT Constrained " "Spherical Deconvolution (aMSMTCSD)" ), URL="https://doi.org/10.1016/j.neuroimage.2024.120516", ), Description="White matter", OrientationEncoding=dict( EncodingAxis=3, Reference="xyz", AntipodalSymmetry=False, Type="odf", Sphere=tracking_params["sphere"], ), Source=msmtcsd_params, )
@immlib.calc("msmt_aodf_asi") @as_file(suffix="_model-msmtcsd_param-asi_dwimap.nii.gz", subfolder="models") @as_fit_deriv("MSMT_AODF")
[docs] def msmt_aodf_asi(msmt_aodf_params, data_imap): """ full path to a nifti file containing the MSMT CSD Asymmetric Index (ASI) [1] References ---------- [1] S. Cetin Karayumak, E. Özarslan, and G. Unal, "Asymmetric Orientation Distribution Functions (AODFs) revealing intravoxel geometry in diffusion MRI" Magnetic Resonance Imaging, vol. 49, pp. 145-158, Jun. 2018, doi: https://doi.org/10.1016/j.mri.2018.03.006. """ aodf = nib.load(msmt_aodf_params).get_fdata() brain_mask = nib.load(data_imap["brain_mask"]).get_fdata().astype(bool) asi = compute_asymmetry_index(aodf, brain_mask) return asi, {"Description": "Asymmetry Index"}
@immlib.calc("msmt_aodf_opm") @as_file(suffix="_model-msmtcsd_param-opm_dwimap.nii.gz", subfolder="models") @as_fit_deriv("MSMT_AODF")
[docs] def msmt_aodf_opm(msmt_aodf_params, data_imap): """ full path to a nifti file containing the MSMT CSD odd-power map [1] References ---------- [1] C. Poirier, E. St-Onge, and M. Descoteaux, "Investigating the Occurrence of Asymmetric Patterns in White Matter Fiber Orientation Distribution Functions" [Abstract], In: Proc. Intl. Soc. Mag. Reson. Med. 29 (2021), 2021 May 15-20, Vancouver, BC, Abstract number 0865. """ aodf = nib.load(msmt_aodf_params).get_fdata() brain_mask = nib.load(data_imap["brain_mask"]).get_fdata().astype(bool) opm = compute_odd_power_map(aodf, brain_mask) return opm, {"Description": "Odd Power Map"}
@immlib.calc("msmt_aodf_nufid") @as_file(suffix="_model-msmtcsd_param-nufid_dwimap.nii.gz", subfolder="models") @as_fit_deriv("MSMT_AODF")
[docs] def msmt_aodf_nufid(msmt_aodf_params, data_imap, tracking_params, pve_internal): """ full path to a nifti file containing the MSMT CSD Number of fiber directions (nufid) map [1] References ---------- [1] C. Poirier and M. Descoteaux, "Filtering Methods for Asymmetric ODFs: Where and How Asymmetry Occurs in the White Matter." bioRxiv. 2022 Jan 1; 2022.12.18.520881. doi: https://doi.org/10.1101/2022.12.18.520881 """ pve_img = nib.load(pve_internal) pve_data = pve_img.get_fdata() aodf_img = nib.load(msmt_aodf_params) aodf = aodf_img.get_fdata() csf = resample( moving=pve_data[..., 0], static=aodf[..., 0], moving_affine=pve_img.affine, static_affine=aodf_img.affine, ).get_fdata() brain_mask = nib.load(data_imap["brain_mask"]).get_fdata().astype(bool) logger.info("Number of fiber directions (nufid) map from AODF...") nufid = compute_nufid_asym( aodf, get_sphere(name=tracking_params["sphere"]), csf, brain_mask ) return nufid, {"Description": "Number of Fiber Directions"}
@immlib.calc("csd_aodf_nufid") @as_file(suffix="_model-csd_param-nufid_dwimap.nii.gz", subfolder="models") @as_img
[docs] def csd_aodf_nufid(data_imap, pve_internal, tracking_params): """ full path to a nifti file containing the CSD Number of fiber directions (nufid) map [1] References ---------- [1] C. Poirier and M. Descoteaux, "Filtering Methods for Asymmetric ODFs: Where and How Asymmetry Occurs in the White Matter." bioRxiv. 2022 Jan 1; 2022.12.18.520881. doi: https://doi.org/10.1101/2022.12.18.520881 """ pve_img = nib.load(pve_internal) pve_data = pve_img.get_fdata() aodf_img = nib.load(data_imap["csd_aodf_params"]) aodf = aodf_img.get_fdata() csf = resample( moving=pve_data[..., 0], static=aodf[..., 0], moving_affine=pve_img.affine, static_affine=aodf_img.affine, ).get_fdata() brain_mask = nib.load(data_imap["brain_mask"]).get_fdata().astype(bool) logger.info("Number of fiber directions (nufid) map from AODF...") nufid = compute_nufid_asym( aodf, get_sphere(name=tracking_params["sphere"]), csf, brain_mask ) return nufid, dict( Model=dict( Description=( "ODFs for Asymmetric Constrained Spherical Deconvolution (aCSD)" ), URL="https://doi.org/10.1016/j.neuroimage.2024.120516", ), Source=data_imap["csd_aodf_params"], Description="Number of Fiber Directions", )
[docs] def get_tissue_plan(kwargs): tissue_tasks = with_name( [ pve_internal, wm_gm_interface, msmt_params, msmt_apm, msmt_aodf, msmt_aodf_asi, msmt_aodf_opm, msmt_aodf_nufid, csd_aodf_nufid, ] ) pve = kwargs.get("pve", None) if isinstance(pve, PVEImages): probseg_func = pve.get_image_getter("tissue") tissue_tasks["csf_res"] = immlib.calc("pve_csf")( as_file("_desc-csf_probseg.nii.gz", subfolder="models")( pve.probseg_funcs[0] ) ) tissue_tasks["gm_res"] = immlib.calc("pve_gm")( as_file("_desc-gm_probseg.nii.gz", subfolder="models")(pve.probseg_funcs[1]) ) tissue_tasks["wm_res"] = immlib.calc("pve_wm")( as_file("_desc-wm_probseg.nii.gz", subfolder="models")(pve.probseg_funcs[2]) ) tissue_tasks["pve_internal_res"] = immlib.calc("pve_internal")( as_file("_desc-pve_probseg.nii.gz")(probseg_func) ) elif isinstance(pve, PVEImage): tissue_tasks["pve_internal_res"] = immlib.calc("pve_internal")( as_file("_desc-pve_probseg.nii.gz")(pve.get_image_getter("tissue")) ) else: logger.warning( "It is recommended to provide CSF/GM/WM " "segmentations using PVEImage or PVEImages " "in AFQ.definitions.image. Otherwise, " "SynthSeg2 will be used" ) return immlib.plan(**tissue_tasks)