Files
lightcraft/crates/segment/src/tracker.rs
T
Brandon ThomasandClaude Opus 5.5 b52f43d1d5 M12: lightcraft-segment: Apache-2.0, checked weights, pure-Rust model download
- The crate ports Apache-2.0-only Hugging Face transformers code: it is
  now `license = "Apache-2.0"`, and every ported file carries the
  modification notice Apache-2.0 §4(b) asks for.
- Weights::open checks every tensor's byte range (and the header)
  against the file's length: a truncated or damaged download is an
  error at load, never a short read later; regression test with
  hostile offsets.
- fetch/: the model download, only when asked for. An ordered mirror
  list (env LIGHTCRAFT_SAM3_MIRRORS, a mirrors file, DEFAULT_MIRRORS —
  empty until LightCraft's CDN exists, TODO for the maintainer),
  fallback to the next mirror, resume via Range, redirects, connect and
  stall timeouts, cancel, size caps, exact size + SHA-256 (pinned from
  tools/install-sam3.sh) before an atomic rename; damaged files are
  deleted, HTTP error bodies never written.
- HTTP/1.1 client over std sockets + rustls with the RustCrypto provider
  (rustls-rustcrypto) and webpki-roots: no ring / aws-lc-rs, nothing
  compiled from C (no new `cc` users in the app's build graph).
- Tests against a local TcpListener server (no internet): fallback,
  redirects, chunked bodies, resume, 416, hash mismatch, too much data,
  stalls, cancel.
- *.safetensors are gitignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 09:56:49 -04:00

254 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Point prompts (SAM 3's interactive "tracker" head, SAM 2 style): a prompt encoder for
//! positive/negative clicks and a two-way-transformer mask decoder over the image features.
//!
//! Modified work (Apache License 2.0, §4(b)): ported by the LightCraft contributors in 2026 from
//! the Python/PyTorch SAM 3 code of Hugging Face Transformers (`models/sam3_tracker/modeling_sam3_tracker.py`), Copyright The HuggingFace
//! Team and Meta Platforms, Inc.; translated to Rust on candle and restructured. See NOTICE.
use candle_core::{DType, Device, IndexOp, Module, Tensor};
use candle_nn::{Conv2d, Conv2dConfig, ConvTranspose2d, ConvTranspose2dConfig, LayerNorm, VarBuilder};
use crate::nn::{Attention, FeedForward, TORCH_LN_EPS, ln, ln_channels};
use crate::vit::{GRID, IMAGE};
use crate::{Result, nn::to_vec};
const HIDDEN: usize = 256;
const MASK_TOKENS: usize = 4;
/// Low-resolution mask side (4 × the 72 grid).
pub const LOW_RES: usize = 288;
/// The image features the decoder needs, computed once per image.
pub struct Features {
/// Level 2 (72 × 72, 256 channels).
pub image: Tensor,
/// `conv_s0`(level 0): `[1, 32, 288, 288]`.
pub s0: Tensor,
/// `conv_s1`(level 1): `[1, 64, 144, 144]`.
pub s1: Tensor,
}
/// One click: position normalized to the image (0..1), and whether it includes or excludes.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Click {
pub x: f32,
pub y: f32,
pub positive: bool,
}
/// A predicted mask: 288 × 288 logits over the (squared) model input, and its quality.
pub struct Prediction {
pub logits: Vec<f32>,
pub iou: f32,
pub object_score: f32,
}
/// Random-Fourier positional encoding of points normalized to 0..1 (`Sam3TrackerPositionalEmbedding`).
struct Fourier {
pe: Vec<f32>, // [2, 128]
}
impl Fourier {
fn encode(&self, x: f32, y: f32) -> Vec<f32> {
let half = self.pe.len() / 2;
let (cx, cy) = (2.0 * x - 1.0, 2.0 * y - 1.0);
let proj: Vec<f32> = (0..half)
.map(|i| {
let a = cx * self.pe.get(i).copied().unwrap_or(0.0) + cy * self.pe.get(half + i).copied().unwrap_or(0.0);
2.0 * std::f32::consts::PI * a
})
.collect();
proj.iter().map(|a| a.sin()).chain(proj.iter().map(|a| a.cos())).collect()
}
}
struct TwoWayBlock {
self_attn: Attention,
ln1: LayerNorm,
t2i: Attention,
ln2: LayerNorm,
mlp: FeedForward,
ln3: LayerNorm,
ln4: LayerNorm,
i2t: Attention,
skip_first_pe: bool,
}
/// `Sam3TrackerAttention` on `[1, L, C]` (the point batch of one is folded away).
fn attn(a: &Attention, q: &Tensor, k: &Tensor, v: &Tensor) -> Result<Tensor> {
a.forward(q, k, v, None)
}
impl TwoWayBlock {
fn forward(&self, queries: &Tensor, keys: &Tensor, qpe: &Tensor, kpe: &Tensor) -> Result<(Tensor, Tensor)> {
let queries = if self.skip_first_pe {
attn(&self.self_attn, queries, queries, queries)?
} else {
let q = (queries + qpe)?;
(queries + attn(&self.self_attn, &q, &q, queries)?)?
};
let queries = self.ln1.forward(&queries)?;
let (q, k) = ((&queries + qpe)?, (keys + kpe)?);
let queries = self.ln2.forward(&(&queries + attn(&self.t2i, &q, &k, keys)?)?)?;
let queries = self.ln3.forward(&(&queries + self.mlp.forward(&queries)?)?)?;
let (q, k) = ((&queries + qpe)?, (keys + kpe)?);
let keys = self.ln4.forward(&(keys + attn(&self.i2t, &k, &q, &queries)?)?)?;
Ok((queries, keys))
}
}
pub struct Tracker {
device: Device,
point_pe: Fourier,
image_pe: Tensor,
point_embed: Tensor,
not_a_point: Tensor,
no_mask: Tensor,
conv_s0: Conv2d,
conv_s1: Conv2d,
output_tokens: Tensor,
blocks: Vec<TwoWayBlock>,
final_t2i: Attention,
ln_final: LayerNorm,
up1: ConvTranspose2d,
up_ln: LayerNorm,
up2: ConvTranspose2d,
hyper: Vec<FeedForward>,
iou_head: FeedForward,
obj_head: FeedForward,
}
fn read_vec(vb: &VarBuilder, shape: (usize, usize), name: &str) -> Result<Vec<f32>> {
to_vec(&vb.get(shape, name)?)
}
impl Tracker {
/// `vb` at `tracker_model`.
pub fn new(vb: VarBuilder) -> Result<Self> {
let device = vb.device().clone();
let pe = vb.pp("prompt_encoder");
let md = vb.pp("mask_decoder");
let image_fourier = Fourier { pe: read_vec(&vb, (2, HIDDEN / 2), "shared_image_embedding.positional_embedding")? };
// image-wide positional embedding: pixel centres of the 72 × 72 grid
let mut grid = Vec::with_capacity(GRID * GRID * HIDDEN);
for r in 0..GRID {
for c in 0..GRID {
grid.extend(image_fourier.encode((c as f32 + 0.5) / GRID as f32, (r as f32 + 0.5) / GRID as f32));
}
}
let image_pe = Tensor::from_vec(grid, (1, GRID * GRID, HIDDEN), &device)?;
let t = md.pp("transformer");
let mut blocks = Vec::new();
for i in 0..2 {
let b = t.pp(format!("layers.{i}"));
blocks.push(TwoWayBlock {
self_attn: Attention::new(HIDDEN, HIDDEN, 8, b.pp("self_attn"))?,
ln1: ln(HIDDEN, TORCH_LN_EPS, b.pp("layer_norm1"))?,
t2i: Attention::new(HIDDEN, HIDDEN / 2, 8, b.pp("cross_attn_token_to_image"))?,
ln2: ln(HIDDEN, TORCH_LN_EPS, b.pp("layer_norm2"))?,
mlp: FeedForward::new(HIDDEN, 2048, HIDDEN, 2, false, b.pp("mlp"))?,
ln3: ln(HIDDEN, TORCH_LN_EPS, b.pp("layer_norm3"))?,
ln4: ln(HIDDEN, TORCH_LN_EPS, b.pp("layer_norm4"))?,
i2t: Attention::new(HIDDEN, HIDDEN / 2, 8, b.pp("cross_attn_image_to_token"))?,
skip_first_pe: i == 0,
});
}
let output_tokens = Tensor::cat(
&[
md.get((1, HIDDEN), "obj_score_token.weight")?,
md.get((1, HIDDEN), "iou_token.weight")?,
md.get((MASK_TOKENS, HIDDEN), "mask_tokens.weight")?,
],
0,
)?;
let up = ConvTranspose2dConfig { stride: 2, ..Default::default() };
let one = Conv2dConfig::default();
Ok(Self {
point_pe: Fourier { pe: read_vec(&pe, (2, HIDDEN / 2), "shared_embedding.positional_embedding")? },
image_pe,
point_embed: pe.get((4, HIDDEN), "point_embed.weight")?.to_device(&Device::Cpu)?,
not_a_point: pe.get((1, HIDDEN), "not_a_point_embed.weight")?.to_device(&Device::Cpu)?,
no_mask: pe.get((1, HIDDEN), "no_mask_embed.weight")?.reshape((1, HIDDEN, 1, 1))?,
conv_s0: candle_nn::conv2d(HIDDEN, HIDDEN / 8, 1, one, md.pp("conv_s0"))?,
conv_s1: candle_nn::conv2d(HIDDEN, HIDDEN / 4, 1, one, md.pp("conv_s1"))?,
output_tokens,
blocks,
final_t2i: Attention::new(HIDDEN, HIDDEN / 2, 8, t.pp("final_attn_token_to_image"))?,
ln_final: ln(HIDDEN, TORCH_LN_EPS, t.pp("layer_norm_final_attn"))?,
up1: candle_nn::conv_transpose2d(HIDDEN, HIDDEN / 4, 2, up, md.pp("upscale_conv1"))?,
up_ln: ln(HIDDEN / 4, 1e-6, md.pp("upscale_layer_norm"))?,
up2: candle_nn::conv_transpose2d(HIDDEN / 4, HIDDEN / 8, 2, up, md.pp("upscale_conv2"))?,
hyper: (0..MASK_TOKENS)
.map(|i| FeedForward::new(HIDDEN, HIDDEN, HIDDEN / 8, 3, false, md.pp(format!("output_hypernetworks_mlps.{i}"))))
.collect::<Result<_>>()?,
iou_head: FeedForward::new(HIDDEN, HIDDEN, MASK_TOKENS, 3, true, md.pp("iou_prediction_head"))?,
obj_head: FeedForward::new(HIDDEN, HIDDEN, 1, 3, false, md.pp("pred_obj_score_head"))?,
device,
})
}
/// The decoder's image features from the tracker neck's levels (288, 144, 72 per side).
pub fn features(&self, levels: &[Tensor]) -> Result<Features> {
let [l0, l1, l2, ..] = levels else { return Err(crate::Error::Model("tracker needs three feature levels".into())) };
Ok(Features { image: l2.clone(), s0: self.conv_s0.forward(l0)?, s1: self.conv_s1.forward(l1)? })
}
/// Sparse prompt embeddings `[1, n + 1, 256]` (a padding point closes the list).
fn embed_clicks(&self, clicks: &[Click]) -> Result<Tensor> {
let point_embed = to_vec(&self.point_embed)?;
let not_a_point = to_vec(&self.not_a_point)?;
let mut rows = Vec::with_capacity((clicks.len() + 1) * HIDDEN);
for c in clicks {
// the reference shifts pixel coordinates (in the 1008² input) by half a pixel
let (x, y) = ((c.x * IMAGE as f32 + 0.5) / IMAGE as f32, (c.y * IMAGE as f32 + 0.5) / IMAGE as f32);
let e = self.point_pe.encode(x, y);
let label = usize::from(c.positive);
rows.extend(e.iter().enumerate().map(|(i, v)| v + point_embed.get(label * HIDDEN + i).copied().unwrap_or(0.0)));
}
rows.extend_from_slice(&not_a_point);
Ok(Tensor::from_vec(rows, (1, clicks.len() + 1, HIDDEN), &self.device)?)
}
/// Predict masks for `clicks`. `multimask`: the three alternatives (best by predicted IoU is
/// returned), else the single-mask output with SAM 2's stability fallback.
pub fn predict(&self, f: &Features, clicks: &[Click], multimask: bool) -> Result<Prediction> {
let sparse = self.embed_clicks(clicks)?;
let tokens = Tensor::cat(&[self.output_tokens.unsqueeze(0)?, sparse], 1)?; // [1, T, 256]
let image = f.image.broadcast_add(&self.no_mask)?; // dense prompt: "no mask"
let (b, c, h, w) = image.dims4()?;
let keys = image.flatten_from(2)?.transpose(1, 2)?.contiguous()?; // [1, HW, 256]
let (mut q, mut k) = (tokens.clone(), keys);
for blk in &self.blocks {
(q, k) = blk.forward(&q, &k, &tokens, &self.image_pe)?;
}
let qf = (&q + &tokens)?;
let kf = (&k + &self.image_pe)?;
let q = self.ln_final.forward(&(&q + attn(&self.final_t2i, &qf, &kf, &k)?)?)?;
let iou_token = q.i((.., 1, ..))?;
let image = k.transpose(1, 2)?.reshape((b, c, h, w))?;
let up = (self.up1.forward(&image)? + &f.s1)?;
let up = ln_channels(&up, &self.up_ln)?.gelu_erf()?;
let up = (self.up2.forward(&up)? + &f.s0)?.gelu_erf()?; // [1, 32, 288, 288]
let (_, uc, uh, uw) = up.dims4()?;
let hyper = (0..MASK_TOKENS)
.map(|i| self.hyper.get(i).map_or(Err(crate::Error::Model("hypernetwork".into())), |m| m.forward(&q.i((.., 2 + i, ..))?)))
.collect::<Result<Vec<_>>>()?;
let hyper = Tensor::stack(&hyper, 1)?; // [1, 4, 32]
let masks = hyper.matmul(&up.reshape((b, uc, uh * uw))?)?.reshape((b, MASK_TOKENS, uh, uw))?;
let iou = to_vec(&self.iou_head.forward(&iou_token)?)?;
let object_score = to_vec(&self.obj_head.forward(&q.i((.., 0, ..))?)?)?.first().copied().unwrap_or(0.0);
let pick = |i: usize| -> Result<Prediction> {
Ok(Prediction { logits: to_vec(&masks.i((0, i))?)?, iou: iou.get(i).copied().unwrap_or(0.0), object_score })
};
let best_multi = || (1..MASK_TOKENS).max_by(|a, b| iou.get(*a).partial_cmp(&iou.get(*b)).unwrap_or(std::cmp::Ordering::Equal)).unwrap_or(1);
if multimask {
return pick(best_multi());
}
// single-mask output unless it is unstable (area changes > 2 % when the threshold moves ±0.05)
let single = masks.i((0, 0))?;
let inter = single.gt(0.05)?.to_dtype(DType::F32)?.sum_all()?.to_scalar::<f32>()?;
let union = single.gt(-0.05)?.to_dtype(DType::F32)?.sum_all()?.to_scalar::<f32>()?;
let stability = if union > 0.0 { inter / union } else { 1.0 };
if stability >= 0.98 { pick(0) } else { pick(best_multi()) }
}
}