Spaces:
Runtime error
Runtime error
File size: 6,023 Bytes
1cf3825 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | """Generate v9b JEPA anomaly-map overlays for visual localization check.
Picks ~8 OOD samples across all 4 sources (mix of tumor + healthy),
runs JEPA prediction_error_map, and saves a 3-panel figure per sample:
[original MRI] [anomaly heatmap] [overlay (original + thresholded mask)]
Outputs to samples/ood/v9b_localization/*.png so you can eyeball whether
JEPA fires on the actual tumor location or just on random texture.
"""
from __future__ import annotations
import sys
import time
from pathlib import Path
import numpy as np
import torch
from PIL import Image
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from src.research.jepa import IJEPAModel
JEPA_CKPT = ROOT / 'v9b_artifacts' / 'v9b_jepa' / 'last.pt'
SAMPLES = ROOT / 'samples' / 'ood'
OUT_DIR = SAMPLES / 'v9b_localization'
IMAGE_SIZE = 256
# Threshold for binary anomaly mask: matches the best-F1 operating point
# (per scripts/analyze_v9b_thresholds.py)
ANOMALY_THR = 0.40
def viridis_rgb(g: np.ndarray) -> np.ndarray:
"""Lightweight 5-anchor viridis colormap, numpy only."""
g = np.clip(g.astype(np.float32), 0.0, 1.0)
anchors = np.array([
[0.267, 0.005, 0.329], [0.282, 0.140, 0.458],
[0.254, 0.265, 0.530], [0.207, 0.372, 0.553],
[0.993, 0.906, 0.144],
], dtype=np.float32)
t = g * 4.0
lo = np.clip(np.floor(t).astype(np.int32), 0, 3)
hi = np.clip(lo + 1, 0, 4)
frac = (t - lo)[..., None]
out = anchors[lo] * (1.0 - frac) + anchors[hi] * frac
return (out * 255).astype(np.uint8)
def load_jepa(device):
ck = torch.load(str(JEPA_CKPT), map_location=device, weights_only=False)
a = ck.get('args', {})
m = IJEPAModel(image_size=a.get('image_size', 256), patch_size=16,
embed_dim=a.get('embed_dim', 384), depth=a.get('depth', 12),
heads=a.get('heads', 6))
m.load_state_dict(ck['model_state_dict'])
return m.to(device).eval()
def make_panel(orig_rgb: np.ndarray, emap: np.ndarray, mask: np.ndarray,
title: str) -> np.ndarray:
"""Build a 3-panel image: original | heatmap | overlay. Returns RGB uint8."""
H, W = orig_rgb.shape[:2]
# Normalise heatmap to [0,1] across this single image for visualisation
emap_norm = (emap - emap.min()) / max(emap.max() - emap.min(), 1e-6)
heatmap_rgb = viridis_rgb(emap_norm)
overlay = orig_rgb.copy().astype(np.float32)
red = np.array([220, 30, 30], dtype=np.float32)
alpha = 0.5
overlay[mask > 0] = (1 - alpha) * overlay[mask > 0] + alpha * red
overlay = np.clip(overlay, 0, 255).astype(np.uint8)
# Stitch horizontally with 4 px gap
gap = 4
pad = np.zeros((H, gap, 3), dtype=np.uint8) + 50
stitched = np.concatenate([orig_rgb, pad, heatmap_rgb, pad, overlay], axis=1)
# Add a small title bar
bar_h = 26
bar = np.zeros((bar_h, stitched.shape[1], 3), dtype=np.uint8) + 24
# Crude title via PIL since we don't want a matplotlib dep
from PIL import ImageDraw, ImageFont
bar_pil = Image.fromarray(bar)
draw = ImageDraw.Draw(bar_pil)
try:
font = ImageFont.truetype('arial.ttf', 14)
except Exception:
font = ImageFont.load_default()
draw.text((8, 4), title, fill=(230, 230, 230), font=font)
bar = np.array(bar_pil)
return np.concatenate([bar, stitched], axis=0)
def main():
if not JEPA_CKPT.exists():
sys.exit(f'missing {JEPA_CKPT}')
OUT_DIR.mkdir(parents=True, exist_ok=True)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f'[init] device={device}')
model = load_jepa(device)
# Pick 2 samples per source (8 total). Stratified by GT.
picks: list[tuple[str, str, Path]] = []
by_src: dict[str, list[Path]] = {}
for p in sorted(SAMPLES.rglob('*')):
if p.suffix.lower() not in ('.png','.jpg','.jpeg'): continue
if p.parent.name not in (
'healthy_coronal_T1_openneuro',
'tumor_proprietary_multimodal_unidata',
'tumor_multi_patient_ultralytics',
'tumor_binary_navoneel_via_miladfa7',
):
continue
by_src.setdefault(p.parent.name, []).append(p)
for src, files in by_src.items():
# Pick first + middle to get variety
for p in (files[0], files[len(files)//2]):
picks.append((src, p.name, p))
print(f'[init] {len(picks)} samples picked for localization viz')
for src, fname, p in picks:
gt = 'TUMOR-GT' if 'tumor' in src else 'HEALTHY-GT'
img = Image.open(p).convert('RGB').resize((IMAGE_SIZE, IMAGE_SIZE), Image.BILINEAR)
arr = np.asarray(img, dtype=np.float32) / 255.0
x = torch.from_numpy(arr.transpose(2, 0, 1)).unsqueeze(0).to(device)
t0 = time.perf_counter()
with torch.no_grad():
emap = model.prediction_error_map(x).squeeze().cpu().numpy()
p95 = float(np.percentile(emap, 95))
# Per-image threshold = take the top X% of pixels as anomalous
# (more useful visually than the absolute scaled threshold)
thr = np.percentile(emap, 90)
mask = (emap > thr).astype(np.uint8)
ano_frac = float(mask.mean())
orig_rgb = (arr * 255).astype(np.uint8)
title = (f'{gt} | src={src[:35]} | file={fname[:30]} | '
f'p95={p95:.3f} anomaly_pixels={ano_frac:.0%} '
f'inference={time.perf_counter()-t0:.1f}s')
panel = make_panel(orig_rgb, emap, mask, title)
out = OUT_DIR / f'{src}__{fname.rsplit(".",1)[0]}.png'
Image.fromarray(panel).save(out)
print(f' {out.name} (p95={p95:.3f}, gt={gt})')
print(f'\n[done] {len(picks)} panels in {OUT_DIR}/')
print('Open the PNGs to see whether the anomaly heatmap lights up '
'where the tumor actually is.')
if __name__ == '__main__':
main()
|