From 73b2f03413b1ac5b680cf2209e1f7e5796681f2e Mon Sep 17 00:00:00 2001 From: mnoergaard Date: Sat, 24 May 2025 16:51:47 +0200 Subject: [PATCH 1/9] FIX: fix petref to report connection and add test --- fmriprep/workflows/pet/confounds.py | 29 ++++++++++++++---------- fmriprep/workflows/pet/fit.py | 1 + fmriprep/workflows/pet/tests/test_fit.py | 14 ++++++++++++ 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/fmriprep/workflows/pet/confounds.py b/fmriprep/workflows/pet/confounds.py index d07651f..736ce68 100644 --- a/fmriprep/workflows/pet/confounds.py +++ b/fmriprep/workflows/pet/confounds.py @@ -350,18 +350,6 @@ def init_pet_confs_wf( mem_gb=DEFAULT_MEMORY_MIN_GB, ) - def _last(inlist): - return inlist[-1] - - def _select_cols(table): - import pandas as pd - - return [ - col - for col in pd.read_table(table, nrows=2).columns - if not col.startswith(('a_comp_cor_', 't_comp_cor_', 'std_dvars')) - ] - workflow.connect([ # connect inputnode to each non-anatomical confound node (inputnode, dvars, [('pet', 'in_file'), @@ -619,3 +607,20 @@ def _get_zooms(in_file): import nibabel as nb return tuple(nb.load(in_file).header.get_zooms()[:3]) + + +def _last(inlist): + """Return the last element of a list.""" + + return inlist[-1] + + +def _select_cols(table): + """Return confound columns excluding a/tCompCor and std_dvars.""" + import pandas as pd + + return [ + col + for col in pd.read_table(table, nrows=2).columns + if not col.startswith(("a_comp_cor_", "t_comp_cor_", "std_dvars")) + ] diff --git a/fmriprep/workflows/pet/fit.py b/fmriprep/workflows/pet/fit.py index ce604f7..a4dc1fa 100644 --- a/fmriprep/workflows/pet/fit.py +++ b/fmriprep/workflows/pet/fit.py @@ -228,6 +228,7 @@ def init_pet_fit_wf( ('subjects_dir', 'inputnode.subjects_dir'), ('subject_id', 'inputnode.subject_id'), ]), + (petref_buffer, func_fit_reports_wf, [('petref', 'inputnode.petref')]), (outputnode, func_fit_reports_wf, [ ('pet_mask', 'inputnode.pet_mask'), ('petref2anat_xfm', 'inputnode.petref2anat_xfm'), diff --git a/fmriprep/workflows/pet/tests/test_fit.py b/fmriprep/workflows/pet/tests/test_fit.py index c19de56..96e8e7c 100644 --- a/fmriprep/workflows/pet/tests/test_fit.py +++ b/fmriprep/workflows/pet/tests/test_fit.py @@ -154,3 +154,17 @@ def test_pet_fit_mask_connections(bids_root: Path, tmp_path: Path): ds_edge = wf._graph.get_edge_data(merge_mask, wf.get_node('ds_petmask_wf')) assert ('out', 'inputnode.petmask') in ds_edge['connect'] + + +def test_petref_report_connections(bids_root: Path, tmp_path: Path): + """Ensure the PET reference is passed to the reports workflow.""" + pet_file = str(bids_root / 'sub-01' / 'pet' / 'sub-01_task-rest_run-1_pet.nii.gz') + img = nb.Nifti1Image(np.zeros((2, 2, 2, 1)), np.eye(4)) + img.to_filename(pet_file) + + with mock_config(bids_dir=bids_root): + wf = init_pet_fit_wf(pet_file=pet_file, precomputed={}, omp_nthreads=1) + + petref_buffer = wf.get_node('petref_buffer') + edge = wf._graph.get_edge_data(petref_buffer, wf.get_node('func_fit_reports_wf')) + assert ('petref', 'inputnode.petref') in edge['connect'] From 3dec9389003ae093252e514a014f32098c0692bf Mon Sep 17 00:00:00 2001 From: mnoergaard Date: Sat, 24 May 2025 19:10:43 +0200 Subject: [PATCH 2/9] FIX: Fix merge_rois node crash --- fmriprep/workflows/pet/confounds.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/fmriprep/workflows/pet/confounds.py b/fmriprep/workflows/pet/confounds.py index 736ce68..31a0611 100644 --- a/fmriprep/workflows/pet/confounds.py +++ b/fmriprep/workflows/pet/confounds.py @@ -382,8 +382,8 @@ def init_pet_confs_wf( (acompcor_tfm, acompcor_bin, [('output_image', 'in_file')]), (acompcor_bin, merge_rois, [ (('out_mask', _last), 'in3'), - (('out_mask', lambda masks: masks[0]), 'in1'), - (('out_mask', lambda masks: masks[1]), 'in2'), + (('out_mask', _first), 'in1'), + (('out_mask', _second), 'in2'), ]), (merge_rois, signals, [('out', 'label_files')]), @@ -615,6 +615,17 @@ def _last(inlist): return inlist[-1] +def _first(inlist): + """Return the first element of a list.""" + + return inlist[0] + + +def _second(inlist): + """Return the second element of a list.""" + + return inlist[1] + def _select_cols(table): """Return confound columns excluding a/tCompCor and std_dvars.""" import pandas as pd @@ -622,5 +633,5 @@ def _select_cols(table): return [ col for col in pd.read_table(table, nrows=2).columns - if not col.startswith(("a_comp_cor_", "t_comp_cor_", "std_dvars")) + if not col.startswith(('a_comp_cor_', 't_comp_cor_', 'std_dvars')) ] From 3afd51cbafe113fcf2790214402c390e8998b535 Mon Sep 17 00:00:00 2001 From: mnoergaard Date: Sat, 24 May 2025 19:49:54 +0200 Subject: [PATCH 3/9] FIX: update output path --- fmriprep/workflows/pet/base.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/fmriprep/workflows/pet/base.py b/fmriprep/workflows/pet/base.py index 72e1b7b..0725bd4 100644 --- a/fmriprep/workflows/pet/base.py +++ b/fmriprep/workflows/pet/base.py @@ -152,7 +152,7 @@ def init_pet_wf( precomputed = {} pet_file = pet_series - fmriprep_dir = config.execution.petprep_dir + petprep_dir = config.execution.petprep_dir omp_nthreads = config.nipype.omp_nthreads all_metadata = [config.execution.layout.get_metadata(file) for file in pet_series] @@ -272,7 +272,7 @@ def init_pet_wf( if petref_out: ds_pet_native_wf = init_ds_pet_native_wf( bids_root=str(config.execution.bids_dir), - output_dir=fmriprep_dir, + output_dir=petprep_dir, pet_output=petref_out, all_metadata=all_metadata, ) @@ -288,7 +288,7 @@ def init_pet_wf( # Fill-in datasinks of reportlets seen so far for node in workflow.list_node_names(): if node.split('.')[-1].startswith('ds_report'): - workflow.get_node(node).inputs.base_directory = fmriprep_dir + workflow.get_node(node).inputs.base_directory = petprep_dir workflow.get_node(node).inputs.source_file = pet_file return workflow @@ -319,7 +319,7 @@ def init_pet_wf( if nonstd_spaces.intersection(('anat', 'T1w')): ds_pet_t1_wf = init_ds_volumes_wf( bids_root=str(config.execution.bids_dir), - output_dir=fmriprep_dir, + output_dir=petprep_dir, metadata=all_metadata[0], name='ds_pet_t1_wf', ) @@ -350,7 +350,7 @@ def init_pet_wf( ) ds_pet_std_wf = init_ds_volumes_wf( bids_root=str(config.execution.bids_dir), - output_dir=fmriprep_dir, + output_dir=petprep_dir, metadata=all_metadata[0], name='ds_pet_std_wf', ) @@ -401,7 +401,7 @@ def init_pet_wf( surface_spaces=freesurfer_spaces, medial_surface_nan=config.workflow.medial_surface_nan, metadata=all_metadata[0], - output_dir=fmriprep_dir, + output_dir=petprep_dir, name='pet_surf_wf', ) pet_surf_wf.inputs.inputnode.source_file = pet_file @@ -459,7 +459,7 @@ def init_pet_wf( ds_pet_cifti = pe.Node( DerivativesDataSink( - base_directory=fmriprep_dir, + base_directory=petprep_dir, space='fsLR', density=config.workflow.cifti_output, suffix='pet', @@ -523,7 +523,7 @@ def init_pet_wf( ds_confounds = pe.Node( DerivativesDataSink( - base_directory=fmriprep_dir, + base_directory=petprep_dir, desc='confounds', suffix='timeseries', ), @@ -589,7 +589,7 @@ def _last(inlist): # Fill-in datasinks of reportlets seen so far for node in workflow.list_node_names(): if node.split('.')[-1].startswith('ds_report'): - workflow.get_node(node).inputs.base_directory = fmriprep_dir + workflow.get_node(node).inputs.base_directory = petprep_dir workflow.get_node(node).inputs.source_file = pet_file return workflow From 36af8950201ac8041b29977078f6d6b168e0abbc Mon Sep 17 00:00:00 2001 From: mnoergaard Date: Sat, 24 May 2025 19:50:14 +0200 Subject: [PATCH 4/9] FIX: Update merge_rois connections in workflow --- fmriprep/workflows/pet/confounds.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fmriprep/workflows/pet/confounds.py b/fmriprep/workflows/pet/confounds.py index 31a0611..102a251 100644 --- a/fmriprep/workflows/pet/confounds.py +++ b/fmriprep/workflows/pet/confounds.py @@ -274,7 +274,7 @@ def init_pet_confs_wf( iterfield=['in_file'], ) merge_rois = pe.Node( - niu.Merge(3, ravel_inputs=True), name='merge_rois', run_without_submitting=True + niu.Merge(4, ravel_inputs=True), name='merge_rois', run_without_submitting=True ) signals = pe.Node( SignalExtraction(class_labels=signals_class_labels), name='signals', mem_gb=mem_gb @@ -380,10 +380,14 @@ def init_pet_confs_wf( ('petref2anat_xfm', 'transforms'), ]), (acompcor_tfm, acompcor_bin, [('output_image', 'in_file')]), + (union_mask, merge_rois, [('out', 'in1')]), (acompcor_bin, merge_rois, [ (('out_mask', _last), 'in3'), (('out_mask', _first), 'in1'), (('out_mask', _second), 'in2'), + (('out_mask', _first), 'in2'), + (('out_mask', _second), 'in3'), + (('out_mask', _last), 'in4'), ]), (merge_rois, signals, [('out', 'label_files')]), From bc1950241e0e241f644d1a9faa28d71dab6b0a5a Mon Sep 17 00:00:00 2001 From: mnoergaard Date: Sat, 24 May 2025 19:52:23 +0200 Subject: [PATCH 5/9] FIX: fix merge_rois connection --- fmriprep/workflows/pet/confounds.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/fmriprep/workflows/pet/confounds.py b/fmriprep/workflows/pet/confounds.py index 102a251..3d754bf 100644 --- a/fmriprep/workflows/pet/confounds.py +++ b/fmriprep/workflows/pet/confounds.py @@ -382,9 +382,6 @@ def init_pet_confs_wf( (acompcor_tfm, acompcor_bin, [('output_image', 'in_file')]), (union_mask, merge_rois, [('out', 'in1')]), (acompcor_bin, merge_rois, [ - (('out_mask', _last), 'in3'), - (('out_mask', _first), 'in1'), - (('out_mask', _second), 'in2'), (('out_mask', _first), 'in2'), (('out_mask', _second), 'in3'), (('out_mask', _last), 'in4'), From 15342703db4a665ccf852c8e903a9fba761dd4ad Mon Sep 17 00:00:00 2001 From: mnoergaard Date: Sat, 24 May 2025 20:41:20 +0200 Subject: [PATCH 6/9] ENH: Add PET report support in fmriprep --- fmriprep/cli/workflow.py | 2 +- fmriprep/data/reports-spec-pet.yml | 14 ++++++++++++ fmriprep/data/reports-spec.yml | 8 +++++++ fmriprep/reports/core.py | 19 +++++++++++++++- fmriprep/reports/tests/test_reports.py | 31 ++++++++++++++++++++++++++ 5 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 fmriprep/data/reports-spec-pet.yml diff --git a/fmriprep/cli/workflow.py b/fmriprep/cli/workflow.py index 310e7fd..5fd1917 100644 --- a/fmriprep/cli/workflow.py +++ b/fmriprep/cli/workflow.py @@ -86,7 +86,7 @@ def build_workflow(config_file, retval): if config.execution.reports_only: build_log.log(25, 'Running --reports-only on participants %s', ', '.join(subject_list)) session_list = ( - config.execution.bids_filters.get('bold', {}).get('session') + config.execution.bids_filters.get('pet', config.execution.bids_filters.get('bold', {})).get('session') if config.execution.bids_filters else None ) diff --git a/fmriprep/data/reports-spec-pet.yml b/fmriprep/data/reports-spec-pet.yml new file mode 100644 index 0000000..87af2fd --- /dev/null +++ b/fmriprep/data/reports-spec-pet.yml @@ -0,0 +1,14 @@ +package: fmriprep +title: PET report for participant '{subject}', session '{session}' - fMRIPrep +sections: +- name: PET + ordering: session + reportlets: + - bids: {datatype: figures, desc: summary, suffix: pet} + - bids: {datatype: figures, desc: validation, suffix: pet} + - bids: {datatype: figures, desc: carpetplot, suffix: pet} + - bids: {datatype: figures, desc: confoundcorr, suffix: pet} + - bids: {datatype: figures, suffix: pet} +- name: About + reportlets: + - bids: {datatype: figures, desc: about, suffix: T1w} diff --git a/fmriprep/data/reports-spec.yml b/fmriprep/data/reports-spec.yml index 89d854b..ce2ff4a 100644 --- a/fmriprep/data/reports-spec.yml +++ b/fmriprep/data/reports-spec.yml @@ -106,6 +106,14 @@ sections: effects and can inform decisions about feature orthogonalization prior to confound regression. subtitle: Correlations among nuisance regressors +- name: PET + ordering: session + reportlets: + - bids: {datatype: figures, desc: summary, suffix: pet} + - bids: {datatype: figures, desc: validation, suffix: pet} + - bids: {datatype: figures, desc: carpetplot, suffix: pet} + - bids: {datatype: figures, desc: confoundcorr, suffix: pet} + - bids: {datatype: figures, suffix: pet} - name: About nested: true reportlets: diff --git a/fmriprep/reports/core.py b/fmriprep/reports/core.py index a7b0419..6898f19 100644 --- a/fmriprep/reports/core.py +++ b/fmriprep/reports/core.py @@ -119,7 +119,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("pet", all_filters.get("bold", {})) session_list = config.execution.layout.get_sessions( subject=subject_label, **filters ) @@ -145,4 +145,21 @@ def generate_reports( if report_error is not None: errors.append(report_error) + bootstrap_file = data.load('reports-spec-pet.yml') + html_report = f'sub-{subject_label}_ses-{session_label}_pet.html' + + report_error = run_reports( + output_dir, + subject_label, + run_uuid, + bootstrap_file=bootstrap_file, + out_filename=html_report, + reportlets_dir=reportlets_dir, + errorname=f'report-{run_uuid}-{subject_label}-pet.err', + subject=subject_label, + session=session_label, + ) + if report_error is not None: + errors.append(report_error) + return errors diff --git a/fmriprep/reports/tests/test_reports.py b/fmriprep/reports/tests/test_reports.py index 8489c28..2d4dc41 100644 --- a/fmriprep/reports/tests/test_reports.py +++ b/fmriprep/reports/tests/test_reports.py @@ -23,6 +23,10 @@ 'sub-001_ses-003_func.html', 'sub-001_ses-004_func.html', 'sub-001_ses-005_func.html', + 'sub-001_ses-001_pet.html', + 'sub-001_ses-003_pet.html', + 'sub-001_ses-004_pet.html', + 'sub-001_ses-005_pet.html', ], ), (4, ['sub-001.html']), @@ -109,3 +113,30 @@ def mock_session_list(*args, **kwargs): assert 'One or more execution steps failed' in html_content, ( f'The file {expected_files[0]} did not contain the reported error.' ) + + +def test_pet_report(tmp_path, monkeypatch): + fake_uuid = 'fake_uuid' + + pet_source = data_dir / 'work/reportlets/fmriprep' + sub_dir = tmp_path / 'sub-01' / 'figures' + sub_dir.mkdir(parents=True) + + shutil.copy2(pet_source / 'sub-001/figures/sub-001_desc-about_T1w.html', sub_dir / 'sub-01_desc-about_T1w.html') + shutil.copy2(pet_source / 'sub-001/figures/sub-001_ses-001_task-qct_dir-LR_part-mag_desc-summary_bold.html', sub_dir / 'sub-01_ses-baseline_desc-summary_pet.html') + shutil.copy2(pet_source / 'sub-001/figures/sub-001_ses-001_task-qct_dir-LR_part-mag_desc-validation_bold.html', sub_dir / 'sub-01_ses-baseline_desc-validation_pet.html') + shutil.copy2(pet_source / 'sub-001/figures/sub-001_ses-001_task-qct_dir-LR_part-mag_desc-carpetplot_bold.svg', sub_dir / 'sub-01_ses-baseline_desc-carpetplot_pet.svg') + shutil.copy2(pet_source / 'sub-001/figures/sub-001_ses-001_task-qct_dir-LR_part-mag_desc-confoundcorr_bold.svg', sub_dir / 'sub-01_ses-baseline_desc-confoundcorr_pet.svg') + shutil.copy2(pet_source / 'sub-01/func/sub-01_task-mixedgamblestask_run-01_bold_bbr.svg', sub_dir / 'sub-01_ses-baseline_pet.svg') + + config.execution.aggr_ses_reports = 4 + config.execution.layout = BIDSLayout(data_dir / 'pet') + monkeypatch.setattr(config.execution, 'bids_filters', {'pet': {'session': ['baseline']}}) + + failed_reports = generate_reports(['01'], tmp_path, fake_uuid) + + assert not failed_reports + html_file = tmp_path / 'sub-01.html' + assert html_file.is_file() + html_content = html_file.read_text() + assert '
Date: Sat, 24 May 2025 20:46:43 +0200 Subject: [PATCH 7/9] ENH: Increase threshold for PET brain mask --- fmriprep/workflows/pet/confounds.py | 22 ++++++++++++++++++++++ fmriprep/workflows/pet/fit.py | 11 +++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/fmriprep/workflows/pet/confounds.py b/fmriprep/workflows/pet/confounds.py index 3d754bf..e8a7350 100644 --- a/fmriprep/workflows/pet/confounds.py +++ b/fmriprep/workflows/pet/confounds.py @@ -579,6 +579,28 @@ def _binary_union(mask1, mask2): return str(out_name) +def _smooth_binarize(in_file, fwhm=10.0, thresh=0.2): + """Smooth ``in_file`` with a Gaussian kernel and binarize at ``thresh``.""" + from pathlib import Path + + import nibabel as nb + import numpy as np + from scipy.ndimage import gaussian_filter + + img = nb.load(in_file) + data = img.get_fdata(dtype=np.float32) + zooms = np.array(img.header.get_zooms()[:3], dtype=float) + sigma = (fwhm / 2.3548) / zooms + smoothed = gaussian_filter(data, sigma=sigma) + mask = smoothed > (thresh * smoothed.max()) + + out_img = img.__class__(mask.astype('uint8'), img.affine, img.header) + out_img.set_data_dtype('uint8') + out_name = Path('smoothed_bin_mask.nii.gz').absolute() + out_img.to_filename(out_name) + return str(out_name) + + def _carpet_parcellation(segmentation, crown_mask, nifti=False): """Generate a segmentation for carpet plot visualization.""" from pathlib import Path diff --git a/fmriprep/workflows/pet/fit.py b/fmriprep/workflows/pet/fit.py index a4dc1fa..20efc3b 100644 --- a/fmriprep/workflows/pet/fit.py +++ b/fmriprep/workflows/pet/fit.py @@ -357,15 +357,18 @@ def init_pet_fit_wf( # Stage 4: Estimate PET brain mask from niworkflows.interfaces.fixes import FixHeaderApplyTransforms as ApplyTransforms - from niworkflows.interfaces.nibabel import Binarize - from .confounds import _binary_union + from .confounds import _binary_union, _smooth_binarize t1w_mask_tfm = pe.Node( ApplyTransforms(interpolation='MultiLabel', invert_transform_flags=[True]), name='t1w_mask_tfm', ) - petref_mask = pe.Node(Binarize(thresh_low=0.2), name='petref_mask') + petref_mask = pe.Node( + niu.Function(function=_smooth_binarize), name='petref_mask' + ) + petref_mask.inputs.fwhm = 10.0 + petref_mask.inputs.thresh = 0.2 merge_mask = pe.Node(niu.Function(function=_binary_union), name='merge_mask') if not petref2anat_xform: @@ -380,7 +383,7 @@ def init_pet_fit_wf( (inputnode, t1w_mask_tfm, [('t1w_mask', 'input_image')]), (petref_buffer, t1w_mask_tfm, [('petref', 'reference_image')]), (petref_buffer, petref_mask, [('petref', 'in_file')]), - (petref_mask, merge_mask, [('out_mask', 'mask1')]), + (petref_mask, merge_mask, [('out', 'mask1')]), (t1w_mask_tfm, merge_mask, [('output_image', 'mask2')]), (merge_mask, outputnode, [('out', 'pet_mask')]), ] From af4d5f4954e71f1fa2a9a7406ff8317144c4f6a6 Mon Sep 17 00:00:00 2001 From: mnoergaard Date: Sat, 24 May 2025 21:12:38 +0200 Subject: [PATCH 8/9] ENH: improve pet mask generation --- fmriprep/workflows/pet/confounds.py | 10 ++++++++-- .../pet/tests/test_smooth_binarize.py | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 fmriprep/workflows/pet/tests/test_smooth_binarize.py diff --git a/fmriprep/workflows/pet/confounds.py b/fmriprep/workflows/pet/confounds.py index e8a7350..72b69b3 100644 --- a/fmriprep/workflows/pet/confounds.py +++ b/fmriprep/workflows/pet/confounds.py @@ -580,12 +580,12 @@ def _binary_union(mask1, mask2): def _smooth_binarize(in_file, fwhm=10.0, thresh=0.2): - """Smooth ``in_file`` with a Gaussian kernel and binarize at ``thresh``.""" + """Smooth ``in_file`` with a Gaussian kernel, binarize and keep largest cluster.""" from pathlib import Path import nibabel as nb import numpy as np - from scipy.ndimage import gaussian_filter + from scipy.ndimage import gaussian_filter, label img = nb.load(in_file) data = img.get_fdata(dtype=np.float32) @@ -594,6 +594,12 @@ def _smooth_binarize(in_file, fwhm=10.0, thresh=0.2): smoothed = gaussian_filter(data, sigma=sigma) mask = smoothed > (thresh * smoothed.max()) + labeled, n_labels = label(mask) + if n_labels > 1: + sizes = np.bincount(labeled.ravel()) + sizes[0] = 0 # ignore background + mask = labeled == sizes.argmax() + out_img = img.__class__(mask.astype('uint8'), img.affine, img.header) out_img.set_data_dtype('uint8') out_name = Path('smoothed_bin_mask.nii.gz').absolute() diff --git a/fmriprep/workflows/pet/tests/test_smooth_binarize.py b/fmriprep/workflows/pet/tests/test_smooth_binarize.py new file mode 100644 index 0000000..1d4073a --- /dev/null +++ b/fmriprep/workflows/pet/tests/test_smooth_binarize.py @@ -0,0 +1,19 @@ +import nibabel as nb +import numpy as np +from scipy.ndimage import label + +from ..confounds import _smooth_binarize + + +def test_smooth_binarize_largest(tmp_path): + data = np.zeros((5, 5, 5)) + data[1:3, 1:3, 1:3] = 1 + data[4, 4, 4] = 1 + img = nb.Nifti1Image(data, np.eye(4)) + src = tmp_path / 'input.nii.gz' + img.to_filename(src) + + out = _smooth_binarize(str(src), fwhm=0.0, thresh=0.5) + result = nb.load(out).get_fdata() + _, num = label(result > 0) + assert num == 1 \ No newline at end of file From bac3cc94f855ba8bffa9b62246641f4c37f113f4 Mon Sep 17 00:00:00 2001 From: mnoergaard Date: Sat, 24 May 2025 21:27:55 +0200 Subject: [PATCH 9/9] FIX: Add PET report support in fmriprep --- fmriprep/data/reports-spec-pet.yml | 2 +- fmriprep/data/reports-spec.yml | 30 +++++++++++++++++++++++++++--- fmriprep/workflows/pet/outputs.py | 3 ++- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/fmriprep/data/reports-spec-pet.yml b/fmriprep/data/reports-spec-pet.yml index 87af2fd..6ca78cf 100644 --- a/fmriprep/data/reports-spec-pet.yml +++ b/fmriprep/data/reports-spec-pet.yml @@ -8,7 +8,7 @@ sections: - bids: {datatype: figures, desc: validation, suffix: pet} - bids: {datatype: figures, desc: carpetplot, suffix: pet} - bids: {datatype: figures, desc: confoundcorr, suffix: pet} - - bids: {datatype: figures, suffix: pet} + - bids: {datatype: figures, desc: coreg, suffix: pet} - name: About reportlets: - bids: {datatype: figures, desc: about, suffix: T1w} diff --git a/fmriprep/data/reports-spec.yml b/fmriprep/data/reports-spec.yml index ce2ff4a..4713372 100644 --- a/fmriprep/data/reports-spec.yml +++ b/fmriprep/data/reports-spec.yml @@ -1,5 +1,5 @@ package: fmriprep -title: Visual report for participant '{subject}' - fMRIPrep +title: Visual report for participant '{subject}' - PETPrep sections: - name: Summary reportlets: @@ -107,13 +107,37 @@ sections: confound regression. subtitle: Correlations among nuisance regressors - name: PET - ordering: session + ordering: session,task,acquisition,ceagent,reconstruction,direction,run reportlets: - bids: {datatype: figures, desc: summary, suffix: pet} + caption: Summary of PET data acquisition parameters and processing workflow overview, including details such as injected dose, radiotracer used, and scan duration. + static: true + subtitle: PET Acquisition and Workflow Summary + - bids: {datatype: figures, desc: validation, suffix: pet} + caption: Validation of PET images against BIDS specifications and initial quality assessment including checks for missing slices, artifacts, and alignment issues. + static: true + subtitle: PET Data Validation + - bids: {datatype: figures, desc: carpetplot, suffix: pet} + caption: | + Summary statistics and global PET signal measures are presented. + A carpet plot displays voxel-level PET tracer uptake over time within the brain mask. Global signals calculated across the whole-brain (GS), white matter (WM), and cerebrospinal fluid (CSF) regions are plotted, along with DVARS and framewise displacement (FD) to visualize potential motion or acquisition artifacts. + "Ctx" = cortex, "Cb" = cerebellum, "WM" = white matter, "CSF" = cerebrospinal fluid. + static: false + subtitle: PET Summary and Carpet Plot + - bids: {datatype: figures, desc: confoundcorr, suffix: pet} - - bids: {datatype: figures, suffix: pet} + caption: | + Left: Correlation heatmap illustrating relationships among PET-derived confound variables (e.g., motion parameters, global signal). + Right: Magnitude of correlation between each PET confound time series and the global PET signal. High correlations suggest potential partial volume effects or motion-induced artifacts, informing subsequent confound regression strategies. + static: false + subtitle: PET Confound Correlation + + - bids: {datatype: figures, desc: coreg, suffix: pet} + caption: PET to anatomical alignment check + static: false + subtitle: Additional PET Visualizations - name: About nested: true reportlets: diff --git a/fmriprep/workflows/pet/outputs.py b/fmriprep/workflows/pet/outputs.py index a14a100..d2c3b51 100644 --- a/fmriprep/workflows/pet/outputs.py +++ b/fmriprep/workflows/pet/outputs.py @@ -307,7 +307,7 @@ def init_func_fit_reports_wf( pet_t1_report = pe.Node( SimpleBeforeAfter( before_label='T1w', - after_label='EPI', + after_label='PET', dismiss_affine=True, ), name='pet_t1_report', @@ -317,6 +317,7 @@ def init_func_fit_reports_wf( ds_pet_t1_report = pe.Node( DerivativesDataSink( base_directory=output_dir, + desc='coreg', suffix='pet', datatype='figures', ),