Download eval_multi_thunder.py from augustander/her2match-src-ds-arch-sweep: direct link, hf CLI and curl.
- Browser
- Download file 7.98 kB
-
https://huggingface.co/augustander/her2match-src-ds-arch-sweep/resolve/main/eval_multi_thunder.py
- Command line
-
hf download hf://augustander/her2match-src-ds-arch-sweep/eval_multi_thunder.py
-
curl -L -o eval_multi_thunder.py https://huggingface.co/augustander/her2match-src-ds-arch-sweep/resolve/main/eval_multi_thunder.py
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) | |