Download code/a1_pipeline/spatial.py from Mohith202/csai: direct link, hf CLI and curl.
- Browser
- Download file 6.66 kB
-
https://huggingface.co/spaces/Mohith202/csai/resolve/main/code/a1_pipeline/spatial.py
- Command line
-
hf download hf://spaces/Mohith202/csai/code/a1_pipeline/spatial.py
-
curl -L -o spatial.py https://huggingface.co/spaces/Mohith202/csai/resolve/main/code/a1_pipeline/spatial.py
6.66 kB
| """Spatial harmonization utilities for A1 baseline bootstrap.""" | |
| from __future__ import annotations | |
| from dataclasses import asdict, dataclass | |
| from pathlib import Path | |
| from typing import Any | |
| import nibabel as nib | |
| from nibabel.processing import resample_from_to | |
| import numpy as np | |
| import pandas as pd | |
| class RunMaskQC: | |
| """Per-run QC record used when building the analysis mask.""" | |
| subject: str | |
| run: int | |
| derivatives_bold_path: str | |
| run_mask_method: str | |
| needs_resample_to_reference: bool | |
| run_mask_voxels: int | |
| def _make_reference_3d_from_manifest(manifest_df: pd.DataFrame) -> nib.Nifti1Image: | |
| if manifest_df.empty: | |
| raise ValueError("Manifest is empty; cannot create spatial reference") | |
| first_path = Path(str(manifest_df.iloc[0]["derivatives_bold_path"])) | |
| first_img = nib.load(str(first_path)) | |
| if len(first_img.shape) != 4: | |
| raise ValueError(f"Expected 4D BOLD image but got shape={first_img.shape} for {first_path}") | |
| shape_3d = first_img.shape[:3] | |
| return nib.Nifti1Image(np.zeros(shape_3d, dtype=np.uint8), first_img.affine) | |
| def _same_grid(img_a: nib.Nifti1Image, img_b: nib.Nifti1Image, atol: float = 1e-5) -> bool: | |
| return img_a.shape == img_b.shape and np.allclose(img_a.affine, img_b.affine, atol=atol) | |
| def _extract_run_mask_from_bold( | |
| bold_img: nib.Nifti1Image, | |
| method: str, | |
| epsilon: float, | |
| ) -> nib.Nifti1Image: | |
| shape = bold_img.shape | |
| if len(shape) != 4: | |
| raise ValueError(f"Expected 4D BOLD image, got shape={shape}") | |
| if method == "first_volume_nonzero": | |
| volume = np.asanyarray(bold_img.dataobj[..., 0]) | |
| run_mask = np.abs(volume) > epsilon | |
| elif method == "temporal_any_nonzero": | |
| run_mask = np.zeros(shape[:3], dtype=bool) | |
| for time_index in range(shape[3]): | |
| volume = np.asanyarray(bold_img.dataobj[..., time_index]) | |
| run_mask |= np.abs(volume) > epsilon | |
| else: | |
| raise ValueError(f"Unsupported run mask method: {method}") | |
| return nib.Nifti1Image(run_mask.astype(np.uint8), bold_img.affine) | |
| def _resample_binary_mask_to_reference( | |
| binary_mask_img: nib.Nifti1Image, | |
| reference_3d_img: nib.Nifti1Image, | |
| ) -> nib.Nifti1Image: | |
| resampled = resample_from_to( | |
| binary_mask_img, | |
| (reference_3d_img.shape, reference_3d_img.affine), | |
| order=0, | |
| ) | |
| binary_data = (resampled.get_fdata() > 0.5).astype(np.uint8) | |
| return nib.Nifti1Image(binary_data, reference_3d_img.affine) | |
| def build_symmetric_analysis_mask( | |
| manifest_df: pd.DataFrame, | |
| run_mask_method: str = "first_volume_nonzero", | |
| epsilon: float = 1e-6, | |
| ) -> tuple[nib.Nifti1Image, dict[tuple[str, int], np.ndarray], pd.DataFrame, dict[str, Any]]: | |
| """Build analysis mask in a canonical grid with left-right symmetry enforced. | |
| Returns: | |
| analysis_mask_img: symmetric boolean mask image in reference grid | |
| run_mask_map: (subject, run) -> boolean 3D run mask in reference grid | |
| run_mask_qc_df: per-run mask stats including whether resampling was needed | |
| mask_qc: summary dictionary | |
| """ | |
| if manifest_df.empty: | |
| raise ValueError("Manifest is empty; cannot build analysis mask") | |
| required_columns = {"subject", "run", "derivatives_bold_path"} | |
| missing = required_columns.difference(manifest_df.columns) | |
| if missing: | |
| raise ValueError(f"Manifest missing required columns: {sorted(missing)}") | |
| reference_3d_img = _make_reference_3d_from_manifest(manifest_df) | |
| reference_affine = reference_3d_img.affine | |
| run_mask_map: dict[tuple[str, int], np.ndarray] = {} | |
| run_qc_rows: list[RunMaskQC] = [] | |
| intersection_mask: np.ndarray | None = None | |
| n_resampled = 0 | |
| for row in manifest_df.itertuples(index=False): | |
| subject = str(getattr(row, "subject")) | |
| run = int(getattr(row, "run")) | |
| bold_path = Path(str(getattr(row, "derivatives_bold_path"))) | |
| bold_img = nib.load(str(bold_path)) | |
| run_mask_img = _extract_run_mask_from_bold( | |
| bold_img=bold_img, | |
| method=run_mask_method, | |
| epsilon=epsilon, | |
| ) | |
| needs_resample = not _same_grid(run_mask_img, reference_3d_img) | |
| if needs_resample: | |
| n_resampled += 1 | |
| run_mask_img = _resample_binary_mask_to_reference(run_mask_img, reference_3d_img) | |
| run_mask_bool = run_mask_img.get_fdata() > 0.5 | |
| run_key = (subject, run) | |
| run_mask_map[run_key] = run_mask_bool | |
| if intersection_mask is None: | |
| intersection_mask = run_mask_bool.copy() | |
| else: | |
| intersection_mask &= run_mask_bool | |
| run_qc_rows.append( | |
| RunMaskQC( | |
| subject=subject, | |
| run=run, | |
| derivatives_bold_path=str(bold_path), | |
| run_mask_method=run_mask_method, | |
| needs_resample_to_reference=needs_resample, | |
| run_mask_voxels=int(run_mask_bool.sum()), | |
| ) | |
| ) | |
| if intersection_mask is None: | |
| raise RuntimeError("Could not compute intersection mask from manifest entries") | |
| swapped_mask = np.flip(intersection_mask, axis=0) | |
| symmetric_mask = intersection_mask & swapped_mask | |
| analysis_mask_img = nib.Nifti1Image(symmetric_mask.astype(np.uint8), reference_affine) | |
| shape_x = symmetric_mask.shape[0] | |
| mid_x = shape_x // 2 | |
| left_voxels = int(symmetric_mask[:mid_x, :, :].sum()) | |
| right_voxels = int(symmetric_mask[-mid_x:, :, :].sum()) if mid_x > 0 else 0 | |
| run_mask_qc_df = pd.DataFrame([asdict(row) for row in run_qc_rows]) | |
| if not run_mask_qc_df.empty: | |
| run_mask_qc_df = run_mask_qc_df.sort_values(["subject", "run"]).reset_index(drop=True) | |
| orientation_codes = "".join(nib.aff2axcodes(reference_affine)) | |
| mask_qc: dict[str, Any] = { | |
| "run_mask_method": run_mask_method, | |
| "n_manifest_rows": int(len(manifest_df)), | |
| "n_run_masks": int(len(run_mask_map)), | |
| "n_resampled_run_masks": int(n_resampled), | |
| "reference_shape": [int(dim) for dim in analysis_mask_img.shape], | |
| "reference_orientation": orientation_codes, | |
| "intersection_voxels": int(intersection_mask.sum()), | |
| "symmetric_mask_voxels": int(symmetric_mask.sum()), | |
| "left_hemisphere_voxels": left_voxels, | |
| "right_hemisphere_voxels": right_voxels, | |
| } | |
| if not run_mask_qc_df.empty: | |
| mask_qc["run_mask_voxels_min"] = int(run_mask_qc_df["run_mask_voxels"].min()) | |
| mask_qc["run_mask_voxels_max"] = int(run_mask_qc_df["run_mask_voxels"].max()) | |
| return analysis_mask_img, run_mask_map, run_mask_qc_df, mask_qc | |