File size: 7,641 Bytes
b57d99f 507d72b b57d99f 507d72b b57d99f 507d72b b57d99f | 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 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | #!/usr/bin/env python
"""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__)))) # repo ๋ฃจํธ(eval/์ ์์)
DATA = os.environ.get("ST_ROOT", f"{ROOT}/data/st_bench") # AMC-HCC-ST ์ฝํธํธ(๋น๊ณต๊ฐ; ์ฝ๋๋ง ๊ณต๊ฐ)
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
# openpath: ์ฐ๋ฆฌ teacher_checkpoint(dinov2 teacher ํฌ๋งท) ๋ก๋
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) # ์: pt<N>-<M> ๋๋ pt<N>
m = re.match(r"(pt\d+)", b)
return m.group(1) if m else b # pt<N>-<M>, pt<N>-<K> โ ๋์ผ ํ์ pt<N>
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) # spots ร genes (log-norm)
pos = pd.read_csv(pos_f[0], index_col=0) # barcode โ pxl coords
sf = json.load(open(sf_f[0]))
diam = int(round(sf["spot_diameter_fullres"])) # ~147px
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)))) # (224,224,3) uint8
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) # atomic
return patches
@torch.no_grad()
def embed_slide(sl, model, device, bs=256):
patches = _load_patches(sl) # (N,224,224,3) uint8
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,3,224,224)
b = ((b - mean) / std).to(device)
embs.append(model(b).float().cpu().numpy())
return np.concatenate(embs, 0) # (n_spots, dim)
def top_hvg(exp_all, k=50):
# ํ์ต์
์ ์ฒด log-norm ๋ฐํ์ ๋ถ์ฐ ์์ k ์ ์ ์
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))
# leave-one-patient-out
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๋ ํ์ต์
์ ์ ํ
hvg_idx = np.argsort(-Ytr.var(0))[:args.k_genes]
Ytr_h, Yte_h = Ytr[:, hvg_idx], Yte[:, hvg_idx]
# ํ์คํ + PCA + Ridge
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()
|