diff --git a/README.rst b/README.rst index 85e7f70e..3ac66855 100644 --- a/README.rst +++ b/README.rst @@ -29,6 +29,14 @@ software engineering principles: Its reliability is permanently checked and maintained with `CircleCI `_. +PETQC support +------------- +*MRIQC* also provides workflows for quality control of positron emission +tomography (PET) images. When PET data are detected in a BIDS dataset, +the PETQC pipeline runs automatically and generates PET-specific IQMs and +HTML reports. Further details can be found in the +`documentation `__. + Citation -------- .. topic:: **When using MRIQC, please include the following citation:** diff --git a/docs/source/index.rst b/docs/source/index.rst index cbae42c1..0313742c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -18,6 +18,7 @@ Contents measures reports workflows + petqc dsa license changes diff --git a/docs/source/petqc.rst b/docs/source/petqc.rst new file mode 100644 index 00000000..d5ef5c8d --- /dev/null +++ b/docs/source/petqc.rst @@ -0,0 +1,27 @@ +.. _petqc: + +PETQC workflows +*************** + +MRIQC includes experimental support for positron emission tomography (PET) quality +control. When PET data are present in a BIDS dataset, the ``pet`` workflow is +initialized automatically and generates PET-specific image quality metrics. + +Running PETQC +------------- + +Executing PETQC does not require additional command line options beyond the +standard *MRIQC* interface. Simply run:: + + mriqc participant + +MRIQC will detect ``pet`` images and process them along with other modalities. + +Interpreting PETQC outputs +-------------------------- + +Individual HTML reports for each PET run are written to +``/reports``. Summary metrics are collated in +``/pet.csv`` and a group report is produced when the ``group`` level +is executed. These outputs mirror those of the MRI workflows and can be used to +identify artifacts or outlier scans. diff --git a/mriqc/config.py b/mriqc/config.py index 898cf94a..543c7297 100644 --- a/mriqc/config.py +++ b/mriqc/config.py @@ -139,7 +139,7 @@ os.environ['PYTHONWARNINGS'] = 'ignore' -SUPPORTED_SUFFIXES: tuple[str, ...] = ('T1w', 'T2w', 'bold', 'dwi') +SUPPORTED_SUFFIXES: tuple[str, ...] = ('T1w', 'T2w', 'bold', 'dwi', 'pet') DEFAULT_MEMORY_MIN_GB: float = 0.01 DSA_MESSAGE: str = """\ @@ -510,10 +510,10 @@ def init(cls) -> None: # Ignore all modality subfolders, except for func/ or anat/ re.compile( r'^/sub-[a-zA-Z0-9]+(/ses-[a-zA-Z0-9]+)?/' - r'(beh|fmap|pet|perf|meg|eeg|ieeg|micr|nirs)' + r'(beh|fmap|perf|meg|eeg|ieeg|micr|nirs)' ), # Ignore all files, except for the supported modalities - re.compile(r'^.+(?MRIQC may have recorded failure conditions." + title: Errors + - metadata: "input" + settings: + folded: true + id: 'about-metadata' + caption: | + Thanks for using MRIQC. The following information may assist in + reconstructing the provenance of the corresponding derivatives. + title: Reproducibility and provenance information + +plugins: + - module: nireports.assembler + path: data/rating-widget/bootstrap.yml diff --git a/mriqc/data/nipreps.json b/mriqc/data/nipreps.json new file mode 100644 index 00000000..d2f53ec5 --- /dev/null +++ b/mriqc/data/nipreps.json @@ -0,0 +1,193 @@ +{ + "name": "nipreps", + "entities": [ + { + "name": "subject", + "pattern": "[/\\\\]+sub-([a-zA-Z0-9+]+)", + "directory": "{subject}" + }, + { + "name": "session", + "pattern": "[_/\\\\]+ses-([a-zA-Z0-9+]+)", + "mandatory": false, + "directory": "{subject}{session}" + }, + { + "name": "task", + "pattern": "[_/\\\\]+task-([a-zA-Z0-9+]+)" + }, + { + "name": "acquisition", + "pattern": "[_/\\\\]+acq-([a-zA-Z0-9+]+)" + }, + { + "name": "ceagent", + "pattern": "[_/\\\\]+ce-([a-zA-Z0-9+]+)" + }, + { + "name": "reconstruction", + "pattern": "[_/\\\\]+rec-([a-zA-Z0-9+]+)" + }, + { + "name": "direction", + "pattern": "[_/\\\\]+dir-([a-zA-Z0-9+]+)" + }, + { + "name": "run", + "pattern": "[_/\\\\]+run-(\\d+)", + "dtype": "int" + }, + { + "name": "proc", + "pattern": "[_/\\\\]+proc-([a-zA-Z0-9+]+)" + }, + { + "name": "modality", + "pattern": "[_/\\\\]+mod-([a-zA-Z0-9+]+)" + }, + { + "name": "echo", + "pattern": "[_/\\\\]+echo-([0-9]+)" + }, + { + "name": "flip", + "pattern": "[_/\\\\]+flip-([0-9]+)" + }, + { + "name": "inv", + "pattern": "[_/\\\\]+inv-([0-9]+)" + }, + { + "name": "mt", + "pattern": "[_/\\\\]+mt-(on|off)" + }, + { + "name": "part", + "pattern": "[_/\\\\]+part-(mag|phase|real|imag)" + }, + { + "name": "recording", + "pattern": "[_/\\\\]+recording-([a-zA-Z0-9+]+)" + }, + { + "name": "space", + "pattern": "[_/\\\\]+space-([a-zA-Z0-9+]+)" + }, + { + "name": "suffix", + "pattern": "[._]*([a-zA-Z0-9]*?)\\.[^/\\\\]+$" + }, + { + "name": "scans", + "pattern": "(.*\\_scans.tsv)$" + }, + { + "name": "fmap", + "pattern": "(phasediff|magnitude[1-2]|phase[1-2]|fieldmap|epi)\\.nii" + }, + { + "name": "datatype", + "pattern": "[/\\\\]+(func|anat|pet|fmap|dwi|meg|eeg|perf|figures)[/\\\\]+" + }, + { + "name": "extension", + "pattern": "[._]*[a-zA-Z0-9]*?(\\.[^/\\\\]+)$" + }, + { + "name": "atlas", + "pattern": "[_/\\\\]+atlas-([a-zA-Z0-9+]+)" + }, + { + "name": "roi", + "pattern": "[_/\\\\]+roi-([a-zA-Z0-9+]+)" + }, + { + "name": "label", + "pattern": "[_/\\\\]+label-([a-zA-Z0-9+]+)" + }, + { + "name": "fmapid", + "pattern": "[_/\\\\]+fmapid-([a-zA-Z0-9+]+)" + }, + { + "name": "desc", + "pattern": "[_/\\\\]+desc-([a-zA-Z0-9+]+)" + }, + { + "name": "from", + "pattern": "(?:^|_)from-([a-zA-Z0-9+]+).*xfm" + }, + { + "name": "to", + "pattern": "(?:^|_)to-([a-zA-Z0-9+]+).*xfm" + }, + { + "name": "mode", + "pattern": "(?:^|_)mode-(image|points).*xfm" + }, + { + "name": "hemi", + "pattern": "hemi-(L|R)" + }, + { + "name": "model", + "pattern": "model-([a-zA-Z0-9+]+)" + }, + { + "name": "subset", + "pattern": "subset-([a-zA-Z0-9+]+)" + }, + { + "name": "resolution", + "pattern": "res-([a-zA-Z0-9+]+)" + }, + { + "name": "density", + "pattern": "res-([a-zA-Z0-9+]+)" + }, + { + "name": "cohort", + "pattern": "[_/\\\\]+cohort-0*(\\d+)", + "dtype": "int" + } + ], + "default_path_patterns": [ + "sub-{subject}[/ses-{session}]/{datatype|anat}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_res-{resolution}][_desc-{desc}]_{suffix}{extension<.nii|.nii.gz|.json>|.nii.gz}", + "sub-{subject}[/ses-{session}]/{datatype|anat}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}][_hemi-{hemi}]_from-{from}_to-{to}_mode-{mode|image}_{suffix|xfm}{extension<.txt|.h5>}", + "sub-{subject}[/ses-{session}]/{datatype|anat}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}]_hemi-{hemi}[_space-{space}][_cohort-{cohort}][_den-{density}][_desc-{desc}]_{suffix}{extension<.surf.gii|.shape.gii>}", + "sub-{subject}[/ses-{session}]/{datatype|anat}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_den-{density}][_desc-{desc}]_{suffix}{extension<.dscalar.nii|.json>}", + "sub-{subject}[/ses-{session}]/{datatype|anat}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_res-{resolution}]_desc-{desc}_{suffix|mask}{extension<.nii|.nii.gz|.json>|.nii.gz}", + "sub-{subject}[/ses-{session}]/{datatype|anat}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_res-{resolution}]_label-{label}[_desc-{desc}]_{suffix|probseg}{extension<.nii|.nii.gz|.json>|.nii.gz}", + "sub-{subject}[/ses-{session}]/{datatype|func}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_echo-{echo}][_part-{part}][_space-{space}][_cohort-{cohort}][_res-{resolution}][_desc-{desc}]_{suffix}{extension<.nii|.nii.gz|.json>|.nii.gz}", + "sub-{subject}[/ses-{session}]/{datatype|func}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_echo-{echo}][_space-{space}][_cohort-{cohort}][_res-{resolution}][_desc-{desc}]_{suffix}{extension<.nii|.nii.gz|.json>|.nii.gz}", + "sub-{subject}[/ses-{session}]/{datatype|func}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_hemi-{hemi}]_from-{from}_to-{to}_mode-{mode|image}[_desc-{desc}]_{suffix|xfm}{extension<.txt|.h5>}", + "sub-{subject}[/ses-{session}]/{datatype|func}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_echo-{echo}][_part-{part}][_space-{space}][_cohort-{cohort}][_res-{resolution}]_desc-{desc}_{suffix|mask}{extension<.nii|.nii.gz|.json>|.nii.gz}", + "sub-{subject}[/ses-{session}]/{datatype|func}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_echo-{echo}][_part-{part}][_space-{space}][_cohort-{cohort}][_desc-{desc}]_{suffix|AROMAnoiseICs}{extension<.csv|.tsv>|.csv}", + "sub-{subject}[/ses-{session}]/{datatype|func}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_echo-{echo}][_part-{part}][_space-{space}][_cohort-{cohort}][_desc-{desc}]_{suffix|timeseries}{extension<.json|.tsv>|.tsv}", + "sub-{subject}[/ses-{session}]/{datatype|func}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_echo-{echo}][_part-{part}][_space-{space}][_cohort-{cohort}][_desc-{desc}]_{suffix|components}{extension<.json|.tsv|.nii|.nii.gz>|.tsv}", + "sub-{subject}[/ses-{session}]/{datatype|func}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_echo-{echo}][_part-{part}][_space-{space}][_cohort-{cohort}][_desc-{desc}]_{suffix|decomposition}{extension<.json>|.json}", + "sub-{subject}[/ses-{session}]/{datatype|func}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_echo-{echo}][_hemi-{hemi}][_space-{space}][_cohort-{cohort}][_den-{density}][_desc-{desc}]_{suffix}{extension<.dtseries.nii|.dtseries.json|.func.gii|.func.json>}", + "sub-{subject}[/ses-{session}]/{datatype}/sub-{subject}[_ses-{session}][_task-{task}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_space-{space}][_cohort-{cohort}][_res-{resolution}][_desc-{desc}]_{suffix}{extension<.nii|.nii.gz|.json>|.nii.gz}", + "sub-{subject}[/ses-{session}]/{datatype|dwi}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_space-{space}][_cohort-{cohort}][_res-{resolution}][_desc-{desc}]_{suffix}{extension<.json|.nii.gz|.nii>|.nii.gz}", + "sub-{subject}[/ses-{session}]/{datatype|dwi}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_space-{space}][_cohort-{cohort}][_res-{resolution}]_desc-{desc}_{suffix}{extension<.json|.nii.gz|.nii>|.nii.gz}", + "sub-{subject}[/ses-{session}]/{datatype|dwi}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_space-{space}][_cohort-{cohort}][_res-{resolution}][_desc-{desc}]_{suffix}{extension<.tsv|.bval|.bvec|.b>|.tsv}", + "sub-{subject}[/ses-{session}]/{datatype|dwi}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_rec-{reconstruction}][_dir-{direction}][_run-{run}]_from-{from}_to-{to}_mode-{mode|image}[_desc-{desc}]_{suffix|xfm}{extension<.txt|.h5>}", + "sub-{subject}[/ses-{session}]/{datatype|perf}/sub-{subject}[_ses-{session}][_task-{task}][_acq-{acquisition}][_rec-{reconstruction}][_dir-{direction}][_run-{run}]_{suffix}{extension<.tsv|.json>|.tsv}", + "sub-{subject}[/ses-{session}]/{datatype|perf}/sub-{subject}[_ses-{session}][_task-{task}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}]_from-{from}_to-{to}_mode-{mode|image}_{suffix|xfm}{extension<.txt|.h5>}", + "sub-{subject}[/ses-{session}]/{datatype|perf}/sub-{subject}[_ses-{session}][_task-{task}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_space-{space}][_atlas-{atlas}][_cohort-{cohort}][_desc-{desc}]_{suffix}{extension<.json|.tsv>|.tsv}", + "sub-{subject}[/ses-{session}]/{datatype|perf}/sub-{subject}[_ses-{session}][_task-{task}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_space-{space}][_atlas-{atlas}][_cohort-{cohort}][_desc-{desc}]_{suffix}{extension<.nii|.nii.gz|.json|.tsv>|.tsv}", + "sub-{subject}[/ses-{session}]/{datatype|fmap}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_dir-{direction}][_run-{run}][_part-{part}][_space-{space}][_cohort-{cohort}][_res-{resolution}][_fmapid-{fmapid}][_desc-{desc}]_{suffix}{extension<.nii|.nii.gz|.json>|.nii.gz}", + "sub-{subject}[/ses-{session}]/{datatype|pet}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_res-{resolution}][_desc-{desc}]_{suffix}{extension<.nii|.nii.gz|.json>|.nii.gz}", + "sub-{subject}[/ses-{session}]/{datatype|pet}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}][_hemi-{hemi}]_from-{from}_to-{to}_mode-{mode|image}_{suffix|xfm}{extension<.txt|.h5>}", + "sub-{subject}[/ses-{session}]/{datatype|pet}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}]_hemi-{hemi}[_space-{space}][_cohort-{cohort}][_den-{density}][_desc-{desc}]_{suffix}{extension<.surf.gii|.shape.gii>}", + "sub-{subject}[/ses-{session}]/{datatype|pet}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_den-{density}][_desc-{desc}]_{suffix}{extension<.dscalar.nii|.json>}", + "sub-{subject}[/ses-{session}]/{datatype|pet}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_res-{resolution}]_desc-{desc}_{suffix|mask}{extension<.nii|.nii.gz|.json>|.nii.gz}", + "sub-{subject}[/ses-{session}]/{datatype|pet}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_res-{resolution}]_label-{label}[_desc-{desc}]_{suffix|probseg}{extension<.nii|.nii.gz|.json>|.nii.gz}", + "sub-{subject}[/ses-{session}]/{datatype|pet}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_part-{part}][_space-{space}][_atlas-{atlas}][_cohort-{cohort}][_desc-{desc}]_{suffix|timeseries}{extension<.json|.tsv>|.tsv}", + "sub-{subject}/{datatype}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_desc-{desc}]_{suffix}{extension<.html|.svg>|.svg}", + "sub-{subject}/{datatype}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_fmapid-{fmapid}][_desc-{desc}]_{suffix}{extension<.html|.svg>|.svg}", + "sub-{subject}/{datatype}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_echo-{echo}][_part-{part}][_space-{space}][_cohort-{cohort}][_desc-{desc}]_{suffix}{extension<.html|.svg>|.svg}", + "sub-{subject}/{datatype}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_dir-{direction}][_run-{run}][_echo-{echo}][_part-{part}][_space-{space}][_cohort-{cohort}][_desc-{desc}]_{suffix}{extension<.html|.svg>|.svg}", + "sub-{subject}/{datatype}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_desc-{desc}]_{suffix}{extension<.html|.svg|.png>|.html}" + ] +} diff --git a/mriqc/data/pet/dataset_description.json b/mriqc/data/pet/dataset_description.json new file mode 100644 index 00000000..27243733 --- /dev/null +++ b/mriqc/data/pet/dataset_description.json @@ -0,0 +1,11 @@ +{ + "Name": "PETPrep HMC workflow", + "DatasetType": "derivative", + "BIDSVersion": "1.7.0", + "GeneratedBy": [ + { + "Name": "petprep_hmc", + "Version": "0.0.10" + } + ] +} \ No newline at end of file diff --git a/mriqc/data/pet/derivatives/mriqc/.bids_db/layout_index.sqlite b/mriqc/data/pet/derivatives/mriqc/.bids_db/layout_index.sqlite new file mode 100644 index 00000000..a0d5e7a6 Binary files /dev/null and b/mriqc/data/pet/derivatives/mriqc/.bids_db/layout_index.sqlite differ diff --git a/mriqc/data/pet/derivatives/mriqc/logs/config-20250509-150839_62d91c5f-fc2a-4ed5-87b2-8c474d5e033e.toml b/mriqc/data/pet/derivatives/mriqc/logs/config-20250509-150839_62d91c5f-fc2a-4ed5-87b2-8c474d5e033e.toml new file mode 100644 index 00000000..7b2ba845 --- /dev/null +++ b/mriqc/data/pet/derivatives/mriqc/logs/config-20250509-150839_62d91c5f-fc2a-4ed5-87b2-8c474d5e033e.toml @@ -0,0 +1,81 @@ +[environment] +cache_path = "PosixPath('/Users/martinnorgaard/.cache/mriqc')" +cpu_count = 10 +exec_env = "posix" +freesurfer_home = "/Applications/freesurfer/7.4.1" +overcommit_policy = "n/a" +overcommit_limit = "n/a" +nipype_version = "1.10.0" +synthstrip_path = "PosixPath('/Applications/freesurfer/7.4.1/models/synthstrip.1.pt')" +templateflow_version = "24.2.2" +total_memory = 32.0 +version = "25.1.0.dev49+g1b92a165.d20250509" + +[execution] +ants_float = false +bids_dir = "/Users/martinnorgaard/Documents/GitHub/mriqc/mriqc/data/pet" +bids_dir_datalad = false +bids_database_dir = "/Users/martinnorgaard/Documents/GitHub/mriqc/mriqc/data/pet/derivatives/mriqc/.bids_db" +bids_database_wipe = false +cwd = "/Users/martinnorgaard/Dropbox/Mac/Documents/GitHub/mriqc" +datalad_get = true +debug = false +dry_run = false +dsname = "" +float32 = true +layout = "BIDS Layout: /Users/martinnorgaard/Documents/GitHub/mriqc/mriqc/data/pet" +log_dir = "/Users/martinnorgaard/Documents/GitHub/mriqc/mriqc/data/pet/derivatives/mriqc/logs" +log_level = 25 +modalities = [ "T1w", "T2w", "bold", "dwi", "pet",] +no_sub = true +notrack = false +output_dir = "/Users/martinnorgaard/Documents/GitHub/mriqc/mriqc/data/pet/derivatives/mriqc" +participant_label = [ "01",] +pdb = false +reports_only = false +resource_monitor = false +run_uuid = "20250509-150839_62d91c5f-fc2a-4ed5-87b2-8c474d5e033e" +templateflow_home = "/Users/martinnorgaard/.cache/templateflow" +upload_strict = false +verbose_reports = false +webapi_url = "https://mriqc.nimh.nih.gov:443/api/v1" +work_dir = "/Users/martinnorgaard/Dropbox/Mac/Documents/GitHub/mriqc/work" +write_graph = false + +[workflow] +analysis_level = [ "participant",] +deoblique = false +despike = false +fd_thres = 0.2 +fd_radius = 50 +fft_spikes_detector = false +inputs_path = "PosixPath('/Users/martinnorgaard/Dropbox/Mac/Documents/GitHub/mriqc/work/inputs-20250509-150839_62d91c5f-fc2a-4ed5-87b2-8c474d5e033e.pkl')" +min_len_dwi = 7 +min_len_bold = 5 +species = "human" +template_id = "MNI152NLin2009cAsym" + +[nipype] +crashfile_format = "txt" +get_linked_libs = false +local_hash_check = true +nprocs = 10 +omp_nthreads = 10 +plugin = "MultiProc" +remove_node_directories = false +resource_monitor = false +stop_on_first_crash = true + +[settings] +file_path = "/Users/martinnorgaard/Documents/GitHub/mriqc/mriqc/data/pet/derivatives/mriqc/logs/config-20250509-150839_62d91c5f-fc2a-4ed5-87b2-8c474d5e033e.toml" +start_time = 1746796119.322388 + +[execution.bids_filters] + +[workflow.biggest_file_gb] +pet = 0.04553897213190794 + +[nipype.plugin_args] +maxtasksperchild = 1 +raise_insufficient = false +n_procs = 10 diff --git a/mriqc/data/pet/derivatives/petprep_hmc/dataset_description.json b/mriqc/data/pet/derivatives/petprep_hmc/dataset_description.json new file mode 100644 index 00000000..27243733 --- /dev/null +++ b/mriqc/data/pet/derivatives/petprep_hmc/dataset_description.json @@ -0,0 +1,11 @@ +{ + "Name": "PETPrep HMC workflow", + "DatasetType": "derivative", + "BIDSVersion": "1.7.0", + "GeneratedBy": [ + { + "Name": "petprep_hmc", + "Version": "0.0.10" + } + ] +} \ No newline at end of file diff --git a/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-confounds_timeseries.tsv b/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-confounds_timeseries.tsv new file mode 100644 index 00000000..f9299b4a --- /dev/null +++ b/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-confounds_timeseries.tsv @@ -0,0 +1,22 @@ + trans_x trans_y trans_z rot_x rot_y rot_z max_x max_y max_z max_tot median_tot +0 0.042524 0.033254 0.201559 0.005093 0.002339 0.001308 0.13510399999999834 0.39417200000000463 0.31259900000000584 0.4144521632577178 0.24044795261758672 +1 0.042524 0.033254 0.201559 0.005093 0.002339 0.001308 0.13510399999999834 0.39417200000000463 0.31259900000000584 0.4144521632577178 0.24044795261758672 +2 0.042524 0.033254 0.201559 0.005093 0.002339 0.001308 0.13510399999999834 0.39417200000000463 0.31259900000000584 0.4144521632577178 0.24044795261758672 +3 0.042524 0.033254 0.201559 0.005093 0.002339 0.001308 0.13510399999999834 0.39417200000000463 0.31259900000000584 0.4144521632577178 0.24044795261758672 +4 0.062664 0.167271 -0.256301 0.00418 0.003669 0.002652 0.2502160000000089 0.388264999999997 0.5241060000000033 0.6028335918327759 0.3463396698610647 +5 0.168891 0.343776 -0.780523 0.000624 0.003962 0.001453 0.2699439999999953 0.3434820000000016 0.7194990000000061 0.799197440563345 0.63273720463041 +6 0.106591 0.272566 -0.564354 0.000642 0.002471 0.000904 0.1644629999999978 0.2901329999999973 0.5417169999999984 0.6110877928898555 0.5080529114299173 +7 0.13754 0.27817 -0.548284 0.001489 0.003523 0.00127 0.21731200000000683 0.34546000000000276 0.5598179999999999 0.6425998955306523 0.4926435889280838 +8 0.174567 0.222818 -0.554918 0.001433 0.003675 0.001076 0.23878399999999544 0.2927350000000004 0.5558189999999996 0.618726189759256 0.465822407279853 +9 0.147141 0.287726 -0.520696 0.000968 0.00332 0.001259 0.21775800000000345 0.31749599999999845 0.4967269999999999 0.5849997671324294 0.44919262281229305 +10 0.135763 0.294311 -0.862506 -0.002533 0.003885 0.001593 0.23634099999999592 0.20630599999999788 0.6611520000000013 0.6748301069350773 0.4783911898636546 +11 0.133987 0.316514 -0.895106 -0.004445 0.003294 0.001094 0.20574100000000328 0.2536970000000025 0.6721299999999957 0.6796346486296888 0.4418570389321888 +12 0.128307 0.301193 -0.836072 -0.004811 0.003721 0.001356 0.21856700000000728 0.22622900000000357 0.5877809999999997 0.5904814613203331 0.3421570358811808 +13 0.092678 0.187613 -0.553462 -0.003839 0.003561 0.00176 0.21528600000000608 0.2772340000000071 0.3308460000000011 0.3442636194894741 0.1966820596826297 +14 -0.041942 0.116471 -0.263375 -0.003876 0.00214 0.001622 0.15661199999999553 0.3395190000000028 0.3210550000000012 0.4017416189953524 0.23365494368620213 +15 0.004062 -0.070845 -0.16049 -0.004344 0.001881 0.00033 0.14010300000000342 0.46297400000000266 0.45114199999999727 0.5742322447163773 0.389676632863509 +16 0.007914 -0.104361 -0.187322 -0.005488 0.001501 -4.3e-05 0.1222270000000023 0.5696659999999909 0.5147029999999972 0.6719007325111281 0.4528683969156977 +17 -0.076009 -0.128338 0.00757 -0.005087 0.001424 0.000657 0.16710299999999734 0.6089350000000024 0.6598400000000026 0.8121806209144716 0.5925741595733051 +18 -0.073613 -0.212594 0.01942 -0.004817 0.001732 0.000646 0.19015100000000018 0.6689420000000084 0.667942999999994 0.8669613148820281 0.6566910918807108 +19 -0.019034 -0.430565 0.448751 -0.002102 0.001828 -0.000195 0.1856509999999929 0.5980779999999939 0.8220900000000029 0.9870592088841462 0.8726034584787558 +20 -0.016395 -0.801118 0.663921 -0.004665 -0.003338 -0.003624 0.28188299999999344 1.0198799999999935 0.9927750000000017 1.3441046995145112 1.0747232794021842 diff --git a/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-mc_pet.json b/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-mc_pet.json new file mode 100644 index 00000000..dc470eb1 --- /dev/null +++ b/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-mc_pet.json @@ -0,0 +1,13 @@ +{ + "Description": "Motion-corrected PET file", + "Sources": "/Users/martinnorgaard/Downloads/eddymotion_pet_testdata/data/sub-01/ses-baseline/pet/sub-01_ses-baseline_pet.nii.gz", + "ReferenceImage": "Robust template using mri_robust_register", + "CostFunction": "ROB", + "MCTreshold": "20", + "MCFWHM": "10", + "MCStartTime": "60", + "QC": "", + "SoftwareName": "PETPrep HMC workflow", + "SoftwareVersion": "0.0.10", + "CommandLine": "run.py /Users/martinnorgaard/Downloads/eddymotion_pet_testdata/data/ /Users/martinnorgaard/Downloads/eddymotion_pet_testdata/data/derivatives/petprep_hmc participant --n_procs 6 --mc_start_time 60" +} \ No newline at end of file diff --git a/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-mc_pet.nii.gz b/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-mc_pet.nii.gz new file mode 100644 index 00000000..6c31473f Binary files /dev/null and b/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-mc_pet.nii.gz differ diff --git a/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-movement.png b/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-movement.png new file mode 100644 index 00000000..1bfcc8c4 Binary files /dev/null and b/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-movement.png differ diff --git a/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-rotation.png b/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-rotation.png new file mode 100644 index 00000000..655cc047 Binary files /dev/null and b/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-rotation.png differ diff --git a/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-translation.png b/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-translation.png new file mode 100644 index 00000000..6ac296ed Binary files /dev/null and b/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-translation.png differ diff --git a/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-with_motion_correction.gif b/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-with_motion_correction.gif new file mode 100644 index 00000000..0dce3df1 Binary files /dev/null and b/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-with_motion_correction.gif differ diff --git a/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-without_motion_correction.gif b/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-without_motion_correction.gif new file mode 100644 index 00000000..60d43976 Binary files /dev/null and b/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_desc-without_motion_correction.gif differ diff --git a/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_report.html b/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_report.html new file mode 100644 index 00000000..323317e9 --- /dev/null +++ b/mriqc/data/pet/derivatives/petprep_hmc/sub-01/ses-baseline/sub-01_ses-baseline_report.html @@ -0,0 +1,6 @@ +Motion Correction Report

PET with Motion Correction

PET scan with motion correction applied. This shows the stabilized sequence. +


PET without Motion Correction

Original PET scan without any motion correction. Notice the movements affecting the quality. +


Movement Metrics

Overall movement metrics over time. +


Rotation Metrics

Rotation components of motion correction. +


Translation Metrics

Translation components of motion correction. +


\ No newline at end of file diff --git a/mriqc/data/pet/sub-01/ses-baseline/anat/sub-01_ses-baseline_T1w.nii b/mriqc/data/pet/sub-01/ses-baseline/anat/sub-01_ses-baseline_T1w.nii new file mode 100644 index 00000000..c2b983de Binary files /dev/null and b/mriqc/data/pet/sub-01/ses-baseline/anat/sub-01_ses-baseline_T1w.nii differ diff --git a/mriqc/data/pet/sub-01/ses-baseline/pet/.Rhistory b/mriqc/data/pet/sub-01/ses-baseline/pet/.Rhistory new file mode 100644 index 00000000..e69de29b diff --git a/mriqc/data/pet/sub-01/ses-baseline/pet/.nfs000000073462970e000001f1 b/mriqc/data/pet/sub-01/ses-baseline/pet/.nfs000000073462970e000001f1 new file mode 100644 index 00000000..25358cd9 Binary files /dev/null and b/mriqc/data/pet/sub-01/ses-baseline/pet/.nfs000000073462970e000001f1 differ diff --git a/mriqc/data/pet/sub-01/ses-baseline/pet/sub-01_ses-baseline_pet.json b/mriqc/data/pet/sub-01/ses-baseline/pet/sub-01_ses-baseline_pet.json new file mode 100644 index 00000000..c69fbb7d --- /dev/null +++ b/mriqc/data/pet/sub-01/ses-baseline/pet/sub-01_ses-baseline_pet.json @@ -0,0 +1,235 @@ +{ + "Manufacturer": "Siemens", + "ManufacturersModelName": "HR+", + "Units": "Bq/mL", + "BodyPart": "Brain", + "TracerName": "DASB", + "TracerRadionuclide": "C11", + "TracerMolecularWeight": 282.39, + "TracerMolecularWeightUnits": "g/mol", + "InjectedRadioactivity": 629.74, + "InjectedRadioactivityUnits": "MBq", + "MolarActivity": 55.5, + "MolarActivityUnits": "MBq/nmol", + "SpecificRadioactivity": 196.53670455752683, + "SpecificRadioactivityUnits": "MBq/ug", + "Purity": 99, + "ModeOfAdministration": "bolus", + "InjectedMass": 3.2041852, + "InjectedMassUnits": "ug", + "AcquisitionMode": "list mode", + "ImageDecayCorrected": true, + "ImageDecayCorrectionTime": 0, + "TimeZero": "17:28:40", + "ScanStart": 0, + "InjectionStart": 0, + "FrameDuration": [ + 20, + 20, + 20, + 60, + 60, + 60, + 120, + 120, + 120, + 300, + 300.066, + 600, + 600, + 600, + 600, + 600, + 600, + 600, + 600, + 600, + 600 + ], + "FrameTimesStart": [ + 0, + 20, + 40, + 60, + 120, + 180, + 240, + 360, + 480, + 600, + 900, + 1200.066, + 1800.066, + 2400.066, + 3000.066, + 3600.066, + 4200.066, + 4800.066, + 5400.066, + 6000.066, + 6600.066 + ], + "ReconMethodParameterLabels": [ + "lower_threshold", + "upper_threshold", + "recon_zoom" + ], + "ReconMethodParameterUnits": [ + "keV", + "keV", + "none" + ], + "ReconMethodParameterValues": [ + 0, + 650, + 3 + ], + "ScaleFactor": [ + 8.548972374455843e-08, + 1.7544691388593492e-07, + 1.3221580275057931e-07, + 1.2703590357432404e-07, + 1.1155360368775291e-07, + 2.2050951997698576e-07, + 2.184752503353593e-07, + 1.7056818535365892e-07, + 1.6606901453997125e-07, + 1.5532630470715958e-07, + 2.19175134930083e-07, + 2.0248222654117853e-07, + 2.277063231304055e-07, + 2.425933018912474e-07, + 2.3802238047210267e-07, + 2.514642005735368e-07, + 2.802861729378492e-07, + 2.797820570776821e-07, + 3.5299004252919985e-07, + 4.6313422785715375e-07, + 4.904185857412813e-07 + ], + "ScatterFraction": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "DecayCorrectionFactor": [ + 1.0056782960891724, + 1.0171427726745605, + 1.0287377834320068, + 1.0522810220718384, + 1.0886797904968262, + 1.1263376474380493, + 1.1851094961166382, + 1.2685142755508423, + 1.3577889204025269, + 1.5278561115264893, + 1.811025857925415, + 2.328737735748291, + 3.271937131881714, + 4.597157001495361, + 6.459125518798828, + 9.075239181518555, + 12.750947952270508, + 17.915414810180664, + 25.1716251373291, + 35.36678695678711, + 49.69125747680664 + ], + "PromptRate": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "RandomRate": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "SinglesRate": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "ReconMethodName": "Vendor", + "ReconFilterType": [ + "Shepp 0.5", + "All-pass 0.4" + ], + "ReconFilterSize": [ + 2.5, + 2 + ], + "AttenuationCorrection": "transmission scan" +} diff --git a/mriqc/data/pet/sub-01/ses-baseline/pet/sub-01_ses-baseline_pet.nii.gz b/mriqc/data/pet/sub-01/ses-baseline/pet/sub-01_ses-baseline_pet.nii.gz new file mode 100644 index 00000000..b4154aee Binary files /dev/null and b/mriqc/data/pet/sub-01/ses-baseline/pet/sub-01_ses-baseline_pet.nii.gz differ diff --git a/mriqc/data/pet/sub-01/ses-baseline/pet/sub-01_ses-baseline_recording-manual_blood.json b/mriqc/data/pet/sub-01/ses-baseline/pet/sub-01_ses-baseline_recording-manual_blood.json new file mode 100644 index 00000000..82ef0c2f --- /dev/null +++ b/mriqc/data/pet/sub-01/ses-baseline/pet/sub-01_ses-baseline_recording-manual_blood.json @@ -0,0 +1,22 @@ +{ + "PlasmaAvail": true, + "PlasmaFreeFraction": 15.062331, + "PlasmaFreeFractionMethod": "Ultrafiltration", + "WholeBloodAvail": false, + "DispersionCorrected": false, + "MetaboliteAvail": true, + "MetaboliteMethod": "HPLC", + "MetaboliteRecoveryCorrectionApplied": false, + "time": { + "Description": "Time in relation to time zero defined by the _pet.json", + "Units": "s" + }, + "plasma_radioactivity": { + "Description": "Radioactivity in plasma samples", + "Units": "Bq/mL" + }, + "metabolite_parent_fraction": { + "Description": "Parent fraction of the radiotracer", + "Units": "arbitrary" + } +} diff --git a/mriqc/data/pet/sub-01/ses-baseline/pet/sub-01_ses-baseline_recording-manual_blood.tsv b/mriqc/data/pet/sub-01/ses-baseline/pet/sub-01_ses-baseline_recording-manual_blood.tsv new file mode 100644 index 00000000..50b8d66a --- /dev/null +++ b/mriqc/data/pet/sub-01/ses-baseline/pet/sub-01_ses-baseline_recording-manual_blood.tsv @@ -0,0 +1,33 @@ +time plasma_radioactivity metabolite_parent_fraction +0 0 0 +10.0000002 19.30438 n/a +19.9999998 11.41598 n/a +30 -0.8288 n/a +40.002 187.60369 n/a +49.998 4788.811580000001 n/a +60 12365.39704 n/a +70.00200000000001 16107.85861 n/a +79.99799999999999 18571.96204 n/a +90 16183.6483 n/a +100.002 12361.91645 n/a +109.998 10091.18575 n/a +120 9192.42208 0.50342533 +139.998 7552.26055 n/a +160.002 7151.83101 n/a +180 7033.1857 n/a +199.998 6608.60293 n/a +220.002 6162.81509 n/a +240 5882.2452 n/a +360 5416.896940000001 n/a +480 5278.37153 n/a +720 4343.44517 0.62502916 +960 4217.90158 n/a +1200 4192.49812 0.52976961 +1800 4350.59875 n/a +2400 4427.88805 n/a +3300 4470.98528 0.19456334 +3600 4215.10401 n/a +4800 4591.47578 0.1376665 +5400 4271.10425 n/a +6000 4406.48947 0.13406937 +7200 3905.08249 n/a diff --git a/mriqc/interfaces/__init__.py b/mriqc/interfaces/__init__.py index 847c6647..144b8130 100644 --- a/mriqc/interfaces/__init__.py +++ b/mriqc/interfaces/__init__.py @@ -35,6 +35,7 @@ from mriqc.interfaces.common import ConformImage, EnsureSize from mriqc.interfaces.functional import FunctionalQC, GatherTimeseries, Spikes from mriqc.interfaces.webapi import UploadIQMs +from mriqc.interfaces.pet import ChooseRefHMC, FDStats class DerivativesDataSink(_DDSink): @@ -55,4 +56,6 @@ class DerivativesDataSink(_DDSink): 'Spikes', 'StructuralQC', 'UploadIQMs', + 'ChooseRefHMC', + 'FDStats', ] diff --git a/mriqc/interfaces/pet.py b/mriqc/interfaces/pet.py new file mode 100644 index 00000000..4bb93159 --- /dev/null +++ b/mriqc/interfaces/pet.py @@ -0,0 +1,107 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +# +# Copyright 2021 The NiPreps Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +"""Interfaces for basic PET QC operations.""" + +import os + +import nibabel as nb +import numpy as np +from nipype.interfaces.base import ( + BaseInterfaceInputSpec, + File, + SimpleInterface, + TraitedSpec, + traits, +) + + +class _ChooseRefHMCInputSpec(BaseInterfaceInputSpec): + in_file = File(exists=True, mandatory=True, desc='Input PET image file') + + +class _ChooseRefHMCOutputSpec(TraitedSpec): + out_file = File(exists=True, desc='Output file with the selected reference frame') + ref_frame = traits.Int(desc='index of selected frame') + + +class ChooseRefHMC(SimpleInterface): + """Select the frame with the highest global intensity as reference.""" + input_spec = _ChooseRefHMCInputSpec + output_spec = _ChooseRefHMCOutputSpec + + def _run_interface(self, runtime): + in_file = self.inputs.in_file + + # Load the PET image + img = nb.load(in_file) + data = img.get_fdata() + + # Compute the sum of intensities across voxels for each time frame + frame_sums = np.sum(data, axis=(0, 1, 2)) + + # Find the time frame with the highest sum of intensity + max_frame_idx = np.argmax(frame_sums) + + # Extract the corresponding frame + max_frame_data = data[..., max_frame_idx] + + # Create a new NIfTI image for the selected frame + max_frame_img = nb.Nifti1Image(max_frame_data, img.affine, img.header) + + # Save the new NIfTI image + output_filename = os.path.abspath('max_intensity_frame.nii.gz') + nb.save(max_frame_img, output_filename) + + self._results['out_file'] = output_filename + self._results['ref_frame'] = int(max_frame_idx) + return runtime + + +class _FDStatsInputSpec(BaseInterfaceInputSpec): + in_fd = File(exists=True, mandatory=True, desc='Input FD file') + fd_thres = traits.Float(0.2, usedefault=True, desc='motion threshold for FD computation') + + +class _FDStatsOutputSpec(TraitedSpec): + out_fd = traits.Dict(desc='Dictionary with FD metrics: mean, num, perc') + + +class FDStats(SimpleInterface): + """Compute summary statistics for framewise displacement.""" + input_spec = _FDStatsInputSpec + output_spec = _FDStatsOutputSpec + + def _run_interface(self, runtime): + # Load FD data + fd_data = np.loadtxt(self.inputs.in_fd, skiprows=1) + + # Compute number of FD values above the threshold + num_fd = (fd_data > self.inputs.fd_thres).sum() + + # Store results in the output dictionary + self._results['out_fd'] = { + 'mean': float(fd_data.mean()), + 'num': int(num_fd), + 'perc': float(num_fd * 100 / (len(fd_data) + 1)), + } + + return runtime diff --git a/mriqc/interfaces/webapi.py b/mriqc/interfaces/webapi.py index ace2942f..d5f9102a 100644 --- a/mriqc/interfaces/webapi.py +++ b/mriqc/interfaces/webapi.py @@ -242,7 +242,7 @@ def upload_qc_metrics( # Check modality modality = meta.get('modality', None) or meta.get('suffix', None) or modality - if modality not in ('T1w', 'bold', 'T2w'): + if modality not in ('T1w', 'bold', 'T2w','pet'): errmsg = ( 'Submitting to MRIQCWebAPI: image modality should be "bold", "T1w", or "T2w", ' f'(found "{modality}")' diff --git a/mriqc/qc/pet.py b/mriqc/qc/pet.py new file mode 100644 index 00000000..9291d904 --- /dev/null +++ b/mriqc/qc/pet.py @@ -0,0 +1,401 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +# +# Copyright 2021 The NiPreps Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +"""Plotting utilities used in PET quality control.""" + +import os +import re +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import seaborn as sns +from nipype.interfaces.base import ( + BaseInterfaceInputSpec, + File, + SimpleInterface, + TraitedSpec, + traits, +) + + +def setup_plot_style(): + """Configure Seaborn and Matplotlib defaults for PET plots.""" + sns.set_theme(style='whitegrid', font_scale=1.4, context='talk') + plt.figure(figsize=(16, 10)) + + +class _PlotFDInputSpec(BaseInterfaceInputSpec): + in_fd = File( + exists=True, + mandatory=True, + desc='motion parameters for FD computation', + ) + in_file = File(exists=True, mandatory=True, desc='File to be plotted') + metadata = traits.Dict(mandatory=True, desc='Metadata dictionary containing timing info') + out_file = traits.File(exists=False, desc='output file name') + + +class _PlotFDOutputSpec(TraitedSpec): + out_file = File(desc='Output file') + + +class PlotFD(SimpleInterface): + """Create a plot of framewise displacement over time.""" + input_spec = _PlotFDInputSpec + output_spec = _PlotFDOutputSpec + + def _run_interface(self, runtime): + fd_values = np.loadtxt(self.inputs.in_fd, skiprows=1) + times = np.array(self.inputs.metadata['FrameTimesStart']) + durations = np.array(self.inputs.metadata['FrameDuration']) + midframe_times = times[:-1] + durations[:-1] / 2 + + mask = midframe_times >= 120 + midframe_times = midframe_times[mask] + fd_values = fd_values[mask] + + setup_plot_style() + + sns.lineplot(x=midframe_times, y=fd_values, marker='o', linewidth=2.5, color='crimson') + + plt.xlabel('Time (s)', fontsize=20, fontweight='bold') + plt.ylabel('Framewise Displacement (mm)', fontsize=20, fontweight='bold') + plt.title('FD plot for PET QC', fontsize=22, fontweight='bold', pad=20) + plt.xticks(fontsize=16, fontweight='bold') + plt.yticks(fontsize=16, fontweight='bold') + + sns.despine(trim=True) + plt.tight_layout() + + output_filename = os.path.abspath('fd_plot.png') + plt.savefig(output_filename, bbox_inches='tight') + plt.close() + + self._results['out_file'] = output_filename + return runtime + + +class _PlotRotationInputSpec(BaseInterfaceInputSpec): + mot_param = File(exists=True, mandatory=True, desc='motion parameters') + in_file = File(exists=True, mandatory=True, desc='File to be plotted') + metadata = traits.Dict(mandatory=True, desc='Metadata dictionary containing timing info') + out_file = traits.File(exists=False, desc='output file name') + + +class _PlotRotationOutputSpec(TraitedSpec): + out_file = File(desc='Output file') + + +class PlotRotation(SimpleInterface): + """Generate rotation parameter plots.""" + input_spec = _PlotRotationInputSpec + output_spec = _PlotRotationOutputSpec + + def _run_interface(self, runtime): + motion = np.loadtxt(self.inputs.mot_param) + times = np.array(self.inputs.metadata['FrameTimesStart']) + durations = np.array(self.inputs.metadata['FrameDuration']) + midframe_times = times + durations / 2 + + mask = midframe_times >= 120 + midframe_times = midframe_times[mask] + rot_angles = motion[mask, :3] + + rotation_df = pd.DataFrame({ + 'Time (s)': np.tile(midframe_times, 3), + 'Rotation (degrees)': rot_angles.flatten(), + 'Axis': np.repeat(['X', 'Y', 'Z'], len(midframe_times)) + }) + + setup_plot_style() + + sns.lineplot( + data=rotation_df, + x='Time (s)', + y='Rotation (degrees)', + hue='Axis', + marker='o', + linewidth=2.5, + palette='tab10' + ) + + plt.xlabel('Time (s)', fontsize=20, fontweight='bold') + plt.ylabel('Rotation (degrees)', fontsize=20, fontweight='bold') + plt.title('Rotation plot for PET QC', fontsize=22, fontweight='bold', pad=20) + plt.xticks(fontsize=16, fontweight='bold') + plt.yticks(fontsize=16, fontweight='bold') + plt.legend( + title='Axis', + fontsize=14, + title_fontsize=16, + loc='upper center', + bbox_to_anchor=(0.5, -0.1), + ncol=3, + frameon=False, + ) + + sns.despine(trim=True) + plt.tight_layout(rect=[0, 0.1, 1, 1]) + + output_filename = os.path.abspath('rotation_plot.png') + plt.savefig(output_filename, bbox_inches='tight') + plt.close() + + self._results['out_file'] = output_filename + return runtime + + +class _PlotTranslationInputSpec(BaseInterfaceInputSpec): + mot_param = File(exists=True, mandatory=True, desc='motion parameters') + in_file = File(exists=True, mandatory=True, desc='File to be plotted') + metadata = traits.Dict(mandatory=True, desc='Metadata dictionary containing timing info') + out_file = traits.File(exists=False, desc='output file name') + + +class _PlotTranslationOutputSpec(TraitedSpec): + out_file = File(desc='Output file') + + +class PlotTranslation(SimpleInterface): + """Generate translation parameter plots.""" + input_spec = _PlotTranslationInputSpec + output_spec = _PlotTranslationOutputSpec + + def _run_interface(self, runtime): + motion = np.loadtxt(self.inputs.mot_param) + times = np.array(self.inputs.metadata['FrameTimesStart']) + durations = np.array(self.inputs.metadata['FrameDuration']) + midframe_times = times + durations / 2 + + mask = midframe_times >= 120 + midframe_times = midframe_times[mask] + translations = motion[mask, 3:6] + + translation_df = pd.DataFrame({ + 'Time (s)': np.tile(midframe_times, 3), + 'Translation (mm)': translations.flatten(), + 'Axis': np.repeat(['X', 'Y', 'Z'], len(midframe_times)) + }) + + setup_plot_style() + + sns.lineplot( + data=translation_df, + x='Time (s)', + y='Translation (mm)', + hue='Axis', + marker='o', + linewidth=2.5, + palette='tab10' + ) + + plt.xlabel('Time (s)', fontsize=20, fontweight='bold') + plt.ylabel('Translation (mm)', fontsize=20, fontweight='bold') + plt.title('Translation plot for PET QC', fontsize=22, fontweight='bold', pad=20) + plt.xticks(fontsize=16, fontweight='bold') + plt.yticks(fontsize=16, fontweight='bold') + plt.legend( + title='Axis', + fontsize=14, + title_fontsize=16, + loc='upper center', + bbox_to_anchor=(0.5, -0.1), + ncol=3, + frameon=False, + ) + + sns.despine(trim=True) + plt.tight_layout(rect=[0, 0.1, 1, 1]) + + output_filename = os.path.abspath('translation_plot.png') + plt.savefig(output_filename, bbox_inches='tight') + plt.close() + + self._results['out_file'] = output_filename + return runtime + + +def generate_tac_figures(tacs_tsv, metadata, output_dir=None): + """Generate TAC plots grouped by region type.""" + + import os + + import matplotlib.pyplot as plt + import pandas as pd + import seaborn as sns + + # Default to the current directory if output_dir is None + if output_dir is None: + output_dir = os.getcwd() + + Path(output_dir).mkdir(parents=True, exist_ok=True) + + # Load the data + tac_data = pd.read_csv(tacs_tsv, sep='\t') + + # Calculate midframe times + tac_data['midframe'] = (tac_data['frame_times_start'] + tac_data['frame_times_end']) / 2 + + region_cols = [ + col + for col in tac_data.columns + if col not in ['frame_times_start', 'frame_times_end', 'midframe'] + ] + + def average_lr_regions(df, region_columns): + averaged_data = pd.DataFrame() + processed_regions = set() + + for col in region_columns: + base_name = re.sub(r'(_L|_R)$', '', col) + + if base_name in processed_regions: + continue + + left_col = f'{base_name}_L' + right_col = f'{base_name}_R' + + if left_col in df and right_col in df: + averaged_data[base_name] = df[[left_col, right_col]].mean(axis=1) + else: + averaged_data[base_name] = df[col] + + processed_regions.add(base_name) + + return averaged_data + + avg_tac_data = average_lr_regions(tac_data, region_cols) + avg_tac_data['midframe'] = tac_data['midframe'] + + tac_melted = avg_tac_data.melt( + id_vars=['midframe'], + var_name='Region', + value_name='Uptake' + ) + + cortical_regions = [ + col + for col in avg_tac_data.columns + if any( + keyword in col.lower() + for keyword in [ + 'gyrus', + 'cortex', + 'cingulate', + 'frontal', + 'temporal', + 'parietal', + 'occipital', + 'insula', + 'cuneus', + ] + ) + ] + + subcortical_regions = [ + col + for col in avg_tac_data.columns + if any( + keyword in col.lower() + for keyword in [ + 'caudate', + 'putamen', + 'thalamus', + 'pallidum', + 'accumbens', + 'amygdala', + 'hippocampus', + ] + ) + ] + + ventricular_regions = [ + col for col in avg_tac_data.columns if 'ventricle' in col.lower() + ] + + other_regions = [ + col + for col in avg_tac_data.columns + if col + not in cortical_regions + subcortical_regions + ventricular_regions + ['midframe'] + ] + + unit = metadata.get('Units', 'Uptake') + + def plot_regions(df, regions, title, unit, output_path): + plt.figure(figsize=(16, 10)) + + sns.set(style='whitegrid', font_scale=1.4, context='talk') + palette = sns.color_palette('tab10', n_colors=len(regions)) + + plot = sns.lineplot( + data=df[df['Region'].isin(regions)], + x='midframe', + y='Uptake', + hue='Region', + marker='o', + linewidth=2.5, + markersize=8, + palette=palette + ) + + plot.set_xlabel('Time (s)', fontsize=20, fontweight='bold') + plot.set_ylabel(f'Uptake ({unit})', fontsize=20, fontweight='bold') + plot.set_title(title, fontsize=22, fontweight='bold', pad=20) + + plt.xticks(fontsize=16, fontweight='bold') + plt.yticks(fontsize=16, fontweight='bold') + + plt.legend( + title='Region', + bbox_to_anchor=(0.5, -0.15), + loc='upper center', + fontsize=14, + title_fontsize=16, + frameon=False, + ncol=3 + ) + + sns.despine(trim=True) + + plt.tight_layout(rect=[0, 0.1, 1, 1]) + plt.savefig(output_path) + plt.close() + + figures = [] + region_groups = [ + (cortical_regions, 'Cortical Regions TACs'), + (subcortical_regions, 'Subcortical Regions TACs'), + (ventricular_regions, 'Ventricular Regions TACs'), + (other_regions, 'Other Regions TACs') + ] + + for regions, title in region_groups: + if regions: + fig_filename = title.replace(' ', '_').lower() + '.png' + fig_path = os.path.join(output_dir, fig_filename) + plot_regions(tac_melted, regions, title, unit, fig_path) + figures.append(fig_path) + + return figures diff --git a/mriqc/tests/test_pet.py b/mriqc/tests/test_pet.py new file mode 100644 index 00000000..9f7996a9 --- /dev/null +++ b/mriqc/tests/test_pet.py @@ -0,0 +1,119 @@ +import importlib +from pathlib import Path + +import pytest + +# Skip tests if required packages are missing +np = pytest.importorskip('numpy') +nb = pytest.importorskip('nibabel') + +from mriqc.interfaces.pet import ChooseRefHMC, FDStats + + +@pytest.mark.parametrize('n_frames', [5, 3]) +def test_choose_ref_hmc(tmp_path, n_frames): + """Check that the frame with maximum intensity is selected.""" + data = np.random.rand(2, 2, 2, n_frames) + frame_sums = data.sum(axis=(0, 1, 2)) + max_idx = int(np.argmax(frame_sums)) + + img = nb.Nifti1Image(data, np.eye(4)) + in_file = tmp_path / 'in_pet.nii.gz' + img.to_filename(in_file) + + result = ChooseRefHMC(in_file=str(in_file)).run() + out_file = Path(result.outputs.out_file) + assert out_file.exists() + + # Ensure the interface reports the correct reference frame index + assert result.outputs.ref_frame == max_idx + + out_img = nb.load(out_file) + assert np.allclose(out_img.get_fdata(), data[..., max_idx]) + + +def test_fdstats(tmp_path): + """FDStats should compute mean, count and percentage correctly.""" + fd_values = [0.1, 0.3, 0.15] + in_fd = tmp_path / 'fd.txt' + in_fd.write_text('FD\n' + '\n'.join(str(v) for v in fd_values)) + + res = FDStats(in_fd=str(in_fd)).run() + out = res.outputs.out_fd + + expected_mean = float(np.mean(fd_values)) + expected_num = int(np.sum(np.array(fd_values) > 0.2)) + expected_perc = float(expected_num * 100 / (len(fd_values) + 1)) + + assert out['mean'] == pytest.approx(expected_mean) + assert out['num'] == expected_num + assert out['perc'] == pytest.approx(expected_perc) + + +@pytest.mark.skipif( + any(importlib.util.find_spec(pkg) is None for pkg in ('pandas', 'matplotlib', 'seaborn')), + reason='Required plotting libraries are not installed.' +) +def test_generate_tac_figures(tmp_path): + """generate_tac_figures should create expected figure files.""" + import pandas as pd + from mriqc.qc.pet import generate_tac_figures + + # Create example TAC data + df = pd.DataFrame({ + 'frame_times_start': [0, 10], + 'frame_times_end': [10, 20], + 'region_L': [1.0, 2.0], + 'region_R': [1.5, 2.5], + }) + tsv = tmp_path / 'tac.tsv' + df.to_csv(tsv, sep='\t', index=False) + + meta = {'Units': 'kBq'} + figs = generate_tac_figures(str(tsv), meta, output_dir=str(tmp_path)) + + assert figs, 'No figures were generated.' + for fig in figs: + assert Path(fig).exists() + + +@pytest.mark.skipif( + any(importlib.util.find_spec(pkg) is None for pkg in ('bids', 'niworkflows')), + reason='Required packages are not installed.' +) +def test_tracer_entity_derivatives(tmp_path): + """DerivativesDataSink should keep tracer label in output filenames.""" + from mriqc import config + from mriqc.testing import mock_config + from mriqc.utils.misc import initialize_meta_and_data + from mriqc.interfaces import DerivativesDataSink + + pet_file = ( + Path(__file__).parent.parent + / 'data' + / 'pet' + / 'sub-01' + / 'ses-baseline' + / 'pet' + / 'sub-01_ses-baseline_trc-ps13_pet.nii.gz' + ) + + with mock_config(): + config.execution.bids_dir = pet_file.parents[3] + config.execution.init() + config.workflow.inputs = {'pet': [str(pet_file)]} + initialize_meta_and_data() + assert config.workflow.inputs_entities['pet'][0]['trc'] == 'ps13' + + ds = DerivativesDataSink( + base_directory=tmp_path, + datatype='figures', + desc='carpet', + extension='.svg', + dismiss_entities=('part',), + ) + ds.inputs.in_file = str(pet_file) + ds.inputs.source_file = str(pet_file) + result = ds.run() + + assert 'trc-ps13' in Path(result.outputs.out_file).name \ No newline at end of file diff --git a/mriqc/utils/misc.py b/mriqc/utils/misc.py index 9a06fd20..7387794c 100644 --- a/mriqc/utils/misc.py +++ b/mriqc/utils/misc.py @@ -49,6 +49,7 @@ 'T2w': 'anat', 'bold': 'func', 'dwi': 'dwi', + 'pet': 'pet', } BIDS_COMP = OrderedDict( @@ -56,6 +57,7 @@ ('subject_id', 'sub'), ('session_id', 'ses'), ('task_id', 'task'), + ('trc_id', 'trc'), ('acq_id', 'acq'), ('rec_id', 'rec'), ('run_id', 'run'), @@ -64,7 +66,7 @@ BIDS_EXPR = """\ ^sub-(?P[a-zA-Z0-9]+)(_ses-(?P[a-zA-Z0-9]+))?\ -(_task-(?P[a-zA-Z0-9]+))?(_acq-(?P[a-zA-Z0-9]+))?\ +(_task-(?P[a-zA-Z0-9]+))?(_trc-(?P[a-zA-Z0-9]+))?(_acq-(?P[a-zA-Z0-9]+))?\ (_rec-(?P[a-zA-Z0-9]+))?(_run-(?P[a-zA-Z0-9]+))?\ """ diff --git a/mriqc/workflows/core.py b/mriqc/workflows/core.py index 002a0ac1..f0762082 100644 --- a/mriqc/workflows/core.py +++ b/mriqc/workflows/core.py @@ -29,10 +29,12 @@ from mriqc.workflows.anatomical.base import anat_qc_workflow from mriqc.workflows.diffusion.base import dmri_qc_workflow from mriqc.workflows.functional.base import fmri_qc_workflow +from mriqc.workflows.pet.base import pet_qc_workflow ANATOMICAL_KEYS = 't1w', 't2w' FMRI_KEY = 'bold' DMRI_KEY = 'dwi' +PET_KEY = 'pet' def init_mriqc_wf(): @@ -47,6 +49,10 @@ def init_mriqc_wf(): if FMRI_KEY in config.workflow.inputs: workflow.add_nodes([fmri_qc_workflow()]) + # Create PET QC workflow + if PET_KEY in config.workflow.inputs: + workflow.add_nodes([pet_qc_workflow()]) + # Create dMRI QC workflow if DMRI_KEY in config.workflow.inputs: workflow.add_nodes([dmri_qc_workflow()]) diff --git a/mriqc/workflows/functional/base.py b/mriqc/workflows/functional/base.py index 4d4eb1ed..5266fe69 100644 --- a/mriqc/workflows/functional/base.py +++ b/mriqc/workflows/functional/base.py @@ -492,7 +492,7 @@ def fmri_bmsk_workflow(name='fMRIBrainMask'): return workflow -def hmc(name='fMRI_HMC', omp_nthreads=None): +def hmc(name='fmriHMC', omp_nthreads=None): """ Create a :abbr:`HMC (head motion correction)` workflow for fMRI. diff --git a/mriqc/workflows/pet/__init__.py b/mriqc/workflows/pet/__init__.py new file mode 100644 index 00000000..25573261 --- /dev/null +++ b/mriqc/workflows/pet/__init__.py @@ -0,0 +1,27 @@ +"""PET quality control workflows. + +This submodule defines Nipype workflows used by MRIQC to compute image +quality metrics (IQMs) and generate reportlets for PET datasets. The +:func:`pet_qc_workflow` function orchestrates motion correction, +spatial normalization, TAC extraction and IQM computation, calling the +other constructors in this module. Reportlets can be generated with +:func:`init_pet_report_wf`. +""" + +from .base import ( + pet_qc_workflow, + hmc, + compute_iqms, + pet_mni_align, + extract_tacs, +) +from .output import init_pet_report_wf + +__all__ = [ + 'pet_qc_workflow', + 'hmc', + 'compute_iqms', + 'pet_mni_align', + 'extract_tacs', + 'init_pet_report_wf', +] \ No newline at end of file diff --git a/mriqc/workflows/pet/base.py b/mriqc/workflows/pet/base.py new file mode 100644 index 00000000..cd2fc74b --- /dev/null +++ b/mriqc/workflows/pet/base.py @@ -0,0 +1,701 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +# +# Copyright 2021 The NiPreps Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +"""Workflows building blocks used to generate PET QC derivatives.""" +import os.path as op + +from nilearn.plotting import plot_carpet +from nipype.interfaces import utility as niu +from nipype.interfaces.ants import ApplyTransforms +from nipype.interfaces.utility import Function, IdentityInterface +from nipype.pipeline import engine as pe +from niworkflows.interfaces.bids import ReadSidecarJSON +from niworkflows.interfaces.reportlets.registration import ( + SpatialNormalizationRPT as RobustMNINormalization, +) +from pkg_resources import resource_filename + +from mriqc import config +from mriqc.interfaces import DerivativesDataSink +from mriqc.workflows.pet.output import init_pet_report_wf +from mriqc.workflows.utils import threshold_image_percent + + +def pet_qc_workflow(name='petMRIQC'): + """ + Initialize the (pet)MRIQC workflow. + + .. workflow:: + + import os.path as op + from mriqc.workflows.functional.base import pet_qc_workflow + from mriqc.testing import mock_config + with mock_config(): + wf = pet_qc_workflow() + + """ + + from nipype.interfaces.afni import TStat + + from mriqc.messages import BUILDING_WORKFLOW + + dataset = config.workflow.inputs['pet'] + metadata = config.workflow.inputs_metadata['pet'] + entities = config.workflow.inputs_entities['pet'] + + message = BUILDING_WORKFLOW.format( + modality='pet', + detail=f'for {len(dataset)} PET runs.', + ) + config.loggers.workflow.info(message) + + workflow = pe.Workflow(name=name) + + inputnode = pe.Node( + niu.IdentityInterface( + fields=['in_file', 'metadata', 'entities'], + ), + name='inputnode', + ) + inputnode.synchronize = True + inputnode.iterables = [ + ('in_file', dataset), + ('metadata', metadata), + ('entities', entities), + ] + + load_meta = pe.Node(ReadSidecarJSON(bids_dir=config.execution.bids_dir), name='LoadMetadata') + + outputnode = pe.Node( + niu.IdentityInterface( + fields=[ + 'qc', + 'mosaic', + 'out_group', + 'out_fd', + 'pet_mean', + 'pet_dseg', + 'tacs_tsv', + 'norm_report', + 'tacs_figures', + ] + ), + name='outputnode', + ) + + hmcwf = hmc(omp_nthreads=config.nipype.omp_nthreads) + hmcwf.inputs.inputnode.fd_radius = config.workflow.fd_radius + + mean_pet = pe.Node(TStat(args='-mean', outputtype='NIFTI_GZ'), name='MeanPET') + + normwf = pet_mni_align() + tacswf = extract_tacs() + iqmswf = compute_iqms() + pet_report_wf = init_pet_report_wf() + + norm_report_sink = pe.Node( + DerivativesDataSink( + base_directory=config.execution.work_dir / 'reportlets', + datatype='figures', + desc='norm', + extension='.svg', + dismiss_entities=('part',) + ), + name='norm_report_sink', + run_without_submitting=True, + ) + + carpet_plot = pe.Node(Function( + input_names=['in_pet', 'seg_file', 'metadata', 'output_file'], + output_names=['out_file'], + function=create_pet_carpet_plot + ), name='carpet_plot') + + carpet_plot.inputs.output_file = 'carpet_plot.svg' + + # DataSink node for carpet plot + ds_report_carpet = pe.Node( + DerivativesDataSink( + base_directory=config.execution.work_dir / 'reportlets', + datatype='figures', + desc='carpet', + extension='.svg', + dismiss_entities=('part',), + ), + name='ds_report_carpet', + run_without_submitting=True, + ) + + ds_tacs = pe.MapNode( + DerivativesDataSink( + base_directory=str(config.execution.output_dir), + suffix='timeseries', + atlas='hammers', + space='MNI152', + datatype='pet', + dismiss_entities=('desc',), + extension='.tsv', + ), + name='ds_tacs', + run_without_submitting=True, + iterfield=['in_file', 'source_file'], + ) + + workflow.connect([ + (inputnode, load_meta, [('in_file', 'in_file')]), + (inputnode, hmcwf, [('in_file', 'inputnode.in_file')]), + # Feed IQMs computation + (inputnode, iqmswf, [('in_file', 'inputnode.in_file'), + ('metadata', 'inputnode.metadata'), + ('entities', 'inputnode.entities')]), + (hmcwf, iqmswf, [ + ('outputnode.out_fd', 'inputnode.hmc_fd'), + ('outputnode.ref_frame', 'inputnode.ref_frame'), + ]), + # Feed reportlet generation + (inputnode, pet_report_wf, [('in_file', 'inputnode.name_source')]), + (hmcwf, pet_report_wf, [ + ('outputnode.out_fd', 'inputnode.hmc_fd'), + ('outputnode.out_mot_param', 'inputnode.hmc_mot_param'), + ]), + (iqmswf, pet_report_wf, [ + ('outputnode.out_file', 'inputnode.in_iqms'), + ]), + (tacswf, pet_report_wf, [('outputnode.tacs_tsv', 'inputnode.tacs_tsv')]), + (load_meta, pet_report_wf, [('out_dict', 'inputnode.metadata')]), + (hmcwf, mean_pet, [('outputnode.out_file', 'in_file')]), + (mean_pet, normwf, [('out_file', 'inputnode.pet_mean')]), + (hmcwf, normwf, [('outputnode.out_file', 'inputnode.pet_dynamic')]), + (normwf, tacswf, [('outputnode.pet_dynamic_t1', 'inputnode.pet_dynamic_t1')]), + (load_meta, tacswf, [('out_dict', 'inputnode.pet_json')]), + (hmcwf, outputnode, [('outputnode.out_fd', 'out_fd')]), + (mean_pet, outputnode, [('out_file', 'pet_mean')]), + (normwf, outputnode, [ + ('outputnode.pet_dseg', 'pet_dseg'), + ('outputnode.out_report', 'norm_report') + ]), + (tacswf, outputnode, [('outputnode.tacs_tsv', 'tacs_tsv')]), + (normwf, norm_report_sink, [('outputnode.out_report', 'in_file')]), + (inputnode, norm_report_sink, [('in_file', 'source_file')]), + (normwf, carpet_plot, [('outputnode.pet_dynamic_t1', 'in_pet'), + ('outputnode.pet_dseg', 'seg_file')]), + (load_meta, carpet_plot, [('out_dict', 'metadata')]), + (carpet_plot, ds_report_carpet, [('out_file', 'in_file')]), + (inputnode, ds_report_carpet, [('in_file', 'source_file')]), + (tacswf, ds_tacs, [('outputnode.tacs_tsv', 'in_file')]), + (inputnode, ds_tacs, [('in_file', 'source_file')]), + ]) + + if not config.execution.no_sub: + from mriqc.interfaces.webapi import UploadIQMs + + upldwf = pe.MapNode( + UploadIQMs( + endpoint=config.execution.webapi_url, + auth_token=config.execution.webapi_token, + strict=config.execution.upload_strict, + ), + name='UploadMetrics', + iterfield=['in_iqms'], + ) + + workflow.connect([ + (iqmswf, upldwf, [('outputnode.out_file', 'in_iqms')]), + ]) + + return workflow + + +def hmc(name='petHMC', omp_nthreads=None): + """ + Create a :abbr: petHMC (head motion correction) workflow. + """ + from nipype.algorithms.confounds import FramewiseDisplacement + from nipype.interfaces.afni import Volreg + + from mriqc.interfaces.pet import ChooseRefHMC + + mem_gb = config.workflow.biggest_file_gb['pet'] + + workflow = pe.Workflow(name=name) + + inputnode = pe.Node( + niu.IdentityInterface(fields=['in_file', 'fd_radius']), + name='inputnode', + ) + + outputnode = pe.Node( + niu.IdentityInterface( + fields=['out_file', 'out_mot_param', 'out_fd', 'mpars', 'ref_frame'] + ), + name='outputnode', + ) + + choose_ref_node = pe.Node( + ChooseRefHMC(), + name='ChooseRefHMC', + ) + + estimate_hm = pe.Node( + Volreg(args='-Fourier -twopass', zpad=4, outputtype='NIFTI_GZ'), + name='estimate_hm', + mem_gb=mem_gb * 2.5, + ) + + fdnode = pe.Node( + FramewiseDisplacement(normalize=False, parameter_source='AFNI'), + name='ComputeFD', + ) + + workflow.connect([ + (inputnode, choose_ref_node, [('in_file', 'in_file')]), + (inputnode, estimate_hm, [('in_file', 'in_file')]), + (inputnode, fdnode, [('fd_radius', 'radius')]), + (choose_ref_node, estimate_hm, [('out_file', 'basefile')]), + + # Output corrected 4D PET file (out_file) + (estimate_hm, outputnode, [ + ('out_file', 'out_file'), # <-- added corrected 4D PET + ('oned_file', 'out_mot_param'), + ('oned_file', 'mpars'), + ]), + (estimate_hm, fdnode, [('oned_file', 'in_file')]), + (fdnode, outputnode, [('out_file', 'out_fd')]), + (choose_ref_node, outputnode, [('ref_frame', 'ref_frame')]), + ]) + + return workflow + + +def compute_iqms(name='ComputeIQMs'): + """ + Initialize the workflow that actually computes the IQMs. + + .. workflow:: + + from mriqc.workflows.functional.base import compute_iqms + from mriqc.testing import mock_config + with mock_config(): + wf = compute_iqms() + + """ + from nipype.interfaces.freesurfer import MRIConvert + from nipype.interfaces.utility import Function + + from mriqc.interfaces import IQMFileSink + from mriqc.interfaces.pet import FDStats + from mriqc.interfaces.reports import AddProvenance + + + mem_gb = config.workflow.biggest_file_gb['pet'] + + workflow = pe.Workflow(name=name) + inputnode = pe.Node( + niu.IdentityInterface( + fields=[ + 'in_file', + 'metadata', + 'entities', + 'hmc_fd', + 'ref_frame', + 'fd_thres', + ] + ), + name='inputnode', + ) + outputnode = pe.Node( + niu.IdentityInterface( + fields=[ + 'out_file', + 'fwhm_list', + 'fwhm_mean', + ] + ), + name='outputnode', + ) + + # Set FD threshold + inputnode.inputs.fd_thres = config.workflow.fd_thres + + # Compute FD statistics + fd_stats = pe.Node( + FDStats(), + name='FDStats', + ) + + # Split 4D PET data into 3D frames using MRIConvert + split_pet = pe.Node( + MRIConvert(split=True, out_type='niigz'), + name='SplitPET', + mem_gb=mem_gb * 2, + ) + + # Compute smoothness (FWHM) per frame using MapNode + fwhm_per_frame = pe.MapNode( + Function( + input_names=['in_file'], + output_names=['fwhm_acf'], + function=compute_acf_fwhm + ), + name='FWHMPerFrame', + iterfield=['in_file'] + ) + + mean_fwhm = pe.Node( + niu.Function(input_names=['inlist'], output_names=['out'], function=_mean), + name='FWHMMean' + ) + + addprov = pe.MapNode( + AddProvenance(modality='pet'), + name='provenance', + run_without_submitting=True, + iterfield=['in_file'], + ) + + # Save to JSON file + datasink = pe.MapNode( + IQMFileSink( + modality='pet', + out_dir=str(config.execution.output_dir), + dataset=config.execution.dsname, + ), + name='datasink', + run_without_submitting=True, + iterfield=['in_file', 'root', 'metadata', 'provenance'], + ) + + # fmt: off + workflow.connect([ + (inputnode, addprov, [('in_file', 'in_file')]), + (inputnode, datasink, [('in_file', 'in_file'), + ('entities', 'entities'), + ('metadata', 'metadata'), + ('ref_frame', 'ref_frame')]), + (inputnode, fd_stats, [('hmc_fd', 'in_fd'), + ('fd_thres', 'fd_thres')]), + (inputnode, split_pet, [('in_file', 'in_file')]), + (split_pet, fwhm_per_frame, [('out_file', 'in_file')]), + (fwhm_per_frame, mean_fwhm, [('fwhm_acf', 'inlist')]), + (mean_fwhm, datasink, [('out', 'fwhm_mean')]), + (addprov, datasink, [('out_prov', 'provenance')]), + (fd_stats, datasink, [('out_fd', 'root')]), + (fwhm_per_frame, datasink, [('fwhm_acf', 'fwhm_per_frame')]), + (datasink, outputnode, [('out_file', 'out_file')]), + (fwhm_per_frame, outputnode, [('fwhm_acf', 'fwhm_list')]), + (mean_fwhm, outputnode, [('out', 'fwhm_mean')]) + ]) + # fmt: on + + return workflow + + +def compute_acf_fwhm(in_file): + """Return the ACF-based FWHM estimated by AFNI's 3dFWHMx.""" + import subprocess + + cmd = f'3dFWHMx -input {in_file} -combine -detrend -acf -automask' + result = subprocess.run(cmd.split(), capture_output=True, text=True) + + output_lines = result.stdout.strip().split('\n') + + acf_line = None + for line in output_lines: + if line.startswith(' 0.') or line.startswith('0.'): + values = line.split() + if len(values) >= 4: + acf_line = values + break + + if acf_line is None: + raise ValueError('Failed to parse AFNI 3dFWHMx output correctly.') + + fwhm_acf = float(acf_line[3]) + + return fwhm_acf + + +def _mean(inlist): + from numpy import mean + + return float(mean(inlist)) + + +def pet_mni_align(name='PETSpatialNormalization'): + """Align the PET series to the SPM T1 template with corresponding Hammer's atlas""" + import os.path as op + + from nipype.interfaces.utility import IdentityInterface + from nipype.pipeline import engine as pe + from pkg_resources import resource_filename + + workflow = pe.Workflow(name=name) + + inputnode = pe.Node( + IdentityInterface(fields=['pet_mean', 'pet_dynamic']), + name='inputnode', + ) + + outputnode = pe.Node( + IdentityInterface(fields=['pet_dynamic_t1', 'pet_dseg', 'out_report']), + name='outputnode', + ) + + template_dir = resource_filename('mriqc', 'data/atlas') + template_t1 = op.join(template_dir, 'tpl-SPM_space-MNI152_desc-conform_T1.nii.gz') + template_dseg = op.join(template_dir, 'tpl-SPM_space-MNI152_desc-conform_dseg.nii.gz') + template_mask = op.join(template_dir, 'tpl-SPM_space-MNI152_desc-conform_mask.nii.gz') + + threshold_ref = pe.Node( + niu.Function( + input_names=['in_file', 'percent'], + output_names=['out_file'], + function=threshold_image_percent, + ), + name='ThresholdRef' + ) + threshold_ref.inputs.percent = 0.2 + + ants_norm = pe.Node( + RobustMNINormalization( + moving='boldref', + reference='boldref', + explicit_masking=True, + float=True, + generate_report=True, + reference_image=template_t1, + reference_mask=template_mask, + settings=[op.join(resource_filename('mriqc', 'data/atlas'), + 'petref-mni_registration_precise_000.json')], + ), + name='ANTsNormalization' + ) + + apply_transform_dynamic = pe.Node( + ApplyTransforms( + interpolation='Linear', + input_image_type=3, # Time-series + dimension=3, + float=True, + reference_image=template_t1 + ), + name='ApplyTransformDynamic' + ) + + workflow.connect([ + (inputnode, threshold_ref, [('pet_mean', 'in_file')]), + (threshold_ref, ants_norm, [('out_file', 'moving_image')]), + + # Correct connection: using composite transform + (ants_norm, apply_transform_dynamic, [('composite_transform', 'transforms')]), + + # Connect dynamic PET + (inputnode, apply_transform_dynamic, [('pet_dynamic', 'input_image')]), + + # Output + (apply_transform_dynamic, outputnode, [('output_image', 'pet_dynamic_t1')]), + + (ants_norm, outputnode, [('out_report', 'out_report')]) + ]) + + outputnode.inputs.pet_dseg = template_dseg + + return workflow + + +def extract_tacs(name='ExtractTACs'): + """Extract time-activity curves from normalized dynamic PET.""" + import os.path as op + + from nipype.interfaces.utility import Function + from nipype.pipeline import engine as pe + + workflow = pe.Workflow(name=name) + + inputnode = pe.Node( + IdentityInterface(fields=['pet_dynamic_t1', 'pet_json']), + name='inputnode' + ) + + outputnode = pe.Node( + IdentityInterface(fields=['tacs_tsv']), + name='outputnode' + ) + + template_dir = resource_filename('mriqc', 'data/atlas') + labels_tsv = op.join(template_dir, 'tpl-SPM_space-MNI152_dseg.tsv') + template_dseg = op.join(template_dir, 'tpl-SPM_space-MNI152_desc-conform_dseg.nii.gz') + + def compute_tacs(dseg_file, dynamic_pet, labels_tsv, pet_json): + import os + + import nibabel as nib + import numpy as np + import pandas as pd + + dseg = nib.load(dseg_file).get_fdata() + pet_data = nib.load(dynamic_pet).get_fdata() + labels_df = pd.read_csv(labels_tsv, sep='\t') + + frame_times_start = np.array(pet_json['FrameTimesStart']) + frame_duration = np.array(pet_json['FrameDuration']) + frame_times_end = frame_times_start + frame_duration + + tacs = { + 'frame_times_start': frame_times_start, + 'frame_times_end': frame_times_end + } + + for _, row in labels_df.iterrows(): + label_id = row['index'] + region_name = row['name'] + mask = dseg == label_id + tac = pet_data[mask, :].mean(axis=0) + tacs[region_name] = tac + + df = pd.DataFrame(tacs) + tsv_file = os.path.abspath('tacs.tsv') + df.to_csv(tsv_file, sep='\t', index=False) + return tsv_file + + tac_extraction = pe.Node( + Function( + input_names=['dseg_file', 'dynamic_pet', 'labels_tsv', 'pet_json'], + output_names=['tacs_tsv'], + function=compute_tacs, + ), + name='TACExtraction' + ) + + tac_extraction.inputs.labels_tsv = labels_tsv + tac_extraction.inputs.dseg_file = template_dseg + + workflow.connect([ + (inputnode, tac_extraction, [ + ('pet_dynamic_t1', 'dynamic_pet'), + ('pet_json', 'pet_json') + ]), + (tac_extraction, outputnode, [('tacs_tsv', 'tacs_tsv')]), + ]) + + return workflow + + +def create_pet_carpet_plot(in_pet, seg_file, metadata, output_file): + """Create a carpet plot grouped by tissue type.""" + import os.path as op + + import matplotlib.pyplot as plt + import nibabel as nb + import numpy as np + import pandas as pd + from nilearn.plotting import plot_carpet + from pkg_resources import resource_filename + + pet_img = nb.load(in_pet) + seg_img = nb.load(seg_file) + seg_data = seg_img.get_fdata().astype(int) # Extract segmentation data as numpy array + + template_dir = resource_filename('mriqc', 'data/atlas') + labels_tsv = op.join(template_dir, 'tpl-SPM_space-MNI152_dseg.tsv') + labels_df = pd.read_csv(labels_tsv, sep='\t') + + # Define labels based on segmentation values + map_labels = { + 'Cortical': 1, + 'Subcortical': 2, + 'Cerebellar': 3, + } + + cortical_keywords = [ + 'gyrus', + 'cortex', + 'cingulate', + 'frontal', + 'temporal', + 'parietal', + 'occipital', + 'insula', + 'cuneus', + ] + subcortical_keywords = [ + 'caudate', + 'putamen', + 'thalamus', + 'pallidum', + 'accumbens', + 'amygdala', + 'hippocampus', + ] + cerebellar_keywords = ['cerebellum'] + + # Create a mapping from original labels to simplified labels + label_to_group = {0: 0} # Explicitly handle background + for _, row in labels_df.iterrows(): + label_id = row['index'] + region_name = row['name'].lower() + + if any(keyword in region_name for keyword in cortical_keywords): + label_to_group[label_id] = map_labels['Cortical'] + elif any(keyword in region_name for keyword in subcortical_keywords): + label_to_group[label_id] = map_labels['Subcortical'] + elif any(keyword in region_name for keyword in cerebellar_keywords): + label_to_group[label_id] = map_labels['Cerebellar'] + + # Remap the segmentation image explicitly ensuring no gaps or unknown labels + grouped_dseg_data = np.zeros_like(seg_data, dtype=int) + unique_labels = np.unique(seg_data) + + for original_label in unique_labels: + grouped_dseg_data[seg_data == original_label] = label_to_group.get( + original_label, 0 + ) # Ensure fallback to 0 + + # Ensure data has correct datatype for nilearn + grouped_dseg_data = grouped_dseg_data.astype(np.int32) + + # Generate new segmentation NIfTI image + grouped_dseg_img = nb.Nifti1Image(grouped_dseg_data, seg_img.affine, seg_img.header) + + fig, ax = plt.subplots(figsize=(15, 20)) + + # Directly use plot_carpet and capture the figure object + fig = plot_carpet( + img=pet_img, + mask_img=grouped_dseg_img, + mask_labels=map_labels, + t_r=None, + cmap='turbo', + cmap_labels='Set1', + title='Global uptake patterns over time separated by tissue type' + ) + + # Manually adjust the xlabel on the correct axis (the carpet plot axis is usually axes[1]) + fig.axes[1].set_xlabel('Frame #', fontsize=14) + + # Adjust and save figure + fig.tight_layout() + output_file = op.abspath(output_file) + fig.savefig(output_file, bbox_inches='tight', dpi=300) + plt.close(fig) + + return output_file diff --git a/mriqc/workflows/pet/output.py b/mriqc/workflows/pet/output.py new file mode 100644 index 00000000..79ad4fa6 --- /dev/null +++ b/mriqc/workflows/pet/output.py @@ -0,0 +1,189 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +# +# Copyright 2021 The NiPreps Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +"""Generate reportlet figures for PET quality control.""" + +from nipype.interfaces import utility as niu +from nipype.interfaces.utility import Function +from nipype.pipeline import engine as pe + +from mriqc import config +from mriqc.interfaces import DerivativesDataSink +from mriqc.qc.pet import PlotFD, PlotRotation, PlotTranslation, generate_tac_figures + + +def init_pet_report_wf(name='pet_report_wf'): + """ + Write out individual reportlets. + + .. workflow:: + + from mriqc.workflows.functional.output import init_pet_report_wf + from mriqc.testing import mock_config + with mock_config(): + wf = init_pet_report_wf() + + """ + + reportlets_dir = config.execution.work_dir / 'reportlets' + + workflow = pe.Workflow(name=name) + inputnode = pe.Node( + niu.IdentityInterface( + fields=[ + 'hmc_mot_param', + 'hmc_fd', + 'in_iqms', + 'name_source', + 'tacs_tsv', + 'metadata', + ] + ), + name='inputnode', + ) + + plot_fd = pe.Node( + PlotFD(), + name='plot_fd', + ) + + plot_trans = pe.Node( + PlotTranslation(), + name='plot_translation', + ) + + plot_rot = pe.Node( + PlotRotation(), + name='plot_rotation', + ) + + plot_tacs = pe.Node( + Function( + input_names=['tacs_tsv', 'metadata', 'output_dir'], + output_names=['figures', 'descriptions'], + function=generate_tac_figures_with_desc + ), + name='plot_tacs', + ) + + plot_tacs.inputs.output_dir = None + + ds_report_fd = pe.MapNode( + DerivativesDataSink( + base_directory=reportlets_dir, + desc='fd', + datatype='figures', + dismiss_entities=('part',), + ), + name='ds_report_fd', + run_without_submitting=True, + iterfield=['in_file', 'source_file'], + ) + + ds_report_trans = pe.MapNode( + DerivativesDataSink( + base_directory=reportlets_dir, + desc='translation', + datatype='figures', + dismiss_entities=('part',), + ), + name='ds_report_trans', + run_without_submitting=True, + iterfield=['in_file', 'source_file'], + ) + + ds_report_rot = pe.MapNode( + DerivativesDataSink( + base_directory=reportlets_dir, + desc='rotation', + datatype='figures', + dismiss_entities=('part',), + ), + name='ds_report_rot', + run_without_submitting=True, + iterfield=['in_file', 'source_file'], + ) + + ds_report_tacs = pe.MapNode( + DerivativesDataSink(base_directory=reportlets_dir, + datatype='figures', dismiss_entities=('part',)), + name='ds_report_tacs', run_without_submitting=True, + iterfield=['in_file', 'source_file', 'desc'], + ) + + def repeat_source_file(source_file, figures): + return [source_file] * len(figures) + + repeat_source_file_node = pe.Node( + Function( + input_names=['source_file', 'figures'], + output_names=['source_files'], + function=lambda source_file, figures: [source_file] * len(figures) + ), + name='repeat_source_file_node', + ) + + # fmt: off + workflow.connect([ + # (inputnode, rnode, [("in_iqms", "in_iqms")]), + (inputnode, repeat_source_file_node, [('name_source', 'source_file')]), + (plot_tacs, repeat_source_file_node, [('figures', 'figures')]), + (inputnode, plot_fd, [('hmc_fd', 'in_fd')]), + (inputnode, plot_fd, [('name_source', 'in_file')]), + (inputnode, plot_fd, [('metadata', 'metadata')]), + (inputnode, plot_trans, [('name_source', 'in_file'), + ('hmc_mot_param', 'mot_param'), + ('metadata', 'metadata')]), + (inputnode, plot_rot, [('name_source', 'in_file'), + ('hmc_mot_param', 'mot_param'), + ('metadata', 'metadata')]), + (inputnode, plot_tacs, [('tacs_tsv', 'tacs_tsv'), ('metadata', 'metadata')]), + (inputnode, ds_report_fd, [('name_source', 'source_file')]), + (inputnode, ds_report_trans, [('name_source', 'source_file')]), + (inputnode, ds_report_rot, [('name_source', 'source_file')]), + (plot_fd, ds_report_fd, [('out_file', 'in_file')]), + (plot_trans, ds_report_trans, [('out_file', 'in_file')]), + (plot_rot, ds_report_rot, [('out_file', 'in_file')]), + (plot_tacs, ds_report_tacs, [('figures', 'in_file'), + ('descriptions', 'desc')]), + (repeat_source_file_node, ds_report_tacs, [('source_files', 'source_file')]), + ]) + # fmt: on + + return workflow + + +def generate_tac_figures_with_desc(tacs_tsv, metadata, output_dir=None): + """Return TAC figures with description strings for derivative naming.""" + from mriqc.qc.pet import generate_tac_figures + + figures = generate_tac_figures(tacs_tsv, metadata, output_dir) + descriptions = [ + 'tacsCortical', + 'tacsSubcortical', + 'tacsVentricular', + 'tacsOther' + ] + # Ensure matching lengths + if len(figures) != len(descriptions): + raise ValueError('Mismatch in number of figures and descriptions.') + return figures, descriptions diff --git a/mriqc/workflows/utils.py b/mriqc/workflows/utils.py index 695553ea..7b91f34b 100644 --- a/mriqc/workflows/utils.py +++ b/mriqc/workflows/utils.py @@ -62,6 +62,28 @@ def thresh_image(in_file, thres=0.5, out_file=None): return out_file +def threshold_image_percent(in_file, percent=0.2, out_file=None): + """Threshold ``in_file`` at ``percent`` of its maximum intensity.""" + import os.path as op + + import nibabel as nb + import numpy as np + + if out_file is None: + fname, ext = op.splitext(op.basename(in_file)) + if ext == '.gz': + fname, ext2 = op.splitext(fname) + ext = ext2 + ext + out_file = op.abspath(f'{fname}_pctthresh{ext}') + + im = nb.load(in_file) + data = im.get_fdata() + cutoff = percent * np.max(data) + data[data < cutoff] = 0 + nb.Nifti1Image(data, im.affine, im.header).to_filename(out_file) + return out_file + + def spectrum_mask(size): """Creates a mask to filter the image of size size""" import numpy as np