diff --git a/.circleci/config.yml b/.circleci/config.yml index 937bde1a..c10b3a8a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -125,6 +125,30 @@ jobs: paths: - /src/qsirecon/.circleci/data/hsvs_data + Recon_SS3T_FOD_AutoTrack: + <<: *dockersetup + steps: + - checkout + - restore_cache: + key: singleshell_output-01 + - run: *runinstall + - run: + name: Test the SS3T AutoTrack workflow + no_output_timeout: 1h + command: | + pytest -rP -o log_cli=true -m "ss3t_fod_autotrack" --cov-config=/src/qsirecon/pyproject.toml --cov-append --cov-report term-missing --cov=qsirecon --data_dir=/src/qsirecon/.circleci/data --output_dir=/src/qsirecon/.circleci/out --working_dir=/src/qsirecon/.circleci/work qsirecon + mkdir /src/coverage + mv /src/qsirecon/.coverage /src/coverage/.coverage.ss3t_fod_autotrack + # remove nifti files before uploading artifacts + find /src/qsirecon/.circleci/out/ -name "*.nii.gz" -type f -delete + find /src/qsirecon/.circleci/out/ -name "*.fib.gz" -type f -delete + - persist_to_workspace: + root: /src/coverage/ + paths: + - .coverage.ss3t_fod_autotrack + - store_artifacts: + path: /src/qsirecon/.circleci/out/ss3t_fod_autotrack/ + Recon_3Tissue_Singleshell_ACT: <<: *dockersetup steps: @@ -616,6 +640,13 @@ workflows: tags: only: /.*/ + - Recon_SS3T_FOD_AutoTrack: + requires: + - download_singleshell_output + filters: + tags: + only: /.*/ + - Recon_msmt_Multishell_HSVS: requires: - download_hsvs_data @@ -718,6 +749,7 @@ workflows: requires: - Recon_3Tissue_Singleshell_ACT - Recon_3Tissue_Singleshell_NoACT + - Recon_SS3T_FOD_AutoTrack - Recon_MRtrix3 - Recon_Interfaces - Recon_AutoTrack @@ -743,6 +775,7 @@ workflows: requires: - Recon_3Tissue_Singleshell_ACT - Recon_3Tissue_Singleshell_NoACT + - Recon_SS3T_FOD_AutoTrack - Recon_MRtrix3 - Recon_Interfaces - Recon_msmt_Multishell_HSVS diff --git a/docs/builtin_workflows.rst b/docs/builtin_workflows.rst index 5fcb4473..c25a268a 100644 --- a/docs/builtin_workflows.rst +++ b/docs/builtin_workflows.rst @@ -273,16 +273,22 @@ Other Outputs :widths: 15, 30 -.. _ss3t_autotrack: +.. _ss3t_fod_autotrack: -``ss3t_autotrack`` -======================== +``ss3t_fod_autotrack`` +====================== This workflow is identical to :ref:`dsi_studio_autotrack`, except it substitutes the GQI fit with the ``ss3t_csd_beta1`` algorithm :footcite:p:`dhollander2016novel` to estimate FODs for white matter. +A GQI reconstruction is performed first based on the entire input data. +The QA and ISO images from GQI are used to register the ACPC data to DSI Studio's ICBM 152 template. +The GQI-based registration is used to transform the template bundles to subject ACPC space, +where the SS3T-based FODs are used for tractography. + This is a good workflow for doing tractometry on low-quality single shell data. +If more than one shell is present in the input data, only the highest b-value shell is used. Scalar Maps ----------- @@ -295,7 +301,7 @@ Other Outputs ------------- .. csv-table:: :header: "File Name", "Description" - :file: nonscalars/ss3t_autotrack.csv + :file: nonscalars/ss3t_fod_autotrack.csv :widths: 15, 30 .. _tortoise: @@ -483,6 +489,8 @@ the model-fitting workflows and which sampling schemes work with them. +-------------------------------------------+-------------+------------+-----------------+ |:ref:`mrtrix_singleshell_ss3t_ACT-fast`\* | No | No | Yes | +-------------------------------------------+-------------+------------+-----------------+ +|:ref:`ss3t_fod_autotrack` | Yes | No | Yes | ++-------------------------------------------+-------------+------------+-----------------+ |:ref:`pyafq_tractometry` | Yes | No | Yes | +-------------------------------------------+-------------+------------+-----------------+ |:ref:`pyafq_input_trk` | Yes | No | Yes | @@ -495,8 +503,6 @@ the model-fitting workflows and which sampling schemes work with them. +-------------------------------------------+-------------+------------+-----------------+ |:ref:`dsi_studio_autotrack` | Yes | Yes | Yes | +-------------------------------------------+-------------+------------+-----------------+ -|:ref:`ss3t_autotrack` | No | No | Yes | -+-------------------------------------------+-------------+------------+-----------------+ |:ref:`dipy_mapmri` | Yes | Yes | No | +-------------------------------------------+-------------+------------+-----------------+ |:ref:`dipy_dki` | Yes | No | No | diff --git a/docs/nonscalars/README b/docs/nonscalars/README new file mode 100644 index 00000000..1d78a8a9 --- /dev/null +++ b/docs/nonscalars/README @@ -0,0 +1,2 @@ +WARING: Do not edit these csv files directly! +They are created by ../update_scalardefs.py \ No newline at end of file diff --git a/docs/nonscalars/dsistudio_autotrack.csv b/docs/nonscalars/dsistudio_autotrack.csv index 16b67cfa..0ffe856b 100644 --- a/docs/nonscalars/dsistudio_autotrack.csv +++ b/docs/nonscalars/dsistudio_autotrack.csv @@ -1,5 +1,5 @@ -\*_streamlines.tck.gz,One tck.gz per bundle. The bundle represented by this file is specified in the ``bundle-`` tag. -\*bundles-DSIStudio\*_scalarstats.csv,Statistics on scalars produced by this workflow +\*_streamlines.tck.gz,One tck.gz per bundle. The bundle represented by this file is specified in the ``bundle-`` tag. +\*bundles-DSIStudio\*_scalarstats.csv,Statistics on scalars produced by this workflow \*bundles-DSIStudio\*_tdistats.tsv,Statistics on streamline density in voxels -\*space-ACPC\*_dwimap.fib.gz,DSI Studio fib format containing the GQI ODFs used for AutoTrack. -\*space-ACPC\*_mapping.map.gz,Mapping file produced by DSI Studio. +\*space-ACPC|T1w\*_dwimap.fib.gz,DSI Studio fib format containing the GQI ODFs used for AutoTrack. +\*space-ACPC|T1w\*_dwimap.map.gz,Mapping file produced by DSI Studio. diff --git a/docs/nonscalars/dsistudio_gqi.csv b/docs/nonscalars/dsistudio_gqi.csv index 483a92dc..80fa3577 100644 --- a/docs/nonscalars/dsistudio_gqi.csv +++ b/docs/nonscalars/dsistudio_gqi.csv @@ -1,3 +1,3 @@ -\*_connectivity.mat,MATLAB format mat file containing connectivity matrices for all the selected atlases. This is an hdf5-format file and can be read using ``scipy.io.matlab.loadmat`` in Python. -\*space-ACPC\*_dwimap.fib.gz,DSI Studio fib format containing the GQI ODFs used for AutoTrack. -\*space-ACPC\*_mapping.map.gz,Mapping file produced by DSI Studio. +\*_connectivity.mat,MATLAB format mat file containing connectivity matrices for all the selected atlases. This is an hdf5-format file and can be read using ``scipy.io.matlab.loadmat`` in Python. +\*space-ACPC|T1w\*_dwimap.fib.gz,DSI Studio fib format containing the GQI ODFs used for AutoTrack. +\*space-ACPC|T1w\*_dwimap.map.gz,Mapping file produced by DSI Studio. diff --git a/docs/nonscalars/mrtrix_anat.csv b/docs/nonscalars/mrtrix_anat.csv index de0da820..3f6291bb 100644 --- a/docs/nonscalars/mrtrix_anat.csv +++ b/docs/nonscalars/mrtrix_anat.csv @@ -1,2 +1,2 @@ -\*space-ACPC\*seg-hsvs\*_dseg.nii.gz,Hybrid Surface/Volume Segmentation in MRtrix3 5tt format. Aligned in coordinate space to ``space-ACPC``. +\*space-ACPC|T1w\*seg-hsvs\*_dseg.nii.gz,Hybrid Surface/Voume Segmentation in MRtrix3 5tt format. Aligned in coordinate space to ``space-ACPC``. \*space-fsnative\*seg-hsvs\*_dseg.nii.gz,Hybrid Surface/Volume Segmentation in MRtrix3 5tt format. Aligned to the FreeSurfer ``orig.mgz`` image. diff --git a/docs/nonscalars/ss3t_fod_autotrack.csv b/docs/nonscalars/ss3t_fod_autotrack.csv new file mode 100644 index 00000000..21370341 --- /dev/null +++ b/docs/nonscalars/ss3t_fod_autotrack.csv @@ -0,0 +1,6 @@ +\*bundles-DSIStudio\*_scalarstats.csv,Statistics on scalars produced by this workflow. +\*bundles-DSIStudio\*_tdistats.tsv,Statistics on streamline density in voxels. +\*model-ss3t\*_streamlines.tck.gz,One tck.gz per bundle. The bundle represented by this file is specified in the ``bundle-`` tag. Bundles were tracked using the SS3t FODs. +\*space-ACPC|T1w\*model-gqi\*_dwimap.fib.gz,DSI Studio fib format containing the GQI ODFs used for AutoTrack registration. +\*space-ACPC|T1w\*model-ss3t\*_dwimap.fib.gz,DSI Studio fib format containing the SS3T FODs used for AutoTrack. +\*space-ACPC|T1w\*model-ss3t\*_dwimap.map.gz,Mapping file produced by DSI Studio. Here the model entity specifies ss3t so that DSI Studio associates the mapping with the model-ss3t fib.gz file. Be aware that this mapping was created using the model-gqi fib.gz file. diff --git a/docs/recon_scalars/README b/docs/recon_scalars/README new file mode 100644 index 00000000..1d78a8a9 --- /dev/null +++ b/docs/recon_scalars/README @@ -0,0 +1,2 @@ +WARING: Do not edit these csv files directly! +They are created by ../update_scalardefs.py \ No newline at end of file diff --git a/docs/update_scalardefs.py b/docs/update_scalardefs.py index 6f00af39..e8c89b4d 100644 --- a/docs/update_scalardefs.py +++ b/docs/update_scalardefs.py @@ -90,7 +90,7 @@ def outputs_to_csv(output_def, output_csv): load_yaml(load_data("nonscalars/csdsi_3dshore.yaml")), "nonscalars/csdsi_3dshore.csv" ) outputs_to_csv( - load_yaml(load_data("nonscalars/ss3t_autotrack.yaml")), "nonscalars/ss3t_autotrack.csv" + load_yaml(load_data("nonscalars/ss3t_fod_autotrack.yaml")), "nonscalars/ss3t_fod_autotrack.csv" ) outputs_to_csv(load_yaml(load_data("nonscalars/mrtrix_dwi.yaml")), "nonscalars/mrtrix_dwi.csv") outputs_to_csv(load_yaml(load_data("nonscalars/mrtrix_anat.yaml")), "nonscalars/mrtrix_anat.csv") diff --git a/qsirecon/cli/convertODFs.py b/qsirecon/cli/convertODFs.py index cffdd9e0..4ee61586 100644 --- a/qsirecon/cli/convertODFs.py +++ b/qsirecon/cli/convertODFs.py @@ -111,7 +111,7 @@ def mif_to_fib(): if opts.mask is not None: converter = FODtoFIBGZ( mif_file=opts.mif, - fib_file=opts.fib, + output_fib_file=opts.fib, num_fibers=opts.num_fibers, unit_odf=opts.unit_odf, mask_file=opts.mask, @@ -119,7 +119,7 @@ def mif_to_fib(): else: converter = FODtoFIBGZ( mif_file=opts.mif, - fib_file=opts.fib, + output_fib_file=opts.fib, num_fibers=opts.num_fibers, unit_odf=opts.unit_odf, ) diff --git a/qsirecon/data/io_spec.json b/qsirecon/data/io_spec.json index 1766d5e0..5991aa01 100644 --- a/qsirecon/data/io_spec.json +++ b/qsirecon/data/io_spec.json @@ -14,6 +14,10 @@ "name": "seg", "pattern": "(?:^|_)seg-([a-zA-Z0-9]+)" }, + { + "name": "dsistudiotemplate", + "pattern": "([a-zA-Z0-9_]+)" + }, { "name": "model", "pattern": "(?:^|_)model-([a-zA-Z0-9]+)" @@ -40,6 +44,7 @@ "sub-{subject}[/ses-{session}]/{datatype|dwi}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_seg-{seg}][_model-{model}][_bundles-{bundles}][_param-{param}][_bundle-{bundle}][_label-{label}][_desc-{desc}]_{suffix}.{extension|tck.gz}", "sub-{subject}[/ses-{session}]/{datatype|dwi}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_seg-{seg}][_model-{model}][_bundles-{bundles}][_param-{param}][_bundle-{bundle}][_label-{label}][_desc-{desc}]_{suffix}.{extension|tsv}", "sub-{subject}[/ses-{session}]/{datatype|dwi}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_seg-{seg}][_model-{model}][_bundles-{bundles}][_param-{param}][_bundle-{bundle}][_label-{label}][_desc-{desc}]_{suffix}.{extension|map.gz}", + "sub-{subject}[/ses-{session}]/{datatype|dwi}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_seg-{seg}][_model-{model}][_bundles-{bundles}][_param-{param}][_bundle-{bundle}][_label-{label}][_desc-{desc}]_{suffix}.fib.gz[.{dsistudiotemplate}].{extension|map.gz}", "sub-{subject}[/ses-{session}]/{datatype|dwi}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_seg-{seg}][_model-{model}][_bundles-{bundles}][_param-{param}][_bundle-{bundle}][_label-{label}][_desc-{desc}]_{suffix}.{extension|nii.gz}", "sub-{subject}[/ses-{session}]/{datatype|dwi}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_seg-{seg}][_model-{model}][_bundles-{bundles}][_param-{param}][_bundle-{bundle}][_label-{label}][_desc-{desc}]_{suffix}.{extension|nii.gz}", "sub-{subject}[/ses-{session}]/{datatype|dwi}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_seg-{seg}][_model-{model}][_bundles-{bundles}][_param-{param}][_bundle-{bundle}][_label-{label}][_desc-{desc}]_{suffix}.{extension|txt}", diff --git a/qsirecon/data/nonscalars/dsistudio_autotrack.yaml b/qsirecon/data/nonscalars/dsistudio_autotrack.yaml index 8dcba8c1..51ea8729 100644 --- a/qsirecon/data/nonscalars/dsistudio_autotrack.yaml +++ b/qsirecon/data/nonscalars/dsistudio_autotrack.yaml @@ -26,7 +26,7 @@ scalarstats: fibgz: bids: - space: T1w + space: ACPC|T1w suffix: dwimap extension: fib.gz metadata: @@ -36,8 +36,8 @@ fibgz: mapping: bids: - space: T1w - suffix: mapping + space: ACPC|T1w + suffix: dwimap extension: map.gz metadata: Description: Mapping file produced by DSI Studio. diff --git a/qsirecon/data/nonscalars/dsistudio_gqi.yaml b/qsirecon/data/nonscalars/dsistudio_gqi.yaml index 1a535e5d..1be98a74 100644 --- a/qsirecon/data/nonscalars/dsistudio_gqi.yaml +++ b/qsirecon/data/nonscalars/dsistudio_gqi.yaml @@ -10,7 +10,7 @@ conmat: fibgz: bids: - space: T1w + space: ACPC|T1w suffix: dwimap extension: fib.gz metadata: @@ -20,8 +20,8 @@ fibgz: mapping: bids: - space: T1w - suffix: mapping + space: ACPC|T1w + suffix: dwimap extension: map.gz metadata: Description: Mapping file produced by DSI Studio. diff --git a/qsirecon/data/nonscalars/mrtrix_anat.yaml b/qsirecon/data/nonscalars/mrtrix_anat.yaml index 930f1456..78130c6c 100644 --- a/qsirecon/data/nonscalars/mrtrix_anat.yaml +++ b/qsirecon/data/nonscalars/mrtrix_anat.yaml @@ -2,7 +2,7 @@ qsiprep_5tt_hsvs: bids: seg: hsvs - space: T1w + space: ACPC|T1w suffix: dseg extension: nii.gz metadata: @@ -18,5 +18,5 @@ fs_5tt_hsvs: extension: nii.gz metadata: Description: | - Hybrid Surface/Voume Segmentation in MRtrix3 5tt format. + Hybrid Surface/Volume Segmentation in MRtrix3 5tt format. Aligned to the FreeSurfer ``orig.mgz`` image. \ No newline at end of file diff --git a/qsirecon/data/nonscalars/ss3t_autotrack.yaml b/qsirecon/data/nonscalars/ss3t_autotrack.yaml deleted file mode 100644 index ba87f5e7..00000000 --- a/qsirecon/data/nonscalars/ss3t_autotrack.yaml +++ /dev/null @@ -1,43 +0,0 @@ -bundles: - bids: - suffix: streamlines - extension: tck.gz - metadata: - Description: | - One tck.gz per bundle. The bundle represented by this file is - specified in the ``bundle-`` tag. - -tdistats: - bids: - bundles: DSIStudio - suffix: tdistats - extension: tsv - metadata: - Description: Statistics on streamline density in voxels - -scalarstats: - bids: - bundles: DSIStudio - suffix: scalarstats - extension: csv - metadata: - Description: | - Statistics on scalars produced by this workflow - -fibgz: - bids: - space: T1w - suffix: dwimap - extension: fib.gz - metadata: - Description: | - DSI Studio fib format containing the SS3T FODs used for - AutoTrack. - -mapping: - bids: - space: T1w - suffix: mapping - extension: map.gz - metadata: - Description: Mapping file produced by DSI Studio. diff --git a/qsirecon/data/nonscalars/ss3t_fod_autotrack.yaml b/qsirecon/data/nonscalars/ss3t_fod_autotrack.yaml new file mode 100644 index 00000000..732aa803 --- /dev/null +++ b/qsirecon/data/nonscalars/ss3t_fod_autotrack.yaml @@ -0,0 +1,62 @@ +bundles: + bids: + suffix: streamlines + model: ss3t + extension: tck.gz + metadata: + Description: | + One tck.gz per bundle. The bundle represented by this file is + specified in the ``bundle-`` tag. Bundles were tracked using the + SS3t FODs. + +tdistats: + bids: + bundles: DSIStudio + suffix: tdistats + extension: tsv + metadata: + Description: Statistics on streamline density in voxels. + +scalarstats: + bids: + bundles: DSIStudio + suffix: scalarstats + extension: csv + metadata: + Description: | + Statistics on scalars produced by this workflow. + +gqi_fibgz: + bids: + space: ACPC|T1w + model: gqi + suffix: dwimap + extension: fib.gz + metadata: + Description: | + DSI Studio fib format containing the GQI ODFs used for + AutoTrack registration. + +fibgz: + bids: + space: ACPC|T1w + model: ss3t + suffix: dwimap + extension: fib.gz + metadata: + Description: | + DSI Studio fib format containing the SS3T FODs used for + AutoTrack. + +mapping: + bids: + space: ACPC|T1w + model: ss3t + suffix: dwimap + extension: map.gz + metadata: + Description: | + Mapping file produced by DSI Studio. Here the model entity + specifies ss3t so that DSI Studio associates the mapping + with the model-ss3t fib.gz file. Be aware that this mapping + was created using the model-gqi fib.gz file. diff --git a/qsirecon/data/pipelines/ss3t_autotrack.yaml b/qsirecon/data/pipelines/ss3t_autotrack.yaml deleted file mode 100644 index 32462a62..00000000 --- a/qsirecon/data/pipelines/ss3t_autotrack.yaml +++ /dev/null @@ -1,40 +0,0 @@ -anatomical: [] -name: mrtrix_singleshell_ss3t_noACT -nodes: -- action: csd - input: qsirecon - name: ss3t_csd - parameters: - fod: - algorithm: ss3t - mtnormalize: true - response: - algorithm: dhollander - qsirecon_suffix: MRtrix3_fork-SS3T - software: MRTrix3 -- action: reconstruction - input: qsirecon - name: dsistudio_gqi - parameters: - method: gqi - qsirecon_suffix: DSIStudio - software: DSI Studio -- action: mif_to_fib - input: ss3t_csd - name: convert_mif_to_fib - parameters: - secondary_fib: - method: gqi - qsirecon_suffix: ss3tautotrack -- action: autotrack - input: convert_mif_to_fib - name: ss3tautotrack - parameters: - threshold_index: fod - tolerance: 22,26,30 - track_id: Fasciculus,Cingulum,Aslant,Corticos,Thalamic_R,Reticular,Optic,Fornix,Corpus - track_voxel_ratio: 2.0 - yield_rate: 1.0e-06 - qsirecon_suffix: SS3TAutotrack - software: DSI Studio -space: T1w diff --git a/qsirecon/data/pipelines/ss3t_fod_autotrack.yaml b/qsirecon/data/pipelines/ss3t_fod_autotrack.yaml new file mode 100644 index 00000000..d189d3fa --- /dev/null +++ b/qsirecon/data/pipelines/ss3t_fod_autotrack.yaml @@ -0,0 +1,80 @@ +name: FODAutoTrack +nodes: + + # Fit the actual GQI model to the data +- action: reconstruction + input: qsirecon + name: dsistudio_gqi + parameters: + method: gqi + qsirecon_suffix: SS3TAutoTrack + software: DSI Studio + + # Get 3D images of DSI Studio's scalar maps +- action: export + input: dsistudio_gqi + name: gqi_scalars + qsirecon_suffix: SS3TAutoTrack + software: DSI Studio + + # Perform the registration using the GQI-based QA+ISO +- action: autotrack_registration + input: dsistudio_gqi + name: autotrack_gqi_registration + # qsirecon_suffix: Don't include here - the map.gz is saved in autotrack + software: DSI Studio + + # Select just one shell + b=0 for autotrack +- action: select_gradients + input: qsirecon + name: select_single_shell + parameters: + requested_shells: + - 0 + - highest + bval_distance_cutoff: 100 + + # Do SS3T on the single-shell data +- action: csd + software: MRTrix3 + input: select_single_shell + name: ss3t_csd + parameters: + fod: + algorithm: ss3t + mtnormalize: true + response: + algorithm: dhollander + qsirecon_suffix: SS3TAutoTrack + +- action: fod_fib_merge + name: create_fod_fib + # to include the fib file AND the map file + input: autotrack_gqi_registration + csd_input: ss3t_csd + # outputs include the FOD fib file and the map file is passed through + qsirecon_suffix: SS3TAutoTrack + parameters: + model: ss3t + +- action: autotrack + input: create_fod_fib + name: autotrack_fod + parameters: + tolerance: 22,26,30 + track_id: Association,Projection,Commissure,Cerebellum,CranialNerve + track_voxel_ratio: 2.0 + yield_rate: 1.0e-06 + model: ss3t + qsirecon_suffix: SS3TAutoTrack + software: DSI Studio + +# Uncomment to map scalar maps to template space +# - action: template_map +# input: qsirecon +# name: template_map +# parameters: +# interpolation: NearestNeighbor +# scalars_from: +# - gqi_scalars +# software: qsirecon diff --git a/qsirecon/interfaces/converters.py b/qsirecon/interfaces/converters.py index 71c72f7e..7777281c 100644 --- a/qsirecon/interfaces/converters.py +++ b/qsirecon/interfaces/converters.py @@ -5,7 +5,9 @@ import os import os.path as op import re +import shutil import subprocess +from pathlib import Path import nibabel as nb import numpy as np @@ -40,7 +42,7 @@ class FODtoFIBGZInputSpec(BaseInterfaceInputSpec): mask_file = File(exists=True) num_fibers = traits.Int(5, usedefault=True) unit_odf = traits.Bool(False, usedefault=True) - fib_file = File() + output_fib_file = File() class FODtoFIBGZOutputSpec(TraitedSpec): @@ -54,10 +56,14 @@ class FODtoFIBGZ(SimpleInterface): def _run_interface(self, runtime): mif_file = self.inputs.mif_file mask_file = self.inputs.mask_file - if isdefined(self.inputs.fib_file): - output_fib_file = self.inputs.fib_file - if output_fib_file.endswith(".gz"): - output_fib_file = output_fib_file[:-3] + if isdefined(self.inputs.output_fib_file): + output_fib_path = Path(self.inputs.output_fib_file) + if output_fib_path.name.endswith(".gz"): + LOGGER.warning("A non-gzipped output will be written.") + output_fib_path.name = output_fib_path.name[:-3] + if not output_fib_path.is_absolute(): + output_fib_file = str(Path(runtime.cwd) / output_fib_path) + else: output_fib_file = fname_presuffix( mif_file, newpath=runtime.cwd, suffix=".fib", use_ext=False @@ -95,7 +101,7 @@ def _run_interface(self, runtime): num_fibers=self.inputs.num_fibers, unit_odf=self.inputs.unit_odf, ) - os.remove("amplitudes.nii") + os.remove(odf_amplitudes_nii) return runtime @@ -211,6 +217,63 @@ def _run_interface(self, runtime): return runtime +class _MergeFODGQIFibsInputSpec(BaseInterfaceInputSpec): + csd_fib_file = File(exists=True, mandatory=True) + reference_fib_file = File(exists=True, mandatory=True) + fibgz_map = File(exists=True) + + +class _MergeFODGQIFibsOutputSpec(TraitedSpec): + fibgz = File(exists=True, mandatory=True) + fibgz_map = File(exists=True) + + +class MergeFODGQIFibs(SimpleInterface): + """Merge FOD and GQI fib files.""" + + input_spec = _MergeFODGQIFibsInputSpec + output_spec = _MergeFODGQIFibsOutputSpec + + def _run_interface(self, runtime): + + # fname presuffix doesn't work with .fib.gz + fib_name = Path(self.inputs.reference_fib_file).name.replace(".odf.", ".odf.FOD.") + merged_fib_file = str(Path(runtime.cwd) / fib_name) + merged_fib_file = ( + merged_fib_file if not merged_fib_file.endswith(".gz") else merged_fib_file[:-3] + ) + + combine_gqi_and_csd_fib_files( + path_gqi_fib=self.inputs.reference_fib_file, + path_fod_fib=self.inputs.csd_fib_file, + merged_fib=merged_fib_file, + ) + + # gzip the merged file + merged_fibgz_file = merged_fib_file + ".gz" + + p = subprocess.Popen( + ["gzip", "-v", merged_fib_file], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + response = p.communicate() + if not p.returncode == 0: + raise Exception(f"Gzip exitted with code {p.returncode}: {response}") + if not Path(merged_fibgz_file).exists(): + raise Exception(f"Failed to gzip {merged_fib_file}") + self._results["fibgz"] = merged_fibgz_file + + # Handle the map file if it was provided + if isdefined(self.inputs.fibgz_map): + LOGGER.info(f"Creating new map file to match {merged_fib_file}.") + # DSI Studio stores the template of the mapping file like icbm_adult.map.gz + dsistudiotemplate = self.inputs.fibgz_map.split(".")[-3] + new_mapping_file = merged_fibgz_file + f".{dsistudiotemplate}.map.gz" + shutil.copyfile(self.inputs.fibgz_map, new_mapping_file) + self._results["fibgz_map"] = new_mapping_file + + return runtime + + def get_dsi_studio_ODF_geometry(odf_key): mat_path = pkgr("qsirecon", "data/odfs.mat") m = loadmat(mat_path) @@ -226,6 +289,40 @@ def popen_run(arg_list): LOGGER.info(err) +def combine_gqi_and_csd_fib_files(path_gqi_fib: str, path_fod_fib: str, merged_fib: str): + """ + Combine the GQI and CSD .fib-files such that the new CSD fib file contains ODF information + from the old CSD file and DTI maps from the GQI file. + + Args: + path_gqi_file: Full path to the GQI file + path_csd_file: Full path to the csd file. This will be overwritten with the updated csd file. + """ + + gqi_data = fast_load_fibgz(path_gqi_fib) + fod_data = fast_load_fibgz(path_fod_fib) + merged_data = gqi_data.copy() + + for gqi_key in gqi_data: + if ( + re.match(r"odf\d+", gqi_key) + or re.match(r"fa\d+", gqi_key) + or re.match(r"index\d+", gqi_key) + ): + LOGGER.info(f"Deleting {gqi_key} from GQI data") + del merged_data[gqi_key] + for fod_key in fod_data: + if ( + re.match(r"odf\d+", fod_key) + or re.match(r"fa\d+", fod_key) + or re.match(r"index\d+", fod_key) + ): + LOGGER.info(f"Copying {fod_key} from FOD data") + merged_data[fod_key] = fod_data[fod_key] + + savemat(merged_fib, merged_data, format="4", appendmat=False) + + def amplitudes_to_fibgz( amplitudes_img, odf_dirs, odf_faces, output_file, mask_img, num_fibers=5, unit_odf=False ): diff --git a/qsirecon/interfaces/dsi_studio.py b/qsirecon/interfaces/dsi_studio.py index 2b3a7520..77779a56 100644 --- a/qsirecon/interfaces/dsi_studio.py +++ b/qsirecon/interfaces/dsi_studio.py @@ -31,6 +31,14 @@ LOGGER = logging.getLogger("nipype.interface") DSI_STUDIO_VERSION = "94b9c79" +DSI_STUDIO_TEMPLATES = [ + "c57bl6_mouse", + "icbm152_adult", + "indi_rhesus", + "pitt_marmoset", + "whs_sd_rat", + "dhcp_neonate", +] class DSIStudioCommandLineInputSpec(CommandLineInputSpec): @@ -712,6 +720,7 @@ class _AutoTrackOutputSpec(TraitedSpec): native_trk_files = OutputMultiObject(File(exists=True)) stat_files = OutputMultiObject(File(exists=True)) map_file = File(exists=True) + dsistudiotemplate = traits.Str(desc="DSI Studio's name for the template used for registration") class AutoTrack(CommandLine): @@ -738,7 +747,16 @@ def _list_outputs(self): raise Exception("Too many map files generated") if not map_files: raise Exception("No map files found in " + str(cwd.absolute())) - outputs["map_file"] = str(map_files[0].absolute()) + map_path = map_files[0] + outputs["map_file"] = str(map_path.absolute()) + + # Which of the template spaces was used? + template_space = traits.Undefined + for _template_name in DSI_STUDIO_TEMPLATES: + if _template_name in map_path.name: + template_space = _template_name + outputs["dsistudiotemplate"] = template_space + return outputs diff --git a/qsirecon/tests/data/autotrack_outputs.txt b/qsirecon/tests/data/autotrack_outputs.txt index 59cdfb00..62091cc9 100644 --- a/qsirecon/tests/data/autotrack_outputs.txt +++ b/qsirecon/tests/data/autotrack_outputs.txt @@ -10,11 +10,11 @@ derivatives/qsirecon-DSIStudio/logs/CITATION.tex derivatives/qsirecon-DSIStudio/sub-ABCD derivatives/qsirecon-DSIStudio/sub-ABCD.html derivatives/qsirecon-DSIStudio/sub-ABCD/dwi -derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_bundle-AssociationArcuateFasciculusL_streamlines.tck.gz -derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_bundle-AssociationArcuateFasciculusR_streamlines.tck.gz -derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_bundlestats.csv -derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_dwimap.fib.gz -derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_mapping.map.gz +derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-gqi_bundle-AssociationArcuateFasciculusL_streamlines.tck.gz +derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-gqi_bundle-AssociationArcuateFasciculusR_streamlines.tck.gz +derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-gqi_bundlestats.csv +derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-gqi_dwimap.fib.gz +derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-gqi_dwimap.fib.gz.icbm152_adult.map.gz logs logs/CITATION.bib logs/CITATION.html diff --git a/qsirecon/tests/data/scalar_mapper_outputs.txt b/qsirecon/tests/data/scalar_mapper_outputs.txt index f04ec5ed..5fe01219 100644 --- a/qsirecon/tests/data/scalar_mapper_outputs.txt +++ b/qsirecon/tests/data/scalar_mapper_outputs.txt @@ -89,13 +89,13 @@ derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NL derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-tensor_param-tyz_dwimap.nii.gz derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-tensor_param-tzz_dwimap.json derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_model-tensor_param-tzz_dwimap.nii.gz -derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_bundle-AssociationArcuateFasciculusL_streamlines.tck.gz -derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_bundle-AssociationArcuateFasciculusR_streamlines.tck.gz +derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-gqi_dwimap.fib.gz +derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-gqi_bundle-AssociationArcuateFasciculusL_streamlines.tck.gz +derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-gqi_bundle-AssociationArcuateFasciculusR_streamlines.tck.gz derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_bundles-DSIStudio_scalarstats.tsv derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_bundles-DSIStudio_tdistats.tsv -derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_bundlestats.csv -derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_dwimap.fib.gz -derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_mapping.map.gz +derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-gqi_bundlestats.csv +derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-gqi_dwimap.fib.gz.icbm152_adult.map.gz derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-gqi_param-gfa_dwimap.json derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-gqi_param-gfa_dwimap.nii.gz derivatives/qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_model-gqi_param-iso_dwimap.json @@ -133,4 +133,4 @@ logs/CITATION.bib logs/CITATION.html logs/CITATION.md logs/CITATION.tex -sub-ABCD +sub-ABCD \ No newline at end of file diff --git a/qsirecon/tests/data/ss3t_fod_autotrack_outputs.txt b/qsirecon/tests/data/ss3t_fod_autotrack_outputs.txt new file mode 100644 index 00000000..bffc4049 --- /dev/null +++ b/qsirecon/tests/data/ss3t_fod_autotrack_outputs.txt @@ -0,0 +1,64 @@ +dataset_description.json +derivatives +derivatives/qsirecon-SS3TAutoTrack +derivatives/qsirecon-SS3TAutoTrack/dataset_description.json +derivatives/qsirecon-SS3TAutoTrack/logs +derivatives/qsirecon-SS3TAutoTrack/logs/CITATION.bib +derivatives/qsirecon-SS3TAutoTrack/logs/CITATION.html +derivatives/qsirecon-SS3TAutoTrack/logs/CITATION.md +derivatives/qsirecon-SS3TAutoTrack/logs/CITATION.tex +derivatives/qsirecon-SS3TAutoTrack/sub-PNC +derivatives/qsirecon-SS3TAutoTrack/sub-PNC.html +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-ss3t_bundle-AssociationArcuateFasciculusL_streamlines.tck.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-ss3t_bundle-AssociationArcuateFasciculusR_streamlines.tck.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-ss3t_bundlestats.csv +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-ss3t_dwimap.fib.gz.icbm152_adult.map.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-gqi_dwimap.fib.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-gqi_param-gfa_dwimap.json +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-gqi_param-gfa_dwimap.nii.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-gqi_param-iso_dwimap.json +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-gqi_param-iso_dwimap.nii.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-gqi_param-qa_dwimap.json +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-gqi_param-qa_dwimap.nii.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-mtnorm_param-inliermask_dwimap.nii.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-mtnorm_param-norm_dwimap.nii.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-rdi_param-rd1_dwimap.json +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-rdi_param-rd1_dwimap.nii.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-rdi_param-rd2_dwimap.json +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-rdi_param-rd2_dwimap.nii.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-ss3t_dwimap.fib.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-ss3t_param-fod_label-CSF_dwimap.mif.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-ss3t_param-fod_label-CSF_dwimap.txt +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-ss3t_param-fod_label-GM_dwimap.mif.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-ss3t_param-fod_label-GM_dwimap.txt +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-ss3t_param-fod_label-WM_dwimap.mif.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-ss3t_param-fod_label-WM_dwimap.txt +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-ad_dwimap.json +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-ad_dwimap.nii.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-fa_dwimap.json +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-fa_dwimap.nii.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-ha_dwimap.json +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-ha_dwimap.nii.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-md_dwimap.json +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-md_dwimap.nii.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-rd_dwimap.json +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-rd_dwimap.nii.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-txx_dwimap.json +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-txx_dwimap.nii.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-txy_dwimap.json +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-txy_dwimap.nii.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-txz_dwimap.json +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-txz_dwimap.nii.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-tyy_dwimap.json +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-tyy_dwimap.nii.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-tyz_dwimap.json +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-tyz_dwimap.nii.gz +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-tzz_dwimap.json +derivatives/qsirecon-SS3TAutoTrack/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_model-tensor_param-tzz_dwimap.nii.gz +logs +logs/CITATION.bib +logs/CITATION.html +logs/CITATION.md +logs/CITATION.tex +sub-PNC \ No newline at end of file diff --git a/qsirecon/tests/test_cli.py b/qsirecon/tests/test_cli.py index 0733259c..0e9383e0 100644 --- a/qsirecon/tests/test_cli.py +++ b/qsirecon/tests/test_cli.py @@ -26,6 +26,34 @@ nipype_config.enable_debug_mode() +@pytest.mark.integration +@pytest.mark.ss3t_fod_autotrack +def test_mrtrix_singleshell_ss3t_fod_autotrack(data_dir, output_dir, working_dir): + """Test the ss3t FOD-based autotrack workflow. + + Inputs + ------ + - qsirecon single shell results + """ + TEST_NAME = "ss3t_fod_autotrack" + + dataset_dir = download_test_data("singleshell_output", data_dir) + dataset_dir = os.path.join(dataset_dir, "qsiprep") + out_dir = os.path.join(output_dir, TEST_NAME) + work_dir = os.path.join(working_dir, TEST_NAME) + + parameters = [ + dataset_dir, + out_dir, + "participant", + f"-w={work_dir}", + "--sloppy", + "--recon-spec=ss3t_fod_autotrack", + ] + + _run_and_generate(TEST_NAME, parameters, test_main=False) + + @pytest.mark.integration @pytest.mark.mrtrix_singleshell_ss3t_act def test_mrtrix_singleshell_ss3t_act(data_dir, output_dir, working_dir): diff --git a/qsirecon/workflows/recon/build_workflow.py b/qsirecon/workflows/recon/build_workflow.py index 844e7ddb..e36d5a62 100644 --- a/qsirecon/workflows/recon/build_workflow.py +++ b/qsirecon/workflows/recon/build_workflow.py @@ -5,13 +5,14 @@ from ... import config from ...interfaces.interchange import default_input_set, recon_workflow_input_fields from .amico import init_amico_noddi_fit_wf -from .converters import init_mif_to_fibgz_wf, init_qsirecon_to_fsl_wf +from .converters import init_fod_fib_wf, init_mif_to_fibgz_wf, init_qsirecon_to_fsl_wf from .dipy import ( init_dipy_brainsuite_shore_recon_wf, init_dipy_dki_recon_wf, init_dipy_mapmri_recon_wf, ) from .dsi_studio import ( + init_dsi_studio_autotrack_registration_wf, init_dsi_studio_autotrack_wf, init_dsi_studio_connectivity_wf, init_dsi_studio_export_wf, @@ -69,7 +70,7 @@ def init_dwi_recon_workflow( nodes_to_add = [] workflow_metadata_nodes = {} for node_spec in workflow_spec["nodes"]: - if not node_spec["name"]: + if not node_spec.get("name"): raise Exception(f"Node has no name [{node_spec}]") new_node = workflow_from_spec( @@ -184,6 +185,42 @@ def init_dwi_recon_workflow( node, "inputnode.mapping_metadata") # fmt:skip + # There are some special cases where we need a second input node. + if "csd_input" in node_spec: + csd_input = node_spec["csd_input"] + config.loggers.workflow.info(f"Using csd inputs from {csd_input} in {node_name}.") + + special_upstream_node = workflow.get_node(csd_input) + special_upstream_outputnode_name = f"{csd_input}.outputnode" + special_upstream_outputnode = workflow.get_node(special_upstream_outputnode_name) + special_upstream_outputs = set(special_upstream_outputnode.outputs.get().keys()) + + downstream_inputnode_name = f"{node_name}.inputnode" + downstream_inputnode = workflow.get_node(downstream_inputnode_name) + downstream_inputs = set(downstream_inputnode.outputs.get().keys()) + + connect_from_special_upstream = special_upstream_outputs.intersection( + downstream_inputs + ) + config.loggers.workflow.info( + "connecting %s from %s to %s", + connect_from_special_upstream, + special_upstream_node, + node, + ) + workflow.connect([ + ( + special_upstream_node, + node, + _as_connections( + connect_from_special_upstream - set(("mapping_metadata",)), + src_prefix='outputnode.', + dest_prefix='inputnode.', + ), + ), + ]) # fmt:skip + _check_repeats(workflow.list_node_names()) + # Set the source_file for any datasinks for node in workflow.list_node_names(): node_name = node.split(".")[-1] @@ -235,6 +272,8 @@ def workflow_from_spec(inputs_dict, node_spec): return init_dsi_studio_tractography_wf(**kwargs) if node_spec["action"] == "connectivity": return init_dsi_studio_connectivity_wf(**kwargs) + if node_spec["action"] == "autotrack_registration": + return init_dsi_studio_autotrack_registration_wf(**kwargs) if node_spec["action"] == "autotrack": return init_dsi_studio_autotrack_wf(**kwargs) @@ -293,6 +332,8 @@ def workflow_from_spec(inputs_dict, node_spec): return init_scalar_to_template_wf(**kwargs) if node_spec["action"] == "test_workflow": return init_test_wf(**kwargs) + if node_spec["action"] == "fod_fib_merge": + return init_fod_fib_wf(**kwargs) raise Exception("Unknown node %s" % node_spec) diff --git a/qsirecon/workflows/recon/converters.py b/qsirecon/workflows/recon/converters.py index 00f6046c..fc59f7b7 100644 --- a/qsirecon/workflows/recon/converters.py +++ b/qsirecon/workflows/recon/converters.py @@ -14,7 +14,7 @@ from niworkflows.engine.workflows import LiterateWorkflow as Workflow from ...interfaces.bids import DerivativesDataSink -from ...interfaces.converters import FODtoFIBGZ +from ...interfaces.converters import FODtoFIBGZ, MergeFODGQIFibs from ...interfaces.images import ConformDwi from ...interfaces.interchange import recon_workflow_input_fields from ...utils.bids import clean_datasinks @@ -51,9 +51,7 @@ def init_mif_to_fibgz_wf(inputs_dict, name="mif_to_fibgz", qsirecon_suffix="", p workflow = Workflow(name=name) convert_to_fib = pe.Node(FODtoFIBGZ(), name="convert_to_fib") workflow.connect([ - (inputnode, convert_to_fib, [ - ('fod_sh_mif', 'mif_file'), - ('fibgz', 'fib_file')]), + (inputnode, convert_to_fib, [('fod_sh_mif', 'mif_file')]), (convert_to_fib, outputnode, [('fib_file', 'fibgz')]), ]) # fmt:skip @@ -68,7 +66,7 @@ def init_mif_to_fibgz_wf(inputs_dict, name="mif_to_fibgz", qsirecon_suffix="", p name="ds_fibgz", run_without_submitting=True, ) - workflow.connect(convert_to_fib, 'fib_file', + workflow.connect(convert_to_fib, 'fib', ds_fibgz, 'in_file') # fmt:skip return clean_datasinks(workflow, qsirecon_suffix) @@ -93,6 +91,80 @@ def init_fibgz_to_mif_wf(name="fibgz_to_mif", qsirecon_suffix="", params={}): return clean_datasinks(workflow, qsirecon_suffix) +def init_fod_fib_wf(inputs_dict, name="fod_fib", qsirecon_suffix="", params={}): + """Converts MRTrix FODs to a DSI Studio fib file for the FOD-AutoTrack method. + + Inputs + + fod_sh_mif + MRTrix format mif file containing sh coefficients representing FODs. + fibgz + DSI Studio file containing a standard GQI reconstruction + fibgz_map + DSI Studio spatial normalization file. + + Outputs + + fibgz + DSI Studio fib file containing the FODs from the input ``mif_file``. + fibgz_map + DSI Studio spatial normalization file. Passed through unchanged, but renamed + to match ``fibgz`` so it will be recognized by DSI Studio. + recon_scalars + Ununsed. + + """ + inputnode = pe.Node( + niu.IdentityInterface( + fields=recon_workflow_input_fields + ["fod_sh_mif", "fibgz", "fibgz_map"] + ), + name="inputnode", + ) + outputnode = pe.Node( + niu.IdentityInterface(fields=["fibgz", "fibgz_map", "recon_scalars"]), name="outputnode" + ) + outputnode.inputs.recon_scalars = [] + workflow = Workflow(name=name) + + # Convert the FOD sh nifti into a valid fib file + convert_fod_to_fib = pe.Node(FODtoFIBGZ(), name="convert_fod_to_fib") + # Merge the reference fib file and the FOD fib file + merge_fod_and_qgi_fibs = pe.Node(MergeFODGQIFibs(), name="merge_fod_and_gqi_fibs") + + workflow.connect([ + (inputnode, convert_fod_to_fib, [ + ('fod_sh_mif', 'mif_file'), + ('dwi_mask', 'mask_file'), + ]), + (inputnode, merge_fod_and_qgi_fibs, [ + ('fibgz', 'reference_fib_file'), + ('fibgz_map', 'fibgz_map'), + ]), + (convert_fod_to_fib, merge_fod_and_qgi_fibs, [('fib_file', 'csd_fib_file')]), + (merge_fod_and_qgi_fibs, outputnode, [ + ('fibgz_map', 'fibgz_map'), + ('fibgz', 'fibgz'), + ]), + ]) # fmt:skip + + if qsirecon_suffix: + # Save the output in the outputs directory + ds_fibgz = pe.Node( + DerivativesDataSink( + dismiss_entities=("desc",), + suffix="dwimap", + extension=".fib.gz", + model=params.get("model", "csd"), + compress=True, + ), + name="ds_fibgz", + run_without_submitting=True, + ) + workflow.connect(outputnode, "fibgz", ds_fibgz, "in_file") + + return clean_datasinks(workflow, qsirecon_suffix) + + def init_qsirecon_to_fsl_wf(inputs_dict, name="qsirecon_to_fsl", qsirecon_suffix="", params={}): """Converts QSIRecon outputs (images, bval, bvec) to fsl standard orientation""" inputnode = pe.Node( diff --git a/qsirecon/workflows/recon/dsi_studio.py b/qsirecon/workflows/recon/dsi_studio.py index 9ca4e967..0c21f2db 100644 --- a/qsirecon/workflows/recon/dsi_studio.py +++ b/qsirecon/workflows/recon/dsi_studio.py @@ -147,6 +147,7 @@ def init_dsi_studio_recon_wf(inputs_dict, name="dsi_studio_recon", qsirecon_suff dismiss_entities=("desc",), suffix="dwimap", extension=".fib.gz", + model="gqi", compress=True, ), name="ds_gqi_fibgz", @@ -264,6 +265,63 @@ def init_dsi_studio_tractography_wf( return clean_datasinks(workflow, qsirecon_suffix) +def init_dsi_studio_autotrack_registration_wf( + inputs_dict, + params={}, + qsirecon_suffix="", + name="dsi_studio_autotrack_registration_wf", +): + """Run DSI Studio's AutoTrack method to create a map.gz file. No bundles are saved. + + This workflow is designed to be used as input to other workflows that need both a + fib and a map file (eg FOD Autotrack). The registration is better using GQI scalars + than the imported FOD scalars. + + As such, this workflow does not produce derivatives (qsirecon_suffix is ignored). + The map file will instead be included in the derivatives of the autotrack workflow. + + Inputs + + fibgz + A DSI Studio fib file produced by DSI Studio reconstruction. + + Outputs + + fibgz + The input fibgz file, unaltered + fibgz_map + A map.gz file corresponding to the fibgz file + + """ + inputnode = pe.Node( + niu.IdentityInterface(fields=recon_workflow_input_fields + ["fibgz"]), + name="inputnode", + ) + outputnode = pe.Node( + niu.IdentityInterface(fields=["fibgz", "fibgz_map", "recon_scalars"]), + name="outputnode", + ) + outputnode.inputs.recon_scalars = [] + + omp_nthreads = config.nipype.omp_nthreads + workflow = Workflow(name=name) + + # Run autotrack on only one bundle. The important part is getting the map.gz + registration_atk = pe.Node( + AutoTrack(num_threads=omp_nthreads, track_id="Association_ArcuateFasciculusL"), + name="registration_atk", + n_procs=omp_nthreads, + ) + + workflow.connect([ + (inputnode, registration_atk, [('fibgz', 'fib_file')]), + (inputnode, outputnode, [('fibgz', 'fibgz')]), + (registration_atk, outputnode, [('map_file', 'fibgz_map')]), + ]) # fmt:skip + + return clean_datasinks(workflow, qsirecon_suffix) + + def init_dsi_studio_autotrack_wf( inputs_dict, params={}, @@ -313,9 +371,13 @@ def init_dsi_studio_autotrack_wf( 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) + + model_name: str + The name of the model used for ODFs (default "gqi") """ inputnode = pe.Node( - niu.IdentityInterface(fields=recon_workflow_input_fields + ["fibgz"]), name="inputnode" + niu.IdentityInterface(fields=recon_workflow_input_fields + ["fibgz", "fibgz_map"]), + name="inputnode", ) outputnode = pe.Node( niu.IdentityInterface(fields=["tck_files", "bundle_names", "recon_scalars"]), @@ -327,6 +389,7 @@ def init_dsi_studio_autotrack_wf( "DSI Studio (version %s) and bundle shape statistics were calculated [@autotrack]. " % DSI_STUDIO_VERSION ) + model_name = params.pop("model", "gqi") omp_nthreads = config.nipype.omp_nthreads bundle_names = _get_dsi_studio_bundles(params.get("track_id", "")) bundle_desc = ( @@ -366,6 +429,7 @@ def init_dsi_studio_autotrack_wf( DerivativesDataSink( dismiss_entities=("desc",), suffix="streamlines", + model=model_name, extension=".tck.gz", compress=True, ), @@ -378,6 +442,7 @@ def init_dsi_studio_autotrack_wf( DerivativesDataSink( dismiss_entities=("desc",), suffix="bundlestats", + model=model_name, extension=".csv", ), name="ds_bundle_csv", @@ -388,8 +453,9 @@ def init_dsi_studio_autotrack_wf( ds_mapping = pe.Node( DerivativesDataSink( dismiss_entities=("desc",), - suffix="mapping", - extension=".map.gz", + suffix="dwimap", + model=model_name, + extension="map.gz", compress=True, ), name="ds_mapping", @@ -397,10 +463,14 @@ def init_dsi_studio_autotrack_wf( ) workflow.connect([ - (inputnode, actual_trk, [('fibgz', 'fib_file')]), + (inputnode, actual_trk, [ + ('fibgz', 'fib_file'), + ('fibgz_map', 'map_file')]), (inputnode, aggregate_atk_results, [('dwi_file', 'source_file')]), (inputnode, convert_to_tck, [('dwi_file', 'reference_nifti')]), - (actual_trk, ds_mapping, [('map_file', 'in_file')]), + (actual_trk, ds_mapping, [ + ('map_file', 'in_file'), + ('dsistudiotemplate', 'dsistudiotemplate')]), (actual_trk, aggregate_atk_results, [ ("native_trk_files", "trk_files"), ("stat_files", "stat_files"), diff --git a/qsirecon/workflows/recon/utils.py b/qsirecon/workflows/recon/utils.py index 1196f762..054cf997 100644 --- a/qsirecon/workflows/recon/utils.py +++ b/qsirecon/workflows/recon/utils.py @@ -62,7 +62,16 @@ def init_discard_repeated_samples_wf( niu.IdentityInterface(fields=recon_workflow_input_fields), name="inputnode" ) outputnode = pe.Node( - niu.IdentityInterface(fields=["dwi_file", "bval_file", "bvec_file", "local_bvec_file"]), + niu.IdentityInterface( + fields=[ + "dwi_file", + "bval_file", + "bvec_file", + "local_bvec_file", + "b_file", + "btable_file", + ] + ), name="outputnode", ) workflow = Workflow(name=name) @@ -97,7 +106,16 @@ def init_gradient_select_wf( niu.IdentityInterface(fields=recon_workflow_input_fields), name="inputnode" ) outputnode = pe.Node( - niu.IdentityInterface(fields=["dwi_file", "bval_file", "bvec_file", "local_bvec_file"]), + niu.IdentityInterface( + fields=[ + "dwi_file", + "bval_file", + "bvec_file", + "local_bvec_file", + "b_file", + "btable_file", + ] + ), name="outputnode", ) workflow = Workflow(name=name)