arthur/js/synth.js

70 lines
3.2 KiB
JavaScript
Raw Normal View History

// Synthetic landmark frames, shaped exactly like FaceLandmarker output.
//
// Exists so the whole chain downstream of detection - Procrustes, smoothing,
// stabilisation, key selection, rasterising, take writing - can be exercised and
// verified without a video file. A synthetic face is also the only way to test
// stabilisation against a KNOWN head motion, since real footage gives no ground
// truth to compare against.
import { LIPS_OUTER, LIPS_INNER, FACE_OVAL, RIGID, EYE_INNER } from './landmarks.js';
const NUM = 478;
export function synthDense(nFrames = 72) {
const frames = [];
for (let t = 0; t < nFrames; t++) {
const pts = new Array(NUM);
for (let i = 0; i < NUM; i++) pts[i] = { x: 0.5, y: 0.5, z: 0 };
// Known head motion: drift, sway, roll and a slow scale change, plus a
// little per-frame jitter so transform smoothing has something to remove.
const ph = t / nFrames;
const hx = 0.5 + 0.045 * Math.sin(ph * Math.PI * 2) + (Math.random() - 0.5) * 0.002;
const hy = 0.5 + 0.02 * Math.cos(ph * Math.PI * 3) + (Math.random() - 0.5) * 0.002;
const roll = 0.18 * Math.sin(ph * Math.PI * 2.5);
const scale = 1 + 0.06 * Math.sin(ph * Math.PI * 1.5);
const cr = Math.cos(roll), sr = Math.sin(roll);
const place = (i, lx, ly) => {
const sx = lx * scale, sy = ly * scale;
pts[i] = { x: hx + cr * sx - sr * sy, y: hy + sr * sx + cr * sy, z: 0 };
};
// Mouth opens in four sustained beats with holds between, so key selection
// has genuine extremes and genuine plateaux to find.
const beat = Math.floor(t / 9) % 4;
const target = [0.004, 0.05, 0.022, 0.0];
const openAmt = target[beat];
const wide = 0.10 + (beat === 1 ? 0.012 : beat === 3 ? -0.008 : 0);
place(RIGID[0], -0.075, -0.045); place(RIGID[1], -0.028, -0.043);
place(RIGID[2], 0.028, -0.043); place(RIGID[3], 0.075, -0.045);
place(RIGID[4], 0.000, -0.050); place(RIGID[5], 0.000, -0.020);
place(RIGID[6], 0.000, 0.012);
place(EYE_INNER[0], -0.028, -0.043); place(EYE_INNER[1], 0.028, -0.043);
// Lip rings as ellipse arcs, traversed so ring ORDER matches the tables:
// slot 0 = right corner, 5 = top centre, 10 = left corner, 15 = bottom
// centre, with y growing downward. Getting this convention wrong swaps two
// opposite vertices and the ring self-intersects into a bowtie - see the
// ring-simplicity assertion in selftest.
const ring = (table, rx, ry, cy) => {
const n = table.length;
for (let k = 0; k < n; k++) {
const a = -(k / n) * Math.PI * 2;
place(table[k], rx * Math.cos(a), cy + ry * Math.sin(a));
}
};
ring(LIPS_OUTER, wide / 2, 0.012 + openAmt * 0.6, 0.075);
// APERTURE (13, 14) are slots 5 and 15 of the inner ring, so the ring itself
// places them at the vertical extremes. Writing them again afterwards is what
// produced the bowtie; the aperture is simply the inner ring's height.
ring(LIPS_INNER, wide / 2.6, 0.001 + openAmt, 0.075);
for (let k = 0; k < FACE_OVAL.length; k++) {
const a = -Math.PI / 2 + (k / FACE_OVAL.length) * Math.PI * 2;
place(FACE_OVAL[k], 0.105 * Math.cos(a), 0.145 * Math.sin(a) + 0.01);
}
frames.push(pts);
}
return frames;
}