@rig-c/pose 0.7.0 → 0.8.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 +78 -0
- package/cli.ts +63 -3
- package/package.json +1 -1
- package/src/compare.ts +1046 -0
- package/src/runcompare.ts +119 -0
- package/src/sheet.ts +106 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `rig-pose compare`, end to end: read the two sequences, the map and the
|
|
3
|
+
* optional inputs, compare (`compare.ts`), print a summary line per unit and a
|
|
4
|
+
* line per finding, and write the report and the sheet when asked. A finding is
|
|
5
|
+
* not a failure: the run exits 0 whenever it compared.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { dirname } from 'node:path';
|
|
9
|
+
import { compare, findingLine, readCloth, readHands, readSpans, summaryLine, writeComparison, type ClothWindow, type CompareInput, type Comparison } from './compare.ts';
|
|
10
|
+
import { readMap } from './map.ts';
|
|
11
|
+
import { personTrack, readPoseSequence, type PoseSequence } from './pose.ts';
|
|
12
|
+
import { Problems, Refusal } from './problems.ts';
|
|
13
|
+
import { drawable, drawSheet } from './sheet.ts';
|
|
14
|
+
|
|
15
|
+
export interface CompareArgs {
|
|
16
|
+
reference: string;
|
|
17
|
+
render: string;
|
|
18
|
+
map: string;
|
|
19
|
+
retargetReport?: string;
|
|
20
|
+
referenceHands?: string;
|
|
21
|
+
renderHands?: string;
|
|
22
|
+
clothReference?: string;
|
|
23
|
+
clothRender?: string;
|
|
24
|
+
clothWindows?: ClothWindow[];
|
|
25
|
+
report?: string;
|
|
26
|
+
sheet?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function runCompare(a: CompareArgs, log: (line: string) => void): Comparison {
|
|
30
|
+
const p = new Problems();
|
|
31
|
+
const inputs: Array<[string, string | undefined]> = [
|
|
32
|
+
['--reference', a.reference], ['--render', a.render], ['--map', a.map], ['--retarget-report', a.retargetReport],
|
|
33
|
+
['--reference-hands', a.referenceHands], ['--render-hands', a.renderHands], ['--cloth-reference', a.clothReference], ['--cloth-render', a.clothRender],
|
|
34
|
+
];
|
|
35
|
+
for (const [flag, path] of inputs) {
|
|
36
|
+
if (path === undefined) continue;
|
|
37
|
+
if (!existsSync(path)) p.add('ARG_PATH', `${flag} ${path} does not exist`);
|
|
38
|
+
else if (!statSync(path).isFile()) p.add('ARG_PATH', `${flag} ${path} is not a file`);
|
|
39
|
+
}
|
|
40
|
+
for (const [flag, path] of [['--report', a.report], ['--sheet', a.sheet]] as const) {
|
|
41
|
+
if (path === undefined) continue;
|
|
42
|
+
const dir = dirname(path);
|
|
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
|
+
}
|
|
46
|
+
p.throwIfAny();
|
|
47
|
+
|
|
48
|
+
// Every reader runs, and every refusal is reported together.
|
|
49
|
+
const all: Refusal[] = [];
|
|
50
|
+
const read = <T>(f: () => T): T | null => {
|
|
51
|
+
try {
|
|
52
|
+
return f();
|
|
53
|
+
} catch (e) {
|
|
54
|
+
if (e instanceof Refusal) {
|
|
55
|
+
all.push(e);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
throw e;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const seqOf = (path: string) => read(() => {
|
|
62
|
+
const seq = readPoseSequence(readFileSync(path, 'utf8'), path);
|
|
63
|
+
return { seq, frames: personTrack(seq, undefined, path) };
|
|
64
|
+
});
|
|
65
|
+
const ref = seqOf(a.reference);
|
|
66
|
+
const ren = seqOf(a.render);
|
|
67
|
+
const map = read(() => readMap(readFileSync(a.map, 'utf8'), a.map));
|
|
68
|
+
const report = a.retargetReport === undefined ? null : read(() => readSpans(readFileSync(a.retargetReport!, 'utf8'), a.retargetReport!));
|
|
69
|
+
const handsA = a.referenceHands === undefined ? null : read(() => readHands(readFileSync(a.referenceHands!, 'utf8'), a.referenceHands!));
|
|
70
|
+
const handsB = a.renderHands === undefined ? null : read(() => readHands(readFileSync(a.renderHands!, 'utf8'), a.renderHands!));
|
|
71
|
+
const clothA = a.clothReference === undefined ? null : read(() => readCloth(readFileSync(a.clothReference!, 'utf8'), a.clothReference!));
|
|
72
|
+
const clothB = a.clothRender === undefined ? null : read(() => readCloth(readFileSync(a.clothRender!, 'utf8'), a.clothRender!));
|
|
73
|
+
if (all.length > 0) throw new Refusal(all.flatMap((e) => e.problems));
|
|
74
|
+
if (report !== null && report.frames !== ref!.seq.frames.length) {
|
|
75
|
+
throw new Refusal([{ code: 'COMPARE_REPORT', text: `${a.retargetReport} is a retarget over ${report.frames} frame(s); the reference ${a.reference} has ${ref!.seq.frames.length} — it is the report of another run` }]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const side = (path: string, s: { seq: PoseSequence; frames: CompareInput['reference']['frames'] }) => ({ where: path, times: s.seq.frames.map((f) => f.t), frames: s.frames });
|
|
79
|
+
const input: CompareInput = {
|
|
80
|
+
reference: side(a.reference, ref!),
|
|
81
|
+
render: side(a.render, ren!),
|
|
82
|
+
map: map!,
|
|
83
|
+
spans: report === null ? null : report.spans,
|
|
84
|
+
retargetScale: report === null ? null : report.scale,
|
|
85
|
+
hands: handsA !== null && handsB !== null ? { reference: handsA, render: handsB } : null,
|
|
86
|
+
cloth: clothA !== null && clothB !== null ? { reference: clothA, render: clothB, windows: a.clothWindows ?? [] } : null,
|
|
87
|
+
};
|
|
88
|
+
const c = compare(input);
|
|
89
|
+
|
|
90
|
+
log(`rig-pose compare: ${c.frames} frame(s), ${c.duration.toFixed(3)}s — reference ${a.reference}, render ${a.render}; median hip width ${px(c.hipWidth.reference)} and ${px(c.hipWidth.render)}${a.retargetReport === undefined ? '' : `; spans from ${a.retargetReport}`}`);
|
|
91
|
+
for (const u of c.units) log(` ${u.unit.toUpperCase().padEnd(6)} ${summaryLine(u, c.times)}`);
|
|
92
|
+
for (const f of c.findings) log(` FIND ${findingLine(f, c.times)}`);
|
|
93
|
+
if (c.cloth !== null) {
|
|
94
|
+
for (const w of c.cloth.windows) {
|
|
95
|
+
const fig = (x: { lag: number; swingRatio: number | null; overshoot: number | null; strokes: number }) => `lag ${x.lag.toFixed(3)}, swing ${x.swingRatio === null ? '—' : x.swingRatio.toFixed(3)}, overshoot ${x.overshoot === null ? '—' : x.overshoot.toFixed(3)} (${x.strokes} strokes)`;
|
|
96
|
+
log(` CLOTH window ${w.name} ${w.t[0]}–${w.t[1]}s (frames ${w.frames[0]}–${w.frames[1]}): reference ${fig(w.reference)}; render ${fig(w.render)}; strokes from ${c.cloth.stroke.reference} and ${c.cloth.stroke.render.toFixed(4)} hip widths (reference, render)`);
|
|
97
|
+
}
|
|
98
|
+
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
|
+
}
|
|
100
|
+
if (a.report !== undefined) {
|
|
101
|
+
writeFileSync(a.report, writeComparison(c, {
|
|
102
|
+
reference: a.reference, render: a.render, map: a.map, retargetReport: a.retargetReport ?? null,
|
|
103
|
+
referenceHands: a.referenceHands ?? null, renderHands: a.renderHands ?? null, clothReference: a.clothReference ?? null, clothRender: a.clothRender ?? null,
|
|
104
|
+
}));
|
|
105
|
+
log(` wrote ${a.report}`);
|
|
106
|
+
}
|
|
107
|
+
if (a.sheet !== undefined) {
|
|
108
|
+
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
|
+
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
|
+
else {
|
|
111
|
+
plate.writePng(a.sheet);
|
|
112
|
+
log(` SHEET ${drawable(c.findings).length} finding(s), reference | render at each worst frame: wrote ${a.sheet}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
log(` ${c.findings.length} finding(s)`);
|
|
116
|
+
return c;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const px = (v: number | null) => (v === null ? 'none' : `${v.toFixed(1)}px`);
|
package/src/sheet.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The comparison's sheet (rig-c/pose#38): for each finding, the reference
|
|
3
|
+
* skeleton beside the render skeleton at its worst frame, one row per finding,
|
|
4
|
+
* worst first within each unit. Skeletons only — no video frame is ever drawn.
|
|
5
|
+
* The finding's own points are drawn over the limbs in yellow.
|
|
6
|
+
*
|
|
7
|
+
* A cloth finding is a figure over a window with no single frame, so it has no
|
|
8
|
+
* row. Pure: it returns the plate; the caller writes it.
|
|
9
|
+
*/
|
|
10
|
+
import { textWidth } from 'rig-c/font5x7';
|
|
11
|
+
import { Plate, type RGBA } from 'rig-c/plate';
|
|
12
|
+
import type { Body18 } from './joints.ts';
|
|
13
|
+
import { positionOf, type Finding, type Frames } from './compare.ts';
|
|
14
|
+
|
|
15
|
+
const RIGHT: RGBA = [214, 69, 65, 255];
|
|
16
|
+
const LEFT: RGBA = [52, 120, 214, 255];
|
|
17
|
+
const MID: RGBA = [120, 120, 120, 255];
|
|
18
|
+
const MARK: RGBA = [236, 180, 20, 255];
|
|
19
|
+
const PANE: RGBA = [232, 232, 232, 255];
|
|
20
|
+
const PAPER: RGBA = [255, 255, 255, 255];
|
|
21
|
+
const INK: RGBA = [30, 30, 30, 255];
|
|
22
|
+
|
|
23
|
+
const LIMBS: Array<[Body18, Body18, RGBA]> = [
|
|
24
|
+
['r_shoulder', 'r_elbow', RIGHT], ['r_elbow', 'r_wrist', RIGHT],
|
|
25
|
+
['r_hip', 'r_knee', RIGHT], ['r_knee', 'r_ankle', RIGHT],
|
|
26
|
+
['l_shoulder', 'l_elbow', LEFT], ['l_elbow', 'l_wrist', LEFT],
|
|
27
|
+
['l_hip', 'l_knee', LEFT], ['l_knee', 'l_ankle', LEFT],
|
|
28
|
+
['r_shoulder', 'l_shoulder', MID], ['r_hip', 'l_hip', MID],
|
|
29
|
+
['r_shoulder', 'r_hip', RIGHT], ['l_shoulder', 'l_hip', LEFT],
|
|
30
|
+
['neck', 'nose', MID], ['nose', 'r_eye', RIGHT], ['nose', 'l_eye', LEFT],
|
|
31
|
+
['r_eye', 'r_ear', RIGHT], ['l_eye', 'l_ear', LEFT],
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export interface SheetSide {
|
|
35
|
+
frames: Frames;
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The pane height in px; each pane keeps its picture's aspect. */
|
|
41
|
+
export const SHEET_PANE = 320;
|
|
42
|
+
const LABEL = 28;
|
|
43
|
+
const GAP = 8;
|
|
44
|
+
|
|
45
|
+
/** The findings a sheet draws: every one with a frame (not cloth). */
|
|
46
|
+
export function drawable(findings: readonly Finding[]): Finding[] {
|
|
47
|
+
return findings.filter((f) => f.unit !== 'cloth');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The sheet as a plate, or null when no finding has a frame to draw. */
|
|
51
|
+
export function drawSheet(reference: SheetSide, render: SheetSide, findings: readonly Finding[]): Plate | null {
|
|
52
|
+
const rows = drawable(findings);
|
|
53
|
+
if (rows.length === 0) return null;
|
|
54
|
+
const wa = Math.round((reference.width * SHEET_PANE) / reference.height);
|
|
55
|
+
const wb = Math.round((render.width * SHEET_PANE) / render.height);
|
|
56
|
+
const width = Math.max(wa + GAP + wb, 480) + 2 * GAP;
|
|
57
|
+
const rowH = LABEL + SHEET_PANE + GAP;
|
|
58
|
+
const plate = new Plate(width, GAP + rows.length * rowH);
|
|
59
|
+
plate.rect(0, 0, plate.width, plate.height, PAPER);
|
|
60
|
+
rows.forEach((f, k) => {
|
|
61
|
+
const y = GAP + k * rowH;
|
|
62
|
+
const title = label(`${k + 1} ${f.unit} ${f.measure} ${f.subject} frame ${f.worst.frame} t=${f.worst.t.toFixed(3)}`);
|
|
63
|
+
plate.text(title, GAP, y, textWidth(title, 2) <= width - 2 * GAP ? 2 : 1, INK);
|
|
64
|
+
plate.text('reference', GAP, y + 16, 1, INK);
|
|
65
|
+
plate.text('render', GAP + wa + GAP, y + 16, 1, INK);
|
|
66
|
+
pane(plate, reference, f, GAP, y + LABEL, wa);
|
|
67
|
+
pane(plate, render, f, GAP + wa + GAP, y + LABEL, wb);
|
|
68
|
+
});
|
|
69
|
+
return plate;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function pane(plate: Plate, side: SheetSide, f: Finding, x0: number, y0: number, w: number): void {
|
|
73
|
+
plate.rect(x0, y0, w, SHEET_PANE, PANE);
|
|
74
|
+
const k = SHEET_PANE / side.height;
|
|
75
|
+
const joints = side.frames[f.worst.frame] ?? {};
|
|
76
|
+
const at = (n: string): [number, number] | null => {
|
|
77
|
+
const p = positionOf(joints, n);
|
|
78
|
+
if (p === null) return null;
|
|
79
|
+
const x = x0 + p[0] * k;
|
|
80
|
+
const y = y0 + p[1] * k;
|
|
81
|
+
// A point outside the picture is not drawn outside its pane.
|
|
82
|
+
return x < x0 || x >= x0 + w || y < y0 || y >= y0 + SHEET_PANE ? null : [x, y];
|
|
83
|
+
};
|
|
84
|
+
const t = 3;
|
|
85
|
+
for (const [a, b, colour] of LIMBS) {
|
|
86
|
+
const pa = at(a);
|
|
87
|
+
const pb = at(b);
|
|
88
|
+
if (pa !== null && pb !== null) plate.line(pa[0], pa[1], pb[0], pb[1], t, colour);
|
|
89
|
+
}
|
|
90
|
+
// A hand's points are not body-18: its wrist is marked.
|
|
91
|
+
const marks = f.measure === 'visibility' ? [f.subject === 'hand_r' ? 'r_wrist' : 'l_wrist'] : f.points;
|
|
92
|
+
for (let i = 0; i + 1 < marks.length; i++) {
|
|
93
|
+
const pa = at(marks[i]!);
|
|
94
|
+
const pb = at(marks[i + 1]!);
|
|
95
|
+
if (pa !== null && pb !== null) plate.line(pa[0], pa[1], pb[0], pb[1], t + 2, MARK);
|
|
96
|
+
}
|
|
97
|
+
for (const q of marks) {
|
|
98
|
+
const pq = at(q);
|
|
99
|
+
if (pq !== null) plate.disc(pq[0], pq[1], t * 2, MARK);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The 5×7 font has capitals, digits and a little punctuation; anything else becomes a space. */
|
|
104
|
+
function label(s: string): string {
|
|
105
|
+
return s.toUpperCase().replace(/[^A-Z0-9 _\-.,:/()!+=<>]/g, ' ');
|
|
106
|
+
}
|