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 +""" 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')]),