From 67be955478786dd475de75192debd5602da25665 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Wed, 11 Oct 2023 14:05:28 -0400 Subject: [PATCH] Update the QC metrics (#958) --- xcp_d/interfaces/plotting.py | 71 ++++++++++++++++++++++++------------ xcp_d/interfaces/report.py | 36 ++++++++++-------- xcp_d/utils/qcmetrics.py | 24 ++++++------ 3 files changed, 80 insertions(+), 51 deletions(-) diff --git a/xcp_d/interfaces/plotting.py b/xcp_d/interfaces/plotting.py index e0bb072d3..0d1d39322 100644 --- a/xcp_d/interfaces/plotting.py +++ b/xcp_d/interfaces/plotting.py @@ -277,7 +277,7 @@ class QCPlots(SimpleInterface): output_spec = _QCPlotsOutputSpec def _run_interface(self, runtime): - # Load confound matrix and load motion with motion filtering + # Load confound matrix and load motion without motion filtering confounds_df = pd.read_table(self.inputs.fmriprep_confounds_file) preproc_motion_df = load_motion( confounds_df.copy(), @@ -297,6 +297,7 @@ def _run_interface(self, runtime): censoring_df = pd.read_table(self.inputs.temporal_mask) tmask_arr = censoring_df["framewise_displacement"].values num_censored_volumes = int(tmask_arr.sum()) + num_retained_volumes = int((tmask_arr == 0).sum()) # Apply temporal mask to interpolated/full data rmsd_censored = rmsd[tmask_arr == 0] @@ -382,40 +383,57 @@ def _run_interface(self, runtime): # Calculate QC measures mean_fd = np.mean(preproc_fd_timeseries) - mean_rms = np.nanmean(rmsd_censored) # first value can be NaN if no dummy scans + mean_fd_post_censoring = np.mean(postproc_fd_timeseries) + mean_relative_rms = np.nanmean(rmsd_censored) # first value can be NaN if no dummy scans mean_dvars_before_processing = np.mean(dvars_before_processing) mean_dvars_after_processing = np.mean(dvars_after_processing) - motionDVCorrInit = np.corrcoef(preproc_fd_timeseries, dvars_before_processing)[0][1] - motionDVCorrFinal = np.corrcoef(postproc_fd_timeseries, dvars_after_processing)[0][1] + fd_dvars_correlation_initial = np.corrcoef(preproc_fd_timeseries, dvars_before_processing)[ + 0, 1 + ] + fd_dvars_correlation_final = np.corrcoef(postproc_fd_timeseries, dvars_after_processing)[ + 0, 1 + ] rmsd_max_value = np.nanmax(rmsd_censored) # A summary of all the values qc_values_dict.update( { - "meanFD": [mean_fd], - "relMeansRMSMotion": [mean_rms], - "relMaxRMSMotion": [rmsd_max_value], - "meanDVInit": [mean_dvars_before_processing], - "meanDVFinal": [mean_dvars_after_processing], + "mean_fd": [mean_fd], + "mean_fd_post_censoring": [mean_fd_post_censoring], + "mean_relative_rms": [mean_relative_rms], + "max_relative_rms": [rmsd_max_value], + "mean_dvars_initial": [mean_dvars_before_processing], + "mean_dvars_final": [mean_dvars_after_processing], + "num_dummy_volumes": [dummy_scans], "num_censored_volumes": [num_censored_volumes], - "nVolsRemoved": [dummy_scans], - "motionDVCorrInit": [motionDVCorrInit], - "motionDVCorrFinal": [motionDVCorrFinal], + "num_retained_volumes": [num_retained_volumes], + "fd_dvars_correlation_initial": [fd_dvars_correlation_initial], + "fd_dvars_correlation_final": [fd_dvars_correlation_final], } ) qc_metadata = { - "meanFD": { + "mean_fd": { "LongName": "Mean Framewise Displacement", "Description": ( "Average framewise displacement without any motion parameter filtering. " "This value includes high-motion outliers, but not dummy volumes. " "FD is calculated according to the Power definition." ), - "Units": "mm", + "Units": "mm / volume", + "Term URL": "https://doi.org/10.1016/j.neuroimage.2011.10.018", + }, + "mean_fd_post_censoring": { + "LongName": "Mean Framewise Displacement After Censoring", + "Description": ( + "Average framewise displacement without any motion parameter filtering. " + "This value does not include high-motion outliers or dummy volumes. " + "FD is calculated according to the Power definition." + ), + "Units": "mm / volume", "Term URL": "https://doi.org/10.1016/j.neuroimage.2011.10.018", }, - "relMeansRMSMotion": { + "mean_relative_rms": { "LongName": "Mean Relative Root Mean Squared", "Description": ( "Average relative root mean squared calculated from motion parameters, " @@ -424,7 +442,7 @@ def _run_interface(self, runtime): ), "Units": "arbitrary", }, - "relMaxRMSMotion": { + "max_relative_rms": { "LongName": "Maximum Relative Root Mean Squared", "Description": ( "Maximum relative root mean squared calculated from motion parameters, " @@ -433,7 +451,7 @@ def _run_interface(self, runtime): ), "Units": "arbitrary", }, - "meanDVInit": { + "mean_dvars_initial": { "LongName": "Mean DVARS Before Postprocessing", "Description": ( "Average DVARS (temporal derivative of root mean squared variance over " @@ -441,7 +459,7 @@ def _run_interface(self, runtime): ), "TermURL": "https://doi.org/10.1016/j.neuroimage.2011.02.073", }, - "meanDVFinal": { + "mean_dvars_final": { "LongName": "Mean DVARS After Postprocessing", "Description": ( "Average DVARS (temporal derivative of root mean squared variance over " @@ -449,6 +467,12 @@ def _run_interface(self, runtime): ), "TermURL": "https://doi.org/10.1016/j.neuroimage.2011.02.073", }, + "num_dummy_volumes": { + "LongName": "Number of Dummy Volumes", + "Description": ( + "The number of non-steady state volumes removed from the time series by XCP-D." + ), + }, "num_censored_volumes": { "LongName": "Number of Censored Volumes", "Description": ( @@ -456,13 +480,14 @@ def _run_interface(self, runtime): "This does not include dummy volumes." ), }, - "nVolsRemoved": { - "LongName": "Number of Dummy Volumes", + "num_retained_volumes": { + "LongName": "Number of Retained Volumes", "Description": ( - "The number of non-steady state volumes removed from the time series by XCP-D." + "The number of volumes retained in the denoised dataset. " + "This does not include dummy volumes or high-motion outliers." ), }, - "motionDVCorrInit": { + "fd_dvars_correlation_initial": { "LongName": "FD-DVARS Correlation Before Postprocessing", "Description": ( "The Pearson correlation coefficient between framewise displacement and DVARS " @@ -470,7 +495,7 @@ def _run_interface(self, runtime): "after removal of dummy volumes, but before removal of high-motion outliers." ), }, - "motionDVCorrFinal": { + "fd_dvars_correlation_final": { "LongName": "FD-DVARS Correlation After Postprocessing", "Description": ( "The Pearson correlation coefficient between framewise displacement and DVARS " diff --git a/xcp_d/interfaces/report.py b/xcp_d/interfaces/report.py index 54ed64bf4..e960cac8f 100644 --- a/xcp_d/interfaces/report.py +++ b/xcp_d/interfaces/report.py @@ -30,12 +30,13 @@ \t\t """ @@ -145,25 +146,28 @@ class FunctionalSummary(SummaryInterface): def _generate_segment(self): space = get_entity(self.inputs.bold_file, "space") qcfile = pd.read_csv(self.inputs.qc_file) - meanFD = str(round(qcfile["meanFD"][0], 4)) - meanRMS = str(round(qcfile["relMeansRMSMotion"][0], 4)) - maxRMS = str(round(qcfile["relMaxRMSMotion"][0], 4)) - dvars = f"{round(qcfile['meanDVInit'][0], 4)}, {round(qcfile['meanDVFinal'][0], 4)}" + mean_fd = str(round(qcfile["mean_fd"][0], 4)) + mean_relative_rms = str(round(qcfile["mean_relative_rms"][0], 4)) + max_relative_rms = str(round(qcfile["max_relative_rms"][0], 4)) + dvars = ( + f"{round(qcfile['mean_dvars_initial'][0], 4)}, " + f"{round(qcfile['mean_dvars_final'][0], 4)}" + ) fd_dvars_correlation = ( - f"{round(qcfile['motionDVCorrInit'][0], 4)}, " - f"{round(qcfile['motionDVCorrFinal'][0], 4)}" + f"{round(qcfile['fd_dvars_correlation_initial'][0], 4)}, " + f"{round(qcfile['fd_dvars_correlation_final'][0], 4)}" ) num_vols_censored = str(round(qcfile["num_censored_volumes"][0], 4)) return QC_TEMPLATE.format( space=space, TR=self.inputs.TR, - meanFD=meanFD, - meanRMS=meanRMS, - maxRMS=maxRMS, + mean_fd=mean_fd, + mean_relative_rms=mean_relative_rms, + max_relative_rms=max_relative_rms, dvars_before_after=dvars, - corrfddv=fd_dvars_correlation, - volcensored=num_vols_censored, + fd_dvars_correlation=fd_dvars_correlation, + num_vols_censored=num_vols_censored, ) diff --git a/xcp_d/utils/qcmetrics.py b/xcp_d/utils/qcmetrics.py index 842996ff0..9619b8b1a 100644 --- a/xcp_d/utils/qcmetrics.py +++ b/xcp_d/utils/qcmetrics.py @@ -46,15 +46,15 @@ def compute_registration_qc(bold2t1w_mask, anat_brainmask, bold2template_mask, t template_mask_arr = nb.load(template_mask).get_fdata() reg_qc = { - "coregDice": [dice(bold2t1w_mask_arr, t1w_mask_arr)], - "coregPearson": [pearson(bold2t1w_mask_arr, t1w_mask_arr)], - "coregCoverage": [overlap(bold2t1w_mask_arr, t1w_mask_arr)], - "normDice": [dice(bold2template_mask_arr, template_mask_arr)], - "normPearson": [pearson(bold2template_mask_arr, template_mask_arr)], - "normCoverage": [overlap(bold2template_mask_arr, template_mask_arr)], + "coreg_dice": [dice(bold2t1w_mask_arr, t1w_mask_arr)], + "coreg_correlation": [pearson(bold2t1w_mask_arr, t1w_mask_arr)], + "coreg_overlap": [overlap(bold2t1w_mask_arr, t1w_mask_arr)], + "norm_dice": [dice(bold2template_mask_arr, template_mask_arr)], + "norm_correlation": [pearson(bold2template_mask_arr, template_mask_arr)], + "norm_overlap": [overlap(bold2template_mask_arr, template_mask_arr)], } qc_metadata = { - "coregDice": { + "coreg_dice": { "LongName": "Coregistration Sørensen-Dice Coefficient", "Description": ( "The Sørensen-Dice coefficient calculated between the binary brain masks from the " @@ -64,7 +64,7 @@ def compute_registration_qc(bold2t1w_mask, anat_brainmask, bold2template_mask, t ), "Term URL": "https://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient", }, - "coregPearson": { + "coreg_correlation": { "LongName": "Coregistration Pearson Correlation", "Description": ( "The Pearson correlation coefficient calculated between the binary brain masks " @@ -74,7 +74,7 @@ def compute_registration_qc(bold2t1w_mask, anat_brainmask, bold2template_mask, t ), "Term URL": "https://en.wikipedia.org/wiki/Pearson_correlation_coefficient", }, - "coregCoverage": { + "coreg_overlap": { "LongName": "Coregistration Coverage Metric", "Description": ( "The Szymkiewicz-Simpson overlap coefficient calculated between the binary brain " @@ -83,7 +83,7 @@ def compute_registration_qc(bold2t1w_mask, anat_brainmask, bold2template_mask, t ), "Term URL": "https://en.wikipedia.org/wiki/Overlap_coefficient", }, - "normDice": { + "norm_dice": { "LongName": "Normalization Sørensen-Dice Coefficient", "Description": ( "The Sørensen-Dice coefficient calculated between the binary brain masks from the " @@ -93,7 +93,7 @@ def compute_registration_qc(bold2t1w_mask, anat_brainmask, bold2template_mask, t ), "Term URL": "https://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient", }, - "normPearson": { + "norm_correlation": { "LongName": "Normalization Pearson Correlation", "Description": ( "The Pearson correlation coefficient calculated between the binary brain masks " @@ -103,7 +103,7 @@ def compute_registration_qc(bold2t1w_mask, anat_brainmask, bold2template_mask, t ), "Term URL": "https://en.wikipedia.org/wiki/Pearson_correlation_coefficient", }, - "normCoverage": { + "norm_overlap": { "LongName": "Normalization Overlap Coefficient", "Description": ( "The Szymkiewicz-Simpson overlap coefficient calculated between the binary brain "