mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-01 22:20:00 -06:00
S2.69: several objects in one Roto Brush instance, tracked in one SAM 2 pass
Strokes carry an object (0 for older projects, nothing written for 0). The tracker follows every object in one pass and the mattes are united; the confidence of a frame is the least sure object's. Object picker in the Layer panel's bar; roto.options/stroke/clearStrokes/status take or report objects. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
fd1d6e623f
commit
08bf6e43fe
@@ -51,6 +51,9 @@ pub struct RotoOptions {
|
||||
pub boundary_color: [f64; 3],
|
||||
/// Start propagating in the background after each stroke.
|
||||
pub auto_propagate: bool,
|
||||
/// The object new strokes belong to (0 is the first). With a tracker every object is followed on its own
|
||||
/// and their mattes are united; the classic engine treats all foreground strokes as one object.
|
||||
pub object: u32,
|
||||
}
|
||||
|
||||
impl Default for RotoOptions {
|
||||
@@ -63,10 +66,14 @@ impl Default for RotoOptions {
|
||||
overlay_opacity: 50.0,
|
||||
boundary_color: [1.0, 0.25, 0.75],
|
||||
auto_propagate: true,
|
||||
object: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The highest object id (SAM 2 follows up to 32 objects in one pass).
|
||||
pub const MAX_OBJECT_ID: u32 = 31;
|
||||
|
||||
pub const VIEWS: [&str; 4] = ["alphaBoundary", "alpha", "alphaOverlay", "none"];
|
||||
|
||||
/// What a job does.
|
||||
|
||||
@@ -27,7 +27,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Roto Brush Stroke",
|
||||
[],
|
||||
None,
|
||||
"{layer?, kind?: fg|bg|refine|refineErase, points: [[x, y], …] (layer pixels), frame? (layer frame; default current), radius? (layer px; default the tool's diameter / 2), effect?}",
|
||||
"{layer?, kind?: fg|bg|refine|refineErase, points: [[x, y], …] (layer pixels), frame? (layer frame; default current), radius? (layer px; default the tool's diameter / 2), effect?, object? (0–31; default the current object: with a tracker each object is followed on its own and the mattes are united)}",
|
||||
has_comp,
|
||||
stroke
|
||||
),
|
||||
@@ -43,14 +43,22 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
cmd!("roto.span", "Set Segmentation Span", [], None, "{layer?, effect?, start?, end? (layer frames)}", has_comp, span),
|
||||
cmd!("roto.freeze", "Freeze", [], None, "{layer?, effect?, wait?}", has_comp, freeze),
|
||||
cmd!("roto.unfreeze", "Unfreeze", [], None, "{layer?, effect?}", has_comp, unfreeze),
|
||||
cmd!("roto.clearStrokes", "Remove Roto Brush Strokes", [], None, "{layer?, effect?, frame? (only this layer frame), kind?}", has_comp, clear_strokes),
|
||||
cmd!(
|
||||
"roto.clearStrokes",
|
||||
"Remove Roto Brush Strokes",
|
||||
[],
|
||||
None,
|
||||
"{layer?, effect?, frame? (only this layer frame), kind?, object? (only this object's strokes)}",
|
||||
has_comp,
|
||||
clear_strokes
|
||||
),
|
||||
cmd!("roto.cancel", "Stop Roto Brush", [], None, "{}", is_running, |s, _| Ok(json!({"stopped": s.stop_roto()}))),
|
||||
cmd!(
|
||||
"roto.options",
|
||||
"Roto Brush Options",
|
||||
[],
|
||||
None,
|
||||
"{diameter?, refineDiameter?, view?: alphaBoundary|alpha|alphaOverlay|none, overlayColor?: [r,g,b], overlayOpacity?, boundaryColor?: [r,g,b], autoPropagate?}",
|
||||
"{diameter?, refineDiameter?, view?: alphaBoundary|alpha|alphaOverlay|none, overlayColor?: [r,g,b], overlayOpacity?, boundaryColor?: [r,g,b], autoPropagate?, object? (the object new strokes belong to, 0–31)}",
|
||||
always,
|
||||
options
|
||||
),
|
||||
@@ -232,7 +240,8 @@ fn stroke(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if existing.is_none() && !kind.is_segmentation() {
|
||||
return Err(bad(cmd, "draw a foreground stroke with the Roto Brush tool first"));
|
||||
}
|
||||
let st = Stroke { kind, frame, radius, points };
|
||||
let object = f_p(p, "object").map_or(s.state.roto.object, |v| v.clamp(0.0, rt::MAX_OBJECT_ID as f64).round() as u32);
|
||||
let st = Stroke { kind, frame, radius, points, object };
|
||||
let label = if kind.is_segmentation() { "Roto Brush Stroke" } else { "Refine Edge Stroke" };
|
||||
let (uid, d) = s.edit(label, None, |proj, _| {
|
||||
let mut next = proj.next_id;
|
||||
@@ -263,7 +272,7 @@ fn stroke(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.state.selected_layers = vec![lid];
|
||||
}
|
||||
Ok(json!({
|
||||
"layer": lid.0, "effect": uid, "frame": frame, "kind": kind, "radius": radius,
|
||||
"layer": lid.0, "effect": uid, "frame": frame, "kind": kind, "radius": radius, "object": object,
|
||||
"base": d.base, "span": d.span, "strokes": d.strokes.len(),
|
||||
}))
|
||||
}
|
||||
@@ -369,8 +378,9 @@ fn clear_strokes(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(k) => Some(StrokeKind::from_name(k).ok_or_else(|| bad(cmd, format!("unknown kind `{k}`")))?),
|
||||
None => None,
|
||||
};
|
||||
let object = f_p(p, "object").map(|v| v.round() as u32);
|
||||
let n0 = d.strokes.len();
|
||||
d.strokes.retain(|st| !(frame.is_none_or(|f| st.frame == f) && kind.is_none_or(|k| st.kind == k)));
|
||||
d.strokes.retain(|st| !(frame.is_none_or(|f| st.frame == f) && kind.is_none_or(|k| st.kind == k) && object.is_none_or(|o| st.object == o)));
|
||||
if d.strokes.iter().all(|s| !s.kind.is_segmentation()) {
|
||||
d.base = None;
|
||||
} else if d.base.is_some_and(|b| !d.strokes.iter().any(|s| s.frame == b && s.kind.is_segmentation())) {
|
||||
@@ -412,6 +422,9 @@ fn options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(b) = b_p(p, "autoPropagate") {
|
||||
o.auto_propagate = b;
|
||||
}
|
||||
if let Some(v) = f_p(p, "object") {
|
||||
o.object = v.clamp(0.0, rt::MAX_OBJECT_ID as f64).round() as u32;
|
||||
}
|
||||
s.state.roto = o.clone();
|
||||
Ok(serde_json::to_value(o).unwrap_or(Value::Null))
|
||||
}
|
||||
@@ -430,6 +443,8 @@ fn status(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
out["base"] = json!(d.base);
|
||||
out["span"] = json!(d.span);
|
||||
out["strokes"] = json!(d.strokes.len());
|
||||
out["objects"] = json!(d.objects().iter().map(|o| json!({"id": o, "strokes": d.strokes.iter().filter(|s| s.object == *o).count()})).collect::<Vec<_>>());
|
||||
out["currentObject"] = json!(s.state.roto.object);
|
||||
out["strokeFrames"] = json!(d.stroke_frames());
|
||||
out["frozen"] = json!(frozen_of(&g));
|
||||
out["pending"] = json!(s.roto_pending.contains(&(cid, lid, uid)));
|
||||
|
||||
@@ -1007,3 +1007,28 @@ fn a_correction_re_tracks_only_the_frames_it_changes() {
|
||||
assert_eq!(trust.len(), 21);
|
||||
assert!(trust.values().all(|t| t.confidence > 0.9 && !t.occluded));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strokes_belong_to_the_current_object() {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"width": 64, "height": 64, "frameRate": 25, "duration": 1})).unwrap();
|
||||
let layer = s.execute("layer.newSolid", json!({"width": 64, "height": 64})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("roto.stroke", json!({"layer": layer, "points": [[10, 10]], "radius": 4})).unwrap();
|
||||
// the current object is an option shared by the tool and the commands
|
||||
s.execute("roto.options", json!({"object": 1})).unwrap();
|
||||
s.execute("roto.stroke", json!({"layer": layer, "points": [[50, 50]], "radius": 4})).unwrap();
|
||||
s.execute("roto.stroke", json!({"layer": layer, "kind": "bg", "points": [[30, 30]], "radius": 4})).unwrap();
|
||||
// an explicit object wins over the option, and is clamped to the supported range
|
||||
s.execute("roto.stroke", json!({"layer": layer, "points": [[20, 50]], "radius": 4, "object": 99})).unwrap();
|
||||
let st = s.execute("roto.status", json!({"layer": layer})).unwrap();
|
||||
assert_eq!(st["currentObject"], 1);
|
||||
assert_eq!(st["objects"], json!([{"id": 0, "strokes": 1}, {"id": 1, "strokes": 2}, {"id": 31, "strokes": 1}]));
|
||||
// removing one object's strokes leaves the others
|
||||
let r = s.execute("roto.clearStrokes", json!({"layer": layer, "object": 1})).unwrap();
|
||||
assert_eq!(r["removed"], 2);
|
||||
let st = s.execute("roto.status", json!({"layer": layer})).unwrap();
|
||||
assert_eq!(st["objects"], json!([{"id": 0, "strokes": 1}, {"id": 31, "strokes": 1}]));
|
||||
// the object survives a save and a load
|
||||
let json = s.project.to_json();
|
||||
assert!(json.contains("\\\"object\\\":31"), "the project file carries the object");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user