From 2f3a7ccb0243926c3e631eb48a8fb65dcaf69111 Mon Sep 17 00:00:00 2001 From: celprov Date: Wed, 17 Apr 2024 15:54:37 +0200 Subject: [PATCH 1/4] enh: implement an interface to generate a summary for DWI enh: save the summary in html form using DerivativesDataSink enh: modify models output of NumberOfShells so it returns 0 in case data is not DSI --- mriqc/interfaces/diffusion.py | 81 +++++++++++++++++++++++++++++-- mriqc/workflows/diffusion/base.py | 20 ++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/mriqc/interfaces/diffusion.py b/mriqc/interfaces/diffusion.py index c24c39cd7..1e8616dbb 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 @@ -65,6 +66,17 @@ 'WeightedStat', ) +DWI_TEMPLATE = """\ +\t +""" FD_THRESHOLD = 0.2 @@ -345,6 +357,69 @@ 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 = 'DSI' + else: + dwi_type = 'single-shell' if self.inputs.n_shells == 1 else 'multi-shell' + + # 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=self.inputs.in_file, + pedir=self.inputs.metadata.get('PhaseEncodingDirection'), + 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( @@ -398,7 +473,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, @@ -456,7 +531,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 11573409a..436e6d546 100644 --- a/mriqc/workflows/diffusion/base.py +++ b/mriqc/workflows/diffusion/base.py @@ -48,6 +48,7 @@ import numpy as np from nipype.interfaces import utility as niu from nipype.pipeline import engine as pe +from niworkflows.interfaces.bids import DerivativesDataSink from mriqc import config from mriqc.workflows.diffusion.output import init_dwi_report_wf @@ -78,6 +79,7 @@ def dmri_qc_workflow(name='dwiMRIQC'): CCSegmentation, CorrectSignalDrift, DiffusionModel, + DWISummary, ExtractOrientations, NumberOfShells, ReadDWIMetadata, @@ -144,6 +146,16 @@ def dmri_qc_workflow(name='dwiMRIQC'): name='load_bmat', ) shells = pe.Node(NumberOfShells(), name='shells') + summary = pe.Node(DWISummary(), name='shells') + 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', @@ -241,6 +253,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'), @@ -253,6 +267,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')]), From 098af89cf975ed9722fe8a6208d39168f3cb033f Mon Sep 17 00:00:00 2001 From: celprov Date: Thu, 18 Apr 2024 16:30:38 +0200 Subject: [PATCH 2/4] fix: summary node name fix: model type was mistakenly reversed fix: import DerivativesDataSink from mriqc.interfaces rather than niworkflows fix: bootstrap dwi entities to find summary.html fix: keep only basename for filename --- mriqc/data/bootstrap-dwi.yml | 2 ++ mriqc/interfaces/diffusion.py | 6 +++--- mriqc/workflows/diffusion/base.py | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/mriqc/data/bootstrap-dwi.yml b/mriqc/data/bootstrap-dwi.yml index 999cd11b8..9551afefc 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 1e8616dbb..1ff1ff833 100644 --- a/mriqc/interfaces/diffusion.py +++ b/mriqc/interfaces/diffusion.py @@ -399,9 +399,9 @@ class DWISummary(SummaryInterface): def _generate_segment(self): # Determine type of DWI data (multi-shell, single-shell or DSI) if self.inputs.models == [0]: - dwi_type = 'DSI' - else: 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( @@ -410,7 +410,7 @@ def _generate_segment(self): ) return DWI_TEMPLATE.format( - filename=self.inputs.in_file, + filename=os.path.basename(self.inputs.in_file), pedir=self.inputs.metadata.get('PhaseEncodingDirection'), n_b0=len(self.inputs.b_indices[0]), b0_indices=self.inputs.b_indices[0], diff --git a/mriqc/workflows/diffusion/base.py b/mriqc/workflows/diffusion/base.py index 436e6d546..61e790c9c 100644 --- a/mriqc/workflows/diffusion/base.py +++ b/mriqc/workflows/diffusion/base.py @@ -48,7 +48,7 @@ import numpy as np from nipype.interfaces import utility as niu from nipype.pipeline import engine as pe -from niworkflows.interfaces.bids import DerivativesDataSink +from mriqc.interfaces import DerivativesDataSink from mriqc import config from mriqc.workflows.diffusion.output import init_dwi_report_wf @@ -146,7 +146,7 @@ def dmri_qc_workflow(name='dwiMRIQC'): name='load_bmat', ) shells = pe.Node(NumberOfShells(), name='shells') - summary = pe.Node(DWISummary(), name='shells') + summary = pe.Node(DWISummary(), name='summary') ds_report_summary = pe.Node( DerivativesDataSink( base_directory=config.execution.output_dir, From 350ade97dd5a8612458a3377bd0965db4748b4ca Mon Sep 17 00:00:00 2001 From: celprov Date: Fri, 19 Apr 2024 17:22:49 +0200 Subject: [PATCH 3/4] enh: also report echotime and repetitiontime --- mriqc/interfaces/diffusion.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mriqc/interfaces/diffusion.py b/mriqc/interfaces/diffusion.py index 1ff1ff833..0f08dd9f1 100644 --- a/mriqc/interfaces/diffusion.py +++ b/mriqc/interfaces/diffusion.py @@ -70,6 +70,8 @@ \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}
  • @@ -412,6 +414,8 @@ def _generate_segment(self): 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, From de892cc215c6e0ca2291ce1d99551642c3962867 Mon Sep 17 00:00:00 2001 From: celprov Date: Tue, 23 Apr 2024 11:27:54 +0200 Subject: [PATCH 4/4] sty: ruff --- mriqc/interfaces/diffusion.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mriqc/interfaces/diffusion.py b/mriqc/interfaces/diffusion.py index 0f08dd9f1..cb16e57ab 100644 --- a/mriqc/interfaces/diffusion.py +++ b/mriqc/interfaces/diffusion.py @@ -414,13 +414,13 @@ def _generate_segment(self): 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'), + 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 + n_dwis=n_dwis, )