Spaces:
Running
Running
| """Few-shot MedCLIPSeg-style training: frozen CLIP + text-conditioned decoder. | |
| Train on 8 animals (1A,1B,2A,3A,3B,4Aa,4Bb,5A); test on C1 and C2b (unseen). | |
| Faithful to the MedCLIPSeg approach that this machine can run: FROZEN CLIP | |
| encoders provide image patch features + a text-prompt embedding; a lightweight | |
| decoder (FiLM text conditioning) is trained data-efficiently with Dice+BCE to | |
| segment the fibers. Reports Dice (DSC) on the held-out animals and saves | |
| predictions. Labels = IMARIS reconstruction masks (approximate). | |
| """ | |
| import os, sys, glob, time | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| from skimage.transform import resize | |
| from skimage.io import imsave | |
| MED = "/Users/slf20757/Desktop/Dr. Fernandes/.claude/worktrees/intelligent-kalam-5d8731/Desktop/Dr. Fernandes/hf_neuron_tracer_medclip" | |
| sys.path.insert(0, MED) | |
| import processing as P | |
| import medclipseg as MC | |
| from prepare_medclip_data import seg_xyz, skel_mask # reconstruction label | |
| ROOT = "/Users/slf20757/Desktop/Dr. Fernandes" | |
| TRAIN = {"1A", "1B", "2A", "3A", "3B", "4Aa", "4Bb", "5A"} | |
| TEST = {"C1", "C2b"} | |
| SIZE = 224 | |
| dev = ("mps" if torch.backends.mps.is_available() | |
| else "cuda" if torch.cuda.is_available() else "cpu") | |
| def clip_features(mip_u8): | |
| """Frozen CLIP vision patch grid (768,14,14) for a 224 grayscale MIP.""" | |
| clip, proc = MC._load() | |
| rgb = np.stack([resize(mip_u8, (SIZE, SIZE), preserve_range=True)] * 3, -1).astype(np.uint8) | |
| px = proc(images=rgb, return_tensors="pt")["pixel_values"] | |
| with torch.no_grad(): | |
| toks = clip.vision_model(pixel_values=px).last_hidden_state[0, 1:, :] # (196,768) | |
| g = int(round(toks.shape[0] ** 0.5)) | |
| return toks.reshape(g, g, -1).permute(2, 0, 1).contiguous() # (768,g,g) | |
| class Decoder(nn.Module): | |
| def __init__(self, cin=768, tdim=512, C=128): | |
| super().__init__() | |
| self.reduce = nn.Conv2d(cin, C, 1) | |
| self.film = nn.Linear(tdim, 2 * C) | |
| def up(ci, co): | |
| return nn.Sequential(nn.ConvTranspose2d(ci, co, 2, 2), | |
| nn.Conv2d(co, co, 3, padding=1), | |
| nn.BatchNorm2d(co), nn.ReLU(True)) | |
| self.up = nn.Sequential(up(C, C), up(C, C), up(C, C // 2), up(C // 2, C // 4)) | |
| self.out = nn.Conv2d(C // 4, 1, 1) | |
| def forward(self, feat, tvec): | |
| x = self.reduce(feat) | |
| g, b = self.film(tvec).chunk(2, -1) | |
| x = x * (1 + g[:, :, None, None]) + b[:, :, None, None] | |
| x = self.up(x) # 14 -> 224 | |
| return torch.sigmoid(self.out(x)) | |
| def dice_metric(p, t, thr=0.5): | |
| p = (p > thr).float() | |
| return float((2 * (p * t).sum() + 1) / (p.sum() + t.sum() + 1)) | |
| # ---- build cached features + masks ---- | |
| print(f"device={dev}; extracting frozen CLIP features ...", flush=True) | |
| tvec = MC._text_embeds(MC.FG_PROMPTS).mean(0, keepdim=True).to(dev) # (1,512) prompt | |
| feats, masks, animals, stems = [], [], [], [] | |
| t0 = time.time() | |
| for csv in sorted(glob.glob(os.path.join(ROOT, "*", "*_Statistics", "*_Filament_Length_(sum).csv"))): | |
| sd = os.path.dirname(csv); stem = os.path.basename(csv).replace("_Filament_Length_(sum).csv", "") | |
| animal = os.path.relpath(csv, ROOT).split(os.sep)[0] | |
| czi = os.path.join(ROOT, animal, stem.replace(" filament", "") + ".czi") | |
| if not (os.path.exists(czi) and os.path.exists(os.path.join(sd, f"{stem}_Segment_Position_X.csv"))): | |
| continue | |
| if animal not in TRAIN and animal not in TEST: | |
| continue | |
| img = P.load_image(czi); nf, _ = P.guess_channels(img); vol = img.data[nf] | |
| mip = P.channel_preview(vol) | |
| feats.append(clip_features(mip)) | |
| m = skel_mask(seg_xyz(sd, stem), img.voxel, (vol.shape[1], vol.shape[2])) | |
| masks.append(torch.tensor(m.astype(np.float32))) | |
| animals.append(animal); stems.append(stem) | |
| del img, vol | |
| F = torch.stack(feats).to(dev) # (N,768,14,14) | |
| M = torch.stack(masks)[:, None].to(dev) # (N,1,224,224) | |
| animals = np.array(animals) | |
| tr = np.isin(animals, list(TRAIN)); te = np.isin(animals, list(TEST)) | |
| print(f"extracted {len(F)} imgs ({tr.sum()} train / {te.sum()} test) in {time.time()-t0:.0f}s", flush=True) | |
| # ---- train decoder ---- | |
| net = Decoder().to(dev) | |
| opt = torch.optim.Adam(net.parameters(), 1e-3, weight_decay=1e-5) | |
| idx = np.where(tr)[0] | |
| t1 = time.time(); net.train() | |
| for ep in range(300): | |
| perm = np.random.permutation(idx) | |
| for k in range(0, len(perm), 8): | |
| b = perm[k:k + 8] | |
| opt.zero_grad() | |
| p = net(F[b], tvec.expand(len(b), -1)) | |
| bce = nn.functional.binary_cross_entropy(p, M[b]) | |
| dsc = 1 - (2 * (p * M[b]).sum() + 1) / (p.sum() + M[b].sum() + 1) | |
| (bce + dsc).backward(); opt.step() | |
| if (ep + 1) % 100 == 0: | |
| print(f" epoch {ep+1}/300 ({time.time()-t1:.0f}s)", flush=True) | |
| # ---- evaluate on held-out C1 + C2b ---- | |
| net.eval() | |
| O = os.path.dirname(__file__) | |
| print("\n=== held-out test (C1, C2b — never trained on) ===", flush=True) | |
| train_dsc = [] | |
| with torch.no_grad(): | |
| for i in np.where(tr)[0]: | |
| train_dsc.append(dice_metric(net(F[i:i+1], tvec), M[i:i+1])) | |
| dscs = [] | |
| for i in np.where(te)[0]: | |
| p = net(F[i:i+1], tvec) | |
| d = dice_metric(p, M[i:i+1]); dscs.append(d) | |
| pred = (p[0, 0].cpu().numpy() > 0.5).astype(np.uint8) * 255 | |
| gt = (M[i, 0].cpu().numpy() * 255).astype(np.uint8) | |
| imsave(os.path.join(O, f"pred_{stems[i].replace(' ','_')}.png"), | |
| np.concatenate([gt, np.full((224, 6), 128, np.uint8), pred], 1)) | |
| print(f" {stems[i]:16s} Dice={d:.3f}", flush=True) | |
| print(f"\nTRAIN mean Dice={np.mean(train_dsc):.3f} | " | |
| f"HELD-OUT (C1+C2b) mean Dice={np.mean(dscs):.3f}", flush=True) | |
| torch.save({"model": net.state_dict()}, os.path.join(MED, "weights_medclip_decoder.pt")) | |
| print("saved decoder weights + prediction images (pred_*.png: GT | prediction)", flush=True) | |