arthur/js/landmarks.js
Your Name 7a6bdea157 roto: video -> take file builder with interactive tuning
Analysis half of the pipeline in docs/roto-puppet.md. Stabilises a face out
of a clip via a similarity fit on rigid landmarks, reduces the lip contour to
a fixed vertex budget, selects sparse keys on velocity minima, and previews
the result as flat indexed fills so timing can be judged without an Animator
Pro render.

- landmarks.js  ordered lip/oval rings; slot position is vertex identity
- mathutil.js   closed-form 2D similarity, Procrustes mean, transform smoothing
- pipeline.js   stabilise -> subsample -> key-select
- raster.js     indexed scanline fill, no antialiasing
- take.js       take-file writer
- selftest.js   29 assertions, incl. ring simplicity at every vertex budget

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 14:38:07 -04:00

54 lines
2.3 KiB
JavaScript

// MediaPipe FaceLandmarker index tables.
// Ring arrays are ORDERED traversals, not raw connection sets: vertex position
// within a ring is the vertex's identity, and every downstream stage depends on
// that ordering being stable. See docs/roto-puppet.md, "Fixed topology".
// Rigid landmarks for the similarity fit. Eye corners, nose bridge, nose tip.
// Nothing here may be a feature that moves under performance: including the
// mouth or brows bleeds performance into the stabilization.
export const RIGID = [33, 133, 362, 263, 168, 6, 1];
// Outer lip ring, clockwise from the right corner over the top.
// index 0 = right corner, 5 = top centre, 10 = left corner, 15 = bottom centre.
export const LIPS_OUTER = [
61, 185, 40, 39, 37, 0, 267, 269, 270, 409,
291, 375, 321, 405, 314, 17, 84, 181, 91, 146,
];
// Inner lip ring, same orientation and the same four cardinal positions.
export const LIPS_INNER = [
78, 191, 80, 81, 82, 13, 312, 311, 310, 415,
308, 324, 318, 402, 317, 14, 87, 178, 88, 95,
];
// Inner upper / lower lip centres. Their separation is the aperture signal that
// decides whether the mouth interior is present at all.
export const APERTURE = [13, 14];
// Face oval, used only to derive the placeholder plate in v1.
export const FACE_OVAL = [
10, 338, 297, 332, 284, 251, 389, 356, 454, 323, 361, 288,
397, 365, 379, 378, 400, 377, 152, 148, 176, 149, 150, 136,
172, 58, 132, 93, 234, 127, 162, 21, 54, 103, 67, 109,
];
// Eye corners, for the calibration box and for reporting fit residual.
export const EYE_INNER = [133, 362];
// Pick `n` slots from a ring of `len` by even spacing. Returns RING POSITIONS,
// not landmark ids: positions are the vertex identity downstream, and mapping ids
// back to positions with indexOf would silently pick the wrong slot if a table
// ever repeated an id.
//
// For even n this naturally lands on the cardinal positions (corners and lip
// centres) of a 20-point ring. Fixed indices, never adaptive decimation: the
// vertex at slot k means the same thing on every frame of the shot.
export function subsampleSlots(len, n) {
const out = [];
for (let k = 0; k < n; k++) out.push(Math.round((k * len) / n) % len);
return out;
}
export function subsampleRing(ring, n) {
return subsampleSlots(ring.length, n).map((s) => ring[s]);
}