diff --git a/.circleci/DSCSDSI_outputs.txt b/.circleci/DSCSDSI_outputs.txt index 3e4511a3..7fbf76ba 100644 --- a/.circleci/DSCSDSI_outputs.txt +++ b/.circleci/DSCSDSI_outputs.txt @@ -1,6 +1,5 @@ qsiprep qsiprep/dataset_description.json -qsiprep/dwiqc.json qsiprep/logs qsiprep/logs/CITATION.html qsiprep/logs/CITATION.md diff --git a/.circleci/DSDTI_nofmap_outputs.txt b/.circleci/DSDTI_nofmap_outputs.txt index 62b76d40..ef61321f 100644 --- a/.circleci/DSDTI_nofmap_outputs.txt +++ b/.circleci/DSDTI_nofmap_outputs.txt @@ -1,6 +1,5 @@ qsiprep qsiprep/dataset_description.json -qsiprep/dwiqc.json qsiprep/logs qsiprep/logs/CITATION.html qsiprep/logs/CITATION.md diff --git a/.circleci/DSDTI_outputs.txt b/.circleci/DSDTI_outputs.txt index 871db087..daf11d1f 100644 --- a/.circleci/DSDTI_outputs.txt +++ b/.circleci/DSDTI_outputs.txt @@ -1,6 +1,5 @@ qsiprep qsiprep/dataset_description.json -qsiprep/dwiqc.json qsiprep/logs qsiprep/logs/CITATION.html qsiprep/logs/CITATION.md diff --git a/.circleci/DSDTI_synsdc_outputs.txt b/.circleci/DSDTI_synsdc_outputs.txt index b49ce493..ea6501fd 100644 --- a/.circleci/DSDTI_synsdc_outputs.txt +++ b/.circleci/DSDTI_synsdc_outputs.txt @@ -1,6 +1,5 @@ qsiprep qsiprep/dataset_description.json -qsiprep/dwiqc.json qsiprep/logs qsiprep/logs/CITATION.html qsiprep/logs/CITATION.md diff --git a/.circleci/IntramodalTemplate_outputs.txt b/.circleci/IntramodalTemplate_outputs.txt index ddd0406b..020a87bb 100644 --- a/.circleci/IntramodalTemplate_outputs.txt +++ b/.circleci/IntramodalTemplate_outputs.txt @@ -1,6 +1,5 @@ qsiprep qsiprep/dataset_description.json -qsiprep/dwiqc.json qsiprep/logs qsiprep/logs/CITATION.html qsiprep/logs/CITATION.md diff --git a/pyproject.toml b/pyproject.toml index 67e67628..beb7d173 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ "nibabel <= 5.2.0", "nilearn == 0.10.1", "nipype == 1.8.6", + "nireports", "niworkflows >=1.9,<= 1.10", "numpy <= 1.26.3", "pandas < 2.0.0", diff --git a/qsiprep/cli/run.py b/qsiprep/cli/run.py index 9fcf7e62..fa2e44c6 100644 --- a/qsiprep/cli/run.py +++ b/qsiprep/cli/run.py @@ -170,16 +170,16 @@ def main(): errno = 0 finally: - from ..viz.reports import generate_reports + from ..reports.core import generate_reports # Generate reports phase - # session_list = ( - # config.execution.get().get('bids_filters', {}).get('dwi', {}).get('session') - # ) + session_list = config.execution.get().get("bids_filters", {}).get("dwi", {}).get("session") failed_reports = generate_reports( - config.execution.participant_label, - # session_list=session_list, + subject_list=config.execution.participant_label, + output_dir=config.execution.output_dir, + run_uuid=config.execution.run_uuid, + session_list=session_list, ) write_derivative_description( config.execution.bids_dir, @@ -198,4 +198,4 @@ def main(): # if sentry_sdk is not None: # sentry_sdk.capture_message(msg, level='error') - sys.exit(int(errno + failed_reports) > 0) + sys.exit(int(errno + len(failed_reports)) > 0) diff --git a/qsiprep/cli/workflow.py b/qsiprep/cli/workflow.py index f590b505..1daf0408 100644 --- a/qsiprep/cli/workflow.py +++ b/qsiprep/cli/workflow.py @@ -45,8 +45,8 @@ def build_workflow(config_file, retval): # from niworkflows.utils.misc import check_valid_fs_license # from ..utils.bids import check_pipeline_version from .. import config + from ..reports.core import generate_reports from ..utils.misc import check_deps - from ..viz.reports import generate_reports from ..workflows.base import init_qsiprep_wf config.load(config_file) @@ -94,9 +94,9 @@ def build_workflow(config_file, retval): ) failed_reports = generate_reports( - config.execution.participant_label, - config.execution.output_dir, - config.execution.run_uuid, + subject_list=config.execution.participant_label, + output_dir=config.execution.output_dir, + run_uuid=config.execution.run_uuid, session_list=session_list, ) if failed_reports: diff --git a/qsiprep/config.py b/qsiprep/config.py index ba75b221..aecadccd 100644 --- a/qsiprep/config.py +++ b/qsiprep/config.py @@ -432,6 +432,7 @@ class execution(_Config): """Write out the computational graph corresponding to the planned preprocessing.""" dataset_links = {} """A dictionary of dataset links to be used to track Sources in sidecars.""" + aggr_ses_reports = 4 _layout = None diff --git a/qsiprep/data/reports-spec-anat.yml b/qsiprep/data/reports-spec-anat.yml new file mode 100644 index 00000000..1c4189e5 --- /dev/null +++ b/qsiprep/data/reports-spec-anat.yml @@ -0,0 +1,51 @@ +package: qsiprep +title: Anatomical report for participant '{subject}' - QSIPrep +sections: +- name: Summary + reportlets: + - bids: {datatype: figures, desc: summary, suffix: T1w} +- name: Anatomical + reportlets: + - bids: + datatype: figures + desc: conform + extension: [.html] + suffix: T1w + - bids: {datatype: figures, suffix: dseg} + caption: | + This panel shows the final, preprocessed T1-weighted image, + with contours delineating the detected brain mask and brain tissue segmentations. + subtitle: Brain mask and brain tissue segmentation of the T1w + - bids: {datatype: figures, space: .*, suffix: T1w, regex_search: True} + caption: Spatial normalization of the T1w image to the {space} template. + description: | + Results of nonlinear alignment of the T1w reference one or more template + space(s). Hover on the panels with the mouse pointer to transition between both + spaces. + static: false + subtitle: Spatial normalization of the anatomical T1w reference + - bids: {datatype: figures, desc: reconall, suffix: T1w} + caption: | + Surfaces (white and pial) reconstructed with FreeSurfer (recon-all) + overlaid on the participant's T1w template. + subtitle: Surface reconstruction +- name: About + reportlets: + - bids: {datatype: figures, desc: about, suffix: T1w} + - custom: boilerplate + path: '{out_dir}/logs' + bibfile: ['qsiprep', 'data/boilerplate.bib'] + caption: | +

We kindly ask to report results preprocessed with this tool using the following boilerplate.

+ + title: Methods + - custom: errors + path: '{out_dir}/sub-{subject}/log/{run_uuid}' + captions: NiReports may have recorded failure conditions. + title: Errors diff --git a/qsiprep/data/reports-spec-dwi.yml b/qsiprep/data/reports-spec-dwi.yml new file mode 100644 index 00000000..0f01ee14 --- /dev/null +++ b/qsiprep/data/reports-spec-dwi.yml @@ -0,0 +1,173 @@ +package: qsiprep +title: Diffusion report for participant '{subject}', session '{session}' - QSIPrep +sections: +- name: B0 field mapping + ordering: session,acquisition,run,fmapid + reportlets: + - bids: {datatype: figures, desc: mapped, suffix: fieldmap} + caption: | + Inhomogeneities of the B0 field introduce (oftentimes severe) spatial distortions + along the phase-encoding direction of the image. Some scanners produce a B0 + mapping of the field, using Spiral Echo Imaging (SEI) or postprocessing a "phase-difference" + acquisition. The plot below shows an anatomical "magnitude" reference and the corresponding + fieldmap. + description: | + Hover over the panels with the mouse pointer to also visualize the intensity of the + field inhomogeneity in Hertz. + static: false + subtitle: "Preprocessed B0 mapping acquisition" + - bids: {datatype: figures, desc: phasediff, suffix: fieldmap} + caption: | + Inhomogeneities of the B0 field introduce (oftentimes severe) spatial distortions + along the phase-encoding direction of the image. A Gradient-Recalled Echo (GRE) scheme was included for the + mapping of the B0 inhomogeneities by subtracting the phase maps obtained at + two subsequent echoes. The plot below shows an anatomical "magnitude" reference and the corresponding + fieldmap. + description: | + Hover over the panels with the mouse pointer to also visualize the intensity of the + field inhomogeneity in Hertz. + static: false + subtitle: "Preprocessed mapping of phase-difference acquisition" + - bids: {datatype: figures, desc: pepolar, suffix: fieldmap} + caption: | + Inhomogeneities of the B0 field introduce (oftentimes severe) spatial distortions + along the phase-encoding direction of the image. Utilizing two or more images with different + phase-encoding polarities (PEPolar) or directions, it is possible to estimate the inhomogeneity + of the field. The plot below shows a reference EPI (echo-planar imaging) volume generated + using two or more EPI images with varying phase-encoding blips. + description: | + Hover on the panels with the mouse pointer to also visualize the intensity of the + inhomogeneity of the field in Hertz. + static: false + subtitle: "Preprocessed estimation with varying Phase-Encoding (PE) blips" + - bids: {datatype: figures, desc: anat, suffix: fieldmap} + caption: | + Inhomogeneities of the B0 field introduce (oftentimes severe) spatial distortions + along the phase-encoding direction of the image. Utilizing an anatomically-correct acquisition + (for instance, T1w or T2w), it is possible to estimate the inhomogeneity of the field by means of nonlinear + registration. The plot below shows a reference EPI (echo-planar imaging) volume generated + using two or more EPI images with the same PE encoding, after alignment to the anatomical scan. + description: | + Hover on the panels with the mouse pointer to also visualize the intensity of the + inhomogeneity of the field in Hertz. + static: false + subtitle: "Preprocessed estimation by nonlinear registration to an anatomical scan (“fieldmap-less”)" + +- name: Functional + ordering: session,task,acquisition,ceagent,reconstruction,direction,run,echo + reportlets: + - bids: {datatype: figures, desc: summary, suffix: dwi} + - bids: {datatype: figures, desc: validation, suffix: dwi} + - bids: {datatype: figures, desc: fmapCoreg, suffix: dwi} + caption: | + The estimated fieldmap was aligned to the corresponding EPI reference + with a rigid-registration process of the fieldmap reference image, + using antsRegistration. + Overlaid on top of the co-registration results, the final DWI mask is represented + with a red contour for reference. + static: false + subtitle: Alignment between the anatomical reference of the fieldmap and the target EPI + - bids: {datatype: figures, desc: fieldmap, suffix: dwi} + caption: | + Estimated fieldmap, as reconstructed on the target DWI run space to allow + the assessment of its alignment with the distorted data. + The anatomical reference is the fieldmap's reference moved into the target EPI's grid through + the estimated transformation. + In other words, this plot should be equivalent to that of the + Preprocessed estimation with varying Phase-Encoding (PE) blips shown above in the + fieldmap section. + Therefore, the fieldmap should be positioned relative to the anatomical reference exactly + as it is positioned in the reportlet above. + static: false + subtitle: "Reconstructed B0 map in the corresponding run's space (debug mode)" + - bids: {datatype: figures, desc: sdc, suffix: dwi} + caption: | + Results of performing susceptibility distortion correction (SDC) on the + DWI reference image. The "distorted" image is the image that would be used to + align to the anatomical reference if SDC were not applied. The "corrected" + image is the image that was used. + static: false + subtitle: Susceptibility distortion correction + - bids: {datatype: figures, desc: forcedsyn, suffix: dwi} + caption: | + The dataset contained some fieldmap information, but the argument --force-syn + was used. The higher-priority SDC method was used. Here, we show the results + of performing SyN-based SDC on the EPI for comparison. + static: false + subtitle: Experimental fieldmap-less susceptibility distortion correction + - bids: {datatype: figures, desc: t2scomp, suffix: dwi} + caption: | + A T2* map was calculated from the echos. Here, we show the comparison + of the T2* map and the DWI reference map used for DWI-T1w coregistration. + The red contour shows the anatomical gray-matter mask resampled into DWI space. + static: false + subtitle: T2* map + - bids: {datatype: figures, desc: t2starhist, suffix: dwi} + caption: | + A histogram of estimated T2* values within the anatomically-derived gray-matter mask + shown in the previous plot. Note that values are clipped at 100ms, so any extreme outliers will + appear in the 100ms bin. + static: false + subtitle: T2* gray-matter values + - bids: {datatype: figures, desc: coreg, suffix: dwi} + caption: | + This panel shows the alignment of the reference EPI (DWI) image to the + anatomical (T1-weighted) image. + The reference EPI has been contrast enhanced and susceptibility-distortion + corrected (if applicable) for improved anatomical fidelity. + The anatomical image has been resampled into EPI space, as well as the + anatomical white matter mask, which appears as a red contour. + static: false + subtitle: Alignment of functional and anatomical MRI data (coregistration) + - bids: {datatype: figures, desc: rois, suffix: dwi} + caption: | + Brain mask calculated on the DWI signal (red contour), along with the + regions of interest (ROIs) used for the estimation of physiological and movement + confounding components that can be then used as nuisance regressors in analysis.
+ The anatomical CompCor ROI (magenta contour) is a mask combining + CSF and WM (white-matter), where voxels containing a minimal partial volume + of GM have been removed.
+ The temporal CompCor ROI (blue contour) contains the top 2% most + variable voxels within the brain mask.
+ The brain edge (or crown) ROI (green contour) picks signals + outside but close to the brain, which are decomposed into 24 principal components. + subtitle: Brain mask and (anatomical/temporal) CompCor ROIs + - bids: + datatype: figures + desc: '[at]compcor' + extension: [.html] + suffix: dwi + - bids: {datatype: figures, desc: 'compcorvar', suffix: dwi} + caption: | + The cumulative variance explained by the first k components of the + t/aCompCor decomposition, plotted for all values of k. + The number of components that must be included in the model in order to + explain some fraction of variance in the decomposition mask can be used + as a feature selection criterion for confound regression. + subtitle: Variance explained by t/aCompCor components + - bids: {datatype: figures, desc: carpetplot, suffix: dwi} + caption: | + Summary statistics are plotted, which may reveal trends or artifacts + in the DWI data. Global signals calculated within the whole-brain (GS), within + the white-matter (WM) and within cerebro-spinal fluid (CSF) show the mean DWI + signal in their corresponding masks. DVARS and FD show the standardized DVARS + and framewise-displacement measures for each time point.
+ A carpet plot shows the time series for all voxels within the brain mask, + or if --cifti-output was enabled, all grayordinates. + See the figure legend for specific color mappings. + "Ctx" = cortex, "Cb" = cerebellum, "WM" = white matter, "CSF" = cerebrospinal fluid. + "d" and "s" prefixes indicate "deep" and "shallow" relative to the cortex. + "Edge" indicates regions just outside the brain. + subtitle: DWI Summary + - bids: {datatype: figures, desc: 'confoundcorr', suffix: dwi} + caption: | + Left: Heatmap summarizing the correlation structure among confound variables. + (Cosine bases and PCA-derived CompCor components are inherently orthogonal.) + Right: magnitude of the correlation between each confound time series and the + mean global signal. Strong correlations might be indicative of partial volume + effects and can inform decisions about feature orthogonalization prior to + confound regression. + subtitle: Correlations among nuisance regressors +- name: About + reportlets: + - bids: {datatype: figures, desc: about, suffix: T1w} diff --git a/qsiprep/data/reports-spec.yml b/qsiprep/data/reports-spec.yml new file mode 100644 index 00000000..04db2693 --- /dev/null +++ b/qsiprep/data/reports-spec.yml @@ -0,0 +1,220 @@ +package: qsiprep +title: Visual report for participant '{subject}' - QSIPrep +sections: +- name: Summary + reportlets: + - bids: {datatype: figures, desc: summary, suffix: [T1w, T2w]} +- name: Anatomical + reportlets: + - bids: + datatype: figures + desc: conform + extension: [.html] + suffix: [T1w, T2w] + - bids: {datatype: figures, suffix: dseg} + caption: | + This panel shows the final, preprocessed T1-weighted image, + with contours delineating the detected brain mask and brain tissue segmentations. + subtitle: Brain mask and brain tissue segmentation of the T1w + - bids: {datatype: figures, space: .*, suffix: T1w, regex_search: True} + caption: Spatial normalization of the T1w image to the {space} template. + description: | + Results of nonlinear alignment of the T1w reference one or more template + space(s). Hover on the panels with the mouse pointer to transition between both + spaces. + static: false + subtitle: Spatial normalization of the anatomical T1w reference + - bids: {datatype: figures, desc: reconall, suffix: T1w} + caption: | + Surfaces (white and pial) reconstructed with FreeSurfer (recon-all) + overlaid on the participant's T1w template. + subtitle: Surface reconstruction + +- name: B0 field mapping + ordering: session,acquisition,run,fmapid + reportlets: + - bids: {datatype: figures, desc: mapped, suffix: fieldmap} + caption: | + Inhomogeneities of the B0 field introduce (oftentimes severe) spatial distortions + along the phase-encoding direction of the image. Some scanners produce a B0 + mapping of the field, using Spiral Echo Imaging (SEI) or postprocessing a "phase-difference" + acquisition. The plot below shows an anatomical "magnitude" reference and the corresponding + fieldmap. + description: | + Hover over the panels with the mouse pointer to also visualize the intensity of the + field inhomogeneity in Hertz. + static: false + subtitle: "Preprocessed B0 mapping acquisition" + - bids: {datatype: figures, desc: phasediff, suffix: fieldmap} + caption: | + Inhomogeneities of the B0 field introduce (oftentimes severe) spatial distortions + along the phase-encoding direction of the image. A Gradient-Recalled Echo (GRE) scheme was included for the + mapping of the B0 inhomogeneities by subtracting the phase maps obtained at + two subsequent echoes. The plot below shows an anatomical "magnitude" reference and the corresponding + fieldmap. + description: | + Hover over the panels with the mouse pointer to also visualize the intensity of the + field inhomogeneity in Hertz. + static: false + subtitle: "Preprocessed mapping of phase-difference acquisition" + - bids: {datatype: figures, desc: pepolar, suffix: fieldmap} + caption: | + Inhomogeneities of the B0 field introduce (oftentimes severe) spatial distortions + along the phase-encoding direction of the image. Utilizing two or more images with different + phase-encoding polarities (PEPolar) or directions, it is possible to estimate the inhomogeneity + of the field. The plot below shows a reference EPI (echo-planar imaging) volume generated + using two or more EPI images with varying phase-encoding blips. + description: | + Hover on the panels with the mouse pointer to also visualize the intensity of the + inhomogeneity of the field in Hertz. + static: false + subtitle: "Preprocessed estimation with varying Phase-Encoding (PE) blips" + - bids: {datatype: figures, desc: anat, suffix: fieldmap} + caption: | + Inhomogeneities of the B0 field introduce (oftentimes severe) spatial distortions + along the phase-encoding direction of the image. Utilizing an anatomically-correct acquisition + (for instance, T1w or T2w), it is possible to estimate the inhomogeneity of the field by means of nonlinear + registration. The plot below shows a reference EPI (echo-planar imaging) volume generated + using two or more EPI images with the same PE encoding, after alignment to the anatomical scan. + description: | + Hover on the panels with the mouse pointer to also visualize the intensity of the + inhomogeneity of the field in Hertz. + static: false + subtitle: "Preprocessed estimation by nonlinear registration to an anatomical scan (“fieldmap-less”)" + +- name: Diffusion + ordering: session,task,acquisition,ceagent,reconstruction,direction,run,echo + reportlets: + - bids: {datatype: figures, desc: summary, suffix: dwi} + - bids: {datatype: figures, desc: validation, suffix: dwi} + - bids: {datatype: figures, desc: fmapCoreg, suffix: dwi} + caption: | + The estimated fieldmap was aligned to the corresponding EPI reference + with a rigid-registration process of the fieldmap reference image, + using antsRegistration. + Overlaid on top of the co-registration results, the final DWI mask is represented + with a red contour for reference. + static: false + subtitle: Alignment between the anatomical reference of the fieldmap and the target EPI + - bids: {datatype: figures, desc: fieldmap, suffix: dwi} + caption: | + Estimated fieldmap, as reconstructed on the target DWI run space to allow + the assessment of its alignment with the distorted data. + The anatomical reference is the fieldmap's reference moved into the target EPI's grid through + the estimated transformation. + In other words, this plot should be equivalent to that of the + Preprocessed estimation with varying Phase-Encoding (PE) blips shown above in the + fieldmap section. + Therefore, the fieldmap should be positioned relative to the anatomical reference exactly + as it is positioned in the reportlet above. + static: false + subtitle: "Reconstructed B0 map in the corresponding run's space (debug mode)" + - bids: {datatype: figures, desc: sdc, suffix: dwi} + caption: | + Results of performing susceptibility distortion correction (SDC) on the + DWI reference image. The "distorted" image is the image that would be used to + align to the anatomical reference if SDC were not applied. The "corrected" + image is the image that was used. + static: false + subtitle: Susceptibility distortion correction + - bids: {datatype: figures, desc: forcedsyn, suffix: dwi} + caption: | + The dataset contained some fieldmap information, but the argument --force-syn + was used. The higher-priority SDC method was used. Here, we show the results + of performing SyN-based SDC on the EPI for comparison. + static: false + subtitle: Experimental fieldmap-less susceptibility distortion correction + - bids: {datatype: figures, desc: t2scomp, suffix: dwi} + caption: | + A T2* map was calculated from the echos. Here, we show the comparison + of the T2* map and the DWI reference map used for DWI-T1w coregistration. + The red contour shows the anatomical gray-matter mask resampled into DWI space. + static: false + subtitle: T2* map + - bids: {datatype: figures, desc: t2starhist, suffix: dwi} + caption: | + A histogram of estimated T2* values within the anatomically-derived gray-matter mask + shown in the previous plot. Note that values are clipped at 100ms, so any extreme outliers will + appear in the 100ms bin. + static: false + subtitle: T2* gray-matter values + - bids: {datatype: figures, desc: coreg, suffix: dwi} + caption: | + This panel shows the alignment of the reference EPI (DWI) image to the + anatomical (T1-weighted) image. + The reference EPI has been contrast enhanced and susceptibility-distortion + corrected (if applicable) for improved anatomical fidelity. + The anatomical image has been resampled into EPI space, as well as the + anatomical white matter mask, which appears as a red contour. + static: false + subtitle: Alignment of functional and anatomical MRI data (coregistration) + - bids: {datatype: figures, desc: rois, suffix: dwi} + caption: | + Brain mask calculated on the DWI signal (red contour), along with the + regions of interest (ROIs) used for the estimation of physiological and movement + confounding components that can be then used as nuisance regressors in analysis.
+ The anatomical CompCor ROI (magenta contour) is a mask combining + CSF and WM (white-matter), where voxels containing a minimal partial volume + of GM have been removed.
+ The temporal CompCor ROI (blue contour) contains the top 2% most + variable voxels within the brain mask.
+ The brain edge (or crown) ROI (green contour) picks signals + outside but close to the brain, which are decomposed into 24 principal components. + subtitle: Brain mask and (anatomical/temporal) CompCor ROIs + - bids: + datatype: figures + desc: '[at]compcor' + extension: [.html] + suffix: dwi + - bids: {datatype: figures, desc: 'compcorvar', suffix: dwi} + caption: | + The cumulative variance explained by the first k components of the + t/aCompCor decomposition, plotted for all values of k. + The number of components that must be included in the model in order to + explain some fraction of variance in the decomposition mask can be used + as a feature selection criterion for confound regression. + subtitle: Variance explained by t/aCompCor components + - bids: {datatype: figures, desc: carpetplot, suffix: dwi} + caption: | + Summary statistics are plotted, which may reveal trends or artifacts + in the DWI data. Global signals calculated within the whole-brain (GS), within + the white-matter (WM) and within cerebro-spinal fluid (CSF) show the mean DWI + signal in their corresponding masks. DVARS and FD show the standardized DVARS + and framewise-displacement measures for each time point.
+ A carpet plot shows the time series for all voxels within the brain mask, + or if --cifti-output was enabled, all grayordinates. + See the figure legend for specific color mappings. + "Ctx" = cortex, "Cb" = cerebellum, "WM" = white matter, "CSF" = cerebrospinal fluid. + "d" and "s" prefixes indicate "deep" and "shallow" relative to the cortex. + "Edge" indicates regions just outside the brain. + subtitle: DWI Summary + - bids: {datatype: figures, desc: 'confoundcorr', suffix: dwi} + caption: | + Left: Heatmap summarizing the correlation structure among confound variables. + (Cosine bases and PCA-derived CompCor components are inherently orthogonal.) + Right: magnitude of the correlation between each confound time series and the + mean global signal. Strong correlations might be indicative of partial volume + effects and can inform decisions about feature orthogonalization prior to + confound regression. + subtitle: Correlations among nuisance regressors +- name: About + nested: true + reportlets: + - bids: {datatype: figures, desc: about, suffix: T1w} + - custom: boilerplate + path: '{out_dir}/logs' + bibfile: ['qsiprep', 'data/boilerplate.bib'] + caption: | +

We kindly ask to report results preprocessed with this tool using the following boilerplate.

+ + title: Methods + - custom: errors + path: '{out_dir}/sub-{subject}/log/{run_uuid}' + captions: NiReports may have recorded failure conditions. + title: Errors diff --git a/qsiprep/reports/core.py b/qsiprep/reports/core.py index a9924d4b..b45bf80b 100644 --- a/qsiprep/reports/core.py +++ b/qsiprep/reports/core.py @@ -116,7 +116,7 @@ def generate_reports( # we separate the functional reports per session if session_list is None: all_filters = config.execution.bids_filters or {} - filters = all_filters.get("bold", {}) + filters = all_filters.get("dwi", {}) session_list = config.execution.layout.get_sessions( subject=subject_label, **filters ) @@ -125,8 +125,8 @@ def generate_reports( session_list = [ses[4:] if ses.startswith("ses-") else ses for ses in session_list] for session_label in session_list: - bootstrap_file = data.load("reports-spec-func.yml") - html_report = f'sub-{subject_label.lstrip("sub-")}_ses-{session_label}_func.html' + bootstrap_file = data.load("reports-spec-dwi.yml") + html_report = f'sub-{subject_label.lstrip("sub-")}_ses-{session_label}_dwi.html' report_error = run_reports( output_dir, @@ -135,7 +135,7 @@ def generate_reports( bootstrap_file=bootstrap_file, out_filename=html_report, reportlets_dir=reportlets_dir, - errorname=f"report-{run_uuid}-{subject_label}-func.err", + errorname=f"report-{run_uuid}-{subject_label}-dwi.err", subject=subject_label, session=session_label, ) diff --git a/qsiprep/tests/data/drbuddi_rpe_outputs.txt b/qsiprep/tests/data/drbuddi_rpe_outputs.txt index d8cb76a0..5a37bc48 100644 --- a/qsiprep/tests/data/drbuddi_rpe_outputs.txt +++ b/qsiprep/tests/data/drbuddi_rpe_outputs.txt @@ -1,5 +1,4 @@ dataset_description.json -dwiqc.json logs logs/CITATION.bib logs/CITATION.html diff --git a/qsiprep/tests/data/drbuddi_shoreline_epi_outputs.txt b/qsiprep/tests/data/drbuddi_shoreline_epi_outputs.txt index 086f14a7..a622eee3 100644 --- a/qsiprep/tests/data/drbuddi_shoreline_epi_outputs.txt +++ b/qsiprep/tests/data/drbuddi_shoreline_epi_outputs.txt @@ -1,5 +1,4 @@ dataset_description.json -dwiqc.json logs logs/CITATION.bib logs/CITATION.html diff --git a/qsiprep/tests/data/drbuddi_tensorline_epi_outputs.txt b/qsiprep/tests/data/drbuddi_tensorline_epi_outputs.txt index 0c94e5fa..02768c18 100644 --- a/qsiprep/tests/data/drbuddi_tensorline_epi_outputs.txt +++ b/qsiprep/tests/data/drbuddi_tensorline_epi_outputs.txt @@ -1,5 +1,4 @@ dataset_description.json -dwiqc.json logs logs/CITATION.bib logs/CITATION.html diff --git a/qsiprep/tests/data/dscsdsi_outputs.txt b/qsiprep/tests/data/dscsdsi_outputs.txt index 8e84e73f..869bbc5a 100644 --- a/qsiprep/tests/data/dscsdsi_outputs.txt +++ b/qsiprep/tests/data/dscsdsi_outputs.txt @@ -1,5 +1,4 @@ dataset_description.json -dwiqc.json logs logs/CITATION.bib logs/CITATION.html diff --git a/qsiprep/tests/data/dsdti_nofmap_outputs.txt b/qsiprep/tests/data/dsdti_nofmap_outputs.txt index 5f1820f6..39bd8b1f 100644 --- a/qsiprep/tests/data/dsdti_nofmap_outputs.txt +++ b/qsiprep/tests/data/dsdti_nofmap_outputs.txt @@ -1,5 +1,4 @@ dataset_description.json -dwiqc.json logs logs/CITATION.bib logs/CITATION.html diff --git a/qsiprep/tests/data/dsdti_synfmap_outputs.txt b/qsiprep/tests/data/dsdti_synfmap_outputs.txt index 5f1820f6..39bd8b1f 100644 --- a/qsiprep/tests/data/dsdti_synfmap_outputs.txt +++ b/qsiprep/tests/data/dsdti_synfmap_outputs.txt @@ -1,5 +1,4 @@ dataset_description.json -dwiqc.json logs logs/CITATION.bib logs/CITATION.html diff --git a/qsiprep/tests/data/dsdti_topup_outputs.txt b/qsiprep/tests/data/dsdti_topup_outputs.txt index 5f1820f6..39bd8b1f 100644 --- a/qsiprep/tests/data/dsdti_topup_outputs.txt +++ b/qsiprep/tests/data/dsdti_topup_outputs.txt @@ -1,5 +1,4 @@ dataset_description.json -dwiqc.json logs logs/CITATION.bib logs/CITATION.html diff --git a/qsiprep/tests/data/forrest_gump_outputs.txt b/qsiprep/tests/data/forrest_gump_outputs.txt index c5a1df38..6d627024 100644 --- a/qsiprep/tests/data/forrest_gump_outputs.txt +++ b/qsiprep/tests/data/forrest_gump_outputs.txt @@ -1,5 +1,4 @@ dataset_description.json -dwiqc.json logs logs/CITATION.bib logs/CITATION.html diff --git a/qsiprep/tests/data/intramodal_template_outputs.txt b/qsiprep/tests/data/intramodal_template_outputs.txt index 560a0ccb..2733d287 100644 --- a/qsiprep/tests/data/intramodal_template_outputs.txt +++ b/qsiprep/tests/data/intramodal_template_outputs.txt @@ -1,6 +1,5 @@ /tmp/twoses/derivatives dataset_description.json -dwiqc.json logs logs/CITATION.html logs/CITATION.md diff --git a/qsiprep/tests/data/maternal_brain_project_outputs.txt b/qsiprep/tests/data/maternal_brain_project_outputs.txt index 9c5274b6..99a7c4c4 100644 --- a/qsiprep/tests/data/maternal_brain_project_outputs.txt +++ b/qsiprep/tests/data/maternal_brain_project_outputs.txt @@ -1,5 +1,4 @@ dataset_description.json -dwiqc.json logs logs/CITATION.bib logs/CITATION.html diff --git a/qsiprep/tests/test_cli.py b/qsiprep/tests/test_cli.py index d2989568..1be64e83 100644 --- a/qsiprep/tests/test_cli.py +++ b/qsiprep/tests/test_cli.py @@ -10,13 +10,13 @@ from qsiprep.cli import run from qsiprep.cli.parser import parse_args from qsiprep.cli.workflow import build_boilerplate, build_workflow +from qsiprep.reports.core import generate_reports from qsiprep.tests.utils import ( check_generated_files, download_test_data, get_test_data_path, ) from qsiprep.utils.bids import write_derivative_description -from qsiprep.viz.reports import generate_reports nipype_config.enable_debug_mode() diff --git a/qsiprep/viz/__init__.py b/qsiprep/viz/__init__.py index 0a006d62..e7e29412 100644 --- a/qsiprep/viz/__init__.py +++ b/qsiprep/viz/__init__.py @@ -5,4 +5,3 @@ """ The qsiprep reporting engine for visual assessment """ -from .reports import generate_reports, run_reports diff --git a/qsiprep/viz/config.json b/qsiprep/viz/config.json deleted file mode 100644 index 56662d62..00000000 --- a/qsiprep/viz/config.json +++ /dev/null @@ -1,247 +0,0 @@ -{ -"sections": -[ - { - "name": "Summary", - "reportlets": - [ - { - "name": "anat/summary", - "file_pattern": "anat/.*_summary", - "raw": true - } - ] - }, - { - "name": "Anatomical", - "reportlets": - [ - { - "name": "anat/conform", - "file_pattern": "anat/.*_conform", - "raw": true - }, - { - "name": "anat_preproc/t1_brain_seg", - "file_pattern": "anat/.*desc-seg_mask", - "title": "Brain mask and brain tissue segmentation of the T1w", - "description": "This panel shows the template T1-weighted image (if several T1w images were found), with contours delineating the detected brain mask and brain tissue segmentations.", - "imgtype": "svg+xml" - }, - { - "name": "anat_preproc/t1w2mni", - "file_pattern": "anat/.*t1w2mni", - "title": "T1 to MNI registration", - "description": "Nonlinear mapping of the T1w image into MNI space. Hover on the panel with the mouse to transition between both spaces.", - "imgtype": "svg+xml" - }, - { - "name": "anat_preproc/reconall", - "file_pattern": "anat/.*reconall", - "title": "Surface reconstruction", - "description": "Surfaces (white and pial) reconstructed with FreeSurfer (recon-all) overlaid on the participant's T1w template.", - "imgtype": "svg+xml" - }, - { - "name": "anat_preproc/intramodal_template", - "file_pattern": "anat/.*imtcoreg\\.", - "title": "Intramodal B0 template to T1w registration", - "description": "The intramodal B0 template registered to the T1w image", - "imgtype": "svg+xml" - } - ] - }, - { - "name": "Fieldmaps", - "reportlets": - [ - { - "name": "fmap/magnitude_mask", - "file_pattern": "fmap/.*fmap_mask\\.", - "title": "Skull stripped magnitude image", - "description": "Brain extraction of the magnitude image from the fieldmap", - "imgtype": "svg+xml" - }, - { - "name": "epi/topup_summary", - "title": "TOPUP Inputs", - "file_pattern": "dwi/.*_topupsummary\\.", - "raw": true - } - ] - }, - { - "name": "Denoising", - "reportlets": - [ - { - "name": "epi/denoising", - "file_pattern": "dwi/.*_denoising\\.", - "title": "DWI denoising", - "description": "Effect of denoising on a low and high-b image.", - "imgtype": "svg+xml" - }, - { - "name": "epi/unringing", - "file_pattern": "dwi/.*_unringing\\.", - "title": "Gibbs Ringing Removal", - "description": "Effect of removing Gibbs ringing on a low and high-b image.", - "imgtype": "svg+xml" - }, - { - "name": "epi/bias_correction", - "file_pattern": "dwi/.*_biascorr\\.", - "title": "DWI Bias correction", - "description": "Effect of bias correction on a low and high-b image. Bias field contour lines are drawn as an overlay.", - "imgtype": "svg+xml" - } - ] - }, - { - "name": "Diffusion", - "reportlets": - [ - { - "name": "epi/summary", - "file_pattern": "dwi/.*_summary", - "raw": true - }, - { - "name": "epi/validation", - "file_pattern": "dwi/.*_validation\\.", - "raw": true - }, - { - "name": "epi/b0ref", - "file_pattern":"dwi/.*_b0ref\\.", - "title": "b=0 Reference Image", - "description": "b=0 template and final mask output. The t1 and signal intersection mask is blue, their xor is red and the entire mask is plotted in cyan.", - "imgtype": "svg+xml" - }, - { - "name": "epi/sampling_scheme", - "file_pattern": "dwi/.*sampling_scheme\\.", - "title": "DWI Sampling Scheme", - "description": "Animation of the DWI sampling scheme. Each separate scan is its own color.", - "imgtype": "gif" - }, - { - "name": "epi/shoreline_convergence", - "file_pattern": "dwi/.*shoreline_iterdata\\.", - "title": "SHORELine Convergence", - "description": "Difference in motion estimates over SHORELine iterations. Values close to zero indicate good convergence.", - "imgtype": "svg+xml" - }, - { - "name": "epi/intramodal_template", - "file_pattern": "dwi/.*tointramodal\\.", - "title": "Registration to Intramodal Template", - "description": "b0 reference image warped to the across-scan/session b0 template", - "imgtype": "svg+xml" - }, - { - "name": "epi/shoreline_reg", - "file_pattern": "dwi/.*shoreline_animation\\.", - "title": "SHORELine Registration", - "description": "Maximum intensity projections of each DWI before and after SHORELine registration. Orange lines are from the observed image and magenta lines are from the model-based registration target.", - "imgtype": "gif" - }, - { - "name": "epi/fmap_reg", - "file_pattern": "dwi/.*fmapreg\\.", - "title": "Fieldmap to EPI registration", - "description": "Results of affine coregistration between the magnitude image of the fieldmap and the reference EPI image", - "imgtype": "svg+xml" - }, - { - "name": "epi/fmap_reg", - "file_pattern": "dwi/.*fmapregvsm\\.", - "title": "Fieldmap", - "description": "Overlaid on the reference EPI image", - "imgtype": "svg+xml" - }, - { - "name": "epi/unwarp", - "file_pattern": "dwi/.*sdc_b0.*\\.", - "title": "Susceptibility distortion correction (TOPUP)", - "description": "Results of performing susceptibility distortion correction (SDC) using b=0 images in TOPUP", - "imgtype": "svg+xml" - }, - { - "name": "epi/unwarp", - "file_pattern": "dwi/.*sdcdrbuddi_b0.*\\.", - "title": "Susceptibility distortion correction (DRBUDDI)", - "description": "Results of performing susceptibility distortion correction (SDC) using DRBUDDI. b=0 images are shown", - "imgtype": "svg+xml" - }, - { - "name": "epi/unwarp", - "file_pattern": "dwi/.*sdcdrbuddi_b0t2w.*\\.", - "title": "Susceptibility distortion correction (DRBUDDI+T2w)", - "description": "Results of performing susceptibility distortion correction (SDC) using DRBUDDI. The overlay shown is an ad-hoc segmentation of a t2w image and is only for display purposes.", - "imgtype": "svg+xml" - }, - { - "name": "epi/unwarp", - "file_pattern": "dwi/.*sdc_fa.*\\.", - "title": "Susceptibility distortion correction", - "description": "Results of performing susceptibility distortion correction (SDC) on Fractional Anisotropy (FA) images.", - "imgtype": "svg+xml" - }, - { - "name": "epi/sdc_syn", - "file_pattern": "dwi/.*_forcedsyn\\.", - "title": "Experimental fieldmap-less susceptibility distortion correction", - "description": "The dataset contained some fieldmap information, but the argument --force-syn was used. The higher-priority SDC method was used. Here, we show the results of performing SyN-based SDC on the EPI for comparison.", - "imgtype": "svg+xml" - }, - { - "name": "epi_mean_t1_registration/b0_coreg", - "file_pattern": "dwi/.*_coreg\\.", - "title": "b=0 to anatomical reference registration", - "description": "antsRegistration was used to generate transformations from the b=0 reference image to the T1w-image.", - "imgtype": "svg+xml" - }, - { - "name": "epi_mean_t1_registration/b0_acpc", - "file_pattern": "dwi/.*_acpc\\.", - "title": "b=0 transformation to AC-PC space", - "description": "antsRegistration was used to generate transformations from the b=0 reference image to AC-PC alignment.", - "imgtype": "svg+xml" - }, - { - "name": "epi/intramodal_reg", - "file_pattern": "dwi/.*tointramodal\\.", - "title": "Registration to the Intramodal Template", - "description": "The b=0 template registered to the midpoint b=0 image", - "imgtype": "svg+xml" - }, - { - "name": "epi/interactive_qc", - "file_pattern": "dwi/.*interactive\\.", - "title": "INTERACT", - "description": "Interactive QC Widgets", - "raw":true - }, - { - "name": "epi/carpetplot", - "file_pattern": "dwi/.*carpetplot\\.", - "title": "DWI Summary", - "description": "Summary statistics are plotted.", - "imgtype": "svg+xml" - } - ] - }, - { - "name": "About", - "reportlets": - [ - { - "name": "anat/about", - "file_pattern": "anat/.*_about", - "raw": true - } - ] - } -] -} diff --git a/qsiprep/viz/report.tpl b/qsiprep/viz/report.tpl deleted file mode 100644 index 4487bc1a..00000000 --- a/qsiprep/viz/report.tpl +++ /dev/null @@ -1,186 +0,0 @@ - - - - - - - - - - - - - - - - - - -{% for sub_report in sections %} -
-

{{ sub_report.name }}

- {% if sub_report.isnested %} - {% for run_report in sub_report.reportlets %} -
-

Reports for {{ run_report.title }}

- {% for elem in run_report.reportlets %} - {% if elem.contents %} - {% if elem.title %}

{{ elem.title }}

{% endif %} - {% if elem.description %}

{{ elem.description }}

{% endif %} - {% for content in elem.contents %} - {% if elem.raw %}{{ content }}{% else %} -

- - Problem loading figure {{ content }}. If the link below works, please try reloading the report in your browser. -
-
- Get figure file: {{ content }} -
- {% endif %} - {% endfor %} - {% endif %} - {% endfor %} -
- {% endfor %} - {% else %} - {% for elem in sub_report.reportlets %} - {% if elem.contents %} - {% if elem.title %}

{{ elem.title }}

{% endif %} - {% if elem.description %}

{{ elem.description }}


{% endif %} - {% for content in elem.contents %} - {% if elem.raw %}{{ content }}{% else %} -

- filename:{{ content }} -
-
- Get figure file: {{ content }} -
- {% endif %} - {% endfor %} - {% endif %} - {% endfor %} - {% endif %} -
-{% endfor %} - -
-

Methods

- {% if boilerplate %} -

We kindly ask to report results preprocessed with qsiprep using the following - boilerplate

- -
- {% for b in boilerplate %} -
{{ b[2] }}
- {% endfor %} -
- {% else %} -

Failed to generate the boilerplate

- {% endif %} -

Alternatively, an interactive boilerplate generator is available in the documentation website.

-
- -
-

Errors

- -
- - - - - diff --git a/qsiprep/viz/reports.py b/qsiprep/viz/reports.py deleted file mode 100644 index dd41ae83..00000000 --- a/qsiprep/viz/reports.py +++ /dev/null @@ -1,425 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- -# vi: set ft=python sts=4 ts=4 sw=4 et: -""" -qsiprep reports builder -^^^^^^^^^^^^^^^^^^^^^^^^ - - -""" -import html -import json -import re -from pathlib import Path - -import jinja2 -from nipype.utils.filemanip import copyfile, loadcrash -from pkg_resources import resource_filename as pkgrf - -from .. import config - - -class Element(object): - """ - Just a basic component of a report - """ - - def __init__(self, name, title=None): - self.name = name - self.title = title - - -class Reportlet(Element): - """ - A reportlet has title, description and a list of graphical components - """ - - def __init__( - self, name, imgtype=None, file_pattern=None, title=None, description=None, raw=False - ): - self.name = name - self.file_pattern = re.compile(file_pattern) - self.title = title - self.description = description - self.source_files = [] - self.contents = [] - self.raw = raw - self.imgtype = imgtype - - -class SubReport(Element): - """ - SubReports are sections within a Report - """ - - def __init__(self, name, reportlets=None, title=""): - self.name = name - self.title = title - self.reportlets = [] - if reportlets: - self.reportlets += reportlets - self.isnested = False - - -class Report(object): - """ - The full report object - """ - - def __init__( - self, - path, - viz_config, - out_dir, - run_uuid, - out_filename="report.html", - ): - self.root = path - self.sections = [] - self.errors = [] - self.out_dir = Path(out_dir) - self.out_filename = out_filename - self.run_uuid = run_uuid - - self._load_config(viz_config) - - def _load_config(self, viz_config): - with open(viz_config, "r") as configfh: - viz_config = json.load(configfh) - - self.index(viz_config["sections"]) - - def index(self, viz_config): - fig_dir = "figures" - subject_dir = self.root.split("/")[-1] - subject = re.search("^(?Psub-[a-zA-Z0-9]+)$", subject_dir).group() - svg_dir = self.out_dir / subject / fig_dir - svg_dir.mkdir(parents=True, exist_ok=True) - reportlet_list = list(sorted([str(f) for f in Path(self.root).glob("**/*.*")])) - - for subrep_cfg in viz_config: - reportlets = [] - for reportlet_cfg in subrep_cfg["reportlets"]: - rlet = Reportlet(**reportlet_cfg) - for src in reportlet_list: - ext = src.split(".")[-1] - if rlet.file_pattern.search(src): - contents = None - if ext == "html": - with open(src) as fp: - contents = fp.read().strip() - elif ext in ("svg", "gif", "png"): - fbase = Path(src).name - copyfile(src, str(svg_dir / fbase), copy=True, use_hardlink=True) - contents = str(Path(subject) / fig_dir / fbase) - if contents: - rlet.source_files.append(src) - rlet.contents.append(contents) - - if rlet.source_files: - reportlets.append(rlet) - - if reportlets: - sub_report = SubReport( - subrep_cfg["name"], reportlets=reportlets, title=subrep_cfg.get("title") - ) - self.sections.append(order_by_run(sub_report)) - - error_dir = self.out_dir / subject / "log" / self.run_uuid - if error_dir.is_dir(): - self.index_error_dir(error_dir) - - def index_error_dir(self, error_dir): - """ - Crawl subjects crash directory for the corresponding run and return text for - .pklz crash file found. - """ - for crashfile in error_dir.glob("crash*.*"): - if crashfile.suffix == ".pklz": - self.errors.append(self._read_pkl(crashfile)) - elif crashfile.suffix == ".txt": - self.errors.append(self._read_txt(crashfile)) - - @staticmethod - def _read_pkl(path): - fname = str(path) - crash_data = loadcrash(fname) - data = { - "file": fname, - "traceback": "".join(crash_data["traceback"]).replace("\\n", "
"), - } - if "node" in crash_data: - data["node"] = crash_data["node"] - if data["node"].base_dir: - data["node_dir"] = data["node"].output_dir() - else: - data["node_dir"] = "Node crashed before execution" - data["inputs"] = sorted(data["node"].inputs.trait_get().items()) - return data - - @staticmethod - def _read_txt(path): - lines = path.read_text(encoding="UTF-8").splitlines() - data = {"file": str(path)} - traceback_start = 0 - if lines[0].startswith("Node"): - data["node"] = lines[0].split(": ", 1)[1] - data["node_dir"] = lines[1].split(": ", 1)[1] - inputs = [] - for i, line in enumerate(lines[5:], 5): - if not line: - traceback_start = i + 1 - break - inputs.append(tuple(map(html.escape, line.split(" = ", 1)))) - data["inputs"] = sorted(inputs) - else: - data["node_dir"] = "Node crashed before execution" - data["traceback"] = "\n".join(lines[traceback_start:]) - return data - - def generate_report(self): - logs_path = self.out_dir / "logs" - - boilerplate = [] - boiler_idx = 0 - - if (logs_path / "CITATION.html").exists(): - text = (logs_path / "CITATION.html").read_text(encoding="UTF-8") - text = ( - '
%s
' - % re.compile("(.*?)", re.DOTALL | re.IGNORECASE) - .findall(text)[0] - .strip() - ) - boilerplate.append((boiler_idx, "HTML", text)) - boiler_idx += 1 - - if (logs_path / "CITATION.md").exists(): - text = "
%s
\n" % (logs_path / "CITATION.md").read_text(encoding="UTF-8") - boilerplate.append((boiler_idx, "Markdown", text)) - boiler_idx += 1 - - if (logs_path / "CITATION.tex").exists(): - text = (logs_path / "CITATION.tex").read_text(encoding="UTF-8") - text = ( - re.compile(r"\\begin{document}(.*?)\\end{document}", re.DOTALL | re.IGNORECASE) - .findall(text)[0] - .strip() - ) - text = "
%s
\n" % text - text += "

Bibliography

\n" - text += "
%s
\n" % Path(pkgrf("qsiprep", "data/boilerplate.bib")).read_text( - encoding="UTF-8" - ) - boilerplate.append((boiler_idx, "LaTeX", text)) - boiler_idx += 1 - - searchpath = pkgrf("qsiprep", "/") - env = jinja2.Environment( - loader=jinja2.FileSystemLoader(searchpath=searchpath), - trim_blocks=True, - lstrip_blocks=True, - ) - report_tpl = env.get_template("viz/report.tpl") - report_render = report_tpl.render( - sections=self.sections, errors=self.errors, boilerplate=boilerplate - ) - - # Write out report - (self.out_dir / self.out_filename).write_text(report_render, encoding="UTF-8") - return len(self.errors) - - -def order_by_run(subreport): - ordered = [] - run_reps = {} - for element in subreport.reportlets: - if len(element.source_files) == 1 and element.source_files[0]: - ordered.append(element) - continue - - for filename, file_contents in zip(element.source_files, element.contents): - name, title = generate_name_title(filename) - if not filename or not name: - continue - - new_element = Reportlet( - name=element.name, - title=element.title, - file_pattern=element.file_pattern, - description=element.description, - raw=element.raw, - imgtype=element.imgtype, - ) - new_element.contents.append(file_contents) - new_element.source_files.append(filename) - - if name not in run_reps: - run_reps[name] = SubReport(name, title=title) - - run_reps[name].reportlets.append(new_element) - - if run_reps: - keys = list(sorted(run_reps.keys())) - for key in keys: - ordered.append(run_reps[key]) - subreport.isnested = True - - subreport.reportlets = ordered - return subreport - - -def generate_name_title(filename): - fname = Path(filename).name - expr = re.compile( - "^sub-(?P[a-zA-Z0-9]+)(_ses-(?P[a-zA-Z0-9]+))?" - "(_task-(?P[a-zA-Z0-9]+))?(_acq-(?P[a-zA-Z0-9]+))?" - "(_rec-(?P[a-zA-Z0-9]+))?(_run-(?P[a-zA-Z0-9]+))?" - ) - outputs = expr.search(fname) - if outputs: - outputs = outputs.groupdict() - else: - return None, None - - name = "{session}{task}{acq}{rec}{run}".format( - session="_ses-" + outputs["session_id"] if outputs["session_id"] else "", - task="_task-" + outputs["task_id"] if outputs["task_id"] else "", - acq="_acq-" + outputs["acq_id"] if outputs["acq_id"] else "", - rec="_rec-" + outputs["rec_id"] if outputs["rec_id"] else "", - run="_run-" + outputs["run_id"] if outputs["run_id"] else "", - ) - title = "{session}{task}{acq}{rec}{run}".format( - session=" Session: " + outputs["session_id"] if outputs["session_id"] else "", - task=" Task: " + outputs["task_id"] if outputs["task_id"] else "", - acq=" Acquisition: " + outputs["acq_id"] if outputs["acq_id"] else "", - rec=" Reconstruction: " + outputs["rec_id"] if outputs["rec_id"] else "", - run=" Run: " + outputs["run_id"] if outputs["run_id"] else "", - ) - return name.strip("_"), title - - -def run_reports(reportlets_dir, out_dir, subject_label, run_uuid): - """ - Runs the reports - - >>> import os - >>> from shutil import copytree - >>> from tempfile import TemporaryDirectory - >>> filepath = os.path.dirname(os.path.realpath(__file__)) - >>> test_data_path = os.path.realpath(os.path.join(filepath, - ... '../data/tests/work')) - >>> curdir = os.getcwd() - >>> tmpdir = TemporaryDirectory() - >>> os.chdir(tmpdir.name) - >>> data_dir = copytree(test_data_path, os.path.abspath('work')) - >>> os.makedirs('out/qsiprep', exist_ok=True) - >>> run_reports(os.path.abspath('work/reportlets'), - ... os.path.abspath('out'), - ... '01', 'madeoutuuid') - 0 - >>> os.chdir(curdir) - >>> tmpdir.cleanup() - - """ - reportlet_path = str(Path(reportlets_dir) / ("sub-%s" % subject_label)) - viz_config = pkgrf("qsiprep", "viz/config.json") - - out_filename = "sub-{}.html".format(subject_label) - report = Report(reportlet_path, viz_config, out_dir, run_uuid, out_filename) - return report.generate_report() - - -def generate_reports(subject_list): - """ - A wrapper to run_reports on a given ``subject_list`` - """ - reports_dir = str(config.execution.output_dir) - run_uuid = config.execution.run_uuid - output_dir = str(config.execution.output_dir) - report_errors = [ - run_reports(reports_dir, output_dir, subject_label, run_uuid=run_uuid) - for subject_label in subject_list - ] - - errno = sum(report_errors) - errno += generate_interactive_report_summary(output_dir) - if errno: - import logging - - logger = logging.getLogger("cli") - logger.warning( - "Errors occurred while generating reports for participants: %s.", - ", ".join( - ["%s (%d)" % (subid, err) for subid, err in zip(subject_list, report_errors)] - ), - ) - return errno - - -def generate_interactive_report_summary(output_dir): - """ - Gather the dwiqc values from the outputs in a - """ - report_errors = [] - qc_report = { - "report_type": "dwi_qc_report", - "pipeline": "qsiprep", - "pipeline_version": 0, - "boilerplate": "", - "metric_explanation": { - "raw_dimension_x": "Number of x voxels in raw images", - "raw_dimension_y": "Number of y voxels in raw images", - "raw_dimension_z": "Number of z voxels in raw images", - "raw_voxel_size_x": "Voxel size in x direction in raw images", - "raw_voxel_size_y": "Voxel size in y direction in raw images", - "raw_voxel_size_z": "Voxel size in z direction in raw images", - "raw_max_b": "Maximum b-value in s/mm^2 in raw images", - "raw_neighbor_corr": "Neighboring DWI Correlation (NDC) of raw images", - "raw_num_bad_slices": "Number of bad slices in raw images (from DSI Studio)", - "raw_num_directions": "Number of directions sampled in raw images", - "t1_dimension_x": "Number of x voxels in preprocessed images", - "t1_dimension_y": "Number of y voxels in preprocessed images", - "t1_dimension_z": "Number of z voxels in preprocessed images", - "t1_voxel_size_x": "Voxel size in x direction in preprocessed images", - "t1_voxel_size_y": "Voxel size in y direction in preprocessed images", - "t1_voxel_size_z": "Voxel size in z direction in preprocessed images", - "t1_max_b": "Maximum b-value s/mm^2 in preprocessed images", - "t1_neighbor_corr": "Neighboring DWI Correlation (NDC) of preprocessed images", - "t1_num_bad_slices": "Number of bad slices in preprocessed images (from DSI Studio)", - "t1_num_directions": "Number of directions sampled in preprocessed images", - "mean_fd": "Mean framewise displacement from head motion", - "max_fd": "Maximum framewise displacement from head motion", - "max_rotation": "Maximum rotation from head motion", - "max_translation": "Maximum translation from head motion", - "max_rel_rotation": "Maximum rotation relative to the previous head position", - "max_rel_translation": "Maximum translation relative to the previous head position", - "t1_dice_distance": "Dice score for the overlap of the T1w-based brain mask " - "and the b=0 ref mask", - }, - } - qc_values = [] - output_path = Path(output_dir) - dwiqc_jsons = output_path.rglob("**/sub-*dwiqc.json") - - for qc_file in dwiqc_jsons: - try: - with open(qc_file, "r") as qc_json: - dwi_qc = json.load(qc_json)["qc_scores"] - dwi_qc["participant_id"] = dwi_qc.get("subject_id", "subject") - qc_values.append(dwi_qc) - except Exception: - report_errors.append(1) - - errno = sum(report_errors) - if errno: - import logging - - logger = logging.getLogger("cli") - logger.warning("Errors occurred while generating interactive report summary.") - qc_report["subjects"] = qc_values - with open(output_path / "dwiqc.json", "w") as project_qc: - json.dump(qc_report, project_qc, indent=2) - - return errno diff --git a/qsiprep/viz/utils.py b/qsiprep/viz/utils.py index a312f2c2..dba519b8 100644 --- a/qsiprep/viz/utils.py +++ b/qsiprep/viz/utils.py @@ -1,6 +1,5 @@ """Visualization utilities.""" -import numpy as np from lxml import etree from nilearn.plotting import plot_anat from niworkflows.viz.utils import SVGNS, extract_svg, robust_set_limits, uuid4 @@ -152,17 +151,3 @@ def plot_acpc( out_files.append(svg_fig) return out_files - - -def slices_from_bbox(mask_data, cuts=3, padding=0): - """Finds equi-spaced cuts for presenting images""" - B = np.argwhere(mask_data > 0) - start_coords = B.min(0) - stop_coords = B.max(0) + 1 - - vox_coords = [] - for start, stop in zip(start_coords, stop_coords): - inc = abs(stop - start) / (cuts + 2 * padding + 1) - slices = [start + (i + 1) * inc for i in range(cuts + 2 * padding)] - vox_coords.append(slices[padding:-padding]) - return {k: [int(_v) for _v in v] for k, v in zip(["x", "y", "z"], vox_coords)}