Spaces:
Sleeping
Sleeping
File size: 9,232 Bytes
ad83752 e1c2dc2 ad83752 e1c2dc2 ad83752 6ba8390 ad83752 e1c2dc2 ad83752 e1c2dc2 ad83752 e1c2dc2 ad83752 e1c2dc2 ad83752 e1c2dc2 ad83752 e1c2dc2 ad83752 e1c2dc2 6ba8390 e1c2dc2 6ba8390 e1c2dc2 6ba8390 e1c2dc2 6ba8390 e1c2dc2 6ba8390 e1c2dc2 ad83752 6ba8390 ad83752 6ba8390 ad83752 e1c2dc2 ad83752 e1c2dc2 ad83752 e1c2dc2 ad83752 e1c2dc2 ad83752 e1c2dc2 6ba8390 e1c2dc2 6ba8390 e1c2dc2 6ba8390 ad83752 6ba8390 ad83752 e1c2dc2 6ba8390 e1c2dc2 ad83752 e1c2dc2 6ba8390 ad83752 e1c2dc2 ad83752 6ba8390 e1c2dc2 ad83752 e1c2dc2 ad83752 e1c2dc2 ad83752 e1c2dc2 6ba8390 e1c2dc2 ad83752 | 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 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | """
predictor.py
------------
Model loading and inference for the Crop vs No-Crop SegFormer-B2 app.
Architecture is rebuilt from the `nvidia/mit-b2` backbone config with
num_labels=2, then the fine-tuned `best.pt` state dict is loaded on top.
Robust loader: auto-detects common key-naming differences, matches tensors by
name AND shape, and reports detailed diagnostics (including whether the
segmentation HEAD/classifier loaded) so an all-one-class prediction can be
debugged precisely.
"""
from __future__ import annotations
import logging
import os
import numpy as np
import torch
import torch.nn.functional as F
from PIL import Image
from torchvision import transforms
from transformers import SegformerConfig, SegformerForSemanticSegmentation
logger = logging.getLogger(__name__)
# --------------------------------------------------------------------------- #
# Configuration
# --------------------------------------------------------------------------- #
MODEL_NAME = os.environ.get("MODEL_NAME", "nvidia/mit-b2")
NUM_LABELS = 2
INPUT_SIZE = int(os.environ.get("INPUT_SIZE", "512"))
IMAGENET_MEAN = (0.485, 0.456, 0.406)
IMAGENET_STD = (0.229, 0.224, 0.225)
WEIGHTS_FILENAME = os.environ.get("WEIGHTS_FILENAME", "best.pt")
HF_REPO_ID = os.environ.get("HF_REPO_ID", "").strip()
HF_TOKEN = os.environ.get("HF_TOKEN", "").strip() or None
class ModelLoadError(Exception):
"""Raised when the model or its weights cannot be loaded."""
class InferenceError(Exception):
"""Raised when inference fails for a given image."""
def _resolve_weights_path() -> str:
"""Return a local path to best.pt (download from HF Hub if HF_REPO_ID set)."""
if HF_REPO_ID:
try:
from huggingface_hub import hf_hub_download
return hf_hub_download(
repo_id=HF_REPO_ID, filename=WEIGHTS_FILENAME, token=HF_TOKEN
)
except Exception as exc: # noqa: BLE001
raise ModelLoadError(
f"Failed to download '{WEIGHTS_FILENAME}' from '{HF_REPO_ID}': {exc}"
) from exc
here = os.path.join(os.path.dirname(__file__), WEIGHTS_FILENAME)
if os.path.exists(here):
return here
if os.path.exists(WEIGHTS_FILENAME):
return WEIGHTS_FILENAME
raise ModelLoadError(
f"Weights file '{WEIGHTS_FILENAME}' not found. Commit it to the repo "
"or set the HF_REPO_ID environment variable."
)
def _extract_state_dict(checkpoint):
if isinstance(checkpoint, dict):
for key in ("state_dict", "model_state_dict", "model"):
inner = checkpoint.get(key)
if isinstance(inner, dict):
return inner
return checkpoint
def _strip_module(state_dict):
cleaned = {}
for key, value in state_dict.items():
new_key = key
while new_key.startswith("module."):
new_key = new_key[len("module."):]
cleaned[new_key] = value
return cleaned
def _align_state_dict(ckpt_sd, model_sd):
"""
Match checkpoint tensors to model tensors by name (with a few common
transforms) AND shape. Returns (aligned_dict, info) where info reports
matched count, the model layers still MISSING weights, the checkpoint
layers that were UNUSED, and whether the classifier head loaded.
"""
ckpt_sd = _strip_module(ckpt_sd)
transforms_to_try = [
lambda k: k, # as-is
lambda k: "segformer." + k, # add prefix
lambda k: k[len("segformer."):] # strip prefix
if k.startswith("segformer.") else None,
lambda k: k[len("model."):] if k.startswith("model.") else None,
]
best = {}
used_ckpt_keys = set()
for key, value in ckpt_sd.items():
for transform in transforms_to_try:
new_key = transform(key)
if (
new_key
and new_key in model_sd
and new_key not in best
and tuple(model_sd[new_key].shape) == tuple(value.shape)
):
best[new_key] = value
used_ckpt_keys.add(key)
break
missing = sorted(set(model_sd) - set(best))
unexpected = sorted(set(ckpt_sd) - used_ckpt_keys)
# The decode-head classifier is what turns features into crop/background.
classifier_loaded = any("classifier" in k for k in best)
info = {
"model_total": len(model_sd),
"ckpt_total": len(ckpt_sd),
"matched": len(best),
"missing": missing,
"unexpected": unexpected,
"classifier_loaded": classifier_loaded,
}
return best, info
class CropSegmenter:
"""Loads the fine-tuned SegFormer-B2 model and performs inference."""
def __init__(self) -> None:
self.device = torch.device(
"cuda" if torch.cuda.is_available() else "cpu"
)
logger.info("Using device: %s", self.device)
self.last_logit_summary: dict | None = None
# Build architecture from config (weights come from best.pt)
try:
seg_config = SegformerConfig.from_pretrained(
MODEL_NAME, num_labels=NUM_LABELS
)
self.model = SegformerForSemanticSegmentation(seg_config)
except Exception as exc: # noqa: BLE001
try:
self.model = SegformerForSemanticSegmentation.from_pretrained(
MODEL_NAME, num_labels=NUM_LABELS, ignore_mismatched_sizes=True
)
except Exception as exc2: # noqa: BLE001
raise ModelLoadError(
f"Could not build SegFormer from '{MODEL_NAME}': {exc2}"
) from exc
weights_path = _resolve_weights_path()
try:
try:
checkpoint = torch.load(weights_path, map_location=self.device)
except Exception:
checkpoint = torch.load(
weights_path, map_location=self.device, weights_only=False
)
except Exception as exc: # noqa: BLE001
raise ModelLoadError(
f"Failed to read weights file '{weights_path}': {exc}"
) from exc
ckpt_sd = _extract_state_dict(checkpoint)
aligned, self.load_info = _align_state_dict(
ckpt_sd, self.model.state_dict()
)
self.model.load_state_dict(aligned, strict=False)
logger.info(
"Weights matched %d/%d (ckpt had %d). classifier_loaded=%s",
self.load_info["matched"],
self.load_info["model_total"],
self.load_info["ckpt_total"],
self.load_info["classifier_loaded"],
)
if self.load_info["matched"] == 0:
raise ModelLoadError(
"None of the weights in best.pt matched the SegFormer-B2 "
"architecture. The checkpoint is likely from a different model."
)
self.model.to(self.device)
self.model.eval()
def _build_preprocess(self, size: int, normalization: str):
steps = [transforms.Resize((size, size)), transforms.ToTensor()]
if normalization == "imagenet":
steps.append(transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD))
elif normalization == "minus_one_one":
steps.append(transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)))
# "zero_one" -> ToTensor only (pixels in [0, 1])
return transforms.Compose(steps)
@torch.no_grad()
def predict(
self,
image: Image.Image,
input_size: int | None = None,
swap_classes: bool = False,
normalization: str = "imagenet",
) -> np.ndarray:
"""
Run segmentation. Returns a uint8 mask (H, W) in {0, 1} at the original
resolution. Also stores per-call logit stats in self.last_logit_summary.
"""
try:
size = int(input_size or INPUT_SIZE)
if image.mode != "RGB":
image = image.convert("RGB")
orig_w, orig_h = image.size
preprocess = self._build_preprocess(size, normalization)
tensor = preprocess(image).unsqueeze(0).to(self.device)
logits = self.model(pixel_values=tensor).logits # (1, 2, h/4, w/4)
upsampled = F.interpolate(
logits, size=(orig_h, orig_w),
mode="bilinear", align_corners=False,
)
class0 = upsampled[:, 0]
class1 = upsampled[:, 1]
self.last_logit_summary = {
"frac_pixels_pred_crop": float((class1 > class0).float().mean()),
"mean_gap_crop_minus_bg": float((class1 - class0).mean().item()),
"logit_min": float(upsampled.min().item()),
"logit_max": float(upsampled.max().item()),
}
mask = upsampled.argmax(dim=1).squeeze(0).to("cpu").numpy().astype(np.uint8)
if swap_classes:
mask = (1 - mask).astype(np.uint8)
return mask
except Exception as exc: # noqa: BLE001
raise InferenceError(f"Inference failed: {exc}") from exc
|