From a1b604f7abf9bb7dcd8189d7d79a24c3d7c08510 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Wed, 8 Mar 2023 17:50:21 +0100 Subject: [PATCH] enh(interfaces): nipype interfaces API refactor Resolves: #46. --- nireports/interfaces/__init__.py | 35 ++++++ nireports/interfaces/base.py | 49 ++++++++ nireports/interfaces/fmri.py | 112 ++++++++++++++++++ nireports/interfaces/{viz.py => mosaic.py} | 43 +++---- .../interfaces/{plotting.py => nuisance.py} | 84 +------------ 5 files changed, 212 insertions(+), 111 deletions(-) create mode 100644 nireports/interfaces/base.py create mode 100644 nireports/interfaces/fmri.py rename nireports/interfaces/{viz.py => mosaic.py} (82%) rename nireports/interfaces/{plotting.py => nuisance.py} (62%) diff --git a/nireports/interfaces/__init__.py b/nireports/interfaces/__init__.py index e69de29b..24bdf11d 100644 --- a/nireports/interfaces/__init__.py +++ b/nireports/interfaces/__init__.py @@ -0,0 +1,35 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +# +# Copyright 2023 The NiPreps Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +"""NiPype interfaces to generate reportlets.""" +from nireports.interfaces.fmri import FMRISummary +from nireports.interfaces.nuisance import CompCorVariancePlot, ConfoundsCorrelationPlot +from nireports.interfaces.mosaic import PlotContours, PlotMosaic, PlotSpikes + +__all__ = ( + "CompCorVariancePlot", + "ConfoundsCorrelationPlot", + "FMRISummary", + "PlotContours", + "PlotMosaic", + "PlotSpikes", +) diff --git a/nireports/interfaces/base.py b/nireports/interfaces/base.py new file mode 100644 index 00000000..6c6191d1 --- /dev/null +++ b/nireports/interfaces/base.py @@ -0,0 +1,49 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +# +# Copyright 2023 The NiPreps Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +# STATEMENT OF CHANGES: This file was ported carrying over full git history from MRIQC, +# another NiPreps project licensed under the Apache-2.0 terms, and has been changed since. +# The original file this work derives from is found at: +# https://github.com/nipreps/mriqc/blob/1ffd4c8d1a20b44ebfea648a7b12bb32a425d4ec/ +# mriqc/interfaces/viz.py +"""NiPype interface -- basic tooling.""" +from nipype.interfaces.base import ( + BaseInterfaceInputSpec, + File, + traits, +) + + +class _PlotBaseInputSpec(BaseInterfaceInputSpec): + in_file = File(exists=True, mandatory=True, desc="File to be plotted") + title = traits.Str(desc="a title string for the plot") + annotate = traits.Bool(True, usedefault=True, desc="annotate left/right") + figsize = traits.Tuple( + (11.69, 8.27), + traits.Float, + traits.Float, + usedefault=True, + desc="Figure size", + ) + dpi = traits.Int(300, usedefault=True, desc="Desired DPI of figure") + out_file = File("mosaic.svg", usedefault=True, desc="output file name") + cmap = traits.Str("Greys_r", usedefault=True) diff --git a/nireports/interfaces/fmri.py b/nireports/interfaces/fmri.py new file mode 100644 index 00000000..4d752e39 --- /dev/null +++ b/nireports/interfaces/fmri.py @@ -0,0 +1,112 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +# +# Copyright 2023 The NiPreps Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +"""Functional MRI -specific visualization.""" +import numpy as np +import nibabel as nb + +from nipype.utils.filemanip import fname_presuffix +from nipype.interfaces.base import ( + File, + BaseInterfaceInputSpec, + TraitedSpec, + SimpleInterface, + traits, + isdefined, +) +from nireports.tools.timeseries import cifti_timeseries, get_tr, nifti_timeseries +from nireports.reportlets.modality.func import fMRIPlot + + +class _FMRISummaryInputSpec(BaseInterfaceInputSpec): + in_func = File(exists=True, mandatory=True, desc="") + in_spikes_bg = File(exists=True, desc="") + fd = File(exists=True, desc="") + dvars = File(exists=True, desc="") + outliers = File(exists=True, desc="") + in_segm = File(exists=True, desc="") + tr = traits.Either(None, traits.Float, usedefault=True, desc="the TR") + fd_thres = traits.Float(0.2, usedefault=True, desc="") + drop_trs = traits.Int(0, usedefault=True, desc="dummy scans") + + +class _FMRISummaryOutputSpec(TraitedSpec): + out_file = File(exists=True, desc="written file path") + + +class FMRISummary(SimpleInterface): + """Prepare an fMRI summary plot for the report.""" + + input_spec = _FMRISummaryInputSpec + output_spec = _FMRISummaryOutputSpec + + def _run_interface(self, runtime): + import pandas as pd + + self._results["out_file"] = fname_presuffix( + self.inputs.in_func, + suffix="_fmriplot.svg", + use_ext=False, + newpath=runtime.cwd, + ) + + dataframe = pd.DataFrame({ + "outliers": np.loadtxt(self.inputs.outliers, usecols=[0]).tolist(), + # Pick non-standardize dvars (col 1) + # First timepoint is NaN (difference) + "DVARS": [np.nan] + + np.loadtxt(self.inputs.dvars, skiprows=1, usecols=[1]).tolist(), + # First timepoint is zero (reference volume) + "FD": [0.0] + + np.loadtxt(self.inputs.fd, skiprows=1, usecols=[0]).tolist(), + }) if ( + isdefined(self.inputs.outliers) + and isdefined(self.inputs.dvars) + and isdefined(self.inputs.fd) + ) else None + + input_data = nb.load(self.inputs.in_func) + seg_file = self.inputs.in_segm if isdefined(self.inputs.in_segm) else None + dataset, segments = ( + cifti_timeseries(input_data) + if isinstance(input_data, nb.Cifti2Image) else + nifti_timeseries(input_data, seg_file) + ) + + fig = fMRIPlot( + dataset, + segments=segments, + spikes_files=( + [self.inputs.in_spikes_bg] + if isdefined(self.inputs.in_spikes_bg) else None + ), + tr=( + self.inputs.tr if isdefined(self.inputs.tr) else + get_tr(input_data) + ), + confounds=dataframe, + units={"outliers": "%", "FD": "mm"}, + vlines={"FD": [self.inputs.fd_thres]}, + nskip=self.inputs.drop_trs, + ).plot() + fig.savefig(self._results["out_file"], bbox_inches="tight") + return runtime diff --git a/nireports/interfaces/viz.py b/nireports/interfaces/mosaic.py similarity index 82% rename from nireports/interfaces/viz.py rename to nireports/interfaces/mosaic.py index d42637c2..4dbae397 100644 --- a/nireports/interfaces/viz.py +++ b/nireports/interfaces/mosaic.py @@ -25,7 +25,7 @@ # The original file this work derives from is found at: # https://github.com/nipreps/mriqc/blob/1ffd4c8d1a20b44ebfea648a7b12bb32a425d4ec/ # mriqc/interfaces/viz.py -"""Visualization interfaces.""" +"""Visualization of n-D images with mosaics cutting through planes.""" from pathlib import Path import numpy as np @@ -38,10 +38,11 @@ traits, ) +from nireports.interfaces.base import _PlotBaseInputSpec from nireports.reportlets.mosaic import plot_mosaic, plot_segmentation, plot_spikes -class PlotContoursInputSpec(BaseInterfaceInputSpec): +class _PlotContoursInputSpec(BaseInterfaceInputSpec): in_file = File(exists=True, mandatory=True, desc="File to be plotted") in_contours = File(exists=True, mandatory=True, desc="file to pick the contours from") cut_coords = traits.Int(8, usedefault=True, desc="number of slices") @@ -69,15 +70,15 @@ class PlotContoursInputSpec(BaseInterfaceInputSpec): vmax = traits.Float(desc="maximum intensity") -class PlotContoursOutputSpec(TraitedSpec): +class _PlotContoursOutputSpec(TraitedSpec): out_file = File(exists=True, desc="output svg file") class PlotContours(SimpleInterface): """Plot contours""" - input_spec = PlotContoursInputSpec - output_spec = PlotContoursOutputSpec + input_spec = _PlotContoursInputSpec + output_spec = _PlotContoursOutputSpec def _run_interface(self, runtime): in_file_ref = Path(self.inputs.in_file) @@ -108,28 +109,12 @@ def _run_interface(self, runtime): return runtime -class PlotBaseInputSpec(BaseInterfaceInputSpec): - in_file = File(exists=True, mandatory=True, desc="File to be plotted") - title = traits.Str(desc="a title string for the plot") - annotate = traits.Bool(True, usedefault=True, desc="annotate left/right") - figsize = traits.Tuple( - (11.69, 8.27), - traits.Float, - traits.Float, - usedefault=True, - desc="Figure size", - ) - dpi = traits.Int(300, usedefault=True, desc="Desired DPI of figure") - out_file = File("mosaic.svg", usedefault=True, desc="output file name") - cmap = traits.Str("Greys_r", usedefault=True) - - -class PlotMosaicInputSpec(PlotBaseInputSpec): +class _PlotMosaicInputSpec(_PlotBaseInputSpec): bbox_mask_file = File(exists=True, desc="brain mask") only_noise = traits.Bool(False, desc="plot only noise") -class PlotMosaicOutputSpec(TraitedSpec): +class _PlotMosaicOutputSpec(TraitedSpec): out_file = File(exists=True, desc="output pdf file") @@ -139,8 +124,8 @@ class PlotMosaic(SimpleInterface): Plots slices of a 3D volume into a pdf file """ - input_spec = PlotMosaicInputSpec - output_spec = PlotMosaicOutputSpec + input_spec = _PlotMosaicInputSpec + output_spec = _PlotMosaicOutputSpec def _run_interface(self, runtime): mask = None @@ -164,20 +149,20 @@ def _run_interface(self, runtime): return runtime -class PlotSpikesInputSpec(PlotBaseInputSpec): +class _PlotSpikesInputSpec(_PlotBaseInputSpec): in_spikes = File(exists=True, mandatory=True, desc="tsv file of spikes") in_fft = File(exists=True, mandatory=True, desc="nifti file with the 4D FFT") -class PlotSpikesOutputSpec(TraitedSpec): +class _PlotSpikesOutputSpec(TraitedSpec): out_file = File(exists=True, desc="output svg file") class PlotSpikes(SimpleInterface): """Plot slices of a dataset with spikes.""" - input_spec = PlotSpikesInputSpec - output_spec = PlotSpikesOutputSpec + input_spec = _PlotSpikesInputSpec + output_spec = _PlotSpikesOutputSpec def _run_interface(self, runtime): out_file = str((Path(runtime.cwd) / self.inputs.out_file).resolve()) diff --git a/nireports/interfaces/plotting.py b/nireports/interfaces/nuisance.py similarity index 62% rename from nireports/interfaces/plotting.py rename to nireports/interfaces/nuisance.py index 5f1fef56..8dcde064 100644 --- a/nireports/interfaces/plotting.py +++ b/nireports/interfaces/nuisance.py @@ -1,7 +1,7 @@ # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- # vi: set ft=python sts=4 ts=4 sw=4 et: # -# Copyright 2021 The NiPreps Developers +# Copyright 2023 The NiPreps Developers # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,10 +20,7 @@ # # https://www.nipreps.org/community/licensing/ # -"""Visualization tools.""" -import numpy as np -import nibabel as nb - +"""Screening nuisance signals.""" from nipype.utils.filemanip import fname_presuffix from nipype.interfaces.base import ( File, @@ -33,85 +30,8 @@ traits, isdefined, ) -from nireports.tools.timeseries import cifti_timeseries, get_tr, nifti_timeseries from nireports.reportlets.nuisance import confounds_correlation_plot from nireports.reportlets.xca import compcor_variance_plot -from nireports.reportlets.modality.func import fMRIPlot - - -class _FMRISummaryInputSpec(BaseInterfaceInputSpec): - in_func = File(exists=True, mandatory=True, desc="") - in_spikes_bg = File(exists=True, desc="") - fd = File(exists=True, desc="") - dvars = File(exists=True, desc="") - outliers = File(exists=True, desc="") - in_segm = File(exists=True, desc="") - tr = traits.Either(None, traits.Float, usedefault=True, desc="the TR") - fd_thres = traits.Float(0.2, usedefault=True, desc="") - drop_trs = traits.Int(0, usedefault=True, desc="dummy scans") - - -class _FMRISummaryOutputSpec(TraitedSpec): - out_file = File(exists=True, desc="written file path") - - -class FMRISummary(SimpleInterface): - """Prepare an fMRI summary plot for the report.""" - - input_spec = _FMRISummaryInputSpec - output_spec = _FMRISummaryOutputSpec - - def _run_interface(self, runtime): - import pandas as pd - - self._results["out_file"] = fname_presuffix( - self.inputs.in_func, - suffix="_fmriplot.svg", - use_ext=False, - newpath=runtime.cwd, - ) - - dataframe = pd.DataFrame({ - "outliers": np.loadtxt(self.inputs.outliers, usecols=[0]).tolist(), - # Pick non-standardize dvars (col 1) - # First timepoint is NaN (difference) - "DVARS": [np.nan] - + np.loadtxt(self.inputs.dvars, skiprows=1, usecols=[1]).tolist(), - # First timepoint is zero (reference volume) - "FD": [0.0] - + np.loadtxt(self.inputs.fd, skiprows=1, usecols=[0]).tolist(), - }) if ( - isdefined(self.inputs.outliers) - and isdefined(self.inputs.dvars) - and isdefined(self.inputs.fd) - ) else None - - input_data = nb.load(self.inputs.in_func) - seg_file = self.inputs.in_segm if isdefined(self.inputs.in_segm) else None - dataset, segments = ( - cifti_timeseries(input_data) - if isinstance(input_data, nb.Cifti2Image) else - nifti_timeseries(input_data, seg_file) - ) - - fig = fMRIPlot( - dataset, - segments=segments, - spikes_files=( - [self.inputs.in_spikes_bg] - if isdefined(self.inputs.in_spikes_bg) else None - ), - tr=( - self.inputs.tr if isdefined(self.inputs.tr) else - get_tr(input_data) - ), - confounds=dataframe, - units={"outliers": "%", "FD": "mm"}, - vlines={"FD": [self.inputs.fd_thres]}, - nskip=self.inputs.drop_trs, - ).plot() - fig.savefig(self._results["out_file"], bbox_inches="tight") - return runtime class _CompCorVariancePlotInputSpec(BaseInterfaceInputSpec):