double-exposure / app /segment.py
Eddie Faillace
double-exposure app deploy snapshot 2026-07-21 (WP-24 calibration pass)
7dff04f
Raw
History Blame Contribute Delete
3.77 kB
"""WP-22: click-to-segment — the user taps an object, SAM supplies the precision.
The brush asked users to be precise with their hand (shading four paintings with
a mouse; hopeless on a phone), then guided-filter propagation guessed the edges.
Click-to-segment inverts the contract: the user supplies INTENT (one tap: "this
thing, it's a painting, Scene 2") and a promptable segmenter supplies the pixels.
Verified live on photo 128 (2026-07-19): SlimSAM-uniform-50 (28M params) returns
whole-print masks with real edges THROUGH the other exposure's ghosts, IoU
0.79–0.93, 0.7 s one-time image embed + ~33 ms per click on CPU.
Everything is lazy and fail-soft: no transformers / no weights → point_mask
returns None and the UI falls back to the brush. The photo never leaves the
process — segmentation is local, free, and private.
"""
from __future__ import annotations
from typing import List, Optional, Tuple
import numpy as np
# Small enough for Space cold-starts (~28 MB weights), good enough for tap-to-mask
# on clear objects; the brush remains for everything else.
SEGMENT_MODEL: str = "Zigeng/SlimSAM-uniform-50"
_MODEL = None # (model, processor) after first successful load
_LOAD_FAILED: Optional[str] = None
# Single-slot embedding cache: (key, embeddings, original PIL size). One photo is
# worked on at a time; re-embedding on photo switch costs ~1 s.
_EMBED: dict = {}
def _load():
global _MODEL, _LOAD_FAILED
if _MODEL is not None or _LOAD_FAILED is not None:
return _MODEL
try:
from transformers import SamModel, SamProcessor
model = SamModel.from_pretrained(SEGMENT_MODEL)
model.eval()
_MODEL = (model, SamProcessor.from_pretrained(SEGMENT_MODEL))
except Exception as exc: # missing dep, no network for weights, OOM…
_LOAD_FAILED = str(exc)
return _MODEL
def _image_key(rgb: np.ndarray) -> tuple:
"""Cheap content key: shape + a sparse pixel checksum (no full-image hash)."""
flat = rgb.reshape(-1)
stride = max(1, flat.size // 4096)
return (rgb.shape, float(flat[::stride].sum()))
def point_mask(
rgb: np.ndarray, points: List[Tuple[float, float]]
) -> Optional[Tuple[np.ndarray, float]]:
"""Mask for the object under the given positive click points.
``rgb`` float [0,1] HxWx3 in the SAME geometry the user clicked (the working
frame); ``points`` are (x, y) pixel coords, several clicks refining one
object. Returns (bool HxW mask, iou confidence), or None when the model is
unavailable (caller falls back to the brush) or the points are empty.
"""
if not points:
return None
loaded = _load()
if loaded is None:
return None
model, processor = loaded
import torch
from PIL import Image
pil = Image.fromarray((np.clip(rgb, 0, 1) * 255).astype(np.uint8))
key = _image_key(rgb)
if _EMBED.get("key") != key:
inputs = processor(pil, return_tensors="pt")
with torch.no_grad():
emb = model.get_image_embeddings(inputs["pixel_values"])
_EMBED.update(key=key, emb=emb)
inp = processor(pil, input_points=[[list(p) for p in points]], return_tensors="pt")
inp.pop("pixel_values")
with torch.no_grad():
out = model(**inp, image_embeddings=_EMBED["emb"], multimask_output=True)
masks = processor.image_processor.post_process_masks(
out.pred_masks.cpu(), inp["original_sizes"].cpu(), inp["reshaped_input_sizes"].cpu()
)[0][0]
scores = out.iou_scores[0, 0].cpu().numpy()
best = int(scores.argmax())
return masks[best].numpy().astype(bool), float(scores[best])
def load_error() -> Optional[str]:
"""Why the segmenter is unavailable (None while unloaded or fine)."""
return _LOAD_FAILED