From d144e4665ce1777ca6fdc38d0f3caf2569134e3c Mon Sep 17 00:00:00 2001 From: Steven Meisler Date: Tue, 25 Mar 2025 14:54:49 -0400 Subject: [PATCH 1/5] Add `smoothing` and `otsu_threshold` autotrack arguments (#219) * add smoothing and otsu_threshold to dsistudio autotrack * Add smoothing and otsu to dsi_studio.py argstring Co-authored-by: Taylor Salo * finish linting interfaces/dsi_studio.py --------- Co-authored-by: Taylor Salo --- qsirecon/interfaces/dsi_studio.py | 12 ++++++++++++ qsirecon/workflows/recon/dsi_studio.py | 14 ++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/qsirecon/interfaces/dsi_studio.py b/qsirecon/interfaces/dsi_studio.py index 5c661623..6ebdbf9c 100644 --- a/qsirecon/interfaces/dsi_studio.py +++ b/qsirecon/interfaces/dsi_studio.py @@ -722,6 +722,18 @@ class _AutoTrackInputSpec(DSIStudioCommandLineInputSpec): template = traits.Int( 0, usedefault=True, argstr="--template=%d", desc="Must be 0 for autotrack" ) + smoothing = traits.Float( + 0, + usedefault=False, + argstr="--smoothing=%.10f", + desc="Smoothing", + ) + otsu_threshold = traits.Float( + 0.6, + usedefault=False, + argstr="--otsu_threshold=%.10f", + desc="The ratio of otsu threshold to derive default anisotropy threshold.", + ) _boilerplate_traits = [ "track_id", "track_voxel_ratio", diff --git a/qsirecon/workflows/recon/dsi_studio.py b/qsirecon/workflows/recon/dsi_studio.py index ceba7468..e71f4e99 100644 --- a/qsirecon/workflows/recon/dsi_studio.py +++ b/qsirecon/workflows/recon/dsi_studio.py @@ -377,6 +377,20 @@ def init_dsi_studio_autotrack_wf( This rate will be used to terminate tracking early if DSI Studio finds that the fiber tracking is not generating results. (default: 0.00001) + smoothing: float + Smoothing serves like a “momentum”. For example, if smoothing is 0, the + propagation direction is independent of the previous incoming direction. + If the smoothing is 0.5, each moving direction remains 50% of the “momentum”, + which is the previous propagation vector. This function makes the tracks + appear smoother. In implementation detail, there is a weighting sum on every + two consecutive moving directions. For smoothing value 0.2, each subsequent + direction has 0.2 weightings contributed from the previous moving direction + and 0.8 contributed from the income direction. To disable smoothing set + its value to 0. Assign 1.0 to do a random selection of the value from 0% to 95%. + + otsu_threshold: float + The ratio of otsu threshold to derive default anisotropy threshold. + model_name: str The name of the model used for ODFs (default "gqi") """ From 8b9f08bb9b55ace9374a9c232db14ecda93ecc53 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Wed, 26 Mar 2025 10:14:58 -0400 Subject: [PATCH 2/5] Allow desc entity in recon scalar derivatives (#220) * Try raising on desc issue. * Update dipy.py * Update recon_scalars.py * Fix? * Update recon_scalars.py * Revert new desc entity. --- qsirecon/interfaces/recon_scalars.py | 6 ++++++ qsirecon/workflows/recon/dipy.py | 2 +- qsirecon/workflows/recon/utils.py | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/qsirecon/interfaces/recon_scalars.py b/qsirecon/interfaces/recon_scalars.py index ee0b7485..18801275 100644 --- a/qsirecon/interfaces/recon_scalars.py +++ b/qsirecon/interfaces/recon_scalars.py @@ -270,6 +270,11 @@ class _OrganizeScalarDataOutputSpec(TraitedSpec): traits.Str(), Undefined, ) + desc = traits.Either( + traits.Str(), + Undefined, + None, + ) class OrganizeScalarData(SimpleInterface): @@ -282,5 +287,6 @@ def _run_interface(self, runtime): self._results["metadata"] = scalar_config.get("metadata", {}) self._results["model"] = scalar_config.get("bids", {}).get("model", Undefined) self._results["param"] = scalar_config.get("bids", {}).get("param", Undefined) + self._results["desc"] = scalar_config.get("bids", {}).get("desc", None) return runtime diff --git a/qsirecon/workflows/recon/dipy.py b/qsirecon/workflows/recon/dipy.py index 0693c91a..419673c7 100644 --- a/qsirecon/workflows/recon/dipy.py +++ b/qsirecon/workflows/recon/dipy.py @@ -460,7 +460,7 @@ def init_dipy_mapmri_recon_wf( omp_nthreads = config.nipype.omp_nthreads recon_map = pe.Node(MAPMRIReconstruction(**params), name="recon_map") recon_scalars = pe.Node( - DIPYMAPMRIReconScalars(qsirecon_suffix=name), + DIPYMAPMRIReconScalars(dismiss_entities=["desc"], qsirecon_suffix=name), name="recon_scalars", run_without_submitting=True, ) diff --git a/qsirecon/workflows/recon/utils.py b/qsirecon/workflows/recon/utils.py index 054cf997..5980e4a1 100644 --- a/qsirecon/workflows/recon/utils.py +++ b/qsirecon/workflows/recon/utils.py @@ -174,7 +174,7 @@ def init_scalar_output_wf( suffix="dwimap", extension="nii.gz", ), - iterfield=["in_file", "meta_dict", "model", "param"], + iterfield=["in_file", "meta_dict", "model", "param", "desc"], name="ds_scalar", run_without_submitting=True, ) @@ -188,6 +188,7 @@ def init_scalar_output_wf( ("metadata", "meta_dict"), ("model", "model"), ("param", "param"), + ("desc", "desc"), ]), (ds_scalar, outputnode, [("out_file", "scalar_files")]), ]) # fmt:skip From 099e2131937b8859cbf93bc8f5cbc30503b09655 Mon Sep 17 00:00:00 2001 From: araikes Date: Thu, 3 Apr 2025 10:29:35 -0700 Subject: [PATCH 3/5] Tissue fraction modulated ICVF and OD maps (#218) * Add AMICO modulated maps and RMSE to output - AMICO produces ICVF and OD maps modulated by the non isotropic tissue fraction. This makes these available. - AMICO also produces (N)RMSE maps of the observed vs predicted signal. Saving these enables QC and other assessments of parameter selection. * Add outputs to NODDI output spec * Make sure modulated files are available for FIBGZ * fix typo * still fixing * Update converters.py * Fix report documentation. * Update amico_noddi.yaml Change naming structure so that "desc" isn't used. * fix reports * last report typos * Now final typo * fix formatting issues * Other whitespace * Update amico_noddi.csv * Hopefully final linting errors * Update builtin_workflows.rst Adds reference to Parker for use of tissue fraction modulated outputs. * Updates to use desc field * Update qsirecon/workflows/recon/amico.py Co-authored-by: Taylor Salo * Fix tests * Make linter happy * calculate tissue fraction * tf in expected outputs * json + lint --------- Co-authored-by: Taylor Salo Co-authored-by: Taylor Salo Co-authored-by: Matt Cieslak --- docs/builtin_workflows.rst | 10 ++-- docs/recon_scalars/amico_noddi.csv | 4 ++ docs/sphinxext/github_link.py | 30 ++++++------ qsirecon/data/boilerplate.bib | 14 ++++++ qsirecon/data/scalars/amico_noddi.yaml | 32 +++++++++++++ qsirecon/interfaces/amico.py | 43 ++++++++++++++++- qsirecon/interfaces/converters.py | 25 ++++++++-- qsirecon/tests/data/amico_noddi_outputs.txt | 10 ++++ qsirecon/workflows/base.py | 2 +- qsirecon/workflows/recon/amico.py | 51 ++++++++++++++++++--- qsirecon/workflows/recon/anatomical.py | 2 +- 11 files changed, 190 insertions(+), 33 deletions(-) diff --git a/docs/builtin_workflows.rst b/docs/builtin_workflows.rst index c25a268a..b0d0a7e1 100644 --- a/docs/builtin_workflows.rst +++ b/docs/builtin_workflows.rst @@ -185,11 +185,15 @@ PyAFQ Outputs =============== This workflow estimates the NODDI :footcite:p:`noddi` model using the implementation from -AMICO :footcite:p:`amico`. Images with intra-cellular volume fraction (ICVF), isotropic volume -fraction (ISOVF), orientation dispersion (OD) are written to outputs. Additionally, a DSI -Studio fib file is created using the peak directions and ICVF as a stand-in for QA to be +AMICO :footcite:p:`amico` and tissue fraction modulation described in :footcite:p:`parker2021not`. +Images with (modulated) intra-cellular volume fraction (ICVF), isotropic volume fraction (ISOVF), +(modulated) orientation dispersion (OD), root mean square error (RMSE) and normalized RMSE are written to outputs. +Additionally, a DSI Studio fib file is created using the peak directions and ICVF as a stand-in for QA to be used for tractography. +Please see Parker 2021 :footcite:p:`parker2021not` for a detailed description of use and application of the +tissue fraction modulated outputs. + Scalar Maps ----------- .. csv-table:: diff --git a/docs/recon_scalars/amico_noddi.csv b/docs/recon_scalars/amico_noddi.csv index 0de4cb8e..ec63fcb5 100644 --- a/docs/recon_scalars/amico_noddi.csv +++ b/docs/recon_scalars/amico_noddi.csv @@ -2,3 +2,7 @@ noddi,direction,Peak directions from NODDI noddi,icvf,Intracellular volume fraction from NODDI noddi,isovf,Isotropic volume fraction from NODDI noddi,od,Orientation dispersion index from NODDI +noddi,modulated icvf,Tissue fraction modulated intracellular volume fraction from NODDI +noddi,modulated od,Tissue fraction modulated orientation dispersion from NODDI +noddi,rmse,Root mean square error between predicted and observed signal from NODDI +noddi,rmse,Normalized RMSE between predicted and observed signal from NODDI diff --git a/docs/sphinxext/github_link.py b/docs/sphinxext/github_link.py index 47ff2a76..3e7e1898 100644 --- a/docs/sphinxext/github_link.py +++ b/docs/sphinxext/github_link.py @@ -2,6 +2,7 @@ This script comes from scikit-learn: https://github.com/scikit-learn/scikit-learn/blob/master/doc/sphinxext/github_link.py """ + from operator import attrgetter import inspect import subprocess @@ -9,16 +10,16 @@ import sys from functools import partial -REVISION_CMD = 'git rev-parse --short HEAD' +REVISION_CMD = "git rev-parse --short HEAD" def _get_git_revision(): try: revision = subprocess.check_output(REVISION_CMD.split()).strip() except (subprocess.CalledProcessError, OSError): - print('Failed to execute git to get revision') + print("Failed to execute git to get revision") return None - return revision.decode('utf-8') + return revision.decode("utf-8") def _linkcode_resolve(domain, info, package, url_fmt, revision): @@ -38,17 +39,17 @@ def _linkcode_resolve(domain, info, package, url_fmt, revision): if revision is None: return - if domain not in ('py', 'pyx'): + if domain not in ("py", "pyx"): return - if not info.get('module') or not info.get('fullname'): + if not info.get("module") or not info.get("fullname"): return - class_name = info['fullname'].split('.')[0] + class_name = info["fullname"].split(".")[0] if type(class_name) != str: # Python 2 only - class_name = class_name.encode('utf-8') - module = __import__(info['module'], fromlist=[class_name]) - obj = attrgetter(info['fullname'])(module) + class_name = class_name.encode("utf-8") + module = __import__(info["module"], fromlist=[class_name]) + obj = attrgetter(info["fullname"])(module) try: fn = inspect.getsourcefile(obj) @@ -62,14 +63,12 @@ def _linkcode_resolve(domain, info, package, url_fmt, revision): if not fn: return - fn = os.path.relpath(fn, - start=os.path.dirname(__import__(package).__file__)) + fn = os.path.relpath(fn, start=os.path.dirname(__import__(package).__file__)) try: lineno = inspect.getsourcelines(obj)[1] except Exception: - lineno = '' - return url_fmt.format(revision=revision, package=package, - path=fn, lineno=lineno) + lineno = "" + return url_fmt.format(revision=revision, package=package, path=fn, lineno=lineno) def make_linkcode_resolve(package, url_fmt): @@ -84,5 +83,4 @@ def make_linkcode_resolve(package, url_fmt): '{path}#L{lineno}') """ revision = _get_git_revision() - return partial(_linkcode_resolve, revision=revision, package=package, - url_fmt=url_fmt) + return partial(_linkcode_resolve, revision=revision, package=package, url_fmt=url_fmt) diff --git a/qsirecon/data/boilerplate.bib b/qsirecon/data/boilerplate.bib index 0e36a8f1..cca3cfde 100644 --- a/qsirecon/data/boilerplate.bib +++ b/qsirecon/data/boilerplate.bib @@ -827,3 +827,17 @@ @article{najdenovska2018vivo url={https://doi.org/10.1038/sdata.2018.270}, doi={10.1038/sdata.2018.270} } + +@article{parker2021not, +title = {Not all voxels are created equal: Reducing estimation bias in regional NODDI metrics using tissue-weighted means}, +journal = {NeuroImage}, +volume = {245}, +pages = {118749}, +year = {2021}, +issn = {1053-8119}, +doi = {https://doi.org/10.1016/j.neuroimage.2021.118749}, +url = {https://www.sciencedirect.com/science/article/pii/S1053811921010211}, +author = {C.S. Parker and T. Veale and M. Bocchetta and C.F. Slattery and I.B. Malone and D.L. Thomas and J.M. Schott and D.M. Cash and H. Zhang}, +keywords = {Diffusion MRI, Microstructure imaging, Region-of-interest, Arithmetic mean, Tissue-weighted mean}, +abstract = {Neurite orientation dispersion and density imaging (NODDI) estimates microstructural properties of brain tissue relating to the organisation and processing capacity of neurites, which are essential elements for neuronal communication. Descriptive statistics of NODDI tissue metrics are commonly analyzed in regions-of-interest (ROI) to identify brain-phenotype associations. Here, the conventional method to calculate the ROI mean weights all voxels equally. However, this produces biased estimates in the presence of CSF partial volume. This study introduces the tissue-weighted mean, which calculates the mean NODDI metric across the tissue within an ROI, utilising the tissue fraction estimate from NODDI to reduce estimation bias. We demonstrate the proposed mean in a study of white matter abnormalities in young onset Alzheimer's disease (YOAD). Results show the conventional mean induces significant bias that correlates with CSF partial volume, primarily affecting periventricular regions and more so in YOAD subjects than in healthy controls. Due to the differential extent of bias between healthy controls and YOAD subjects, the conventional mean under- or over-estimated the effect size for group differences in many ROIs. This demonstrates the importance of using the correct estimation procedure when inferring group differences in studies where the extent of CSF partial volume differs between groups. These findings are robust across different acquisition and processing conditions. Bias persists in ROIs at higher image resolution, as demonstrated using data obtained from the third phase of the Alzheimer's disease neuroimaging initiative (ADNI); and when performing ROI analysis in template space. This suggests that conventional ROI means of NODDI metrics are biased estimates under most contemporary experimental conditions, the correction of which requires the proposed tissue-weighted mean. The tissue-weighted mean produces accurate estimates of ROI means and group differences when ROIs contain voxels with CSF partial volume. In addition to NODDI, the technique can be applied to other multi-compartment models that account for CSF partial volume, such as the free water elimination method. We expect the technique to help generate new insights into normal and abnormal variation in tissue microstructure of regions typically confounded by CSF partial volume, such as those in individuals with larger ventricles due to atrophy associated with neurodegenerative disease.} +} \ No newline at end of file diff --git a/qsirecon/data/scalars/amico_noddi.yaml b/qsirecon/data/scalars/amico_noddi.yaml index 5697c96c..cca4f1bb 100644 --- a/qsirecon/data/scalars/amico_noddi.yaml +++ b/qsirecon/data/scalars/amico_noddi.yaml @@ -23,3 +23,35 @@ od_image: param: od metadata: Description: Orientation dispersion index from NODDI +modulated_icvf_image: + bids: + model: noddi + param: icvf + desc: modulated + metadata: + Description: Tissue fraction modulated intracellular volume fraction from NODDI +modulated_od_image: + bids: + model: noddi + param: od + desc: modulated + metadata: + Description: Tissue fraction modulated intracellular volume fraction from NODDI +rmse_image: + bids: + model: noddi + param: rmse + metadata: + Description: RMSE between predicted and measured signal from NODDI +nrmse_image: + bids: + model: noddi + param: nrmse + metadata: + Description: NRMSE between predicted and measured signal from NODDI +tf_image: + bids: + model: noddi + param: tf + metadata: + Description: Tissue fraction from NODDI \ No newline at end of file diff --git a/qsirecon/interfaces/amico.py b/qsirecon/interfaces/amico.py index e8607d22..cf18746f 100644 --- a/qsirecon/interfaces/amico.py +++ b/qsirecon/interfaces/amico.py @@ -12,6 +12,7 @@ import os.path as op import nibabel as nb +import nilearn.image as nim import numpy as np from dipy.core.gradients import gradient_table from dipy.core.sphere import HemiSphere @@ -137,6 +138,10 @@ class NODDIOutputSpec(AmicoOutputSpec): icvf_image = File() od_image = File() isovf_image = File() + modulated_icvf_image = File() + modulated_od_image = File() + rmse_image = File() + nrmse_image = File() config_file = File() @@ -177,12 +182,20 @@ def _run_interface(self, runtime): ) LOGGER.info("Fitting NODDI Model.") aeval.set_model("NODDI") + + # Set global configuration aeval.set_config("BLAS_nthreads", 1) aeval.set_config("nthreads", self.inputs.num_threads) - # set the parameters + aeval.set_config("doSaveModulatedMaps", True) + aeval.set_config("doComputeRMSE", True) + aeval.set_config("doComputeNRMSE", True) + + # Set model parameters aeval.model.dPar = self.inputs.dPar aeval.model.dIso = self.inputs.dIso aeval.model.isExvivo = self.inputs.isExvivo + + # Generate kernels and fit aeval.generate_kernels() aeval.load_kernels() aeval.fit() @@ -193,6 +206,34 @@ def _run_interface(self, runtime): self._results["icvf_image"] = shim_dir + "/AMICO/NODDI/fit_NDI.nii.gz" self._results["od_image"] = shim_dir + "/AMICO/NODDI/fit_ODI.nii.gz" self._results["isovf_image"] = shim_dir + "/AMICO/NODDI/fit_FWF.nii.gz" + self._results["modulated_od_image"] = shim_dir + "/AMICO/NODDI/fit_ODI_modulated.nii.gz" + self._results["modulated_icvf_image"] = shim_dir + "/AMICO/NODDI/fit_NDI_modulated.nii.gz" + self._results["rmse_image"] = shim_dir + "/AMICO/NODDI/fit_RMSE.nii.gz" + self._results["nrmse_image"] = shim_dir + "/AMICO/NODDI/fit_NRMSE.nii.gz" self._results["config_file"] = shim_dir + "/AMICO/NODDI/config.pickle" return runtime + + +class _NODDITissueFractionInputSpec(BaseInterfaceInputSpec): + isovf_image = File(exists=True, mandatory=True) + mask_image = File(exists=True, mandatory=True) + + +class _NODDITissueFractionOutputSpec(TraitedSpec): + tf_image = File() + + +class NODDITissueFraction(SimpleInterface): + input_spec = _NODDITissueFractionInputSpec + output_spec = _NODDITissueFractionOutputSpec + + def _run_interface(self, runtime): + isovf_image = self.inputs.isovf_image + mask_image = self.inputs.mask_image + + tf_image = nim.math_img("(1 - isovf) * mask", isovf=isovf_image, mask=mask_image) + out_file = fname_presuffix(isovf_image, suffix="_tf", newpath=runtime.cwd) + tf_image.to_filename(out_file) + self._results["tf_image"] = out_file + return runtime diff --git a/qsirecon/interfaces/converters.py b/qsirecon/interfaces/converters.py index 7777281c..9aef9c36 100644 --- a/qsirecon/interfaces/converters.py +++ b/qsirecon/interfaces/converters.py @@ -142,6 +142,8 @@ class NODDItoFIBGZInputSpec(BaseInterfaceInputSpec): icvf_file = File(exists=True) isovf_file = File(exists=True) od_file = File(exists=True) + modulated_icvf_file = File(exists=True) + modulated_od_file = File(exists=True) directions_file = File(exists=True) mask_file = File(exists=True) @@ -163,6 +165,8 @@ def _run_interface(self, runtime): directions_img=nb.load(self.inputs.directions_file), od_img=nb.load(self.inputs.od_file), icvf_img=nb.load(self.inputs.icvf_file), + modulated_od_img=nb.load(self.inputs.modulated_od_file), + modulated_icvf_img=nb.load(self.inputs.modulated_icvf_file), isovf_img=nb.load(self.inputs.isovf_file), odf_dirs=verts, odf_faces=faces, @@ -425,7 +429,16 @@ def amplitudes_to_fibgz( def amico_directions_to_fibgz( - directions_img, od_img, icvf_img, isovf_img, odf_dirs, odf_faces, output_file, mask_img + directions_img, + od_img, + icvf_img, + modulated_od_img, + modulated_icvf_img, + isovf_img, + odf_dirs, + odf_faces, + output_file, + mask_img, ): """Convert a NiftiImage of ODF amplitudes to a DSI Studio fib file. @@ -482,6 +495,8 @@ def amico_directions_to_fibgz( isovf_vec = isovf_img.get_fdata().flatten(order="F") icvf_vec = icvf_img.get_fdata().flatten(order="F") od_vec = od_img.get_fdata().flatten(order="F") + mod_icvf_vec = modulated_icvf_img.get_fdata().flatten(order="F") + mod_od_vec = modulated_od_img.get_fdata().flatten(order="F") # z0 = np.nanmax(isovf_vec) peak_indices = np.zeros(n_odfs) @@ -500,9 +515,11 @@ def amico_directions_to_fibgz( dir0[flat_mask] = peak_indices dsi_mat["index0"] = dir0.astype("int16") dsi_mat["fa0"] = icvf_vec - dsi_mat["ICVF0"] = icvf_vec - dsi_mat["ISOVF0"] = isovf_vec - dsi_mat["OD0"] = od_vec + dsi_mat["icvf0"] = icvf_vec + dsi_mat["isovf0"] = isovf_vec + dsi_mat["od0"] = od_vec + dsi_mat["mod_icvf0"] = mod_icvf_vec + dsi_mat["mod_od0"] = mod_od_vec dsi_mat["odf_vertices"] = odf_dirs.T dsi_mat["odf_faces"] = odf_faces.T savemat(output_file, dsi_mat, format="4", appendmat=False) diff --git a/qsirecon/tests/data/amico_noddi_outputs.txt b/qsirecon/tests/data/amico_noddi_outputs.txt index 69679f28..2fc6e48e 100644 --- a/qsirecon/tests/data/amico_noddi_outputs.txt +++ b/qsirecon/tests/data/amico_noddi_outputs.txt @@ -15,10 +15,20 @@ derivatives/qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-nod derivatives/qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-noddi_param-direction_dwimap.nii.gz derivatives/qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-noddi_param-icvf_dwimap.json derivatives/qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-noddi_param-icvf_dwimap.nii.gz +derivatives/qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-noddi_param-icvf_desc-modulated_dwimap.json +derivatives/qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-noddi_param-icvf_desc-modulated_dwimap.nii.gz derivatives/qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-noddi_param-isovf_dwimap.json derivatives/qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-noddi_param-isovf_dwimap.nii.gz derivatives/qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-noddi_param-od_dwimap.json derivatives/qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-noddi_param-od_dwimap.nii.gz +derivatives/qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-noddi_param-od_desc-modulated_dwimap.json +derivatives/qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-noddi_param-od_desc-modulated_dwimap.nii.gz +derivatives/qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-noddi_param-rmse_dwimap.json +derivatives/qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-noddi_param-rmse_dwimap.nii.gz +derivatives/qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-noddi_param-nrmse_dwimap.json +derivatives/qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-noddi_param-nrmse_dwimap.nii.gz +derivatives/qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-noddi_param-tf_dwimap.nii.gz +derivatives/qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-noddi_param-tf_dwimap.json derivatives/qsirecon-NODDI/sub-PNC/sub-PNC.html logs logs/CITATION.bib diff --git a/qsirecon/workflows/base.py b/qsirecon/workflows/base.py index 6e321f12..acdbab4e 100644 --- a/qsirecon/workflows/base.py +++ b/qsirecon/workflows/base.py @@ -107,7 +107,7 @@ def init_single_subject_recon_wf(subject_id): Many internal operations of *QSIRecon* use *Nilearn* {nilearn_ver} [@nilearn, RRID:SCR_001362] and -*Dipy* {dipy_ver}[@dipy]. +*Dipy* {dipy_ver} [@dipy]. For more details of the pipeline, see [the section corresponding to workflows in *QSIRecon*'s documentation]\ (https://qsirecon.readthedocs.io/en/latest/workflows.html). diff --git a/qsirecon/workflows/recon/amico.py b/qsirecon/workflows/recon/amico.py index cf1cf09b..8d20ee26 100644 --- a/qsirecon/workflows/recon/amico.py +++ b/qsirecon/workflows/recon/amico.py @@ -11,7 +11,7 @@ from niworkflows.engine.workflows import LiterateWorkflow as Workflow from ... import config -from ...interfaces.amico import NODDI +from ...interfaces.amico import NODDI, NODDITissueFraction from ...interfaces.bids import DerivativesDataSink from ...interfaces.converters import NODDItoFIBGZ from ...interfaces.interchange import recon_workflow_input_fields @@ -45,6 +45,14 @@ def init_amico_noddi_fit_wf( Voxelwise Orientation Dispersion isovf_image Voxelwise ISOVF + modulated_icvf_image + Voxelwise modulated ICVF (ICVF * (1 - ISOVF)) + modulated_od_image + Voxelwise modulated Orientation Dispersion (OD * (1 - ISOVF)) + rmse_image + Voxelwise root mean square error between predicted and measured signal + nrmse_image + Voxelwise normalized root mean square error between predicted and measured signal config_file Pickle file with model configurations in it fibgz @@ -61,9 +69,14 @@ def init_amico_noddi_fit_wf( "icvf_image", "od_image", "isovf_image", + "modulated_icvf_image", + "modulated_od_image", + "rmse_image", + "nrmse_image", "config_file", "fibgz", "recon_scalars", + "tf_image", ], ), name="outputnode", @@ -72,9 +85,10 @@ def init_amico_noddi_fit_wf( workflow = Workflow(name=name) plot_reports = params.pop("plot_reports", True) - desc = """NODDI Reconstruction + desc = """ +### NODDI Reconstruction -: """ +""" desc += """\ The NODDI model (@noddi) was fit using the AMICO implementation (@amico). A value of %.1E was used for parallel diffusivity and %.1E for isotropic @@ -83,14 +97,19 @@ def init_amico_noddi_fit_wf( params["dIso"], ) if params.get("is_exvivo"): - desc += " An additional component was added to the model foe ex-vivo data." + desc += " An additional component was added to the model for ex-vivo data." + + desc += """\ + Tissue fraction (1 - ISOVF) modulated ICVF and Orientation Dispersion maps +were also computed (@parker2021not).""" recon_scalars = pe.Node( - AMICOReconScalars(qsirecon_suffix=qsirecon_suffix), + AMICOReconScalars(dismiss_entities=["desc"], qsirecon_suffix=qsirecon_suffix), name="recon_scalars", run_without_submitting=True, ) noddi_fit = pe.Node(NODDI(**params), name="recon_noddi", n_procs=omp_nthreads) + noddi_tissue_fraction = pe.Node(NODDITissueFraction(), name="noddi_tissue_fraction") convert_to_fibgz = pe.Node(NODDItoFIBGZ(), name="convert_to_fibgz") workflow.connect([ @@ -100,11 +119,21 @@ def init_amico_noddi_fit_wf( ('bvec_file', 'bvec_file'), ('dwi_mask', 'mask_file'), ]), + (inputnode, noddi_tissue_fraction, [('dwi_mask', 'mask_image')]), + (noddi_fit, noddi_tissue_fraction, [ + ('isovf_image', 'isovf_image'), + ]), + (noddi_tissue_fraction, outputnode, [('tf_image', 'tf_image')]), + (noddi_tissue_fraction, recon_scalars, [('tf_image', 'tf_image')]), (noddi_fit, outputnode, [ ('directions_image', 'directions_image'), ('icvf_image', 'icvf_image'), ('od_image', 'od_image'), ('isovf_image', 'isovf_image'), + ('modulated_icvf_image', 'modulated_icvf_image'), + ('modulated_od_image', 'modulated_od_image'), + ('rmse_image', 'rmse_image'), + ('nrmse_image', 'nrmse_image'), ('config_file', 'config_file'), ]), (noddi_fit, recon_scalars, [ @@ -112,6 +141,10 @@ def init_amico_noddi_fit_wf( ('od_image', 'od_image'), ('isovf_image', 'isovf_image'), ('directions_image', 'directions_image'), + ('modulated_icvf_image', 'modulated_icvf_image'), + ('modulated_od_image', 'modulated_od_image'), + ('rmse_image', 'rmse_image'), + ('nrmse_image', 'nrmse_image'), ]), (recon_scalars, outputnode, [("scalar_info", "recon_scalars")]), (noddi_fit, convert_to_fibgz, [ @@ -119,6 +152,8 @@ def init_amico_noddi_fit_wf( ('icvf_image', 'icvf_file'), ('od_image', 'od_file'), ('isovf_image', 'isovf_file'), + ('modulated_icvf_image', 'modulated_icvf_file'), + ('modulated_od_image', 'modulated_od_file'), ]), (inputnode, convert_to_fibgz, [('dwi_mask', 'mask_file')]), (convert_to_fibgz, outputnode, [('fibgz_file', 'fibgz')]) @@ -150,7 +185,9 @@ def init_amico_noddi_fit_wf( derivatives_config = load_yaml(load_data("nonscalars/amico_noddi.yaml")) ds_fibgz = pe.Node( DerivativesDataSink( - dismiss_entities=("desc",), compress=True, **derivatives_config["fibgz"]["bids"] + dismiss_entities=["desc"], + compress=True, + **derivatives_config["fibgz"]["bids"], ), name=f"ds_{qsirecon_suffix}_fibgz", run_without_submitting=True, @@ -165,7 +202,7 @@ def init_amico_noddi_fit_wf( ds_config = pe.Node( DerivativesDataSink( - dismiss_entities=("desc",), + dismiss_entities=["desc"], compress=True, **derivatives_config["config_file"]["bids"], ), diff --git a/qsirecon/workflows/recon/anatomical.py b/qsirecon/workflows/recon/anatomical.py index af651903..a7eef6b2 100644 --- a/qsirecon/workflows/recon/anatomical.py +++ b/qsirecon/workflows/recon/anatomical.py @@ -570,7 +570,7 @@ def _get_status(): # up to version 0.14.3 if has_qsiprep_t1w and not prefer_dwi_mask: desc += ( - f"Brainmasks from {skull_strip_method} were used in all subsequent reconstruction " + f"Brain masks from {skull_strip_method} were used in all subsequent reconstruction " "steps. " ) # Resample anat mask From d6192798b81bc903a5d8e68ec1a0cd56a5e8a9e2 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Thu, 10 Apr 2025 16:09:17 -0400 Subject: [PATCH 4/5] Calculate kurtosis microstructure scalars with new `DKI_reconstruction` parameter (#223) * Add new DIPYDKI metrics. * Update expected outputs. * Try fixing. * Fix! * Update dipy.py * Fix. * Only get microstructure metrics if not sloppy. * Drop new expected outputs. * Change model of microstructural metrics. * Update. * Split DKI nodes. * Add new scalars. * Drop ga. * Update dipy.py * Enable wmti just to test. * Revert "Enable wmti just to test." This reverts commit 3b70b5dc80449239f2b180da43d60b5ea762c9e2. * Add dkimicro kfa scalar. --- docs/builtin_workflows.rst | 11 +- docs/outputs.rst | 5 + docs/recon_scalars/dipy_dki.csv | 32 +++-- qsirecon/data/pipelines/dipy_dki.yaml | 2 + qsirecon/data/pipelines/hbcd_scalar_maps.yaml | 2 + .../data/pipelines/multishell_scalarfest.yaml | 2 + qsirecon/data/pipelines/test_scalar_maps.yaml | 2 + qsirecon/data/scalars/dipy_dki.yaml | 102 ++++++++++++++-- qsirecon/interfaces/dipy.py | 101 ++++++++++++++-- qsirecon/tests/data/dipy_dki_outputs.txt | 6 + qsirecon/tests/data/scalar_mapper_outputs.txt | 12 ++ qsirecon/utils/sloppy_recon.py | 1 + qsirecon/workflows/recon/dipy.py | 109 +++++++++++++++--- 13 files changed, 338 insertions(+), 49 deletions(-) diff --git a/docs/builtin_workflows.rst b/docs/builtin_workflows.rst index b0d0a7e1..90fb4a80 100644 --- a/docs/builtin_workflows.rst +++ b/docs/builtin_workflows.rst @@ -185,9 +185,9 @@ PyAFQ Outputs =============== This workflow estimates the NODDI :footcite:p:`noddi` model using the implementation from -AMICO :footcite:p:`amico` and tissue fraction modulation described in :footcite:p:`parker2021not`. -Images with (modulated) intra-cellular volume fraction (ICVF), isotropic volume fraction (ISOVF), -(modulated) orientation dispersion (OD), root mean square error (RMSE) and normalized RMSE are written to outputs. +AMICO :footcite:p:`amico` and tissue fraction modulation described in :footcite:p:`parker2021not`. +Images with (modulated) intra-cellular volume fraction (ICVF), isotropic volume fraction (ISOVF), +(modulated) orientation dispersion (OD), root mean square error (RMSE) and normalized RMSE are written to outputs. Additionally, a DSI Studio fib file is created using the peak directions and ICVF as a stand-in for QA to be used for tractography. @@ -363,6 +363,11 @@ Other Outputs A DKI model is fit to the dMRI signal and multiple scalar maps are produced. +.. important:: + + In order to calculate microstructural metrics with the ``dkimicro`` model, + you must set the ``wmti`` flag to ``True``. + Scalar Maps ----------- .. csv-table:: diff --git a/docs/outputs.rst b/docs/outputs.rst index b8b8539c..43d005af 100644 --- a/docs/outputs.rst +++ b/docs/outputs.rst @@ -82,6 +82,11 @@ DKI and TORTOISE:: _model-dki_param-rk_dwimap.nii.gz _model-tensor_param-fa_dwimap.json _model-tensor_param-fa_dwimap.nii.gz + # Microstructural metrics calculated if wmti is True + _model-dkimicro_param-awf_dwimap.json + _model-dkimicro_param-awf_dwimap.nii.gz + _model-dkimicro_param-rde_dwimap.json + _model-dkimicro_param-rde_dwimap.nii.gz qsirecon-TORTOISE/ dataset_description.json diff --git a/docs/recon_scalars/dipy_dki.csv b/docs/recon_scalars/dipy_dki.csv index 46fbe926..2ff9dac3 100644 --- a/docs/recon_scalars/dipy_dki.csv +++ b/docs/recon_scalars/dipy_dki.csv @@ -1,9 +1,23 @@ -dki,ad,DKI AD -dki,ak,DKI AK -dki,kfa,DKI KFA -dki,md,DKI MD -dki,mk,DKI MK -dki,mkt,DKI MKT -dki,rd,DKI RD -dki,rk,DKI RK -tensor,fa,DKI FA +dki,ad,DKI Axial Diffusivity +dki,ak,DKI Axial Kurtosis +dki,kfa,DKI Kurtosis Fractional Anisotropy +dki,linearity,DKI Linearity +dki,md,DKI Mean Diffusivity +dki,mk,DKI Mean Kurtosis +dki,mkt,DKI Mean Kurtosis Tensor +dki,planarity,DKI Planarity +dki,rd,DKI Radial Diffusivity +dki,rk,DKI Radial Kurtosis +dki,sphericity,DKI Sphericity +tensor,fa,DKI Fractional Anisotropy +dkimicro,ad,DKI Microstructural Axial Diffusivity +dkimicro,ade,DKI Microstructural Axial Diffusivity of the Extra-Cellular Compartment +dkimicro,ak,DKI Microstructural Axial Kurtosis +dkimicro,awf,DKI Microstructural Axonal Water Fraction +dkimicro,axonald,DKI Microstructural Axonal Diffusivity +dkimicro,kfa,DKI Microstructural Kurtosis Fractional Anisotropy +dkimicro,md,DKI Microstructural Mean Diffusivity +dkimicro,rd,DKI Microstructural Radial Diffusivity +dkimicro,rde,DKI Microstructural Radial Diffusivity of the Extra-Cellular Compartment +dkimicro,tortuosity,DKI Microstructural Tortuosity +dkimicro,trace,DKI Microstructural Trace diff --git a/qsirecon/data/pipelines/dipy_dki.yaml b/qsirecon/data/pipelines/dipy_dki.yaml index 96d887aa..0a943eed 100644 --- a/qsirecon/data/pipelines/dipy_dki.yaml +++ b/qsirecon/data/pipelines/dipy_dki.yaml @@ -5,6 +5,8 @@ nodes: input: qsirecon name: dki_recon parameters: + # Calculate microstructural metrics + wmti: true write_fibgz: false write_mif: false qsirecon_suffix: DKI diff --git a/qsirecon/data/pipelines/hbcd_scalar_maps.yaml b/qsirecon/data/pipelines/hbcd_scalar_maps.yaml index 1bef2477..4ef3c029 100644 --- a/qsirecon/data/pipelines/hbcd_scalar_maps.yaml +++ b/qsirecon/data/pipelines/hbcd_scalar_maps.yaml @@ -7,6 +7,8 @@ nodes: input: qsirecon name: dipy_dki parameters: + # Calculate microstructural metrics + wmti: true write_fibgz: false write_mif: false qsirecon_suffix: DIPYDKI diff --git a/qsirecon/data/pipelines/multishell_scalarfest.yaml b/qsirecon/data/pipelines/multishell_scalarfest.yaml index 27cfe9ff..e992235f 100644 --- a/qsirecon/data/pipelines/multishell_scalarfest.yaml +++ b/qsirecon/data/pipelines/multishell_scalarfest.yaml @@ -58,6 +58,8 @@ nodes: input: qsirecon name: dki_recon parameters: + # Calculate microstructural metrics + wmti: true write_fibgz: false write_mif: false qsirecon_suffix: DKI diff --git a/qsirecon/data/pipelines/test_scalar_maps.yaml b/qsirecon/data/pipelines/test_scalar_maps.yaml index f1943e31..c205d0ab 100644 --- a/qsirecon/data/pipelines/test_scalar_maps.yaml +++ b/qsirecon/data/pipelines/test_scalar_maps.yaml @@ -5,6 +5,8 @@ nodes: input: qsirecon name: dipy_dki parameters: + # Calculate microstructural metrics + wmti: true write_fibgz: false write_mif: false qsirecon_suffix: DIPYDKI diff --git a/qsirecon/data/scalars/dipy_dki.yaml b/qsirecon/data/scalars/dipy_dki.yaml index 94f201ce..9cb5a644 100644 --- a/qsirecon/data/scalars/dipy_dki.yaml +++ b/qsirecon/data/scalars/dipy_dki.yaml @@ -3,52 +3,136 @@ dki_ad: model: dki param: ad metadata: - Description: DKI AD + Description: DKI axial diffusivity dki_ak: bids: model: dki param: ak metadata: - Description: DKI AK + Description: DKI axial kurtosis dki_fa: bids: model: tensor param: fa metadata: - Description: DKI FA + Description: DKI fractional anisotropy dki_kfa: bids: model: dki param: kfa metadata: - Description: DKI KFA + Description: DKI kurtosis fractional anisotropy +dki_linearity: + bids: + model: dki + param: linearity + metadata: + Description: DKI linearity dki_md: bids: model: dki param: md metadata: - Description: DKI MD + Description: DKI mean diffusivity dki_mk: bids: model: dki param: mk metadata: - Description: DKI MK + Description: DKI mean kurtosis dki_mkt: bids: model: dki param: mkt metadata: - Description: DKI MKT + Description: DKI mean of the kurtosis tensor +dki_planarity: + bids: + model: dki + param: planarity + metadata: + Description: DKI planarity dki_rd: bids: model: dki param: rd metadata: - Description: DKI RD + Description: DKI radial diffusivity dki_rk: bids: model: dki param: rk metadata: - Description: DKI RK + Description: DKI radial kurtosis +dki_sphericity: + bids: + model: dki + param: sphericity + metadata: + Description: DKI sphericity +dkimicro_ad: + bids: + model: dkimicro + param: ad + metadata: + Description: DKI Microstructural Axial Diffusivity +dkimicro_ade: + bids: + model: dkimicro + param: ade + metadata: + Description: DKI Microstructural Axial Diffusivity of the Extra-Cellular Compartment +dkimicro_ak: + bids: + model: dkimicro + param: ak + metadata: + Description: DKI Microstructural Axial Kurtosis +dkimicro_awf: + bids: + model: dkimicro + param: awf + metadata: + Description: DKI axonal water fraction +dkimicro_axonald: + bids: + model: dkimicro + param: axonald + metadata: + Description: DKI Microstructural Axonal Diffusivity +dkimicro_kfa: + bids: + model: dkimicro + param: kfa + metadata: + Description: DKI Microstructural Kurtosis Fractional Anisotropy +dkimicro_md: + bids: + model: dkimicro + param: md + metadata: + Description: DKI Microstructural Mean Diffusivity +dkimicro_rd: + bids: + model: dkimicro + param: rd + metadata: + Description: DKI Microstructural Radial Diffusivity +dkimicro_rde: + bids: + model: dkimicro + param: rde + metadata: + Description: DKI radial diffusivity of the extra-cellular compartment +dkimicro_tortuosity: + bids: + model: dkimicro + param: tortuosity + metadata: + Description: DKI Microstructural Tortuosity +dkimicro_trace: + bids: + model: dkimicro + param: trace + metadata: + Description: DKI Microstructural Trace diff --git a/qsirecon/interfaces/dipy.py b/qsirecon/interfaces/dipy.py index 3bef3dc9..66e4a2dc 100644 --- a/qsirecon/interfaces/dipy.py +++ b/qsirecon/interfaces/dipy.py @@ -15,7 +15,7 @@ from dipy.core.gradients import gradient_table from dipy.core.sphere import HemiSphere from dipy.io.utils import nifti1_symmat -from dipy.reconst import dki, dti, mapmri +from dipy.reconst import dki, dki_micro, dti, mapmri from dipy.segment.mask import median_otsu from nipype import logging from nipype.interfaces.base import ( @@ -531,16 +531,19 @@ class _KurtosisReconstructionInputSpec(DipyReconInputSpec): class _KurtosisReconstructionOutputSpec(DipyReconOutputSpec): tensor = File() - fa = File() - md = File() - rd = File() ad = File() + ak = File() colorFA = File() + fa = File() kfa = File() + linearity = File() + md = File() mk = File() - ak = File() - rk = File() mkt = File() + planarity = File() + rd = File() + rk = File() + sphericity = File() class KurtosisReconstruction(DipyReconInterface): @@ -565,8 +568,22 @@ def _run_interface(self, runtime): self._results["tensor"] = output_tensor_file # FA MD RD and AD - for metric in ["fa", "md", "rd", "ad", "colorFA", "kfa"]: - metric_attr = metric if metric != "colorFA" else "color_fa" + metric_attrs = { + "colorFA": "color_fa", + } + base_metrics = [ + "ad", + "colorFA", + "fa", + "kfa", + "linearity", + "md", + "planarity", + "rd", + "sphericity", + ] + for metric in base_metrics: + metric_attr = metric_attrs.get(metric, metric) data = np.nan_to_num(getattr(dkifit, metric_attr).astype("float32"), 0) out_name = fname_presuffix( self.inputs.dwi_file, suffix="DKI" + metric, newpath=runtime.cwd, use_ext=True @@ -575,7 +592,8 @@ def _run_interface(self, runtime): self._results[metric] = out_name # Get the kurtosis metrics - for metric in ["mk", "ak", "rk", "mkt"]: + kurtosis_metrics = ["ak", "mk", "mkt", "rk"] + for metric in kurtosis_metrics: data = np.nan_to_num( getattr(dkifit, metric)( float(self.inputs.kurtosis_clip_min), float(self.inputs.kurtosis_clip_max) @@ -589,3 +607,68 @@ def _run_interface(self, runtime): self._results[metric] = out_name return runtime + + +class _KurtosisReconstructionMicrostructureInputSpec(DipyReconInputSpec): + kurtosis_clip_min = traits.Float(-0.42857142857142855, usedefault=True) + kurtosis_clip_max = traits.Float(10.0, usedefault=True) + + +class _KurtosisReconstructionMicrostructureOutputSpec(DipyReconOutputSpec): + ad = File() + ade = File() + awf = File() + axonald = File() + kfa = File() + md = File() + rd = File() + rde = File() + tortuosity = File() + trace = File() + + +class KurtosisReconstructionMicrostructure(DipyReconInterface): + input_spec = _KurtosisReconstructionMicrostructureInputSpec + output_spec = _KurtosisReconstructionMicrostructureOutputSpec + + def _run_interface(self, runtime): + gtab = self._get_gtab() + dwi_img = nb.load(self.inputs.dwi_file) + dwi_data = dwi_img.get_fdata(dtype="float32") + mask_img, mask_array = self._get_mask(dwi_img, gtab) + + # Fit it + dkimodel = dki_micro.KurtosisMicrostructureModel(gtab) + dkifit = dkimodel.fit(dwi_data, mask_array) + + # FA MD RD and AD + metric_attrs = { + "ade": "hindered_ad", + "rde": "hindered_rd", + "axonald": "axonal_diffusivity", + } + base_metrics = [ + "ad", + "ade", + "awf", + "axonald", + "kfa", + "md", + "rd", + "rde", + "tortuosity", + "trace", + ] + for metric in base_metrics: + metric_attr = metric_attrs.get(metric, metric) + data = np.nan_to_num(getattr(dkifit, metric_attr).astype("float32"), 0) + out_name = fname_presuffix( + self.inputs.dwi_file, + suffix="DKIMicro" + metric, + newpath=runtime.cwd, + use_ext=True, + ) + nb.Nifti1Image(data, dwi_img.affine).to_filename(out_name) + self._results[metric] = out_name + + return runtime diff --git a/qsirecon/tests/data/dipy_dki_outputs.txt b/qsirecon/tests/data/dipy_dki_outputs.txt index 085e8d54..b9dbf551 100644 --- a/qsirecon/tests/data/dipy_dki_outputs.txt +++ b/qsirecon/tests/data/dipy_dki_outputs.txt @@ -16,16 +16,22 @@ derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_ derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-ak_dwimap.nii.gz derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-kfa_dwimap.json derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-kfa_dwimap.nii.gz +derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-linearity_dwimap.json +derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-linearity_dwimap.nii.gz derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-md_dwimap.json derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-md_dwimap.nii.gz derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-mk_dwimap.json derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-mk_dwimap.nii.gz derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-mkt_dwimap.json derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-mkt_dwimap.nii.gz +derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-planarity_dwimap.json +derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-planarity_dwimap.nii.gz derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-rd_dwimap.json derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-rd_dwimap.nii.gz derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-rk_dwimap.json derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-rk_dwimap.nii.gz +derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-sphericity_dwimap.json +derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-sphericity_dwimap.nii.gz derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-tensor_param-fa_dwimap.json derivatives/qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-tensor_param-fa_dwimap.nii.gz logs diff --git a/qsirecon/tests/data/scalar_mapper_outputs.txt b/qsirecon/tests/data/scalar_mapper_outputs.txt index 5fe01219..674fbcfd 100644 --- a/qsirecon/tests/data/scalar_mapper_outputs.txt +++ b/qsirecon/tests/data/scalar_mapper_outputs.txt @@ -16,16 +16,22 @@ derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-dki_param-ak_dwimap.nii.gz derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-dki_param-kfa_dwimap.json derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-dki_param-kfa_dwimap.nii.gz +derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-dki_param-linearity_dwimap.json +derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-dki_param-linearity_dwimap.nii.gz derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-dki_param-md_dwimap.json derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-dki_param-md_dwimap.nii.gz derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-dki_param-mk_dwimap.json derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-dki_param-mk_dwimap.nii.gz derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-dki_param-mkt_dwimap.json derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-dki_param-mkt_dwimap.nii.gz +derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-dki_param-planarity_dwimap.json +derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-dki_param-planarity_dwimap.nii.gz derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-dki_param-rd_dwimap.json derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-dki_param-rd_dwimap.nii.gz derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-dki_param-rk_dwimap.json derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-dki_param-rk_dwimap.nii.gz +derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-dki_param-sphericity_dwimap.json +derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-dki_param-sphericity_dwimap.nii.gz derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-tensor_param-fa_dwimap.json derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-tensor_param-fa_dwimap.nii.gz derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_bundles-DSIStudio_scalarstats.tsv @@ -35,16 +41,22 @@ derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model- derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-ak_dwimap.nii.gz derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-kfa_dwimap.json derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-kfa_dwimap.nii.gz +derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-linearity_dwimap.json +derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-linearity_dwimap.nii.gz derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-md_dwimap.json derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-md_dwimap.nii.gz derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-mk_dwimap.json derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-mk_dwimap.nii.gz derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-mkt_dwimap.json derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-mkt_dwimap.nii.gz +derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-planarity_dwimap.json +derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-planarity_dwimap.nii.gz derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-rd_dwimap.json derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-rd_dwimap.nii.gz derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-rk_dwimap.json derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-rk_dwimap.nii.gz +derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-sphericity_dwimap.json +derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-dki_param-sphericity_dwimap.nii.gz derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-tensor_param-fa_dwimap.json derivatives/qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-tensor_param-fa_dwimap.nii.gz derivatives/qsirecon-DSIStudio diff --git a/qsirecon/utils/sloppy_recon.py b/qsirecon/utils/sloppy_recon.py index 68023491..7b74f02a 100644 --- a/qsirecon/utils/sloppy_recon.py +++ b/qsirecon/utils/sloppy_recon.py @@ -4,6 +4,7 @@ def make_sloppy(spec): fast_options = { ("Dipy", "3dSHORE_reconstruction"): {"extrapolate_scheme": "ABCD"}, + ("Dipy", "DKI_reconstruction"): {"wmti": False}, ("Dipy", "MAPMRI_reconstruction"): { "extrapolate_scheme": "ABCD", "anisotropic_scaling": False, diff --git a/qsirecon/workflows/recon/dipy.py b/qsirecon/workflows/recon/dipy.py index 419673c7..bd6bdce4 100644 --- a/qsirecon/workflows/recon/dipy.py +++ b/qsirecon/workflows/recon/dipy.py @@ -19,6 +19,7 @@ from ...interfaces.dipy import ( BrainSuiteShoreReconstruction, KurtosisReconstruction, + KurtosisReconstructionMicrostructure, MAPMRIReconstruction, ) from ...interfaces.interchange import recon_workflow_input_fields @@ -571,6 +572,10 @@ def init_dipy_dki_recon_wf(inputs_dict, name="dipy_dki_recon", qsirecon_suffix=" ak rk mkt + awf + Only if wmti is True + rde + Only if wmti is True Params @@ -590,16 +595,31 @@ def init_dipy_dki_recon_wf(inputs_dict, name="dipy_dki_recon", qsirecon_suffix=" niu.IdentityInterface( fields=[ "tensor", - "fa", - "md", - "rd", - "ad", "colorFA", + "ad", + "ak", + "fa", "kfa", + "linearity", + "md", "mk", - "ak", - "rk", "mkt", + "planarity", + "rd", + "rk", + "sphericity", + # Only if wmti is True + "dkimicro_ad", + "dkimicro_ade", + "dkimicro_awf", + "dkimicro_axonald", + "dkimicro_kfa", + "dkimicro_md", + "dkimicro_rd", + "dkimicro_rde", + "dkimicro_tortuosity", + "dkimicro_trace", + # Aggregated scalars "recon_scalars", ] ), @@ -612,7 +632,10 @@ def init_dipy_dki_recon_wf(inputs_dict, name="dipy_dki_recon", qsirecon_suffix=" ) workflow = Workflow(name=name) desc = "#### Dipy Reconstruction\n\n" + plot_reports = not config.execution.skip_odf_reports + micro_metrics = params.pop("wmti", False) + recon_dki = pe.Node(KurtosisReconstruction(**params), name="recon_dki") workflow.connect([ @@ -620,33 +643,81 @@ def init_dipy_dki_recon_wf(inputs_dict, name="dipy_dki_recon", qsirecon_suffix=" ('dwi_file', 'dwi_file'), ('bval_file', 'bval_file'), ('bvec_file', 'bvec_file'), - ('dwi_mask', 'mask_file')]), + ('dwi_mask', 'mask_file'), + ]), (recon_dki, outputnode, [ ('tensor', 'tensor'), - ('fa', 'fa'), - ('md', 'md'), - ('rd', 'rd'), + ('fibgz', 'fibgz'), ('ad', 'ad'), + ('ak', 'ak'), ('colorFA', 'colorFA'), + ('fa', 'fa'), ('kfa', 'kfa'), + ('linearity', 'linearity'), + ('md', 'md'), ('mk', 'mk'), - ('ak', 'ak'), - ('rk', 'rk'), ('mkt', 'mkt'), - ('fibgz', 'fibgz')]), + ('planarity', 'planarity'), + ('rd', 'rd'), + ('rk', 'rk'), + ('sphericity', 'sphericity'), + ]), (recon_dki, recon_scalars, [ - ('fa', 'dki_fa'), - ('md', 'dki_md'), - ('rd', 'dki_rd'), ('ad', 'dki_ad'), + ('ak', 'dki_ak'), + ('fa', 'dki_fa'), ('kfa', 'dki_kfa'), + ('linearity', 'dki_linearity'), + ('md', 'dki_md'), ('mk', 'dki_mk'), - ('ak', 'dki_ak'), + ('mkt', 'dki_mkt'), + ('planarity', 'dki_planarity'), + ('rd', 'dki_rd'), ('rk', 'dki_rk'), - ('mkt', 'dki_mkt')]), - (recon_scalars, outputnode, [("scalar_info", "recon_scalars")]) + ('sphericity', 'dki_sphericity'), + ]), + (recon_scalars, outputnode, [("scalar_info", "recon_scalars")]), ]) # fmt:skip + if micro_metrics: + recon_dkimicro = pe.Node( + KurtosisReconstructionMicrostructure(**params), + name="recon_dkimicro", + ) + # Only produce microstructural metrics if wmti is True + workflow.connect([ + (inputnode, recon_dkimicro, [ + ('dwi_file', 'dwi_file'), + ('bval_file', 'bval_file'), + ('bvec_file', 'bvec_file'), + ('dwi_mask', 'mask_file'), + ]), + (recon_dkimicro, outputnode, [ + ('ad', 'dkimicro_ad'), + ('ade', 'dkimicro_ade'), + ('awf', 'dkimicro_awf'), + ('axonald', 'dkimicro_axonald'), + ('kfa', 'dkimicro_kfa'), + ('md', 'dkimicro_md'), + ('rd', 'dkimicro_rd'), + ('rde', 'dkimicro_rde'), + ('tortuosity', 'dkimicro_tortuosity'), + ('trace', 'dkimicro_trace'), + ]), + (recon_dkimicro, recon_scalars, [ + ('ad', 'dkimicro_ad'), + ('ade', 'dkimicro_ade'), + ('awf', 'dkimicro_awf'), + ('axonald', 'dkimicro_axonald'), + ('kfa', 'dkimicro_kfa'), + ('md', 'dkimicro_md'), + ('rd', 'dkimicro_rd'), + ('rde', 'dkimicro_rde'), + ('tortuosity', 'dkimicro_tortuosity'), + ('trace', 'dkimicro_trace'), + ]), + ]) # fmt:skip + if plot_reports and False: plot_peaks = pe.Node( CLIReconPeaksReport(peaks_only=True), From af43da92df114bdaa8dd9fffa02631f7042aa791 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 14 Apr 2025 14:53:35 -0400 Subject: [PATCH 5/5] Prepare for 1.1.0 release (#228) Prepare for 1.1.0 release. --- CITATION.cff | 8 ++++++-- docs/changes.md | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 98503812..5633886d 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -43,6 +43,10 @@ authors: given-names: Michael affiliation: Max Planck Institute for Human Development, Berlin, Germany orcid: https://orcid.org/0000-0002-3878-6542 + - family-names: Raikes + given-names: Adam + affiliation: Center for Innovation in Brain Science, University of Arizona + orcid: https://orcid.org/0000-0002-1609-6727 - family-names: Sadil given-names: Patrick affiliation: Johns Hopkins Bloomberg School of Public Health @@ -167,5 +171,5 @@ keywords: - BIDS - BIDS-App license: BSD-3-Clause -version: 1.0.1 -date-released: '2025-03-11' +version: 1.1.0 +date-released: '2025-04-14' diff --git a/docs/changes.md b/docs/changes.md index 884182ef..306c3436 100644 --- a/docs/changes.md +++ b/docs/changes.md @@ -1,5 +1,23 @@ # What's New +## 1.1.0 + +### 🎉 Exciting New Features + +* Tissue fraction modulated ICVF and OD maps by @araikes in https://github.com/PennLINC/qsirecon/pull/218 +* Calculate kurtosis microstructure scalars with new `DKI_reconstruction` parameter by @tsalo in https://github.com/PennLINC/qsirecon/pull/223 + +### Other Changes + +* Add `smoothing` and `otsu_threshold` autotrack arguments by @smeisler in https://github.com/PennLINC/qsirecon/pull/219 +* Allow desc entity in recon scalar derivatives by @tsalo in https://github.com/PennLINC/qsirecon/pull/220 + +## New Contributors + +* @araikes made their first contribution in https://github.com/PennLINC/qsirecon/pull/218 + +**Full Changelog**: https://github.com/PennLINC/qsirecon/compare/1.0.1...1.1.0 + ## 1.0.1 ### 🎉 Exciting New Features