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()