Spaces:
Running on Zero
Running on Zero
File size: 6,895 Bytes
73e760d | 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 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | """
Extract DINOv2 ViT-S/14 embeddings for each cell in a segmentation mask.
For each cell, crops a patch around the cell's bounding box (with padding),
resizes to 224x224, and passes through DINOv2 to get a 384-dim embedding.
Returns a DataFrame with columns cnn_emb_0 ... cnn_emb_383 indexed by cell_id.
"""
import warnings
import numpy as np
import pandas as pd
from skimage.measure import regionprops
def load_dinov2(weights_path=None, use_gpu=True):
"""Load the DINOv2 backbone (optionally with fine-tuned weights) onto the
right device and put it in eval mode.
Building this hits ``torch.hub`` and reads an ~85 MB state dict, so a
long-running caller (a web server) should call this once at startup and
pass the result to ``extract_cnn_embeddings(model=...)``. Returns None if
the backbone cannot be loaded.
"""
try:
import torch
except ImportError:
warnings.warn(
"PyTorch not available - skipping CNN embedding extraction. "
"Install torch + torchvision to enable DINOv2 features."
)
return None
device = torch.device("cpu")
if use_gpu and torch.cuda.is_available():
device = torch.device("cuda")
try:
print(f" CNN embeddings: using GPU ({torch.cuda.get_device_name(0)})")
except Exception: # noqa: BLE001
# ZeroGPU and similar lazy-allocation setups report cuda as
# available before a device is actually attached.
print(" CNN embeddings: using GPU")
else:
if use_gpu:
print(" CNN embeddings: GPU requested but not available, using CPU")
else:
print(" CNN embeddings: using CPU")
# Determine which architecture to load. If weights_path is provided and
# a sibling meta.json exists (written by finetune_dinov2.py), honor the
# `arch` / `hub_name` recorded there. Otherwise default to ViT-S/14.
hub_name = "dinov2_vits14_reg"
if weights_path is not None:
from pathlib import Path as _P
import json as _json
meta_path = _P(weights_path).parent / "meta.json"
if meta_path.exists():
try:
with open(meta_path) as _f:
_meta = _json.load(_f)
hub_name = _meta.get("hub_name", hub_name)
except Exception:
pass
print(f" CNN embeddings: loading {hub_name}...")
try:
model = torch.hub.load(
'facebookresearch/dinov2', hub_name, verbose=False
)
except Exception as e:
warnings.warn(
f"Failed to load DINOv2 model: {e}. "
"Skipping CNN embedding extraction."
)
return None
if weights_path is not None:
try:
state = torch.load(weights_path, map_location="cpu")
missing, unexpected = model.load_state_dict(state, strict=False)
print(f" CNN embeddings: loaded fine-tuned weights from "
f"{weights_path} (missing={len(missing)}, unexpected={len(unexpected)})")
except Exception as e:
warnings.warn(f"Failed to load fine-tuned weights ({e}); using pretrained.")
model = model.to(device)
model.eval()
return model
def extract_cnn_embeddings(masks, img_rgb, batch_size=64, use_gpu=True,
padding=16, weights_path=None, model=None):
"""
Extract DINOv2 ViT-S/14 embeddings for each cell in the segmentation mask.
Parameters
----------
masks : np.ndarray (H, W) int32
Cell segmentation mask (0 = background, >0 = cell IDs).
img_rgb : np.ndarray (H, W, 3) uint8
Original RGB image.
batch_size : int
Number of cells to process at once.
use_gpu : bool
Move model to CUDA if available.
padding : int
Extra pixels around each cell's bounding box for context.
Returns
-------
pd.DataFrame
Columns: cell_id, cnn_emb_0, cnn_emb_1, ..., cnn_emb_383
"""
try:
import torch
import torchvision.transforms as T
except ImportError:
warnings.warn(
"PyTorch not available - skipping CNN embedding extraction. "
"Install torch + torchvision to enable DINOv2 features."
)
return None
if model is None:
model = load_dinov2(weights_path=weights_path, use_gpu=use_gpu)
if model is None:
return None
device = next(model.parameters()).device
# DINOv2 preprocessing: resize to 224x224, normalize with ImageNet stats
transform = T.Compose([
T.ToPILImage(),
T.Resize((224, 224)),
T.ToTensor(),
T.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]),
])
H, W = masks.shape
props = regionprops(masks)
if not props:
return None
# Handle grayscale: if all channels are identical, it's grayscale
# DINOv2 expects 3-channel input; grayscale is already repeated to RGB
# by ensure_rgb_uint8, but double-check
if img_rgb.ndim == 2:
img_rgb = np.stack([img_rgb, img_rgb, img_rgb], axis=-1)
elif img_rgb.shape[-1] == 1:
img_rgb = np.concatenate([img_rgb, img_rgb, img_rgb], axis=-1)
# Collect cell patches
cell_ids = []
patches = []
for p in props:
cid = p.label
# Bounding box: (min_row, min_col, max_row, max_col)
r0, c0, r1, c1 = p.bbox
# Add padding for context
r0_pad = max(0, r0 - padding)
c0_pad = max(0, c0 - padding)
r1_pad = min(H, r1 + padding)
c1_pad = min(W, c1 + padding)
patch = img_rgb[r0_pad:r1_pad, c0_pad:c1_pad].copy()
# Ensure patch is not empty
if patch.shape[0] == 0 or patch.shape[1] == 0:
continue
cell_ids.append(cid)
patches.append(patch)
if not patches:
return None
# Process in batches
all_embeddings = []
n_cells = len(patches)
with torch.no_grad():
for start in range(0, n_cells, batch_size):
end = min(start + batch_size, n_cells)
batch_tensors = []
for patch in patches[start:end]:
tensor = transform(patch)
batch_tensors.append(tensor)
batch = torch.stack(batch_tensors).to(device)
embeddings = model(batch) # (batch_size, 384)
all_embeddings.append(embeddings.cpu().numpy())
all_embeddings = np.vstack(all_embeddings) # (n_cells, 384)
emb_dim = all_embeddings.shape[1]
# Build DataFrame
emb_cols = [f"cnn_emb_{i}" for i in range(emb_dim)]
df = pd.DataFrame(all_embeddings, columns=emb_cols)
df.insert(0, "cell_id", cell_ids)
print(f" CNN embeddings: extracted {emb_dim}-dim embeddings for "
f"{len(cell_ids)} cells")
return df
|