moved_repo_first_update
This commit is contained in:
+376
@@ -0,0 +1,376 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare cached crop bounds/features vs GT-derived crops from the manifest."""
|
||||
from __future__ import annotations
|
||||
|
||||
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
|
||||
import torch
|
||||
from torchvision import transforms
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from classes.hypertower import ManifestImageCropper, UNetImageCropper
|
||||
from classes.refuge_segmentation import UNet as RefugeUNet
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# Config (edit in IDE)
|
||||
# ---------------------------
|
||||
CACHE_DIR = Path("analysis_data/hypertower_crops")
|
||||
MANIFEST_PATH = Path("manifest.csv")
|
||||
IMAGE_DIR = Path("Papila/FundusImages")
|
||||
SCALE = 2.5
|
||||
MAX_SAMPLES = 200 # set None to scan all
|
||||
TOL_BOUNDS = 1.0 # pixels
|
||||
TOL_FEATURES = 1e-3
|
||||
UNET_VARIANTS = [
|
||||
("norm_imagenet", Path("models/unet_segmenter/norm_imagenet/best.pt"), "imagenet"),
|
||||
("normalize_none", Path("models/unet_segmenter/normalize_none/best.pt"), "none"),
|
||||
("norm_per_image", Path("models/unet_segmenter/norm_per_image/best.pt"), "per_image"),
|
||||
]
|
||||
REFUGE_SEG_WEIGHTS = Path("models/refuge/segmentation/refuge_segmentation_best.pt")
|
||||
|
||||
|
||||
def _load_cache(path: Path) -> Optional[Dict[str, np.ndarray]]:
|
||||
try:
|
||||
data = np.load(path, allow_pickle=False)
|
||||
except Exception:
|
||||
return None
|
||||
return {k: data[k] for k in data.files}
|
||||
|
||||
|
||||
def _parse_stem(path: Path) -> str:
|
||||
# expects RET###OS_s250.npz -> RET###OS
|
||||
stem = path.stem
|
||||
if "_s" in stem:
|
||||
stem = stem.split("_s")[0]
|
||||
return stem
|
||||
|
||||
|
||||
def _image_path_from_stem(stem: str) -> Optional[Path]:
|
||||
cand = IMAGE_DIR / f"{stem}.jpg"
|
||||
if cand.exists():
|
||||
return cand
|
||||
cand = IMAGE_DIR / f"{stem}.png"
|
||||
if cand.exists():
|
||||
return cand
|
||||
return None
|
||||
|
||||
|
||||
def _gt_info(
|
||||
cropper: ManifestImageCropper, image_path: Path
|
||||
) -> Optional[Dict[str, float]]:
|
||||
try:
|
||||
image = Image.open(image_path).convert("RGB")
|
||||
except Exception:
|
||||
return None
|
||||
info = cropper._compute_crop_info(image, image_path)
|
||||
return info
|
||||
|
||||
|
||||
def _unet_info(
|
||||
cropper: UNetImageCropper, image_path: Path
|
||||
) -> Optional[Dict[str, float]]:
|
||||
try:
|
||||
image = Image.open(image_path).convert("RGB")
|
||||
except Exception:
|
||||
return None
|
||||
info = cropper._compute_crop_info(image, image_path)
|
||||
return info
|
||||
|
||||
|
||||
def _load_refuge_model(device: str) -> Optional[RefugeUNet]:
|
||||
if not REFUGE_SEG_WEIGHTS.exists():
|
||||
return None
|
||||
model = RefugeUNet()
|
||||
try:
|
||||
state = torch.load(REFUGE_SEG_WEIGHTS, map_location=device)
|
||||
except Exception:
|
||||
return None
|
||||
state_dict = state.get("model", state) if isinstance(state, dict) else state
|
||||
try:
|
||||
model.load_state_dict(state_dict)
|
||||
except Exception:
|
||||
return None
|
||||
model.to(device)
|
||||
model.eval()
|
||||
return model
|
||||
|
||||
|
||||
def _refuge_seg_info(
|
||||
model: RefugeUNet, device: str, image_path: Path
|
||||
) -> Optional[Dict[str, float]]:
|
||||
try:
|
||||
image = Image.open(image_path).convert("RGB")
|
||||
except Exception:
|
||||
return None
|
||||
original_size = image.size
|
||||
image_resized = image.resize((512, 512), Image.BILINEAR)
|
||||
tensor = transforms.ToTensor()(image_resized).unsqueeze(0).to(device)
|
||||
with torch.no_grad():
|
||||
logits = model(tensor)
|
||||
mask = torch.sigmoid(logits)[0, 0]
|
||||
mask_np = (mask.cpu().numpy() > 0.5).astype(np.float32)
|
||||
mask_img = Image.fromarray(mask_np)
|
||||
mask_img = mask_img.resize(original_size, Image.NEAREST)
|
||||
mask_np = np.array(mask_img, dtype=np.float32)
|
||||
coords = np.argwhere(mask_np > 0.5)
|
||||
if coords.size == 0:
|
||||
return None
|
||||
ys, xs = coords[:, 0], coords[:, 1]
|
||||
centre_x = float(xs.mean())
|
||||
centre_y = float(ys.mean())
|
||||
width = float(xs.max() - xs.min())
|
||||
height = float(ys.max() - ys.min())
|
||||
diameter = max(width, height)
|
||||
radius = diameter / 2.0
|
||||
crop_radius = radius * SCALE
|
||||
left = max(0.0, centre_x - crop_radius)
|
||||
upper = max(0.0, centre_y - crop_radius)
|
||||
right = min(float(image.width), centre_x + crop_radius)
|
||||
lower = min(float(image.height), centre_y + crop_radius)
|
||||
return {
|
||||
"left": left,
|
||||
"upper": upper,
|
||||
"right": right,
|
||||
"lower": lower,
|
||||
}
|
||||
|
||||
|
||||
def _diff_bounds(cache: Dict[str, np.ndarray], gt: Dict[str, float]) -> Optional[float]:
|
||||
keys = ("left", "upper", "right", "lower")
|
||||
if not all(k in cache for k in keys):
|
||||
return None
|
||||
diffs = [abs(float(cache[k]) - float(gt[k])) for k in keys]
|
||||
return float(max(diffs))
|
||||
|
||||
|
||||
def _diff_features(
|
||||
cache: Dict[str, np.ndarray], gt: Dict[str, float]
|
||||
) -> Optional[float]:
|
||||
if "features" not in cache or "features" not in gt:
|
||||
return None
|
||||
cf = np.asarray(cache["features"], dtype=float).ravel()
|
||||
gf = np.asarray(gt["features"], dtype=float).ravel()
|
||||
if cf.shape != gf.shape:
|
||||
return None
|
||||
return float(np.max(np.abs(cf - gf)))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not CACHE_DIR.exists():
|
||||
raise SystemExit(f"Cache dir not found: {CACHE_DIR}")
|
||||
if not MANIFEST_PATH.exists():
|
||||
raise SystemExit(f"Manifest not found: {MANIFEST_PATH}")
|
||||
|
||||
cache_files = sorted(CACHE_DIR.glob(f"*_s{int(SCALE * 100)}.npz"))
|
||||
if MAX_SAMPLES is not None:
|
||||
cache_files = cache_files[:MAX_SAMPLES]
|
||||
print(f"[debug] cache files found: {len(cache_files)}")
|
||||
|
||||
try:
|
||||
manifest_df = pd.read_csv(MANIFEST_PATH)
|
||||
except Exception as exc:
|
||||
raise SystemExit(f"Failed to read manifest: {exc}")
|
||||
manifest_images = manifest_df.get("image_path")
|
||||
if manifest_images is None:
|
||||
raise SystemExit("Manifest is missing image_path column.")
|
||||
manifest_images = manifest_images.dropna().astype(str)
|
||||
manifest_stems = {Path(p).stem for p in manifest_images}
|
||||
print(f"[debug] manifest image_path count: {len(manifest_images)}")
|
||||
print(f"[debug] manifest unique stems: {len(manifest_stems)}")
|
||||
|
||||
cache_stems = {_parse_stem(p) for p in cache_files}
|
||||
overlap = cache_stems & manifest_stems
|
||||
print(
|
||||
f"[debug] cache stems: {len(cache_stems)} overlap with manifest stems: {len(overlap)}"
|
||||
)
|
||||
if cache_files:
|
||||
print(f"[debug] example cache stems: {sorted(list(cache_stems))[:5]}")
|
||||
if manifest_stems:
|
||||
print(f"[debug] example manifest stems: {sorted(list(manifest_stems))[:5]}")
|
||||
|
||||
cropper = ManifestImageCropper(
|
||||
manifest_path=MANIFEST_PATH,
|
||||
scale=SCALE,
|
||||
target_size=224,
|
||||
cache_dir=None,
|
||||
)
|
||||
|
||||
rows: List[Dict[str, object]] = []
|
||||
for cache_path in cache_files:
|
||||
cache = _load_cache(cache_path)
|
||||
if cache is None:
|
||||
continue
|
||||
stem = _parse_stem(cache_path)
|
||||
image_path = _image_path_from_stem(stem)
|
||||
if image_path is None:
|
||||
continue
|
||||
|
||||
gt = _gt_info(cropper, image_path)
|
||||
if gt is None:
|
||||
continue
|
||||
|
||||
bounds_diff = _diff_bounds(cache, gt)
|
||||
feat_diff = _diff_features(cache, gt)
|
||||
|
||||
rows.append(
|
||||
{
|
||||
"file": cache_path.name,
|
||||
"bounds_diff": bounds_diff,
|
||||
"features_diff": feat_diff,
|
||||
"bounds_match": bounds_diff is not None and bounds_diff <= TOL_BOUNDS,
|
||||
"features_match": feat_diff is not None and feat_diff <= TOL_FEATURES,
|
||||
}
|
||||
)
|
||||
|
||||
if not rows:
|
||||
print("[warn] No cache entries matched GT manifest entries.")
|
||||
else:
|
||||
df = pd.DataFrame(rows)
|
||||
print(df.head(10).to_string(index=False))
|
||||
print("\nSummary:")
|
||||
print(df[["bounds_diff", "features_diff"]].describe().to_string())
|
||||
if df["bounds_match"].notna().any():
|
||||
match_rate = df["bounds_match"].mean()
|
||||
print(f"\nBounds match rate (<= {TOL_BOUNDS}px): {match_rate:.3f}")
|
||||
if df["features_match"].notna().any():
|
||||
match_rate = df["features_match"].mean()
|
||||
print(f"Features match rate (<= {TOL_FEATURES}): {match_rate:.3f}")
|
||||
|
||||
print("\nUNet variant comparisons (no cache writes):")
|
||||
for name, weights, normalize in UNET_VARIANTS:
|
||||
if not weights.exists():
|
||||
print(f"[warn] {name}: weights not found at {weights}")
|
||||
continue
|
||||
|
||||
unet = UNetImageCropper(
|
||||
manifest_path=MANIFEST_PATH,
|
||||
weights_path=weights,
|
||||
normalize=normalize,
|
||||
threshold=0.5,
|
||||
tta=False,
|
||||
scale=SCALE,
|
||||
target_size=224,
|
||||
cache_dir=None, # ensure no cache writes
|
||||
)
|
||||
|
||||
u_rows: List[Dict[str, object]] = []
|
||||
missing_images = 0
|
||||
unet_none = 0
|
||||
cache_missing = 0
|
||||
exceptions = 0
|
||||
for cache_path in cache_files:
|
||||
cache = _load_cache(cache_path)
|
||||
if cache is None:
|
||||
cache_missing += 1
|
||||
continue
|
||||
stem = _parse_stem(cache_path)
|
||||
image_path = _image_path_from_stem(stem)
|
||||
if image_path is None:
|
||||
missing_images += 1
|
||||
continue
|
||||
try:
|
||||
info = _unet_info(unet, image_path)
|
||||
except Exception:
|
||||
exceptions += 1
|
||||
continue
|
||||
if info is None:
|
||||
unet_none += 1
|
||||
continue
|
||||
bounds_diff = _diff_bounds(cache, info)
|
||||
feat_diff = _diff_features(cache, info)
|
||||
u_rows.append(
|
||||
{
|
||||
"bounds_diff": bounds_diff,
|
||||
"features_diff": feat_diff,
|
||||
"bounds_match": bounds_diff is not None
|
||||
and bounds_diff <= TOL_BOUNDS,
|
||||
"features_match": feat_diff is not None
|
||||
and feat_diff <= TOL_FEATURES,
|
||||
}
|
||||
)
|
||||
|
||||
if not u_rows:
|
||||
print(
|
||||
f"[warn] {name}: no comparisons computed "
|
||||
f"(cache_missing={cache_missing}, missing_images={missing_images}, "
|
||||
f"unet_none={unet_none}, exceptions={exceptions})"
|
||||
)
|
||||
continue
|
||||
u_df = pd.DataFrame(u_rows)
|
||||
b_mean = float(u_df["bounds_diff"].mean())
|
||||
f_mean = float(u_df["features_diff"].mean())
|
||||
b_match = float(u_df["bounds_match"].mean())
|
||||
f_match = float(u_df["features_match"].mean())
|
||||
|
||||
print(
|
||||
f"{name}: mean bounds diff={b_mean:.3f}, mean feat diff={f_mean:.6f}, "
|
||||
f"bounds match rate={b_match:.3f}, features match rate={f_match:.3f}"
|
||||
)
|
||||
|
||||
print("\nRefuge segmentation model comparison (bounds only, no cache writes):")
|
||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
refuge_model = _load_refuge_model(device)
|
||||
if refuge_model is None:
|
||||
print(f"[warn] refuge_segmentation_best.pt not found or failed to load at {REFUGE_SEG_WEIGHTS}")
|
||||
return
|
||||
|
||||
r_rows: List[Dict[str, object]] = []
|
||||
missing_images = 0
|
||||
cache_missing = 0
|
||||
model_none = 0
|
||||
exceptions = 0
|
||||
for cache_path in cache_files:
|
||||
cache = _load_cache(cache_path)
|
||||
if cache is None:
|
||||
cache_missing += 1
|
||||
continue
|
||||
stem = _parse_stem(cache_path)
|
||||
image_path = _image_path_from_stem(stem)
|
||||
if image_path is None:
|
||||
missing_images += 1
|
||||
continue
|
||||
try:
|
||||
info = _refuge_seg_info(refuge_model, device, image_path)
|
||||
except Exception:
|
||||
exceptions += 1
|
||||
continue
|
||||
if info is None:
|
||||
model_none += 1
|
||||
continue
|
||||
bounds_diff = _diff_bounds(cache, info)
|
||||
r_rows.append(
|
||||
{
|
||||
"bounds_diff": bounds_diff,
|
||||
"bounds_match": bounds_diff is not None and bounds_diff <= TOL_BOUNDS,
|
||||
}
|
||||
)
|
||||
|
||||
if not r_rows:
|
||||
print(
|
||||
"[warn] refuge_segmentation_best: no comparisons computed "
|
||||
f"(cache_missing={cache_missing}, missing_images={missing_images}, "
|
||||
f"model_none={model_none}, exceptions={exceptions})"
|
||||
)
|
||||
return
|
||||
|
||||
r_df = pd.DataFrame(r_rows)
|
||||
b_mean = float(r_df["bounds_diff"].mean())
|
||||
b_match = float(r_df["bounds_match"].mean())
|
||||
print(
|
||||
f"refuge_segmentation_best: mean bounds diff={b_mean:.3f}, "
|
||||
f"bounds match rate={b_match:.3f}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user