// 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/design.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]); } // ---- eyes ---- // // Eyelid rings, under the same contract as the lip rings: ORDERED traversals // where slot position IS vertex identity. Both eyes start at the OUTER corner // and go over the UPPER lid first, so slot k means the same anatomy on both // sides. On a 16-slot ring that puts the four cardinals exactly on the four // quarter slots - 0 outer corner, 4 upper lid centre, 8 inner corner, 12 lower // lid centre - so every even vertex budget lands on real landmarks. // // The two rings traverse opposite directions on screen, because they are // mirrored anatomy described the same way. Nothing downstream cares: an // even-odd fill has no winding, and ring SIMPLICITY is what is asserted. export const EYE_R_RING = [ 33, 246, 161, 160, 159, 158, 157, 173, 133, 155, 154, 153, 145, 144, 163, 7, ]; export const EYE_L_RING = [ 263, 466, 388, 387, 386, 385, 384, 398, 362, 382, 381, 380, 374, 373, 390, 249, ]; // Outer, inner corner per eye. All four are also in RIGID, and that is the // point: the eye's reference frame is built only from landmarks that do not // move under performance, so a blink cannot be mistaken for a change of gaze. export const EYE_R_CORNERS = [33, 133]; export const EYE_L_CORNERS = [263, 362]; // Upper and lower lid centres. Their separation over the corner distance is the // openness signal that decides whether the eye is shut - the same shape of // measurement as APERTURE is for the mouth, but normalised, so one threshold // carries across takes and faces. export const EYE_R_LIDS = [159, 145]; export const EYE_L_LIDS = [386, 374]; // The two iris blocks the refined mesh appends: centre first, then four ring // points. WHICH BLOCK BELONGS TO WHICH EYE IS NOT DECLARED HERE - MediaPipe's // own "left"/"right" is viewer-relative in some docs and subject-relative in // others, and a swap looks almost right, so it would survive an eyeball and // then read as a permanently wall-eyed character. pipeline.js resolves it from // the geometry instead. export const IRIS_A = [468, 469, 470, 471, 472]; export const IRIS_B = [473, 474, 475, 476, 477]; // ---- brows ---- // // Each brow is two five-point chains, an upper edge and a lower edge, which // close into a ten-point ring: out along one edge from the outer end to the // inner, back along the other. // // WHICH EDGE IS UPPER IS DELIBERATELY NOT DECLARED, and unlike the iris it does // not need to be. Swapping them traverses the same ring the other way round, // and an even-odd fill has no winding, so the shape is identical either way. // What the ring guarantees instead is that the two ENDS land on fixed slots: // 0 and 9 are one end, 4 and 5 the other. Averaging a pair therefore gives the // brow's height at that end whichever edge is on top, which is all the raise // and tilt measurement needs. // // Which end is the OUTER one is resolved from geometry in pipeline.js, because // getting it backwards mirrors the tilt - inner-up "worried" would render as // outer-up - and that is a expression error, not a glitch, so it would read as // a directed performance choice rather than as a bug. export const BROW_A_RING = [ 70, 63, 105, 66, 107, 55, 65, 52, 53, 46, ]; export const BROW_B_RING = [ 300, 293, 334, 296, 336, 285, 295, 282, 283, 276, ]; // The slots at each end of a brow ring, as pairs to average. export const BROW_END_0 = [0, 9]; export const BROW_END_1 = [4, 5];