Skip to content

Commit

Permalink
Improve executive summary carpet plots (#747)
Browse files Browse the repository at this point in the history
  • Loading branch information
tsalo authored Feb 12, 2023
1 parent da108d3 commit d4c03f4
Show file tree
Hide file tree
Showing 22 changed files with 390 additions and 498 deletions.
3 changes: 2 additions & 1 deletion .circleci/NiftiWithoutFreeSurferTest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ XCPD_CMD="$BASE_XCPD_CMD \
-vv \
--nuisance-regressors 27P \
--disable-bandpass-filter \
--dummy-scans 1"
--dummy-scans 1 \
--dcan_qc"

echo $XCPD_CMD

Expand Down
4 changes: 2 additions & 2 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ xcp_d-combineqc
xcp_d.interfaces.bids
xcp_d.interfaces.c3
xcp_d.interfaces.connectivity
xcp_d.interfaces.executive_summary
xcp_d.interfaces.filtering
xcp_d.interfaces.layout_builder
xcp_d.interfaces.nilearn
xcp_d.interfaces.prepostcleaning
xcp_d.interfaces.qc_plot
Expand Down Expand Up @@ -89,7 +89,7 @@ xcp_d-combineqc
xcp_d.utils.fcon
xcp_d.utils.filemanip
xcp_d.utils.modified_data
xcp_d.utils.plot
xcp_d.utils.plotting
xcp_d.utils.qcmetrics
xcp_d.utils.sentry
xcp_d.utils.utils
Expand Down
1 change: 0 additions & 1 deletion xcp_d/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,6 @@ def main(args=None):
subjects=subject_list,
fmri_dir=str(fmri_dir),
output_dir=str(Path(str(output_dir)) / "xcp_d/"),
work_dir=work_dir,
cifti=opts.cifti,
dcan_qc=opts.dcan_qc,
dummy_scans=opts.dummy_scans,
Expand Down
10 changes: 4 additions & 6 deletions xcp_d/interfaces/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@
bids,
c3,
connectivity,
executive_summary,
filtering,
layout_builder,
nilearn,
plotting,
prepostcleaning,
qc_plot,
regression,
report,
report_core,
resting_state,
surfplotting,
workbench,
)

Expand All @@ -25,14 +24,13 @@
"c3",
"connectivity",
"filtering",
"layout_builder",
"executive_summary",
"nilearn",
"prepostcleaning",
"qc_plot",
"plotting",
"regression",
"report_core",
"report",
"resting_state",
"surfplotting",
"workbench",
]
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ def collect_inputs(self):

for task_entity_set in task_entity_sets:
task_file_figures = task_entity_set.copy()
task_file_figures[
"key"
] = f"task-{task_entity_set['task']}_run-{task_entity_set.get('run', 0)}"

query = {
"subject": self.subject_id,
Expand Down Expand Up @@ -208,6 +211,9 @@ def collect_inputs(self):

task_files.append(task_file_figures)

# Sort the files by the desired key
task_files = sorted(task_files, key=lambda d: d["key"])

self.task_files_ = task_files

def generate_report(self, out_file=None):
Expand Down
141 changes: 125 additions & 16 deletions xcp_d/interfaces/qc_plot.py → xcp_d/interfaces/plotting.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
# vi: set ft=python sts=4 ts=4 sw=4 et:
"""Quality control plotting interfaces."""
"""Plotting interfaces."""
import os

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from nilearn.plotting import plot_anat
from nipype import logging
from nipype.interfaces.base import (
BaseInterfaceInputSpec,
Expand All @@ -20,7 +21,7 @@
from xcp_d.utils.confounds import load_confound, load_motion
from xcp_d.utils.filemanip import fname_presuffix
from xcp_d.utils.modified_data import compute_fd
from xcp_d.utils.plot import FMRIPlot
from xcp_d.utils.plotting import FMRIPlot, plot_fmri_es
from xcp_d.utils.qcmetrics import compute_dvars, compute_registration_qc
from xcp_d.utils.write_save import read_ndata, write_ndata

Expand Down Expand Up @@ -183,7 +184,7 @@ def _run_interface(self, runtime):
return runtime


class _QCPlotInputSpec(BaseInterfaceInputSpec):
class _QCPlotsInputSpec(BaseInterfaceInputSpec):
bold_file = File(exists=True, mandatory=True, desc="Raw bold file from fMRIPrep")
dummy_scans = traits.Int(mandatory=True, desc="Dummy time to drop")
tmask = File(exists=True, mandatory=True, desc="Temporal mask")
Expand All @@ -205,14 +206,14 @@ class _QCPlotInputSpec(BaseInterfaceInputSpec):
bold2temp_mask = File(exists=True, mandatory=False, desc="Bold mask in T1W")


class _QCPlotOutputSpec(TraitedSpec):
class _QCPlotsOutputSpec(TraitedSpec):
qc_file = File(exists=True, mandatory=True, desc="qc file in tsv")
raw_qcplot = File(exists=True, mandatory=True, desc="qc plot before regression")
clean_qcplot = File(exists=True, mandatory=True, desc="qc plot after regression")


class QCPlot(SimpleInterface):
"""Generate a quality control (QC) figure.
class QCPlots(SimpleInterface):
"""Generate pre- and post-processing quality control (QC) figures.
Examples
--------
Expand All @@ -221,20 +222,20 @@ class QCPlot(SimpleInterface):
>>> tmpdir = TemporaryDirectory()
>>> os.chdir(tmpdir.name)
.. doctest::
computeqcwf = QCPlot()
computeqcwf.inputs.cleaned_file = datafile
computeqcwf.inputs.bold_file = rawbold
computeqcwf.inputs.TR = TR
computeqcwf.inputs.tmask = temporalmask
computeqcwf.inputs.mask_file = mask
computeqcwf.inputs.dummy_scans = dummy_scans
computeqcwf.run()
qcplots = QCPlots()
qcplots.inputs.cleaned_file = datafile
qcplots.inputs.bold_file = rawbold
qcplots.inputs.TR = TR
qcplots.inputs.tmask = temporalmask
qcplots.inputs.mask_file = mask
qcplots.inputs.dummy_scans = dummy_scans
qcplots.run()
.. testcleanup::
>>> tmpdir.cleanup()
"""

input_spec = _QCPlotInputSpec
output_spec = _QCPlotOutputSpec
input_spec = _QCPlotsInputSpec
output_spec = _QCPlotsOutputSpec

def _run_interface(self, runtime):
# Load confound matrix and load motion with motion filtering
Expand Down Expand Up @@ -445,3 +446,111 @@ def _run_interface(self, runtime):
df.to_csv(self._results["qc_file"], index=False, header=True)

return runtime


class _QCPlotsESInputSpec(BaseInterfaceInputSpec):
rawdata = File(exists=True, mandatory=True, desc="Raw data")
regressed_data = File(
exists=True,
mandatory=True,
desc="Data after regression and interpolation, but not filtering.",
)
residual_data = File(exists=True, mandatory=True, desc="Data after filtering")
filtered_motion = File(
exists=True,
mandatory=True,
desc="TSV file with filtered motion parameters.",
)
TR = traits.Float(default_value=1, desc="Repetition time")

# Optional inputs
mask = File(exists=True, mandatory=False, desc="Bold mask")
seg_data = File(exists=True, mandatory=False, desc="Segmentation file")
dummy_scans = traits.Int(
0,
usedefault=True,
desc="Number of dummy volumes to drop from the beginning of the run.",
)


class _QCPlotsESOutputSpec(TraitedSpec):
before_process = File(exists=True, mandatory=True, desc=".SVG file before processing")
after_process = File(exists=True, mandatory=True, desc=".SVG file after processing")


class QCPlotsES(SimpleInterface):
"""Plot fd, dvars, and carpet plots of the bold data before and after regression/filtering.
This is essentially equivalent to the QCPlots
(which are paired pre- and post-processing FMRIPlots), but adapted for the executive summary.
It takes in the data that's regressed, the data that's filtered and regressed,
as well as the segmentation files, TR, FD, bold_mask and unprocessed data.
It outputs the .SVG files before after processing has taken place.
"""

input_spec = _QCPlotsESInputSpec
output_spec = _QCPlotsESOutputSpec

def _run_interface(self, runtime):
before_process_fn = fname_presuffix(
"carpetplot_before_",
suffix="file.svg",
newpath=runtime.cwd,
use_ext=False,
)

after_process_fn = fname_presuffix(
"carpetplot_after_",
suffix="file.svg",
newpath=runtime.cwd,
use_ext=False,
)

mask_file = self.inputs.mask
mask_file = mask_file if isdefined(mask_file) else None

segmentation_file = self.inputs.seg_data
segmentation_file = segmentation_file if isdefined(segmentation_file) else None

self._results["before_process"], self._results["after_process"] = plot_fmri_es(
preprocessed_file=self.inputs.rawdata,
residuals_file=self.inputs.regressed_data,
denoised_file=self.inputs.residual_data,
dummy_scans=self.inputs.dummy_scans,
TR=self.inputs.TR,
mask=mask_file,
filtered_motion=self.inputs.filtered_motion,
seg_data=segmentation_file,
processed_filename=after_process_fn,
unprocessed_filename=before_process_fn,
)

return runtime


class _AnatomicalPlotInputSpec(BaseInterfaceInputSpec):
in_file = File(exists=True, mandatory=True, desc="plot image")


class _AnatomicalPlotOutputSpec(TraitedSpec):
out_file = File(exists=True, desc="out image")


class AnatomicalPlot(SimpleInterface):
"""Python class to plot x,y, and z of image data."""

input_spec = _AnatomicalPlotInputSpec
output_spec = _AnatomicalPlotOutputSpec

def _run_interface(self, runtime):
self._results["out_file"] = fname_presuffix(
self.inputs.in_file, suffix="_file.svg", newpath=runtime.cwd, use_ext=False
)

fig = plt.figure(constrained_layout=False, figsize=(25, 10))
plot_anat(self.inputs.in_file, draw_cross=False, figure=fig)
fig.savefig(self._results["out_file"], bbox_inches="tight", pad_inches=None)

return runtime
2 changes: 1 addition & 1 deletion xcp_d/interfaces/regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def _run_interface(self, runtime):
standardize=False,
sample_mask=None,
confounds=confound,
standardize_confounds=False, # do we want to set this to True?
standardize_confounds=True, # do we want to set this to True?
filter=None,
low_pass=None,
high_pass=None,
Expand Down
2 changes: 1 addition & 1 deletion xcp_d/interfaces/report_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from niworkflows.reports.core import Report as _Report

from xcp_d.interfaces.layout_builder import ExecutiveSummary
from xcp_d.interfaces.executive_summary import ExecutiveSummary
from xcp_d.utils.bids import get_entity
from xcp_d.utils.doc import fill_doc

Expand Down
Loading

0 comments on commit d4c03f4

Please sign in to comment.