@rig-c/pose 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -9
- package/cli.ts +70 -7
- package/package.json +1 -1
- package/src/clean.ts +339 -7
- package/src/legs.ts +211 -15
- package/src/pose.ts +307 -21
- package/src/retarget.ts +351 -41
- package/src/run.ts +63 -10
- package/src/switch.ts +164 -0
- package/src/verify.ts +29 -4
package/src/pose.ts
CHANGED
|
@@ -70,16 +70,91 @@ export interface CleanSpike {
|
|
|
70
70
|
}
|
|
71
71
|
/**
|
|
72
72
|
* Who said the hips are `inferred` (#63): `declared` — the person running
|
|
73
|
-
* `rig-pose clean --hips-inferred` said so
|
|
74
|
-
*
|
|
73
|
+
* `rig-pose clean --hips-inferred` said so; `measured` (#86) — a tool read the
|
|
74
|
+
* pelvis line off the picture (`tools/pelvis.py`, after clean) and turned the
|
|
75
|
+
* hip pair to a measured angle on every frame. The exact shape:
|
|
76
|
+
*
|
|
77
|
+
* { by: "measured", method: string,
|
|
78
|
+
* read: Run[], interpolated: Run[], held: Run[],
|
|
79
|
+
* declined: Array<{ frames: Run, why: string }> } Run = [first, last]
|
|
80
|
+
*
|
|
81
|
+
* `method` names the reading. Runs are integer frame indices inside the
|
|
82
|
+
* sequence, first ≤ last, increasing and not overlapping within each list.
|
|
83
|
+
* Every frame of the sequence is in exactly one of `read` (the frame's own
|
|
84
|
+
* reading), `interpolated` (a declined frame between two reads close in time,
|
|
85
|
+
* the angle interpolated) and `held` (any other declined frame, the earlier
|
|
86
|
+
* read carried to the end of its run; at the clip's start, the later read). `declined` lists, with the reason, exactly the frames that are
|
|
87
|
+
* interpolated or held. Anything else is refused by name (POSE_CLEANED).
|
|
88
|
+
*/
|
|
89
|
+
export type Run = [number, number];
|
|
90
|
+
export type HipsInferred =
|
|
91
|
+
| { by: 'declared' }
|
|
92
|
+
| { by: 'measured'; method: string; read: Run[]; interpolated: Run[]; held: Run[]; declined: Array<{ frames: Run; why: string }> };
|
|
93
|
+
|
|
94
|
+
/** A copy of the record, in the writer's key order. */
|
|
95
|
+
export function copyHipsInferred(h: HipsInferred): HipsInferred {
|
|
96
|
+
const runs = (rs: Run[]): Run[] => rs.map(([a, b]) => [a, b]);
|
|
97
|
+
return h.by === 'declared'
|
|
98
|
+
? { by: 'declared' }
|
|
99
|
+
: { by: 'measured', method: h.method, read: runs(h.read), interpolated: runs(h.interpolated), held: runs(h.held), declined: h.declined.map((d) => ({ frames: [d.frames[0], d.frames[1]], why: d.why })) };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The frames a `measured` record gives an angle on (read, interpolated, held), as a set; null for any other record or none. */
|
|
103
|
+
export function measuredFrames(h: HipsInferred | undefined): Set<number> | null {
|
|
104
|
+
if (h === undefined || h.by !== 'measured') return null;
|
|
105
|
+
const out = new Set<number>();
|
|
106
|
+
for (const rs of [h.read, h.interpolated, h.held]) for (const [a, b] of rs) for (let f = a; f <= b; f++) out.add(f);
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The stillness hold's figures (#91): speed in trunk lengths per frame, reach in trunk lengths, the shortest hold in seconds, the blend in frames. */
|
|
111
|
+
export interface HoldParams {
|
|
112
|
+
speed: number;
|
|
113
|
+
reach: number;
|
|
114
|
+
minSeconds: number;
|
|
115
|
+
blend: number;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* One stillness hold of one joint: its frames (inclusive), and the joint's
|
|
119
|
+
* jitter inside them before and after, px/f². `after` is 0 on a hold held at
|
|
120
|
+
* its median throughout; on a hold that carries a seam's cross-fade it is the
|
|
121
|
+
* ramp's own curvature, expected and not noise (the lines print "ramp").
|
|
75
122
|
*/
|
|
76
|
-
export interface
|
|
77
|
-
|
|
123
|
+
export interface CleanHold {
|
|
124
|
+
joint: Body18;
|
|
125
|
+
frames: [number, number];
|
|
126
|
+
jitter: { before: number | null; after: number | null };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* A run of frames (`frames`, inside a region the holds cover) on which a joint
|
|
130
|
+
* has a position and no hold, with the test it fails there: `speed` (its
|
|
131
|
+
* largest median speed, trunk lengths a frame; null where its speed is not
|
|
132
|
+
* known on some frame), `reach` (its largest distance from its median over the
|
|
133
|
+
* run, trunk lengths), `short` (it passes both and the run lasts under the
|
|
134
|
+
* minimum; the figure is its seconds), or `span` (it passes all three and its
|
|
135
|
+
* spans broke elsewhere; the figure is its reach).
|
|
136
|
+
*/
|
|
137
|
+
/** Two holds of one joint meeting (back to back, or a gap of under 6 frames): the second's first frame, the step between their medians in trunk lengths, and the gap in frames (cross-faded). */
|
|
138
|
+
export interface CleanSeam {
|
|
139
|
+
joint: Body18;
|
|
140
|
+
frame: number;
|
|
141
|
+
step: number;
|
|
142
|
+
gap: number;
|
|
143
|
+
}
|
|
144
|
+
export interface CleanUnheld {
|
|
145
|
+
joint: Body18;
|
|
146
|
+
frames: [number, number];
|
|
147
|
+
test: 'speed' | 'reach' | 'short' | 'span';
|
|
148
|
+
value: number | null;
|
|
78
149
|
}
|
|
79
150
|
export interface CleanRecord {
|
|
80
151
|
spec: typeof CLEAN_SPEC;
|
|
81
|
-
/**
|
|
82
|
-
|
|
152
|
+
/**
|
|
153
|
+
* `hipsInferred` is present only when clean marked the hips (`--hips-inferred`).
|
|
154
|
+
* `hold` (#91): the stillness hold's figures, or "off"; absent in a record
|
|
155
|
+
* written before #91.
|
|
156
|
+
*/
|
|
157
|
+
params: { loop: boolean; legs: number; rest: number; hipsInferred?: HipsInferred; hold?: HoldParams | 'off' };
|
|
83
158
|
/**
|
|
84
159
|
* `found`: every joint the spike step replaced, by frame (index into `frames`)
|
|
85
160
|
* and joint. `untested`: the frames the spike step did not test, because a
|
|
@@ -88,6 +163,10 @@ export interface CleanRecord {
|
|
|
88
163
|
spikes: { found: CleanSpike[]; untested: number[] };
|
|
89
164
|
/** Mean |second difference| of y, px/f², before and after; null where no joint has three consecutive positions. */
|
|
90
165
|
jitter: { before: number | null; after: number | null };
|
|
166
|
+
/** The stillness holds (#91), per joint; present, with `unheld`, exactly when `params.hold` is an object. */
|
|
167
|
+
holds?: CleanHold[];
|
|
168
|
+
unheld?: CleanUnheld[];
|
|
169
|
+
seams?: CleanSeam[];
|
|
91
170
|
}
|
|
92
171
|
export interface PoseSequence {
|
|
93
172
|
spec: typeof POSE_SPEC;
|
|
@@ -95,7 +174,7 @@ export interface PoseSequence {
|
|
|
95
174
|
width: number;
|
|
96
175
|
height: number;
|
|
97
176
|
source: string;
|
|
98
|
-
/** Optional, only with `camera`: where it came from — "tracked",
|
|
177
|
+
/** Optional, only with `camera`: where it came from — "tracked", "hand-labelled, N keys" (#63), or "hand windows, floor-snapped: …" (#82). */
|
|
99
178
|
cameraSource?: string;
|
|
100
179
|
/** Optional: one per frame, when a scan read the camera (#63). */
|
|
101
180
|
camera?: CameraFrame[];
|
|
@@ -234,32 +313,38 @@ function readCamera(p: Problems, where: string, camera: unknown, frames: number
|
|
|
234
313
|
return out;
|
|
235
314
|
}
|
|
236
315
|
|
|
237
|
-
/** `cleaned`: { spec, params: { loop, legs, rest, hipsInferred? }, spikes: { found: [{ frame, joint }], untested: [frame] }, jitter: { before, after } }. */
|
|
316
|
+
/** `cleaned`: { spec, params: { loop, legs, rest, hipsInferred?, hold? }, spikes: { found: [{ frame, joint }], untested: [frame] }, jitter: { before, after }, holds?, unheld?, seams? }. */
|
|
238
317
|
function readCleaned(p: Problems, where: string, c: unknown, frames: number | null): CleanRecord | undefined {
|
|
239
318
|
const cw = `${where} "cleaned"`;
|
|
240
|
-
const shape = '{ spec, params: { loop, legs, rest, hipsInferred? }, spikes: { found: [{ frame, joint }], untested: [frame] }, jitter: { before, after } }';
|
|
319
|
+
const shape = '{ spec, params: { loop, legs, rest, hipsInferred?, hold? }, spikes: { found: [{ frame, joint }], untested: [frame] }, jitter: { before, after }, holds?, unheld?, seams? }';
|
|
241
320
|
if (!isObject(c)) {
|
|
242
321
|
p.add('POSE_CLEANED', `${cw} is ${typeName(c)}; it is the record rig-pose clean writes, ${shape}`);
|
|
243
322
|
return undefined;
|
|
244
323
|
}
|
|
245
324
|
const before = p.list.length;
|
|
246
|
-
refuseUnknownKeys(p, 'POSE_CLEANED', cw, c, ['spec', 'params', 'spikes', 'jitter']);
|
|
325
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', cw, c, ['spec', 'params', 'spikes', 'jitter', 'holds', 'unheld', 'seams']);
|
|
247
326
|
for (const k of ['spec', 'params', 'spikes', 'jitter']) if (!(k in c)) p.add('POSE_CLEANED', `${cw} has no "${k}"; the record is ${shape}`);
|
|
248
327
|
if ('spec' in c && c.spec !== CLEAN_SPEC) p.add('POSE_CLEANED', `${cw} "spec" is ${typeName(c.spec)}; this reader reads "${CLEAN_SPEC}"`);
|
|
249
328
|
const window = (v: unknown) => Number.isInteger(v) && (v === 1 || ((v as number) >= 5 && (v as number) % 2 === 1));
|
|
250
329
|
if ('params' in c) {
|
|
251
330
|
const q = c.params;
|
|
252
|
-
if (!isObject(q)) p.add('POSE_CLEANED', `${cw} "params" is ${typeName(q)}; it must be { loop, legs, rest, hipsInferred? }`);
|
|
331
|
+
if (!isObject(q)) p.add('POSE_CLEANED', `${cw} "params" is ${typeName(q)}; it must be { loop, legs, rest, hipsInferred?, hold? }`);
|
|
253
332
|
else {
|
|
254
|
-
refuseUnknownKeys(p, 'POSE_CLEANED', `${cw} "params"`, q, ['loop', 'legs', 'rest', 'hipsInferred']);
|
|
255
|
-
if ('
|
|
256
|
-
const h = q.
|
|
257
|
-
|
|
333
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', `${cw} "params"`, q, ['loop', 'legs', 'rest', 'hipsInferred', 'hold']);
|
|
334
|
+
if ('hold' in q) {
|
|
335
|
+
const h = q.hold;
|
|
336
|
+
const hw = `${cw} "params" "hold"`;
|
|
337
|
+
if (h === 'off') {
|
|
338
|
+
for (const k of ['holds', 'unheld', 'seams']) if (k in c) p.add('POSE_CLEANED', `${cw} has "${k}" with "params" "hold" "off"; a record lists holds only when the hold ran`);
|
|
339
|
+
} else if (!isObject(h)) p.add('POSE_CLEANED', `${hw} is ${typeName(h)}; it must be "off" or { speed, reach, minSeconds, blend }`);
|
|
258
340
|
else {
|
|
259
|
-
refuseUnknownKeys(p, 'POSE_CLEANED',
|
|
260
|
-
if (h
|
|
341
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', hw, h, ['speed', 'reach', 'minSeconds', 'blend']);
|
|
342
|
+
for (const k of ['speed', 'reach', 'minSeconds'] as const) if (!(typeof h[k] === 'number' && Number.isFinite(h[k]) && h[k] > 0)) p.add('POSE_CLEANED', `${hw} "${k}" is ${typeName(h[k])}; it must be a number > 0 (${k === 'minSeconds' ? 'seconds' : k === 'speed' ? 'trunk lengths per frame' : 'trunk lengths'})`);
|
|
343
|
+
if (!(Number.isInteger(h.blend) && (h.blend as number) >= 0)) p.add('POSE_CLEANED', `${hw} "blend" is ${typeName(h.blend)}; it must be a whole number of frames ≥ 0`);
|
|
344
|
+
for (const k of ['holds', 'unheld', 'seams']) if (!(k in c)) p.add('POSE_CLEANED', `${cw} has "params" "hold" and no "${k}"; a record whose hold ran lists its holds, the joints it did not hold and its seams (an empty array when none)`);
|
|
261
345
|
}
|
|
262
|
-
}
|
|
346
|
+
} else for (const k of ['holds', 'unheld', 'seams']) if (k in c) p.add('POSE_CLEANED', `${cw} has "${k}" and no "params" "hold"; the holds' figures are recorded in "params" "hold"`);
|
|
347
|
+
if ('hipsInferred' in q) readHipsInferred(p, `${cw} "params" "hipsInferred"`, q.hipsInferred, frames);
|
|
263
348
|
if (typeof q.loop !== 'boolean') p.add('POSE_CLEANED', `${cw} "params" "loop" is ${typeName(q.loop)}; it must be true or false`);
|
|
264
349
|
for (const k of ['legs', 'rest'] as const) if (!window(q[k])) p.add('POSE_CLEANED', `${cw} "params" "${k}" is ${typeName(q[k])}; it must be 1 (none) or an odd whole number ≥ 5`);
|
|
265
350
|
}
|
|
@@ -299,20 +384,213 @@ function readCleaned(p: Problems, where: string, c: unknown, frames: number | nu
|
|
|
299
384
|
}
|
|
300
385
|
}
|
|
301
386
|
}
|
|
387
|
+
if ('holds' in c) {
|
|
388
|
+
const hs = c.holds;
|
|
389
|
+
if (!Array.isArray(hs)) p.add('POSE_CLEANED', `${cw} "holds" is ${typeName(hs)}; it must be an array of { joint, frames: [first, last], jitter: { before, after } }`);
|
|
390
|
+
else hs.forEach((h: unknown, i: number) => {
|
|
391
|
+
const hw = `${cw} holds[${i}]`;
|
|
392
|
+
if (!isObject(h)) {
|
|
393
|
+
p.add('POSE_CLEANED', `${hw} is ${typeName(h)}; a hold is { joint, frames: [first, last], jitter: { before, after } }`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', hw, h, ['joint', 'frames', 'jitter']);
|
|
397
|
+
if (typeof h.joint !== 'string' || !isBody18(h.joint)) p.add('POSE_CLEANED', `${hw} "joint" is ${typeName(h.joint)}; it must be a body-18 joint: ${BODY18.join(', ')}`);
|
|
398
|
+
const f = h.frames;
|
|
399
|
+
if (!(Array.isArray(f) && f.length === 2 && frameOk(f[0]) && frameOk(f[1]) && (f[0] as number) <= (f[1] as number))) p.add('POSE_CLEANED', `${hw} "frames" is ${typeName(f)}; it must be [first, last], frame indices in ${frameRange}, first ≤ last`);
|
|
400
|
+
const j = h.jitter;
|
|
401
|
+
if (!isObject(j)) p.add('POSE_CLEANED', `${hw} "jitter" is ${typeName(j)}; it must be { before, after }`);
|
|
402
|
+
else {
|
|
403
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', `${hw} "jitter"`, j, ['before', 'after']);
|
|
404
|
+
for (const k of ['before', 'after'] as const) {
|
|
405
|
+
const v = j[k];
|
|
406
|
+
if (!(v === null || (typeof v === 'number' && Number.isFinite(v) && v >= 0))) p.add('POSE_CLEANED', `${hw} "jitter" "${k}" is ${typeName(v)}; it must be px/f² ≥ 0, or null (not measured)`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
if ('unheld' in c) {
|
|
412
|
+
const us = c.unheld;
|
|
413
|
+
if (!Array.isArray(us)) p.add('POSE_CLEANED', `${cw} "unheld" is ${typeName(us)}; it must be an array of { joint, frames: [first, last], test, value }`);
|
|
414
|
+
else us.forEach((u: unknown, i: number) => {
|
|
415
|
+
const uw = `${cw} unheld[${i}]`;
|
|
416
|
+
if (!isObject(u)) {
|
|
417
|
+
p.add('POSE_CLEANED', `${uw} is ${typeName(u)}; an unheld joint is { joint, frames: [first, last], test, value }`);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', uw, u, ['joint', 'frames', 'test', 'value']);
|
|
421
|
+
if (typeof u.joint !== 'string' || !isBody18(u.joint)) p.add('POSE_CLEANED', `${uw} "joint" is ${typeName(u.joint)}; it must be a body-18 joint: ${BODY18.join(', ')}`);
|
|
422
|
+
const f = u.frames;
|
|
423
|
+
if (!(Array.isArray(f) && f.length === 2 && frameOk(f[0]) && frameOk(f[1]) && (f[0] as number) <= (f[1] as number))) p.add('POSE_CLEANED', `${uw} "frames" is ${typeName(f)}; it must be [first, last], frame indices in ${frameRange}, first ≤ last`);
|
|
424
|
+
if (u.test !== 'speed' && u.test !== 'reach' && u.test !== 'short' && u.test !== 'span') p.add('POSE_CLEANED', `${uw} "test" is ${typeName(u.test)}; it must be "speed", "reach", "short" or "span"`);
|
|
425
|
+
const v = u.value;
|
|
426
|
+
const ok = typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
427
|
+
if (!(ok || (v === null && u.test === 'speed'))) p.add('POSE_CLEANED', `${uw} "value" is ${typeName(v)}; it must be a figure ≥ 0 (trunk lengths, or seconds for "short"), or null for a speed not known`);
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
if ('seams' in c) {
|
|
431
|
+
const ss = c.seams;
|
|
432
|
+
if (!Array.isArray(ss)) p.add('POSE_CLEANED', `${cw} "seams" is ${typeName(ss)}; it must be an array of { joint, frame, step, gap }`);
|
|
433
|
+
else ss.forEach((x: unknown, i: number) => {
|
|
434
|
+
const sw = `${cw} seams[${i}]`;
|
|
435
|
+
if (!isObject(x)) {
|
|
436
|
+
p.add('POSE_CLEANED', `${sw} is ${typeName(x)}; a seam is { joint, frame, step, gap }`);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', sw, x, ['joint', 'frame', 'step', 'gap']);
|
|
440
|
+
if (!(Number.isInteger(x.gap) && (x.gap as number) >= 0)) p.add('POSE_CLEANED', `${sw} "gap" is ${typeName(x.gap)}; it must be a whole number of frames ≥ 0`);
|
|
441
|
+
if (typeof x.joint !== 'string' || !isBody18(x.joint)) p.add('POSE_CLEANED', `${sw} "joint" is ${typeName(x.joint)}; it must be a body-18 joint: ${BODY18.join(', ')}`);
|
|
442
|
+
if (!frameOk(x.frame)) p.add('POSE_CLEANED', `${sw} "frame" is ${typeName(x.frame)}; it must be a frame index in ${frameRange}`);
|
|
443
|
+
if (!(typeof x.step === 'number' && Number.isFinite(x.step) && x.step >= 0)) p.add('POSE_CLEANED', `${sw} "step" is ${typeName(x.step)}; it must be trunk lengths ≥ 0`);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
302
446
|
if (p.list.length > before) return undefined;
|
|
303
|
-
const q = c.params as { loop: boolean; legs: number; rest: number; hipsInferred?: HipsInferred };
|
|
447
|
+
const q = c.params as { loop: boolean; legs: number; rest: number; hipsInferred?: HipsInferred; hold?: HoldParams | 'off' };
|
|
304
448
|
const j = c.jitter as { before: number | null; after: number | null };
|
|
305
449
|
return {
|
|
306
450
|
spec: CLEAN_SPEC,
|
|
307
|
-
params: { loop: q.loop, legs: q.legs, rest: q.rest, ...(q.hipsInferred !== undefined ? { hipsInferred: {
|
|
451
|
+
params: { loop: q.loop, legs: q.legs, rest: q.rest, ...(q.hipsInferred !== undefined ? { hipsInferred: copyHipsInferred(q.hipsInferred) } : {}), ...(q.hold !== undefined ? { hold: holdParamsOf(q.hold) } : {}) },
|
|
308
452
|
spikes: {
|
|
309
453
|
found: ((c.spikes as { found: Array<{ frame: number; joint: Body18 }> }).found).map((s) => ({ frame: s.frame, joint: s.joint })),
|
|
310
454
|
untested: [...(c.spikes as { untested: number[] }).untested],
|
|
311
455
|
},
|
|
312
456
|
jitter: { before: j.before, after: j.after },
|
|
457
|
+
...('holds' in c ? { holds: (c.holds as CleanHold[]).map(copyHold) } : {}),
|
|
458
|
+
...('unheld' in c ? { unheld: (c.unheld as CleanUnheld[]).map(copyUnheld) } : {}),
|
|
459
|
+
...('seams' in c ? { seams: (c.seams as CleanSeam[]).map((x) => ({ joint: x.joint, frame: x.frame, step: x.step, gap: x.gap })) } : {}),
|
|
313
460
|
};
|
|
314
461
|
}
|
|
315
462
|
|
|
463
|
+
/** `hipsInferred`: { by: "declared" } or the measured record (the shape at `HipsInferred`), refused by name otherwise. */
|
|
464
|
+
function readHipsInferred(p: Problems, hw: string, h: unknown, frames: number | null): void {
|
|
465
|
+
const measuredShape = '{ by: "measured", method, read, interpolated, held: [[first, last]], declined: [{ frames: [first, last], why }] }';
|
|
466
|
+
if (!isObject(h)) {
|
|
467
|
+
p.add('POSE_CLEANED', `${hw} is ${typeName(h)}; it must be { by: "declared" } or ${measuredShape}`);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (h.by === 'declared') {
|
|
471
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', hw, h, ['by']);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (h.by !== 'measured') {
|
|
475
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', hw, h, ['by']);
|
|
476
|
+
p.add('POSE_CLEANED', `${hw} "by" is ${typeName(h.by)}; this reader reads "declared" (the person running rig-pose clean --hips-inferred said so) or "measured" (a tool read the pelvis line off the picture, tools/pelvis.py)`);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const before = p.list.length;
|
|
480
|
+
const CLASSES = ['read', 'interpolated', 'held'] as const;
|
|
481
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', hw, h, ['by', 'method', ...CLASSES, 'declined']);
|
|
482
|
+
for (const k of ['method', ...CLASSES, 'declined']) if (!(k in h)) p.add('POSE_CLEANED', `${hw} has no "${k}"; a measured record is ${measuredShape}`);
|
|
483
|
+
if ('method' in h && !(typeof h.method === 'string' && h.method.trim() !== '')) p.add('POSE_CLEANED', `${hw} "method" is ${typeName(h.method)}; it must be a non-empty string naming the reading`);
|
|
484
|
+
const range = `[0, ${frames === null ? 'the frame count' : frames - 1}]`;
|
|
485
|
+
/** A run [first, last], or null after naming what is wrong with it. */
|
|
486
|
+
const run = (v: unknown, rw: string): Run | null => {
|
|
487
|
+
if (!(Array.isArray(v) && v.length === 2)) {
|
|
488
|
+
p.add('POSE_CLEANED', `${rw} is ${typeName(v)}; a run is [first, last], two frame indices`);
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
let ok = true;
|
|
492
|
+
v.forEach((f: unknown, k: number) => {
|
|
493
|
+
if (!(Number.isInteger(f) && (f as number) >= 0 && (frames === null || (f as number) < frames))) {
|
|
494
|
+
p.add('POSE_CLEANED', `${rw}[${k}] is ${typeName(f)}; it must be a frame index in ${range}`);
|
|
495
|
+
ok = false;
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
if (!ok) return null;
|
|
499
|
+
const [a, b] = v as Run;
|
|
500
|
+
if (a > b) {
|
|
501
|
+
p.add('POSE_CLEANED', `${rw} is [${a}, ${b}]; a run's first frame must not come after its last`);
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
return [a, b];
|
|
505
|
+
};
|
|
506
|
+
type Named = { r: Run; w: string };
|
|
507
|
+
/** Runs in order and apart: each starts after the previous one ends. */
|
|
508
|
+
const ordered = (rs: Named[], what: string) => {
|
|
509
|
+
for (let k = 1; k < rs.length; k++) {
|
|
510
|
+
const prev = rs[k - 1]!;
|
|
511
|
+
const cur = rs[k]!;
|
|
512
|
+
if (cur.r[0] <= prev.r[1]) p.add('POSE_CLEANED', `${cur.w} [${cur.r[0]}, ${cur.r[1]}] starts at or before the end of ${prev.w} [${prev.r[0]}, ${prev.r[1]}]; ${what} runs must increase and not overlap`);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
const classes: Record<(typeof CLASSES)[number], Named[]> = { read: [], interpolated: [], held: [] };
|
|
516
|
+
for (const c of CLASSES) {
|
|
517
|
+
if (!(c in h)) continue;
|
|
518
|
+
const list = h[c];
|
|
519
|
+
if (!Array.isArray(list)) {
|
|
520
|
+
p.add('POSE_CLEANED', `${hw} "${c}" is ${typeName(list)}; it must be an array of runs [first, last]`);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
list.forEach((v: unknown, k: number) => {
|
|
524
|
+
const w = `${hw} ${c}[${k}]`;
|
|
525
|
+
const r = run(v, w);
|
|
526
|
+
if (r !== null) classes[c].push({ r, w });
|
|
527
|
+
});
|
|
528
|
+
ordered(classes[c], c);
|
|
529
|
+
}
|
|
530
|
+
const declined: Named[] = [];
|
|
531
|
+
if ('declined' in h) {
|
|
532
|
+
if (!Array.isArray(h.declined)) p.add('POSE_CLEANED', `${hw} "declined" is ${typeName(h.declined)}; it must be an array of { frames: [first, last], why }`);
|
|
533
|
+
else h.declined.forEach((d: unknown, k: number) => {
|
|
534
|
+
const w = `${hw} declined[${k}]`;
|
|
535
|
+
if (!isObject(d)) {
|
|
536
|
+
p.add('POSE_CLEANED', `${w} is ${typeName(d)}; a declined run is { frames: [first, last], why }`);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', w, d, ['frames', 'why']);
|
|
540
|
+
if (!('why' in d)) p.add('POSE_CLEANED', `${w} has no "why"; a declined run says why, a non-empty string`);
|
|
541
|
+
else if (!(typeof d.why === 'string' && d.why.trim() !== '')) p.add('POSE_CLEANED', `${w} "why" is ${typeName(d.why)}; a declined run says why, a non-empty string`);
|
|
542
|
+
if (!('frames' in d)) {
|
|
543
|
+
p.add('POSE_CLEANED', `${w} has no "frames"; a declined run is { frames: [first, last], why }`);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const r = run(d.frames, `${w} "frames"`);
|
|
547
|
+
if (r !== null) declined.push({ r, w: `${w} "frames"` });
|
|
548
|
+
});
|
|
549
|
+
ordered(declined, 'declined');
|
|
550
|
+
}
|
|
551
|
+
// Across lists: the classes are disjoint, they cover every frame, and declined is exactly interpolated ∪ held.
|
|
552
|
+
for (let x = 0; x < CLASSES.length; x++) {
|
|
553
|
+
for (let y = x + 1; y < CLASSES.length; y++) {
|
|
554
|
+
for (const a of classes[CLASSES[x]!]) {
|
|
555
|
+
for (const b of classes[CLASSES[y]!]) {
|
|
556
|
+
if (a.r[0] <= b.r[1] && b.r[0] <= a.r[1]) p.add('POSE_CLEANED', `${a.w} [${a.r[0]}, ${a.r[1]}] and ${b.w} [${b.r[0]}, ${b.r[1]}] share frame(s) ${Math.max(a.r[0], b.r[0])}–${Math.min(a.r[1], b.r[1])}; a frame is one of read, interpolated and held`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (p.list.length > before) return;
|
|
562
|
+
const set = (rs: Named[]) => {
|
|
563
|
+
const out = new Set<number>();
|
|
564
|
+
for (const { r } of rs) for (let f = r[0]; f <= r[1]; f++) out.add(f);
|
|
565
|
+
return out;
|
|
566
|
+
};
|
|
567
|
+
const spans = (fs: number[]) => {
|
|
568
|
+
const out: Run[] = [];
|
|
569
|
+
for (const f of fs) {
|
|
570
|
+
const last = out[out.length - 1];
|
|
571
|
+
if (last !== undefined && f === last[1] + 1) last[1] = f;
|
|
572
|
+
else out.push([f, f]);
|
|
573
|
+
}
|
|
574
|
+
return out.map(([a, b]) => (a === b ? `${a}` : `${a}–${b}`)).join(', ');
|
|
575
|
+
};
|
|
576
|
+
const read = set(classes.read);
|
|
577
|
+
const filled = set([...classes.interpolated, ...classes.held]);
|
|
578
|
+
const dec = set(declined);
|
|
579
|
+
if (frames !== null) {
|
|
580
|
+
const none = Array.from({ length: frames }, (_, f) => f).filter((f) => !read.has(f) && !filled.has(f));
|
|
581
|
+
if (none.length > 0) p.add('POSE_CLEANED', `${hw}: frame(s) ${spans(none)} are in none of read, interpolated and held; every frame of the sequence is one of them`);
|
|
582
|
+
}
|
|
583
|
+
const both = [...dec].filter((f) => read.has(f)).sort((a, b) => a - b);
|
|
584
|
+
if (both.length > 0) p.add('POSE_CLEANED', `${hw}: frame(s) ${spans(both)} are both read and declined; a frame is read or declined, not both`);
|
|
585
|
+
const unfilled = [...dec].filter((f) => !read.has(f) && !filled.has(f)).sort((a, b) => a - b);
|
|
586
|
+
if (unfilled.length > 0) p.add('POSE_CLEANED', `${hw}: frame(s) ${spans(unfilled)} are declined but neither interpolated nor held; a declined frame takes one of them`);
|
|
587
|
+
const undeclared = [...filled].filter((f) => !dec.has(f)).sort((a, b) => a - b);
|
|
588
|
+
if (undeclared.length > 0) p.add('POSE_CLEANED', `${hw}: frame(s) ${spans(undeclared)} are interpolated or held but not declined; only a frame the reading declined is filled, and "declined" says why`);
|
|
589
|
+
}
|
|
590
|
+
const holdParamsOf = (h: HoldParams | 'off'): HoldParams | 'off' => (h === 'off' ? 'off' : { speed: h.speed, reach: h.reach, minSeconds: h.minSeconds, blend: h.blend });
|
|
591
|
+
const copyHold = (h: CleanHold): CleanHold => ({ joint: h.joint, frames: [h.frames[0], h.frames[1]], jitter: { before: h.jitter.before, after: h.jitter.after } });
|
|
592
|
+
const copyUnheld = (u: CleanUnheld): CleanUnheld => ({ joint: u.joint, frames: [u.frames[0], u.frames[1]], test: u.test, value: u.value });
|
|
593
|
+
|
|
316
594
|
function readJoints(p: Problems, pw: string, joints: unknown): Partial<Record<Body18, Joint>> {
|
|
317
595
|
const out: Partial<Record<Body18, Joint>> = {};
|
|
318
596
|
if (!isObject(joints)) {
|
|
@@ -499,6 +777,14 @@ export function writePoseSequence(seq: PoseSequence): string {
|
|
|
499
777
|
const camera = seq.camera?.map((c) => ({ s: round(c.s, 6), tx: round(c.tx, 3), ty: round(c.ty, 3), inliers: c.inliers }));
|
|
500
778
|
const c = seq.cleaned;
|
|
501
779
|
const jit = (v: number | null) => (v === null ? null : round(v, 4));
|
|
502
|
-
const cleaned = c === undefined ? undefined : {
|
|
780
|
+
const cleaned = c === undefined ? undefined : {
|
|
781
|
+
spec: c.spec,
|
|
782
|
+
params: { loop: c.params.loop, legs: c.params.legs, rest: c.params.rest, ...(c.params.hipsInferred !== undefined ? { hipsInferred: copyHipsInferred(c.params.hipsInferred) } : {}), ...(c.params.hold !== undefined ? { hold: holdParamsOf(c.params.hold) } : {}) },
|
|
783
|
+
spikes: { found: c.spikes.found.map((s) => ({ frame: s.frame, joint: s.joint })), untested: [...c.spikes.untested] },
|
|
784
|
+
jitter: { before: jit(c.jitter.before), after: jit(c.jitter.after) },
|
|
785
|
+
...(c.holds !== undefined ? { holds: c.holds.map((h) => ({ joint: h.joint, frames: [h.frames[0], h.frames[1]], jitter: { before: jit(h.jitter.before), after: jit(h.jitter.after) } })) } : {}),
|
|
786
|
+
...(c.seams !== undefined ? { seams: c.seams.map((x) => ({ joint: x.joint, frame: x.frame, step: round(x.step, 4), gap: x.gap })) } : {}),
|
|
787
|
+
...(c.unheld !== undefined ? { unheld: c.unheld.map((u) => ({ joint: u.joint, frames: [u.frames[0], u.frames[1]], test: u.test, value: u.value === null ? null : round(u.value, 4) })) } : {}),
|
|
788
|
+
};
|
|
503
789
|
return `${JSON.stringify({ spec: seq.spec, space: seq.space, width: seq.width, height: seq.height, source: seq.source, ...(seq.cameraSource !== undefined ? { cameraSource: seq.cameraSource } : {}), ...(camera !== undefined ? { camera } : {}), ...(cleaned !== undefined ? { cleaned } : {}), frames }, null, 2)}\n`;
|
|
504
790
|
}
|