289 lines
11 KiB
Rust
289 lines
11 KiB
Rust
//! Motion trackers in the property tree, laid out like After Effects':
|
|
//!
|
|
//! ```text
|
|
//! Motion Trackers (match `motionTrackers`, first group of the layer)
|
|
//! Tracker 1 (GroupKind::Tracker: type, target, options)
|
|
//! Track Point 1 (match `trackPoint`)
|
|
//! Feature Center, Feature Size, Search Offset, Search Size,
|
|
//! Confidence, Attach Point, Attach Point Offset
|
|
//! ```
|
|
//!
|
|
//! Feature Center, Confidence and Attach Point get one keyframe per analysed frame (layer time).
|
|
//! The tracking algorithms live in `effectcraft-track`; applying tracks lives in the engine.
|
|
|
|
use effectcraft_keyframe::Value;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::build::Ids;
|
|
use crate::props::{GroupKind, ParamUi, PropGroup};
|
|
use crate::{Layer, LayerId, Uid};
|
|
|
|
/// Tracker panel ▸ Track Type.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum TrackKind {
|
|
Stabilize,
|
|
#[default]
|
|
Transform,
|
|
/// Parallel (affine) corner pin: three points, the fourth corner completes the parallelogram.
|
|
#[serde(alias = "parallel", alias = "parallelCornerPin")]
|
|
Affine,
|
|
/// Perspective corner pin: four points.
|
|
#[serde(alias = "perspectiveCornerPin")]
|
|
Perspective,
|
|
/// Raw: track data only (nothing to apply).
|
|
Raw,
|
|
}
|
|
|
|
impl TrackKind {
|
|
pub const ALL: [TrackKind; 5] = [TrackKind::Stabilize, TrackKind::Transform, TrackKind::Affine, TrackKind::Perspective, TrackKind::Raw];
|
|
pub fn label(self) -> &'static str {
|
|
match self {
|
|
TrackKind::Stabilize => "Stabilize",
|
|
TrackKind::Transform => "Transform",
|
|
TrackKind::Affine => "Parallel Corner Pin",
|
|
TrackKind::Perspective => "Perspective Corner Pin",
|
|
TrackKind::Raw => "Raw",
|
|
}
|
|
}
|
|
pub fn id(self) -> &'static str {
|
|
match self {
|
|
TrackKind::Stabilize => "stabilize",
|
|
TrackKind::Transform => "transform",
|
|
TrackKind::Affine => "affine",
|
|
TrackKind::Perspective => "perspective",
|
|
TrackKind::Raw => "raw",
|
|
}
|
|
}
|
|
pub fn from_name(s: &str) -> Option<TrackKind> {
|
|
let n = s.to_ascii_lowercase().replace([' ', '_', '-'], "");
|
|
match n.as_str() {
|
|
"parallel" | "parallelcornerpin" | "affinecornerpin" => return Some(TrackKind::Affine),
|
|
"perspectivecornerpin" => return Some(TrackKind::Perspective),
|
|
_ => {}
|
|
}
|
|
TrackKind::ALL.into_iter().find(|k| k.id() == n || k.label().to_ascii_lowercase().replace(' ', "") == n)
|
|
}
|
|
pub fn is_corner_pin(self) -> bool {
|
|
matches!(self, TrackKind::Affine | TrackKind::Perspective)
|
|
}
|
|
/// Number of track points for this type (Transform/Stabilize: 2 with rotation or scale).
|
|
pub fn point_count(self, rotation_or_scale: bool) -> usize {
|
|
match self {
|
|
TrackKind::Affine => 3,
|
|
TrackKind::Perspective => 4,
|
|
_ if rotation_or_scale => 2,
|
|
_ => 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Motion Tracker Options ▸ Channel.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum TrackChannel {
|
|
Rgb,
|
|
#[default]
|
|
Luminance,
|
|
Saturation,
|
|
}
|
|
|
|
/// Motion Tracker Options ▸ "If Confidence is Below … %".
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub enum LowConfidence {
|
|
Continue,
|
|
Stop,
|
|
Extrapolate,
|
|
#[default]
|
|
Adapt,
|
|
}
|
|
|
|
/// Motion Tracker Options.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
#[serde(default, rename_all = "camelCase")]
|
|
pub struct TrackerOptions {
|
|
pub channel: TrackChannel,
|
|
/// Process Before Match ▸ Blur radius in pixels (0 = off).
|
|
pub blur: f64,
|
|
/// Process Before Match ▸ Enhance.
|
|
pub enhance: bool,
|
|
pub subpixel: bool,
|
|
/// Adapt Feature on Every Frame.
|
|
pub adapt_every_frame: bool,
|
|
/// Confidence threshold, percent.
|
|
pub threshold: f64,
|
|
pub action: LowConfidence,
|
|
/// Follow each feature's rotation and scale while matching (EffectCraft extension).
|
|
pub track_shape: bool,
|
|
}
|
|
|
|
impl Default for TrackerOptions {
|
|
fn default() -> Self {
|
|
TrackerOptions {
|
|
channel: TrackChannel::Luminance,
|
|
blur: 0.0,
|
|
enhance: false,
|
|
subpixel: true,
|
|
adapt_every_frame: false,
|
|
threshold: 80.0,
|
|
action: LowConfidence::Adapt,
|
|
track_shape: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A tracker's non-animatable settings (Tracker panel).
|
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
|
#[serde(default, rename_all = "camelCase")]
|
|
pub struct TrackerSettings {
|
|
pub kind: TrackKind,
|
|
pub position: bool,
|
|
pub rotation: bool,
|
|
pub scale: bool,
|
|
/// Motion Target (None: no target; Stabilize always applies to the tracked layer).
|
|
pub target: Option<LayerId>,
|
|
pub options: TrackerOptions,
|
|
}
|
|
|
|
impl TrackerSettings {
|
|
pub fn new(kind: TrackKind) -> TrackerSettings {
|
|
TrackerSettings { kind, position: true, ..Default::default() }
|
|
}
|
|
pub fn point_count(&self) -> usize {
|
|
self.kind.point_count(self.rotation || self.scale)
|
|
}
|
|
}
|
|
|
|
/// Initial regions of a track point (layer pixels).
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub struct PointInit {
|
|
pub center: [f64; 2],
|
|
pub feature_size: [f64; 2],
|
|
pub search_offset: [f64; 2],
|
|
pub search_size: [f64; 2],
|
|
pub attach_offset: [f64; 2],
|
|
}
|
|
|
|
pub const MOTION_TRACKERS: &str = "motionTrackers";
|
|
pub const TRACK_POINT: &str = "trackPoint";
|
|
|
|
/// The (empty) Motion Trackers group.
|
|
pub fn motion_trackers(ids: &mut Ids) -> PropGroup {
|
|
ids.group(MOTION_TRACKERS, "Motion Trackers")
|
|
}
|
|
|
|
/// One Track Point group.
|
|
pub fn track_point(ids: &mut Ids, name: &str, p: &PointInit) -> PropGroup {
|
|
let pt = |ids: &mut Ids, m: &str, n: &str, v: [f64; 2]| ids.prop(m, n, Value::Vec2(v)).with_ui(ParamUi::Point).spatial();
|
|
let vec = |ids: &mut Ids, m: &str, n: &str, v: [f64; 2]| ids.prop(m, n, Value::Vec2(v)).with_ui(ParamUi::Point);
|
|
let attach = [p.center[0] + p.attach_offset[0], p.center[1] + p.attach_offset[1]];
|
|
let fc = pt(ids, "featureCenter", "Feature Center", p.center);
|
|
let fs = vec(ids, "featureSize", "Feature Size", p.feature_size);
|
|
let so = vec(ids, "searchOffset", "Search Offset", p.search_offset);
|
|
let ss = vec(ids, "searchSize", "Search Size", p.search_size);
|
|
let cf = ids.prop("confidence", "Confidence", Value::Scalar(0.0)).with_ui(ParamUi::Percent);
|
|
let ap = pt(ids, "attachPoint", "Attach Point", attach);
|
|
let ao = vec(ids, "attachPointOffset", "Attach Point Offset", p.attach_offset);
|
|
let mut g = ids.group(TRACK_POINT, name).with(fc).with(fs).with(so).with(ss).with(cf).with(ap).with(ao);
|
|
g.kind = GroupKind::Indexed;
|
|
g
|
|
}
|
|
|
|
/// A Tracker group with its points.
|
|
pub fn tracker(ids: &mut Ids, name: &str, settings: TrackerSettings, points: &[PointInit]) -> PropGroup {
|
|
let mut g = ids.group("tracker", name);
|
|
g.kind = GroupKind::Tracker { settings: Box::new(settings) };
|
|
for (i, p) in points.iter().enumerate() {
|
|
let tp = track_point(ids, &format!("Track Point {}", i + 1), p);
|
|
g.children.push(tp.into());
|
|
}
|
|
g
|
|
}
|
|
|
|
/// Default track point layout for a tracker type, centred on a layer of size `size`.
|
|
pub fn default_points(kind: TrackKind, n: usize, size: [f64; 2]) -> Vec<PointInit> {
|
|
let m = size[0].min(size[1]).max(1.0);
|
|
let f = (m * 0.04).clamp(16.0, 80.0).round();
|
|
let s = (f * 2.0).round();
|
|
let c = [size[0] / 2.0, size[1] / 2.0];
|
|
let mk = |p: [f64; 2]| PointInit { center: p, feature_size: [f, f], search_offset: [0.0; 2], search_size: [s, s], attach_offset: [0.0; 2] };
|
|
let (dx, dy) = ((size[0] * 0.18).max(2.0 * s), (size[1] * 0.18).max(2.0 * s));
|
|
match (kind, n) {
|
|
(TrackKind::Perspective, _) | (_, 4) => {
|
|
vec![mk([c[0] - dx, c[1] - dy]), mk([c[0] + dx, c[1] - dy]), mk([c[0] - dx, c[1] + dy]), mk([c[0] + dx, c[1] + dy])]
|
|
}
|
|
(TrackKind::Affine, _) | (_, 3) => vec![mk([c[0] - dx, c[1] - dy]), mk([c[0] + dx, c[1] - dy]), mk([c[0] - dx, c[1] + dy])],
|
|
(_, 2) => vec![mk([c[0] - dx, c[1]]), mk([c[0] + dx, c[1]])],
|
|
_ => vec![mk(c)],
|
|
}
|
|
}
|
|
|
|
impl Layer {
|
|
/// The Motion Trackers group, if the layer has one.
|
|
pub fn motion_trackers(&self) -> Option<&PropGroup> {
|
|
self.props.sub(MOTION_TRACKERS)
|
|
}
|
|
/// Trackers of the layer (groups with [`GroupKind::Tracker`]).
|
|
pub fn trackers(&self) -> impl Iterator<Item = (&PropGroup, &TrackerSettings)> {
|
|
self.motion_trackers().into_iter().flat_map(|g| g.groups()).filter_map(|g| match &g.kind {
|
|
GroupKind::Tracker { settings } => Some((g, settings.as_ref())),
|
|
_ => None,
|
|
})
|
|
}
|
|
pub fn tracker(&self, uid: Uid) -> Option<(&PropGroup, &TrackerSettings)> {
|
|
self.trackers().find(|(g, _)| g.uid == uid)
|
|
}
|
|
/// The Motion Trackers group, created (first in the layer's tree, like AE) when missing.
|
|
pub fn motion_trackers_mut(&mut self, ids: &mut Ids) -> Option<&mut PropGroup> {
|
|
if self.props.sub(MOTION_TRACKERS).is_none() {
|
|
self.props.children.insert(0, motion_trackers(ids).into());
|
|
}
|
|
self.props.sub_mut(MOTION_TRACKERS)
|
|
}
|
|
}
|
|
|
|
impl PropGroup {
|
|
/// Tracker settings of a tracker group.
|
|
pub fn tracker_settings(&self) -> Option<&TrackerSettings> {
|
|
match &self.kind {
|
|
GroupKind::Tracker { settings } => Some(settings),
|
|
_ => None,
|
|
}
|
|
}
|
|
pub fn tracker_settings_mut(&mut self) -> Option<&mut TrackerSettings> {
|
|
match &mut self.kind {
|
|
GroupKind::Tracker { settings } => Some(settings),
|
|
_ => None,
|
|
}
|
|
}
|
|
/// Track Point groups of a tracker group, in order.
|
|
pub fn track_points(&self) -> impl Iterator<Item = &PropGroup> {
|
|
self.groups().filter(|g| g.match_id == TRACK_POINT)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn tracker_tree_and_serde() {
|
|
let mut n = 1;
|
|
let mut ids = Ids(&mut n);
|
|
let pts = default_points(TrackKind::Perspective, 4, [1920.0, 1080.0]);
|
|
let g = tracker(&mut ids, "Tracker 1", TrackerSettings::new(TrackKind::Perspective), &pts);
|
|
assert_eq!(g.track_points().count(), 4);
|
|
let tp = g.track_points().next().unwrap();
|
|
let names: Vec<&str> = tp.props().map(|p| p.name.as_str()).collect();
|
|
assert_eq!(names, ["Feature Center", "Feature Size", "Search Offset", "Search Size", "Confidence", "Attach Point", "Attach Point Offset"]);
|
|
let json = serde_json::to_string(&g).unwrap();
|
|
let back: PropGroup = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(back, g);
|
|
assert_eq!(back.tracker_settings().unwrap().kind, TrackKind::Perspective);
|
|
assert_eq!(TrackKind::from_name("Parallel Corner Pin"), Some(TrackKind::Affine));
|
|
assert_eq!(TrackKind::from_name("perspective"), Some(TrackKind::Perspective));
|
|
assert_eq!(TrackerSettings { rotation: true, ..TrackerSettings::new(TrackKind::Transform) }.point_count(), 2);
|
|
}
|
|
}
|