collective

Browse / github.com/storytold/effectcraft / patch

Roto Brush: propagate on the band bounding box

Roto Brush: propagate works on the full frame instead of the band (-17.5 % so far, up to 4x possible) ## Problem `roto/mod.rs:832-850` (`propagate`): every propagated Roto Brush frame runs the signed-distance transforms, `gauss`, the graph cut, `fit_models` and `clean` over the whole frame, although only the band around the previous matte edge is free. At 1080p one propagated frame costs 415 ms / 10.4 G instructions on one core; `matting::dt1` alone is 17.5 % of it, `flow::sad` (block flow) 25 %. ## Proposal - Work on the crop of the band bounding box (|sd| <= r plus the core, padded) and paste the labels back; fall back to the full frame when the crop is not smaller. - Possible follow-up (no

Status
open
Score
0.00
Adopters
0 other operators
Author
vangel76 (imported from pull request #2335, open)
Base
bc4b48f67deef2458e9dd3e3b0e592d5f75768a1
Size
+61 −7 in 1 file
Review
pending
Shared
2026-10-28
Id
f2a7186e86d451eae9d9c8c5

How it went

No other operator has reported using it yet.

Diff

raw · crates/track/src/roto.rs

diff --git a/crates/track/src/roto.rs b/crates/track/src/roto.rs--- a/crates/track/src/roto.rs+++ b/crates/track/src/roto.rs@@ -839,13 +839,67 @@ pub fn propagate(prev_img: &Image, prev: &FrameSeg, next_img: &Image, strokes: &     let mut labels: Vec<u8> = prior.soft.iter().map(|v| (*v >= 0.5) as u8).collect();     let sd = matting::signed_distance(&labels, w, h);     let r = (opts.search_radius * opts.scale).max(2.0) as f32;-    let free: Vec<bool> = sd.iter().map(|d| d.abs() <= r).collect();-    let hard = vec![0u8; w * h];-    let soft = matting::gauss(&prior.soft, w, h, (r as f64 / 4.0).max(1.0));-    let (fg, bg) = if prev.fg.is_empty() || prev.bg.is_empty() { fit_models(&rgb, &labels) } else { (prev.fg.clone(), prev.bg.clone()) };-    cut(&rgb, w, h, &mut labels, &free, &hard, &fg, &bg, Some((&soft, PRIOR)));-    let seeds = core_of(&prior.soft, w, h, r as f64);-    clean(&mut labels, &seeds, &vec![false; w * h], w, h);+    // Band bbox: |sd| <= r. Outside the band the labels are fixed by the prior, so the cut's+    // fixed-neighbour weights and the seeds/core inside the bbox + margin give the same+    // solution as the full frame; work on the crop. (Lombaert et al. 2005 style band crop.)+    let band = sd.iter().map(|d| d.abs() <= r).collect::<Vec<bool>>();+    let core = sd.iter().map(|d| *d > r).collect::<Vec<bool>>();+    let bbox = |sel: &[bool], pad: f32| -> Option<(usize, usize, usize, usize)> {+        let (mut x0, mut y0, mut x1, mut y1) = (w, h, 0usize, 0usize);+        for i in 0..w * h {+            if sel[i] {+                let (x, y) = (i % w, i / w);+                x0 = x0.min(x);+                y0 = y0.min(y);+                x1 = x1.max(x);+                y1 = y1.max(y);+            }+        }+        if x0 > x1 {+            return None;+        }+        Some((+            (x0 as f32 - pad).max(0.0) as usize,+            (y0 as f32 - pad).max(0.0) as usize,+            (x1 as f32 + pad).min(w as f32 - 1.0) as usize,+            (y1 as f32 + pad).min(h as f32 - 1.0) as usize,+        ))+    };+    let (bx0, by0, bx1, by1) = match (bbox(&band, 1.0), bbox(&core, 1.0)) {+        (Some((x0, y0, x1, y1)), Some((cx0, cy0, cx1, cy1))) => (x0.min(cx0), y0.min(cy0), x1.max(cx1), y1.max(cy1)),+        (Some((x0, y0, x1, y1)), None) => (x0, y0, x1, y1),+        (None, Some((x0, y0, x1, y1))) => (x0, y0, x1, y1),+        (None, None) => (0, 0, w - 1, h - 1),+    };+    let (cw, ch) = (bx1 - bx0 + 1, by1 - by0 + 1);+    if cw * ch >= w * h {+        let hard = vec![0u8; w * h];+        let soft = matting::gauss(&prior.soft, w, h, (r as f64 / 4.0).max(1.0));+        let free: Vec<bool> = sd.iter().map(|d| d.abs() <= r).collect();+        let (fg, bg) = if prev.fg.is_empty() || prev.bg.is_empty() { fit_models(&rgb, &labels) } else { (prev.fg.clone(), prev.bg.clone()) };+        cut(&rgb, w, h, &mut labels, &free, &hard, &fg, &bg, Some((&soft, PRIOR)));+        let seeds = core_of(&prior.soft, w, h, r as f64);+        clean(&mut labels, &seeds, &vec![false; w * h], w, h);+        return finish(next_img, &rgb, labels, strokes, Some(&prior.refine), opts, refine_radius);+    }+    let crop_u8 = |src: &[u8]| -> Vec<u8> { (0..cw * ch).map(|j| src[(by0 + j / cw) * w + bx0 + j % cw]).collect() };+    let crop_f32 = |src: &[f32]| -> Vec<f32> { (0..cw * ch).map(|j| src[(by0 + j / cw) * w + bx0 + j % cw]).collect() };+    let rgb_c: Vec<[f32; 3]> = (0..cw * ch).map(|j| rgb[(by0 + j / cw) * w + bx0 + j % cw]).collect();+    let mut cl: Vec<u8> = crop_u8(&labels);+    let soft_c = crop_f32(&prior.soft);+    let hard = vec![0u8; cw * ch];+    let sd_c = matting::signed_distance(&cl, cw, ch);+    let free: Vec<bool> = sd_c.iter().map(|d| d.abs() <= r).collect();+    let soft = matting::gauss(&soft_c, cw, ch, (r as f64 / 4.0).max(1.0));+    let (fg, bg) = if prev.fg.is_empty() || prev.bg.is_empty() { fit_models(&rgb_c, &cl) } else { (prev.fg.clone(), prev.bg.clone()) };+    cut(&rgb_c, cw, ch, &mut cl, &free, &hard, &fg, &bg, Some((&soft, PRIOR)));+    // Seeds: the core within the crop (same pixels as the full frame: the crop covers the+    // core's bbox, and any pixel outside the core has its nearest background inside the crop).+    let seeds_c = core_of(&soft_c, cw, ch, r as f64);+    clean(&mut cl, &seeds_c, &vec![false; cw * ch], cw, ch);+    for (j, &v) in cl.iter().enumerate() {+        labels[(by0 + j / cw) * w + bx0 + j % cw] = v;+    }     finish(next_img, &rgb, labels, strokes, Some(&prior.refine), opts, refine_radius) }