@rig-c/pose 0.6.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 CHANGED
@@ -45,15 +45,15 @@ and what the run cannot decide for you it lists:
45
45
  - `BEND` — with `--bends clamp`, a hinge bent the wrong way is straightened on that frame instead of refusing.
46
46
  - `FOLD` — `rigc render --folds` over the new animation: every mesh that folds, and when. A reading, not a gate.
47
47
  - `POSTURE` — when the map has a pelvis chain (joints `r_hip`, `l_hip`) and a `translate`: the hip line's tilt range, and the sway — the x-range of mid_hip − mid_shoulder — in rig units and in shoulder widths, the pose's beside the posed rig's. A reading, not a gate.
48
- - `SWITCH` — with `alternates` in the map ([below](#alternate-drawings-alternates)): each span on which a set of alternate drawings shows, its slots, its frames, how far the pose was from the set's key skeleton, and how far each bone with a `drawn` angle was turned from it.
48
+ - `SWITCH` — with `alternates` in the map ([below](#alternate-drawings-alternates)): each span on which a set of alternate drawings shows, its slots, its frames, how far the pose was from the set's key skeleton, how far each bone with a `drawn` angle was turned from it, and where each of its boundaries was moved to (`moved`, [below](#alternate-drawings-alternates)).
49
49
  - `UNCOVERED` — with `alternates` in the map: every fold on a frame where no switch took the folding mesh off screen. This is the list of poses still to paint.
50
50
  - `RANGE` — a chain's `range` ([below](#a-joints-range-range)): a bone whose rotate key the pose asks past its range, outside every span of a set depicting its chain, is clamped at the limit; one line per run, with the frames, the seconds and the largest overflow. The demand for an alternate.
51
51
  - `ART-RANGE` — inside a set's span: a bone with a `drawn` angle in the set, turned from it past its limits — the set's `turn` for that bone (held at the limit) or, without one, its chain's `range` (the bone follows the pose). Listed as the pose asked it either way: the demand for another drawing of that set.
52
52
  - `TURN` — a line across the body (hips, shoulders, eyes) shorter in the picture than 0.5 of its median length, or reversed, for at least 0.15 s: the body or the head turned. A reading of the pose, made whether or not the map uses the line; it changes no key. The demand for a turned-body drawing, as `END-ON` is for a foreshortened limb.
53
53
 
54
- `--report <file>` writes all of it as JSON (`rig-pose-report/1`): `options` (every figure the run used, `switchTolerance` and `switchMin` included), `scale`, `holds`, `short`, `endOn`, `bends`, `flips`, `gate`, `landings`, `posture`, `folds`, and the demand lists:
54
+ `--report <file>` writes all of it as JSON (`rig-pose-report/1`): `options` (every figure the run used, `switchTolerance`, `switchMin` and `switchAt` included), `scale`, `holds`, `short`, `endOn`, `bends`, `flips`, `gate`, `landings`, `posture`, `folds`, and the demand lists:
55
55
 
56
- - `switches: [{ set, chains, slots, hides, offsets, from, to, frames: [first, last], distance: { max, median }, artTurn: { <bone>: { max, median } } }]` — one per span shown; `from` is the first frame's time, `to` the time the base art comes back (the animation's end when the span runs to it), `distance` in degrees over the frames shown, `artTurn` |wrap(target − drawn)| in degrees per bone the set has a `drawn` angle for.
56
+ - `switches: [{ set, chains, slots, hides, offsets, from, to, frames: [first, last], distance: { max, median }, artTurn: { <bone>: { max, median } }, moved: { on, off } }]` — one per span shown; `from` is the first frame's time, `to` the time the base art comes back (the animation's end when the span runs to it), `distance` in degrees over the frames shown, `artTurn` |wrap(target − drawn)| in degrees per bone the set has a `drawn` angle for. `moved.on` is the boundary the span starts on (null from the first frame), `moved.off` the one the base art or the next set comes back on (null to the end), each `{ frame: [selected, keyed], frames, seconds, window: [first, last], speed: { old, new } }`: signed frames and seconds, the frames it could move to, and the speed in degrees per frame on the old and the new frame. A boundary between two sets is the same entry in both.
57
57
  - `uncovered: [{ slot, attachment, chains, times }]` — every entry of `folds` on the times its slot still shows that mesh, with the map's chains that move it (the deepest: the arm, not the trunk it hangs from). `rigc render --folds` reads only what the pose shows, so a mesh swapped out or hidden by a switch is not read on those frames at all; `uncovered` is the fold times less the switched ones.
58
58
  - `endOn` — above, under the short hold.
59
59
  - `turns: [{ line, joints, frames, seconds, minFraction, reversed }]` — `line` is `hip`, `shoulder` or `eye`.
@@ -167,6 +167,12 @@ Bone rotation cannot carry every pose: an arm behind the head, a forearm toward
167
167
 
168
168
  **When one shows.** Per frame and per set, the distance is the largest difference, in degrees, between a segment of its chains as the retarget asked for it (after holds, smoothing, the short hold, the flip guard and the hinge check; before any range clamp) and the same segment in its key skeleton. On each frame the nearest set within `--switch-tolerance` (default 25°) shows, ties going to the map's order; a run of one set shorter than `--switch-min` (default 0.25 s) is dropped and the base art shows there. Both figures were fixed before a run ([#6](https://github.com/rig-c/pose/issues/6)). Sets compete only with those they share a slot (shown or hidden) or an offset bone with, so a drawing of each arm can show at once; two that compete never overlap.
169
169
 
170
+ **Where a switch is keyed: `--switch-at`.** By default (`first`) a boundary — the frame a set starts showing on, and the frame the base art or the next set comes back on — is where the selection above puts it: the first frame within the tolerance, often a frame where the arm is slowing into the pose, so the change of drawing can be seen on a nearly still arm. `--switch-at peak` moves each boundary to the frame within ±`--switch-min`/2 (±3 frames at 24 fps with the default 0.25 s) where the chains it changes turn fastest, so the cut lands in motion ([#31](https://github.com/rig-c/pose/issues/31)). The speed on a frame is the largest |angular speed| in degrees per frame, a central difference of the target world angles (one-sided on the first and last frame), over every segment of the leaving set's and the arriving set's chains; it is read on the same targets as the distance, before the range clamp and any set's `turn`, which depend on where the spans are. The window is clipped so every span keeps `--switch-min`, a gap between two spans of one set keeps it too (the bridging holds), no boundary reaches its neighbour, and a set with `frames` shows only inside them. In it, a frame qualifies only if the set the move newly shows is within **2 × `--switch-tolerance`** of its key skeleton on every frame it newly shows it on; the boundary goes to the fastest qualifying frame, and stays when none qualifies. A tie goes to the frame nearest the original, then the earlier. A boundary between two sets moves once.
171
+
172
+ The bound exists because speed alone rewarded showing art far from its pose. Unbounded, on HQ's dancer, the peak sat mid-swing: the largest distance over the frames a set showed rose from 25° to 135° (K2), 24° to 64° (K4), 21° to 59° (K1) and 21° to 38° (K3), the sets' `turn` held the arms hard against the pose (`ART-RANGE` runs 24 → 36), and the rig snapped to a hands-low drawing two frames before the arms came down. Bounded, every set stays within 50° (the largest is 43°, K4) and the median speed at the boundaries still rises from 9.7 to 14.2°/frame, but `ART-RANGE` runs still rise, 24 → 31 (K4 1 → 3, K1 6 → 8, K2 6 → 7, K3 2 → 4). That is why `first` is the default and `peak` an option ([#31](https://github.com/rig-c/pose/issues/31), round 2).
173
+
174
+ Either way the `SWITCH` line ends with `moved on …, off …`: each boundary's old and new frame (the same under `first`), the frames and seconds it moved, the speed on both and its window, and the report carries the same under `moved`.
175
+
170
176
  **What is written.** An `attachment` track per slot whose picture changes: the set's attachment (or `null` for a hidden slot) where a span starts, and the slot's setup attachment, as the compiled model states it, where it ends. A direct switch from one set to another puts the first set's slots back to setup on the second's first frame, so an extra slot that is empty at setup goes empty. A switch onto frame *i* is keyed halfway between frames *i*−1 and *i*: a stepped key on the frame's own time fires a frame late wherever a player's accumulated time lands just below it (rig-c's docs/AUTHORING.md §4.5), and halfway both the pose's times and a renderer's `i / fps` see it on frame *i*. Before the first key a slot shows its setup attachment, so no key is written at 0 unless a span starts there. Offsets are `translate` keys at the same times, `stepped`. rig-c's gate checks these keys like any other.
171
177
 
172
178
  **TURN.** The cutoff was measured on the three lines across the body before it was fixed ([#6](https://github.com/rig-c/pose/issues/6)): on the 13 s front-facing dance their picture length never drops under 0.81 of its median and none reverses; on a second sequence the hip line falls to 0.17 at frames 465–491 (side-on) and the eye line to 0.22 at 26–58 (head in profile), and 3 frames reverse. 0.5 lies between. A line is reversed when it points more than 90° from the subject's right joint to the left (front-facing, it points at 0°). The lines are read after holds and smoothing, held and smoothed as the map's points are even where the map does not use them; a line whose points are good on no frame is named as not measured.
@@ -175,6 +181,84 @@ Bone rotation cannot carry every pose: an arm behind the head, a forearm toward
175
181
  **A stopgap window: `frames`.** A set may state `"frames": [[first, last], …]`: it shows only inside those windows, and on them it is chosen over any set within tolerance that has no window. It is for two sets whose key skeletons the angles cannot tell apart. Measured on HQ's dancer, the hands clasped behind the back and the hands low in front differ by only 2–21°, and the wrist and hand-point scores do not separate them either; the cue that might is #33's depth state. It is recorded in the map as a stopgap, not a selection rule.
176
182
 
177
183
  **How far a set's art may turn: `turn`.** A drawing reads right only near the angle it was painted at. A set may state, per bone with a `drawn` angle, how far its art may turn from it, `"turn": { "sleeve_r0": [-20, 20] }`. While the set shows, the bone is held within it: its target becomes drawn + clamp(wrap(target − drawn), min, max), each bone on its own world angle, so a forearm under a held upper arm keeps its own target. The landing check measures against the held angle. The overflow the pose asked for is listed as `ART-RANGE`. A `turn` on a bone the set has no `drawn` angle for is refused (`MAP_ALT_TURN`).
184
+ ## Compare: the rig's render against the video
185
+
186
+ ```sh
187
+ rig-pose compare --reference ref.pose.json --render render.pose.json --map map.json \
188
+ [--retarget-report report.json] [--reference-hands ref.wholebody.json --render-hands render.wholebody.json] \
189
+ [--cloth-reference ref.cloth.json --cloth-render render.cloth.json --cloth-windows 3.9-7.4,7.6-11.1] \
190
+ [--report compare.json] [--sheet compare.png]
191
+ ```
192
+
193
+ Polishing a retarget used to mean rendering it, having a person look, and hearing what was wrong. Most of what a person names can be measured with the same pose detector on both sides ([#38](https://github.com/rig-c/pose/issues/38)). `compare` reads two `rig-pose-sequence/1` files on one time base: the **reference** (the source video's keypoints) and the **render** (the same detector run on `rigc render` frames of the retargeted motion). It lists the differences per body unit, worst first, each with the fix it suggests. The person is then needed only for taste. The detector stays outside rig-pose, as it does for `retarget`.
194
+
195
+ The figures and bars below are fixed. They were set in [#38](https://github.com/rig-c/pose/issues/38) before any render was measured, and they are grading lines for [rig-c/exam](https://github.com/rig-c/exam). A change to one is a new comment on that issue first.
196
+
197
+ **One time base.** Both sequences have the same number of frames, and each frame's times differ by at most **1 ms**. Render the motion at the reference's rate, frame for frame. Anything else is refused (`COMPARE_FRAMES`, `COMPARE_TIMES`, with the first frames that differ). The hands files and cloth tracks are held to the same rule against the reference (`COMPARE_HANDS_FRAMES`/`_TIMES`, `COMPARE_CLOTH_FRAMES`/`_TIMES`).
198
+
199
+ **Positions are normalised per side.** A position is divided by that side's own **hip width**: the median over its frames of |r_hip − l_hip|, in picture px. Directions are compared as Spine angles of the picture segment, |wrap(render − reference)|. Every joint with a position counts, whatever its score; only hand visibility reads scores. A frame where either side lacks a point the measure needs is not measured, and it is never over the bar.
200
+
201
+ **A finding** is a run of frames over a measure's bar lasting **≥ 0.15 s**. A run is timed from the first frame's time to the end of the last frame, as `END-ON` is: 4 frames at 24 fps (0.167 s) are a finding, 3 (0.125 s) are not. Runs **one frame apart** merge, and the merged gap is not counted as over the bar. A finding's worst frame is the frame of its largest figure. Within a measure, findings are listed worst first.
202
+
203
+ | unit | measure | figure, per frame | bar | fix class |
204
+ | --- | --- | --- | --- | --- |
205
+ | head | eye-line tilt | \|direction difference\| of r_eye → l_eye | **> 15°** | not derivable |
206
+ | arms | direction, per mapped arm segment | \|direction difference\| | **> 20°** | **paint** inside a set's span, **rig** outside |
207
+ | arms | hand visibility, per hand | mean score of the hand's 21 points (or the wrist's score) on each side | reference **≥ 0.6** while render **< 0.4** | **depth** (#33) |
208
+ | body | torso length | \|mid_shoulder − mid_hip\| ÷ that side's median | the render's range **< 0.5 ×** the reference's, over the whole run | **motion** |
209
+ | body | hip vertical travel | mid_hip's y in hip widths | the same rule as torso length | **motion** |
210
+ | legs | direction, per mapped leg segment | \|direction difference\| | **> 20°** | **paint** inside a set's span, **rig** outside |
211
+ | legs | foot plant, per ankle | the ankle's speed in hip widths per frame (from the frame before) | render **> 0.05** while reference **< 0.02** | **motion** |
212
+ | cloth | lag, swing ratio, overshoot, per window | #34's three figures, on each side | outside #34's bands around the reference's figure | **rig** (physics) |
213
+
214
+ Why these bars ([#38](https://github.com/rig-c/pose/issues/38)):
215
+
216
+ - **20° for a segment's direction.** HQ's rule for a set's art is `drawn` ± 20°: the art may turn up to 20° from the angle it was painted at and still read. A bigger difference is an arm that looks different.
217
+ - **0.6 and 0.4 for a hand.** On the 13 s dance, the hands in front (S4) score 0.53–0.88 and the hands hidden behind the back (S1) score 0.41–0.57. 0.6 and 0.4 sit outside that overlap, one on each side.
218
+ - **15°** for the head's tilt, **0.05 / 0.02** hip widths per frame for a sliding foot, and **0.5 ×** of the reference's range for a missing bounce were fixed with the others before the dancer was measured.
219
+
220
+ **Units and segments.** The map says which segments are compared. An **arm** segment joins two of one side's shoulder, elbow and wrist, and a **leg** segment two of one side's hip, knee and ankle. A segment's name is `chain/bone`. Other chains, such as the trunk, the pelvis or the head, are not compared as segments. The head is measured on its eye line whatever the map says, and the body on its torso and hip. A mapped arm or leg segment whose point has a position on no frame of one side is refused (`COMPARE_JOINT_MISSING`, naming the chain, the bone, the point and the side).
221
+
222
+ **The fix class** is given only when it can be derived:
223
+
224
+ - **An arm or leg direction.** The finding's worst frame is checked against the spans in `--retarget-report` (its `switches`: each set's frames and the chains it depicts). Inside a span of a set that depicts the segment's chain, the class is **paint**: the drawing shown there is not this pose. Outside every such span it is **rig**: the art does not show the bone's direction. Without a report, a chain no set of the map depicts is **rig**. A chain some set depicts is not derivable, and the line names those sets.
225
+ - **Hand visibility** is **depth** ([#33](https://github.com/rig-c/pose/issues/33)): the hand is drawn behind something.
226
+ - **A foot slide or no bounce** is **motion**.
227
+ - **Cloth** is **rig**: the cloth is the rig's physics.
228
+ - **A head tilt** has no class in #38, so the line says it is not derivable.
229
+
230
+ **Hand visibility.** `rig-pose-sequence/1` carries body-18 only, so the hand points come from COCO-WholeBody files, one per side: `--reference-hands` and `--render-hands`. The shape is `{ "spec"?: "coco-wholebody-133", "fps"?, "width"?, "height"?, "source"?, "frames": [{ "t", "points": [[x, y, score] × 133] | null }] }`. The subject's left hand is points 91–111 and the right hand 112–132. A frame's figure is the mean score of the hand's 21 points. A `null` frame (nobody found) is not measured. Without the files, the wrist's score is read instead: an absent or `missing` wrist reads 0, as COCO's v = 0 does. The summary line then says `from wrist scores`. One of the two files without the other is refused with exit 2, because the two sides must be measured the same way.
231
+
232
+ **Body.** Each figure is read over the whole run, so the unit gives at most one finding per measure, `no bounce`, spanning every frame. Its figure is the ratio of the ranges, render ÷ reference. Its worst frame, the one the sheet draws, is the reference's frame farthest from its median. The summary line always gives both ranges.
233
+
234
+ **Cloth.** Cloth is measured only when both cloth tracks are given, with `--cloth-windows`. Without them the unit reads `not measured`. A cloth track is HQ's `rig-c-cloth-track/0`: `{ "spec": "rig-c-cloth-track/0", "subject"?, "method"?, "source"?, "fps"?, "frames_count"?, "units"?, "hipWidth": { "median", "unit": "picture px" | "rig world units" }, "origin"?, "axes"?, "skirtSwing"?, "frames": [{ "i", "t", "hip_x", "hip_y", "hip_w", "tilt_deg", "hem_mid", "hem_l", "hem_r", "hem_l_y", "hem_r_y", "hem_band_cx", "skirt_cx", "skirt_top", "skirt_bottom" }] }`, the frame keys all numbers. Every position is already divided by the subject's own median hip width, with x to the right. `hip_x` is the hips' midpoint and `hem_mid` the midpoint of the hem's leftmost and rightmost points in the lower half of the skirt ([#34](https://github.com/rig-c/pose/issues/34)'s hem); only those two and `t` are read. Any other spec, or a key outside that list, is refused (`COMPARE_CLOTH`). The three figures are #34's, as HQ's skirt criteria define them, ported from HQ's measuring script. On HQ's per-frame record of the dancer they reproduce its numbers exactly (reference A 0.958 / 2.634 / 0.061, B 0.696 / 1.409 / 0.000). Read through `rig-c-cloth-track/0`, whose values are rounded to 5 decimals, the reference's lag in A reads 0.959.
235
+
236
+ - Both signals are smoothed with a Gaussian, σ = 1 frame over ± 4 frames, ends padded, before differentiating (numpy's `gradient`).
237
+ - **lag**, in frames, hem after hip positive: the peak of the normalised cross-correlation of hip-x velocity with hem-x velocity over the window, searched over −6…+12 frames and refined by a parabola.
238
+ - **swing ratio**: per hip stroke, the hem's excursion over the hip's. A stroke runs from one hip reversal (a zero crossing of hip velocity) to the next and moves at least the stroke threshold. A wiggle under the threshold is dropped as a pair. The hem is read `round(lag)` frames after each reversal. The figure is the median over the window's strokes.
239
+ - **overshoot**: per stroke, how far the hem keeps travelling in the stroke's direction after the hips turn, within half of the next stroke (or 12 frames after the last), as a fraction of the hip stroke. The figure is the median.
240
+ - **The stroke threshold** is 0.2 *reference* hip widths: one physical size on both sides (the criteria's Revision 1), expressed in the render's own hip widths. A render track in rig world units is carried by the retarget's scale, `scale.factor` in `--retarget-report` (rig units per reference px): 0.2 × reference hip width × factor ÷ render hip width. On the dancer that is 0.2 × 128.1 × 0.54152 ÷ 92.202 = 0.1505, the criteria's own figure. Without the report it is refused (`COMPARE_CLOTH_SCALE`). A render track in picture px is carried by the torso, as the retarget scales the rig: 0.2 × (reference hip width ÷ reference torso) ÷ (render hip width ÷ render torso), each torso the sequence's median mid_hip → mid_shoulder. A reference track not in picture px is refused (`COMPARE_CLOTH_UNITS`). The `CLOTH` lines print both thresholds and how the render's was carried.
241
+ - **The bands**, per window, around the reference's own figure: lag ± 1.0 frame; swing ratio ± 25 %; overshoot ± 0.10, absolute, floored at 0. A render figure outside its band is a finding over the window's frames. A window with no stroke on one side is listed as not measured.
242
+ - A window is `t0-t1` in seconds, and windows are named A, B, … in the order given. A window's frames are `round((t − t₀) × fps)` up to the end, exclusive. A window of 12 frames or fewer is refused, since the lag search reaches 12.
243
+
244
+ **Output.** The first line gives the frames, the duration, both hip widths, and the report the spans came from. Then come one summary line per unit (`HEAD`, `ARMS`, `BODY`, `LEGS`, `CLOTH`) and one `FIND` line per finding. A summary line gives the unit's findings and, per measure, its findings, its worst figure with the subject and frame, the frames over the bar (in runs of any length), and the bar. A `FIND` line gives the unit, the measure, the subject and its points, the worst figure with both sides' readings and its frame, the frames and seconds, the frames over the bar, the bar, and the fix class with the reason. With cloth, a `CLOTH` line per window gives both sides' figures. The run ends with the number of findings.
245
+
246
+ `--report <file>` writes the same as JSON (`rig-pose-compare/1`):
247
+
248
+ - `inputs`, `frames`, `duration`, `hipWidth: { reference, render }`, `torso: { reference, render }` (medians, px);
249
+ - `rule: { findingSeconds, findingGap, timeTolerance }` and `bars` (every bar above, by name);
250
+ - `units: [{ unit, findings, notMeasured, measures: [{ measure, subjects, source, findings, overBar, notMeasured, bar, ranges? }] }]`;
251
+ - `findings: [{ unit, measure, subject, points, chain, frames: [first, last], seconds, over, worst: { frame, t, figure, reference, render }, figureUnit, bar, fix, fixWhy }]`;
252
+ - `cloth`: `null`, or `{ windows: [{ name, t, frames, reference, render, band }], stroke: { reference, render, carried } }`.
253
+
254
+ Numbers are rounded to 4 decimals, once, at the writer.
255
+
256
+ `--sheet <png>` draws the reference skeleton beside the render skeleton at each finding's worst frame, one row per finding, with the finding's own points marked in yellow. It never draws a video frame. A cloth finding is a figure over a window, with no single frame, so it has no row. When no finding has a frame, no sheet is written and the run says so.
257
+
258
+ **The control** is the reference compared with itself. It must report no finding in any unit, and every summary line says `0 findings`. On HQ's dancer (313 frames, RTMPose, with its whole-body hand points and the retarget's report) it does.
259
+
260
+ Exit codes: `0` compared, whether it found anything or not, since a finding is not a failure; `1` refused, with every reason printed; `2` bad invocation.
261
+
178
262
  ## Requirements
179
263
 
180
264
  [Bun](https://bun.sh) ≥ 1.2 on `PATH` — rig-pose, like rig-c, runs its TypeScript sources directly. `rig-c` comes as a dependency. With `@esotericsoftware/spine-core` installed beside it, rig-c's gate runs the spine-core round trip; without it, rig-c's own gate, and the gate's report says which.
package/cli.ts CHANGED
@@ -9,7 +9,9 @@
9
9
  import { readFileSync, writeFileSync } from 'node:fs';
10
10
  import pkg from './package.json' with { type: 'json' };
11
11
  import { convertCoco17, writePoseSequence } from './src/pose.ts';
12
+ import { parseWindows } from './src/compare.ts';
12
13
  import { lineOf, Refusal } from './src/problems.ts';
14
+ import { runCompare, type CompareArgs } from './src/runcompare.ts';
13
15
  import { runRetarget, type RetargetArgs } from './src/run.ts';
14
16
 
15
17
  const HELP = `rig-pose ${pkg.version} — motion from a pose sequence onto a rig-c rig
@@ -17,6 +19,7 @@ const HELP = `rig-pose ${pkg.version} — motion from a pose sequence onto a rig
17
19
  Usage:
18
20
  rig-pose retarget --rig <rig> --map <map.json> --pose <pose.json> --out <motion.json> [--name dance] [flags]
19
21
  rig-pose convert --coco <coco17.json> --out <pose.json>
22
+ rig-pose compare --reference <pose.json> --render <pose.json> --map <map.json> [flags]
20
23
  rig-pose --version | --help
21
24
 
22
25
  retarget
@@ -49,6 +52,10 @@ retarget
49
52
  --switch-tolerance <d> a set of alternate drawings (the map's "alternates") may show on a frame where
50
53
  every segment of its chains is within d degrees of its key skeleton (default: 25)
51
54
  --switch-min <s> a run of one set shorter than s seconds is dropped (default: 0.25)
55
+ --switch-at first|peak where a switch is keyed: first (default) keeps the frame the selection gave it;
56
+ peak moves each boundary to the frame within ±switch-min/2 where the chains it
57
+ changes turn fastest, among those that newly show a set only on frames within
58
+ 2 × the switch tolerance of its key skeleton (it stays when none qualifies)
52
59
  --profile spine|spine-html rigc's gate profile (default: spine, rigc's own default)
53
60
  --build <dir> keep the gated build (skeleton.json, .atlas, .model.json) here
54
61
  --render <dir> keep rigc's render of the animation (frames, contact.png, folds.json) here
@@ -69,7 +76,30 @@ convert
69
76
  keypoints in COCO's order as 17 [x, y, score] or 51 numbers, or null
70
77
  --out <file> the rig-pose-sequence/1 it becomes; neck = the shoulders' midpoint
71
78
 
72
- Exit codes: 0 written, 1 refused (every reason printed), 2 bad invocation.
79
+ compare
80
+ --reference <file> rig-pose-sequence/1: the source video's keypoints
81
+ --render <file> rig-pose-sequence/1: the same detector on rigc's render of the retargeted motion,
82
+ rendered at the reference's rate — same frame count, times within 1 ms
83
+ --map <map.json> rig-pose-map/1: its arm and leg segments are compared
84
+ --retarget-report <f> the retarget's --report: a limb finding inside a set's span suggests paint, outside rig
85
+ --reference-hands <f> --render-hands <f>
86
+ COCO-WholeBody keypoints ({ frames: [{ t, points: [[x, y, score] x 133] }] }) for
87
+ hand visibility from the 21 hand points; without them, from the wrist scores
88
+ --cloth-reference <f> --cloth-render <f> --cloth-windows <t0-t1,...>
89
+ cloth tracks (rig-c-cloth-track/0: hip_x and hem_mid per frame, in hip widths) and
90
+ the windows, in seconds, #34's lag, swing ratio and overshoot are read over; a render
91
+ track in rig world units needs --retarget-report (its scale); without the tracks,
92
+ cloth reads "not measured"
93
+ --report <file> write the comparison as JSON (rig-pose-compare/1)
94
+ --sheet <png> draw the reference and render skeletons side by side at each finding's worst frame
95
+
96
+ A finding is a run of frames over a bar lasting >= 0.15 s (gaps of one frame merged), worst first.
97
+ The bars (README, "Compare"): arm and leg segment direction > 20 deg; hand visibility reference >= 0.6
98
+ while render < 0.4; foot plant render > 0.05 while reference < 0.02 hip widths/frame; eye-line tilt
99
+ > 15 deg; torso length and hip height range under 0.5 x the reference's; cloth outside #34's bands.
100
+ Exit 0 whenever it compared, findings or not.
101
+
102
+ Exit codes: 0 written (compare: compared), 1 refused (every reason printed), 2 bad invocation.
73
103
  `;
74
104
 
75
105
  class Usage extends Error {}
@@ -124,9 +154,11 @@ function shortOf(f: Map<string, string>): number | 'off' {
124
154
  }
125
155
 
126
156
  function retargetArgs(argv: string[]): RetargetArgs {
127
- const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'min-score', 'smooth', 'flip', 'short', 'bends', 'bend-tolerance', 'switch-tolerance', 'switch-min', 'profile', 'build', 'render', 'report'], ['coco']);
157
+ const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'min-score', 'smooth', 'flip', 'short', 'bends', 'bend-tolerance', 'switch-tolerance', 'switch-min', 'switch-at', 'profile', 'build', 'render', 'report'], ['coco']);
128
158
  const bends = f.get('bends') ?? 'refuse';
129
159
  if (bends !== 'refuse' && bends !== 'clamp') throw new Usage(`--bends is "${bends}"; it must be refuse or clamp`);
160
+ const switchAt = f.get('switch-at') ?? 'first';
161
+ if (switchAt !== 'peak' && switchAt !== 'first') throw new Usage(`--switch-at is "${switchAt}"; it must be peak or first`);
130
162
  const profile = f.get('profile') ?? 'spine';
131
163
  if (profile !== 'spine' && profile !== 'spine-html') throw new Usage(`--profile is "${profile}"; it must be spine or spine-html`);
132
164
  const name = f.get('name') ?? 'dance';
@@ -148,6 +180,7 @@ function retargetArgs(argv: string[]): RetargetArgs {
148
180
  bendTolerance: num(f, 'bend-tolerance', 0, (n) => n >= 0 && n < 180, 'degrees in [0, 180)'),
149
181
  switchTolerance: num(f, 'switch-tolerance', 25, (n) => n >= 0 && n < 180, 'degrees in [0, 180)'),
150
182
  switchMin: num(f, 'switch-min', 0.25, (n) => n >= 0, 'seconds ≥ 0'),
183
+ switchAt,
151
184
  profile,
152
185
  build: f.get('build'),
153
186
  render: f.get('render'),
@@ -155,6 +188,36 @@ function retargetArgs(argv: string[]): RetargetArgs {
155
188
  };
156
189
  }
157
190
 
191
+ function compareArgs(argv: string[]): CompareArgs {
192
+ const f = parseFlags(argv, ['reference', 'render', 'map', 'retarget-report', 'reference-hands', 'render-hands', 'cloth-reference', 'cloth-render', 'cloth-windows', 'report', 'sheet'], []);
193
+ const pair = (x: string, y: string) => {
194
+ if (f.has(x) !== f.has(y)) throw new Usage(`--${f.has(x) ? x : y} is given without --${f.has(x) ? y : x}; the two are read together, so both sides are measured the same way`);
195
+ };
196
+ pair('reference-hands', 'render-hands');
197
+ pair('cloth-reference', 'cloth-render');
198
+ let clothWindows: CompareArgs['clothWindows'];
199
+ if (f.has('cloth-reference') && !f.has('cloth-windows')) throw new Usage('--cloth-reference and --cloth-render need --cloth-windows <t0-t1,...>: the figures are read per window');
200
+ if (f.has('cloth-windows')) {
201
+ if (!f.has('cloth-reference')) throw new Usage('--cloth-windows is given without --cloth-reference and --cloth-render');
202
+ const w = parseWindows(f.get('cloth-windows')!);
203
+ if (typeof w === 'string') throw new Usage(`--cloth-windows is "${f.get('cloth-windows')}": ${w}; it must be t0-t1[,t0-t1...] in seconds`);
204
+ clothWindows = w;
205
+ }
206
+ return {
207
+ reference: need(f, 'reference'),
208
+ render: need(f, 'render'),
209
+ map: need(f, 'map'),
210
+ retargetReport: f.get('retarget-report'),
211
+ referenceHands: f.get('reference-hands'),
212
+ renderHands: f.get('render-hands'),
213
+ clothReference: f.get('cloth-reference'),
214
+ clothRender: f.get('cloth-render'),
215
+ clothWindows,
216
+ report: f.get('report'),
217
+ sheet: f.get('sheet'),
218
+ };
219
+ }
220
+
158
221
  export function main(argv: string[]): number {
159
222
  const [cmd, ...rest] = argv;
160
223
  if (cmd === undefined || cmd === '--help' || cmd === '-h' || cmd === 'help') {
@@ -174,6 +237,10 @@ export function main(argv: string[]): number {
174
237
  runRetarget(retargetArgs(rest), (l) => process.stdout.write(`${l}\n`));
175
238
  return 0;
176
239
  }
240
+ if (cmd === 'compare') {
241
+ runCompare(compareArgs(rest), (l) => process.stdout.write(`${l}\n`));
242
+ return 0;
243
+ }
177
244
  if (cmd === 'convert') {
178
245
  const f = parseFlags(rest, ['coco', 'out'], []);
179
246
  const input = need(f, 'coco');
@@ -183,7 +250,7 @@ export function main(argv: string[]): number {
183
250
  process.stdout.write(`rig-pose convert: ${seq.frames.length} frame(s) of COCO-17 → body-18; wrote ${out}\n`);
184
251
  return 0;
185
252
  }
186
- process.stderr.write(`rig-pose: unknown command "${cmd}" — the commands are: retarget, convert\n`);
253
+ process.stderr.write(`rig-pose: unknown command "${cmd}" — the commands are: retarget, convert, compare\n`);
187
254
  return 2;
188
255
  } catch (e) {
189
256
  if (e instanceof Usage) {
@@ -192,7 +259,7 @@ export function main(argv: string[]): number {
192
259
  }
193
260
  if (e instanceof Refusal) {
194
261
  for (const p of e.problems) process.stderr.write(`FAIL ${lineOf(p)}\n`);
195
- process.stderr.write(`rig-pose ${cmd}: ${e.problems.length} problem(s) — nothing written\n`);
262
+ process.stderr.write(`rig-pose ${cmd}: ${e.problems.length} problem(s) — ${cmd === 'compare' ? 'not compared' : 'nothing written'}\n`);
196
263
  return 1;
197
264
  }
198
265
  throw e;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/pose",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Puts motion from a body-18 pose sequence onto a rig built with rig-c: bone keys in rig-c's motion format, checked through rig-c's gate before anything is written. A CLI for agents that cannot see the rig.",
5
5
  "type": "module",
6
6
  "bin": {