| |
| """AMC-HCC-ST λ²€μΉλ§ν¬ β Asan Medical Center HCC Visium spatial-transcriptomics μ½νΈνΈ(λΉκ³΅κ°). |
| κ° spotμμ WSI ν¨μΉ μΆμΆ β FM μλ² λ©(CLS) β PCA β Ridge νκ·λ‘ μμ HVG λ°ν μμΈ‘ |
| β Pearson μκ΄(μ μ μνκ· ). Leave-one-patient-out CV. |
| |
| μ¬μ©: |
| PYTHONPATH=OpenPath:eval venv_eva/bin/python eval/st_bench.py \ |
| --backbone openmidnight # μ°Έμ‘°: OpenMidnight |
| ... --backbone phikon |
| ... --backbone openpath --weights data/runs/openpath_run/eval/training_316250/teacher_checkpoint.pth |
| """ |
| import os, sys, json, glob, argparse, re |
| import numpy as np, pandas as pd |
| import torch, openslide |
| from PIL import Image |
| from sklearn.linear_model import Ridge |
| from sklearn.decomposition import PCA |
| from sklearn.preprocessing import StandardScaler |
| from scipy.stats import pearsonr |
| import torchvision.transforms as T |
|
|
| ROOT = os.environ.get("OPENPATH_ROOT", os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
| DATA = os.environ.get("ST_ROOT", f"{ROOT}/data/st_bench") |
| IMAGENET_MEAN = (0.485, 0.456, 0.406); IMAGENET_STD = (0.229, 0.224, 0.225) |
|
|
|
|
| def build_backbone(name, weights): |
| sys.path.insert(0, f"{ROOT}/OpenPath"); sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) |
| import openpath_eva_backbone as B |
| ref = {"uni": (B.build_uni, 1024), "uni2": (B.build_uni2, 1536), |
| "gigapath": (B.build_gigapath, 1536), |
| "virchow2": (B.build_virchow2, 1280), |
| "phikon": (B.build_phikon, 1024), |
| "openmidnight": (B.build_openmidnight, 1536)} |
| if name in ref: |
| fn, dim = ref[name]; return fn(), dim |
| |
| return B.build_openpath(weights), 1536 |
|
|
|
|
| def slide_dirs(): |
| return sorted([d for d in glob.glob(f"{DATA}/*") if os.path.isdir(d)]) |
|
|
|
|
| def patient_of(slide_dir): |
| b = os.path.basename(slide_dir) |
| m = re.match(r"(pt\d+)", b) |
| return m.group(1) if m else b |
|
|
|
|
| def load_one(slide_dir): |
| exp_f = glob.glob(f"{slide_dir}/*.spatial.data.exp.csv") |
| pos_f = glob.glob(f"{slide_dir}/*.tissue_positions_fullres.csv") |
| sf_f = glob.glob(f"{slide_dir}/*.scalefactors_json.json") |
| wsi_f = glob.glob(f"{slide_dir}/*_p0.tif") |
| if not (exp_f and pos_f and sf_f and wsi_f): |
| return None |
| exp = pd.read_csv(exp_f[0], index_col=0) |
| pos = pd.read_csv(pos_f[0], index_col=0) |
| sf = json.load(open(sf_f[0])) |
| diam = int(round(sf["spot_diameter_fullres"])) |
| common = exp.index.intersection(pos.index) |
| exp = exp.loc[common]; pos = pos.loc[common] |
| return dict(dir=slide_dir, exp=exp, pos=pos, diam=diam, wsi=wsi_f[0]) |
|
|
|
|
| def _load_patches(sl): |
| """224 uint8 ν¨μΉ (N,224,224,3). μ¬λΌμ΄λλ³ μΊμ μ¬μ¬μ©(체ν¬ν¬μΈνΈλ§λ€ λμΌ ν¨μΉ).""" |
| import numpy as np |
| cache = f"{DATA}/_pcache/{os.path.basename(sl['dir'])}.pt" |
| if os.path.exists(cache): |
| try: return torch.load(cache) |
| except Exception: pass |
| sldx = openslide.OpenSlide(sl["wsi"]); d = sl["diam"] |
| rs = T.Resize((224, 224)) |
| xs = sl["pos"]["pxl_x_in_fullres"].values.astype(int) |
| ys = sl["pos"]["pxl_y_in_fullres"].values.astype(int) |
| plist = [] |
| for x, y in zip(xs, ys): |
| patch = sldx.read_region((int(x - d // 2), int(y - d // 2)), 0, (d, d)).convert("RGB") |
| plist.append(torch.from_numpy(np.asarray(rs(patch)))) |
| sldx.close() |
| patches = torch.stack(plist) |
| os.makedirs(os.path.dirname(cache), exist_ok=True) |
| tmp = cache + f".tmp{os.getpid()}" |
| torch.save(patches, tmp); os.replace(tmp, cache) |
| return patches |
|
|
|
|
| @torch.no_grad() |
| def embed_slide(sl, model, device, bs=256): |
| patches = _load_patches(sl) |
| mean = torch.tensor(IMAGENET_MEAN).view(1, 3, 1, 1) |
| std = torch.tensor(IMAGENET_STD).view(1, 3, 1, 1) |
| embs = [] |
| for i in range(0, len(patches), bs): |
| b = patches[i:i + bs].permute(0, 3, 1, 2).float().div_(255.0) |
| b = ((b - mean) / std).to(device) |
| embs.append(model(b).float().cpu().numpy()) |
| return np.concatenate(embs, 0) |
|
|
|
|
| def top_hvg(exp_all, k=50): |
| |
| v = exp_all.var(axis=0) |
| return v.sort_values(ascending=False).index[:k].tolist() |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--backbone", required=True, choices=["openpath","openmidnight","phikon","uni","uni2","gigapath","virchow2"]) |
| ap.add_argument("--weights", default=None) |
| ap.add_argument("--k-genes", type=int, default=50) |
| ap.add_argument("--pca", type=int, default=256) |
| ap.add_argument("--alpha", type=float, default=100.0) |
| ap.add_argument("--tag", default=None) |
| args = ap.parse_args() |
| device = "cuda" |
|
|
| model, dim = build_backbone(args.backbone, args.weights) |
| model = model.to(device).eval() |
|
|
| dirs = slide_dirs() |
| print(f"[st]slides={len(dirs)} backbone={args.backbone} dim={dim}", flush=True) |
| slides = [] |
| for d in dirs: |
| sl = load_one(d) |
| if sl is None: print(f" skip {os.path.basename(d)} (νμΌ λΆμ‘±)"); continue |
| sl["emb"] = embed_slide(sl, model, device) |
| sl["pat"] = patient_of(d) |
| slides.append(sl) |
| print(f" {os.path.basename(d)}: spots={len(sl['pos'])} emb={sl['emb'].shape} pat={sl['pat']}", flush=True) |
|
|
| |
| genes = slides[0]["exp"].columns |
| for s in slides[1:]: genes = genes.intersection(s["exp"].columns) |
| genes = list(genes) |
| print(f"[st]κ³΅ν΅ μ μ μ {len(genes)}", flush=True) |
|
|
| pats = sorted(set(s["pat"] for s in slides)) |
| |
| per_gene_corr = [] |
| for held in pats: |
| tr = [s for s in slides if s["pat"] != held] |
| te = [s for s in slides if s["pat"] == held] |
| Xtr = np.concatenate([s["emb"] for s in tr], 0) |
| Ytr = np.concatenate([s["exp"][genes].values for s in tr], 0) |
| Xte = np.concatenate([s["emb"] for s in te], 0) |
| Yte = np.concatenate([s["exp"][genes].values for s in te], 0) |
| |
| hvg_idx = np.argsort(-Ytr.var(0))[:args.k_genes] |
| Ytr_h, Yte_h = Ytr[:, hvg_idx], Yte[:, hvg_idx] |
| |
| sc = StandardScaler().fit(Xtr) |
| Xtr2, Xte2 = sc.transform(Xtr), sc.transform(Xte) |
| p = PCA(n_components=min(args.pca, Xtr2.shape[1])).fit(Xtr2) |
| Xtr3, Xte3 = p.transform(Xtr2), p.transform(Xte2) |
| reg = Ridge(alpha=args.alpha).fit(Xtr3, Ytr_h) |
| pred = reg.predict(Xte3) |
| cors = [] |
| for g in range(Yte_h.shape[1]): |
| if Yte_h[:, g].std() < 1e-8 or pred[:, g].std() < 1e-8: |
| cors.append(0.0) |
| else: |
| cors.append(pearsonr(Yte_h[:, g], pred[:, g])[0]) |
| m = float(np.nanmean(cors)) |
| per_gene_corr.append(m) |
| print(f" [fold {held}] test_spots={Yte.shape[0]} meanPearson={m:.4f}", flush=True) |
|
|
| overall = float(np.mean(per_gene_corr)) |
| tag = args.tag or args.backbone |
| print(f"[AMC-HCC-ST RESULT] backbone={tag} | LOPO mean Pearson = {overall:.4f} (folds={len(pats)}, HVG={args.k_genes})", flush=True) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|