diff --git a/crates/engine/src/analysis/roto.rs b/crates/engine/src/analysis/roto.rs index 286b95095..9cc950e07 100644 --- a/crates/engine/src/analysis/roto.rs +++ b/crates/engine/src/analysis/roto.rs @@ -838,6 +838,25 @@ impl Session { changed } + /// How far to trust the frames a tracker followed (SAM 2), by layer frame: its confidence and + /// whether the object was absent. Frames the classic engine made have no entry. + pub fn roto_trust(&self, comp: ItemId, layer: LayerId, effect: Uid) -> BTreeMap { + let mut out = BTreeMap::new(); + let Some(c) = self.project.comp(comp) else { return out }; + let Some(l) = c.layer(layer) else { return out }; + let Some(g) = l.props.find_group(effect) else { return out }; + if is_object_matte(g) { + return out; + } + let ch = fx::Chain::new(¶ms_static(g), layer_size(&self.project, c, l), c.frame_rate.as_f64(), 1.0); + for f in ch.keys.keys() { + if let Some(t) = ch.trust_at(*f) { + out.insert(*f, t); + } + } + out + } + /// The computed frames of an instance (full resolution), by layer frame. pub fn roto_computed(&self, comp: ItemId, layer: LayerId, effect: Uid) -> BTreeMap { let mut out = BTreeMap::new(); diff --git a/crates/engine/src/tests/roto.rs b/crates/engine/src/tests/roto.rs index 7d18e5c98..66baee148 100644 --- a/crates/engine/src/tests/roto.rs +++ b/crates/engine/src/tests/roto.rs @@ -977,7 +977,9 @@ impl effectcraft_segment::ObjectTracker for Recorder { /// corrected frame; the frames before it keep their tracked results. #[test] fn a_correction_re_tracks_only_the_frames_it_changes() { - let (mut s, clip, _) = painted(center, "disk-retrack.mov"); + let (mut s, clip, uid) = painted(center, "disk-retrack.mov"); + let cid = s.active_comp_id().unwrap(); + assert!(s.roto_trust(cid, clip, uid).is_empty(), "no tracker data before tracking"); let rec = std::sync::Arc::new(Recorder::default()); effectcraft_effects::roto::set_model(Some(rec.clone())); let run = |s: &mut Session| s.execute_checked("roto.propagate", json!({"layer": clip.0, "direction": "forward", "wait": true})); @@ -1000,4 +1002,8 @@ fn a_correction_re_tracks_only_the_frames_it_changes() { let st = s.execute("roto.status", json!({"layer": clip.0, "frame": 3})).unwrap(); assert_eq!(st["source"], "tracker", "{st}"); assert_eq!(st["tracker"]["frames"].as_object().unwrap().len(), 21, "{st}"); + // The span bar's data: the tracker's confidence for every tracked frame. + let trust = s.roto_trust(cid, clip, uid); + assert_eq!(trust.len(), 21); + assert!(trust.values().all(|t| t.confidence > 0.9 && !t.occluded)); } diff --git a/crates/ui-egui/src/panels/roto_tool.rs b/crates/ui-egui/src/panels/roto_tool.rs index ca3c56ac5..f8cd383c6 100644 --- a/crates/ui-egui/src/panels/roto_tool.rs +++ b/crates/ui-egui/src/panels/roto_tool.rs @@ -243,6 +243,20 @@ fn run_stroke(app: &mut EffectcraftApp, layer: u64, kind: &str, points: &[[f64; } } +/// The span bar's colour for a frame a tracker followed: green when it is sure, amber when it is +/// doubtful, red when it is not, slate when the object was absent. +pub(crate) fn trust_color(confidence: f32, occluded: bool) -> Color32 { + if occluded { + Color32::from_rgb(110, 118, 150) + } else if confidence >= 0.85 { + Color32::from_rgb(70, 200, 90) + } else if confidence >= 0.5 { + Color32::from_rgb(230, 180, 50) + } else { + Color32::from_rgb(220, 70, 60) + } +} + /// The segmentation span bar (above the panel's bottom bar): the layer's frames, the span, the /// base frame (yellow), computed frames (green) and stroke frames. Click to go to a frame; drag /// the span's ends to change it. @@ -264,10 +278,17 @@ pub fn span_bar(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, let sp = Rect::from_min_max(pos2(x_of(d.span[0] as f64), track.min.y), pos2(x_of(d.span[1] as f64 + 1.0), track.max.y)); let frozen = fx::is_frozen(¶ms); p.rect_filled(sp, 2.0, if frozen { Color32::from_rgb(60, 110, 190) } else { Color32::from_rgb(90, 90, 96) }); + // Computed frames: green, or (frames a tracker followed) coloured by its confidence, with + // frames where the object was absent in slate. + let trust = app.session.roto_trust(cid, layer.id, uid); for (f, done) in app.session.roto_computed(cid, layer.id, uid) { if done { - let cr = Rect::from_min_max(pos2(x_of(f as f64), track.max.y - 4.0), pos2(x_of(f as f64 + 1.0), track.max.y)); - p.rect_filled(cr, 0.0, Color32::from_rgb(70, 200, 90)); + let (color, h) = match trust.get(&f) { + Some(t) => (trust_color(t.confidence, t.occluded), 6.0), + None => (Color32::from_rgb(70, 200, 90), 4.0), + }; + let cr = Rect::from_min_max(pos2(x_of(f as f64), track.max.y - h), pos2(x_of(f as f64 + 1.0), track.max.y)); + p.rect_filled(cr, 0.0, color); } } for f in d.stroke_frames() { @@ -363,3 +384,21 @@ pub fn bar_buttons(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painte } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn confidence_colours_run_from_green_to_red_and_absent_is_slate() { + let (g, a, r, s) = (trust_color(0.99, false), trust_color(0.6, false), trust_color(0.1, false), trust_color(0.99, true)); + assert_eq!(g, Color32::from_rgb(70, 200, 90)); + assert_eq!(a, Color32::from_rgb(230, 180, 50)); + assert_eq!(r, Color32::from_rgb(220, 70, 60)); + assert_eq!(s, Color32::from_rgb(110, 118, 150)); + // the boundaries + assert_eq!(trust_color(0.85, false), g); + assert_eq!(trust_color(0.5, false), a); + assert_eq!(trust_color(f32::NAN, false), r); + } +}