From c11f809f8d5e2cbd278b3252b64af5a720a9ee12 Mon Sep 17 00:00:00 2001 From: Anthony Galassi <28850131+bendhouseart@users.noreply.github.com> Date: Thu, 28 Aug 2025 10:21:06 -0400 Subject: [PATCH 1/3] Update QC report and workflow to produce necessary qa items during main run (#63) * use niivue to dynamically render report with rotating heads * serve nifti's to niivue over http * scan niivue preview added to full report * generating svg's for comparison w/ good ole nipype * add entrypoint for qa reports * standardize commands * refactored from defaced dir to bids dir to make qa usage more clear * clean up toml and fix import --- README.md | 63 + docs/modules.rst | 8 + docs/usage.rst | 75 +- petdeface/pet.py | 20 +- petdeface/petdeface.py | 241 +++- petdeface/qa.py | 1948 ++++++++++-------------------- petdeface/serve.py | 321 +++++ petdeface/templates/niivue.html | 95 ++ petdeface/templates/scan.html | 234 ++++ petdeface/templates/subject.html | 226 ++++ petdeface/templates/viewer.html | 20 + pyproject.toml | 13 +- 12 files changed, 1949 insertions(+), 1315 deletions(-) create mode 100644 petdeface/serve.py create mode 100644 petdeface/templates/niivue.html create mode 100644 petdeface/templates/scan.html create mode 100644 petdeface/templates/subject.html create mode 100644 petdeface/templates/viewer.html diff --git a/README.md b/README.md index ffb6f30..4e96e3e 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ options: Options: 't1' (included T1w template), 'mni' (MNI template), or 'pet' (averaged PET image). --open_browser Open browser to show QA reports after completion + --qa-port QA_PORT Port for NIfTI preview server (default: 8000) ``` Working example usage: @@ -106,6 +107,68 @@ Example usage with template anatomical: petdeface /inputfolder /outputfolder --use_template_anat t1 --n_procs 16 ``` +### Quality Assessment (QA) Reports + +PETdeface includes a comprehensive quality assessment system that generates reports to help validate defacing results. The QA system creates both SVG reports and interactive NIfTI viewers. + +#### QA Report Location + +QA reports are automatically generated in the input BIDS directory under `derivatives/petdeface/qa/`. This includes: +- SVG reports showing before/after defacing comparisons +- Interactive HTML viewers for 3D NIfTI visualization +- An index page linking to all available reports + +#### Running QA Reports + +**Automatic QA generation:** +```bash +petdeface /inputfolder /outputfolder --open_browser +``` + +**Manual QA generation using the separate QA tool:** +```bash +petdeface-qa /inputfolder --open-browser --start-server +``` + +#### NIfTI Preview Server + +For 3D NIfTI visualization, PETdeface can start a local HTTP server to serve NIfTI files. This is required due to browser security restrictions. + +**Start with server:** +```bash +petdeface-qa /inputfolder --start-server --open-browser +``` + +**Custom port:** +```bash +petdeface-qa /inputfolder --start-server --qa-port 8080 +``` + +#### QA Tool Options + +The `petdeface-qa` command provides the following options: + +```bash +usage: petdeface-qa [-h] bids_dir [--output-dir OUTPUT_DIR] + [--open-browser] [--start-server] [--qa-port QA_PORT] + +Generate SVG QA reports for PET deface workflow. + +positional arguments: + bids_dir BIDS directory containing the defaced dataset (with derivatives/petdeface) + +options: + -h, --help show this help message and exit + --output-dir OUTPUT_DIR, --output_dir OUTPUT_DIR + Output directory for HTML files (default: derivatives/petdeface/qa/) + --open-browser Open browser automatically + --start-server Start local HTTP server for NIfTI file access (required for NIfTI viewers) + --qa-port QA_PORT, --qa_port QA_PORT + Port for NIfTI preview server (default: 8000) +``` + +**Note**: The NIfTI preview server is required for 3D visualization due to browser CORS restrictions. Keep the terminal running while viewing NIfTI files. + ### Docker Usage Requirements: diff --git a/docs/modules.rst b/docs/modules.rst index e5e5987..9895ef3 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -26,6 +26,14 @@ petdeface.petdeface module -------------------------- .. automodule:: petdeface.petdeface + :members: + :undoc-members: + :show-inheritance: + +petdeface.qa module +------------------ + +.. automodule:: petdeface.qa :members: :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/docs/usage.rst b/docs/usage.rst index 3d1fede..23867d9 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -127,6 +127,7 @@ options: Options: 't1' (included T1w template), 'mni' (MNI template), or 'pet' (averaged PET image). --open_browser Following defacing this flag will open the browser to view the defacing results + --qa-port QA_PORT Port for NIfTI preview server (default: 8000) Docker Based ------------ @@ -205,4 +206,76 @@ Example usage with template anatomical: .. code-block:: bash - petdeface /inputfolder /outputfolder --use_template_anat t1 --n_procs 16 \ No newline at end of file + petdeface /inputfolder /outputfolder --use_template_anat t1 --n_procs 16 + +Quality Assessment (QA) Reports +------------------------------ + +PETdeface includes a comprehensive quality assessment system that generates reports to help validate defacing results. The QA system creates both SVG reports and interactive NIfTI viewers. + +QA Report Location +~~~~~~~~~~~~~~~~~ + +QA reports are automatically generated in the input BIDS directory under ``derivatives/petdeface/qa/``. This includes: + +- SVG reports showing before/after defacing comparisons +- Interactive HTML viewers for 3D NIfTI visualization +- An index page linking to all available reports + +Running QA Reports +~~~~~~~~~~~~~~~~~ + +**Automatic QA generation:** + +.. code-block:: bash + + petdeface /inputfolder /outputfolder --open_browser + +**Manual QA generation using the separate QA tool:** + +.. code-block:: bash + + petdeface-qa /inputfolder --open-browser --start-server + +NIfTI Preview Server +~~~~~~~~~~~~~~~~~~~ + +For 3D NIfTI visualization, PETdeface can start a local HTTP server to serve NIfTI files. This is required due to browser security restrictions. + +**Start with server:** + +.. code-block:: bash + + petdeface-qa /inputfolder --start-server --open-browser + +**Custom port:** + +.. code-block:: bash + + petdeface-qa /inputfolder --start-server --qa-port 8080 + +QA Tool Options +~~~~~~~~~~~~~~ + +The ``petdeface-qa`` command provides the following options: + +.. code-block:: bash + + usage: petdeface-qa [-h] bids_dir [--output-dir OUTPUT_DIR] + [--open-browser] [--start-server] [--qa-port QA_PORT] + + Generate SVG QA reports for PET deface workflow. + + positional arguments: + bids_dir BIDS directory containing the defaced dataset (with derivatives/petdeface) + + options: + -h, --help show this help message and exit + --output-dir OUTPUT_DIR, --output_dir OUTPUT_DIR + Output directory for HTML files (default: derivatives/petdeface/qa/) + --open-browser Open browser automatically + --start-server Start local HTTP server for NIfTI file access (required for NIfTI viewers) + --qa-port QA_PORT, --qa_port QA_PORT + Port for NIfTI preview server (default: 8000) + +**Note**: The NIfTI preview server is required for 3D visualization due to browser CORS restrictions. Keep the terminal running while viewing NIfTI files. \ No newline at end of file diff --git a/petdeface/pet.py b/petdeface/pet.py index e2631fe..2c4e6a8 100644 --- a/petdeface/pet.py +++ b/petdeface/pet.py @@ -12,6 +12,11 @@ class WeightedAverageInputSpec(BaseInterfaceInputSpec): pet_file = File(exists=True, desc="Dynamic PET", mandatory=True) + sidecar_file = File( + exists=True, + desc="Optional sidecar JSON file for timing info. If not provided, uses sidecar from pet_file.", + mandatory=False, + ) class WeightedAverageOutputSpec(TraitedSpec): @@ -47,9 +52,18 @@ def _run_interface(self, runtime): img = nib.load(pet_file) data = img.get_fdata() - meta = ReadSidecarJSON( - in_file=pet_file, bids_dir=bids_dir, bids_validate=False - ).run() + # Use optional sidecar file if provided, otherwise use pet_file's sidecar + if hasattr(self.inputs, "sidecar_file") and self.inputs.sidecar_file: + sidecar_file = self.inputs.sidecar_file + sidecar_bids_dir = os.path.dirname(sidecar_file) + meta = ReadSidecarJSON( + in_file=sidecar_file, bids_dir=sidecar_bids_dir, bids_validate=False + ).run() + else: + # Default behavior: use pet_file's sidecar + meta = ReadSidecarJSON( + in_file=pet_file, bids_dir=bids_dir, bids_validate=False + ).run() frames_start = np.array(meta.outputs.out_dict["FrameTimesStart"]) frames_duration = np.array(meta.outputs.out_dict["FrameDuration"]) diff --git a/petdeface/petdeface.py b/petdeface/petdeface.py index 3aef18d..4ce5993 100755 --- a/petdeface/petdeface.py +++ b/petdeface/petdeface.py @@ -11,7 +11,9 @@ from pathlib import Path import subprocess from typing import Union -from nipype.interfaces.freesurfer import MRICoreg + + +from nipype.interfaces.freesurfer import MRICoreg, ApplyVolTransform from nipype.interfaces.io import DataSink from nipype.interfaces.base.traits_extension import File as traits_extensionFile from nipype.pipeline import Node @@ -19,6 +21,7 @@ from niworkflows.utils.bids import collect_data from niworkflows.utils.bids import collect_participants from niworkflows.utils.misc import check_valid_fs_license +from nireports.interfaces.reporting.base import SimpleBeforeAfterRPT from petutils.petutils import collect_anat_and_pet from importlib.metadata import version @@ -412,6 +415,10 @@ def init_single_subject_wf( datasink.inputs.substitutions = [ (".face-after", "_desc-faceafter_T1w"), (".face-before", "_desc-facebefore_T1w"), + # Fix SVG naming to match actual file names + ("_t1w_before_after.svg", "_desc-beforeafter_T1w.svg"), + ("_pet_before_after.svg", "_desc-beforeafter_pet.svg"), + ("_registration_to_t1w.svg", "_desc-petregistration_pet.svg"), ] # deface t1w(s) @@ -564,12 +571,66 @@ def init_single_subject_wf( coreg_pet_to_t1w = Node(mricoreg, "coreg_pet_to_t1w") + # warp pet image to T1w space for later QA + warp_pet_to_t1w_space = Node(ApplyVolTransform(), "warp_pet_to_t1w_space") + warp_pet_to_t1w_space.inputs.target_file = t1w_file + + # warp defaced pet image to T1w space for QA comparison + warp_defaced_pet_to_t1w_space = Node( + ApplyVolTransform(), "warp_defaced_pet_to_t1w_space" + ) + warp_defaced_pet_to_t1w_space.inputs.target_file = t1w_file + + # weighted average of the defaced PET image (4D -> 3D) using timing from original PET + # Use the modified WeightedAverage interface with the original PET's sidecar + weighted_average_defaced = Node( + WeightedAverage(pet_file=pet_file), name="weighted_average_defaced" + ) + # Set the original PET file as the sidecar source for timing info + weighted_average_defaced.inputs.sidecar_file = pet_file + deface_pet = Node(ApplyMideface(in_file=pet_file), name="deface_pet") + # create simple before and after reports + t1w_before_after_report = Node( + SimpleBeforeAfterRPT( + before_label="Faced T1w", + after_label="Defaced T1w", + out_report=f"{anat_string}_t1w_before_after.svg", + ), + name=f"{anat_string}_before_and_after_report", + ) + + t1w_before_after_report.inputs.before = t1w_file + + pet_before_and_after_report = Node( + SimpleBeforeAfterRPT( + before_label=f"Faced {pet_string}", + after_label=f"Defaced {pet_string}", + out_report=f"{pet_string}_pet_before_after.svg", + ), + name=f"{pet_string}_before_and_after_report", + ) + pet_to_t1w_coregistration = Node( + SimpleBeforeAfterRPT( + before_label=f"{pet_string}", + after_label=f"{anat_string}", + out_report=f"{pet_string}_registration_to_t1w.svg", + ), + name=f"{pet_string}_to_{anat_string}_registration", + ) + + # Connect all the nodes in the PET workflow pet_wf.connect( [ + # Connection 1: weighted_average -> coreg_pet_to_t1w + # Takes the averaged PET image and feeds it as the source for registration (weighted_average, coreg_pet_to_t1w, [("out_file", "source_file")]), + # Connection 2: coreg_pet_to_t1w -> deface_pet + # Takes the registration matrix (LTA file) and feeds it to the defacing node (coreg_pet_to_t1w, deface_pet, [("out_lta_file", "lta_file")]), + # Connection 3: coreg_pet_to_t1w -> datasink + # Saves the registration matrix (LTA file) to the output directory ( coreg_pet_to_t1w, datasink, @@ -580,6 +641,34 @@ def init_single_subject_wf( ) ], ), + # Connection 4: weighted_average -> warp_pet_to_t1w_space + # Takes the averaged PET image and feeds it as the source for warping to T1w space + ( + weighted_average, + warp_pet_to_t1w_space, + [("out_file", "source_file")], + ), + # Connection 5: coreg_pet_to_t1w -> warp_pet_to_t1w_space + # Takes the registration matrix and feeds it as the transformation for warping + ( + coreg_pet_to_t1w, + warp_pet_to_t1w_space, + [("out_lta_file", "reg_file")], + ), + # Connection 6: warp_pet_to_t1w_space -> datasink + # Saves the PET image warped to T1w space for QA purposes + ( + warp_pet_to_t1w_space, + datasink, + [ + ( + "transformed_file", + f"{pet_string.replace('_', '.')}.pet.@warped{run_id}", + ) + ], + ), + # Connection 7: deface_pet -> datasink + # Saves the final defaced PET image to the output directory ( deface_pet, datasink, @@ -590,17 +679,119 @@ def init_single_subject_wf( ) ], ), + # Connection 8: deface_pet -> weighted_average_defaced + # Takes the defaced PET image (4D) and feeds it to weighted averaging + ( + deface_pet, + weighted_average_defaced, + [("out_file", "pet_file")], + ), + # Connection 9: weighted_average_defaced -> warp_defaced_pet_to_t1w_space + # Takes the weighted averaged defaced PET image (3D) and feeds it as the source for warping + ( + weighted_average_defaced, + warp_defaced_pet_to_t1w_space, + [("out_file", "source_file")], + ), + # Connection 10: coreg_pet_to_t1w -> warp_defaced_pet_to_t1w_space + # Takes the registration matrix and feeds it as the transformation for warping defaced PET + ( + coreg_pet_to_t1w, + warp_defaced_pet_to_t1w_space, + [("out_lta_file", "reg_file")], + ), + # Connection 11: warp_defaced_pet_to_t1w_space -> datasink + # Saves the defaced PET image warped to T1w space for QA comparison + ( + warp_defaced_pet_to_t1w_space, + datasink, + [ + ( + "transformed_file", + f"{pet_string.replace('_', '.')}.pet.@defaced_warped{run_id}", + ) + ], + ), + # create simple before and after report for pet image + ( + warp_pet_to_t1w_space, + pet_before_and_after_report, + [("transformed_file", "before")], + ), + ( + warp_defaced_pet_to_t1w_space, + pet_before_and_after_report, + [("transformed_file", "after")], + ), + # create a simple report showing the registration for pet + ( + warp_defaced_pet_to_t1w_space, + pet_to_t1w_coregistration, + [("transformed_file", "before")], + ), + ( + t1w_workflows[t1w_file]["workflow"].get_node( + f"deface_t1w_{t1w_workflows[t1w_file]['anat_string']}" + ), + pet_to_t1w_coregistration, + [("out_file", "after")], + ), + # create a before and after image for the t1w defacing + ( + t1w_workflows[t1w_file]["workflow"].get_node( + f"deface_t1w_{t1w_workflows[t1w_file]['anat_string']}" + ), + t1w_before_after_report, + [("out_file", "after")], + ), + # Move svg reports to datasink + ( + pet_before_and_after_report, + datasink, + [ + ( + "out_report", + f"{pet_string.replace('_','.')}.pet.@beforeafter{run_id}", + ) + ], + ), + ( + pet_to_t1w_coregistration, + datasink, + [ + ( + "out_report", + f"{pet_string.replace('_', '.')}.pet.@registration{run_id}", + ) + ], + ), + ( + t1w_before_after_report, + datasink, + [ + ( + "out_report", + f"{anat_string.replace('_', '.')}.anat.@beforeafter{run_id}", + ) + ], + ), ] ) + # Connect the T1w workflow to the PET workflow + # This connects the facemask from the T1w defacing to the PET defacing workflow.connect( [ ( - t1w_workflows[t1w_file]["workflow"], - pet_wf, + t1w_workflows[t1w_file][ + "workflow" + ], # Source: T1w defacing workflow + pet_wf, # Target: PET workflow [ ( + # Output from T1w defacing: the facemask file f"deface_t1w_{t1w_workflows[t1w_file]['anat_string']}.out_facemask", + # Input to PET defacing: the facemask file (same mask used for both T1w and PET) "deface_pet.facemask", ) ], @@ -1086,6 +1277,7 @@ def cli(): help="Only deface anatomical images", ) parser.add_argument( + "--participant-label", "--participant_label", "-pl", help="The label(s) of the participant/subject to be processed. When specifying multiple subjects separate them with spaces.", @@ -1109,11 +1301,17 @@ def cli(): help="Run in singularity container", ), parser.add_argument( + "--n-procs", "--n_procs", help="Number of processors to use when running the workflow", default=2, ) - parser.add_argument("--skip_bids_validator", action="store_true", default=False) + parser.add_argument( + "--skip-bids-validator", + "--skip_bids_validator", + action="store_true", + default=False, + ) parser.add_argument( "--version", "-v", @@ -1132,6 +1330,7 @@ def cli(): default="adjacent", ) parser.add_argument( + "--remove-existing", "--remove_existing", "-r", help="Remove existing output files in output_dir.", @@ -1139,12 +1338,14 @@ def cli(): default=False, ) parser.add_argument( + "--preview-pics", "--preview_pics", help="Create preview pictures of defacing, defaults to false for docker", action="store_true", default=False, ) parser.add_argument( + "--participant-label-exclude", "--participant_label_exclude", help="Exclude a subject(s) from the defacing workflow. e.g. --participant_label_exclude sub-01 sub-02", type=str, @@ -1153,6 +1354,7 @@ def cli(): default=[], ) parser.add_argument( + "--session-label", "--session_label", help="Select only a specific session(s) to include in the defacing workflow", type=str, @@ -1161,6 +1363,7 @@ def cli(): default=[], ) parser.add_argument( + "--session-label-exclude", "--session_label_exclude", help="Select a specific session(s) to exclude from the defacing workflow", type=str, @@ -1169,18 +1372,18 @@ def cli(): default=[], ) parser.add_argument( - "--use_template_anat", - help="Use template anatomical image when no T1w is available for PET scans. Options: 't1' (included T1w template), 'mni' (MNI template), or 'pet' (averaged PET image).", - type=str, - required=False, - default=False, - ) - parser.add_argument( + "--open-browser", "--open_browser", - help="Open browser to show QA reports after completion", action="store_true", default=False, ) + parser.add_argument( + "--qa-port", + "--qa_port", + type=int, + default=8000, + help="User can manually choose a default port to serve the nifti images at for 3D previewing of defacing should the default value of 8000 not work.", + ) arguments = parser.parse_args() return arguments @@ -1390,15 +1593,12 @@ def main(): # noqa: max-complexity: 12 try: # Determine the defaced directory based on placement - # Run QA + # Run QA with server if open_browser is requested qa_result = run_qa( - faced_dir=str(args.bids_dir), - defaced_dir=str(args.output_dir), - output_dir=str(args.bids_dir / "derivatives" / "petdeface"), - subject=( - " ".join(args.participant_label) if args.participant_label else None - ), + defaced_dir=str(args.bids_dir), open_browser=args.open_browser, + start_server=args.open_browser, # Start server when opening browser + server_port=args.qa_port, ) print("\n" + "=" * 60) @@ -1410,6 +1610,9 @@ def main(): # noqa: max-complexity: 12 print(f"\nError generating QA reports: {e}") print("QA report generation failed, but defacing completed successfully.") + print("launch QA reports with:") + print(f"petdeface-qa {args.bids_dir} --open_browser --start_server") + if __name__ == "__main__": main() diff --git a/petdeface/qa.py b/petdeface/qa.py index 108349e..3b15bc8 100644 --- a/petdeface/qa.py +++ b/petdeface/qa.py @@ -1,24 +1,19 @@ -import argparse +#!/usr/bin/env python3 +""" +Simple QA system for PET deface SVG reports. +""" + import os -import tempfile -import shutil -import sys -from glob import glob -import nilearn -from nilearn import plotting +import glob +import argparse import webbrowser -import nibabel as nib -import numpy as np -from nilearn import image -import matplotlib.pyplot as plt -from matplotlib.animation import FuncAnimation -import imageio -import multiprocessing as mp -from functools import partial -import seaborn as sns -from PIL import Image, ImageDraw -from nipype import Workflow, Node -from tempfile import TemporaryDirectory +import json +import http.server +import socketserver +import time +import subprocess +import signal +import sys from pathlib import Path # Handle imports for both script and module execution (including debugger) @@ -39,1368 +34,758 @@ from nireports.interfaces.reporting.base import SimpleBeforeAfterRPT -def preprocess_single_subject(s, output_dir): - """Preprocess a single subject's images (for parallel processing).""" - temp_dir = os.path.join(output_dir, "temp_3d_images") - - # Debug: Print what files we're processing - print(f"Preprocessing subject {s['id']}:") - print(f" Original: {s['orig_path']}") - print(f" Defaced: {s['defaced_path']}") - - # Extract BIDS suffix from original path to preserve meaningful naming - orig_basename = os.path.basename(s["orig_path"]) - defaced_basename = os.path.basename(s["defaced_path"]) - - # Extract the meaningful part (e.g., "sub-01_ses-baseline_T1w" or "sub-01_ses-baseline_pet") - def extract_bids_name(basename): - # Remove .nii.gz extension - name = basename.replace(".nii.gz", "").replace(".nii", "") - return name - - orig_bids_name = extract_bids_name(orig_basename) - defaced_bids_name = extract_bids_name(defaced_basename) - - # Preprocess original image - orig_result = load_and_preprocess_image(s["orig_path"]) - if isinstance(orig_result, nib.Nifti1Image): - # Need to save the averaged image with meaningful name - orig_3d_path = os.path.join(temp_dir, f"orig_{orig_bids_name}.nii.gz") - nib.save(orig_result, orig_3d_path) - orig_img = orig_result - else: - # Already 3D, use original path and load image - orig_3d_path = orig_result - orig_img = nib.load(orig_result) - - # Preprocess defaced image - defaced_result = load_and_preprocess_image(s["defaced_path"]) - if isinstance(defaced_result, nib.Nifti1Image): - # Need to save the averaged image with meaningful name - defaced_3d_path = os.path.join(temp_dir, f"defaced_{defaced_bids_name}.nii.gz") - nib.save(defaced_result, defaced_3d_path) - defaced_img = defaced_result - else: - # Already 3D, use original path and load image - defaced_3d_path = defaced_result - defaced_img = nib.load(defaced_result) - - # Create new subject dict with preprocessed paths (update paths to 3D versions) - preprocessed_subject = { - "id": s["id"], - "orig_path": orig_3d_path, # Update to 3D path - "defaced_path": defaced_3d_path, # Update to 3D path - "orig_img": orig_img, # Keep loaded image for direct use - "defaced_img": defaced_img, # Keep loaded image for direct use - } - - print(f" Preprocessed {s['id']}") - return preprocessed_subject - - -def preprocess_images(subjects: dict, output_dir, n_jobs=None): - """Preprocess all images once: load and convert 4D to 3D if needed.""" - print("Preprocessing images (4D→3D conversion)...") - - # Create temp directory - temp_dir = os.path.join(output_dir, "temp_3d_images") - os.makedirs(temp_dir, exist_ok=True) - - # Set number of jobs for parallel processing - if n_jobs is None: - n_jobs = mp.cpu_count() - print(f"Using {n_jobs} parallel processes for preprocessing") +def collect_svg_reports(defaced_dir, output_dir): + """Collect SVG files from derivatives/petdeface directory.""" + derivatives_dir = os.path.join(defaced_dir, "derivatives", "petdeface") + if not os.path.exists(derivatives_dir): + print( + f"Warning: derivatives/petdeface directory not found at {derivatives_dir}" + ) + print("Looking for SVG files in the main defaced directory...") + derivatives_dir = defaced_dir - # Process subjects in parallel - with mp.Pool(processes=n_jobs) as pool: - # Create a partial function with the output_dir fixed - preprocess_func = partial(preprocess_single_subject, output_dir=output_dir) + # Find all SVG files recursively, excluding the qa directory + svg_files = [] + for svg_file in glob.glob( + os.path.join(derivatives_dir, "**", "*.svg"), recursive=True + ): + # Skip files in the qa directory + if "qa" not in svg_file: + svg_files.append(svg_file) - # Process all subjects in parallel - preprocessed_subjects = pool.map(preprocess_func, subjects) + if not svg_files: + print("No SVG files found!") + return [] - print(f"Preprocessed {len(preprocessed_subjects)} subjects") - return preprocessed_subjects + print(f"Found {len(svg_files)} SVG files") + # Just return the original file paths - no copying needed + for svg_file in svg_files: + print(f"Found: {Path(svg_file).name}") -def generate_simple_before_and_after(preprocessed_subjects: dict, output_dir): - if not output_dir: - output_dir = TemporaryDirectory() - wf = Workflow( - name="simple_before_after_report", base_dir=Path(output_dir) / "images/" - ) + return svg_files - # Create a list to store all nodes - nodes = [] - - for s in preprocessed_subjects: - # only run this on the T1w images for now - if "T1w" in s["orig_path"]: - o_path = Path(s["orig_path"]) - # Create a valid node name by replacing invalid characters but preserving session info - # Use the full path to ensure uniqueness - path_parts = s["orig_path"].split(os.sep) - subject_part = next( - (p for p in path_parts if p.startswith("sub-")), s["id"] - ) - session_part = next((p for p in path_parts if p.startswith("ses-")), "") - if session_part: - valid_name = f"before_after_{subject_part}_{session_part}".replace( - "-", "_" - ) - else: - valid_name = f"before_after_{subject_part}".replace("-", "_") - node = Node( - SimpleBeforeAfterRPT( - before=s["orig_path"], - after=s["defaced_path"], - before_label="Original", - after_label="Defaced", - out_report=f"{s['id']}_simple_before_after.svg", - ), - name=valid_name, - ) - nodes.append(node) +def kill_process_on_port(port): + """Kill any process using the specified port.""" + try: + # Find process using the port + result = subprocess.run( + ["lsof", "-ti", str(port)], capture_output=True, text=True, check=False + ) - # Add all nodes to the workflow - wf.add_nodes(nodes) + if result.returncode == 0 and result.stdout.strip(): + pids = result.stdout.strip().split("\n") + for pid in pids: + if pid: + print(f"Killing process {pid} using port {port}") + try: + os.kill(int(pid), signal.SIGKILL) + except ProcessLookupError: + print(f"Process {pid} already terminated") + except ValueError: + print(f"Invalid PID: {pid}") + + # Give it a moment to fully terminate + time.sleep(0.5) + print(f"Port {port} is now free") + else: + print(f"Port {port} is already free") - # Only run if we have nodes to process - if nodes: - wf.run(plugin="MultiProc", plugin_args={"n_procs": mp.cpu_count()}) - # Collect SVG files and move them to images folder - collect_svg_reports(wf, output_dir) - else: - print("No T1w images found for SVG report generation") + except FileNotFoundError: + print("Warning: 'lsof' command not found, cannot check for existing processes") + except Exception as e: + print(f"Error killing process on port {port}: {e}") -def collect_svg_reports(wf, output_dir): - """Collect SVG reports from workflow and move them to images folder.""" - import glob +def start_local_server(port=8000, directory=None): + """Start a simple HTTP server to serve files locally.""" + if directory is None: + directory = os.getcwd() - # Find all SVG files in the workflow directory - workflow_dir = wf.base_dir - svg_files = glob.glob(os.path.join(workflow_dir, "**", "*.svg"), recursive=True) + os.chdir(directory) - print(f"Found {len(svg_files)} SVG reports") + class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): + def end_headers(self): + # Add CORS headers to allow cross-origin requests + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + self.send_header("Access-Control-Allow-Headers", "*") + super().end_headers() - # Move each SVG to the images folder - for svg_file in svg_files: - filename = os.path.basename(svg_file) - dest_path = os.path.join(output_dir, "images", filename) - shutil.move(svg_file, dest_path) - print(f" Moved: {filename}") + handler = CustomHTTPRequestHandler - # Create HTML page for SVG reports - create_svg_index_html(svg_files, output_dir) + with socketserver.TCPServer(("", port), handler) as httpd: + print(f"Server started at http://localhost:{port}") + print(f"Serving files from: {directory}") + print("Press Ctrl+C to stop the server") + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\nServer stopped.") -def create_svg_index_html(svg_files, output_dir): - """Create HTML index page for SVG reports.""" - svg_entries = "" - for svg_file in svg_files: - filename = os.path.basename(svg_file) - subject_id = filename.replace("_simple_before_after.svg", "") - - svg_entries += f""" -
Before/After comparison reports using nireports
-Side-by-side comparison of original vs defaced neuroimaging data
+Switching between Original and Defaced images
-