Files
hypertower/scripts/grid_search_analytics/compare_suspect_geometry_vs_image.py
T
2026-02-24 10:39:48 +01:00

250 lines
8.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""Compare suspect AUC from image tower vs crop-derived geometry (CDR)."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import sys
import numpy as np
import pandas as pd
from PIL import Image
from sklearn.metrics import roc_auc_score
REPO_ROOT = Path(__file__).resolve().parents[2]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from classes import build_papila_clinical
from classes.hypertower import UNetImageCropper, ManifestImageCropper
# ---------------------------
# Config (edit in IDE)
# ---------------------------
RUN_DIRS = [
Path("analysis_data/1030_Balanced_Unet_Perimg_Resnet_SE16NormB_SE16NormT_multi_fused/1030_Balanced_Unet_Perimg_Resnet_SE16NormB_SE16NormT_multi_fused_20251030_091842"),
Path("analysis_data/1030_Balanced_GT_Perimg_Resnet_SE16NormB_SE16NormT_multi_fused/1030_Balanced_GT_Perimg_Resnet_SE16NormB_SE16NormT_multi_fused_20251030_113730"),
]
GEOM_CACHE_ROOT = Path("analysis_data/geometry_cache")
SUSPECT_LABEL = 2
def _load_json(path: Path) -> Dict:
if not path.exists():
return {}
try:
return json.loads(path.read_text())
except Exception:
return {}
def _drop_holdout_rows(clinical, holdout_path: Path) -> None:
if not holdout_path.exists():
return
holdout = pd.read_csv(holdout_path)
if holdout.empty:
return
if "Patient ID" not in holdout.columns or "eyeID" not in holdout.columns:
return
holdout_keys = set(zip(holdout["Patient ID"].astype(int), holdout["eyeID"].astype(str)))
df = clinical.df.copy()
df["_key"] = list(zip(df["Patient ID"].astype(int), df["eyeID"].astype(str)))
df = df[~df["_key"].isin(holdout_keys)].drop(columns=["_key"]).reset_index(drop=True)
clinical.frames = [df.copy()]
clinical.df = df.copy()
clinical._infer_or_validate_feature_types()
clinical._compute_numeric_stats()
clinical._build_cat_maps()
clinical._compute_feature_dim()
clinical._build_kfold_indices()
def _make_cropper(args: Dict, cache_dir: Path):
manifest = args.get("img_crop_manifest")
if not manifest:
raise RuntimeError("img_crop_manifest missing; cannot compute geometry features.")
scale = float(args.get("img_crop_scale", 2.5))
target_size = int(args.get("img_crop_size", 224))
use_gt = bool(args.get("img_crop_gt", False))
if use_gt:
return ManifestImageCropper(
manifest_path=Path(manifest),
scale=scale,
target_size=target_size,
cache_dir=cache_dir,
)
weights = args.get("img_crop_weights")
if not weights:
raise RuntimeError("img_crop_weights missing for UNet cropper.")
normalize = args.get("img_crop_normalize", "per_image")
threshold = float(args.get("img_crop_threshold", 0.5))
tta = bool(args.get("img_crop_tta", False))
return UNetImageCropper(
manifest_path=Path(manifest),
weights_path=Path(weights),
normalize=normalize,
threshold=threshold,
tta=tta,
scale=scale,
target_size=target_size,
cache_dir=cache_dir,
)
def _geometry_scores(
clinical,
cropper,
test_df: pd.DataFrame,
) -> Tuple[np.ndarray, np.ndarray]:
scores: List[float] = []
keep_mask: List[bool] = []
for _, row in test_df.iterrows():
img_path = clinical.get_image_path(row)
try:
image = Image.open(img_path).convert("RGB")
except Exception:
scores.append(float("nan"))
keep_mask.append(False)
continue
feats = cropper.geometry_features(image, img_path)
if feats is None or len(feats) == 0:
scores.append(float("nan"))
keep_mask.append(False)
else:
scores.append(float(feats[0])) # area_ratio (CDR)
keep_mask.append(True)
return np.asarray(scores, dtype=float), np.asarray(keep_mask, dtype=bool)
def _suspect_auc(y_true: np.ndarray, scores: np.ndarray) -> float:
y = (y_true == SUSPECT_LABEL).astype(int)
if y.sum() == 0 or y.sum() == len(y):
return float("nan")
return float(roc_auc_score(y, scores))
def main() -> None:
rows: List[Dict[str, object]] = []
for run_dir in RUN_DIRS:
cli_path = run_dir / "cli_args.json"
cli_args = _load_json(cli_path)
if not cli_args:
print(f"[warn] Missing cli_args.json in {run_dir}")
continue
label_col = cli_args.get("label_col", "Diagnosis")
cat_cols = cli_args.get("cat_cols", ["Gender", "Phakic/Pseudophakic"])
n_splits = int(cli_args.get("n_splits", 5))
fold_seed = int(cli_args.get("fold_seed", 42))
eval_mode = str(cli_args.get("eval_mode", "multiclass")).lower()
clinical = build_papila_clinical(
image_dir=cli_args.get("image_dir", "Papila/FundusImages"),
clinical_dir=cli_args.get("clinical_dir", "Papila/ClinicalData"),
label_col=label_col,
cat_cols=cat_cols,
n_splits=n_splits,
random_seed=fold_seed,
)
if eval_mode == "binary":
clinical.df = clinical.df[clinical.df[label_col].isin([0, 1])].reset_index(drop=True)
clinical.frames = [clinical.df.copy()]
clinical._infer_or_validate_feature_types()
clinical._compute_numeric_stats()
clinical._build_cat_maps()
clinical._compute_feature_dim()
clinical._build_kfold_indices()
_drop_holdout_rows(clinical, run_dir / "holdout.csv")
cache_dir = GEOM_CACHE_ROOT / run_dir.name
cache_dir.mkdir(parents=True, exist_ok=True)
cropper = _make_cropper(cli_args, cache_dir=cache_dir)
all_geom_scores: List[float] = []
all_img_scores: List[float] = []
all_y: List[int] = []
for fold in range(n_splits):
y_path = run_dir / f"fold{fold}_y_true.npy"
p_img_path = run_dir / f"fold{fold}_probs_img.npy"
if not y_path.exists() or not p_img_path.exists():
continue
y_true = np.load(y_path)
probs_img = np.load(p_img_path)
if probs_img.ndim != 2 or probs_img.shape[1] <= SUSPECT_LABEL:
continue
_, test_df = clinical.get_split_dfs(fold)
if len(test_df) != len(y_true):
print(
f"[warn] {run_dir.name} fold{fold}: test_df len {len(test_df)} != y_true len {len(y_true)}"
)
geom_scores, keep_mask = _geometry_scores(clinical, cropper, test_df)
if keep_mask.sum() == 0:
print(f"[warn] {run_dir.name} fold{fold}: no valid geometry features")
continue
y_fold = y_true[: len(geom_scores)][keep_mask]
geom_fold = geom_scores[keep_mask]
img_fold = probs_img[: len(geom_scores), SUSPECT_LABEL][keep_mask]
geom_auc = _suspect_auc(y_fold, geom_fold)
img_auc = _suspect_auc(y_fold, img_fold)
rows.append(
{
"run": run_dir.name,
"fold": fold,
"metric": "suspect_auc",
"image_auc": img_auc,
"geometry_auc": geom_auc,
"n": int(len(y_fold)),
}
)
all_geom_scores.append(geom_fold)
all_img_scores.append(img_fold)
all_y.append(y_fold)
if all_y:
y_all = np.concatenate(all_y)
geom_all = np.concatenate(all_geom_scores)
img_all = np.concatenate(all_img_scores)
rows.append(
{
"run": run_dir.name,
"fold": "all",
"metric": "suspect_auc",
"image_auc": _suspect_auc(y_all, img_all),
"geometry_auc": _suspect_auc(y_all, geom_all),
"n": int(len(y_all)),
}
)
if not rows:
raise SystemExit("No results produced; check run paths and files.")
df = pd.DataFrame(rows)
out_path = GEOM_CACHE_ROOT / "suspect_auc_geometry_vs_image.csv"
out_path.parent.mkdir(parents=True, exist_ok=True)
df.to_csv(out_path, index=False)
print(df.to_string(index=False, float_format=lambda x: f"{x:.4f}"))
print(f"\nSaved: {out_path}")
if __name__ == "__main__":
main()