diff --git a/mriqc/data/bootstrap-dwi.yml b/mriqc/data/bootstrap-dwi.yml
index 999cd11b..9551afef 100644
--- a/mriqc/data/bootstrap-dwi.yml
+++ b/mriqc/data/bootstrap-dwi.yml
@@ -31,6 +31,8 @@ sections:
- name: Summary
reportlets:
- bids: {datatype: figures, desc: summary, extension: [.html]}
+ caption: This section provides a summary of the DWI properties.
+ subtitle: Textual summary
- bids: {datatype: figures, desc: heatmap}
caption: This visualization divides the data by shells, and shows the joint distribution
of SNR vs. FA. At the bottom, the distributions are marginalized for SNR.
diff --git a/mriqc/interfaces/diffusion.py b/mriqc/interfaces/diffusion.py
index 3e345129..9675abab 100644
--- a/mriqc/interfaces/diffusion.py
+++ b/mriqc/interfaces/diffusion.py
@@ -21,9 +21,10 @@
# https://www.nipreps.org/community/licensing/
#
"""Interfaces for manipulating DWI data."""
-
from __future__ import annotations
+import os
+
import nibabel as nb
import numpy as np
import scipy.ndimage as nd
@@ -67,6 +68,19 @@
'WeightedStat',
)
+DWI_TEMPLATE = """\
+\t
+\t\t- Filename: {filename}
+\t\t\t- Phase-encoding (PE) direction: {pedir}
+\t\t\t- Echo time (TE): {te}
+\t\t\t- Repetition time (TR): {tr}
+\t\t\t- Number of b=0: {n_b0}
+\t\t\t- Indices of the b=0 scans: {b0_indices}
+\t\t\t- Type of DWIs: {dwi_type}
+\t\t\t- Number of shells (after 'shell-ifying' if DSI): {n_shells}
+\t\t\t- Number of DWIs: {n_dwis}
+\t
+"""
FD_THRESHOLD = 0.2
@@ -349,6 +363,71 @@ def _run_interface(self, runtime):
return runtime
+class SummaryOutputSpec(_TraitedSpec):
+ out_report = File(exists=True, desc='HTML segment containing summary')
+
+
+class SummaryInterface(SimpleInterface):
+ output_spec = SummaryOutputSpec
+
+ def _run_interface(self, runtime):
+ segment = self._generate_segment()
+ fname = os.path.join(runtime.cwd, 'report.html')
+ with open(fname, 'w') as fobj:
+ fobj.write(segment)
+ self._results['out_report'] = fname
+ return runtime
+
+ def _generate_segment(self):
+ raise NotImplementedError
+
+
+class DWISummaryInputSpec(_BaseInterfaceInputSpec):
+ in_file = File(exists=True, mandatory=True, desc='the input DWI nifti file')
+ metadata = traits.Dict(desc='layout metadata')
+ n_shells = traits.Int(desc='number of shells')
+ models = traits.List(traits.Int, minlen=1, desc='number of shells ordered by model fit on DSI')
+ b_indices = traits.List(
+ traits.List(traits.Int, minlen=1),
+ minlen=1,
+ desc='list of ``n_shells`` b-value-wise indices lists',
+ )
+ b_values = traits.List(
+ traits.Float,
+ minlen=1,
+ desc='list of ``n_shells`` b-values associated with each shell (only nonzero)',
+ )
+
+
+class DWISummary(SummaryInterface):
+ input_spec = DWISummaryInputSpec
+
+ def _generate_segment(self):
+ # Determine type of DWI data (multi-shell, single-shell or DSI)
+ if self.inputs.models == [0]:
+ dwi_type = 'single-shell' if self.inputs.n_shells == 1 else 'multi-shell'
+ else:
+ dwi_type = 'DSI'
+
+ # Format string to display number of DWIs per shell
+ n_dwis = ', '.join(
+ f'{len(indices)} DWIs at b={bval:.0f}'
+ for bval, indices in zip(self.inputs.b_values, self.inputs.b_indices[1:])
+ )
+
+ return DWI_TEMPLATE.format(
+ filename=os.path.basename(self.inputs.in_file),
+ pedir=self.inputs.metadata.get('PhaseEncodingDirection'),
+ te=self.inputs.metadata.get('EchoTime'),
+ tr=self.inputs.metadata.get('RepetitionTime'),
+ n_b0=len(self.inputs.b_indices[0]),
+ b0_indices=self.inputs.b_indices[0],
+ dwi_type=dwi_type,
+ n_shells=self.inputs.n_shells,
+ n_dwis=n_dwis,
+ )
+
+
class _WeightedStatInputSpec(_BaseInterfaceInputSpec):
in_file = File(exists=True, mandatory=True, desc='an image')
in_weights = traits.List(
@@ -402,7 +481,7 @@ class _NumberOfShellsInputSpec(_BaseInterfaceInputSpec):
class _NumberOfShellsOutputSpec(_TraitedSpec):
- models = traits.List(traits.Int, minlen=1, desc='number of shells ordered by model fit')
+ models = traits.List(traits.Int, minlen=1, desc='number of shells ordered by model fit on DSI')
n_shells = traits.Int(desc='number of shells')
out_data = traits.List(
traits.Float,
@@ -460,7 +539,7 @@ def _run_interface(self, runtime):
if len(shell_bvals) <= self.inputs.dsi_threshold:
self._results['n_shells'] = len(shell_bvals)
- self._results['models'] = [self._results['n_shells']]
+ self._results['models'] = [0]
self._results['out_data'] = round_bvals.tolist()
self._results['b_values'] = shell_bvals
else:
diff --git a/mriqc/workflows/diffusion/base.py b/mriqc/workflows/diffusion/base.py
index 0053dc0d..d3f3b700 100644
--- a/mriqc/workflows/diffusion/base.py
+++ b/mriqc/workflows/diffusion/base.py
@@ -45,6 +45,7 @@
from nipype.interfaces import utility as niu
from nipype.pipeline import engine as pe
+from mriqc.interfaces import DerivativesDataSink
from mriqc import config
from mriqc.workflows.diffusion.output import init_dwi_report_wf
@@ -75,6 +76,7 @@ def dmri_qc_workflow(name='dwiMRIQC'):
CCSegmentation,
CorrectSignalDrift,
DiffusionModel,
+ DWISummary,
ExtractOrientations,
NumberOfShells,
ReadDWIMetadata,
@@ -128,6 +130,16 @@ def dmri_qc_workflow(name='dwiMRIQC'):
name='load_bmat',
)
shells = pe.Node(NumberOfShells(), name='shells')
+ summary = pe.Node(DWISummary(), name='summary')
+ ds_report_summary = pe.Node(
+ DerivativesDataSink(
+ base_directory=config.execution.output_dir,
+ desc='summary',
+ datatype='figures',
+ ),
+ name='ds_report_summary',
+ run_without_submitting=True,
+ )
get_lowb = pe.Node(
ExtractOrientations(),
name='get_lowb',
@@ -225,6 +237,8 @@ def dmri_qc_workflow(name='dwiMRIQC'):
# fmt: off
workflow.connect([
+ (inputnode, summary, [('in_file', 'in_file')]),
+ (inputnode, ds_report_summary, [('in_file', 'source_file')]),
(inputnode, load_bmat, [('in_file', 'in_file')]),
(inputnode, dwi_report_wf, [
('in_file', 'inputnode.name_source'),
@@ -241,6 +255,12 @@ def dmri_qc_workflow(name='dwiMRIQC'):
(shells, dwi_ref, [(('b_masks', _first), 't_mask')]),
(shells, sp_mask, [('b_masks', 'b_masks')]),
(load_bmat, shells, [('out_bval_file', 'in_bvals')]),
+ (load_bmat, summary, [('out_dict', 'metadata')]),
+ (shells, summary, [('n_shells', 'n_shells'),
+ ('models', 'models'),
+ ('b_indices', 'b_indices'),
+ ('b_values', 'b_values'),]),
+ (summary, ds_report_summary, [('out_report', 'in_file')]),
(sanitize, drift, [('out_file', 'full_epi')]),
(shells, get_lowb, [(('b_indices', _first), 'indices')]),
(sanitize, get_lowb, [('out_file', 'in_file')]),