@rig-c/pose 0.38.0 → 0.39.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 +40 -6
- package/cli.ts +33 -5
- package/package.json +1 -1
- package/src/clean.ts +24 -2
- package/src/depth.ts +190 -0
- package/src/legs.ts +189 -0
- package/src/outdir.ts +54 -0
- package/src/pelvis.ts +100 -15
- package/src/pose.ts +150 -18
- package/src/run.ts +74 -18
- package/src/runcompare.ts +6 -7
- package/src/verify.ts +90 -9
package/src/pose.ts
CHANGED
|
@@ -26,14 +26,57 @@
|
|
|
26
26
|
* the version the file records of what wrote it: a file of a minor newer than
|
|
27
27
|
* this reader's is read, and each key it does not know is refused naming the
|
|
28
28
|
* minor the file says it was written as.
|
|
29
|
+
*
|
|
30
|
+
* `rig-pose-sequence/1.2` (rig-c/pose#281) adds derived depth: a per-frame
|
|
31
|
+
* `depth`, one `{ ratio, sign, source }` per limb segment, written by `clean
|
|
32
|
+
* --depth walk|run` (`source: "derived"`) or by a 3D detector (`"measured"`,
|
|
33
|
+
* read and never written here). A minor includes the keys of the minors before
|
|
34
|
+
* it, so a sequence with depth is "/1.2" whether or not it also carries the
|
|
35
|
+
* pelvis track; one with the track and no depth stays "/1.1".
|
|
29
36
|
*/
|
|
30
37
|
import { BODY18, COCO17, COCO17_TO_BODY18, isBody18, type Body18 } from './joints.ts';
|
|
31
38
|
import { isObject, parseJson, Problems, refuseUnknownKeys, refuseUnknownKeys as refuseKeys, typeName } from './problems.ts';
|
|
32
39
|
|
|
33
40
|
export const POSE_SPEC = 'rig-pose-sequence/1';
|
|
34
|
-
/** The minor with the pelvis track (rig-c/pose#288)
|
|
41
|
+
/** The minor with the pelvis track (rig-c/pose#288). */
|
|
35
42
|
export const POSE_SPEC_PELVIS = 'rig-pose-sequence/1.1';
|
|
36
|
-
|
|
43
|
+
/** The minor with derived depth (rig-c/pose#281), and the newest minor this reader reads. */
|
|
44
|
+
export const POSE_SPEC_DEPTH = 'rig-pose-sequence/1.2';
|
|
45
|
+
const POSE_MINOR = 2;
|
|
46
|
+
/**
|
|
47
|
+
* The limb segments a frame's `depth` names (rig-c/pose#281), each from its
|
|
48
|
+
* proximal joint to its distal one: the sign is + when the distal joint is
|
|
49
|
+
* nearer the camera than the proximal one.
|
|
50
|
+
*/
|
|
51
|
+
export const DEPTH_SEGMENTS = {
|
|
52
|
+
r_upper_arm: ['r_shoulder', 'r_elbow'],
|
|
53
|
+
r_forearm: ['r_elbow', 'r_wrist'],
|
|
54
|
+
r_thigh: ['r_hip', 'r_knee'],
|
|
55
|
+
r_shin: ['r_knee', 'r_ankle'],
|
|
56
|
+
l_upper_arm: ['l_shoulder', 'l_elbow'],
|
|
57
|
+
l_forearm: ['l_elbow', 'l_wrist'],
|
|
58
|
+
l_thigh: ['l_hip', 'l_knee'],
|
|
59
|
+
l_shin: ['l_knee', 'l_ankle'],
|
|
60
|
+
} as const satisfies Record<string, readonly [Body18, Body18]>;
|
|
61
|
+
export type DepthSegment = keyof typeof DEPTH_SEGMENTS;
|
|
62
|
+
export const DEPTH_SEGMENT_NAMES = Object.keys(DEPTH_SEGMENTS) as DepthSegment[];
|
|
63
|
+
/** Where a segment's depth came from: read off the picture's 2D lengths (`clean --depth`), or a 3D detector's. */
|
|
64
|
+
export const DEPTH_SOURCES = ['derived', 'measured'] as const;
|
|
65
|
+
export type DepthSource = (typeof DEPTH_SOURCES)[number];
|
|
66
|
+
/**
|
|
67
|
+
* One segment's depth on one frame (rig-c/pose#281): `ratio` its drawn length ÷
|
|
68
|
+
* its rest length; `sign` +1 when its distal joint is nearer the camera than its
|
|
69
|
+
* proximal one, −1 farther, 0 flat (or not decided); `source` as above.
|
|
70
|
+
*/
|
|
71
|
+
export interface SegmentDepth {
|
|
72
|
+
ratio: number;
|
|
73
|
+
sign: -1 | 0 | 1;
|
|
74
|
+
source: DepthSource;
|
|
75
|
+
}
|
|
76
|
+
export type FrameDepth = Partial<Record<DepthSegment, SegmentDepth>>;
|
|
77
|
+
/** The gait `clean --depth` was told the clip is: it decides the sign of a foreshortened shin. */
|
|
78
|
+
export const DEPTH_GAITS = ['walk', 'run'] as const;
|
|
79
|
+
export type DepthGait = (typeof DEPTH_GAITS)[number];
|
|
37
80
|
/**
|
|
38
81
|
* Where a sequence's `pelvisTilt` came from (rig-c/pose#288): `silhouette` — the
|
|
39
82
|
* straight edge of the drawn figure across the hips; `waistband` — the top edge
|
|
@@ -72,6 +115,12 @@ export interface PoseFrame {
|
|
|
72
115
|
* a sequence with a `pelvisSource`, on none of one without.
|
|
73
116
|
*/
|
|
74
117
|
pelvisTilt?: number | null;
|
|
118
|
+
/**
|
|
119
|
+
* rig-c/pose#281, minor 1.2: each limb segment's depth on this frame (a segment
|
|
120
|
+
* whose joints are not both placed on the frame is not named). Present on every
|
|
121
|
+
* frame of a sequence with depth, on none of one without.
|
|
122
|
+
*/
|
|
123
|
+
depth?: FrameDepth;
|
|
75
124
|
}
|
|
76
125
|
/**
|
|
77
126
|
* The picture's camera on one frame against frame 0's (rig-c/pose#63), as
|
|
@@ -207,6 +256,10 @@ export interface CleanUnheld {
|
|
|
207
256
|
export interface PelvisParams {
|
|
208
257
|
window: number;
|
|
209
258
|
}
|
|
259
|
+
/** rig-c/pose#281: clean derived each frame's `depth` (`source: "derived"`) for this gait. */
|
|
260
|
+
export interface DepthParams {
|
|
261
|
+
gait: DepthGait;
|
|
262
|
+
}
|
|
210
263
|
export interface CleanRecord {
|
|
211
264
|
spec: typeof CLEAN_SPEC;
|
|
212
265
|
/**
|
|
@@ -214,7 +267,7 @@ export interface CleanRecord {
|
|
|
214
267
|
* `hold` (#91): the stillness hold's figures, or "off"; absent in a record
|
|
215
268
|
* written before #91.
|
|
216
269
|
*/
|
|
217
|
-
params: { loop: boolean; legs: number; rest: number; hipsInferred?: HipsInferred; hold?: HoldParams | 'off'; pelvis?: PelvisParams };
|
|
270
|
+
params: { loop: boolean; legs: number; rest: number; hipsInferred?: HipsInferred; hold?: HoldParams | 'off'; pelvis?: PelvisParams; depth?: DepthParams };
|
|
218
271
|
/**
|
|
219
272
|
* `found`: every joint the spike step replaced, by frame (index into `frames`)
|
|
220
273
|
* and joint. `untested`: the frames the spike step did not test, because a
|
|
@@ -282,15 +335,20 @@ export function readPoseSequence(text: string, where: string): PoseSequence {
|
|
|
282
335
|
const frames0 = doc.spec === POSE_SPEC_FRAMES0;
|
|
283
336
|
// 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
337
|
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 "${
|
|
338
|
+
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_DEPTH}" — read it with a rig-pose that reads that minor` : '';
|
|
286
339
|
const pelvisKnown = minor !== null && minor >= 1;
|
|
287
|
-
|
|
288
|
-
const
|
|
340
|
+
// rig-c/pose#281: depth is minor 2's.
|
|
341
|
+
const depthKnown = minor !== null && minor >= 2;
|
|
342
|
+
/** A key of a later minor in a file that says an earlier one: one line naming the minor, not an unknown key. */
|
|
343
|
+
const minorKey = (key: string, on: string) => (key === 'depth'
|
|
344
|
+
? p.add('POSE_SPEC', `${on} carries "depth", a key of "${POSE_SPEC_DEPTH}", and the file says ${JSON.stringify(doc.spec)}; a sequence carrying depth is written as "${POSE_SPEC_DEPTH}" (rig-pose clean --depth)`)
|
|
345
|
+
: 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
346
|
const without = (o: Record<string, unknown>, key: string) => (pelvisKnown || !(key in o) ? o : Object.fromEntries(Object.entries(o).filter(([k]) => k !== key)));
|
|
347
|
+
const withoutDepth = (o: Record<string, unknown>) => (depthKnown || !('depth' in o) ? o : Object.fromEntries(Object.entries(o).filter(([k]) => k !== 'depth')));
|
|
290
348
|
if (!pelvisKnown && 'pelvisSource' in doc) minorKey('pelvisSource', where);
|
|
291
349
|
refuseUnknownKeys(p, 'POSE_KEYS', where, without(doc, 'pelvisSource'), frames0 ? [...TOP_KEYS, ...OPTIONAL_KEYS, 'fps'] : [...TOP_KEYS, ...OPTIONAL_KEYS, ...(pelvisKnown ? ['pelvisSource'] : [])], note);
|
|
292
350
|
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(', ')}`);
|
|
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}"`);
|
|
351
|
+
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}", "${POSE_SPEC_DEPTH}"; a newer minor is read, and a key it adds is refused naming it), and the older name "${POSE_SPEC_FRAMES0}"`);
|
|
294
352
|
const space = frames0 ? FRAMES0_SPACE : POSE_SPACE;
|
|
295
353
|
if ('space' in doc && JSON.stringify(doc.space) !== JSON.stringify(space)) {
|
|
296
354
|
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)}`);
|
|
@@ -307,6 +365,7 @@ export function readPoseSequence(text: string, where: string): PoseSequence {
|
|
|
307
365
|
} else {
|
|
308
366
|
let lastT = -Infinity;
|
|
309
367
|
const minorFrames: number[] = [];
|
|
368
|
+
const depthFrames: number[] = [];
|
|
310
369
|
doc.frames.forEach((f: unknown, i: number) => {
|
|
311
370
|
const fw = `${where} frames[${i}]`;
|
|
312
371
|
if (!isObject(f)) {
|
|
@@ -314,7 +373,9 @@ export function readPoseSequence(text: string, where: string): PoseSequence {
|
|
|
314
373
|
return;
|
|
315
374
|
}
|
|
316
375
|
if (!pelvisKnown && 'pelvisTilt' in f) minorFrames.push(i);
|
|
317
|
-
|
|
376
|
+
if (!depthKnown && 'depth' in f) depthFrames.push(i);
|
|
377
|
+
refuseUnknownKeys(p, 'POSE_KEYS', fw, withoutDepth(without(f, 'pelvisTilt')), [...(pelvisKnown ? ['t', 'people', 'pelvisTilt'] : ['t', 'people']), ...(depthKnown ? ['depth'] : [])], note);
|
|
378
|
+
const depth = depthKnown && 'depth' in f ? readDepth(p, fw, f.depth) : undefined;
|
|
318
379
|
let pelvisTilt: number | null | undefined;
|
|
319
380
|
if (pelvisKnown && 'pelvisTilt' in f) {
|
|
320
381
|
const v = f.pelvisTilt;
|
|
@@ -349,9 +410,11 @@ export function readPoseSequence(text: string, where: string): PoseSequence {
|
|
|
349
410
|
people.push({ id: person.id, joints: readJoints(p, pw, person.joints, note) });
|
|
350
411
|
});
|
|
351
412
|
}
|
|
352
|
-
frames.push({ t: typeof t === 'number' ? t : NaN, people, ...(pelvisTilt !== undefined ? { pelvisTilt } : {}) });
|
|
413
|
+
frames.push({ t: typeof t === 'number' ? t : NaN, people, ...(pelvisTilt !== undefined ? { pelvisTilt } : {}), ...(depth !== undefined ? { depth } : {}) });
|
|
353
414
|
});
|
|
354
|
-
|
|
415
|
+
const framesText = (fs: number[]) => `${where} ${fs.length === 1 ? `frames[${fs[0]}]` : `${fs.length} frames (${fs[0]}–${fs[fs.length - 1]})`}`;
|
|
416
|
+
if (minorFrames.length > 0) minorKey('pelvisTilt', framesText(minorFrames));
|
|
417
|
+
if (depthFrames.length > 0) minorKey('depth', framesText(depthFrames));
|
|
355
418
|
}
|
|
356
419
|
}
|
|
357
420
|
let pelvisSource: PelvisSource | undefined;
|
|
@@ -369,13 +432,30 @@ export function readPoseSequence(text: string, where: string): PoseSequence {
|
|
|
369
432
|
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)`);
|
|
370
433
|
}
|
|
371
434
|
}
|
|
435
|
+
if (depthKnown && Array.isArray(doc.frames)) {
|
|
436
|
+
const carry = doc.frames.map((f: unknown) => isObject(f) && 'depth' in f);
|
|
437
|
+
const lacking = carry.flatMap((c: boolean, i: number) => (c ? [] : [i]));
|
|
438
|
+
if (lacking.length > 0 && lacking.length < carry.length) p.add('POSE_DEPTH', `${where} has "depth" on ${carry.length - lacking.length} frame(s) and none on frame(s) ${spansText(lacking)}; a sequence with depth carries it on every frame (an empty object where no segment is placed)`);
|
|
439
|
+
if (lacking.length < carry.length) {
|
|
440
|
+
const ids = [...new Set(frames.flatMap((f) => f.people.map((q) => q.id)))].sort();
|
|
441
|
+
if (ids.length > 1) p.add('POSE_DEPTH', `${where} carries depth and ${ids.length} people (${ids.join(', ')}); the depth is one figure's, so the sequence holds one person (rig-pose clean writes only the one it cleaned)`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
372
444
|
const camera = 'camera' in doc ? readCamera(p, where, doc.camera, Array.isArray(doc.frames) ? doc.frames.length : null, note) : undefined;
|
|
373
445
|
if ('cameraSource' in doc) {
|
|
374
446
|
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")`);
|
|
375
447
|
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`);
|
|
376
448
|
}
|
|
377
449
|
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`);
|
|
378
|
-
const cleaned = 'cleaned' in doc ? readCleaned(p, where, doc.cleaned, Array.isArray(doc.frames) ? doc.frames.length : null, pelvisKnown, note) : undefined;
|
|
450
|
+
const cleaned = 'cleaned' in doc ? readCleaned(p, where, doc.cleaned, Array.isArray(doc.frames) ? doc.frames.length : null, pelvisKnown, note, depthKnown) : undefined;
|
|
451
|
+
if (depthKnown) {
|
|
452
|
+
// rig-c/pose#281: "derived" depth is clean's, and clean records the gait it read it for; "measured" is a 3D detector's.
|
|
453
|
+
const sources = new Set(frames.flatMap((f) => Object.values(f.depth ?? {}).map((d) => d.source)));
|
|
454
|
+
const recorded = isObject(doc.cleaned) && isObject(doc.cleaned.params) && 'depth' in doc.cleaned.params;
|
|
455
|
+
if (sources.size > 1) p.add('POSE_DEPTH', `${where} carries "derived" and "measured" depth together; a sequence's depth comes from one source — clean --depth derives it, a 3D detector measures it`);
|
|
456
|
+
else if (sources.has('derived') && !recorded) p.add('POSE_DEPTH', `${where} carries "derived" depth and ${'cleaned' in doc ? 'its "cleaned" record has no "params" "depth"' : 'no "cleaned" record'}; derived depth is written by rig-pose clean --depth walk|run, which records the gait it was read for`);
|
|
457
|
+
if (recorded && cleaned !== undefined && frames.some((f) => f.depth === undefined || Object.values(f.depth).some((d) => d.source !== 'derived'))) p.add('POSE_DEPTH', `${where} "cleaned" "params" has "depth" and the frames do not all carry derived depth; the record says clean derived it on every frame`);
|
|
458
|
+
}
|
|
379
459
|
if (cleaned !== undefined && pelvisKnown) {
|
|
380
460
|
const hasPelvis = isObject(doc.cleaned) && isObject(doc.cleaned.params) && 'pelvis' in doc.cleaned.params;
|
|
381
461
|
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`);
|
|
@@ -386,6 +466,47 @@ export function readPoseSequence(text: string, where: string): PoseSequence {
|
|
|
386
466
|
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
467
|
}
|
|
388
468
|
|
|
469
|
+
/** rig-c/pose#281: a frame's `depth` — `{ "<segment>": { ratio, sign, source } }`, every part refused by name. */
|
|
470
|
+
function readDepth(p: Problems, fw: string, d: unknown): FrameDepth | undefined {
|
|
471
|
+
const dw = `${fw} "depth"`;
|
|
472
|
+
if (!isObject(d)) {
|
|
473
|
+
p.add('POSE_DEPTH', `${dw} is ${typeName(d)}; it must be an object keyed by limb segment (${DEPTH_SEGMENT_NAMES.join(', ')}), each { ratio, sign, source }`);
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
const out: FrameDepth = {};
|
|
477
|
+
for (const [name, v] of Object.entries(d)) {
|
|
478
|
+
const sw = `${dw} "${name}"`;
|
|
479
|
+
if (!(DEPTH_SEGMENT_NAMES as readonly string[]).includes(name)) {
|
|
480
|
+
p.add('POSE_DEPTH', `${dw} names ${JSON.stringify(name)}; the segments are ${DEPTH_SEGMENT_NAMES.join(', ')}`);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (!isObject(v)) {
|
|
484
|
+
p.add('POSE_DEPTH', `${sw} is ${typeName(v)}; it must be { ratio, sign, source }`);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
refuseUnknownKeys(p, 'POSE_DEPTH', sw, v, ['ratio', 'sign', 'source']);
|
|
488
|
+
let ok = true;
|
|
489
|
+
for (const k of ['ratio', 'sign', 'source']) if (!(k in v)) {
|
|
490
|
+
p.add('POSE_DEPTH', `${sw} has no "${k}"; a segment's depth is { ratio, sign, source }`);
|
|
491
|
+
ok = false;
|
|
492
|
+
}
|
|
493
|
+
if ('ratio' in v && !(typeof v.ratio === 'number' && Number.isFinite(v.ratio) && v.ratio >= 0)) {
|
|
494
|
+
p.add('POSE_DEPTH', `${sw} "ratio" is ${typeName(v.ratio)}; it must be a finite number ≥ 0 (the segment's drawn length ÷ its rest length)`);
|
|
495
|
+
ok = false;
|
|
496
|
+
}
|
|
497
|
+
if ('sign' in v && v.sign !== -1 && v.sign !== 0 && v.sign !== 1) {
|
|
498
|
+
p.add('POSE_DEPTH', `${sw} "sign" is ${typeName(v.sign)}; it must be 1 (the distal joint nearer the camera), -1 (farther) or 0 (flat, or not decided)`);
|
|
499
|
+
ok = false;
|
|
500
|
+
}
|
|
501
|
+
if ('source' in v && !(typeof v.source === 'string' && (DEPTH_SOURCES as readonly string[]).includes(v.source))) {
|
|
502
|
+
p.add('POSE_DEPTH', `${sw} "source" is ${typeName(v.source)}; it must be "derived" (clean --depth, from the picture's lengths) or "measured" (a 3D detector)`);
|
|
503
|
+
ok = false;
|
|
504
|
+
}
|
|
505
|
+
if (ok) out[name as DepthSegment] = { ratio: v.ratio as number, sign: v.sign as -1 | 0 | 1, source: v.source as DepthSource };
|
|
506
|
+
}
|
|
507
|
+
return out;
|
|
508
|
+
}
|
|
509
|
+
|
|
389
510
|
/** 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
511
|
function minorOf(spec: unknown): number | null {
|
|
391
512
|
if (spec === POSE_SPEC) return 0;
|
|
@@ -443,7 +564,7 @@ function readCamera(p: Problems, where: string, camera: unknown, frames: number
|
|
|
443
564
|
}
|
|
444
565
|
|
|
445
566
|
/** `cleaned`: { spec, params: { loop, legs, rest, hipsInferred?, hold? }, spikes: { found: [{ frame, joint }], untested: [frame] }, jitter: { before, after }, holds?, unheld?, seams? }. */
|
|
446
|
-
function readCleaned(p: Problems, where: string, c: unknown, frames: number | null, pelvisKnown: boolean, note: string): CleanRecord | undefined {
|
|
567
|
+
function readCleaned(p: Problems, where: string, c: unknown, frames: number | null, pelvisKnown: boolean, note: string, depthKnown: boolean): CleanRecord | undefined {
|
|
447
568
|
const refuseUnknownKeys = (q: Problems, code: string, w: string, o: Record<string, unknown>, known: readonly string[]) => refuseKeys(q, code, w, o, known, note);
|
|
448
569
|
const cw = `${where} "cleaned"`;
|
|
449
570
|
const shape = '{ spec, params: { loop, legs, rest, hipsInferred?, hold? }, spikes: { found: [{ frame, joint }], untested: [frame] }, jitter: { before, after }, holds?, unheld?, seams? }';
|
|
@@ -460,7 +581,16 @@ function readCleaned(p: Problems, where: string, c: unknown, frames: number | nu
|
|
|
460
581
|
const q = c.params;
|
|
461
582
|
if (!isObject(q)) p.add('POSE_CLEANED', `${cw} "params" is ${typeName(q)}; it must be { loop, legs, rest, hipsInferred?, hold? }`);
|
|
462
583
|
else {
|
|
463
|
-
refuseUnknownKeys(p, 'POSE_CLEANED', `${cw} "params"`, q, ['loop', 'legs', 'rest', 'hipsInferred', 'hold', ...(pelvisKnown ? ['pelvis'] : [])]);
|
|
584
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', `${cw} "params"`, q, ['loop', 'legs', 'rest', 'hipsInferred', 'hold', ...(pelvisKnown ? ['pelvis'] : []), ...(depthKnown ? ['depth'] : [])]);
|
|
585
|
+
if (depthKnown && 'depth' in q) {
|
|
586
|
+
const v = q.depth;
|
|
587
|
+
const vw = `${cw} "params" "depth"`;
|
|
588
|
+
if (!isObject(v)) p.add('POSE_CLEANED', `${vw} is ${typeName(v)}; it must be { gait } (rig-c/pose#281: the gait clean --depth read the depth for)`);
|
|
589
|
+
else {
|
|
590
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', vw, v, ['gait']);
|
|
591
|
+
if (!(typeof v.gait === 'string' && (DEPTH_GAITS as readonly string[]).includes(v.gait))) p.add('POSE_CLEANED', `${vw} "gait" is ${typeName(v.gait)}; it must be ${DEPTH_GAITS.map((g) => `"${g}"`).join(' or ')}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
464
594
|
if (pelvisKnown && 'pelvis' in q) {
|
|
465
595
|
const v = q.pelvis;
|
|
466
596
|
const vw = `${cw} "params" "pelvis"`;
|
|
@@ -589,11 +719,11 @@ function readCleaned(p: Problems, where: string, c: unknown, frames: number | nu
|
|
|
589
719
|
});
|
|
590
720
|
}
|
|
591
721
|
if (p.list.length > before) return undefined;
|
|
592
|
-
const q = c.params as { loop: boolean; legs: number; rest: number; hipsInferred?: HipsInferred; hold?: HoldParams | 'off'; pelvis?: PelvisParams };
|
|
722
|
+
const q = c.params as { loop: boolean; legs: number; rest: number; hipsInferred?: HipsInferred; hold?: HoldParams | 'off'; pelvis?: PelvisParams; depth?: DepthParams };
|
|
593
723
|
const j = c.jitter as { before: number | null; after: number | null };
|
|
594
724
|
return {
|
|
595
725
|
spec: CLEAN_SPEC,
|
|
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 } } : {}) },
|
|
726
|
+
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 } } : {}), ...(depthKnown && q.depth !== undefined ? { depth: { gait: q.depth.gait } } : {}) },
|
|
597
727
|
spikes: {
|
|
598
728
|
found: ((c.spikes as { found: Array<{ frame: number; joint: Body18 }> }).found).map((s) => ({ frame: s.frame, joint: s.joint })),
|
|
599
729
|
untested: [...(c.spikes as { untested: number[] }).untested],
|
|
@@ -922,6 +1052,8 @@ export function writePoseSequence(seq: PoseSequence): string {
|
|
|
922
1052
|
const frames = seq.frames.map((f) => ({
|
|
923
1053
|
t: f.t,
|
|
924
1054
|
...(f.pelvisTilt !== undefined ? { pelvisTilt: f.pelvisTilt === null ? null : round(f.pelvisTilt, 3) } : {}),
|
|
1055
|
+
// rig-c/pose#281: the segments in DEPTH_SEGMENTS' order, each { ratio, sign, source }.
|
|
1056
|
+
...(f.depth !== undefined ? { depth: Object.fromEntries(DEPTH_SEGMENT_NAMES.filter((k) => f.depth![k] !== undefined).map((k) => [k, { ratio: round(f.depth![k]!.ratio, 3), sign: f.depth![k]!.sign, source: f.depth![k]!.source }])) } : {}),
|
|
925
1057
|
people: f.people.map((q) => ({
|
|
926
1058
|
id: q.id,
|
|
927
1059
|
joints: Object.fromEntries(
|
|
@@ -941,14 +1073,14 @@ export function writePoseSequence(seq: PoseSequence): string {
|
|
|
941
1073
|
const jit = (v: number | null) => (v === null ? null : round(v, 4));
|
|
942
1074
|
const cleaned = c === undefined ? undefined : {
|
|
943
1075
|
spec: c.spec,
|
|
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 } } : {}) },
|
|
1076
|
+
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 } } : {}), ...(c.params.depth !== undefined ? { depth: { gait: c.params.depth.gait } } : {}) },
|
|
945
1077
|
spikes: { found: c.spikes.found.map((s) => ({ frame: s.frame, joint: s.joint })), untested: [...c.spikes.untested] },
|
|
946
1078
|
jitter: { before: jit(c.jitter.before), after: jit(c.jitter.after) },
|
|
947
1079
|
...(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) } })) } : {}),
|
|
948
1080
|
...(c.seams !== undefined ? { seams: c.seams.map((x) => ({ joint: x.joint, frame: x.frame, step: round(x.step, 4), gap: x.gap })) } : {}),
|
|
949
1081
|
...(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) })) } : {}),
|
|
950
1082
|
};
|
|
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;
|
|
1083
|
+
// rig-c/pose#288, #281: the minor the content needs — /1.2 with depth (it includes 1.1's keys), /1.1 with a pelvis track, /1 otherwise.
|
|
1084
|
+
const spec = seq.frames.some((f) => f.depth !== undefined) ? POSE_SPEC_DEPTH : seq.pelvisSource !== undefined ? POSE_SPEC_PELVIS : seq.spec;
|
|
953
1085
|
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`;
|
|
954
1086
|
}
|
package/src/run.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { tmpdir } from 'node:os';
|
|
|
10
10
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
11
11
|
import { knownSetsOf, readMap, resolveMap, type JointMap, type Rest } from './map.ts';
|
|
12
12
|
import { cleanLine, cleanSummaryLine, inferredSource } from './clean.ts';
|
|
13
|
+
import { depthLine, layersOf, type FrameLayers } from './depth.ts';
|
|
13
14
|
import { convertCoco17, personTrack, readPoseSequence, TRAVEL_IN_PLACE, type CameraFrame } from './pose.ts';
|
|
14
15
|
import { isObject, lineOf, parseJson, Problems, Refusal, type Problem } from './problems.ts';
|
|
15
16
|
import { boneIdentity, droppedLine, keptOutLine, mergeProblems, readFragment, readIdle, targetOf, trackLabel, type IdleReading, type RigcTrack, type Target } from './idle.ts';
|
|
@@ -18,11 +19,12 @@ import { attachmentBones, baseMotionOf, buildInto, rigcVersion, folds, gate, rea
|
|
|
18
19
|
import { partsPath, projectSeams, readSeamDoc, readSeamPairs, seamLines, type SeamPair } from './seams.ts';
|
|
19
20
|
import { artRangeLine, chainsMoving, phaseLines, raisedLine, RAISED_WITHIN, reachLine, switchLine, uncoveredFolds } from './switch.ts';
|
|
20
21
|
import { jitterLine, landed, type FeetReading, type JitterRow, type Landing, measureFeet, measureJitter, measureLanding, measurePosture, measureShear, poseFrames, postureLines } from './verify.ts';
|
|
21
|
-
import { legsOf, PLANT_SWITCH, STANCE_BOUNDS } from './legs.ts';
|
|
22
|
+
import { FRONTAL_DISAGREE, legsOf, PLANT_SWITCH, STANCE_BOUNDS } from './legs.ts';
|
|
22
23
|
import { M2_IDENTITY, mulM2, reduceKeys, reduceScale, reduceUnderScale, SCALE_REDUCE, setupLocal, smoothTrunk, type M2, type ReduceBone, type Reduced, type ReduceRow, type UnderBone } from './reduce.ts';
|
|
23
|
-
import type { BlendHold, Closure, Floor, FreeAnkleShift, LegScale, LegSegmentRatio, Lowered, Side, Stance, SupportScaleRun } from './legs.ts';
|
|
24
|
+
import type { BlendHold, Closure, Feet, Floor, FreeAnkleShift, LegScale, LegSegmentRatio, Lowered, Side, Stance, SupportScaleRun, SupportSignal } from './legs.ts';
|
|
24
25
|
import { reportRenderDomain, writeReport, type ReportInput } from './report.ts';
|
|
25
26
|
import { PROVENANCE_FILE, provenanceText } from './provenance.ts';
|
|
27
|
+
import { madeLine, planOutDirs } from './outdir.ts';
|
|
26
28
|
import { bodyLine, faceLine } from './face.ts';
|
|
27
29
|
|
|
28
30
|
export interface RetargetArgs extends RetargetOptions {
|
|
@@ -104,15 +106,9 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
104
106
|
const report = a.report ?? defaultReportPath(a.out);
|
|
105
107
|
// A file this run writes goes into a directory the user named: one that is not there yet is made
|
|
106
108
|
// after the run is green (one line says so), as --build and --render make theirs; a file in the way is refused.
|
|
107
|
-
const makeDirs =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const dir = dirname(path);
|
|
111
|
-
let up = dir;
|
|
112
|
-
while (!existsSync(up) && dirname(up) !== up) up = dirname(up);
|
|
113
|
-
if (!statSync(up).isDirectory()) args.add('ARG_PATH', up === dir ? `${flag} ${path} — ${dir} is not a directory` : `${flag} ${path} — ${up} is a file, so directory ${dir} cannot be made under it`);
|
|
114
|
-
else if (up !== dir) makeDirs.set(dir, [...(makeDirs.get(dir) ?? []), `${flag} ${path}`]);
|
|
115
|
-
}
|
|
109
|
+
const makeDirs = planOutDirs([['--out', a.out], ['--report', report], ['--camera', a.camera]], args);
|
|
110
|
+
// #284: --build and --render make their own directories (rigc, the copy); a file in the way is refused here, up front.
|
|
111
|
+
planOutDirs([['--build', a.build], ['--render', a.render]], args);
|
|
116
112
|
if (a.reference !== undefined && a.build === undefined) args.add('ARG_REFERENCE', `--reference ${a.reference} needs --build: the record it goes in is written beside the build (${PROVENANCE_FILE})`);
|
|
117
113
|
args.throwIfAny();
|
|
118
114
|
const input = resolveRig(a.rig, a.baseMotion);
|
|
@@ -207,9 +203,14 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
207
203
|
for (const e of r.byHip) if (e.short.length > 0) say(` REACH ${reachLine(e, r.times)}`);
|
|
208
204
|
for (const e of r.reach) say(` REACH ${reachCostLine(e, r.times)}`);
|
|
209
205
|
for (const e of r.artRange) say(` ART-RANGE ${artRangeLine(e, r.times)}`);
|
|
210
|
-
|
|
206
|
+
// #287: a range that clamps no frame says so once, on the last segment's line (the line count stays one per segment).
|
|
207
|
+
const noClamp = legScaleNoClamp(r.legScale);
|
|
208
|
+
r.legScale.forEach((l, k) => say(` LEG-SCALE ${legScaleLine(l, r.times, r.feet?.legLength ?? null, r.rigLeg)}${k === r.legScale.length - 1 && noClamp !== null ? `; ${noClamp}` : ''}`));
|
|
211
209
|
for (const l of r.legScale) for (const s of l.support ?? []) say(` LEG-SCALE ${supportScaleLine(l, s, r.times)}`);
|
|
212
210
|
if (r.pelvis !== null) say(` ${pelvisLine(r.pelvis, seq.frames.length)}`);
|
|
211
|
+
// rig-c/pose#281: the limbs' front-to-back order from the sequence's depth, only when it carries depth.
|
|
212
|
+
const layers: FrameLayers[] | null = seq.frames.some((x) => x.depth !== undefined) ? frames.map((j, i) => layersOf(j, seq.frames[i]!.depth ?? {})) : null;
|
|
213
|
+
if (layers !== null) say(` ${depthLine(layers, seq.frames, seq.cleaned!.params.depth?.gait ?? null)}`);
|
|
213
214
|
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`);
|
|
214
215
|
if (r.feet !== null) {
|
|
215
216
|
const f = r.feet;
|
|
@@ -217,6 +218,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
217
218
|
say(` FEET ${r.planted ? 'planted' : 'read, not planted (no --plant-feet)'}: leg length (median hip → ankle) r ${f.legLength.r.toFixed(1)}px, l ${f.legLength.l.toFixed(1)}px; ${f.supports.length} support span(s), ${f.changes.filter((c) => c.from !== null).length} change(s) of support, ${f.jumps.length} jump(s), ${f.steps.length} step(s)`);
|
|
218
219
|
for (const sg of r.segments) say(` FEET ${segmentLine(sg)}`);
|
|
219
220
|
say(` FEET supports: ${f.supports.map((x) => `${x.side} ${span(x.frames)}`).join(', ') || 'none'}`);
|
|
221
|
+
if (f.signal !== undefined) for (const l of signalLines(f.signal, f.supports)) say(` FEET ${l}`);
|
|
220
222
|
say(` FEET jumps (both ankles and mid_hip > 0.05 leg lengths above their rolling floor, ≥ 2 frames): ${f.jumps.map(span).join(', ') || 'none'}`);
|
|
221
223
|
if (f.lowered !== undefined) say(` FEET ${loweredLine(f.lowered)}`);
|
|
222
224
|
if (f.tracked !== undefined) say(` FEET tracked (the lower foot supports on the floor, following its scaled ankle track until still, rig-c/pose#119): ${f.tracked.map((x) => `${x.side} ${span(x.frames)}`).join(', ')}`);
|
|
@@ -355,7 +357,7 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
355
357
|
|
|
356
358
|
const lengths = new Map(doc.bones.map((b) => [b.name, b.length ?? 0]));
|
|
357
359
|
const hips = seq.cleaned?.params.hipsInferred;
|
|
358
|
-
const posture = measurePosture(posed, lengths, map, r, frames, a.minScore, inferredSource(seq.cleaned), hips?.by === 'measured' ? hips : null);
|
|
360
|
+
const posture = measurePosture(posed, lengths, map, r, frames, a.minScore, inferredSource(seq.cleaned), hips?.by === 'measured' ? hips : null, seq.pelvisSource !== undefined ? { source: seq.pelvisSource, tilt: seq.frames.map((f) => f.pelvisTilt ?? null) } : null);
|
|
359
361
|
if (posture !== null) for (const l of postureLines(posture)) say(` POSTURE ${l}`);
|
|
360
362
|
const legBones = legBonesOf(map);
|
|
361
363
|
const feetReading = legBones === null ? null : measureFeet(posed, lengths, r, legBones);
|
|
@@ -444,6 +446,8 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
444
446
|
legScale: r.legScale.map(({ scale: _scale, asked: _asked, picture: _picture, ...rest }) => rest),
|
|
445
447
|
pivot: r.pivot,
|
|
446
448
|
pelvis: r.pelvis,
|
|
449
|
+
// Only when the sequence carries depth (#281): a sequence without it writes the report it always has.
|
|
450
|
+
...(layers !== null ? { layers: layers.map((l, frame) => ({ frame, order: l.order, depth: l.depth })) } : {}),
|
|
447
451
|
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 },
|
|
448
452
|
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 },
|
|
449
453
|
shear,
|
|
@@ -916,6 +920,48 @@ function loweredLine(lowered: readonly Lowered[]): string {
|
|
|
916
920
|
return `${head}: ${frames} frame(s) read differently from before — ${lowered.map((x) => `frames ${span(x.frames)} ${side(x.was)} → ${side(x.now)}${x.cause === undefined ? '' : ` (${CAUSE[x.cause]})`}`).join(', ')}`;
|
|
917
921
|
}
|
|
918
922
|
|
|
923
|
+
/**
|
|
924
|
+
* The support-signal lines (rig-c/pose#280). On a clip read as frontal: two
|
|
925
|
+
* lines — how often the ankle's vertical speed contradicts ankle height against
|
|
926
|
+
* `FRONTAL_DISAGREE`, and where the stride shows (in depth) — then one line per
|
|
927
|
+
* support span (a stride) naming the signal that decided its frames and what
|
|
928
|
+
* ankle height alone would have read. On any other clip ankle height decided as
|
|
929
|
+
* before and nothing is printed: the output of a clip the second signal does not
|
|
930
|
+
* decide stays as it was, in every mode (`Feet.signal` still carries the figures).
|
|
931
|
+
*/
|
|
932
|
+
export function signalLines(sig: SupportSignal, supports: Feet['supports']): string[] {
|
|
933
|
+
const span = (x: [number, number]) => (x[0] === x[1] ? `${x[0]}` : `${x[0]}–${x[1]}`);
|
|
934
|
+
const n = Math.round(FRONTAL_DISAGREE * 100);
|
|
935
|
+
const pct = sig.read === 0 ? 0 : (100 * sig.contradict.length) / sig.read;
|
|
936
|
+
if (!sig.frontal) return [];
|
|
937
|
+
const ranges = `x · y against the hips: r ${sig.range.r.x.toFixed(1)} · ${sig.range.r.y.toFixed(1)} px, l ${sig.range.l.x.toFixed(1)} · ${sig.range.l.y.toFixed(1)} px`;
|
|
938
|
+
const contra = `${sig.contradict.length} of ${sig.read} frame(s) (${pct.toFixed(1)} %`;
|
|
939
|
+
const out = [
|
|
940
|
+
`support: ankle vertical speed (rig-c/pose#280) — it contradicts ankle height on ${contra}, past ${n} %): the lower ankle moving while the higher moves with the ground`,
|
|
941
|
+
`support: read as a frontal view — ankles move more in y than x (${ranges}); ankle height only where neither foot moves with the ground`,
|
|
942
|
+
];
|
|
943
|
+
for (const sp of supports) {
|
|
944
|
+
let speed = 0;
|
|
945
|
+
let height = 0;
|
|
946
|
+
let both = 0;
|
|
947
|
+
let differ = 0;
|
|
948
|
+
const was = new Set<string>();
|
|
949
|
+
for (let i = sp.frames[0]; i <= sp.frames[1]; i++) {
|
|
950
|
+
if (sig.by[i] === 'speed') speed++;
|
|
951
|
+
else if (sig.by[i] === 'height') height++;
|
|
952
|
+
if (sig.contact[i] === 'both') both++;
|
|
953
|
+
if (sig.height[i] !== sp.side) {
|
|
954
|
+
differ++;
|
|
955
|
+
was.add(sig.height[i] ?? 'none');
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
const by = speed > 0 ? `ankle vertical speed on ${speed} frame(s)${height > 0 ? `, ankle height on ${height} (neither foot moved with the ground)` : ''}` : `ankle height on ${height} frame(s) (neither foot moved with the ground)`;
|
|
959
|
+
const alone = differ === 0 ? 'ankle height alone reads the same' : `ankle height alone would read ${[...was].join(' or ')} on ${differ} of them`;
|
|
960
|
+
out.push(`stride ${sp.side} ${span(sp.frames)}: ${by} (both feet down on ${both}); ${alone}`);
|
|
961
|
+
}
|
|
962
|
+
return out;
|
|
963
|
+
}
|
|
964
|
+
|
|
919
965
|
/**
|
|
920
966
|
* The in-place line without --plant-feet (rig-c/pose#242): what the declaration still changes, and the one thing it
|
|
921
967
|
* cannot without planted feet — the loop closure, which spreads back the planted hip's drift. `drift` is the
|
|
@@ -1052,6 +1098,21 @@ export function legScaleLine(l: LegScale, times: readonly number[], legLength: R
|
|
|
1052
1098
|
return `chain "${l.chain}" bone "${l.bone}" (${l.joints[0]} → ${l.joints[1]}): straight length ${l.straight.toFixed(1)}px (larger 95th percentile of ${times.length} frames, both legs)${share}; picture length ÷ straight ${l.ratio.min.toFixed(3)} … ${l.ratio.max.toFixed(3)} (median ${l.ratio.median.toFixed(3)}); scalex keyed in [${l.limits[0]}, ${l.limits[1]}] of the straight length (${limitsPx}): ${clamp(l.low, l.limits[0], 'under', 'low')}; ${clamp(l.high, l.limits[1], 'over', 'high')}${free}`;
|
|
1053
1099
|
}
|
|
1054
1100
|
|
|
1101
|
+
/**
|
|
1102
|
+
* rig-c/pose#287: when `--leg-scale`'s range clamps no frame on any leg segment
|
|
1103
|
+
* (no run under the floor, none over the ceiling, no supporting frame scaled past
|
|
1104
|
+
* the floor), the clause naming the range and the picture's own spread across
|
|
1105
|
+
* every segment, so a reader learns the range never bound and what floor would.
|
|
1106
|
+
* Null when any frame was clamped, or with no segment scaled.
|
|
1107
|
+
*/
|
|
1108
|
+
export function legScaleNoClamp(ls: readonly LegScale[]): string | null {
|
|
1109
|
+
if (ls.length === 0 || ls.some((l) => l.low.length > 0 || l.high.length > 0 || (l.support ?? []).length > 0)) return null;
|
|
1110
|
+
const [floor, ceiling] = ls[0]!.limits;
|
|
1111
|
+
const min = Math.min(...ls.map((l) => l.ratio.min));
|
|
1112
|
+
const max = Math.max(...ls.map((l) => l.ratio.max));
|
|
1113
|
+
return `no frame clamped on any leg segment: the range [${floor}, ${ceiling}] holds the picture's whole spread, picture length ÷ straight ${min.toFixed(3)} … ${max.toFixed(3)} over the ${ls.length} segment(s), so each segment is scaled to the picture on every frame; a floor above ${min.toFixed(3)} would clamp`;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1055
1116
|
/** A LEG-SCALE past the floor (support) line (rig-c/pose#133): the supporting leg segment scaled below `--leg-scale`'s floor down to #95's bound. */
|
|
1056
1117
|
export function supportScaleLine(l: LegScale, s: SupportScaleRun, times: readonly number[]): string {
|
|
1057
1118
|
const span = (x: [number, number]) => (x[0] === x[1] ? `frame ${x[0]} (t=${times[x[0]]!.toFixed(3)}s)` : `frames ${x[0]}–${x[1]} (t=${times[x[0]]!.toFixed(3)}–${times[x[1]]!.toFixed(3)}s)`);
|
|
@@ -1136,11 +1197,6 @@ export function defaultReportPath(out: string): string {
|
|
|
1136
1197
|
return `${out.endsWith('.json') ? out.slice(0, -'.json'.length) : out}.report.json`;
|
|
1137
1198
|
}
|
|
1138
1199
|
|
|
1139
|
-
/** rig-c/pose#239: the line a directory this run made prints — the directory, and the paths that named it. */
|
|
1140
|
-
export function madeLine(dir: string, flags: readonly string[]): string {
|
|
1141
|
-
return ` made directory ${dir} — it did not exist; ${flags.join(' and ')} ${flags.length === 1 ? 'is' : 'are'} written into it`;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
1200
|
/** rig-c/pose#239: the report's `wrote` line — whether it was the default, and the mtime of the report it replaced. */
|
|
1145
1201
|
export function reportLine(report: string, byDefault: boolean, previous: Date | null): string {
|
|
1146
1202
|
return ` wrote ${report} — the run's report (rig-pose-report/1)${byDefault ? ', beside --out by default; name another path with --report' : ''}${previous === null ? '' : `; it replaced the report there last written ${previous.toISOString()}`}`;
|
package/src/runcompare.ts
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* not a failure: the run exits 0 whenever it compared.
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
8
|
-
import { dirname } from 'node:path';
|
|
9
8
|
import { compare, findingLine, readCloth, readHands, readSpans, summaryLine, writeComparison, type ClothWindow, type CompareInput, type Comparison } from './compare.ts';
|
|
10
9
|
import { readMap } from './map.ts';
|
|
11
10
|
import { personTrack, readPoseSequence, type PoseSequence } from './pose.ts';
|
|
12
11
|
import { Problems, Refusal } from './problems.ts';
|
|
13
12
|
import { drawable, drawSheet } from './sheet.ts';
|
|
13
|
+
import { makeOutDirs, planOutDirs } from './outdir.ts';
|
|
14
14
|
|
|
15
15
|
export interface CompareArgs {
|
|
16
16
|
reference: string;
|
|
@@ -37,12 +37,9 @@ export function runCompare(a: CompareArgs, log: (line: string) => void): Compari
|
|
|
37
37
|
if (!existsSync(path)) p.add('ARG_PATH', `${flag} ${path} does not exist`);
|
|
38
38
|
else if (!statSync(path).isFile()) p.add('ARG_PATH', `${flag} ${path} is not a file`);
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (!existsSync(dir)) p.add('ARG_PATH', `${flag} ${path} — directory ${dir} does not exist`);
|
|
44
|
-
else if (!statSync(dir).isDirectory()) p.add('ARG_PATH', `${flag} ${path} — ${dir} is not a directory`);
|
|
45
|
-
}
|
|
40
|
+
// #284: a directory --report or --sheet names that is not there yet is made just before that file is written.
|
|
41
|
+
const reportDirs = planOutDirs([['--report', a.report]], p);
|
|
42
|
+
const sheetDirs = planOutDirs([['--sheet', a.sheet]], p);
|
|
46
43
|
p.throwIfAny();
|
|
47
44
|
|
|
48
45
|
// Every reader runs, and every refusal is reported together.
|
|
@@ -98,6 +95,7 @@ export function runCompare(a: CompareArgs, log: (line: string) => void): Compari
|
|
|
98
95
|
if (c.cloth.windows.length > 0) log(` CLOTH the render's stroke threshold ${c.cloth.stroke.render.toFixed(4)} of its hip widths = ${c.cloth.stroke.carried}`);
|
|
99
96
|
}
|
|
100
97
|
if (a.report !== undefined) {
|
|
98
|
+
makeOutDirs(reportDirs, log);
|
|
101
99
|
writeFileSync(a.report, writeComparison(c, {
|
|
102
100
|
reference: a.reference, render: a.render, map: a.map, retargetReport: a.retargetReport ?? null,
|
|
103
101
|
referenceHands: a.referenceHands ?? null, renderHands: a.renderHands ?? null, clothReference: a.clothReference ?? null, clothRender: a.clothRender ?? null,
|
|
@@ -108,6 +106,7 @@ export function runCompare(a: CompareArgs, log: (line: string) => void): Compari
|
|
|
108
106
|
const plate = drawSheet({ frames: ref!.frames, width: ref!.seq.width, height: ref!.seq.height }, { frames: ren!.frames, width: ren!.seq.width, height: ren!.seq.height }, c.findings);
|
|
109
107
|
if (plate === null) log(` SHEET no finding has a frame to draw (${c.findings.length} finding(s), none outside cloth); ${a.sheet} not written`);
|
|
110
108
|
else {
|
|
109
|
+
makeOutDirs(sheetDirs, log);
|
|
111
110
|
plate.writePng(a.sheet);
|
|
112
111
|
log(` SHEET ${drawable(c.findings).length} finding(s), reference | render at each worst frame: wrote ${a.sheet}`);
|
|
113
112
|
}
|