diff --git a/CHANGES.rst b/CHANGES.rst index 456b472f..fbac1fb6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,49 @@ +0.18.0 (June 9, 2023) +===================== + +No technical changes to the pipeline here, but citations and methods boilerplate have been updated to +reflect the changes in 0.18.0alpha0. + + + +0.18.0alpha0 (May 26, 2023) +=========================== + +First release moving towards 1.0! Please open bug reports if anything suspicious comes up. This release +changes the anatomical workflow significantly, synthstrip and synthseg are used. The recon workflow +"dsi_studio_autotrack" has also been added. + +## What's Changed +* Bump sentry-sdk from 0.13.1 to 1.14.0 by @dependabot in https://github.com/PennLINC/qsiprep/pull/539 +* [ENH] Update FreeSurfer to 7.3.1, dmri-amico to 1.5.4 by @mattcieslak in https://github.com/PennLINC/qsiprep/pull/537 +* WIP: ENH: Make pyAFQ tests faster, add export all by @36000 in https://github.com/PennLINC/qsiprep/pull/534 +* [ENH] move biascorrect so it runs on resampled data by default by @mattcieslak in https://github.com/PennLINC/qsiprep/pull/527 +* [Fix] Fix threading on DRBUDDI interface by @mattcieslak in https://github.com/PennLINC/qsiprep/pull/540 +* [ENH] add CNR to the imageqc.csv by @mattcieslak in https://github.com/PennLINC/qsiprep/pull/541 +* [FIX] pin pandas version to < 2.0.0 by @mattcieslak in https://github.com/PennLINC/qsiprep/pull/543 +* ENH: Replace avscale with non-fsl tools by @jbh1091 in https://github.com/PennLINC/qsiprep/pull/542 +* ENH: Replace fsl applymask by @jbh1091 in https://github.com/PennLINC/qsiprep/pull/544 +* Replace fsl split by @jbh1091 in https://github.com/PennLINC/qsiprep/pull/548 +* [FIX] Update distortion_group_merge.py by @smeisler in https://github.com/PennLINC/qsiprep/pull/555 +* [ENH] Redo anatomical workflow by @mattcieslak in https://github.com/PennLINC/qsiprep/pull/553 +* [FIX] remove pre bids-filter acq type argument by @octomike in https://github.com/PennLINC/qsiprep/pull/557 +* FIX: Replace deprecated `np.int` instances by @smeisler in https://github.com/PennLINC/qsiprep/pull/558 +* [WIP] ENH: 482 remove fsl dependency by @jbh1091 in https://github.com/PennLINC/qsiprep/pull/498 +* [ENH] Update TORTOISE for improved T2w registration by @mattcieslak in https://github.com/PennLINC/qsiprep/pull/564 +* [FIX] T2w anat-modality issues by @mattcieslak in https://github.com/PennLINC/qsiprep/pull/565 +* [FIX] update boost in tortoise by @mattcieslak in https://github.com/PennLINC/qsiprep/pull/569 +* [FIX] connections on multi-anat workflow by @mattcieslak in https://github.com/PennLINC/qsiprep/pull/572 +* [ENH] Update DSI Studio to the latest commit by @mattcieslak in https://github.com/PennLINC/qsiprep/pull/573 +* [ENH] Add DSI Studio AutoTrack recon workflow by @mattcieslak in https://github.com/PennLINC/qsiprep/pull/576 + +## New Contributors +* @dependabot made their first contribution in https://github.com/PennLINC/qsiprep/pull/539 +* @jbh1091 made their first contribution in https://github.com/PennLINC/qsiprep/pull/542 +* @smeisler made their first contribution in https://github.com/PennLINC/qsiprep/pull/555 + +**Full Changelog**: https://github.com/PennLINC/qsiprep/compare/0.17.0...0.18.0alpha0 + + 0.16.1 (October 10, 2022) ========================= diff --git a/qsiprep/data/boilerplate.bib b/qsiprep/data/boilerplate.bib index 62b1e79b..d7d07cef 100644 --- a/qsiprep/data/boilerplate.bib +++ b/qsiprep/data/boilerplate.bib @@ -134,7 +134,7 @@ @article{ants year = 2008 } -@article{fsl_fast, +@article{fsl_fast, author = {Zhang, Y. and Brady, M. and Smith, S.}, doi = {10.1109/42.906424}, issn = {0278-0062}, @@ -594,4 +594,56 @@ @article{pfgibbs pages={2733--2750}, year={2021}, publisher={Wiley Online Library} +} + +@article{synthstrip, + title={SynthStrip: skull-stripping for any brain image}, + author={Hoopes, Andrew and Mora, Jocelyn S and Dalca, Adrian V and Fischl, Bruce and Hoffmann, Malte}, + journal={NeuroImage}, + volume={260}, + pages={119474}, + year={2022}, + publisher={Elsevier} +} + +@article{synthseg1, + title={SynthSeg: Segmentation of brain MRI scans of any contrast and resolution without retraining}, + author={Billot, Benjamin and Greve, Douglas N and Puonti, Oula and Thielscher, Axel and Van Leemput, Koen and Fischl, Bruce and Dalca, Adrian V and Iglesias, Juan Eugenio and others}, + journal={Medical image analysis}, + volume={86}, + pages={102789}, + year={2023}, + publisher={Elsevier} +} + +@article{synthseg2, + title={Robust machine learning segmentation for large-scale analysis of heterogeneous clinical brain MRI datasets}, + author={Billot, Benjamin and Magdamo, Colin and Cheng, You and Arnold, Steven E and Das, Sudeshna and Iglesias, Juan Eugenio}, + journal={Proceedings of the National Academy of Sciences}, + volume={120}, + number={9}, + pages={e2216399120}, + year={2023}, + publisher={National Acad Sciences} +} + +@article{yeh2013deterministic, + title={Deterministic diffusion fiber tracking improved by quantitative anisotropy}, + author={Yeh, Fang-Cheng and Verstynen, Timothy D and Wang, Yibao and Fern{\'a}ndez-Miranda, Juan C and Tseng, Wen-Yih Isaac}, + journal={PloS one}, + volume={8}, + number={11}, + pages={e80713}, + year={2013}, + publisher={Public Library of Science San Francisco, USA} +} + +@article{autotrack, + title={Shape analysis of the human association pathways}, + author={Yeh, Fang-Cheng}, + journal={Neuroimage}, + volume={223}, + pages={117329}, + year={2020}, + publisher={Elsevier} } \ No newline at end of file diff --git a/qsiprep/interfaces/dsi_studio.py b/qsiprep/interfaces/dsi_studio.py index c05ab688..cecf9180 100644 --- a/qsiprep/interfaces/dsi_studio.py +++ b/qsiprep/interfaces/dsi_studio.py @@ -18,6 +18,7 @@ import pandas as pd from .bids import get_bids_params LOGGER = logging.getLogger('nipype.interface') +DSI_STUDIO_VERSION = "94b9c79" class DSIStudioCommandLineInputSpec(CommandLineInputSpec): @@ -807,6 +808,7 @@ class _AutoTrackInputSpec(DSIStudioCommandLineInputSpec): argstr="%s", usedefault=True, desc="Forces DSI Studio to write results in cwd") + _boilerplate_traits = ["track_id", "track_voxel_ratio", "tolerance", "yield_rate"] class _AutoTrackOutputSpec(DSIStudioCommandLineInputSpec): diff --git a/qsiprep/interfaces/utils.py b/qsiprep/interfaces/utils.py index e1721711..4f2c269d 100644 --- a/qsiprep/interfaces/utils.py +++ b/qsiprep/interfaces/utils.py @@ -19,13 +19,12 @@ traits, isdefined, File, InputMultiPath, TraitedSpec, DynamicTraitedSpec, BaseInterfaceInputSpec, SimpleInterface ) -from nipype.interfaces.io import add_traits +from nipype.interfaces.io import add_traits, isdefined from nipype.interfaces import ants from ..utils.atlases import get_atlases IFLOGGER = logging.getLogger('nipype.interfaces') - class GetConnectivityAtlasesInputSpec(BaseInterfaceInputSpec): atlas_names = traits.List(mandatory=True, desc='atlas names to be used') forward_transform = File(exists=True, desc='transform to get atlas into T1w space if desired') @@ -514,3 +513,21 @@ def _concat_xfms(in_list, invert): out_xfm = np.linalg.inv(out_xfm) return out_xfm + + +def interface_to_boilerplate(interface, ignore=["args", "environ", "output_dir"]): + """Create a string from an interface.""" + + if not hasattr(interface.inputs, "_boilerplate_traits"): + return "" + boilerplate_traits = interface.inputs._boilerplate_traits + display_items = [] + for trait_name, trait_value in interface.inputs.traits(): + if not trait_name in boilerplate_traits: + continue + if not isdefined(trait_value): + continue + + + + return "" diff --git a/qsiprep/workflows/anatomical/volume.py b/qsiprep/workflows/anatomical/volume.py index c879a284..c57b1ec6 100644 --- a/qsiprep/workflows/anatomical/volume.py +++ b/qsiprep/workflows/anatomical/volume.py @@ -45,6 +45,8 @@ class DerivativesDataSink(FDerivativesDataSink): 'MNI152NLin2009cAsym': 'mni_icbm152_nlin_asym_09c', } +ANTS_VERSION=BrainExtraction().version or '' +FS_VERSION="7.3.1" # pylint: disable=R0914 def init_anat_preproc_wf(template, debug, dwi_only, @@ -162,9 +164,6 @@ def init_anat_preproc_wf(template, debug, dwi_only, name='outputnode') # Make sure we have usable anatomical reference images/masks - desc = """\ -A template {contrast}w image in {template} space was selected as a standard -reference image. """ get_template_image = pe.Node( GetTemplate(template_name=template, infant_mode=infant_mode, @@ -180,7 +179,7 @@ def init_anat_preproc_wf(template, debug, dwi_only, (reference_grid_wf, outputnode, [('outputnode.grid_image', 'dwi_sampling_grid')])]) if dwi_only: - LOGGER.info("No anatomical scans available! Visual reports will show template masks.") + LOGGER.info("No anatomical scans will be processed! Visual reports will show template masks.") workflow.connect([ (get_template_image, outputnode, [ ('template_file', 't1_preproc'), @@ -190,16 +189,6 @@ def init_anat_preproc_wf(template, debug, dwi_only, workflow.add_nodes([inputnode]) return workflow - workflow.__postdesc__ = """\ -Brain tissue segmentation of cerebrospinal fluid (CSF), -white-matter (WM) and gray-matter (GM) was performed on -the {contrast} using `SynthSeg` [FreeSurfer, @synthseg]. -Brain extraction was performed on the {contrast} image -using `SynthStrip` [FreeSurfer, @synthstrip] -""".format( - ants_ver=BrainExtraction().version or '', - contrast=anatomical_contrast - ) desc = """Anatomical data preprocessing : """ @@ -215,7 +204,7 @@ def init_anat_preproc_wf(template, debug, dwi_only, """ workflow.__desc__ = desc.format( num_anats=num_anat_images, - ants_ver=BrainExtraction().version or '', + ants_ver=ANTS_VERSION, contrast=anatomical_contrast[:-1], # remove the "w" template=template ) @@ -242,6 +231,16 @@ def init_anat_preproc_wf(template, debug, dwi_only, sloppy=debug, name="synthseg_anat_wf") + # Synthstrip is used a lot elsewhere, so make boilerplate for the anatomy-specific + # version here. TODO: get version number automatically + workflow.__postdesc__ = """\ +Brain extraction was performed on the {contrast} image using +SynthStrip [@synthstrip] and automated segmentation was +performed using SynthSeg [@synthseg1, @synthseg2] from +FreeSurfer version {fs_version}. """.format( + fs_version=FS_VERSION, + contrast=anatomical_contrast) + # Perform registrations anat_normalization_wf = init_anat_normalization_wf( sloppy=debug, @@ -442,28 +441,17 @@ def init_t2w_preproc_wf(omp_nthreads, num_t2ws, longitudinal, sloppy, fields=['t2_preproc', 't2w_unfatsat']), name='outputnode') -# desc = """\ -# Additionally, a total of {num_t2ws} T2-weighted (T2w) images were found within the input -# BIDS dataset. -# All of them were corrected for intensity non-uniformity (INU) -# using `N4BiasFieldCorrection` [@n4, ANTs {ants_ver}]. -# """ if num_t2ws > 1 else """\ -# The T2-weighted (T2w) image was corrected for intensity non-uniformity (INU) -# using `N4BiasFieldCorrection` [@n4, ANTs {ants_ver}], -# and used as an anatomical reference throughout the workflow. -# """ -# workflow.__desc__ = desc.format( -# num_anats=num_t2ws, -# ants_ver=BrainExtraction().version or '' -# ) - - # Ensure there is 1 and only 1 anatomical reference + # Ensure there is 1 and only 1 T2w reference anat_reference_wf = init_anat_template_wf(longitudinal=longitudinal, omp_nthreads=omp_nthreads, num_images=num_t2ws, sloppy=sloppy, anatomical_contrast="T2w") - + # ^ this also provides some boilerplate. + workflow.__postdesc__ = """\ +The additional T2w reference image was registered to the T1w-ACPC reference +image using an affine transformation in antsRegistration. +""" # Skull strip the anatomical reference synthstrip_anat_wf = init_synthstrip_wf( do_padding=True, @@ -474,7 +462,7 @@ def init_t2w_preproc_wf(omp_nthreads, num_t2ws, longitudinal, sloppy, # Perform registrations settings = pkgr("qsiprep", "data/affine.json") t2_brain_to_t1_brain = pe.Node( - ants.Registration(), + ants.Registration(from_file=settings), name="t2_brain_to_t1_brain", n_procs=omp_nthreads) @@ -728,6 +716,10 @@ def init_anat_normalization_wf(sloppy, template_name, omp_nthreads, # get a good ACPC transform + desc = """\ +The anatomical reference image was reoriented into AC-PC alignment via +a 6-DOF transform extracted from a full Affine registration to the +{} template. """.format(template_name) acpc_settings = pkgr( "qsiprep", "data/intramodal_ACPC.json" if not sloppy else "data/intramodal_ACPC_sloppy.json") @@ -762,8 +754,11 @@ def init_anat_normalization_wf(sloppy, template_name, omp_nthreads, ]) if not do_nonlinear: + workflow.__desc__ = desc return workflow - + desc += """\ +A full nonlinear registration to the template from AC-PC space was +estimated via symmetric nonlinear registration (SyN) using antsRegistration. """ rigid_acpc_resample_anat = pe.Node( ants.ApplyTransforms(input_image_type=0, interpolation='LanczosWindowedSinc'), @@ -820,6 +815,7 @@ def init_anat_normalization_wf(sloppy, template_name, omp_nthreads, ]) if has_rois: + desc += "ROI masks of abnormal tissue were incorporated into the registration. " rigid_acpc_resample_roi = pe.Node( ants.ApplyTransforms(input_image_type=0, interpolation='MultiLabel'), @@ -834,7 +830,7 @@ def init_anat_normalization_wf(sloppy, template_name, omp_nthreads, ('roi', 'input_image')]), ]) - + workflow.__desc__ = desc return workflow diff --git a/qsiprep/workflows/recon/dsi_studio.py b/qsiprep/workflows/recon/dsi_studio.py index e6464572..275de18f 100644 --- a/qsiprep/workflows/recon/dsi_studio.py +++ b/qsiprep/workflows/recon/dsi_studio.py @@ -13,7 +13,7 @@ DSIStudioAtlasGraph, DSIStudioExport, DSIStudioTracking, AutoTrack, _get_dsi_studio_bundles, FixDSIStudioExportHeader, AggregateAutoTrackResults, - AutoTrackInit) + AutoTrackInit, DSI_STUDIO_VERSION) import logging from ...interfaces.bids import ReconDerivativesDataSink @@ -70,7 +70,7 @@ def init_dsi_studio_recon_wf(omp_nthreads, available_anatomical_data, name="dsi_ desc += """\ Diffusion orientation distribution functions (ODFs) were reconstructed using generalized q-sampling imaging (GQI, @yeh2010gqi) with a ratio of mean diffusion -distance of %02f.""" % romdd +distance of %02f in DSI Studio (version %s). """ % (romdd, DSI_STUDIO_VERSION) workflow.connect([ (inputnode, create_src, [('dwi_file', 'input_nifti_file'), @@ -190,6 +190,10 @@ def init_dsi_studio_tractography_wf(omp_nthreads, available_anatomical_data, nam name="outputnode") plot_reports = params.pop("plot_reports", True) workflow = Workflow(name=name) + desc = """DSI Studio Tractography + +: Tractography was run in DSI Studio (version %s) using a deterministic algorithm +[@yeh2013deterministic]. """ % DSI_STUDIO_VERSION tracking = pe.Node( DSIStudioTracking( num_threads=omp_nthreads, @@ -212,11 +216,7 @@ def init_dsi_studio_tractography_wf(omp_nthreads, available_anatomical_data, nam def init_dsi_studio_autotrack_wf(omp_nthreads, available_anatomical_data, params={}, output_suffix="", name="dsi_studio_autotrack_wf"): - """Calculate streamline-based connectivity matrices using DSI Studio. - - DSI Studio has a deterministic tractography algorithm that can be used to - estimate pairwise regional connectivity. It calculates multiple connectivity - measures. + """Run DSI Studio's AutoTrack method to produce bundles and bundle stats. Inputs @@ -225,9 +225,40 @@ def init_dsi_studio_autotrack_wf(omp_nthreads, available_anatomical_data, Outputs + tck_files + MRtrix3 format tck files for each bundle + bundle_names + Names that describe which bundles are present in `tck_files` Params: + track_id: str + specify the id number or the name of the bundle. The id can be found in + /atlas/ICBM152/HCP1065.tt.gz.txt . This text file is included in DSI + Studio package (For Mac, right-click on dsi_studio_64.app to find + content). You can specify partial name of the bundle: + + example: + for tracking left and right arcuate fasciculus, assign + --track_id=0,1 or --track_id=arcuate (DSI Studio will find bundles + with names containing "arcuate", case insensitive) + + example: + for tracking left and right arcuate and cingulum, assign + -track_id=0,1,2,3 or -track_id=arcuate,cingulum + + track_voxel_ratio: float + the track-voxel ratio for the total number of streamline count. A larger + value gives better mapping with the expense of computation time. (default: 2.0) + + tolerance: str + the tolerance for the bundle recognition. The unit is in mm. Multiple values + can be assigned using comma separator. A larger value may include larger track + variation but also subject to more false results. (default: "22,26,30") + + yield_rate: float + This rate will be used to terminate tracking early if DSI Studio finds that the + fiber tracking is not generating results. (default: 0.00001) """ if omp_nthreads < 3: LOGGER.warn("Please set --omp-nthreads to 3 or greater and ensure you have enough CPU " @@ -238,11 +269,18 @@ def init_dsi_studio_autotrack_wf(omp_nthreads, available_anatomical_data, name="inputnode") outputnode = pe.Node(niu.IdentityInterface(fields=['tck_files', "bundle_names"]), name="outputnode") + desc = """DSI Studio Automatic Tractography +: Automatic Tractography was run in DSI Studio (version %s) and +bundle shape statistics were calculated [@autotrack]. """ % DSI_STUDIO_VERSION + plot_reports = params.pop("plot_reports", True) bundle_names = _get_dsi_studio_bundles(params.get("track_id", "")) - LOGGER.info("Running AutoTrack on the following bundles:\n\t" + "\n\t".join(bundle_names)) + bundle_desc = "AutoTrack attempted to reconstruct the following bundles:\n * " \ + + "\n * ".join(bundle_names) + "\n\n" + LOGGER.info(bundle_desc) workflow = Workflow(name=name) + workflow.__desc__ = desc + bundle_desc # This initial autotrack is used to calculate the registration to the template register_bundles = pe.Node( @@ -286,7 +324,7 @@ def init_dsi_studio_autotrack_wf(omp_nthreads, available_anatomical_data, ds_mapping = pe.Node( ReconDerivativesDataSink( suffix=output_suffix), - name="ds__csv", + name="ds_mapping", run_without_submitting=True) workflow.connect([