Frame removal for plates; mouth keeps every frame
Sparseness was being applied for two different reasons at once. Aesthetic sparseness is set by the extraction rate; labour sparseness only binds on the plate, because a human draws each one. The mouth is traced and therefore free, and in limited animation lip sync is routinely the densest element - on 1s while heads hold on 2s and 3s. So: the mouth gets a key on every frame, and the frame strip is now the editing surface for deciding which frames need their own plate drawing. All frames start kept; delete the ones you don't want. - strip of face-cropped thumbnails, keep/drop per frame, keyboard driven - worksheet panel lists the drawings needed and the range each one holds - Suggest runs error-tolerance decimation on head pose as a starting point - export writes sparse plate keys + dense mouth keys, with a hold manifest - smoothContours: bounded exception to "never smooth the contour", which held only while keys were sparse enough to reject detector noise by sampling - averages are now a RADIUS in frames: 0 is off, 1 is +-1 - GPU delegate falls back to CPU instead of failing - #synth / #frames autorun for headless smoke tests Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7a6bdea157
commit
a082208ad5
7 changed files with 489 additions and 265 deletions
|
|
@ -74,9 +74,12 @@ export function procrustesMean(framesRigid, iters = 3) {
|
|||
return ref;
|
||||
}
|
||||
|
||||
function movingAverage(vals, win) {
|
||||
if (win <= 1) return vals.slice();
|
||||
const half = Math.floor(win / 2), out = new Array(vals.length);
|
||||
// `radius` is in frames either side: 0 is off, 1 averages over 3 frames, 2 over
|
||||
// 5. Expressed as a radius rather than a window so that "off" is 0 and every
|
||||
// value is symmetric - an even window would be lopsided in time.
|
||||
function movingAverage(vals, radius) {
|
||||
if (radius <= 0) return vals.slice();
|
||||
const half = Math.floor(radius), out = new Array(vals.length);
|
||||
for (let i = 0; i < vals.length; i++) {
|
||||
let acc = 0, cnt = 0;
|
||||
for (let j = i - half; j <= i + half; j++) {
|
||||
|
|
@ -95,12 +98,12 @@ export { movingAverage };
|
|||
// where the low-pass belongs; smoothing the contour would destroy the
|
||||
// performance, which is the entire asset.
|
||||
// Angles are smoothed as (cos, sin) so wrapping cannot produce a spike.
|
||||
export function smoothTransforms(tfs, win) {
|
||||
const c = movingAverage(tfs.map((t) => Math.cos(t.theta)), win);
|
||||
const sn = movingAverage(tfs.map((t) => Math.sin(t.theta)), win);
|
||||
const s = movingAverage(tfs.map((t) => t.s), win);
|
||||
const tx = movingAverage(tfs.map((t) => t.tx), win);
|
||||
const ty = movingAverage(tfs.map((t) => t.ty), win);
|
||||
export function smoothTransforms(tfs, radius) {
|
||||
const c = movingAverage(tfs.map((t) => Math.cos(t.theta)), radius);
|
||||
const sn = movingAverage(tfs.map((t) => Math.sin(t.theta)), radius);
|
||||
const s = movingAverage(tfs.map((t) => t.s), radius);
|
||||
const tx = movingAverage(tfs.map((t) => t.tx), radius);
|
||||
const ty = movingAverage(tfs.map((t) => t.ty), radius);
|
||||
return tfs.map((_, i) => ({
|
||||
theta: Math.atan2(sn[i], c[i]), s: s[i], tx: tx[i], ty: ty[i],
|
||||
}));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue