@rig-c/pose 0.37.1 → 0.38.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 +36 -3
- package/cli.ts +40 -1
- package/package.json +1 -1
- package/src/clean.ts +50 -3
- package/src/map.ts +24 -3
- package/src/pelvis.ts +413 -0
- package/src/pose.ts +126 -23
- package/src/problems.ts +6 -3
- package/src/retarget.ts +98 -3
- package/src/run.ts +6 -3
- package/src/switch.ts +109 -21
package/src/pose.ts
CHANGED
|
@@ -17,11 +17,30 @@
|
|
|
17
17
|
* is: the unit is `picture-px` (a video frame is not a painting), and `at` may
|
|
18
18
|
* lie outside the picture (a dancer's hand leaves the frame; a pose model
|
|
19
19
|
* places it anyway, and its score says how much to believe it).
|
|
20
|
+
*
|
|
21
|
+
* The minor (rig-c/pose#288): `rig-pose-sequence/1.1` adds the pelvis track —
|
|
22
|
+
* a per-frame `pelvisTilt` and the sequence's `pelvisSource` (`rig-pose
|
|
23
|
+
* pelvis`). The writer writes the minor the content needs: a sequence with the
|
|
24
|
+
* track is "rig-pose-sequence/1.1", every other one "rig-pose-sequence/1", so
|
|
25
|
+
* a sequence without it stays readable by a reader of /1. The spec string is
|
|
26
|
+
* the version the file records of what wrote it: a file of a minor newer than
|
|
27
|
+
* this reader's is read, and each key it does not know is refused naming the
|
|
28
|
+
* minor the file says it was written as.
|
|
20
29
|
*/
|
|
21
30
|
import { BODY18, COCO17, COCO17_TO_BODY18, isBody18, type Body18 } from './joints.ts';
|
|
22
|
-
import { isObject, parseJson, Problems, refuseUnknownKeys, typeName } from './problems.ts';
|
|
31
|
+
import { isObject, parseJson, Problems, refuseUnknownKeys, refuseUnknownKeys as refuseKeys, typeName } from './problems.ts';
|
|
23
32
|
|
|
24
33
|
export const POSE_SPEC = 'rig-pose-sequence/1';
|
|
34
|
+
/** The minor with the pelvis track (rig-c/pose#288), and the newest minor this reader reads. */
|
|
35
|
+
export const POSE_SPEC_PELVIS = 'rig-pose-sequence/1.1';
|
|
36
|
+
const POSE_MINOR = 1;
|
|
37
|
+
/**
|
|
38
|
+
* Where a sequence's `pelvisTilt` came from (rig-c/pose#288): `silhouette` — the
|
|
39
|
+
* straight edge of the drawn figure across the hips; `waistband` — the top edge
|
|
40
|
+
* of a garment covering them; `keys` — a hand file (`--pelvis-keys`).
|
|
41
|
+
*/
|
|
42
|
+
export const PELVIS_SOURCES = ['silhouette', 'waistband', 'keys'] as const;
|
|
43
|
+
export type PelvisSource = (typeof PELVIS_SOURCES)[number];
|
|
25
44
|
export const POSE_SPACE = { units: 'picture-px', origin: 'top-left', y: 'down' } as const;
|
|
26
45
|
/**
|
|
27
46
|
* `inferred` (rig-c/pose#63): a position the detector supplied but did not see
|
|
@@ -44,6 +63,15 @@ export interface Person {
|
|
|
44
63
|
export interface PoseFrame {
|
|
45
64
|
t: number;
|
|
46
65
|
people: Person[];
|
|
66
|
+
/**
|
|
67
|
+
* rig-c/pose#288, minor 1.1: the pelvis line on this frame, read off the picture —
|
|
68
|
+
* Spine degrees (counter-clockwise, y up) of the direction from the figure's
|
|
69
|
+
* right hip to its left hip, the angle `pictureAngle(r_hip, l_hip)` gives the
|
|
70
|
+
* hip keypoints (about 0 for a figure facing the camera, about ±180 facing
|
|
71
|
+
* away); null where the reading declined the frame. Present on every frame of
|
|
72
|
+
* a sequence with a `pelvisSource`, on none of one without.
|
|
73
|
+
*/
|
|
74
|
+
pelvisTilt?: number | null;
|
|
47
75
|
}
|
|
48
76
|
/**
|
|
49
77
|
* The picture's camera on one frame against frame 0's (rig-c/pose#63), as
|
|
@@ -175,6 +203,10 @@ export interface CleanUnheld {
|
|
|
175
203
|
test: 'speed' | 'reach' | 'short' | 'span';
|
|
176
204
|
value: number | null;
|
|
177
205
|
}
|
|
206
|
+
/** rig-c/pose#288: clean smoothed the sequence's `pelvisTilt` as its own track, over the legs' window (`loop` as the record's). */
|
|
207
|
+
export interface PelvisParams {
|
|
208
|
+
window: number;
|
|
209
|
+
}
|
|
178
210
|
export interface CleanRecord {
|
|
179
211
|
spec: typeof CLEAN_SPEC;
|
|
180
212
|
/**
|
|
@@ -182,7 +214,7 @@ export interface CleanRecord {
|
|
|
182
214
|
* `hold` (#91): the stillness hold's figures, or "off"; absent in a record
|
|
183
215
|
* written before #91.
|
|
184
216
|
*/
|
|
185
|
-
params: { loop: boolean; legs: number; rest: number; hipsInferred?: HipsInferred; hold?: HoldParams | 'off' };
|
|
217
|
+
params: { loop: boolean; legs: number; rest: number; hipsInferred?: HipsInferred; hold?: HoldParams | 'off'; pelvis?: PelvisParams };
|
|
186
218
|
/**
|
|
187
219
|
* `found`: every joint the spike step replaced, by frame (index into `frames`)
|
|
188
220
|
* and joint. `untested`: the frames the spike step did not test, because a
|
|
@@ -215,6 +247,8 @@ export interface PoseSequence {
|
|
|
215
247
|
* by hand. Absent: the figure's travel in the picture is its travel.
|
|
216
248
|
*/
|
|
217
249
|
travel?: Travel;
|
|
250
|
+
/** rig-c/pose#288, minor 1.1: where every frame's `pelvisTilt` came from; present exactly when the frames carry it. */
|
|
251
|
+
pelvisSource?: PelvisSource;
|
|
218
252
|
frames: PoseFrame[];
|
|
219
253
|
}
|
|
220
254
|
|
|
@@ -246,9 +280,17 @@ export function readPoseSequence(text: string, where: string): PoseSequence {
|
|
|
246
280
|
throw new Error('unreachable');
|
|
247
281
|
}
|
|
248
282
|
const frames0 = doc.spec === POSE_SPEC_FRAMES0;
|
|
249
|
-
|
|
283
|
+
// rig-c/pose#288: the minor the file says it was written as; a newer one is read, and what it adds is refused naming it.
|
|
284
|
+
const minor = frames0 ? 0 : minorOf(doc.spec);
|
|
285
|
+
const note = minor !== null && minor > POSE_MINOR ? `; the file says it was written as ${JSON.stringify(doc.spec)}, a newer minor than this reader's "${POSE_SPEC_PELVIS}" — read it with a rig-pose that reads that minor` : '';
|
|
286
|
+
const pelvisKnown = minor !== null && minor >= 1;
|
|
287
|
+
/** A 1.1 key in a file that says /1 (or the older name): one line naming the minor, not an unknown key. */
|
|
288
|
+
const minorKey = (key: string, on: string) => p.add('POSE_SPEC', `${on} carries ${JSON.stringify(key)}, a key of "${POSE_SPEC_PELVIS}", and the file says ${JSON.stringify(doc.spec)}; a sequence carrying the pelvis track is written as "${POSE_SPEC_PELVIS}" (rig-pose pelvis)`);
|
|
289
|
+
const without = (o: Record<string, unknown>, key: string) => (pelvisKnown || !(key in o) ? o : Object.fromEntries(Object.entries(o).filter(([k]) => k !== key)));
|
|
290
|
+
if (!pelvisKnown && 'pelvisSource' in doc) minorKey('pelvisSource', where);
|
|
291
|
+
refuseUnknownKeys(p, 'POSE_KEYS', where, without(doc, 'pelvisSource'), frames0 ? [...TOP_KEYS, ...OPTIONAL_KEYS, 'fps'] : [...TOP_KEYS, ...OPTIONAL_KEYS, ...(pelvisKnown ? ['pelvisSource'] : [])], note);
|
|
250
292
|
for (const k of TOP_KEYS) if (!(k in doc)) p.add('POSE_KEYS', `${where} has no "${k}"; a ${POSE_SPEC} document carries ${TOP_KEYS.join(', ')}`);
|
|
251
|
-
if ('spec' in doc &&
|
|
293
|
+
if ('spec' in doc && minor === null) p.add('POSE_SPEC', `${where} "spec" is ${typeName(doc.spec)}; this reader reads "${POSE_SPEC}" and its minors ("${POSE_SPEC_PELVIS}"; a newer minor is read, and a key it adds is refused naming it), and the older name "${POSE_SPEC_FRAMES0}"`);
|
|
252
294
|
const space = frames0 ? FRAMES0_SPACE : POSE_SPACE;
|
|
253
295
|
if ('space' in doc && JSON.stringify(doc.space) !== JSON.stringify(space)) {
|
|
254
296
|
p.add('POSE_SPACE', `${where} "space" is ${JSON.stringify(doc.space)}; for "${frames0 ? POSE_SPEC_FRAMES0 : POSE_SPEC}" it must be exactly ${JSON.stringify(space)}`);
|
|
@@ -264,13 +306,21 @@ export function readPoseSequence(text: string, where: string): PoseSequence {
|
|
|
264
306
|
p.add('POSE_FRAMES', `${where} "frames" is ${typeName(doc.frames)}; it must be a non-empty array`);
|
|
265
307
|
} else {
|
|
266
308
|
let lastT = -Infinity;
|
|
309
|
+
const minorFrames: number[] = [];
|
|
267
310
|
doc.frames.forEach((f: unknown, i: number) => {
|
|
268
311
|
const fw = `${where} frames[${i}]`;
|
|
269
312
|
if (!isObject(f)) {
|
|
270
313
|
p.add('POSE_FRAME_SHAPE', `${fw} is ${typeName(f)}; a frame is { t, people }`);
|
|
271
314
|
return;
|
|
272
315
|
}
|
|
273
|
-
|
|
316
|
+
if (!pelvisKnown && 'pelvisTilt' in f) minorFrames.push(i);
|
|
317
|
+
refuseUnknownKeys(p, 'POSE_KEYS', fw, without(f, 'pelvisTilt'), pelvisKnown ? ['t', 'people', 'pelvisTilt'] : ['t', 'people'], note);
|
|
318
|
+
let pelvisTilt: number | null | undefined;
|
|
319
|
+
if (pelvisKnown && 'pelvisTilt' in f) {
|
|
320
|
+
const v = f.pelvisTilt;
|
|
321
|
+
if (v === null || (typeof v === 'number' && Number.isFinite(v) && v > -180 && v <= 180)) pelvisTilt = v;
|
|
322
|
+
else p.add('POSE_PELVIS', `${fw} "pelvisTilt" is ${typeName(v)}; it must be Spine degrees in (-180, 180] (the direction r_hip → l_hip, counter-clockwise, y up), or null where the reading declined the frame`);
|
|
323
|
+
}
|
|
274
324
|
const t = f.t;
|
|
275
325
|
if (typeof t !== 'number' || !Number.isFinite(t) || t < 0) {
|
|
276
326
|
p.add('POSE_TIME', `${fw} "t" is ${typeName(t)}; it must be a finite number of seconds ≥ 0`);
|
|
@@ -289,34 +339,73 @@ export function readPoseSequence(text: string, where: string): PoseSequence {
|
|
|
289
339
|
p.add('POSE_PERSON_SHAPE', `${pw} is ${typeName(person)}; a person is { id, joints }`);
|
|
290
340
|
return;
|
|
291
341
|
}
|
|
292
|
-
refuseUnknownKeys(p, 'POSE_KEYS', pw, person, ['id', 'joints']);
|
|
342
|
+
refuseUnknownKeys(p, 'POSE_KEYS', pw, person, ['id', 'joints'], note);
|
|
293
343
|
if (typeof person.id !== 'string' || person.id === '') {
|
|
294
344
|
p.add('POSE_PERSON_ID', `${pw} "id" is ${typeName(person.id)}; it must be a non-empty string`);
|
|
295
345
|
return;
|
|
296
346
|
}
|
|
297
347
|
if (ids.has(person.id)) p.add('POSE_PERSON_ID', `${pw} "id" "${person.id}" appears twice in frame ${i}`);
|
|
298
348
|
ids.add(person.id);
|
|
299
|
-
people.push({ id: person.id, joints: readJoints(p, pw, person.joints) });
|
|
349
|
+
people.push({ id: person.id, joints: readJoints(p, pw, person.joints, note) });
|
|
300
350
|
});
|
|
301
351
|
}
|
|
302
|
-
frames.push({ t: typeof t === 'number' ? t : NaN, people });
|
|
352
|
+
frames.push({ t: typeof t === 'number' ? t : NaN, people, ...(pelvisTilt !== undefined ? { pelvisTilt } : {}) });
|
|
303
353
|
});
|
|
354
|
+
if (minorFrames.length > 0) minorKey('pelvisTilt', `${where} ${minorFrames.length === 1 ? `frames[${minorFrames[0]}]` : `${minorFrames.length} frames (${minorFrames[0]}–${minorFrames[minorFrames.length - 1]})`}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
let pelvisSource: PelvisSource | undefined;
|
|
358
|
+
if (pelvisKnown && 'pelvisSource' in doc) {
|
|
359
|
+
if (typeof doc.pelvisSource === 'string' && (PELVIS_SOURCES as readonly string[]).includes(doc.pelvisSource)) pelvisSource = doc.pelvisSource as PelvisSource;
|
|
360
|
+
else p.add('POSE_PELVIS', `${where} "pelvisSource" is ${typeName(doc.pelvisSource)}; it must be one of ${PELVIS_SOURCES.map((x) => `"${x}"`).join(', ')} (where every frame's "pelvisTilt" came from)`);
|
|
361
|
+
}
|
|
362
|
+
if (pelvisKnown && Array.isArray(doc.frames)) {
|
|
363
|
+
const carry = doc.frames.map((f: unknown) => isObject(f) && 'pelvisTilt' in f);
|
|
364
|
+
const lacking = carry.flatMap((c: boolean, i: number) => (c ? [] : [i]));
|
|
365
|
+
if ('pelvisSource' in doc && lacking.length > 0) p.add('POSE_PELVIS', `${where} has "pelvisSource" and frame(s) ${spansText(lacking)} carry no "pelvisTilt"; with a pelvis track every frame carries one (null where the reading declined it)`);
|
|
366
|
+
if (!('pelvisSource' in doc) && lacking.length < carry.length) p.add('POSE_PELVIS', `${where} has "pelvisTilt" on ${carry.length - lacking.length} frame(s) and no "pelvisSource"; a pelvis track says where it came from (${PELVIS_SOURCES.join(', ')})`);
|
|
367
|
+
if ('pelvisSource' in doc) {
|
|
368
|
+
const ids = [...new Set(frames.flatMap((f) => f.people.map((q) => q.id)))].sort();
|
|
369
|
+
if (ids.length > 1) p.add('POSE_PELVIS', `${where} carries a pelvis track and ${ids.length} people (${ids.join(', ')}); the track is one figure's, so the sequence holds one person (rig-pose pelvis --person writes only that one)`);
|
|
304
370
|
}
|
|
305
371
|
}
|
|
306
|
-
const camera = 'camera' in doc ? readCamera(p, where, doc.camera, Array.isArray(doc.frames) ? doc.frames.length : null) : undefined;
|
|
372
|
+
const camera = 'camera' in doc ? readCamera(p, where, doc.camera, Array.isArray(doc.frames) ? doc.frames.length : null, note) : undefined;
|
|
307
373
|
if ('cameraSource' in doc) {
|
|
308
374
|
if (typeof doc.cameraSource !== 'string' || doc.cameraSource.trim() === '') p.add('POSE_CAMERA_SOURCE', `${where} "cameraSource" is ${typeName(doc.cameraSource)}; it must be a non-empty string saying where the camera came from ("tracked", "hand-labelled, N keys")`);
|
|
309
375
|
else if (!('camera' in doc)) p.add('POSE_CAMERA_SOURCE', `${where} has "cameraSource" ${JSON.stringify(doc.cameraSource)} and no "camera"; it states where a camera came from, so it needs one`);
|
|
310
376
|
}
|
|
311
377
|
if ('travel' in doc && doc.travel !== TRAVEL_IN_PLACE) p.add('POSE_TRAVEL', `${where} "travel" is ${typeName(doc.travel)}; the one value is "${TRAVEL_IN_PLACE}" (a gait filmed without the figure travelling), and without the key the picture's travel is the figure's`);
|
|
312
|
-
const cleaned = 'cleaned' in doc ? readCleaned(p, where, doc.cleaned, Array.isArray(doc.frames) ? doc.frames.length : null) : undefined;
|
|
378
|
+
const cleaned = 'cleaned' in doc ? readCleaned(p, where, doc.cleaned, Array.isArray(doc.frames) ? doc.frames.length : null, pelvisKnown, note) : undefined;
|
|
379
|
+
if (cleaned !== undefined && pelvisKnown) {
|
|
380
|
+
const hasPelvis = isObject(doc.cleaned) && isObject(doc.cleaned.params) && 'pelvis' in doc.cleaned.params;
|
|
381
|
+
if ('pelvisSource' in doc && !hasPelvis) p.add('POSE_PELVIS', `${where} carries a pelvis track and its "cleaned" record has no "params" "pelvis"; clean smooths the track and records it, so the track was added after clean — run rig-pose pelvis before rig-pose clean`);
|
|
382
|
+
if (!('pelvisSource' in doc) && hasPelvis) p.add('POSE_PELVIS', `${where} "cleaned" "params" has "pelvis" and the sequence no "pelvisSource"; the record says clean smoothed a pelvis track the frames do not carry`);
|
|
383
|
+
}
|
|
313
384
|
p.throwIfAny();
|
|
314
385
|
const cameraSource = typeof doc.cameraSource === 'string' ? doc.cameraSource : undefined;
|
|
315
|
-
return { spec: POSE_SPEC, space: POSE_SPACE, width: doc.width as number, height: doc.height as number, source: doc.source as string, ...(cameraSource !== undefined ? { cameraSource } : {}), ...(camera !== undefined ? { camera } : {}), ...(cleaned !== undefined ? { cleaned } : {}), ...(doc.travel === TRAVEL_IN_PLACE ? { travel: TRAVEL_IN_PLACE } : {}), frames };
|
|
386
|
+
return { spec: POSE_SPEC, space: POSE_SPACE, width: doc.width as number, height: doc.height as number, source: doc.source as string, ...(cameraSource !== undefined ? { cameraSource } : {}), ...(camera !== undefined ? { camera } : {}), ...(cleaned !== undefined ? { cleaned } : {}), ...(doc.travel === TRAVEL_IN_PLACE ? { travel: TRAVEL_IN_PLACE } : {}), ...(pelvisSource !== undefined ? { pelvisSource } : {}), frames };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** The minor a sequence's spec names: 0 for "rig-pose-sequence/1", N for "rig-pose-sequence/1.N"; null for anything else. */
|
|
390
|
+
function minorOf(spec: unknown): number | null {
|
|
391
|
+
if (spec === POSE_SPEC) return 0;
|
|
392
|
+
const m = typeof spec === 'string' ? /^rig-pose-sequence\/1\.([1-9]\d{0,5})$/.exec(spec) : null;
|
|
393
|
+
return m === null ? null : Number(m[1]);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Frame indices as runs: "3, 5–9". */
|
|
397
|
+
function spansText(fs: readonly number[]): string {
|
|
398
|
+
const out: Array<[number, number]> = [];
|
|
399
|
+
for (const f of fs) {
|
|
400
|
+
const last = out[out.length - 1];
|
|
401
|
+
if (last !== undefined && f === last[1] + 1) last[1] = f;
|
|
402
|
+
else out.push([f, f]);
|
|
403
|
+
}
|
|
404
|
+
return out.map(([a, b]) => (a === b ? `${a}` : `${a}–${b}`)).join(', ');
|
|
316
405
|
}
|
|
317
406
|
|
|
318
407
|
/** `camera`: one `{ s > 0, tx, ty, inliers ≥ 0 }` per frame, as many as `frames`. */
|
|
319
|
-
function readCamera(p: Problems, where: string, camera: unknown, frames: number | null): CameraFrame[] {
|
|
408
|
+
function readCamera(p: Problems, where: string, camera: unknown, frames: number | null, note: string): CameraFrame[] {
|
|
320
409
|
const out: CameraFrame[] = [];
|
|
321
410
|
if (!Array.isArray(camera)) {
|
|
322
411
|
p.add('POSE_CAMERA', `${where} "camera" is ${typeName(camera)}; it must be an array with one { s, tx, ty, inliers } per frame`);
|
|
@@ -329,7 +418,7 @@ function readCamera(p: Problems, where: string, camera: unknown, frames: number
|
|
|
329
418
|
p.add('POSE_CAMERA', `${cw} is ${typeName(c)}; a camera entry is { s, tx, ty, inliers }`);
|
|
330
419
|
return;
|
|
331
420
|
}
|
|
332
|
-
refuseUnknownKeys(p, 'POSE_CAMERA', cw, c, CAMERA_KEYS);
|
|
421
|
+
refuseUnknownKeys(p, 'POSE_CAMERA', cw, c, CAMERA_KEYS, note);
|
|
333
422
|
let ok = true;
|
|
334
423
|
for (const k of CAMERA_KEYS) if (!(k in c)) {
|
|
335
424
|
p.add('POSE_CAMERA', `${cw} has no "${k}"; a camera entry carries ${CAMERA_KEYS.join(', ')}`);
|
|
@@ -354,7 +443,8 @@ function readCamera(p: Problems, where: string, camera: unknown, frames: number
|
|
|
354
443
|
}
|
|
355
444
|
|
|
356
445
|
/** `cleaned`: { spec, params: { loop, legs, rest, hipsInferred?, hold? }, spikes: { found: [{ frame, joint }], untested: [frame] }, jitter: { before, after }, holds?, unheld?, seams? }. */
|
|
357
|
-
function readCleaned(p: Problems, where: string, c: unknown, frames: number | null): CleanRecord | undefined {
|
|
446
|
+
function readCleaned(p: Problems, where: string, c: unknown, frames: number | null, pelvisKnown: boolean, note: string): CleanRecord | undefined {
|
|
447
|
+
const refuseUnknownKeys = (q: Problems, code: string, w: string, o: Record<string, unknown>, known: readonly string[]) => refuseKeys(q, code, w, o, known, note);
|
|
358
448
|
const cw = `${where} "cleaned"`;
|
|
359
449
|
const shape = '{ spec, params: { loop, legs, rest, hipsInferred?, hold? }, spikes: { found: [{ frame, joint }], untested: [frame] }, jitter: { before, after }, holds?, unheld?, seams? }';
|
|
360
450
|
if (!isObject(c)) {
|
|
@@ -370,7 +460,16 @@ function readCleaned(p: Problems, where: string, c: unknown, frames: number | nu
|
|
|
370
460
|
const q = c.params;
|
|
371
461
|
if (!isObject(q)) p.add('POSE_CLEANED', `${cw} "params" is ${typeName(q)}; it must be { loop, legs, rest, hipsInferred?, hold? }`);
|
|
372
462
|
else {
|
|
373
|
-
refuseUnknownKeys(p, 'POSE_CLEANED', `${cw} "params"`, q, ['loop', 'legs', 'rest', 'hipsInferred', 'hold']);
|
|
463
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', `${cw} "params"`, q, ['loop', 'legs', 'rest', 'hipsInferred', 'hold', ...(pelvisKnown ? ['pelvis'] : [])]);
|
|
464
|
+
if (pelvisKnown && 'pelvis' in q) {
|
|
465
|
+
const v = q.pelvis;
|
|
466
|
+
const vw = `${cw} "params" "pelvis"`;
|
|
467
|
+
if (!isObject(v)) p.add('POSE_CLEANED', `${vw} is ${typeName(v)}; it must be { window } (rig-c/pose#288: the window clean smoothed the pelvis track over)`);
|
|
468
|
+
else {
|
|
469
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', vw, v, ['window']);
|
|
470
|
+
if (!window(v.window)) p.add('POSE_CLEANED', `${vw} "window" is ${typeName(v.window)}; it must be 1 (none) or an odd whole number ≥ 5`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
374
473
|
if ('hold' in q) {
|
|
375
474
|
const h = q.hold;
|
|
376
475
|
const hw = `${cw} "params" "hold"`;
|
|
@@ -384,7 +483,7 @@ function readCleaned(p: Problems, where: string, c: unknown, frames: number | nu
|
|
|
384
483
|
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)`);
|
|
385
484
|
}
|
|
386
485
|
} 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"`);
|
|
387
|
-
if ('hipsInferred' in q) readHipsInferred(p, `${cw} "params" "hipsInferred"`, q.hipsInferred, frames);
|
|
486
|
+
if ('hipsInferred' in q) readHipsInferred(p, `${cw} "params" "hipsInferred"`, q.hipsInferred, frames, note);
|
|
388
487
|
if (typeof q.loop !== 'boolean') p.add('POSE_CLEANED', `${cw} "params" "loop" is ${typeName(q.loop)}; it must be true or false`);
|
|
389
488
|
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`);
|
|
390
489
|
}
|
|
@@ -490,11 +589,11 @@ function readCleaned(p: Problems, where: string, c: unknown, frames: number | nu
|
|
|
490
589
|
});
|
|
491
590
|
}
|
|
492
591
|
if (p.list.length > before) return undefined;
|
|
493
|
-
const q = c.params as { loop: boolean; legs: number; rest: number; hipsInferred?: HipsInferred; hold?: HoldParams | 'off' };
|
|
592
|
+
const q = c.params as { loop: boolean; legs: number; rest: number; hipsInferred?: HipsInferred; hold?: HoldParams | 'off'; pelvis?: PelvisParams };
|
|
494
593
|
const j = c.jitter as { before: number | null; after: number | null };
|
|
495
594
|
return {
|
|
496
595
|
spec: CLEAN_SPEC,
|
|
497
|
-
params: { loop: q.loop, legs: q.legs, rest: q.rest, ...(q.hipsInferred !== undefined ? { hipsInferred: copyHipsInferred(q.hipsInferred) } : {}), ...(q.hold !== undefined ? { hold: holdParamsOf(q.hold) } : {}) },
|
|
596
|
+
params: { loop: q.loop, legs: q.legs, rest: q.rest, ...(q.hipsInferred !== undefined ? { hipsInferred: copyHipsInferred(q.hipsInferred) } : {}), ...(q.hold !== undefined ? { hold: holdParamsOf(q.hold) } : {}), ...(pelvisKnown && q.pelvis !== undefined ? { pelvis: { window: q.pelvis.window } } : {}) },
|
|
498
597
|
spikes: {
|
|
499
598
|
found: ((c.spikes as { found: Array<{ frame: number; joint: Body18 }> }).found).map((s) => ({ frame: s.frame, joint: s.joint })),
|
|
500
599
|
untested: [...(c.spikes as { untested: number[] }).untested],
|
|
@@ -507,7 +606,8 @@ function readCleaned(p: Problems, where: string, c: unknown, frames: number | nu
|
|
|
507
606
|
}
|
|
508
607
|
|
|
509
608
|
/** `hipsInferred`: { by: "declared" } or the measured record (the shape at `HipsInferred`), refused by name otherwise. */
|
|
510
|
-
function readHipsInferred(p: Problems, hw: string, h: unknown, frames: number | null): void {
|
|
609
|
+
function readHipsInferred(p: Problems, hw: string, h: unknown, frames: number | null, note: string): void {
|
|
610
|
+
const refuseUnknownKeys = (q: Problems, code: string, w: string, o: Record<string, unknown>, known: readonly string[]) => refuseKeys(q, code, w, o, known, note);
|
|
511
611
|
const measuredShape = '{ by: "measured", method, read, interpolated, held: [[first, last]], declined: [{ frames: [first, last], why }] }';
|
|
512
612
|
if (!isObject(h)) {
|
|
513
613
|
p.add('POSE_CLEANED', `${hw} is ${typeName(h)}; it must be { by: "declared" } or ${measuredShape}`);
|
|
@@ -652,7 +752,7 @@ const holdParamsOf = (h: HoldParams | 'off'): HoldParams | 'off' => (h === 'off'
|
|
|
652
752
|
const copyHold = (h: CleanHold): CleanHold => ({ joint: h.joint, frames: [h.frames[0], h.frames[1]], ...(h.excursion !== undefined ? { excursion: [h.excursion[0], h.excursion[1]] as [number, number] } : {}), jitter: { before: h.jitter.before, after: h.jitter.after } });
|
|
653
753
|
const copyUnheld = (u: CleanUnheld): CleanUnheld => ({ joint: u.joint, frames: [u.frames[0], u.frames[1]], test: u.test, value: u.value });
|
|
654
754
|
|
|
655
|
-
function readJoints(p: Problems, pw: string, joints: unknown): Partial<Record<Body18, Joint>> {
|
|
755
|
+
function readJoints(p: Problems, pw: string, joints: unknown, note: string): Partial<Record<Body18, Joint>> {
|
|
656
756
|
const out: Partial<Record<Body18, Joint>> = {};
|
|
657
757
|
if (!isObject(joints)) {
|
|
658
758
|
p.add('POSE_JOINTS', `${pw} "joints" is ${typeName(joints)}; it must be an object keyed by body-18 joint names`);
|
|
@@ -668,7 +768,7 @@ function readJoints(p: Problems, pw: string, joints: unknown): Partial<Record<Bo
|
|
|
668
768
|
p.add('POSE_JOINT_SHAPE', `${jw} is ${typeName(j)}; a joint is { state, at?, score? }`);
|
|
669
769
|
continue;
|
|
670
770
|
}
|
|
671
|
-
refuseUnknownKeys(p, 'POSE_KEYS', jw, j, ['state', 'at', 'score']);
|
|
771
|
+
refuseUnknownKeys(p, 'POSE_KEYS', jw, j, ['state', 'at', 'score'], note);
|
|
672
772
|
const state = j.state;
|
|
673
773
|
if (typeof state !== 'string' || !(STATES as readonly string[]).includes(state)) {
|
|
674
774
|
p.add('POSE_JOINT_STATE', `${jw} "state" is ${typeName(state)}; it must be one of ${STATES.join(', ')}`);
|
|
@@ -821,6 +921,7 @@ export function writePoseSequence(seq: PoseSequence): string {
|
|
|
821
921
|
const round = (v: number, d: number) => Number(v.toFixed(d));
|
|
822
922
|
const frames = seq.frames.map((f) => ({
|
|
823
923
|
t: f.t,
|
|
924
|
+
...(f.pelvisTilt !== undefined ? { pelvisTilt: f.pelvisTilt === null ? null : round(f.pelvisTilt, 3) } : {}),
|
|
824
925
|
people: f.people.map((q) => ({
|
|
825
926
|
id: q.id,
|
|
826
927
|
joints: Object.fromEntries(
|
|
@@ -840,12 +941,14 @@ export function writePoseSequence(seq: PoseSequence): string {
|
|
|
840
941
|
const jit = (v: number | null) => (v === null ? null : round(v, 4));
|
|
841
942
|
const cleaned = c === undefined ? undefined : {
|
|
842
943
|
spec: c.spec,
|
|
843
|
-
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) } : {}) },
|
|
944
|
+
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) } : {}), ...(c.params.pelvis !== undefined ? { pelvis: { window: c.params.pelvis.window } } : {}) },
|
|
844
945
|
spikes: { found: c.spikes.found.map((s) => ({ frame: s.frame, joint: s.joint })), untested: [...c.spikes.untested] },
|
|
845
946
|
jitter: { before: jit(c.jitter.before), after: jit(c.jitter.after) },
|
|
846
947
|
...(c.holds !== undefined ? { holds: c.holds.map((h) => ({ joint: h.joint, frames: [h.frames[0], h.frames[1]], ...(h.excursion !== undefined ? { excursion: [h.excursion[0], h.excursion[1]] } : {}), jitter: { before: jit(h.jitter.before), after: jit(h.jitter.after) } })) } : {}),
|
|
847
948
|
...(c.seams !== undefined ? { seams: c.seams.map((x) => ({ joint: x.joint, frame: x.frame, step: round(x.step, 4), gap: x.gap })) } : {}),
|
|
848
949
|
...(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) })) } : {}),
|
|
849
950
|
};
|
|
850
|
-
|
|
951
|
+
// rig-c/pose#288: the minor the content needs — /1.1 with a pelvis track, /1 otherwise.
|
|
952
|
+
const spec = seq.pelvisSource !== undefined ? POSE_SPEC_PELVIS : seq.spec;
|
|
953
|
+
return `${JSON.stringify({ 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 } : {}), ...(seq.travel !== undefined ? { travel: seq.travel } : {}), ...(seq.pelvisSource !== undefined ? { pelvisSource: seq.pelvisSource } : {}), frames }, null, 2)}\n`;
|
|
851
954
|
}
|
package/src/problems.ts
CHANGED
|
@@ -59,10 +59,13 @@ export function isObject(v: unknown): v is Record<string, unknown> {
|
|
|
59
59
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Refuse the keys of `obj` a reader does not know, by name, listing the ones it does. `note`, when
|
|
64
|
+
* given, ends each line: a sequence written by a newer minor of its format says which (rig-c/pose#288).
|
|
65
|
+
*/
|
|
66
|
+
export function refuseUnknownKeys(problems: Problems, code: string, where: string, obj: Record<string, unknown>, known: readonly string[], note = ''): void {
|
|
64
67
|
for (const k of Object.keys(obj)) {
|
|
65
|
-
if (!known.includes(k)) problems.add(code, `${where} has a key this reader does not know: ${JSON.stringify(k)}; the keys it reads are ${known.join(', ')}`);
|
|
68
|
+
if (!known.includes(k)) problems.add(code, `${where} has a key this reader does not know: ${JSON.stringify(k)}; the keys it reads are ${known.join(', ')}${note}`);
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
|
package/src/retarget.ts
CHANGED
|
@@ -80,7 +80,7 @@ import type { Joint, Travel } from './pose.ts';
|
|
|
80
80
|
import { TRAVEL_IN_PLACE } from './pose.ts';
|
|
81
81
|
import { Problems, Refusal } from './problems.ts';
|
|
82
82
|
import type { ArtRange, AttachmentKey, ByHip, Raised, ReachShort, SlotTrack, Switch, SwitchOptions } from './switch.ts';
|
|
83
|
-
import { clampArt, coveredChains, isRaised, measureArt, overflow, RAISED_WRIST_EYES, RANGE_SLACK, reachLimit, runsOf, selectSwitches, twoBone } from './switch.ts';
|
|
83
|
+
import { clampArt, coveredChains, isRaised, measureArt, overflow, phaseGroupProblems, RAISED_WRIST_EYES, RANGE_SLACK, reachLimit, runsOf, selectSwitches, twoBone } from './switch.ts';
|
|
84
84
|
import type { BlendHold, Closure, Feet, Floor, LegScale, LegSegmentRatio, PathPose, Posing, Side, Stance } from './legs.ts';
|
|
85
85
|
import { figureScaleOf, freeAnkleShiftOf, holdBlend, isLegSegment, legScaleOf, legsOf, pathFrom, percentile, plantHip, posePath, rootOf, scaleSupport, shiftOver, SIDES, stanceOf, stanceRangeOf, STRAIGHT_PERCENTILE, supportsOf, tipOf, unshortenStanceShin } from './legs.ts';
|
|
86
86
|
import type { BodyReading, FaceReading, YawSetSwitch } from './face.ts';
|
|
@@ -174,6 +174,33 @@ export interface RetargetOptions extends SwitchOptions {
|
|
|
174
174
|
travel?: Travel;
|
|
175
175
|
/** The sequence was cleaned with `--loop` (its `cleaned.params.loop`, rig-c/pose#241): the head nod's detrend window wraps, the last frame running into the first. */
|
|
176
176
|
loop?: boolean;
|
|
177
|
+
/**
|
|
178
|
+
* rig-c/pose#288: the sequence's pelvis track (`pelvisTilt` per frame, null where
|
|
179
|
+
* declined, and its `pelvisSource`). The pelvis chain's bone (a chain whose
|
|
180
|
+
* joints are r_hip → l_hip) is keyed from it on every frame it reads, and from
|
|
181
|
+
* the hip line on the frames it does not; absent, from the hip line.
|
|
182
|
+
*/
|
|
183
|
+
pelvis?: { source: string; tilt: ReadonlyArray<number | null> };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* rig-c/pose#288: where the pelvis bone's rotation came from. `source` is the
|
|
188
|
+
* track's (`silhouette`, `waistband`, `keys`) or "hip line"; `bone`/`chain` the
|
|
189
|
+
* map's pelvis chain (null without one, when nothing is keyed); `swing` the
|
|
190
|
+
* peak-to-peak of its rotate keys, degrees (null when not keyed); `track` and
|
|
191
|
+
* `hipLine` how many frames each keyed; `trackSource` the track's source when the
|
|
192
|
+
* sequence carries one (also when it read no frame and the hip line was keyed).
|
|
193
|
+
*/
|
|
194
|
+
export interface PelvisReading {
|
|
195
|
+
source: string;
|
|
196
|
+
trackSource: string | null;
|
|
197
|
+
bone: string | null;
|
|
198
|
+
chain: string | null;
|
|
199
|
+
swing: number | null;
|
|
200
|
+
track: number;
|
|
201
|
+
hipLine: number;
|
|
202
|
+
/** The frames keyed from the hip line on a sequence that carries a track: the frames its reading declined. */
|
|
203
|
+
declined: Array<[number, number]>;
|
|
177
204
|
}
|
|
178
205
|
|
|
179
206
|
export interface Hold {
|
|
@@ -448,6 +475,8 @@ export interface Retarget {
|
|
|
448
475
|
nodSkipped: NodSkipped | null;
|
|
449
476
|
/** Each `reach` chain hanging below a bone the animation turns or translates (#236): its aim's departure from the pose pair's angle. */
|
|
450
477
|
reach: ReachReading[];
|
|
478
|
+
/** rig-c/pose#288: the pelvis bone's source; null when the map has no pelvis chain and the sequence no track. */
|
|
479
|
+
pelvis: PelvisReading | null;
|
|
451
480
|
}
|
|
452
481
|
|
|
453
482
|
export interface FigureScaleFrame {
|
|
@@ -809,6 +838,10 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
809
838
|
// Each mapped bone's target world angle per frame, reaches last.
|
|
810
839
|
const targets = new Map<string, number[]>();
|
|
811
840
|
const restOf = new Map<string, number>();
|
|
841
|
+
// rig-c/pose#288: the pelvis chain (one bone, joints r_hip → l_hip either way) keyed from the track where it reads.
|
|
842
|
+
const pelvisChain = map.chains.find((c) => !c.reach && c.bones.length === 1 && ((c.joints[0] === 'r_hip' && c.joints[1] === 'l_hip') || (c.joints[0] === 'l_hip' && c.joints[1] === 'r_hip'))) ?? null;
|
|
843
|
+
const track = opts.pelvis !== undefined && opts.pelvis.tilt.length === n ? opts.pelvis.tilt : null;
|
|
844
|
+
const fromTrack = pelvisChain !== null && track !== null && track.some((v) => v !== null) ? pelvisChain.bones[0]! : null;
|
|
812
845
|
for (const c of map.chains) {
|
|
813
846
|
c.bones.forEach((b, k) => {
|
|
814
847
|
restOf.set(b, restAngle(rest, b, c.aim[k]!));
|
|
@@ -817,6 +850,12 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
817
850
|
const to = series.get(c.joints[k + 1]!)!;
|
|
818
851
|
const angles: number[] = [];
|
|
819
852
|
for (let i = 0; i < n; i++) {
|
|
853
|
+
const v = b === fromTrack ? track![i] : null;
|
|
854
|
+
if (v !== null && v !== undefined) {
|
|
855
|
+
// The track is the r_hip → l_hip direction; a chain written l_hip → r_hip aims the other way.
|
|
856
|
+
angles.push(c.joints[0] === 'r_hip' ? v : wrap(v + 180));
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
820
859
|
const d = Math.hypot(to[i]![0] - from[i]![0], to[i]![1] - from[i]![1]);
|
|
821
860
|
angles.push(d > 1e-9 ? pictureAngle(from[i]!, to[i]!) : (angles[i - 1] ?? NaN));
|
|
822
861
|
}
|
|
@@ -842,6 +881,8 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
842
881
|
const joints: [string, string] = [c.joints[k]!, c.joints[k + 1]!];
|
|
843
882
|
const across = isAcross(joints[0], joints[1]);
|
|
844
883
|
if (!across && opts.short === 'off') return;
|
|
884
|
+
// rig-c/pose#288: a pelvis keyed from its track is not read off the hip pair's length, so it is not held across it.
|
|
885
|
+
if (b === fromTrack) return;
|
|
845
886
|
const from = series.get(joints[0])!;
|
|
846
887
|
const to = series.get(joints[1])!;
|
|
847
888
|
const lengths = from.map((f, i) => Math.hypot(to[i]![0] - f[0], to[i]![1] - f[1]));
|
|
@@ -1211,6 +1252,16 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1211
1252
|
if (opts.stance !== undefined && opts.stance !== 'off' && !plantOn) p.add('STANCE', `--stance ${opts.stance} plants the free foot beside the supporting one that --plant-feet holds, and --plant-feet is not given`);
|
|
1212
1253
|
if (opts.floor !== undefined && !plantOn) p.add('FLOOR', `--floor ${opts.floor} is the floor --plant-feet plants every support span on, and --plant-feet is not given`);
|
|
1213
1254
|
if (opts.floor !== undefined && !Number.isFinite(opts.floor)) p.add('FLOOR', `--floor is ${opts.floor}; it must be a finite world y`);
|
|
1255
|
+
// rig-c/pose#286: a set with a `phase` shows on the report's support spans, read with or without --plant-feet; a run that
|
|
1256
|
+
// reads no feet has none to show it on, and a group of competing sets mixing phase and angle sets has no rule.
|
|
1257
|
+
const phased = map.alternates.filter((a) => a.phase !== undefined);
|
|
1258
|
+
if (phased.length > 0) {
|
|
1259
|
+
if (feet === null) {
|
|
1260
|
+
const why = legs !== null && 'problems' in legs ? legs.problems.join('; ') : `the feet are read from both hips and both ankles, and the map's chains use ${['r_hip', 'l_hip', 'r_ankle', 'l_ankle'].filter((j) => !series.has(j)).join(', ')} nowhere`;
|
|
1261
|
+
p.add('PHASE_FEET', `set(s) ${phased.map((a) => `"${a.name}" ("phase": "${a.phase}")`).join(', ')} show by the supporting foot (left-step while the right foot supports), and this run reads no supports: ${why}`);
|
|
1262
|
+
}
|
|
1263
|
+
for (const x of phaseGroupProblems(map.alternates)) p.add('PHASE_GROUP', x);
|
|
1264
|
+
}
|
|
1214
1265
|
p.throwIfAny();
|
|
1215
1266
|
const askedScales = new Map<string, number[]>();
|
|
1216
1267
|
for (const ls of legScale) askedScales.set(ls.bone, ls.asked);
|
|
@@ -1485,7 +1536,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
1485
1536
|
}
|
|
1486
1537
|
}
|
|
1487
1538
|
const rawTargets = new Map<string, number[]>([...targets].map(([b, arr]) => [b, [...arr]]));
|
|
1488
|
-
const sw = selectSwitches(map, targets, keyTimes, keyTimes[n - 1]!, slots, { ...opts, shortInfo, handOffsets });
|
|
1539
|
+
const sw = selectSwitches(map, targets, keyTimes, keyTimes[n - 1]!, slots, { ...opts, shortInfo, handOffsets, ...(phased.length > 0 && feet !== null ? { phaseSupports: feet.supports } : {}) });
|
|
1489
1540
|
|
|
1490
1541
|
// The range clamp, outside the spans of a set depicting the bone's chain. Parents
|
|
1491
1542
|
// first, so a bone below takes its clamped parent into its key; a reach is aimed
|
|
@@ -3038,6 +3089,7 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
3038
3089
|
const reach = reachReadings(rest, map, tracks, reachAims, t?.bone ?? null);
|
|
3039
3090
|
|
|
3040
3091
|
const pivot = pivotOf(rest, t?.bone ?? null, legs);
|
|
3092
|
+
const pelvis = pelvisReadingOf(pelvisChain, opts.pelvis, track, fromTrack, tracks);
|
|
3041
3093
|
const scaleFit = t === null || scale === null || origin === null ? null : scaleFitOf({
|
|
3042
3094
|
joints: t.scale.pose, joint: t.joint, bone: t.bone, lengths: scaleLengths, factor: scale.factor,
|
|
3043
3095
|
series, good, legLength: feet?.legLength ?? null, legStraight, rigLeg,
|
|
@@ -3048,10 +3100,53 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
3048
3100
|
animation: { duration: keyTimes[n - 1]!, tracks },
|
|
3049
3101
|
times: keyTimes, targets, origin, holds, bends, short, endOn, flips, scale, scaleFit,
|
|
3050
3102
|
switches: sw.switches, shown: sw.shown, distances: sw.distances, turns, turnsUnread, range, artRange,
|
|
3051
|
-
legScale, segments, legStraight, rigLeg, scales, feet, planted: plantOn, stance, floor, blendHold, closure, openDrift, ankles, pivot, raised, byHip, face, body, figureScale, headNod: headNodReading, nodSkipped, reach,
|
|
3103
|
+
legScale, segments, legStraight, rigLeg, scales, feet, planted: plantOn, stance, floor, blendHold, closure, openDrift, ankles, pivot, raised, byHip, face, body, figureScale, headNod: headNodReading, nodSkipped, reach, pelvis,
|
|
3052
3104
|
};
|
|
3053
3105
|
}
|
|
3054
3106
|
|
|
3107
|
+
/** rig-c/pose#288: the pelvis bone's source, from the chain, the track and the keys written. */
|
|
3108
|
+
function pelvisReadingOf(chain: Chain | null, given: RetargetOptions['pelvis'], track: ReadonlyArray<number | null> | null, fromTrack: string | null, tracks: ReadonlyArray<MotionTrack | SlotTrack>): PelvisReading | null {
|
|
3109
|
+
if (chain === null && given === undefined) return null;
|
|
3110
|
+
const bone = chain?.bones[0] ?? null;
|
|
3111
|
+
const keys = bone === null ? null : tracks.find((tr): tr is MotionTrack => 'bone' in tr && tr.bone === bone && tr.property === 'rotate');
|
|
3112
|
+
const vs = keys === null || keys === undefined ? [] : keys.keys.map((k) => k.v[0]!);
|
|
3113
|
+
const swing = vs.length === 0 ? null : r3(Math.max(...vs) - Math.min(...vs));
|
|
3114
|
+
const n = vs.length;
|
|
3115
|
+
const declined: Array<[number, number]> = [];
|
|
3116
|
+
(track ?? []).forEach((v, i) => {
|
|
3117
|
+
if (v !== null) return;
|
|
3118
|
+
const last = declined[declined.length - 1];
|
|
3119
|
+
if (last !== undefined && last[1] === i - 1) last[1] = i;
|
|
3120
|
+
else declined.push([i, i]);
|
|
3121
|
+
});
|
|
3122
|
+
const onTrack = fromTrack === null ? 0 : (track ?? []).filter((v) => v !== null).length;
|
|
3123
|
+
return {
|
|
3124
|
+
source: fromTrack !== null ? given!.source : 'hip line',
|
|
3125
|
+
trackSource: given?.source ?? null,
|
|
3126
|
+
bone,
|
|
3127
|
+
chain: chain?.name ?? null,
|
|
3128
|
+
swing,
|
|
3129
|
+
track: onTrack,
|
|
3130
|
+
hipLine: bone === null ? 0 : n - onTrack,
|
|
3131
|
+
declined: given === undefined ? [] : declined,
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
/**
|
|
3136
|
+
* The PELVIS line (rig-c/pose#288): where the pelvis bone's rotation came from and
|
|
3137
|
+
* its swing — "from <source>, swing N°", or "from hip line (no track)".
|
|
3138
|
+
*/
|
|
3139
|
+
export function pelvisLine(r: PelvisReading, frames: number): string {
|
|
3140
|
+
const spans = (rs: Array<[number, number]>) => rs.map(([a, b]) => (a === b ? `${a}` : `${a}–${b}`)).join(', ');
|
|
3141
|
+
const where = r.bone === null ? '' : `bone "${r.bone}", chain "${r.chain}"`;
|
|
3142
|
+
if (r.bone === null) return `PELVIS: not keyed — the sequence carries a ${r.trackSource} track (${frames - r.declined.reduce((s, [a, b]) => s + b - a + 1, 0)} of ${frames} frames read) and the map has no pelvis chain (one bone, joints r_hip → l_hip); no bone turns with the pelvis`;
|
|
3143
|
+
const swing = r.swing === null ? '' : `, swing ${r.swing.toFixed(2)}°`;
|
|
3144
|
+
if (r.trackSource === null) return `PELVIS: from hip line (no track)${swing} (${where}; every frame from r_hip → l_hip)`;
|
|
3145
|
+
if (r.track === 0) return `PELVIS: from hip line (the ${r.trackSource} track read none of the ${frames} frames)${swing} (${where}; every frame from r_hip → l_hip)`;
|
|
3146
|
+
const rest = r.hipLine === 0 ? `all ${frames} frames from the track` : `${r.track} of ${frames} frames from the track, the hip line on the ${r.hipLine} it declined (frames ${spans(r.declined)})`;
|
|
3147
|
+
return `PELVIS: from ${r.source}${swing} (${where}; ${rest})`;
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3055
3150
|
/** A reach bone's origin within this many world units of the anchor's, with nothing above it moved, is aimed along the pair: no REACH line. */
|
|
3056
3151
|
export const REACH_OFFSET = 1e-6;
|
|
3057
3152
|
|
package/src/run.ts
CHANGED
|
@@ -13,10 +13,10 @@ import { cleanLine, cleanSummaryLine, inferredSource } from './clean.ts';
|
|
|
13
13
|
import { convertCoco17, personTrack, readPoseSequence, TRAVEL_IN_PLACE, type CameraFrame } from './pose.ts';
|
|
14
14
|
import { isObject, lineOf, parseJson, Problems, Refusal, type Problem } from './problems.ts';
|
|
15
15
|
import { boneIdentity, droppedLine, keptOutLine, mergeProblems, readFragment, readIdle, targetOf, trackLabel, type IdleReading, type RigcTrack, type Target } from './idle.ts';
|
|
16
|
-
import { bendGroupLine, bendGroups, bendLine, endOnLine, flipLine, holdLine, nodLine, rangeLine, reachCostLine, retarget, shortLine, turnLine, type MotionTrack, type RetargetOptions, type ScaleBand, type ScaleFit } from './retarget.ts';
|
|
16
|
+
import { bendGroupLine, bendGroups, bendLine, endOnLine, flipLine, holdLine, nodLine, pelvisLine, rangeLine, reachCostLine, retarget, shortLine, turnLine, type MotionTrack, type RetargetOptions, type ScaleBand, type ScaleFit } from './retarget.ts';
|
|
17
17
|
import { attachmentBones, baseMotionOf, buildInto, rigcVersion, folds, gate, readBuild, resolveRig, restOf, seamRender, slotsOf } from './rigc.ts';
|
|
18
18
|
import { partsPath, projectSeams, readSeamDoc, readSeamPairs, seamLines, type SeamPair } from './seams.ts';
|
|
19
|
-
import { artRangeLine, chainsMoving, raisedLine, RAISED_WITHIN, reachLine, switchLine, uncoveredFolds } from './switch.ts';
|
|
19
|
+
import { artRangeLine, chainsMoving, phaseLines, raisedLine, RAISED_WITHIN, reachLine, switchLine, uncoveredFolds } from './switch.ts';
|
|
20
20
|
import { jitterLine, landed, type FeetReading, type JitterRow, type Landing, measureFeet, measureJitter, measureLanding, measurePosture, measureShear, poseFrames, postureLines } from './verify.ts';
|
|
21
21
|
import { legsOf, PLANT_SWITCH, STANCE_BOUNDS } from './legs.ts';
|
|
22
22
|
import { M2_IDENTITY, mulM2, reduceKeys, reduceScale, reduceUnderScale, SCALE_REDUCE, setupLocal, smoothTrunk, type M2, type ReduceBone, type Reduced, type ReduceRow, type UnderBone } from './reduce.ts';
|
|
@@ -151,7 +151,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
151
151
|
const slots = slotsOf(doc0);
|
|
152
152
|
resolveMap(map, rest, a.map, slots, input.sets);
|
|
153
153
|
|
|
154
|
-
const r = retarget(rest, map, frames, times, { ...a, ...(seq.travel !== undefined ? { travel: seq.travel } : {}), ...(seq.cleaned!.params.loop ? { loop: true } : {}) }, slots, input.sets);
|
|
154
|
+
const r = retarget(rest, map, frames, times, { ...a, ...(seq.travel !== undefined ? { travel: seq.travel } : {}), ...(seq.cleaned!.params.loop ? { loop: true } : {}), ...(seq.pelvisSource !== undefined ? { pelvis: { source: seq.pelvisSource, tilt: seq.frames.map((f) => f.pelvisTilt ?? null) } } : {}) }, slots, input.sets);
|
|
155
155
|
if (fragment.length > 0 && ((r.face?.sets?.length ?? 0) > 0 || (r.body?.sets?.length ?? 0) > 0)) {
|
|
156
156
|
const known = knownSetsOf(map, input.sets);
|
|
157
157
|
for (const s of [...(r.face?.sets ?? []), ...(r.body?.sets ?? [])]) {
|
|
@@ -196,6 +196,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
196
196
|
if (r.switches.length === 0) say(` SWITCH none — no set within ${a.switchTolerance}° of the pose for ${a.switchMin}s`);
|
|
197
197
|
for (const s of r.switches) say(` SWITCH ${switchLine(s, r.times)}`);
|
|
198
198
|
for (const l of windowLines(map, r.switches, r.distances, r.times, a.switchTolerance)) say(` FRAMES ${l}`);
|
|
199
|
+
for (const l of phaseLines(map, r.switches)) say(` PHASE ${l}`);
|
|
199
200
|
}
|
|
200
201
|
for (const e of r.range) say(` RANGE ${rangeLine(e, r.times)}`);
|
|
201
202
|
if (a.raisedByHead === true && r.raised.length === 0) say(` RAISED none — no set drawing an arm within ${RAISED_WITHIN}° of straight up showed; no key moved`);
|
|
@@ -208,6 +209,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
208
209
|
for (const e of r.artRange) say(` ART-RANGE ${artRangeLine(e, r.times)}`);
|
|
209
210
|
for (const l of r.legScale) say(` LEG-SCALE ${legScaleLine(l, r.times, r.feet?.legLength ?? null, r.rigLeg)}`);
|
|
210
211
|
for (const l of r.legScale) for (const s of l.support ?? []) say(` LEG-SCALE ${supportScaleLine(l, s, r.times)}`);
|
|
212
|
+
if (r.pelvis !== null) say(` ${pelvisLine(r.pelvis, seq.frames.length)}`);
|
|
211
213
|
if (r.pivot !== null) say(` PIVOT bone "${r.pivot.bone}"'s origin is ${r.pivot.distance.toFixed(2)} units (${r.pivot.hipWidths.toFixed(3)} hip widths) from its thigh roots' midpoint (${r.pivot.roots.join(' + ')}): dx ${r.pivot.offset[0].toFixed(2)}, dy ${r.pivot.offset[1].toFixed(2)}, at setup — the pivot every hip turn swings the legs about`);
|
|
212
214
|
if (r.feet !== null) {
|
|
213
215
|
const f = r.feet;
|
|
@@ -441,6 +443,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
441
443
|
},
|
|
442
444
|
legScale: r.legScale.map(({ scale: _scale, asked: _asked, picture: _picture, ...rest }) => rest),
|
|
443
445
|
pivot: r.pivot,
|
|
446
|
+
pelvis: r.pelvis,
|
|
444
447
|
stance: r.stance === null ? null : { tolerance: r.stance.tolerance, release: r.stance.release, minSeconds: r.stance.minSeconds, bounds: r.stance.bounds, ...(r.stance.range === undefined ? {} : { range: r.stance.range }), runs: r.stance.runs, over: r.stance.over },
|
|
445
448
|
feet: r.feet === null ? null : { planted: r.planted, legLength: r.feet.legLength, straight: r.legStraight, rigLeg: r.rigLeg, segments: r.segments, supports: r.feet.supports, jumps: r.feet.jumps, steps: r.feet.steps, changes: r.feet.changes, ...(r.feet.lowered === undefined ? {} : { lowered: r.feet.lowered }), ...(r.feet.tracked === undefined ? {} : { tracked: r.feet.tracked }), floor: r.floor?.y ?? null, shifts: r.floor?.shifts ?? [], blend: r.blendHold === null ? null : { bounds: r.blendHold.bounds, ...(r.blendHold.range === undefined ? {} : { range: r.blendHold.range }), runs: r.blendHold.runs, over: r.blendHold.over }, reading: feetReading, closure: r.closure },
|
|
446
449
|
shear,
|