her2match-src-ds-arch-sweep / eval_multi_thunder.py
augustander's picture
multi-arch eval: cleanfid + pytorch-fid + brown-frac for C7/C3/A3
5b9a154 verified
Raw History Blame Contribute Delete
7.98 kB
"""Multi-arch eval on HER2Match val: cleanfid + pytorch-fid + brown-frac.
Supports C7 (UNI-2H encoder), C3 (WaveletJointAEv3), A3 (SkipFreeAE3Stage).
Env-var configured for CI-friendly multi-run:
ARCH=C7|C3|A3 — which model class to instantiate
CKPT_LOCAL — path to best.pt (state_dict must match ARCH)
CKPT_TAG — subfolder tag for outputs (e.g. 'c7_best', 'c3_dab_best')
OUT_ROOT — where to put generated tiles + results json
HF_EXP_SUB — HF sub-path under `augustander/her2match-src-ds-arch-sweep/` for upload
"""
import os, sys, json, time, glob, subprocess
from pathlib import Path
import numpy as np
import torch, torch.nn.functional as F
from PIL import Image
ARCH = os.environ.get('ARCH', 'C7')
CKPT_LOCAL = os.environ['CKPT_LOCAL']
CKPT_TAG = os.environ.get('CKPT_TAG', 'best')
OUT_ROOT = Path(os.environ.get('OUT_ROOT', f'/home/ubuntu/eval_{ARCH}_{CKPT_TAG}'))
HF_EXP_SUB = os.environ.get('HF_EXP_SUB', 'multi_arch_eval')
HF_MODEL_REPO = 'augustander/her2match-src-ds-arch-sweep'
DATA_ROOT = '/home/ubuntu/her2match_full/her2match_full'
GEN_DIR = OUT_ROOT / 'generated_ihc'
GEN_DIR.mkdir(parents=True, exist_ok=True)
device = 'cuda'
sys.path.insert(0, '/home/ubuntu/work')
from src_ds_her2_thunder import (
UniDwtC7, WaveletJointAEv3, SkipFreeAE3Stage,
UNI2HSpatialLoss, StainDeconvDAB,
)
# ---- Load model per ARCH ----
def build_model(arch: str, ckpt: dict, uni_loss=None):
if arch == 'C7':
assert uni_loss is not None, 'C7 needs shared UNI-2H'
m = UniDwtC7(
uni_module=uni_loss.uni, wavelet='haar', taps=(13, 19),
dec_ch=(512, 384, 256, 128, 64), skip_ch=(384, 256, 128),
blocks_per_stage=(4, 3, 3, 2, 2), uni_target_size=224,
)
elif arch == 'C3':
m = WaveletJointAEv3(base_ch=48, wavelet='haar')
elif arch == 'A3':
m = SkipFreeAE3Stage(base_ch=48, bottleneck_ch=256)
else:
raise ValueError(f'unknown ARCH={arch}')
return m
# ---- ALWAYS load shared UNI (needed for C7 arch OR just for future perceptual use) ----
if ARCH == 'C7':
print(f'[{ARCH}/{CKPT_TAG}] loading shared UNI-2H...', flush=True)
uni_loss = UNI2HSpatialLoss(target_size=224).to(device).eval()
for p in uni_loss.uni.parameters(): p.requires_grad_(False)
else:
uni_loss = None
# ---- Build + load ----
G = build_model(ARCH, None, uni_loss=uni_loss).to(device).eval()
ckpt = torch.load(CKPT_LOCAL, map_location='cpu', weights_only=False)
missing, unexpected = G.load_state_dict(ckpt['model'], strict=False)
n_missing_uni = len([k for k in missing if k.startswith('uni.')])
n_missing_other = [k for k in missing if not k.startswith('uni.')]
print(f'[{ARCH}/{CKPT_TAG}] loaded ckpt step={ckpt.get("step","?")} '
f'val_lpips={ckpt.get("val_lpips",-1):.4f} '
f'(missing_uni={n_missing_uni}, missing_other={n_missing_other}, unexpected={len(unexpected)})',
flush=True)
for p in G.parameters(): p.requires_grad_(False)
stain_dab = StainDeconvDAB().to(device).eval()
# ---- Generate 3582 tiles ----
he_val = sorted(glob.glob(f'{DATA_ROOT}/HE/val/*.jpg'))
ihc_val_dir = f'{DATA_ROOT}/IHC/val'
BATCH = 16 if ARCH == 'C7' else 32
print(f'[{ARCH}/{CKPT_TAG}] generating {len(he_val)} tiles (batch={BATCH})...', flush=True)
t0 = time.time()
for i in range(0, len(he_val), BATCH):
chunk = he_val[i:i+BATCH]
imgs = []
for f in chunk:
im = Image.open(f).convert('RGB').resize((256, 256), Image.BICUBIC)
x = torch.from_numpy(np.asarray(im, dtype=np.float32) / 255.0).permute(2, 0, 1)
imgs.append(x * 2 - 1)
x = torch.stack(imgs).to(device)
with torch.no_grad():
y = G(x).clamp(-1, 1)
y_np = ((y.cpu() + 1) * 127.5).clamp(0, 255).byte().permute(0, 2, 3, 1).numpy()
for k, f in enumerate(chunk):
Image.fromarray(y_np[k]).save(GEN_DIR / Path(f).name, quality=92)
if (i // BATCH) % 20 == 0:
done = i + len(chunk); rate = done / max(1e-6, time.time() - t0)
eta = (len(he_val) - done) / max(1e-6, rate)
print(f' gen {done}/{len(he_val)} ({rate:.1f} img/s eta {eta:.0f}s)', flush=True)
print(f'[{ARCH}/{CKPT_TAG}] gen done in {time.time()-t0:.0f}s', flush=True)
# ---- cleanfid ----
print(f'[{ARCH}/{CKPT_TAG}] cleanfid...', flush=True)
try:
from cleanfid import fid as cfid
cleanfid_val = cfid.compute_fid(
str(GEN_DIR), ihc_val_dir,
device='cuda', mode='clean', batch_size=64, num_workers=4)
print(f'[{ARCH}/{CKPT_TAG}] CLEANFID = {cleanfid_val:.3f}', flush=True)
except Exception as e:
print(f'[{ARCH}/{CKPT_TAG}] cleanfid err: {e}', flush=True); cleanfid_val = None
# ---- pytorch-fid ----
print(f'[{ARCH}/{CKPT_TAG}] pytorch-fid...', flush=True)
try:
r = subprocess.run(
['python', '-m', 'pytorch_fid', str(GEN_DIR), ihc_val_dir,
'--device', 'cuda', '--batch-size', '50'],
capture_output=True, text=True, timeout=1800)
pytorch_fid_val = None
for line in r.stdout.splitlines():
if line.strip().startswith('FID:'):
pytorch_fid_val = float(line.split()[1])
break
print(f'[{ARCH}/{CKPT_TAG}] PYTORCH_FID = {pytorch_fid_val}', flush=True)
if pytorch_fid_val is None:
print(f'[{ARCH}/{CKPT_TAG}] pytorch-fid stderr tail: {r.stderr[-500:]}', flush=True)
except Exception as e:
print(f'[{ARCH}/{CKPT_TAG}] pytorch-fid err: {e}', flush=True); pytorch_fid_val = None
# ---- brown-frac ----
print(f'[{ARCH}/{CKPT_TAG}] brown-frac...', flush=True)
def dab_stats(paths, batch=64):
means, fracs = [], []
for i in range(0, len(paths), batch):
chunk = paths[i:i+batch]
imgs = []
for f in chunk:
im = Image.open(f).convert('RGB').resize((256, 256), Image.BICUBIC)
x = torch.from_numpy(np.asarray(im, dtype=np.float32) / 255.0).permute(2, 0, 1)
imgs.append(x * 2 - 1)
x = torch.stack(imgs).to(device)
with torch.no_grad():
dab = stain_dab(x)
means.append(dab.mean(dim=(1,2)).cpu().numpy())
fracs.append((dab > 0.3).float().mean(dim=(1,2)).cpu().numpy())
return np.concatenate(means), np.concatenate(fracs)
gen_files = sorted(GEN_DIR.glob('*.jpg'))
tgt_files = sorted(Path(ihc_val_dir).glob('*.jpg'))
common = sorted({p.name for p in gen_files} & {p.name for p in tgt_files})
gen_files = [GEN_DIR / n for n in common]
tgt_files = [Path(ihc_val_dir) / n for n in common]
gm, gf = dab_stats(gen_files)
tm, tf = dab_stats(tgt_files)
her2p_mask = tf > 0.05
her2p_n = int(her2p_mask.sum())
results = {
'arch': ARCH, 'ckpt_tag': CKPT_TAG, 'ckpt_local': CKPT_LOCAL,
'ckpt_step': int(ckpt.get('step', -1)),
'ckpt_val_lpips': float(ckpt.get('val_lpips', -1)),
'cleanfid': cleanfid_val, 'pytorch_fid': pytorch_fid_val,
'n_tiles': len(common), 'n_her2p_tiles': her2p_n,
'overall_gen_bf': float(gf.mean()), 'overall_tgt_bf': float(tf.mean()),
'overall_gen_dab': float(gm.mean()), 'overall_tgt_dab': float(tm.mean()),
'her2p_gen_bf': float(gf[her2p_mask].mean()) if her2p_n else None,
'her2p_tgt_bf': float(tf[her2p_mask].mean()) if her2p_n else None,
'her2p_under_predict_pct': float((gf[her2p_mask] < 0.5 * tf[her2p_mask]).mean()) if her2p_n else None,
}
print(f'[{ARCH}/{CKPT_TAG}] RESULTS:', json.dumps(results, indent=2), flush=True)
out_json = OUT_ROOT / f'eval_{ARCH}_{CKPT_TAG}.json'
out_json.write_text(json.dumps(results, indent=2))
if 'HF_TOKEN' not in os.environ and os.path.exists('/tmp/hf_token'):
os.environ['HF_TOKEN'] = open('/tmp/hf_token').read().strip()
from huggingface_hub import HfApi
HfApi().upload_file(
path_or_fileobj=str(out_json),
path_in_repo=f'{HF_EXP_SUB}/eval_{ARCH}_{CKPT_TAG}.json',
repo_id=HF_MODEL_REPO, repo_type='model',
commit_message=f'multi-eval: {ARCH} {CKPT_TAG} cleanfid+pytorch-fid+brown-frac')
print(f'=== EVAL_DONE {ARCH}/{CKPT_TAG} ===', flush=True)