Skip to main content

cortiq_engine/
sam3vid.rs

1//! SAM3 **video** mode: the image detector plus the SAM2-style memory-bank
2//! tracker packed in the same CMF (`trk.*`, 309 tensors, plus the tracker's
3//! own FPN neck, which lives in the `fpn.sam2_convs` section).
4//!
5//! One frame goes through the same six steps as transformers 5.20.2
6//! `Sam3VideoModel._det_track_one_frame`:
7//!
8//! 1. **detect** — one image forward ([`Sam3::forward`](crate::sam3::Sam3)),
9//!    score `sigmoid(logit)·sigmoid(presence)`, mask-IoU NMS at
10//!    `det_nms_thresh`, keep above `score_threshold_detection`;
11//! 2. **tracker features** — trunk map → `fpn.sam2_convs` neck → levels
12//!    288/144/72 with `conv_s0`/`conv_s1` on the two finest;
13//! 3. **propagate** — per tracked object the current frame's features are
14//!    fused with the memory bank (`trk.transformer.encoder.*`: 4 RoPE
15//!    layers over past `maskmem_features` + object pointers) and decoded by
16//!    the SAM mask decoder into 288² mask logits, object score and object
17//!    pointer; small holes/sprinkles are filled;
18//! 4. **plan** — IoU association with the detector
19//!    (`assoc_iou_thresh` / `trk_assoc_iou_thresh` / `new_det_thresh`),
20//!    hotstart keep-alive + duplicate bookkeeping, recent-occlusion
21//!    suppression, non-overlap constraints and a fresh memory encode of the
22//!    resulting masks;
23//! 5. **execute** — new detections become conditioning frames
24//!    (`_use_mask_as_output`: the detection mask IS the output, the decoder
25//!    only provides the object pointer), dropped objects leave the session;
26//! 6. **outputs** — per-object 288² mask logits, bilinearly interpolated to
27//!    the frame and binarized at 0; box = mask bounding box (inclusive
28//!    corners, like `torchvision.masks_to_boxes`); `track_id` is the session
29//!    object id, stable for the object's whole life.
30//!
31//! Batch size 1 — one object at a time, as in the reference.
32
33use std::collections::{BTreeMap, HashMap, HashSet};
34use std::time::Instant;
35
36use anyhow::{anyhow, Result};
37use cortiq_core::CmfModel;
38
39use crate::inference::gelu_erf;
40use crate::pool::Pool;
41use crate::sam3::{self, Sam3, Sam3Out};
42use crate::sam3vid_net::{
43    dense, ln_chw, resize2d, sine, sine_1d, sigm, Net, TrkFeats, EMB, HID, MEM, NO_OBJ_SCORE,
44};
45
46/// Low-resolution mask side (`low_res_mask_size`), also the detector's
47/// `cfg.processor.mask_size`.
48pub const MASK_SIDE: usize = 288;
49/// Tracker input resolution (`tracker_config.image_size`).
50const IMG: usize = 1008;
51/// Memory-encoder mask target side (`mask_input_size · 4`).
52const MEM_SIDE: usize = 1152;
53
54// ───────────────────── configuration ─────────────────────
55
56/// Every knob the video pipeline reads, from the CMF's `cfg.tracker`
57/// (the vendor `config.json` of `Sam3VideoModel`). The defaults below are
58/// the shipped values; [`VidCfg::from_cmf`] overrides them from the file.
59#[derive(Clone, Debug, PartialEq)]
60pub struct VidCfg {
61    pub score_threshold_detection: f32,
62    pub det_nms_thresh: f32,
63    pub assoc_iou_thresh: f32,
64    pub trk_assoc_iou_thresh: f32,
65    pub new_det_thresh: f32,
66    pub recondition_on_trk_masks: bool,
67    pub recondition_every_nth_frame: i64,
68    pub hotstart_delay: i64,
69    pub hotstart_unmatch_thresh: usize,
70    pub hotstart_dup_thresh: usize,
71    pub suppress_unmatched_only_within_hotstart: bool,
72    pub init_trk_keep_alive: i64,
73    pub max_trk_keep_alive: i64,
74    pub min_trk_keep_alive: i64,
75    pub suppress_recent_occlusion_thresh: f32,
76    pub decrease_keep_alive_for_empty: bool,
77    pub fill_hole_area: usize,
78    pub max_num_objects: usize,
79    pub high_conf_thresh: f32,
80    pub high_iou_thresh: f32,
81    pub low_res_mask_size: usize,
82    pub num_maskmem: usize,
83    pub max_cond_frame_num: usize,
84    pub max_object_pointers: usize,
85    pub sigmoid_scale: f32,
86    pub sigmoid_bias: f32,
87    pub temporal_ptr_pe: bool,
88    pub occlusion_spatial: bool,
89    /// `multimask_output_in_sam && multimask_output_for_tracking` — the
90    /// tracker decodes three masks and keeps the best-IoU one.
91    pub multimask: bool,
92}
93
94impl Default for VidCfg {
95    fn default() -> Self {
96        Self {
97            score_threshold_detection: 0.5,
98            det_nms_thresh: 0.1,
99            assoc_iou_thresh: 0.1,
100            trk_assoc_iou_thresh: 0.5,
101            new_det_thresh: 0.7,
102            recondition_on_trk_masks: false,
103            recondition_every_nth_frame: 16,
104            hotstart_delay: 15,
105            hotstart_unmatch_thresh: 8,
106            hotstart_dup_thresh: 8,
107            suppress_unmatched_only_within_hotstart: true,
108            init_trk_keep_alive: 30,
109            max_trk_keep_alive: 30,
110            min_trk_keep_alive: -1,
111            suppress_recent_occlusion_thresh: 0.7,
112            decrease_keep_alive_for_empty: false,
113            fill_hole_area: 16,
114            max_num_objects: 10_000,
115            high_conf_thresh: 0.8,
116            high_iou_thresh: 0.8,
117            low_res_mask_size: MASK_SIDE,
118            num_maskmem: 7,
119            max_cond_frame_num: 4,
120            max_object_pointers: 16,
121            sigmoid_scale: 20.0,
122            sigmoid_bias: -10.0,
123            temporal_ptr_pe: true,
124            occlusion_spatial: true,
125            multimask: true,
126        }
127    }
128}
129
130impl VidCfg {
131    /// Parse `cfg.tracker` out of the CMF (missing keys keep the defaults).
132    pub fn from_cmf(m: &CmfModel) -> Result<Self> {
133        let mut c = Self::default();
134        let raw = m
135            .tensor("cfg.tracker")
136            .ok_or_else(|| anyhow!("the CMF carries no cfg.tracker section — not a SAM3 video model"))?;
137        let bytes = m.entry_bytes(raw);
138        let v: serde_json::Value =
139            serde_json::from_slice(bytes).map_err(|e| anyhow!("cfg.tracker is not JSON: {e}"))?;
140        let f = |k: &str, d: f32| v[k].as_f64().map_or(d, |x| x as f32);
141        let i = |k: &str, d: i64| v[k].as_i64().unwrap_or(d);
142        let b = |k: &str, d: bool| v[k].as_bool().unwrap_or(d);
143        let u = |k: &str, d: usize| v[k].as_u64().map_or(d, |x| x as usize);
144
145        c.score_threshold_detection = f("score_threshold_detection", c.score_threshold_detection);
146        c.det_nms_thresh = f("det_nms_thresh", c.det_nms_thresh);
147        c.assoc_iou_thresh = f("assoc_iou_thresh", c.assoc_iou_thresh);
148        c.trk_assoc_iou_thresh = f("trk_assoc_iou_thresh", c.trk_assoc_iou_thresh);
149        c.new_det_thresh = f("new_det_thresh", c.new_det_thresh);
150        c.recondition_on_trk_masks = b("recondition_on_trk_masks", c.recondition_on_trk_masks);
151        c.recondition_every_nth_frame = i("recondition_every_nth_frame", c.recondition_every_nth_frame);
152        c.hotstart_delay = i("hotstart_delay", c.hotstart_delay);
153        c.hotstart_unmatch_thresh = u("hotstart_unmatch_thresh", c.hotstart_unmatch_thresh);
154        c.hotstart_dup_thresh = u("hotstart_dup_thresh", c.hotstart_dup_thresh);
155        c.suppress_unmatched_only_within_hotstart =
156            b("suppress_unmatched_only_within_hotstart", c.suppress_unmatched_only_within_hotstart);
157        c.init_trk_keep_alive = i("init_trk_keep_alive", c.init_trk_keep_alive);
158        c.max_trk_keep_alive = i("max_trk_keep_alive", c.max_trk_keep_alive);
159        c.min_trk_keep_alive = i("min_trk_keep_alive", c.min_trk_keep_alive);
160        c.suppress_recent_occlusion_thresh = f(
161            "suppress_overlapping_based_on_recent_occlusion_threshold",
162            c.suppress_recent_occlusion_thresh,
163        );
164        c.decrease_keep_alive_for_empty =
165            b("decrease_trk_keep_alive_for_empty_masklets", c.decrease_keep_alive_for_empty);
166        c.fill_hole_area = u("fill_hole_area", c.fill_hole_area);
167        c.max_num_objects = u("max_num_objects", c.max_num_objects);
168        c.high_conf_thresh = f("high_conf_thresh", c.high_conf_thresh);
169        c.high_iou_thresh = f("high_iou_thresh", c.high_iou_thresh);
170        c.low_res_mask_size = u("low_res_mask_size", c.low_res_mask_size);
171        let t = &v["tracker_config"];
172        c.num_maskmem = t["num_maskmem"].as_u64().map_or(c.num_maskmem, |x| x as usize);
173        c.max_cond_frame_num = t["max_cond_frame_num"].as_u64().map_or(c.max_cond_frame_num, |x| x as usize);
174        c.max_object_pointers =
175            t["max_object_pointers_in_encoder"].as_u64().map_or(c.max_object_pointers, |x| x as usize);
176        c.sigmoid_scale = t["sigmoid_scale_for_mem_enc"].as_f64().map_or(c.sigmoid_scale, |x| x as f32);
177        c.sigmoid_bias = t["sigmoid_bias_for_mem_enc"].as_f64().map_or(c.sigmoid_bias, |x| x as f32);
178        c.temporal_ptr_pe = t["enable_temporal_pos_encoding_for_object_pointers"]
179            .as_bool()
180            .unwrap_or(c.temporal_ptr_pe);
181        c.occlusion_spatial =
182            t["enable_occlusion_spatial_embedding"].as_bool().unwrap_or(c.occlusion_spatial);
183        c.multimask = t["multimask_output_in_sam"].as_bool().unwrap_or(true)
184            && t["multimask_output_for_tracking"].as_bool().unwrap_or(true);
185        anyhow::ensure!(
186            c.low_res_mask_size == MASK_SIDE,
187            "unsupported low_res_mask_size {}",
188            c.low_res_mask_size
189        );
190        anyhow::ensure!(c.num_maskmem <= 7, "num_maskmem {} > 7", c.num_maskmem);
191        Ok(c)
192    }
193}
194
195// ───────────────────── session state ─────────────────────
196
197/// One object's stored output for one frame.
198#[derive(Default, Clone)]
199struct ObjOut {
200    /// 288² mask logits.
201    pred_masks: Vec<f32>,
202    /// Decoder object-score logit (±10 for mask-input frames).
203    obj_score: f32,
204    /// 256-d object pointer.
205    pointer: Vec<f32>,
206    mem_feat: Option<Vec<f32>>,
207    mem_pos: Option<Vec<f32>>,
208}
209
210#[derive(Default)]
211struct ObjState {
212    cond: BTreeMap<usize, ObjOut>,
213    non_cond: BTreeMap<usize, ObjOut>,
214}
215
216/// Persistent tracking state across frames (`Sam3VideoInferenceSession`).
217pub struct Session {
218    /// Frames in the clip (0 = unknown/streaming); the reference caps
219    /// `max_object_pointers_to_use` with it.
220    pub num_frames: usize,
221    /// Ordered object ids — the **track ids** the CLI reports.
222    pub obj_ids: Vec<i64>,
223    pub max_obj_id: i64,
224    objs: HashMap<i64, ObjState>,
225    /// Detection score captured when the object was created.
226    score: HashMap<i64, f32>,
227    /// Frame-wise tracker score (sigmoid of the object-score logit).
228    trk_score_fw: HashMap<i64, HashMap<usize, f32>>,
229    last_occluded: HashMap<i64, i64>,
230    first_frame: HashMap<i64, i64>,
231    unmatched: HashMap<i64, Vec<usize>>,
232    keep_alive: HashMap<i64, i64>,
233    overlap: HashMap<(i64, i64), Vec<usize>>,
234    removed: HashSet<i64>,
235    suppressed: HashSet<i64>,
236}
237
238impl Default for Session {
239    fn default() -> Self {
240        Self {
241            // the reference starts `max_obj_id` at -1, so the first object
242            // created gets track id 0
243            max_obj_id: -1,
244            num_frames: 0,
245            obj_ids: Vec::new(),
246            objs: HashMap::new(),
247            score: HashMap::new(),
248            trk_score_fw: HashMap::new(),
249            last_occluded: HashMap::new(),
250            first_frame: HashMap::new(),
251            unmatched: HashMap::new(),
252            keep_alive: HashMap::new(),
253            overlap: HashMap::new(),
254            removed: HashSet::new(),
255            suppressed: HashSet::new(),
256        }
257    }
258}
259
260impl Session {
261    pub fn new() -> Self {
262        Self::default()
263    }
264
265    /// Object ids hidden from the outputs (suppressed / hotstart-removed).
266    pub fn hidden_ids(&self) -> Vec<i64> {
267        self.removed.union(&self.suppressed).copied().collect()
268    }
269}
270
271// ───────────────────── outputs ─────────────────────
272
273/// One object's observation on one frame.
274#[derive(Clone, Debug)]
275pub struct TrackObs {
276    pub id: i64,
277    pub score: f32,
278    /// `xyxy` in frame pixels (inclusive corners), from the mask box.
279    pub box_px: [f32; 4],
280    /// Binary mask at frame resolution, row-major `width × height`.
281    pub mask: Vec<u8>,
282    /// `true` when this frame's mask came straight from the detector.
283    pub from_detection: bool,
284    /// `true` when the tracker propagated the object onto this frame.
285    pub from_tracker: bool,
286}
287
288/// Everything one frame produced.
289#[derive(Clone, Debug)]
290pub struct FrameOut {
291    pub frame: usize,
292    pub tracks: Vec<TrackObs>,
293    /// Raw detections above threshold this frame (before association).
294    pub detections: usize,
295    pub presence: f32,
296    pub det_ms: f64,
297    pub trk_ms: f64,
298    pub total_ms: f64,
299}
300
301// ───────────────────── the model ─────────────────────
302
303struct Det {
304    score: f32,
305    mask: Vec<f32>,
306}
307
308/// Detector + tracker over one CMF.
309pub struct Sam3Video {
310    sam: Sam3,
311    pub cfg: VidCfg,
312}
313
314impl Sam3Video {
315    pub fn open(path: &str) -> Result<Self> {
316        let sam = Sam3::open(path)?;
317        let cfg = VidCfg::from_cmf(sam.cmf())?;
318        Ok(Self { sam, cfg })
319    }
320
321    /// The image detector (shared prompt text tower, timing probes).
322    pub fn detector(&mut self) -> &mut Sam3 {
323        &mut self.sam
324    }
325
326    fn net(&self) -> Net<'_> {
327        Net {
328            m: self.sam.cmf(),
329            pool: self.sam.pool_handle().map(|p| p.as_ref()),
330            cfg: &self.cfg,
331        }
332    }
333
334    /// Run one frame: detector → propagation → planning → new objects →
335    /// outputs. `frame_size` is the original `(width, height)`.
336    pub fn forward(
337        &mut self,
338        s: &mut Session,
339        frame_idx: usize,
340        pixels: &[f32],
341        ids: &[u32],
342        attn: &[u8],
343        frame_size: (usize, usize),
344    ) -> Result<FrameOut> {
345        let t0 = Instant::now();
346        // ── 1. detector ────────────────────────────────────────────
347        let t_fwd = Instant::now();
348        let out = self.sam.forward(pixels, ids, attn)?;
349        let mut det_ms = t_fwd.elapsed().as_secs_f64() * 1e3;
350        let t_det = Instant::now();
351        let dets = self.detect(&out)?;
352        det_ms += t_det.elapsed().as_secs_f64() * 1e3;
353        let presence = sigm(out.presence_logit);
354
355        let net = self.net();
356        let t_trk = Instant::now();
357
358        // ── 2. tracker features for this frame ─────────────────────
359        let feats = net.feats(&out.trunk_feats)?;
360
361        // ── 3. propagate the existing objects ──────────────────────
362        let pre_ids = s.obj_ids.clone();
363        let mut trk_masks: Vec<Vec<f32>> = Vec::new();
364        let mut trk_scores: Vec<f32> = Vec::new();
365        if !pre_ids.is_empty() {
366            for &id in &pre_ids {
367                let (o, _high) = self.propagate(&net, s, id, frame_idx, &feats)?;
368                trk_scores.push(o.obj_score);
369                trk_masks.push(o.pred_masks.clone());
370                self.store(s, id, frame_idx, false, o);
371            }
372            // Propagation deliberately does NOT encode memory here. The HF
373            // pipeline defers it until planning has applied its constraints;
374            // `plan` below owns that encode (and execution may replace it when
375            // a new detection causes the tracker forward to run again).
376            fill_holes(&mut trk_masks, self.cfg.fill_hole_area);
377            for (i, &id) in pre_ids.iter().enumerate() {
378                let v = sigm(trk_scores[i]);
379                s.trk_score_fw.entry(id).or_default().insert(frame_idx, v);
380            }
381        }
382
383        // ── 4. planning phase ─────────────────────────────────────
384        let plan = self.plan(&net, s, frame_idx, &dets, &trk_masks, &feats)?;
385
386        // ── 5. execution phase: new objects become conditioning ───
387        if !plan.new_inds.is_empty() {
388            let mut pending: Vec<(i64, ObjOut, Vec<f32>)> = Vec::new();
389            for (k, &di) in plan.new_inds.iter().enumerate() {
390                let id = plan.new_ids[k];
391                let mask: Vec<f32> = dets[di]
392                    .mask
393                    .iter()
394                    .map(|&v| if v > 0.0 { 1.0 } else { 0.0 })
395                    .collect();
396                let (o, high) = self.use_mask_as_output(&net, &feats, &mask)?;
397                self.store(s, id, frame_idx, true, o.clone());
398                if !s.obj_ids.contains(&id) {
399                    s.obj_ids.push(id);
400                }
401                pending.push((id, o, high));
402            }
403            // the reference tracker forward re-runs every object that has no
404            // conditioning output at this frame (reconditioned ones are reused)
405            for &id in &pre_ids {
406                if plan.recond_ids.contains(&id) {
407                    continue;
408                }
409                let (o, high) = self.propagate(&net, s, id, frame_idx, &feats)?;
410                self.store(s, id, frame_idx, false, o.clone());
411                pending.push((id, o, high));
412            }
413            let any_mask_input = !pending.is_empty();
414            self.encode_into(&net, s, frame_idx, pending, &feats.l2, IMG, IMG, any_mask_input)?;
415        }
416
417        // ── 6. outputs ────────────────────────────────────────────
418        let tracks = self.build_outputs(s, frame_idx, &pre_ids, &dets, &plan, frame_size)?;
419        let trk_ms = t_trk.elapsed().as_secs_f64() * 1e3;
420
421        for id in &plan.removed {
422            s.obj_ids.retain(|x| x != id);
423            s.objs.remove(id);
424        }
425        s.suppressed.extend(plan.suppressed.iter().copied());
426        s.removed.extend(plan.removed.iter().copied());
427
428        Ok(FrameOut {
429            frame: frame_idx,
430            tracks,
431            detections: dets.len(),
432            presence,
433            det_ms,
434            trk_ms,
435            total_ms: t0.elapsed().as_secs_f64() * 1e3,
436        })
437    }
438
439    // ── detector post-processing ─────────────────────────────────
440
441    /// Scores → threshold → mask-IoU NMS → detections.
442    fn detect(&self, out: &Sam3Out) -> Result<Vec<Det>> {
443        anyhow::ensure!(
444            !out.pred_boxes.is_empty() && out.pred_logits.len() == out.pred_boxes.len() / 4,
445            "forward returned no scores/masks (SAM3_ONLY debug env set?)"
446        );
447        let presence = sigm(out.presence_logit);
448        let nq = out.pred_boxes.len() / 4;
449        let hw = out.pred_masks.len() / nq.max(1);
450        anyhow::ensure!(hw == MASK_SIDE * MASK_SIDE, "unexpected mask shape {hw}");
451        let thr = self.cfg.score_threshold_detection;
452        let mut scores: Vec<f32> = out.pred_logits.iter().map(|&l| sigm(l) * presence).collect();
453        let keep_inital: Vec<bool> = scores.iter().map(|&s| s > thr).collect();
454        let mut keep = keep_inital.clone();
455
456        if self.cfg.det_nms_thresh > 0.0 {
457            let valid: Vec<usize> = (0..nq).filter(|&i| keep[i]).collect();
458            if valid.len() > 1 {
459                let bin = |q: usize| -> Vec<u8> {
460                    out.pred_masks[q * hw..(q + 1) * hw]
461                        .iter()
462                        .map(|&v| u8::from(v > 0.0))
463                        .collect()
464                };
465                let bins: Vec<(usize, Vec<u8>)> = valid.iter().map(|&q| (q, bin(q))).collect();
466                let mut order: Vec<usize> = (0..bins.len()).collect();
467                order.sort_by(|a, b| {
468                    scores[bins[*a].0]
469                        .partial_cmp(&scores[bins[*b].0])
470                        .unwrap_or(std::cmp::Ordering::Equal)
471                });
472                let mut alive = vec![true; bins.len()];
473                for (k, &i) in order.iter().enumerate() {
474                    if !alive[i] {
475                        continue;
476                    }
477                    for &j in &order[k + 1..] {
478                        if alive[j] && mask_iou(&bins[i].1, &bins[j].1) >= self.cfg.det_nms_thresh {
479                            alive[j] = false;
480                        }
481                    }
482                }
483                for (idx, &(q, _)) in bins.iter().enumerate() {
484                    if !alive[idx] {
485                        keep[q] = false;
486                        scores[q] = 0.0;
487                    }
488                }
489            }
490        }
491        let mut dets = Vec::new();
492        for q in 0..nq {
493            if !keep[q] {
494                continue;
495            }
496            dets.push(Det {
497                score: scores[q],
498                mask: out.pred_masks[q * hw..(q + 1) * hw].to_vec(),
499            });
500        }
501        Ok(dets)
502    }
503
504    // ── tracker: one object, one frame ───────────────────────────
505
506    /// Memory-conditioned single-frame inference for an existing object;
507    /// returns the stored output plus the 1008² mask logits for the memory
508    /// encoder.
509    fn propagate(
510        &self,
511        net: &Net<'_>,
512        s: &Session,
513        id: i64,
514        frame_idx: usize,
515        feats: &TrkFeats,
516    ) -> Result<(ObjOut, Vec<f32>)> {
517        let max_ptr = if s.num_frames > 0 {
518            s.num_frames.min(self.cfg.max_object_pointers)
519        } else {
520            self.cfg.max_object_pointers
521        };
522        let st = s.objs.get(&id).ok_or_else(|| anyhow!("unknown object {id}"))?;
523        let cond = self.select_cond(st, frame_idx);
524        let memories = self.gather_memories(st, frame_idx, &cond)?;
525        let pointers = self.gather_pointers(st, frame_idx, &cond);
526        let pix = Net::pix_rows(feats);
527        // memory attention reads the neck's sine PE, the mask decoder the
528        // prompt encoder's Gaussian PE
529        let neck_pos = Net::neck_pos();
530        let pix = self.memory_cond(net, &pix, &neck_pos, &memories, &pointers, max_ptr)?;
531        let pos = net.image_pos()?;
532
533        let dense_p = net.no_mask_prompt()?;
534        let sparse = net.empty_sparse()?;
535        let dbg = std::env::var("SAM3_VID_DEBUG").is_ok();
536        if dbg {
537            let norm = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt();
538            let mn = pix.iter().cloned().fold(f32::INFINITY, f32::min);
539            let mx = pix.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
540            eprintln!(
541                "[vid] f{frame_idx} obj{id}: memories={} ptr={} pix n={:.3} ({}..{:.3})",
542                memories.len(),
543                pointers.len(),
544                norm(&pix),
545                mn,
546                mx
547            );
548        }
549        let (masks, iou, obj_score, tokens) = net.mask_decoder(
550            &sam3::rows_to_chw(&pix, EMB, EMB),
551            &pos,
552            &dense_p,
553            &sparse,
554            (&feats.l0, &feats.l1),
555            self.cfg.multimask,
556        )?;
557        let n_masks = masks.len() / (MASK_SIDE * MASK_SIDE);
558        let best = argmax(&iou).min(n_masks.saturating_sub(1));
559        if dbg {
560            let (mut cmn, mut cmx, mut cpos) = (f32::INFINITY, f32::NEG_INFINITY, 0usize);
561            for &v in masks.iter() {
562                cmn = cmn.min(v);
563                cmx = cmx.max(v);
564                cpos += usize::from(v > 0.0);
565            }
566            let base = best * MASK_SIDE * MASK_SIDE;
567            let spos = masks[base..base + MASK_SIDE * MASK_SIDE].iter().filter(|&&v| v > 0.0).count();
568            eprintln!(
569                "[vid] f{frame_idx} obj{id}: decoder iou={iou:?} obj_score={obj_score:.4} masks {cmn:.3}..{cmx:.3} pos={cpos}/{} chosen pos={spos}",
570                masks.len()
571            );
572        }
573        let low = masks[best * MASK_SIDE * MASK_SIDE..(best + 1) * MASK_SIDE * MASK_SIDE].to_vec();
574        let tok = &tokens[best * HID..(best + 1) * HID];
575        let appearing = obj_score > 0.0;
576        let low = if appearing {
577            low
578        } else {
579            if dbg {
580                eprintln!("[vid] f{frame_idx} obj{id}: NOT appearing (obj_score {obj_score}) -> mask forced to NO_OBJ");
581            }
582            vec![NO_OBJ_SCORE; MASK_SIDE * MASK_SIDE]
583        };
584        if dbg {
585            let pos = low.iter().filter(|&&v| v > 0.0).count();
586            eprintln!("[vid] f{frame_idx} obj{id}: propagate mask pos={pos}/{}", low.len());
587        }
588        let high = resize2d(&low, MASK_SIDE, MASK_SIDE, IMG, IMG);
589        let pointer = self.object_pointer(net, tok, appearing)?;
590        Ok((
591            ObjOut { pred_masks: low, obj_score, pointer, mem_feat: None, mem_pos: None },
592            high,
593        ))
594    }
595
596    /// `_use_mask_as_output`: the detection mask IS the output; the decoder
597    /// runs once to produce the object pointer.
598    fn use_mask_as_output(
599        &self,
600        net: &Net<'_>,
601        feats: &TrkFeats,
602        mask: &[f32],
603    ) -> Result<(ObjOut, Vec<f32>)> {
604        let appearing = mask.iter().any(|&v| v > 0.0);
605        let obj_score = if appearing { 10.0 } else { -10.0 };
606        // the 288² binary mask is first resampled to the tracker's 1008²
607        // input, then scaled to logits (*20-10) — `_use_mask_as_output`
608        let hi = resize2d(mask, MASK_SIDE, MASK_SIDE, IMG, IMG);
609        let high: Vec<f32> = hi
610            .iter()
611            .map(|v| v * self.cfg.sigmoid_scale + self.cfg.sigmoid_bias)
612            .collect();
613        let low = resize2d(&high, IMG, IMG, MASK_SIDE, MASK_SIDE);
614
615        // pointer path: 4× downsample of the 1008² mask → 288² prompt → decoder
616        let down = mask_downsample(net, &hi)?;
617        let down = resize2d(&down, IMG / 4, IMG / 4, MASK_SIDE, MASK_SIDE);
618        let dense_p = net.mask_prompt(&down)?;
619        let sparse = net.empty_sparse()?;
620        let pix = Net::pix_rows(feats);
621        let pos = net.image_pos()?;
622        let (_m, iou, dec_score, tokens) = net.mask_decoder(
623            &sam3::rows_to_chw(&pix, EMB, EMB),
624            &pos,
625            &dense_p,
626            &sparse,
627            (&feats.l0, &feats.l1),
628            self.cfg.multimask,
629        )?;
630        let n = tokens.len() / HID;
631        // `_single_frame_forward` uses the same best-IoU token for the
632        // object pointer as for the selected mask (not always the last one).
633        let best = argmax(&iou).min(n.saturating_sub(1));
634        let tok = &tokens[best * HID..(best + 1) * HID];
635        let mut pointer = self.object_pointer(net, tok, dec_score > 0.0)?;
636        if !appearing {
637            pointer = net.v("trk.no_obj_ptr")?;
638        }
639        Ok((
640            ObjOut { pred_masks: low, obj_score, pointer, mem_feat: None, mem_pos: None },
641            high,
642        ))
643    }
644
645    fn object_pointer(&self, net: &Net<'_>, tok: &[f32], appearing: bool) -> Result<Vec<f32>> {
646        let p = "trk.obj_ptr_proj.";
647        // FeedForward(256,256,256,3): proj_in, layers.0, proj_out
648        let mut h = dense(
649            &net.w(&format!("{p}layers.0.weight"))?,
650            &net.v(&format!("{p}layers.0.bias"))?,
651            tok,
652            1,
653            net.pool,
654        );
655        h = relu_vec(&h);
656        h = dense(
657            &net.w(&format!("{p}layers.1.weight"))?,
658            &net.v(&format!("{p}layers.1.bias"))?,
659            &h,
660            1,
661            net.pool,
662        );
663        h = relu_vec(&h);
664        let ptr = dense(
665            &net.w(&format!("{p}layers.2.weight"))?,
666            &net.v(&format!("{p}layers.2.bias"))?,
667            &h,
668            1,
669            net.pool,
670        );
671        if appearing {
672            Ok(ptr)
673        } else {
674            net.v("trk.no_obj_ptr")
675        }
676    }
677
678    // ── memory bank ──────────────────────────────────────────────
679
680    /// Conditioning frames closest to `frame_idx`
681    /// (`_select_closest_cond_frames`).
682    fn select_cond(&self, st: &ObjState, frame_idx: usize) -> Vec<usize> {
683        let max = self.cfg.max_cond_frame_num;
684        if max == 0 || st.cond.len() <= max {
685            return st.cond.keys().copied().collect();
686        }
687        let mut sel: Vec<usize> = Vec::new();
688        if let Some(before) = st.cond.keys().copied().filter(|&t| t < frame_idx).next_back() {
689            sel.push(before);
690        }
691        if let Some(after) = st.cond.keys().copied().filter(|&t| t >= frame_idx).next() {
692            sel.push(after);
693        }
694        let remain = max.saturating_sub(sel.len());
695        let mut rest: Vec<usize> = st.cond.keys().copied().filter(|t| !sel.contains(t)).collect();
696        rest.sort_by_key(|t| (*t as i64 - frame_idx as i64).abs());
697        sel.extend(rest.into_iter().take(remain));
698        sel
699    }
700
701    /// `(offset, mem_feat, mem_pos)` for every memory frame
702    /// (`_gather_memory_frame_outputs` + `_build_memory_attention_inputs`).
703    fn gather_memories(
704        &self,
705        st: &ObjState,
706        frame_idx: usize,
707        cond: &[usize],
708    ) -> Result<Vec<(usize, Vec<f32>, Vec<f32>)>> {
709        let mut out = Vec::new();
710        for &t in cond {
711            if let Some(o) = st.cond.get(&t) {
712                if let (Some(f), Some(p)) = (&o.mem_feat, &o.mem_pos) {
713                    out.push((0, f.clone(), p.clone()));
714                }
715            }
716        }
717        for off in (1..self.cfg.num_maskmem).rev() {
718            let prev = frame_idx as i64 - off as i64;
719            if prev < 0 {
720                continue;
721            }
722            if let Some(o) = st.non_cond.get(&(prev as usize)) {
723                if let (Some(f), Some(p)) = (&o.mem_feat, &o.mem_pos) {
724                    out.push((off, f.clone(), p.clone()));
725                }
726            }
727        }
728        Ok(out)
729    }
730
731    /// Object pointers of past frames (`_get_object_pointers`).
732    fn gather_pointers(&self, st: &ObjState, frame_idx: usize, cond: &[usize]) -> Vec<(usize, Vec<f32>)> {
733        let mut offs = Vec::new();
734        let mut toks = Vec::new();
735        for &t in cond {
736            if t > frame_idx {
737                continue;
738            }
739            if let Some(o) = st.cond.get(&t) {
740                offs.push(frame_idx - t);
741                toks.push(o.pointer.clone());
742            }
743        }
744        for d in 1..self.cfg.max_object_pointers.max(2) {
745            let prev = frame_idx as i64 - d as i64;
746            if prev < 0 {
747                break;
748            }
749            if let Some(o) = st.non_cond.get(&(prev as usize)) {
750                offs.push(d);
751                toks.push(o.pointer.clone());
752            }
753        }
754        offs.into_iter().zip(toks).collect()
755    }
756
757    /// `_prepare_memory_conditioned_features`: current features fused with
758    /// the memory bank through the 4-layer RoPE memory attention.
759    fn memory_cond(
760        &self,
761        net: &Net<'_>,
762        pix: &[f32],
763        pos: &[f32],
764        memories: &[(usize, Vec<f32>, Vec<f32>)],
765        pointers: &[(usize, Vec<f32>)],
766        max_ptr: usize,
767    ) -> Result<Vec<f32>> {
768        let n = EMB * EMB;
769        if memories.is_empty() {
770            // conditioning frame: features + no_memory_embedding (a zero buffer)
771            return Ok(pix.to_vec());
772        }
773        let tpos = net.v("trk.maskmem_tpos_enc")?; // (7, MEM)
774        let n_tpos = tpos.len() / MEM;
775        let mut mem: Vec<f32> = Vec::new();
776        let mut mem_pos: Vec<f32> = Vec::new();
777        for (off, f, p) in memories {
778            // cond frames (offset 0) index the LAST temporal row — the
779            // reference's `tpos[offset - 1]` with negative indexing.
780            let row = if *off == 0 { n_tpos - 1 } else { (*off - 1).min(n_tpos - 1) };
781            for (i, v) in p.iter().enumerate() {
782                mem_pos.push(v + tpos[row * MEM + i % MEM]);
783            }
784            mem.extend_from_slice(f);
785        }
786        if !pointers.is_empty() {
787            let max_diff = max_ptr.saturating_sub(1).max(1) as f32;
788            let offs: Vec<f32> = pointers.iter().map(|(o, _)| *o as f32 / max_diff).collect();
789            let pe = sine_1d(&offs, HID);
790            let tw = net.w("trk.obj_ptr_tpos_proj.weight")?;
791            let tb = net.v("trk.obj_ptr_tpos_proj.bias")?;
792            for (i, (_o, p)) in pointers.iter().enumerate() {
793                let proj = dense(&tw, &tb, &pe[i * HID..(i + 1) * HID], 1, net.pool);
794                let splits = HID / MEM;
795                for s in 0..splits {
796                    mem.extend_from_slice(&p[s * MEM..(s + 1) * MEM]);
797                    mem_pos.extend_from_slice(&proj);
798                }
799            }
800        }
801        // Object pointers are keys/values too: their four 64-wide slices are
802        // appended after the spatial memory and excluded only from RoPE.
803        let mem_len = mem.len() / MEM;
804
805        // RoPE over the 72×72 grid: dim 256, spatial half 128 → 64 freqs
806        let head_dim = HID;
807        let spatial = head_dim / 2;
808        let mut inv = vec![0f32; spatial / 2];
809        for i in 0..spatial / 2 {
810            inv[i] = 1.0 / 10000f32.powf((2 * i) as f32 / spatial as f32);
811        }
812        let mut cos = vec![0f32; n * head_dim];
813        let mut sin = vec![0f32; n * head_dim];
814        for y in 0..EMB {
815            for x in 0..EMB {
816                let base = (y * EMB + x) * head_dim;
817                // position_ids row 0 = w (x), row 1 = h (y)
818                for (k, src) in [x, y].iter().enumerate() {
819                    for (j, iv) in inv.iter().enumerate() {
820                        let ang = *src as f32 * *iv;
821                        let dst = base + k * spatial + j * 2; // repeat_interleave(2)
822                        cos[dst] = ang.cos();
823                        cos[dst + 1] = ang.cos();
824                        sin[dst] = ang.sin();
825                        sin[dst + 1] = ang.sin();
826                    }
827                }
828            }
829        }
830
831        let mut q = pix.to_vec();
832        for (a, b) in q.iter_mut().zip(pos) {
833            *a += 0.1 * *b;
834        }
835        let dump_dir = std::env::var("SAM3_VID_DUMP").ok();
836        let dump = |name: &str, v: &[f32]| {
837            if let Some(d) = &dump_dir {
838                let _ = std::fs::create_dir_all(d);
839                let _ = std::fs::write(format!("{d}/{name}.f32"), Self::f32_bytes(v));
840            }
841        };
842        dump("initial_seq", &q);
843        dump("cos", &cos);
844        dump("sin", &sin);
845        dump("memory", &mem);
846        dump("memory_pos", &mem_pos);
847        let tb = "trk.transformer.encoder.";
848        for layer in 0..4 {
849            let lb = format!("{tb}layers.{layer}.");
850            let mut h = q.clone();
851            net.ln_rows(&mut h, &format!("{lb}norm1.weight"), &format!("{lb}norm1.bias"), 1e-5)?;
852            dump(&format!("l{layer}_norm1"), &h);
853            let mut ao = vec![0f32; n * HID];
854            net.attn(&format!("{lb}self_attn."), &h, &h, &h, n, n, HID, HID, HID, Some((&cos, &sin, n)), &mut ao)?;
855            dump(&format!("l{layer}_self"), &ao);
856            for (a, c) in q.iter_mut().zip(&ao) {
857                *a += *c;
858            }
859            dump(&format!("l{layer}_res1"), &q);
860            let mut h = q.clone();
861            net.ln_rows(&mut h, &format!("{lb}norm2.weight"), &format!("{lb}norm2.bias"), 1e-5)?;
862            dump(&format!("l{layer}_norm2"), &h);
863            // keys = memory + memory position encoding (kv_in = MEM)
864            let k: Vec<f32> = mem
865                .chunks(MEM)
866                .zip(mem_pos.chunks(MEM))
867                .flat_map(|(a, b)| a.iter().zip(b).map(|(x, y)| x + y))
868                .collect();
869            if layer == 0 {
870                dump("cross_key", &k);
871            }
872            let mut ao = vec![0f32; n * HID];
873            net.attn(
874                &format!("{lb}cross_attn_image."),
875                &h,
876                &k,
877                &mem,
878                n,
879                mem_len,
880                HID,
881                MEM,
882                HID,
883                Some((&cos, &sin, mem_len - pointers.len() * (HID / MEM))),
884                &mut ao,
885            )?;
886            dump(&format!("l{layer}_cross"), &ao);
887            for (a, c) in q.iter_mut().zip(&ao) {
888                *a += *c;
889            }
890            dump(&format!("l{layer}_res2"), &q);
891            let mut h = q.clone();
892            net.ln_rows(&mut h, &format!("{lb}norm3.weight"), &format!("{lb}norm3.bias"), 1e-5)?;
893            let a = net.lin(&format!("{lb}"), "linear1", &h, n)?;
894            let a = relu_vec(&a);
895            let a = net.lin(&format!("{lb}"), "linear2", &a, n)?;
896            for (x, c) in q.iter_mut().zip(&a) {
897                *x += *c;
898            }
899            dump(&format!("l{layer}_out"), &q);
900        }
901        net.ln_rows(&mut q, &format!("{tb}norm.weight"), &format!("{tb}norm.bias"), 1e-5)?;
902        dump("final", &q);
903        Ok(q)
904    }
905
906    /// Memory encoder: mask → stride-16 downsampler, fused with the pixel
907    /// features, ConvNeXt fuser, projection to `mem_dim` + sine PE.
908    #[allow(dead_code)]
909    fn f32_bytes(v: &[f32]) -> Vec<u8> {
910        let mut b = Vec::with_capacity(v.len() * 4);
911        for &x in v {
912            b.extend_from_slice(&x.to_le_bytes());
913        }
914        b
915    }
916
917    fn encode_memory(
918        &self,
919        net: &Net<'_>,
920        pix: &[f32],        // (256,72,72) CHW
921        mask_logits: &[f32], // HxW logits
922        h: usize,
923        w: usize,
924        obj_score: f32,
925        from_pts: bool,
926    ) -> Result<(Vec<f32>, Vec<f32>)> {
927        let mut m = resize2d(mask_logits, h, w, MEM_SIDE, MEM_SIDE);
928        if from_pts {
929            for v in m.iter_mut() {
930                *v = if *v > 0.0 { 1.0 } else { 0.0 };
931            }
932        } else {
933            for v in m.iter_mut() {
934                *v = sigm(*v);
935            }
936        }
937        for v in m.iter_mut() {
938            *v = *v * self.cfg.sigmoid_scale + self.cfg.sigmoid_bias;
939        }
940        let dbg = std::env::var("SAM3_VID_DEBUG").is_ok();
941        if dbg {
942            let mn = m.iter().cloned().fold(f32::INFINITY, f32::min);
943            let mx = m.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
944            let pos = m.iter().filter(|&&v| v > 0.0).count();
945            eprintln!("[mem] mask input ({}): {mn:.3}..{mx:.3} pos={}/{} scale={} bias={}", m.len(), pos, m.len(), self.cfg.sigmoid_scale, self.cfg.sigmoid_bias);
946        }
947        // mask downsampler: 4 × (conv3 s2 + LN + GELU), then 1×1
948        let p = "trk.maskmem_backbone.mask_downsampler.encoder.";
949        let mut x = m;
950        let mut side = MEM_SIDE;
951        let mut c_in = 1usize;
952        for step in [0usize, 3, 6, 9] {
953            let wgt = net.v(&format!("{p}{step}.weight"))?;
954            let b = net.v(&format!("{p}{step}.bias"))?;
955            x = sam3::conv2d(&wgt, &b, &x, c_in, side, side, 3, 3, 2, 1, net.pool);
956            side /= 2;
957            let ch = b.len();
958            let lw = net.v(&format!("{p}{}.weight", step + 1))?;
959            let lb = net.v(&format!("{p}{}.bias", step + 1))?;
960            ln_chw(&mut x, ch, side * side, &lw, &lb, 1e-6);
961            x.iter_mut().for_each(|v| *v = gelu_erf(*v));
962            c_in = ch;
963            if dbg {
964                let mn = x.iter().cloned().fold(f32::INFINITY, f32::min);
965                let mx = x.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
966                eprintln!("[mem] after layer {step}: side={side} ch={ch} {mn:.3}..{mx:.3}");
967            }
968        }
969        let w12 = net.v(&format!("{p}12.weight"))?;
970        let b12 = net.v(&format!("{p}12.bias"))?;
971        x = sam3::conv2d(&w12, &b12, &x, c_in, side, side, 1, 1, 1, 0, net.pool);
972        anyhow::ensure!(x.len() == HID * EMB * EMB, "mask downsampler output {}", x.len());
973        if dbg {
974            let rms = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt() / (v.len() as f32).sqrt();
975            eprintln!("[mem] mask branch rms={:.4} | pix rms={:.4}", rms(&x), rms(pix));
976        }
977
978        // pixel projection + mask
979        let pw = net.v("trk.maskmem_backbone.pix_feat_proj.weight")?;
980        let pb = net.v("trk.maskmem_backbone.pix_feat_proj.bias")?;
981        let mut f = sam3::conv2d(&pw, &pb, pix, HID, EMB, EMB, 1, 1, 1, 0, net.pool);
982        if dbg {
983            let rms = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt() / (v.len() as f32).sqrt();
984            eprintln!("[mem] pix_proj rms={:.4}", rms(&f));
985        }
986        for (a, b) in f.iter_mut().zip(&x) {
987            *a += *b;
988        }
989        if dbg {
990            let rms = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt() / (v.len() as f32).sqrt();
991            eprintln!("[mem] fused pre-fuser rms={:.4}", rms(&f));
992        }
993        let dump_dir = std::env::var("SAM3_VID_DUMP").ok();
994        if let Some(d) = &dump_dir {
995            let _ = std::fs::create_dir_all(d);
996            let _ = std::fs::write(format!("{d}/mask_branch.f32"), Self::f32_bytes(&x));
997            let _ = std::fs::write(format!("{d}/pix_proj.f32"), Self::f32_bytes(&f));
998        }
999        // ConvNeXt fuser ×2 — `out = block_in + gamma * pwconv2(gelu(pwconv1(LN(dwconv(block_in)))))`
1000        for layer in 0..2 {
1001            let lp = format!("trk.maskmem_backbone.fuser.layers.{layer}.");
1002            let block_in = f.clone(); // the residual branch springs from the BLOCK INPUT
1003            let dw = net.v(&format!("{lp}dwconv.weight"))?;
1004            let db = net.v(&format!("{lp}dwconv.bias"))?;
1005            f = depthwise(&dw, &db, &f, HID, EMB, EMB, 7, net.pool);
1006            let nw = net.v(&format!("{lp}norm.weight"))?;
1007            let nb = net.v(&format!("{lp}norm.bias"))?;
1008            ln_chw(&mut f, HID, EMB * EMB, &nw, &nb, 1e-6);
1009            let ln_out = if dump_dir.is_some() { Some(f.clone()) } else { None };
1010            // ConvNeXt pointwise convs run over the NHWC layout: rows =
1011            // spatial positions, columns = channels
1012            let rows = EMB * EMB;
1013            let dw_out = if dump_dir.is_some() { Some(f.clone()) } else { None };
1014            let w1 = net.w(&format!("{lp}pwconv1.weight"))?;
1015            let b1 = net.v(&format!("{lp}pwconv1.bias"))?;
1016            let h_rows = sam3::chw_to_rows(&f, EMB, EMB);
1017            let mut hh = dense(&w1, &b1, &h_rows, rows, net.pool); // (HW, 1024)
1018            hh.iter_mut().for_each(|v| *v = gelu_erf(*v));
1019            let pw1_out = if dump_dir.is_some() { Some(hh.clone()) } else { None };
1020            let w2 = net.w(&format!("{lp}pwconv2.weight"))?;
1021            let b2 = net.v(&format!("{lp}pwconv2.bias"))?;
1022            let hh = dense(&w2, &b2, &hh, rows, net.pool); // (HW, 256)
1023            let g = net.v(&format!("{lp}gamma"))?;
1024            if let Some(d) = &dump_dir {
1025                let _ = std::fs::write(format!("{d}/fuser_gamma{layer}.f32"), Self::f32_bytes(&g));
1026            }
1027            for c in 0..HID {
1028                for i in 0..rows {
1029                    f[c * rows + i] = block_in[c * rows + i] + g[c] * hh[i * HID + c];
1030                }
1031            }
1032            if let Some(d) = &dump_dir {
1033                let _ = std::fs::write(format!("{d}/fuser_layer{layer}.f32"), Self::f32_bytes(&f));
1034                if layer == 0 {
1035                    if let Some(v) = &dw_out {
1036                        let _ = std::fs::write(format!("{d}/f0_dwconv.f32"), Self::f32_bytes(v));
1037                    }
1038                    if let Some(v) = &ln_out {
1039                        let _ = std::fs::write(format!("{d}/f0_norm.f32"), Self::f32_bytes(v));
1040                    }
1041                    if let Some(v) = &pw1_out {
1042                        let _ = std::fs::write(format!("{d}/f0_pw1.f32"), Self::f32_bytes(v));
1043                    }
1044                    let _ = std::fs::write(format!("{d}/f0_pw2.f32"), Self::f32_bytes(&hh));
1045                }
1046            }
1047        }
1048        if std::env::var("SAM3_VID_DEBUG").is_ok() {
1049            let rms = |v: &[f32]| v.iter().map(|x| x * x).sum::<f32>().sqrt() / (v.len() as f32).sqrt();
1050            eprintln!("[mem] post-fuser rms={:.4}", rms(&f));
1051        }
1052        let ow = net.v("trk.maskmem_backbone.out_proj.weight")?;
1053        let ob = net.v("trk.maskmem_backbone.out_proj.bias")?;
1054        let mut f = sam3::conv2d(&ow, &ob, &f, HID, EMB, EMB, 1, 1, 1, 0, net.pool);
1055        anyhow::ensure!(f.len() == MEM * EMB * EMB, "memory projection {}", f.len());
1056        if self.cfg.occlusion_spatial && obj_score <= 0.0 {
1057            let e = net.v("trk.no_obj_embed_spatial")?;
1058            let hw = EMB * EMB;
1059            for i in 0..hw {
1060                for (c, ev) in e.iter().enumerate() {
1061                    f[c * hw + i] += ev;
1062                }
1063            }
1064        }
1065        let pos = sine(EMB, EMB, MEM / 2);
1066        let rows = sam3::chw_to_rows(&f, EMB, EMB);
1067        Ok((rows, pos))
1068    }
1069
1070    // ── storage helpers ──────────────────────────────────────────
1071
1072    fn store(&self, s: &mut Session, id: i64, frame_idx: usize, cond: bool, o: ObjOut) {
1073        let st = s.objs.entry(id).or_default();
1074        if cond {
1075            st.cond.insert(frame_idx, o);
1076        } else {
1077            st.non_cond.insert(frame_idx, o);
1078        }
1079    }
1080
1081    /// Encode memories for the outputs just written
1082    /// (`_batch_encode_memories`).
1083    fn encode_into(
1084        &self,
1085        net: &Net<'_>,
1086        s: &mut Session,
1087        frame_idx: usize,
1088        pending: Vec<(i64, ObjOut, Vec<f32>)>,
1089        pix: &[f32],
1090        h: usize,
1091        w: usize,
1092        from_pts: bool,
1093    ) -> Result<()> {
1094        if pending.is_empty() {
1095            return Ok(());
1096        }
1097        for (id, o, high) in pending {
1098            let (mf, mp) = self.encode_memory(net, pix, &high, h, w, o.obj_score, from_pts)?;
1099            let st = s.objs.entry(id).or_default();
1100            let slot = if st.cond.contains_key(&frame_idx) {
1101                st.cond.get_mut(&frame_idx)
1102            } else {
1103                st.non_cond.get_mut(&frame_idx)
1104            };
1105            if let Some(slot) = slot {
1106                slot.mem_feat = Some(mf);
1107                slot.mem_pos = Some(mp);
1108            }
1109        }
1110        Ok(())
1111    }
1112}
1113
1114// ───────────────────── planning ─────────────────────
1115
1116struct Plan {
1117    new_inds: Vec<usize>,
1118    new_ids: Vec<i64>,
1119    recond_ids: HashSet<i64>,
1120    removed: HashSet<i64>,
1121    suppressed: HashSet<i64>,
1122}
1123
1124impl Sam3Video {
1125    #[allow(clippy::too_many_arguments)]
1126    fn plan(
1127        &self,
1128        net: &Net<'_>,
1129        s: &mut Session,
1130        frame_idx: usize,
1131        dets: &[Det],
1132        trk_masks: &[Vec<f32>],
1133        feats: &TrkFeats,
1134    ) -> Result<Plan> {
1135        let cfg = &self.cfg;
1136        let pre_ids: Vec<i64> = s.obj_ids.clone();
1137        let det_masks: Vec<&[f32]> = dets.iter().map(|d| d.mask.as_slice()).collect();
1138        let det_scores: Vec<f32> = dets.iter().map(|d| d.score).collect();
1139        let assoc = associate(&det_masks, &det_scores, trk_masks, &pre_ids, cfg);
1140        if std::env::var("SAM3_VID_DEBUG").is_ok() {
1141            let trk_pos: Vec<usize> = trk_masks.iter().map(|m| m.iter().filter(|&&v| v > 0.0).count()).collect();
1142            let det_pos: Vec<usize> = det_masks.iter().map(|m| m.iter().filter(|&&v| v > 0.0).count()).collect();
1143            eprintln!(
1144                "[vid] f{frame_idx} plan: dets={} (pos {:?}, scores {:?}) trk={} (pos {:?}) new={:?} unmatched={:?} empty={:?}",
1145                det_masks.len(), det_pos, det_scores, trk_masks.len(), trk_pos,
1146                assoc.new_inds, assoc.unmatched, assoc.empty
1147            );
1148        }
1149
1150        // object-id budget
1151        let mut new_inds = assoc.new_inds;
1152        if pre_ids.len() + new_inds.len() > cfg.max_num_objects {
1153            let room = cfg.max_num_objects.saturating_sub(pre_ids.len());
1154            let mut by_score = new_inds.clone();
1155            by_score.sort_by(|a, b| {
1156                det_scores[*b].partial_cmp(&det_scores[*a]).unwrap_or(std::cmp::Ordering::Equal)
1157            });
1158            new_inds = by_score.into_iter().take(room).collect();
1159            new_inds.sort_unstable();
1160        }
1161        let start = s.max_obj_id + 1;
1162        let new_ids: Vec<i64> = (0..new_inds.len() as i64).map(|k| start + k).collect();
1163
1164        // ── hotstart (`_process_hotstart`) ─────────────────────────
1165        let mut removed: HashSet<i64> = HashSet::new();
1166        let mut suppressed: HashSet<i64> = HashSet::new();
1167        for (k, &di) in new_inds.iter().enumerate() {
1168            let id = new_ids[k];
1169            let _ = di;
1170            s.first_frame.entry(id).or_insert(frame_idx as i64);
1171            s.keep_alive.insert(id, cfg.init_trk_keep_alive);
1172        }
1173        let matched: HashSet<i64> = assoc.det_to_matched.values().flatten().copied().collect();
1174        for id in &matched {
1175            let ka = s.keep_alive.get(id).copied().unwrap_or(cfg.init_trk_keep_alive);
1176            s.keep_alive.insert(*id, cfg.max_trk_keep_alive.max(ka + 1));
1177        }
1178        for id in &assoc.unmatched {
1179            s.unmatched.entry(*id).or_default().push(frame_idx);
1180            let ka = s.keep_alive.get(id).copied().unwrap_or(cfg.init_trk_keep_alive);
1181            s.keep_alive.insert(*id, cfg.min_trk_keep_alive.max(ka - 1));
1182        }
1183        if cfg.decrease_keep_alive_for_empty {
1184            for id in &assoc.empty {
1185                let ka = s.keep_alive.get(id).copied().unwrap_or(cfg.init_trk_keep_alive);
1186                s.keep_alive.insert(*id, cfg.min_trk_keep_alive.max(ka - 1));
1187            }
1188        }
1189        let hotstart_diff = frame_idx as i64 - cfg.hotstart_delay;
1190        let already_removed: HashSet<i64> = s.removed.clone();
1191        for (id, frames) in s.unmatched.clone() {
1192            if already_removed.contains(&id) || removed.contains(&id) {
1193                continue;
1194            }
1195            if frames.len() >= cfg.hotstart_unmatch_thresh {
1196                if s.first_frame.get(&id).copied().unwrap_or(0) > hotstart_diff {
1197                    removed.insert(id);
1198                }
1199            }
1200            let ka = s.keep_alive.get(&id).copied().unwrap_or(0);
1201            if ka <= 0
1202                && !cfg.suppress_unmatched_only_within_hotstart
1203                && !already_removed.contains(&id)
1204                && !removed.contains(&id)
1205            {
1206                suppressed.insert(id);
1207            }
1208        }
1209        for ids in assoc.det_to_matched.values() {
1210            if ids.len() < 2 {
1211                continue;
1212            }
1213            let first = *ids
1214                .iter()
1215                .min_by_key(|i| s.first_frame.get(i).copied().unwrap_or(i64::MAX))
1216                .unwrap();
1217            for id in ids {
1218                if *id != first {
1219                    s.overlap.entry((first, *id)).or_default().push(frame_idx);
1220                }
1221            }
1222        }
1223        let mut pairs: Vec<((i64, i64), Vec<usize>)> = s.overlap.clone().into_iter().collect();
1224        pairs.sort();
1225        for ((_, id), frames) in pairs {
1226            if s.removed.contains(&id) || removed.contains(&id) {
1227                continue;
1228            }
1229            if s.first_frame.get(&id).copied().unwrap_or(0) > hotstart_diff
1230                && frames.len() >= cfg.hotstart_dup_thresh
1231            {
1232                removed.insert(id);
1233            }
1234        }
1235        s.removed.extend(removed.iter().copied());
1236
1237        // ── periodic reconditioning (`_prepare_recondition_masks`) ──
1238        let mut recond_ids: HashSet<i64> = HashSet::new();
1239        let mut recond_masks: Vec<(usize, Vec<f32>)> = Vec::new();
1240        if cfg.recondition_every_nth_frame > 0
1241            && frame_idx as i64 % cfg.recondition_every_nth_frame == 0
1242            && !assoc.trk_to_high_conf.is_empty()
1243        {
1244            for (&id, &di) in &assoc.trk_to_high_conf {
1245                let Some(i) = pre_ids.iter().position(|x| *x == id) else { continue };
1246                let trk_score = s
1247                    .trk_score_fw
1248                    .get(&id)
1249                    .and_then(|m| m.get(&frame_idx))
1250                    .copied()
1251                    .unwrap_or(0.0);
1252                if trk_score <= cfg.high_conf_thresh {
1253                    continue;
1254                }
1255                recond_ids.insert(id);
1256                if cfg.recondition_on_trk_masks {
1257                    recond_masks.push((i, trk_masks[i].clone()));
1258                } else {
1259                    recond_masks.push((
1260                        i,
1261                        dets[di].mask.iter().map(|&v| if v > 0.0 { 1.0 } else { 0.0 }).collect(),
1262                    ));
1263                }
1264            }
1265        }
1266
1267        // ── recent-occlusion suppression ────────────────────────────
1268        let mut trk = trk_masks.to_vec();
1269        if cfg.suppress_recent_occlusion_thresh > 0.0 && !trk.is_empty() {
1270            let bins: Vec<Vec<u8>> = trk
1271                .iter()
1272                .map(|m| m.iter().map(|&v| u8::from(v > 0.0)).collect())
1273                .collect();
1274            let n = bins.len();
1275            let mut last: Vec<i64> = pre_ids
1276                .iter()
1277                .map(|id| {
1278                    s.last_occluded.get(id).copied().unwrap_or(if s.removed.contains(id) {
1279                        100_000
1280                    } else {
1281                        -1
1282                    })
1283                })
1284                .collect();
1285            let mut to_suppress = vec![false; n];
1286            if n > 1 {
1287                for i in 0..n {
1288                    for j in (i + 1)..n {
1289                        if mask_iou(&bins[i], &bins[j]) < cfg.suppress_recent_occlusion_thresh {
1290                            continue;
1291                        }
1292                        if last[i] > last[j] && last[j] > -1 {
1293                            to_suppress[i] = true;
1294                        }
1295                        if last[j] > last[i] && last[i] > -1 {
1296                            to_suppress[j] = true;
1297                        }
1298                    }
1299                }
1300            }
1301            for (i, m) in trk.iter_mut().enumerate() {
1302                if bins[i].iter().all(|&v| v == 0) || to_suppress[i] {
1303                    for v in m.iter_mut() {
1304                        *v = -10.0;
1305                    }
1306                    last[i] = frame_idx as i64;
1307                }
1308            }
1309            for (i, id) in pre_ids.iter().enumerate() {
1310                s.last_occluded.insert(*id, last[i]);
1311            }
1312        }
1313
1314        // ── `_tracker_update_memories`: plan → constraints → encode ─
1315        let mut per_obj = trk;
1316        for (i, m) in &recond_masks {
1317            per_obj[*i] = m.clone();
1318        }
1319        for (i, &id) in pre_ids.iter().enumerate() {
1320            if recond_ids.contains(&id) {
1321                if let Some(st) = s.objs.get_mut(&id) {
1322                    if let Some(o) = st.non_cond.remove(&frame_idx) {
1323                        st.cond.insert(frame_idx, o);
1324                    }
1325                }
1326            }
1327        }
1328        let per_obj = suppress_area_shrink(&per_obj);
1329        if !per_obj.is_empty() {
1330            let mut pending = Vec::new();
1331            for (i, &id) in pre_ids.iter().enumerate() {
1332                let area = per_obj[i].iter().any(|&v| v > 0.0);
1333                let score = if area { 10.0 } else { -10.0 };
1334                if let Some(st) = s.objs.get_mut(&id) {
1335                    let slot = if st.cond.contains_key(&frame_idx) {
1336                        st.cond.get_mut(&frame_idx)
1337                    } else {
1338                        st.non_cond.get_mut(&frame_idx)
1339                    };
1340                    if let Some(slot) = slot {
1341                        slot.pred_masks = per_obj[i].clone();
1342                        slot.obj_score = score;
1343                    }
1344                }
1345                pending.push((
1346                    id,
1347                    ObjOut {
1348                        pred_masks: per_obj[i].clone(),
1349                        obj_score: score,
1350                        pointer: Vec::new(),
1351                        mem_feat: None,
1352                        mem_pos: None,
1353                    },
1354                    per_obj[i].clone(),
1355                ));
1356            }
1357            self.encode_into(net, s, frame_idx, pending, &feats.l2, MASK_SIDE, MASK_SIDE, false)?;
1358        }
1359
1360        // ── metadata updates ───────────────────────────────────────
1361        for (k, &id) in new_ids.iter().enumerate() {
1362            s.score.insert(id, det_scores[new_inds[k]]);
1363            s.trk_score_fw.entry(id).or_default().insert(frame_idx, det_scores[new_inds[k]]);
1364        }
1365        for id in &removed {
1366            s.score.insert(*id, -1e4);
1367            s.trk_score_fw.entry(*id).or_default().insert(frame_idx, -1e4);
1368        }
1369        s.max_obj_id = (start + new_ids.len() as i64 - 1).max(s.max_obj_id);
1370
1371        Ok(Plan { new_inds, new_ids, recond_ids, removed, suppressed })
1372    }
1373}
1374
1375// ───────────────────── association ─────────────────────
1376
1377struct Assoc {
1378    new_inds: Vec<usize>,
1379    unmatched: Vec<i64>,
1380    empty: Vec<i64>,
1381    det_to_matched: HashMap<usize, Vec<i64>>,
1382    trk_to_high_conf: HashMap<i64, usize>,
1383}
1384
1385/// `_associate_det_trk`: detections against the propagated masklets.
1386fn associate(
1387    det_masks: &[&[f32]],
1388    det_scores: &[f32],
1389    trk_masks: &[Vec<f32>],
1390    trk_ids: &[i64],
1391    cfg: &VidCfg,
1392) -> Assoc {
1393    let blank = Assoc {
1394        new_inds: Vec::new(),
1395        unmatched: Vec::new(),
1396        empty: Vec::new(),
1397        det_to_matched: HashMap::new(),
1398        trk_to_high_conf: HashMap::new(),
1399    };
1400    if trk_masks.is_empty() {
1401        // no tracks yet: every detection above threshold starts an object
1402        return Assoc { new_inds: (0..det_masks.len()).collect(), ..blank };
1403    }
1404    if det_masks.is_empty() {
1405        let mut unmatched = Vec::new();
1406        let mut empty = Vec::new();
1407        for (i, m) in trk_masks.iter().enumerate() {
1408            if m.iter().any(|&v| v > 0.0) {
1409                unmatched.push(trk_ids[i]);
1410            } else {
1411                empty.push(trk_ids[i]);
1412            }
1413        }
1414        return Assoc { unmatched, empty, ..blank };
1415    }
1416    let dbin: Vec<Vec<u8>> = det_masks
1417        .iter()
1418        .map(|m| m.iter().map(|&v| u8::from(v > 0.0)).collect())
1419        .collect();
1420    let tbin: Vec<Vec<u8>> = trk_masks
1421        .iter()
1422        .map(|m| m.iter().map(|&v| u8::from(v > 0.0)).collect())
1423        .collect();
1424    let mut ious = vec![0f32; det_masks.len() * trk_masks.len()];
1425    for (i, d) in dbin.iter().enumerate() {
1426        for (j, t) in tbin.iter().enumerate() {
1427            ious[i * trk_masks.len() + j] = mask_iou(d, t);
1428        }
1429    }
1430    let at = |i: usize, j: usize| ious[i * trk_masks.len() + j];
1431
1432    let mut unmatched = Vec::new();
1433    let mut empty = Vec::new();
1434    for (j, t) in tbin.iter().enumerate() {
1435        let nonempty = t.iter().any(|&v| v != 0);
1436        let matched = (0..det_masks.len()).any(|i| at(i, j) >= cfg.trk_assoc_iou_thresh);
1437        if !nonempty {
1438            empty.push(trk_ids[j]);
1439        } else if !matched {
1440            unmatched.push(trk_ids[j]);
1441        }
1442    }
1443    let mut new_inds = Vec::new();
1444    let mut det_to_matched = HashMap::new();
1445    let mut trk_to_high_conf = HashMap::new();
1446    for i in 0..det_masks.len() {
1447        let matches_any = (0..trk_masks.len()).any(|j| at(i, j) >= cfg.assoc_iou_thresh);
1448        let is_new = det_scores[i] >= cfg.new_det_thresh && !matches_any;
1449        if is_new {
1450            new_inds.push(i);
1451        }
1452        let ms: Vec<i64> = (0..trk_masks.len())
1453            .filter(|&j| at(i, j) >= cfg.assoc_iou_thresh)
1454            .map(|j| trk_ids[j])
1455            .collect();
1456        let mut best = (0usize, 0f32);
1457        for j in 0..trk_masks.len() {
1458            if at(i, j) > best.1 {
1459                best = (j, at(i, j));
1460            }
1461        }
1462        if det_scores[i] >= cfg.high_conf_thresh && !is_new && best.1 >= cfg.high_iou_thresh {
1463            trk_to_high_conf.insert(trk_ids[best.0], i);
1464        }
1465        det_to_matched.insert(i, ms);
1466    }
1467    Assoc { new_inds, unmatched, empty, det_to_matched, trk_to_high_conf }
1468}
1469
1470// ───────────────────── outputs ─────────────────────
1471
1472impl Sam3Video {
1473    #[allow(clippy::too_many_arguments)]
1474    fn build_outputs(
1475        &self,
1476        s: &Session,
1477        frame_idx: usize,
1478        pre_ids: &[i64],
1479        dets: &[Det],
1480        plan: &Plan,
1481        frame_size: (usize, usize),
1482    ) -> Result<Vec<TrackObs>> {
1483        let (fw, fh) = frame_size;
1484        let mut out: Vec<TrackObs> = Vec::new();
1485        // tracker masks of the objects that existed before this frame
1486        for &id in pre_ids {
1487            if plan.removed.contains(&id) {
1488                continue;
1489            }
1490            let Some(st) = s.objs.get(&id) else { continue };
1491            let Some(o) = st.cond.get(&frame_idx).or_else(|| st.non_cond.get(&frame_idx)) else {
1492                if std::env::var("SAM3_VID_DEBUG").is_ok() {
1493                    eprintln!("[vid] f{frame_idx} out: obj{id} has NO output at this frame");
1494                }
1495                continue;
1496            };
1497            if o.pred_masks.len() != MASK_SIDE * MASK_SIDE {
1498                if std::env::var("SAM3_VID_DEBUG").is_ok() {
1499                    eprintln!("[vid] f{frame_idx} out: obj{id} mask len {} != {}", o.pred_masks.len(), MASK_SIDE * MASK_SIDE);
1500                }
1501                continue;
1502            }
1503            if o.pred_masks.iter().all(|&v| v <= 0.0) {
1504                if std::env::var("SAM3_VID_DEBUG").is_ok() {
1505                    eprintln!("[vid] f{frame_idx} out: obj{id} mask is ALL-NEGATIVE (min {:.3}) -> no observation",
1506                        o.pred_masks.iter().cloned().fold(f32::INFINITY, f32::min));
1507                }
1508                continue;
1509            }
1510            let score = s.score.get(&id).copied().unwrap_or(0.0);
1511            push_obs(&mut out, id, score, &o.pred_masks, fw, fh, false, true);
1512        }
1513        // masks of objects created on this frame come from the detector
1514        for (k, &di) in plan.new_inds.iter().enumerate() {
1515            let id = plan.new_ids[k];
1516            if plan.removed.contains(&id) {
1517                continue;
1518            }
1519            let mut m = dets[di].mask.clone();
1520            fill_holes_one(&mut m, self.cfg.fill_hole_area);
1521            let score = s.score.get(&id).copied().unwrap_or(dets[di].score);
1522            push_obs(&mut out, id, score, &m, fw, fh, true, false);
1523        }
1524        // hidden objects never reach the outputs
1525        let hidden: HashSet<i64> = s.removed.union(&s.suppressed).copied().collect();
1526        out.retain(|o| !hidden.contains(&o.id));
1527        if out.len() > 1 {
1528            apply_non_overlap(&mut out);
1529        }
1530        out.sort_by_key(|o| o.id);
1531        Ok(out)
1532    }
1533}
1534
1535fn push_obs(
1536    out: &mut Vec<TrackObs>,
1537    id: i64,
1538    score: f32,
1539    logits: &[f32],
1540    fw: usize,
1541    fh: usize,
1542    from_detection: bool,
1543    from_tracker: bool,
1544) {
1545    if logits.len() != MASK_SIDE * MASK_SIDE {
1546        eprintln!("[vid] push_obs: id {id} logits len {} != {MASK_SIDE}", logits.len());
1547        return;
1548    }
1549    let flat = resize2d(logits, MASK_SIDE, MASK_SIDE, fh, fw);
1550    let mask: Vec<u8> = flat.iter().map(|&v| u8::from(v > 0.0)).collect();
1551    if mask.iter().all(|&v| v == 0) {
1552        eprintln!("[vid] push_obs: id {id} empty after resize to {fw}x{fh}");
1553        return;
1554    }
1555    let mut x0 = usize::MAX;
1556    let mut y0 = usize::MAX;
1557    let mut x1 = 0usize;
1558    let mut y1 = 0usize;
1559    for y in 0..fh {
1560        for x in 0..fw {
1561            if mask[y * fw + x] != 0 {
1562                x0 = x0.min(x);
1563                y0 = y0.min(y);
1564                x1 = x1.max(x);
1565                y1 = y1.max(y);
1566            }
1567        }
1568    }
1569    out.push(TrackObs {
1570        id,
1571        score,
1572        box_px: [x0 as f32, y0 as f32, x1 as f32, y1 as f32],
1573        mask,
1574        from_detection,
1575        from_tracker,
1576    });
1577}
1578
1579/// Pixel-wise "highest score wins" between objects of one prompt group
1580/// (`Sam3VideoProcessor._apply_object_wise_non_overlapping_constraints`).
1581fn apply_non_overlap(out: &mut [TrackObs]) {
1582    let len = match out.first() {
1583        Some(o) => o.mask.len(),
1584        None => return,
1585    };
1586    for i in 0..len {
1587        let mut best = usize::MAX;
1588        let mut best_score = f32::NEG_INFINITY;
1589        for (k, o) in out.iter().enumerate() {
1590            if o.mask[i] == 0 {
1591                continue;
1592            }
1593            if o.score > best_score {
1594                best_score = o.score;
1595                best = k;
1596            }
1597        }
1598        if best == usize::MAX {
1599            continue;
1600        }
1601        for (k, o) in out.iter_mut().enumerate() {
1602            if k != best && o.mask[i] != 0 {
1603                o.mask[i] = 0;
1604            }
1605        }
1606    }
1607}
1608
1609// ───────────────────── mask helpers ─────────────────────
1610
1611/// IoU of two binary masks (empty ∪ empty = 1, as in the reference).
1612pub fn mask_iou(a: &[u8], b: &[u8]) -> f32 {
1613    debug_assert_eq!(a.len(), b.len());
1614    let mut inter = 0u32;
1615    let mut union = 0u32;
1616    for (&x, &y) in a.iter().zip(b) {
1617        let x = x != 0;
1618        let y = y != 0;
1619        inter += u32::from(x && y);
1620        union += u32::from(x || y);
1621    }
1622    if union == 0 {
1623        1.0
1624    } else {
1625        inter as f32 / union as f32
1626    }
1627}
1628
1629/// `fill_holes_in_mask_scores`: small background components become 0.1,
1630/// small foreground components (≤ max_area and ≤ half the mask) become -0.1.
1631fn fill_holes(masks: &mut [Vec<f32>], max_area: usize) {
1632    for m in masks.iter_mut() {
1633        fill_holes_one(m, max_area);
1634    }
1635}
1636
1637fn fill_holes_one(mask: &mut [f32], max_area: usize) {
1638    if max_area == 0 || mask.len() != MASK_SIDE * MASK_SIDE {
1639        return;
1640    }
1641    let (w, h) = (MASK_SIDE, MASK_SIDE);
1642    let bg: Vec<bool> = mask.iter().map(|&v| v <= 0.0).collect();
1643    let areas = cc_areas(&bg, w, h);
1644    for i in 0..mask.len() {
1645        if bg[i] && areas[i] > 0 && areas[i] <= max_area {
1646            mask[i] = 0.1;
1647        }
1648    }
1649    let fg: Vec<bool> = mask.iter().map(|&v| v > 0.0).collect();
1650    let total: usize = fg.iter().filter(|&&v| v).count();
1651    let thr = (total / 2).min(max_area);
1652    let areas = cc_areas(&fg, w, h);
1653    for i in 0..mask.len() {
1654        if fg[i] && areas[i] > 0 && areas[i] <= thr {
1655            mask[i] = -0.1;
1656        }
1657    }
1658}
1659
1660/// 4-connected component sizes (every pixel carries its component's area).
1661fn cc_areas(mask: &[bool], w: usize, h: usize) -> Vec<usize> {
1662    let mut label = vec![usize::MAX; mask.len()];
1663    let mut sizes: Vec<usize> = Vec::new();
1664    let mut stack: Vec<usize> = Vec::new();
1665    for start in 0..mask.len() {
1666        if !mask[start] || label[start] != usize::MAX {
1667            continue;
1668        }
1669        let id = sizes.len();
1670        stack.clear();
1671        stack.push(start);
1672        label[start] = id;
1673        let mut count = 0usize;
1674        while let Some(p) = stack.pop() {
1675            count += 1;
1676            let (x, y) = (p % w, p / w);
1677            for (nx, ny) in
1678                [(x.wrapping_sub(1), y), (x + 1, y), (x, y.wrapping_sub(1)), (x, y + 1)]
1679            {
1680                if nx >= w || ny >= h {
1681                    continue;
1682                }
1683                let q = ny * w + nx;
1684                if mask[q] && label[q] == usize::MAX {
1685                    label[q] = id;
1686                    stack.push(q);
1687                }
1688            }
1689        }
1690        sizes.push(count);
1691    }
1692    label
1693        .into_iter()
1694        .map(|l| if l == usize::MAX { 0 } else { sizes[l] })
1695        .collect()
1696}
1697
1698/// `_suppress_object_pw_area_shrinkage`: pixel-wise non-overlap first, then
1699/// drop the objects whose foreground area shrinks below 30 %.
1700fn suppress_area_shrink(masks: &[Vec<f32>]) -> Vec<Vec<f32>> {
1701    if masks.len() <= 1 {
1702        return masks.to_vec();
1703    }
1704    let len = masks[0].len();
1705    let mut non_overlap: Vec<Vec<f32>> = masks.to_vec();
1706    for i in 0..len {
1707        let mut best = 0usize;
1708        let mut best_v = f32::NEG_INFINITY;
1709        for (k, m) in masks.iter().enumerate() {
1710            let v = if m[i] > 0.0 { m[i] } else { -10.0 };
1711            if v > best_v {
1712                best_v = v;
1713                best = k;
1714            }
1715        }
1716        for (k, m) in non_overlap.iter_mut().enumerate() {
1717            if k != best && m[i] > 0.0 {
1718                m[i] = -10.0;
1719            }
1720        }
1721    }
1722    let mut out = masks.to_vec();
1723    for (k, m) in masks.iter().enumerate() {
1724        let before = m.iter().filter(|&&v| v > 0.0).count();
1725        let after = non_overlap[k].iter().filter(|&&v| v > 0.0).count();
1726        if (after as f32) / (before.max(1) as f32) < 0.3 {
1727            for v in out[k].iter_mut() {
1728                if *v > -10.0 {
1729                    *v = -10.0;
1730                }
1731            }
1732        }
1733    }
1734    out
1735}
1736
1737fn relu_vec(v: &[f32]) -> Vec<f32> {
1738    v.iter().map(|x| x.max(0.0)).collect()
1739}
1740
1741fn argmax(v: &[f32]) -> usize {
1742    v.iter()
1743        .enumerate()
1744        .fold((0usize, f32::NEG_INFINITY), |(bi, bv), (i, &x)| {
1745            if x > bv { (i, x) } else { (bi, bv) }
1746        })
1747        .0
1748}
1749
1750/// Depthwise conv (`w [c, 1, k, k]`, `groups = c`, padding k/2).
1751fn depthwise(
1752    w: &[f32],
1753    b: &[f32],
1754    x: &[f32],
1755    c: usize,
1756    h: usize,
1757    wid: usize,
1758    k: usize,
1759    pool: Option<&Pool>,
1760) -> Vec<f32> {
1761    let pad = k / 2;
1762    let mut out = vec![0f32; c * h * wid];
1763    let ob = out.as_mut_ptr() as usize;
1764    let run = |s: usize, e: usize| {
1765        let o = unsafe { std::slice::from_raw_parts_mut(ob as *mut f32, c * h * wid) };
1766        for idx in s..e {
1767            let ch = idx / (h * wid);
1768            let rem = idx % (h * wid);
1769            let (y, x2) = (rem / wid, rem % wid);
1770            let mut acc = b[ch];
1771            for dy in 0..k {
1772                let iy = y as isize + dy as isize - pad as isize;
1773                if iy < 0 || iy >= h as isize {
1774                    continue;
1775                }
1776                for dx in 0..k {
1777                    let ix = x2 as isize + dx as isize - pad as isize;
1778                    if ix < 0 || ix >= wid as isize {
1779                        continue;
1780                    }
1781                    acc += w[(ch * k + dy) * k + dx]
1782                        * x[ch * h * wid + iy as usize * wid + ix as usize];
1783                }
1784            }
1785            o[idx] = acc;
1786        }
1787    };
1788    match pool {
1789        Some(p) => p.run_rows(c * h * wid, &run),
1790        None => run(0, c * h * wid),
1791    }
1792    out
1793}
1794
1795/// `trk.mask_downsample`: 1008² → 252² (conv k4 s4).
1796fn mask_downsample(net: &Net<'_>, mask: &[f32]) -> Result<Vec<f32>> {
1797    let w = net.v("trk.mask_downsample.weight")?;
1798    let b = net.v("trk.mask_downsample.bias")?;
1799    Ok(sam3::conv2d(&w, &b, mask, 1, IMG, IMG, 4, 4, 4, 0, net.pool))
1800}
1801
1802#[cfg(test)]
1803mod tests {
1804    use super::*;
1805
1806    fn cmf_path() -> Option<std::path::PathBuf> {
1807        [" /home/hermes/models/sam3-q8_2f.cmf"]
1808            .iter()
1809            .map(|s| std::path::PathBuf::from(s.trim()))
1810            .find(|p| p.exists())
1811    }
1812
1813    /// The tracker's shape-sensitive half (prompt encoder, mask decoder,
1814    /// memory encoder, memory attention, object pointers) without paying for
1815    /// a detector forward: fabricate the neck features, condition one frame
1816    /// from a synthetic detection mask, then propagate one frame.
1817    #[test]
1818    fn tracker_memory_smoke() {
1819        let Some(path) = cmf_path() else {
1820            eprintln!("SKIP tracker_memory_smoke: cmf not found");
1821            return;
1822        };
1823        let m = Sam3Video::open(path.to_str().expect("path")).expect("open video model");
1824        let net = m.net();
1825
1826        // deterministic synthetic neck features
1827        let fill = |len: usize, k: f32| -> Vec<f32> {
1828            (0..len)
1829                .map(|i| {
1830                    let h = (i as u32).wrapping_mul(2654435761) >> 16;
1831                    (h % 1000) as f32 / 1000.0 * k
1832                })
1833                .collect()
1834        };
1835        let feats = TrkFeats {
1836            l0: fill(32 * MASK_SIDE * MASK_SIDE, 0.05),
1837            l1: fill(64 * 144 * 144, 0.05),
1838            l2: fill(HID * EMB * EMB, 0.05),
1839        };
1840
1841        // synthetic detection: the square at (48..144)² of the 288² grid
1842        let mut mask = vec![0f32; MASK_SIDE * MASK_SIDE];
1843        for y in 48..144 {
1844            for x in 48..144 {
1845                mask[y * MASK_SIDE + x] = 1.0;
1846            }
1847        }
1848        let mut s = Session::new();
1849        s.obj_ids.push(0);
1850        s.max_obj_id = 0;
1851
1852        let (o, high) = m.use_mask_as_output(&net, &feats, &mask).expect("mask-as-output");
1853        assert_eq!(o.pred_masks.len(), MASK_SIDE * MASK_SIDE, "low-res mask shape");
1854        assert_eq!(o.pointer.len(), HID, "object pointer width");
1855        assert_eq!(high.len(), IMG * IMG, "high-res mask shape");
1856        assert!(o.obj_score > 0.0, "object score {}", o.obj_score);
1857        let area = 96 * 96;
1858        let pos = o.pred_masks.iter().filter(|&&v| v > 0.0).count();
1859        assert!(
1860            (pos as i64 - area as i64).abs() <= area as i64 / 4,
1861            "mask-as-output area {pos} vs input {area}"
1862        );
1863
1864        // conditioning frame + memory encode
1865        m.store(&mut s, 0, 0, true, o.clone());
1866        let pix = feats.l2.clone();
1867        m.encode_into(&net, &mut s, 0, vec![(0, o.clone(), high)], &pix, IMG, IMG, true)
1868            .expect("memory encode");
1869        assert!(s.objs[&0].cond[&0].mem_feat.is_some(), "memory features stored");
1870        assert_eq!(s.objs[&0].cond[&0].mem_feat.as_ref().unwrap().len(), EMB * EMB * MEM);
1871
1872        // propagate one frame: memory attention + decoder
1873        let (o1, high1) = m.propagate(&net, &s, 0, 1, &feats).expect("propagate");
1874        assert_eq!(o1.pred_masks.len(), MASK_SIDE * MASK_SIDE);
1875        assert_eq!(high1.len(), IMG * IMG);
1876        let pos1 = o1.pred_masks.iter().filter(|&&v| v > 0.0).count();
1877        let mn = o1.pred_masks.iter().cloned().fold(f32::INFINITY, f32::min);
1878        let mx = o1.pred_masks.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
1879        eprintln!(
1880            "tracker smoke: mask-as-output {} px, propagated {} px (logit {:.2}..{:.2}), obj_score {:.3} -> {:.3}",
1881            pos,
1882            pos1,
1883            mn,
1884            mx,
1885            o.obj_score,
1886            o1.obj_score
1887        );
1888        // the neck features here are synthetic noise, so the propagated mask
1889        // carries no structure — what this test guards is the SHAPE of every
1890        // stage (prompt encoder, mask decoder, memory encoder, memory
1891        // attention, object pointers) plus the object-score sign flip.
1892        assert!(o1.obj_score.is_finite(), "object score must be finite");
1893        assert!(mn.is_finite() && mx.is_finite(), "mask logits must be finite");
1894        assert!(mx > mn, "the decoder must not emit a constant mask ({mn})");
1895    }
1896
1897    fn f32s(p: &std::path::Path) -> Vec<f32> {
1898        let b = std::fs::read(p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()));
1899        b.chunks_exact(4).map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]])).collect()
1900    }
1901
1902    fn fx_dir() -> std::path::PathBuf {
1903        std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/sam3_video")
1904    }
1905
1906    /// Reference-anchored tracker check (no detector forward — the fixture
1907    /// supplies the real neck features, memory bank and object pointer):
1908    /// 1. the mask decoder on the frame-0 memory prompt must reproduce the
1909    ///    reference object pointer;
1910    /// 2. memory conditioning + decoder on frame 1 must reproduce the
1911    ///    reference object-score logit (+10.1, "the object is there").
1912    #[test]
1913    fn tracker_fixture_parity() {
1914        let fx = fx_dir();
1915        let Some(path) = cmf_path() else {
1916            eprintln!("SKIP tracker_fixture_parity: cmf not found");
1917            return;
1918        };
1919        if !fx.join("f1/obj0/non_cond_frame_outputs/object_score_logits.f32").exists() {
1920            eprintln!("SKIP tracker_fixture_parity: fixtures missing at {}", fx.display());
1921            return;
1922        }
1923        let m = Sam3Video::open(path.to_str().unwrap()).expect("open");
1924        let net = m.net();
1925        let fill = |len: usize| vec![0f32; len];
1926        let feats0 = TrkFeats { l0: fill(32 * MASK_SIDE * MASK_SIDE), l1: fill(64 * 144 * 144), l2: fill(HID * EMB * EMB) };
1927        let _ = &feats0;
1928
1929        // ── 1. frame 0: decoder with the detection mask prompt → pointer ──
1930        let pix0 = f32s(&fx.join("f0/neck_l2.f32"));
1931        assert_eq!(pix0.len(), EMB * EMB * HID);
1932        let pix0_chw = pix0_to_chw(&pix0); // the fixture stores (HW, C) rows
1933        let hi0 = f32s(&fx.join("f0/obj0/cond_frame_outputs/high_res_masks.f32"));
1934        let bin: Vec<f32> = hi0.iter().map(|&v| if v > 0.0 { 1.0 } else { 0.0 }).collect();
1935        assert_eq!(bin.len(), IMG * IMG);
1936        let ds = mask_downsample(&net, &bin).expect("mask_downsample");
1937        let ds = crate::sam3vid_net::resize2d(&ds, IMG / 4, IMG / 4, MASK_SIDE, MASK_SIDE);
1938        let dense = net.mask_prompt(&ds).expect("mask prompt");
1939        let sparse = net.empty_sparse().expect("sparse");
1940        let pos = net.image_pos().expect("pos");
1941        let (_m, iou0, sc0, toks) = net
1942            .mask_decoder(&pix0_to_chw(&pix0), &pos, &dense, &sparse, (&feats0.l0, &feats0.l1), true)
1943            .expect("decoder f0");
1944        let n = toks.len() / HID;
1945        let best = argmax(&iou0).min(n - 1);
1946        let ptr = m.object_pointer(&net, &toks[best * HID..(best + 1) * HID], sc0 > 0.0).expect("pointer");
1947        let want = f32s(&fx.join("f0/obj0/cond_frame_outputs/object_pointer.f32"));
1948        let cos = dot(&ptr, &want) / (norm(&ptr) * norm(&want) + 1e-9);
1949        eprintln!("fixture f0 pointer: cos={cos:.4} obj_score={sc0:.3} (ref mask prompt)");
1950        assert!(cos >= 0.99, "frame-0 object pointer cosine {cos:.4} vs reference");
1951
1952        // ── 2. frame 1: memory conditioning + decoder → object score ──
1953        let pix1 = f32s(&fx.join("f1/neck_l2.f32"));
1954        let mem_feat = f32s(&fx.join("f0/obj0/cond_frame_outputs/maskmem_features.f32"));
1955        let mem_pos = f32s(&fx.join("f0/obj0/cond_frame_outputs/maskmem_pos_enc.f32"));
1956        let mem_feat_ref = mem_feat.clone();
1957        let memories = vec![(0usize, mem_feat, mem_pos)];
1958        let pointers = vec![(1usize, want.clone())];
1959        let neck_pos = Net::neck_pos();
1960        let cond = m.memory_cond(&net, &pix1, &neck_pos, &memories, &pointers, 8).expect("memory cond");
1961        let cond_ref_path = fx.join("f1/memory_conditioned.f32");
1962        if cond_ref_path.exists() {
1963            let want_cond = f32s(&cond_ref_path);
1964            let cond_cos = dot(&cond, &want_cond) / (norm(&cond) * norm(&want_cond) + 1e-9);
1965            let cond_md = cond.iter().zip(&want_cond).map(|(a, b)| (a - b).abs()).fold(0f32, f32::max);
1966            eprintln!("fixture f1 memory conditioning: cos={cond_cos:.6} maxdiff={cond_md:.4}");
1967            assert!(cond_cos >= 0.99, "frame-1 memory conditioning cosine {cond_cos:.6}");
1968        }
1969        let dense = net.no_mask_prompt().expect("no mask");
1970        let (_m, iou1, sc1, t1) = net
1971            .mask_decoder(&pix0_to_chw(&cond), &pos, &dense, &sparse, (&feats0.l0, &feats0.l1), true)
1972            .expect("decoder f1");
1973        let want_sc = f32s(&fx.join("f1/obj0/non_cond_frame_outputs/object_score_logits.f32"))[0];
1974        eprintln!(
1975            "fixture f1: obj_score ours={sc1:.3} reference={want_sc:.3} (delta {:.3}), iou={iou1:?}",
1976            (sc1 - want_sc).abs()
1977        );
1978        assert!(
1979            (sc1 - want_sc).abs() < 1.0,
1980            "frame-1 object score {sc1:.3} vs reference {want_sc:.3}"
1981        );
1982        assert!(sc1 > 0.0, "the tracked object must read as present (got {sc1:.3})");
1983        let best1 = argmax(&iou1).min(t1.len() / HID - 1);
1984        let ptr1 = m
1985            .object_pointer(&net, &t1[best1 * HID..(best1 + 1) * HID], sc1 > 0.0)
1986            .expect("pointer f1");
1987        let want_ptr1 = f32s(&fx.join("f1/obj0/non_cond_frame_outputs/object_pointer.f32"));
1988        for (j, tok) in t1.chunks_exact(HID).enumerate() {
1989            let p = m.object_pointer(&net, tok, sc1 > 0.0).expect("pointer candidate f1");
1990            let c = dot(&p, &want_ptr1) / (norm(&p) * norm(&want_ptr1) + 1e-9);
1991            eprintln!("fixture f1 pointer candidate {j}: cos={c:.5} iou={:.6}", iou1[j]);
1992        }
1993        let ptr_cos1 = dot(&ptr1, &want_ptr1) / (norm(&ptr1) * norm(&want_ptr1) + 1e-9);
1994        eprintln!("fixture f1 pointer: cos={ptr_cos1:.5}");
1995
1996        // ── 2b. frame 2: TWO spatial memories must each get the grid RoPE ──
1997        // This catches a cross-attention failure that frame 1 cannot see: HF
1998        // repeats the 72×72 frequencies for every memory frame, while the
1999        // object pointers at the end stay unrotated.
2000        let pix2 = f32s(&fx.join("f2/neck_l2.f32"));
2001        let memories2 = vec![
2002            (0usize, mem_feat_ref.clone(), f32s(&fx.join("f0/obj0/cond_frame_outputs/maskmem_pos_enc.f32"))),
2003            (
2004                1usize,
2005                f32s(&fx.join("f1/obj0/non_cond_frame_outputs/maskmem_features.f32")),
2006                f32s(&fx.join("f1/obj0/non_cond_frame_outputs/maskmem_pos_enc.f32")),
2007            ),
2008        ];
2009        let pointers2 = vec![(2usize, want.clone()), (1usize, want_ptr1.clone())];
2010        let cond2 = m.memory_cond(&net, &pix2, &neck_pos, &memories2, &pointers2, 8).expect("memory cond f2");
2011        let cond2_ref_path = fx.join("f2/memory_conditioned.f32");
2012        if cond2_ref_path.exists() {
2013            let want_cond2 = f32s(&cond2_ref_path);
2014            let cond_cos2 = dot(&cond2, &want_cond2) / (norm(&cond2) * norm(&want_cond2) + 1e-9);
2015            let cond_md2 = cond2.iter().zip(&want_cond2).map(|(a, b)| (a - b).abs()).fold(0f32, f32::max);
2016            eprintln!("fixture f2 memory conditioning: cos={cond_cos2:.6} maxdiff={cond_md2:.4}");
2017            assert!(cond_cos2 >= 0.99, "frame-2 memory conditioning cosine {cond_cos2:.6}");
2018        }
2019        let (_m2, iou2, sc2, t2) = net
2020            .mask_decoder(&pix0_to_chw(&cond2), &pos, &dense, &sparse, (&feats0.l0, &feats0.l1), true)
2021            .expect("decoder f2");
2022        let want_sc2 = f32s(&fx.join("f2/obj0/non_cond_frame_outputs/object_score_logits.f32"))[0];
2023        let best2 = argmax(&iou2).min(t2.len() / HID - 1);
2024        let ptr2 = m
2025            .object_pointer(&net, &t2[best2 * HID..(best2 + 1) * HID], sc2 > 0.0)
2026            .expect("pointer f2");
2027        let want_ptr2 = f32s(&fx.join("f2/obj0/non_cond_frame_outputs/object_pointer.f32"));
2028        let ptr_cos2 = dot(&ptr2, &want_ptr2) / (norm(&ptr2) * norm(&want_ptr2) + 1e-9);
2029        eprintln!("fixture f2: obj_score ours={sc2:.3} reference={want_sc2:.3}, pointer cos={ptr_cos2:.5}");
2030        assert!(ptr_cos1 >= 0.99, "frame-1 object pointer cosine {ptr_cos1:.5}");
2031        assert!((sc2 - want_sc2).abs() < 1.0, "frame-2 object score {sc2:.3} vs {want_sc2:.3}");
2032        assert!(ptr_cos2 >= 0.99, "frame-2 object pointer cosine {ptr_cos2:.5}");
2033
2034        // ── 3. our own memory encoder vs the reference's stored memory ──
2035        // (the live pipeline feeds the tracker OUR encoding of frame 0, so it
2036        // must reproduce `maskmem_features` for the same inputs)
2037        for from_pts in [true, false] {
2038            let (mf, mp) = m
2039                .encode_memory(&net, &pix0_chw, &hi0, IMG, IMG, sc0, from_pts)
2040                .expect("encode_memory");
2041            let cos = dot(&mf, &mem_feat_ref) / (norm(&mf) * norm(&mem_feat_ref) + 1e-9);
2042            let md = mf
2043                .iter()
2044                .zip(&mem_feat_ref)
2045                .map(|(a, b)| (a - b).abs())
2046                .fold(0f32, f32::max);
2047            eprintln!(
2048                "fixture mem encode from_pts={from_pts}: cos={cos:.5} maxdiff={md:.4} rms={:.4} (ref rms {:.4})",
2049                norm(&mf) / (mf.len() as f32).sqrt(),
2050                norm(&mem_feat_ref) / (mem_feat_ref.len() as f32).sqrt()
2051            );
2052            assert!(cos >= 0.99, "memory features cosine {cos:.5} (from_pts={from_pts})");
2053            let want_mp = f32s(&fx.join("f0/obj0/cond_frame_outputs/maskmem_pos_enc.f32"));
2054            let pos_cos = dot(&mp, &want_mp) / (norm(&mp) * norm(&want_mp) + 1e-9);
2055            assert!(pos_cos >= 0.99, "memory position cosine {pos_cos:.5}");
2056        }
2057    }
2058
2059    fn pix0_to_chw(rows: &[f32]) -> Vec<f32> {
2060        sam3::rows_to_chw(rows, EMB, EMB)
2061    }
2062
2063    fn dot(a: &[f32], b: &[f32]) -> f32 {
2064        a.iter().zip(b).map(|(x, y)| x * y).sum()
2065    }
2066
2067    fn norm(a: &[f32]) -> f32 {
2068        a.iter().map(|x| x * x).sum::<f32>().sqrt()
2069    }
2070}