@rig-c/pose 0.37.1 → 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 CHANGED
@@ -25,7 +25,7 @@ The map names **chains**: each limb as bones that descend from one another, pare
25
25
  ## Clean first: `rig-pose clean`
26
26
 
27
27
  ```sh
28
- rig-pose clean --pose dance.scan.json --out dance.pose.json [--loop] [--legs 5] [--rest 7] [--hips-inferred] [--hold off] [--hold-speed 0.015] [--hold-reach 0.03]
28
+ rig-pose clean --pose dance.scan.json --out dance.pose.json [--loop] [--legs 5] [--rest 7] [--hips-inferred] [--hold off] [--hold-speed 0.015] [--hold-reach 0.03] [--depth walk|run]
29
29
  ```
30
30
 
31
31
  `retarget` reads only a sequence that `clean` wrote ([#67](https://github.com/rig-c/pose/issues/67)). Where the video's camera moves, the order is **scan → clean → retarget** ([#80](https://github.com/rig-c/pose/issues/80)): a zoom step is a jump of the picture, and `clean`'s spike and smoothing steps would read it as motion of the body, so `tools/scan.py` takes the camera out of the raw sequence first and `clean` reads what it writes. A pose model reads each frame alone, so a hanging wrist moves ±20 px a frame and a cloak-hidden leg jumps 100+ px, and every key carries that shake. `clean` takes it out once, at the sequence, where it can be inspected. A sequence without the record is refused by name (`POSE_NOT_CLEANED`), and the message is the `clean` command to run on that file. There is no flag to skip the step. Windows of 1 (`--legs 1 --rest 1`) are a legitimate run that smooths nothing, and they are recorded as such.
@@ -68,7 +68,7 @@ Each hold is printed (`HOLD`, joint by joint, with its largest speed and reach,
68
68
 
69
69
  The `CLEAN` line names, for each region, the joints held over which frames and the frames not held with their test and figure and the seams cross-faded (`HOLD frames a–b: held nose, neck, … a–b; seams cross-faded: neck f (step …); not held: l_elbow c–d (reach … > 0.03)`), or says `no HOLD`. `--hold off` turns the step off: the positions are those of a clean without it, and the record carries `"hold": "off"` and none of `holds`, `seams` and `unheld`. A record written before #91 names none of them, and is read as it was.
70
70
 
71
- **Measured hips: `tools/pelvis.py`** ([#86](https://github.com/rig-c/pose/issues/86), not shipped) reads the pelvis line where the eye reads it under a skirt, off the skirt's waistband, and runs after `clean`: scan → clean → pelvis → retarget. It takes the video and a sequence `clean` wrote without `--hips-inferred`. It refuses by name, writing nothing:
71
+ **Measured hips: `tools/pelvis.py`** ([#86](https://github.com/rig-c/pose/issues/86), not shipped; `rig-pose pelvis`, [below](#the-pelvis-track-rig-pose-pelvis), supersedes it — it reads only a dark skirt on a non-white ground and turns the hip pair, where the command reads line art and light garments and writes a track of its own) reads the pelvis line where the eye reads it under a skirt, off the skirt's waistband, and runs after `clean`: scan → clean → pelvis → retarget. It takes the video and a sequence `clean` wrote without `--hips-inferred`. It refuses by name, writing nothing:
72
72
 
73
73
  - a sequence with no `cleaned` record (`PELVIS_NOT_CLEANED`);
74
74
  - one whose hips are already marked (`PELVIS_HIPS_INFERRED`);
@@ -101,6 +101,61 @@ Both hips are marked `inferred` on every frame and turned about their midpoint t
101
101
 
102
102
  A retarget of the cleaned `dance-a` fixture finds the same jumps (`FEET`) as the same run on the hand-cleaned reference; its hip rotate sd over frames 94–175 is 4.574, against 4.58.
103
103
 
104
+ ## The pelvis track: `rig-pose pelvis`
105
+
106
+ ```sh
107
+ rig-pose pelvis --pose dance.scan.json --frames frames/ --out dance.pelvis.json [--mode silhouette|waistband] [--person <id>]
108
+ rig-pose pelvis --pose dance.scan.json --pelvis-keys pelvis.keys.json --out dance.pelvis.json
109
+ ```
110
+
111
+ A pose detector keeps little of a drawn pelvis tilt ([#288](https://github.com/rig-c/pose/issues/288)): on a frontal walk drawn with the pelvis line in every key, its hips kept 4.5° of a 30.5° swing (correlation +0.27), and `clean` and `retarget` carried that faithfully, so the tilt was lost at the scan. The drawing has it, so `pelvis` reads it off the drawing and writes it into the sequence as its own track, which `clean` smooths and `retarget` keys. The order is **scan → pelvis → clean → retarget**: a sequence that already carries a `cleaned` record is refused (`PELVIS_AFTER_CLEAN`), since `clean` must smooth the track, and so is one that already carries a track (`PELVIS_TRACK`).
112
+
113
+ `--frames` is a folder of PNG frames, one per sequence frame, taken in file-name order and of the sequence's size (`PELVIS_FRAMES`, `PELVIS_FRAME`). There is no video decoding: extract the frames first. The hip keypoints place the reading — where the hips are, and which of them is the figure's right — but their angle is not read. Two readers, both on the hip span (the columns between the two hip keypoints):
114
+
115
+ - **`silhouette`** (the default): the straightest edge of the drawn figure across the hip span, near the hips' height. An edge pixel is a figure pixel (a channel more than 64 from the background colour, the median of the frame's border) beside a background pixel, so a drawn outline and a filled shape both have one. Every straight line within ±40° of level and one hip width of the hips is scored by the edge pixels within 1 px of it; the best is read when its pixels cover at least 60 % of the span's columns, and fitted by least squares (one 3-MAD rejection pass) within 2.5 px rms. It reads a drawn pelvis line, or the edge of a drawn hip.
116
+ - **`waistband`**: the top edge of a garment covering the hips (a skirt, shorts). The garment's colour is the median at the hips' midpoint, and must not be the background's. In each column of the span, the first row from 1.5 hip widths above the hips where three pixels in a row carry it (within 40 per channel) is the top edge, and the line is fitted through those tops on the same terms. The colour must **hold** on its patch: at least 75 % of the patch's pixels carry its median ([#293](https://github.com/rig-c/pose/issues/293)). A dithered or paletted garment (a 216-colour GIF, whose channels alternate between palette steps pixel by pixel) does not, so its pixels are read through the mean of a 5×5 window, which does. Hips on the band's top edge give a patch that is half garment and half ground, so the patch is also tried one and two patch radii (0.3 hip widths) below the hips, and the scan then runs down to that patch. The first patch that holds and is not the background gives the colour, trying each height as is and then through the window mean. If none holds, the midpoint's median is read as before. The `PELVIS` line counts the frames read through the window mean and those whose colour was found below the hips.
117
+
118
+ Both pass over the forearms: pixels within 0.15 hip widths of a segment elbow → wrist (1.5 times that of a wrist, the hand) are not read, and a waistband top right under a forearm is not taken. A frame neither reader can read is **declined**: its `pelvisTilt` is `null` — marked, not guessed — and a `DECLINED` line names the frames and the reason (the pelvis blank, no garment, no straight edge across 60 % of the span, the hips side-on or missing). The `PELVIS` line counts the frames read and declined and gives the swing read.
119
+
120
+ `--pelvis-keys` is the hand override where neither reader applies: `rig-pose-pelvis-keys/1`, `{ "spec": "rig-pose-pelvis-keys/1", "keys": [ { "frame", "deg" } ], "note"? }`. Frames are the sequence's and strictly increase, `deg` is in `pelvisTilt`'s sense; the tilt is linear between keys, the short way round, and held before the first and after the last. A frame may be fractional ([#294](https://github.com/rig-c/pose/issues/294)): a scan interpolated from drawn keys puts them between its frames — k × 4.75 for 8 keys over 38 frames — and a key rounded onto the grid moves up to half a frame and bends the tilt between. On that synthetic case (24 fps, a known tilt sampled at the keys' times), the tilt written on every frame is the keys' linear interpolation within 0.0005° (the writer's rounding), where the same keys rounded to whole frames miss it by up to 1.19° (`test/pelvis.test.ts`). A key outside the sequence's span, `[0, frames − 1]`, is refused by name. Fractional frames stay `/1`: they are a wider value of the same key, and a reader before #294 refuses them by name (`"frame" is number 4.75; it must be a frame index in [0, N]`) rather than misreading them. Anything malformed is refused by name (`PELVIS_KEYS`), and the source is `"keys"`. `--person` picks the person when there is more than one, and only that person is written.
121
+
122
+ **The track** is `pelvisTilt` on every frame and `pelvisSource` (`"silhouette"`, `"waistband"` or `"keys"`) on the sequence, written as `rig-pose-sequence/1.1` ([below](#the-pose-rig-pose-sequence1)). `pelvisTilt` is Spine degrees — counter-clockwise, y up — of the direction from the figure's right hip to its left, the angle `retarget` reads off `r_hip → l_hip`: about 0 for a figure facing the camera, about ±180 facing away.
123
+
124
+ **`clean`** smooths `pelvisTilt` as its own track, not re-read from the hips: the quadratic Savitzky–Golay over the legs' window (`--legs`, the hips' window), circular with `--loop`, on the frames that carry a reading — a `null` stays `null`. It prints a `PELVIS` line with the largest change, and records `"pelvis": { "window" }` in the record's `params` (the `CLEAN` line ends `pelvisTilt smoothed as its own track over N frames`). A sequence with a track whose record lacks it — the track added after `clean` — is refused by name (`POSE_PELVIS`).
125
+
126
+ **`retarget`** keys the map's pelvis chain — one bone, joints `r_hip → l_hip` (either way round) — from the track on every frame it reads, and from the hip line on the frames it declined; such a bone is not held across the hip pair's foreshortening (#184), since the pair no longer sets its angle. The `PELVIS` line says which: `PELVIS: from <source>, swing N°` (N the peak-to-peak of the bone's rotate keys), with the frames on the hip line named; `PELVIS: from hip line (no track)` when the sequence has none; `PELVIS: from hip line (the <source> track read none of the N frames)` when it read nothing; and `PELVIS: not keyed — …` when the sequence has a track and the map no pelvis chain. The report carries it as `pelvis`. A sequence without a track keys exactly what it keyed before.
127
+
128
+ On the synthetic walk with a drawn tilt ([`examples/walk/make-pelvis.ts`](https://github.com/rig-c/pose/blob/main/examples/walk/make-pelvis.ts), the hip keypoints keeping 0.15 of it), posed by rig-c's poser from the gated build, the pelvis swings 30.67° against the drawn 30° (1.02×, r = 1.000) from the `silhouette` track, and 4.51° (0.15×) from the hip line without it; under a garment band whose top edge turns 24° peak to peak out of phase with the hips, the `waistband` track gives 23.76° (0.99×, r = 1.000 against the band's edge); with the pelvis painted out, no frame is read and the motion is the hip line's, byte for byte (`test/pelvis.test.ts`). The same band reduced to the 216-colour palette by a 4×4 ordered dither (the `dithered` variant) is read on 24 of 24 frames, giving 23.95° (1.00×, r = 0.999 against the band's edge). With the hip keypoints also moved up onto the band's edge (`dithered-edge`) it is read on 24 of 24, giving 23.94° (1.00×, r = 0.999); the reader without the change reads none of either.
129
+
130
+ ## Derived depth: `clean --depth`
131
+
132
+ ```sh
133
+ rig-pose clean --pose run.scan.json --out run.pose.json --loop --depth run
134
+ ```
135
+
136
+ A sequence carries x and y only, so nothing downstream can say which limb is nearer the camera ([#281](https://github.com/rig-c/pose/issues/281)). Part of it is in the picture: a segment drawn shorter than its rest length leans out of the picture plane. With `--depth walk|run` (off by default; without it the output is byte for byte what it was), `clean` writes on every frame, for each limb segment whose two joints are placed, `"depth": { "<segment>": { "ratio", "sign", "source": "derived" } }`, read on the cleaned positions. The segments are `r_upper_arm`, `r_forearm`, `r_thigh`, `r_shin` and the same on the left, each from its joint nearer the trunk to the farther one.
137
+
138
+ - **`ratio`** is the segment's drawn length ÷ its rest length. The rest length is, per kind of segment, the larger of the two sides' 95th-percentile picture lengths over the clip: the straight length `--leg-scale` reads (#148). A segment drawn foreshortened on more than 5 % of the clip on both sides has a rest length shorter than its own, and its ratios read high: on the synthetic run below, the forearms (elbows bent 70° forward through the whole stride) rest at 41.05 px against their true 60.
139
+ - **`sign`** is +1 when the segment's far end is nearer the camera than its near end, −1 when it is farther, and 0 when the ratio is within the flat band, **0.95–1.05**, or above it (drawn longer than rest is not a lean). cos 18.2° = 0.95, so a segment within about 18° of the picture plane reads flat; the card set the same band for its flat fixture. Which way a foreshortened segment leans the length cannot say, so the body says it, "forward" being the way the trunk faces:
140
+
141
+ | segment | leans | why |
142
+ | --- | --- | --- |
143
+ | thigh | forward | a hip flexes forward, and extends back too little to foreshorten past the band |
144
+ | shin | forward in a walk, back in a run | the gait rule ([#281](https://github.com/rig-c/pose/issues/281)): a walk's lifted foot comes forward, a run's is kicked back. The knee bends forward in both, so the gait is the flag's value |
145
+ | upper arm | with the opposite leg | in a gait each arm swings with the other side's leg: forward when the other thigh is the more foreshortened, back when its own is, forward (a shoulder flexes forward) when neither is |
146
+ | forearm | forward | an elbow bends toward the back of the body, so the forearm folds forward |
147
+
148
+ The trunk faces the camera (forward is +) when its left shoulder and hip are drawn right of its right ones, away (forward is −) when they are drawn left, and is **side-on** when the shoulders and hips are drawn, on average, less than 0.1 trunk lengths (neck → mid-hip) wide: forward is then across the picture, so every sign on that frame is 0 — undecided, not guessed.
149
+ - **`source`** is `"derived"`. `"measured"` is for a 3D detector, which may fill the same field: it is read, never written here. `clean` carries a measured depth through as it was read (`DEPTH measured depth … carried through`), and refuses `--depth` on one (`CLEAN_DEPTH`), since a derived reading would replace a measurement. A sequence carrying derived depth is refused without `--depth` (`CLEAN_DEPTH`): its depth was read off positions the clean moves.
150
+
151
+ The record says it: `"depth": { "gait" }` in `params`, and the `CLEAN` line ends `depth derived per segment (gait <gait>)`. The `DEPTH` line gives the rest lengths, the band, the segment-frames toward, away and flat, and the frames the trunk faced the camera, faced away and was side-on. The sequence is written as `rig-pose-sequence/1.2` ([below](#the-pose-rig-pose-sequence1)).
152
+
153
+ **Drawn** — `tools/skeleton.ts` (not shipped, the demo's stick-figure pane) colours each segment a frame's `depth` names: toward the camera warm (orange), away cool (blue), flat neutral (grey), the rest of the figure thin and dark, R and L at the wrists and ankles, and a legend in the frame's top-left corner naming the source and the gait. A sequence without depth is drawn exactly as before.
154
+
155
+ **Layered** — `retarget` orders the limbs (`r_arm`, `l_arm`, `r_leg`, `l_leg`) front to back on every frame of a sequence that carries depth. Each joint's depth is its parent's plus its segment's, sign × rest × √(1 − ratio²) px (rest = drawn length ÷ ratio), from the shoulders and hips at 0; a limb's depth is the mean of its middle and end joints'; the nearest comes first, and equal depths (to 0.01 px) share a layer. The `DEPTH` line counts the frames ordered and which limb was in front alone how often, and the report carries `layers` ([below](#what-it-does)). Nothing is keyed from it: the motion is byte for byte the same with and without depth, and changing a rig's draw order from it is the consumer's.
156
+
157
+ On the synthetic fixtures ([`examples/walk/make-depth.ts`](https://github.com/rig-c/pose/blob/main/examples/walk/make-depth.ts): the walk's proportions, made in 3D from joint angles and drawn by a camera in front, behind or beside, so the generator knows every joint's true depth; `test/depth.test.ts`): on the frontal run in place, the 24 shin-frames drawn at ≤ 0.5 of rest (down to 0.409), four on every stride of each leg, read −1 under `--depth run`, as the generator draws them, and +1 under `--depth walk`; on the frontal walk, the 32 shin-frames the generator draws toward the camera read +1, and none reads −1; seen from behind, the run's kicked-back shins read +1; seen from the side, the 480 segment-frames read 0.995–1.000, every sign 0, and no segment is drawn warm or cool. `layers` agrees with the generator on 60 of the run's 60 frames and 44 of the walk's 48, two limbs counting as ordered when their true depths differ by at least the depth a limb can hold while both its segments read flat (√(1 − 0.95²) × (upper + lower ÷ 2): 31.2 px for an arm, 35.9 for a leg), and tied otherwise. The walk's four (frames 6, 18, 30, 42) are where both thighs read flat while the generator has the forward leg 36.4 px in front: the band hides it.
158
+
104
159
  ## What it does
105
160
 
106
161
  ```sh
@@ -109,7 +164,7 @@ rig-pose retarget --rig out/rig --map map.json --sequence dance.pose.json --out
109
164
 
110
165
  **Names** ([#243](https://github.com/rig-c/pose/issues/243)). `--sequence` and `--animation` are the names the rest of the toolchain gives these inputs (rig-c/exam's grader, `rigc render --animation`); `--pose` and `--name`, this command's first names, are the same flags. Both of a pair given with the same value is accepted; with different values the run is refused before anything is read, naming both (`--pose "a.json" and --sequence "b.json" are both given and differ; --sequence is another name for --pose, so give one`). Either spelling writes the same bytes.
111
166
 
112
- **Paths** ([#239](https://github.com/rig-c/pose/issues/239)). `--rig` is the directory @rig-c/parts writes, `<out>/rig/`: it reads `rig.json` and the `motion.json` beside it. Naming the `rig.json` itself reads no motion spec, so a rig that needs its own animations — a slider applies one — is red before anything is added, and the `RIG_GATE_BASE` line says the `motion.json` beside it was not read and names the directory form (or `--base-motion`). When the gate is green without it, the run goes on and `--out` carries none of the rig's own animations, so a `MOTION` line says the same thing, in every output mode ([#250](https://github.com/rig-c/pose/issues/250)). A directory `--out`, `--report` or `--camera` names that is not there yet is made once the run is green, and a `made directory <dir>` line says so and which paths it holds; a red run makes none, and a file where a directory has to be made is refused (`ARG_PATH`). The report is always written: without `--report` it goes beside the motion as `<out>.report.json` (`motion.json` → `motion.report.json`), and its `wrote` line says it was the default. A report that is already at that path is replaced, and the `wrote` line names when the replaced one was last written (its mtime, ISO 8601), so a second run to the same path is never silent. None of this changes the motion's bytes.
167
+ **Paths** ([#239](https://github.com/rig-c/pose/issues/239)). `--rig` is the directory @rig-c/parts writes, `<out>/rig/`: it reads `rig.json` and the `motion.json` beside it. Naming the `rig.json` itself reads no motion spec, so a rig that needs its own animations — a slider applies one — is red before anything is added, and the `RIG_GATE_BASE` line says the `motion.json` beside it was not read and names the directory form (or `--base-motion`). When the gate is green without it, the run goes on and `--out` carries none of the rig's own animations, so a `MOTION` line says the same thing, in every output mode ([#250](https://github.com/rig-c/pose/issues/250)). A directory `--out`, `--report` or `--camera` names that is not there yet is made once the run is green, and a `made directory <dir>` line says so and which paths it holds; a red run makes none, and a file where a directory has to be made, or a directory that cannot be written into, is refused (`ARG_PATH`) before anything is written; `--build` and `--render` make their own and are held to the same refusal. `clean --out`, `pelvis --out`, `convert --out` and `compare --report`/`--sheet` do the same ([#284](https://github.com/rig-c/pose/issues/284)). The report is always written: without `--report` it goes beside the motion as `<out>.report.json` (`motion.json` → `motion.report.json`), and its `wrote` line says it was the default. A report that is already at that path is replaced, and the `wrote` line names when the replaced one was last written (its mtime, ISO 8601), so a second run to the same path is never silent. None of this changes the motion's bytes.
113
168
 
114
169
  For every frame of the pose and every bone the map names, the segment that bone carries (an upper arm: shoulder → elbow) points somewhere in the picture. The bone is turned so its segment points the same way: the picture angle, flipped into rig-c's y-up world, minus the bone's angle at the rig's setup pose (read through rig-c's own poser), less the turn of the mapped bone above it — because a `rotate` key in rig-c adds to the setup rotation and a child turns with its parent. The travel of the pelvis midpoint, scaled by a length measured on both the rig and the pose, moves the hip. When a head chain carries the eye line (`r_eye → l_eye`), the eyes' vertical travel against `neck` in the reference, scaled into the rig's units, keys a vertical `translate` nod channel on the head bone ([#111](https://github.com/rig-c/pose/issues/111)) clamped within `±2%` of the setup neck length (`head − neck`, [#181](https://github.com/rig-c/pose/issues/181)), while on sequences where that clamp acts, the clamped vertical nod (`clampedY`) is coupled in phase into `face.pitch`'s dial (`+2.8°/unit`, mapped through `face.pitch.range`) and the head bone's `rotate` track (`+1.6°/unit`, [#190](https://github.com/rig-c/pose/issues/190), [#205](https://github.com/rig-c/pose/issues/205); gains calibrated against the hand-polished reference `p1`). **How the nod's baseline is read** ([#241](https://github.com/rig-c/pose/issues/241)): on a sequence declared `"travel": "in-place"` it is frame 0, with no window, no clamp and so no coupling ([#242](https://github.com/rig-c/pose/issues/242)); otherwise it is a ±0.5 s running median of the eyes' height against the neck, averaged again over the same window, and the nod is the eyes' height less it. On a sequence `clean --loop` wrote (its `cleaned.params.loop`) and not declared in place, that window **wraps**, the last frame running into the first, as clean's own windows do; cut at the clip's ends instead, the two ends are read against one-sided baselines, and the coupling carries the difference into the head's rotate. On the tests' synthetic loop (#242's walk, 7 cycles of 24 frames, the eyes bobbing 3 px twice a cycle, `travel` deleted), the wrapped window puts cycle 7 on cycle 1 to 0.000° and 0.000 units; cut at the ends it was 1.057° on the rotate and 0.661 units on the nod. `"nod": false` on the eye-line chain ([below](#the-map-rig-pose-map1)) keys no nod and couples none. The `NOD` line says which of these happened. The result is one animation in rig-c's motion format (`rigc-motion/1`): a `rotate` track per mapped bone, a `translate` track on the hip (and on the head when the reference nods), one key per pose frame — and, when the map lists alternate drawings, an `attachment` track per slot they switch (and a `translate` track per bone a set offsets).
115
170
 
@@ -138,8 +193,9 @@ and what the run cannot decide for you it lists:
138
193
  - `SEAM` — rig-c's seam reading (`rigc render --seam`, rig-c ≥ 2.60.0) over the new animation, for the rig's declared neighbour pairs ([#216](https://github.com/rig-c/pose/issues/216)): one line per span of a **gap** (background inside the joint) or a **ghost** (one piece's overlap swung clear of its partner) over the one-cell floor, with the pair, the frames and their times, the rate and the most cells; per pair the frames rigc named that were one cell thick (`under the floor — not reported`), a pair not read and why, or rigc's SKIP (the two drawings share no cell at rest — nothing measured, never a seam that held); then `n span(s) over k pair(s) at <fps> fps — r of d declared pair(s) read`. **The pairs**: a rig spec carries none — `rig.json` holds bones, slots and skins — so they come from @rig-c/parts' `parts.json`, named with `--parts <file or directory>` (its build's `--out`, beside `rig/`): each limb's consecutive segments (a part's `limb` record: `name`, `segment`, and below a joint `joint`), a pair named twice counting once, each part drawn by the slot of its name — the pairs @rig-c/parts' own `check` reads (rig-c/parts#503). A pair whose part has no slot of its name is `not read`, named. Without `--parts`, or with a `parts.json` that holds no limb, the line is `SEAM no neighbour pair declared — <why>; seams: []`, and where the rig's parent directory holds a `parts.json` it says so. **The floor** is @rig-c/parts' so the two tools agree: a frame's gap or ghost counts when it holds more cells than its box is long (its longer side, in cells) — thicker than one cell; a crack one cell deep is the half-opacity edge moving by less than a cell. Its own render, at the rate the folds are read at; it moves no byte of the motion or the build, and never changes the exit or the gate. A malformed `parts.json` is an input and is refused (`PARTS_JSON`).
139
194
  - `JITTER` — one line per bone a chain keys (a face or body dial bone excepted), in map order ([#236](https://github.com/rig-c/pose/issues/236)): the mean |second difference| of its local rotation as rig-c's poser posed the gated build (its world matrix in its parent's axes, so the rotate key up to a constant), against the same figure of its map pair's picture angle less the pair of the nearest keyed bone above it, in °/frame², and the ratio. Both are read on the same triples, three consecutive frames on which every pose point the two pairs need is good. A pair whose figure rounds to 0 is still, and gets no ratio. A group's jitter ratio is its bones' together; the line names the bone that moved it. A reading, not a gate.
140
195
  - `REACH` — a `reach` chain ([below](#the-map-rig-pose-map1)) whose aim departs from its own pair's angle ([#236](https://github.com/rig-c/pose/issues/236)): why (its origin sits off the anchor's at setup, a lever; and the bones above it the animation turns, or translates other than by the anchor's travel), the largest departure with its frame, the departure's mean |second difference|, and the aim's against the pair's, in °/frame², with the ratio. A reach whose bone sits on the anchor's origin with nothing above it moved is aimed along the pair and prints none. This line also names, with `--raised-by-head` or a `"by-hip"` set, a placed arm out of reach (below); the two read `REACH chain …` and `REACH set …`. A reading, not a gate.
141
- - `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. A frame on which either hip is `inferred` is left out of the tilt. When that is every frame, as after `clean --hips-inferred`, the line is `hip tilt not read: hips inferred (declared at clean)` and the report's `posture` has `"tilt": null` with `"tiltWhy"`; on some frames only, the tilt is read over the others and the line counts the frames left out (`posture.hipsInferred`). The source is read from the clean record, or is `source not stated` when the joints carry the state and the record does not. With a `measured` record ([#86](https://github.com/rig-c/pose/issues/86), [#106](https://github.com/rig-c/pose/issues/106)), the read, interpolated, scaled and held frames are the measurement, so the tilt is read on all four, while `unmeasured` frames (`fit.corr < 0.9`) are left out and named. The line adds `tilt measured (<method>) on N frame(s): R read, I interpolated, H held (held: <spans>)` (plus `scaled` and `unmeasured` spans when present). The report's `posture` carries `tiltMeasured`. The sway reads the hips as before. With `--plant-feet`, `POSTURE` also prints one `bounce` line per support span ([#107](https://github.com/rig-c/pose/issues/107), `posture.bounce` in the report): the detrended (centred ±12-frame moving mean removed) vertical standard deviation, rig/reference ratio and correlation for the hip, the neck and `eyes − neck`.
196
+ - `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. A frame on which either hip is `inferred` is left out of the tilt. When that is every frame, as after `clean --hips-inferred`, the line is `hip tilt not read: hips inferred (declared at clean)` and the report's `posture` has `"tilt": null` with `"tiltWhy"`; on some frames only, the tilt is read over the others and the line counts the frames left out (`posture.hipsInferred`). The source is read from the clean record, or is `source not stated` when the joints carry the state and the record does not. With a `measured` record ([#86](https://github.com/rig-c/pose/issues/86), [#106](https://github.com/rig-c/pose/issues/106)), the read, interpolated, scaled and held frames are the measurement, so the tilt is read on all four, while `unmeasured` frames (`fit.corr < 0.9`) are left out and named. The line adds `tilt measured (<method>) on N frame(s): R read, I interpolated, H held (held: <spans>)` (plus `scaled` and `unmeasured` spans when present). The report's `posture` carries `tiltMeasured`. The sway reads the hips as before. When the pelvis bone was keyed from the sequence's pelvis track ([#295](https://github.com/rig-c/pose/issues/295)), the reference tilt is the track's on the frames it read and the hip line's on the frames it declined, and the line names it as the `PELVIS` line does (`reference 30.7° from silhouette (…; all 24 frames from the track)`, `posture.tiltTrack`). The track's turn also swings what the pelvis bone carries, so a second `sway:` line states that the rig's x-range includes the pelvis track's turn, how far it carries the trunk's origin, and the trunk alone — mid_shoulder about that origin — beside the reference (`posture.sway.carried`). Without a track the lines are unchanged. With `--plant-feet`, `POSTURE` also prints one `bounce` line per support span ([#107](https://github.com/rig-c/pose/issues/107), `posture.bounce` in the report): the detrended (centred ±12-frame moving mean removed) vertical standard deviation, rig/reference ratio and correlation for the hip, the neck and `eyes − neck`.
142
197
  - `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)).
198
+ - `PHASE` — one per set with a `phase` ([below](#alternate-drawings-alternates), [#286](https://github.com/rig-c/pose/issues/286)): the supporting foot it shows on, every span it showed and its frame count, and the support hand-over each span was switched on and off at (`on frame 19 r → l, off frame 31 l → r`; `none` where no foot supports). The report's `switches` row of such a set carries the same under `phase`: `{ step, side, on: { frame, from } | null, off: { frame, to } | null }`.
143
199
  - `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.
144
200
  - `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.
145
201
  - `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.
@@ -147,6 +203,8 @@ and what the run cannot decide for you it lists:
147
203
  - `BY-HIP` — with a set's `"place": "by-hip"` ([below](#alternate-drawings-alternates)): the `RAISED` line against the hip, one per span of such a set per chain it places. The unit is the hip width, the offset is from the mid-hip. `BY-HIP none` when the map has such a set and none of its spans showed.
148
204
  - `REACH` — with `--raised-by-head`, or a `"by-hip"` set: a placed span whose target was out of the arm's reach, too far or too near. It names the frames, which way, and the shortfall in eye distances (hip widths for `"by-hip"`). The wrist was placed at the reach limit instead. This is not `ART-RANGE`: the rig's proportions were the limit, not the art's range. A too-near frame is placed at the limit like a too-far one, and graded at the placed distance.
149
205
  - `LEG-SCALE` — with `--leg-scale` ([below](#the-legs-foreshortening-and-planted-feet)): per leg segment, its straight length in px and as a share of `FEET`'s leg length, the spread of its picture length over it, the limits in px, the frames clamped at each limit, where its leg was free on clamped frames how far the clamps on that leg left the free ankle above or below the picture's (in the rig's leg at setup, thigh origin → ankle, and in `FEET`'s px; `legScale[].free` in the report, [#237](https://github.com/rig-c/pose/issues/237)), and (with `--plant-feet`) one `past the floor (support)` line per supporting run that asked under the floor ([#133](https://github.com/rig-c/pose/issues/133), `legScale[].support` in the report).
206
+ - `DEPTH` — when the sequence carries depth ([above](#derived-depth-clean---depth)): where it came from, on how many frames the limbs were ordered front to back, and which limb was in front alone how often — `DEPTH layers from the sequence's derived depth (clean --depth run): the limbs ordered front to back on N of N frame(s); in front alone: r_arm a, l_arm b, r_leg c, l_leg d; tied in front on k (the report's layers)`.
207
+ - `PELVIS` — when the map has a pelvis chain (one bone, joints `r_hip → l_hip`) or the sequence a pelvis track ([above](#the-pelvis-track-rig-pose-pelvis)): where the pelvis bone's rotation came from and its swing — `PELVIS: from <source>, swing N°`, or `PELVIS: from hip line (no track)` — with the frames keyed from the hip line on a track that declined them.
150
208
  - `PIVOT` — when the map translates a bone and has both legs: the translated bone's origin against its thigh roots' midpoint at setup, in units and hip widths — the pivot every hip turn swings the legs about.
151
209
  - `FEET` — when the map has both legs: one `segment` line per mapped leg segment with its picture length ÷ straight length (min · median · max), the scale `--leg-scale` would ask before its clamp, printed with or without the flag so the minimum can be read before choosing the range ([#237](https://github.com/rig-c/pose/issues/237)); the supports and jumps read off the reference ankles (planted or not), and on the posed build, outside the jumps, the lowest ankle's vertical range and each support span's worst vertical range and horizontal drift of the supporting ankle, in leg lengths.
152
210
  - `STANCE` — with `--stance` ([below](#the-legs-foreshortening-and-planted-feet)): a `unit` line first, naming the tolerance's unit (`FEET`'s leg length, each side's median hip → ankle) in px and as a share of the straight leg (the 95th percentile hip → ankle) ([#237](https://github.com/rig-c/pose/issues/237)); one line per run of frames planted both (with the stretch keyed, its largest k), lifted, or past the tolerance below (the pose's height kept, [#114](https://github.com/rig-c/pose/issues/114) — printed only where no `--leg-scale` clamp moved the free ankle; otherwise the frames it moved and how far, [#237](https://github.com/rig-c/pose/issues/237)), with the free side, the frames and the tolerance in leg lengths and px; and one per run whose stretch fell past its bounds (`past the bounds`), with the k the floor needed, the bound it had and how far the foot was left from it; and, where the leg's own range set a lower bound below 0.92, one `bound from the leg's own range` line with each side's lowest.
@@ -168,7 +226,9 @@ and what the run cannot decide for you it lists:
168
226
 
169
227
  Each figure is formatted as the full line formats it, from the same record. Refusals print the same in every mode, and the exit code and the gate do not change with it; `--verbose` with `--summary` is a usage error.
170
228
 
171
- `--report <file>` writes all of it as JSON (`rig-pose-report/1`): `spec`, `rigc` (`{ version, entry }`: the rig-c that gated it), `rig`, `animation`, `inputs` (consumed rig spec, map and pose files, each with invocation-relative `path` and exact-byte `sha256`; a folder rig resolves to its actual rig.json; these three identities do not claim to bind optional base motion, sets or images), `frames`, `duration`, `frameTimes` (the exact motion-key time for each pose frame; never FPS inferred from duration), `render` (the separate renderer sampling domain, `{ fps, frames }` from frame-set metadata, or `null` when not measured), `cleaned` (the sequence's `cleaned` record, [above](#clean-first-rig-pose-clean)), `options` (every figure the run used, `switchTolerance`, `switchMin`, `switchAt`, `legScale`, `plantFeet`, `stance`, `floor` and `raisedByHead` included; `reduceKeepPlanted: true` only with `--reduce-keep-planted`), `scale`, `scaleFit` (the `SCALE` reading: `segment`, `leg`, `hip`, `outside`, `travel`; `null` without a scale), `holds`, `short`, `endOn`, `bends`, `flips`, `gate` (rig-c's gate summary, a string), `landings`, `jitter`, `reach`, `posture`, `folds`, `seams`, `seamPairs`, `body` (`standing`, `read`, `over45`, `framesOver45`, and with `body.yaw` or `body.sets` in the map, `yaw` and `sets`), `legScale`, `pivot`, `feet` (`planted`, `legLength`, `straight`, `rigLeg`, `segments`, `supports`, `jumps`, `jumpEvents`, `steps`, `changes`, `lowered`, `floor`, `shifts`, `blend`, `closure`, and the posed `reading`; `straight` is each side's 95th-percentile picture length hip → ankle in px and `rigLeg` each side's rig leg at setup (thigh origin → ankle) in world units, both `null` without both legs; `segments` holds one `{ chain, bone, joints, straight, ratio: { min, median, max }, why? }` per mapped leg segment, read with or without `--leg-scale` (`straight` and `ratio` `null`, and `why` said, where it was not measured), [#237](https://github.com/rig-c/pose/issues/237); a jump is an inclusive `[first, last]` pair of frames; `lowered` holds `{ frames, was, now, cause? }` runs (`cause`, where `now` is `null`: `released` or `higher`), present only with `--plant-feet` on a sequence not declared in place ([#113](https://github.com/rig-c/pose/issues/113)); `floor` is the world y every support span was planted on and `shifts` holds one `{ side, frames, shift }` per support span in world units, `null` and empty without `--plant-feet`; `blend` is `{ bounds, runs: [{ side, frames, k: { min, max } }], over: [{ side, frames, needed, left }] }`, the supporting leg stretched on the blend frames, `null` without `--plant-feet`; `closure` is `{ drift, frames }`, the hip's x drift closed over an in-place loop, `null` unless the sequence is `"in-place"` and `--plant-feet` is given), `stance` (`tolerance`, `release`, `minSeconds`, `bounds`, `runs`: `{ state, side, frames, maxHeight, maxDepth?, k, absorbed?, moved? }` (`state` is `both`, `lifted` or `below`; `maxDepth`, on a `below` run, is the free ankle's largest distance below the supporting one in leg lengths; `moved`, on a `lifted` or `below` run, the frames whose free ankle a `--leg-scale` clamp moved off the pose's height, as `legScale[].free` is written, [#237](https://github.com/rig-c/pose/issues/237)), `over`: `{ side, frames, needed, left }`; `null` without `--stance`), `shear`, `idleDropped`, `idleKeptOut` and `merged` ([below](#the-idle-what-the-retarget-drops)), `reduce` (the tolerances, each row of `REDUCE` and `ankleDrift`, [below](#few-keys---reduce)), and the demand lists. The fields a run is graded by:
229
+ `--report <file>` writes all of it as JSON (`rig-pose-report/1`): `spec`, `rigc` (`{ version, entry }`: the rig-c that gated it), `rig`, `animation`, `inputs` (consumed rig spec, map and pose files, each with invocation-relative `path` and exact-byte `sha256`; a folder rig resolves to its actual rig.json; these three identities do not claim to bind optional base motion, sets or images), `frames`, `duration`, `frameTimes` (the exact motion-key time for each pose frame; never FPS inferred from duration), `render` (the separate renderer sampling domain, `{ fps, frames }` from frame-set metadata, or `null` when not measured), `cleaned` (the sequence's `cleaned` record, [above](#clean-first-rig-pose-clean)), `options` (every figure the run used, `switchTolerance`, `switchMin`, `switchAt`, `legScale`, `plantFeet`, `stance`, `floor` and `raisedByHead` included; `reduceKeepPlanted: true` only with `--reduce-keep-planted`), `scale`, `scaleFit` (the `SCALE` reading: `segment`, `leg`, `hip`, `outside`, `travel`; `null` without a scale), `holds`, `short`, `endOn`, `bends`, `flips`, `gate` (rig-c's gate summary, a string), `landings`, `jitter`, `reach`, `posture`, `folds`, `seams`, `seamPairs`, `body` (`standing`, `read`, `over45`, `framesOver45`, and with `body.yaw` or `body.sets` in the map, `yaw` and `sets`), `legScale`, `pivot`, `pelvis` (the `PELVIS` reading, [above](#the-pelvis-track-rig-pose-pelvis): `{ source, trackSource, bone, chain, swing, track, hipLine, declined }` — `source` the track's or `"hip line"`, `trackSource` the sequence's `pelvisSource` or `null`, `swing` the peak-to-peak of the bone's rotate keys in degrees, `track` and `hipLine` the frames each keyed, `declined` the frames the track declined as `[first, last]` pairs; `null` when the map has no pelvis chain and the sequence no track), `feet` (`planted`, `legLength`, `straight`, `rigLeg`, `segments`, `supports`, `jumps`, `jumpEvents`, `steps`, `changes`, `lowered`, `floor`, `shifts`, `blend`, `closure`, and the posed `reading`; `straight` is each side's 95th-percentile picture length hip → ankle in px and `rigLeg` each side's rig leg at setup (thigh origin → ankle) in world units, both `null` without both legs; `segments` holds one `{ chain, bone, joints, straight, ratio: { min, median, max }, why? }` per mapped leg segment, read with or without `--leg-scale` (`straight` and `ratio` `null`, and `why` said, where it was not measured), [#237](https://github.com/rig-c/pose/issues/237); a jump is an inclusive `[first, last]` pair of frames; `lowered` holds `{ frames, was, now, cause? }` runs (`cause`, where `now` is `null`: `released` or `higher`), present only with `--plant-feet` on a sequence not declared in place ([#113](https://github.com/rig-c/pose/issues/113)); `floor` is the world y every support span was planted on and `shifts` holds one `{ side, frames, shift }` per support span in world units, `null` and empty without `--plant-feet`; `blend` is `{ bounds, runs: [{ side, frames, k: { min, max } }], over: [{ side, frames, needed, left }] }`, the supporting leg stretched on the blend frames, `null` without `--plant-feet`; `closure` is `{ drift, frames }`, the hip's x drift closed over an in-place loop, `null` unless the sequence is `"in-place"` and `--plant-feet` is given), `stance` (`tolerance`, `release`, `minSeconds`, `bounds`, `runs`: `{ state, side, frames, maxHeight, maxDepth?, k, absorbed?, moved? }` (`state` is `both`, `lifted` or `below`; `maxDepth`, on a `below` run, is the free ankle's largest distance below the supporting one in leg lengths; `moved`, on a `lifted` or `below` run, the frames whose free ankle a `--leg-scale` clamp moved off the pose's height, as `legScale[].free` is written, [#237](https://github.com/rig-c/pose/issues/237)), `over`: `{ side, frames, needed, left }`; `null` without `--stance`), `shear`, `idleDropped`, `idleKeptOut` and `merged` ([below](#the-idle-what-the-retarget-drops)), `reduce` (the tolerances, each row of `REDUCE` and `ankleDrift`, [below](#few-keys---reduce)), and the demand lists. The fields a run is graded by:
230
+ With a sequence that carries depth ([#281](https://github.com/rig-c/pose/issues/281), [above](#derived-depth-clean---depth)) the report also carries `layers`, one `{ frame, t, order, depth }` per frame: `order` the limbs front to back as layers, each an array of the limbs that share it (`[["r_arm"], ["l_arm", "r_leg"], ["l_leg"]]`), and `depth` each ordered limb's depth in px, + toward the camera. A sequence without depth writes no `layers` key, and its report is what it was.
231
+
172
232
  Report event objects with a pose `frame` or inclusive `frames` also carry exact `t` or `times` from `frameTimes`; existing `seconds` remain span lengths, with additive `spanSeconds`. Holds preserve their frame-index `from`/`to`. Existing scan joint/chain identities remain beside map-derived `bone` (a unique mapped target) or `bones` (all actual mapped targets); no joint-name heuristic chooses a target. `feet.jumps` stays an array of original frame pairs; `feet.jumpEvents` adds matching `{ frames, times }` objects. Fold, uncovered and seam rows preserve their discrete `times` and gaps, declare `domain: "render"`, and use the separate root `render` domain, never fabricated nearest pose-frame indices. The run hashes map/pose from the exact buffers passed to their readers, snapshots the resolved rig file before its gate, and rechecks all three before user-output writes. The writer validates coordinates before output writes and never changes motion keys. The switch diagnostic `moved.frame` is a selected-to-keyed pair with signed `frames`/`seconds` displacement; it is preserved as diagnostic data, not reinterpreted as an inclusive interval. An explicitly supplied uniform writer clock requires both positive `fps` and seconds offset `frame0`; canonical pose input does not yet declare FPS, so the retarget producer always writes exact `frameTimes`.
173
233
 
174
234
 
@@ -258,6 +318,7 @@ rig-c's `scalex` multiplies the bone's x axis, and a child with the `normal` inh
258
318
  **`--plant-feet`** places the hip so the supporting foot stays where it took support, through the posed legs at the scale the pose asked for (when a segment asks for a scale below `--leg-scale`'s clamp floor, the clamp does not lift the hip by what it withheld, [#107](https://github.com/rig-c/pose/issues/107); on a supporting leg, the segment's `scalex` follows the asked scale down to the bound #95 derives from the leg's own range so the supporting ankle stays on #85's floor while the free leg keeps the configured clamp, and any frame clamped at #95's bound lifts the hip by the remainder so the ankle never sinks, listed as `LEG-SCALE past the floor (support)`, [#133](https://github.com/rig-c/pose/issues/133)), instead of moving it by the pose's mid_hip travel:
259
319
 
260
320
  - The supporting foot is the one whose reference ankle is lower in the picture. It changes only when the other ankle has been at least **0.02 leg lengths** lower for at least **0.1 s** (timed as `END-ON` runs are), and once confirmed takes support from the frame it crossed that margin ([#102](https://github.com/rig-c/pose/issues/102)); a leg length is each side's median picture length hip → ankle, and the switch compares against their mean.
321
+ - **Frontal views** ([#280](https://github.com/rig-c/pose/issues/280)). Seen from the front, a foot's depth is drawn as height: the nearer foot is the lower one, and it may be the one in the air. So every clip is also read by a second signal, the ankle's **vertical speed**: a planted foot moves with the ground — still under a fixed camera, or sliding back at the ground's speed in a gait drawn in place. Each side's ground speed is the median of its ankle's vertical step over the frames outside every jump (a walking foot is down on more than half of them), and a foot is down on a frame when the step into it or the step out of it departs from that speed by less than **0.01 leg lengths** (the re-plant's stillness); both feet can be down. The two signals **contradict** on a frame where the lower ankle (by 0.02 leg lengths) moves while the higher moves with the ground. A clip is read as frontal when that happens on more than **5 %** of the frames outside the jumps **and** its stride shows in depth: on each side the ankle's range against its hip (5th to 95th percentile) is larger in y than in x — a side view's stride is in x, and its stance ankle rises and falls with the leg's angle, so there the vertical speed does not decide. 5 % sits below the shortest contradiction a frontal walk makes (one frame a step: 2 of the 24 frames of a 1 s cycle, 8.3 %) and above every fixture the tests read in depth (0 %); the tests' synthetic frontal walk contradicts on 18 of 96 frames (18.8 %). On a frontal clip each frame's support is the vertical speed's: the one foot down; on frames both are down, the foot that landed, so support passes on the frame it lands (as #102 takes it from the landing frame); ankle height's only where neither foot moves with the ground. A plant then ends where the other foot lands, so no step releases one and #113's and #119's readings are not made. On a frontal clip `FEET` prints two `support:` lines — the frames contradicted and their share against 5 %, then the ankle ranges — and one `stride` line per support span: the signal that decided its frames, the frames both feet were down, and what ankle height alone would have read there, e.g. `stride l 8–15: ankle vertical speed on 8 frame(s) (both feet down on 3); ankle height alone would read r on 2 of them`. On any other clip ankle height decided as before and nothing is printed, in any output mode, so such a run's output is as it was. On the tests' synthetic frontal walk (an 8-drawing cycle on twos interpolated to 24 fps, both feet down on 4 of the 8 drawings, the nearer foot lower, the ankles moving 17 and 23 px in y) the feet read down are its contact table on all 96 frames, where ankle height alone supported a foot in the air on 16. The third signal the card names, the sole's distance to a floor line, is not read: no floor line in the picture can be declared (`--floor` is a world y, placed from the support it would have to decide).
261
322
  - The new support is held where it is on the frame it takes over, and the hip blends in y from the old derivation to the new over **3 frames** while taking the new derivation's x immediately ([#102](https://github.com/rig-c/pose/issues/102)), so the planted foot does not slide across the blend.
262
323
  - A **jump** — both reference ankles **and** mid_hip more than **0.05** leg lengths above their **rolling floor**, the 95th-percentile picture y over ±1.0 s around the frame, for at least **2 frames**, starting from the first immediately preceding push-off frame where both ankles are already above their rolling floor while mid_hip lags ([#142](https://github.com/rig-c/pose/issues/142)) — releases the plant: the hip follows the pose's travel from where it was, and re-plants on landing (blended the same way). At a walking hand-over the front foot has not landed yet while the back ankle lifts its heel and the hip rises 0.051–0.065 leg lengths; when the front foot lands while the back ankle stays above its floor, a back ankle that holds its height in y (< **0.01** leg lengths a frame for **2 frames**) is on a raised heel rather than in flight and is not read as a jump ([#101](https://github.com/rig-c/pose/issues/101)). The body has to lift: an ankle's picture y drifts with depth, and under one floor for the whole sequence the subject in `dance-a` standing still (frames 0–29 and 277–312, mid_hip moving 10 and 14 px) read as jumping, 152–155 of 313 frames in all.
263
324
  - **In place** ([#92](https://github.com/rig-c/pose/issues/92)). On a sequence with `"travel": "in-place"`, the supporting foot of a figure that does not travel moves back in the picture while the body stays. Holding it where it planted turns that slide into the body's travel, so the hip walks off and never comes back. Under the declaration each plant's anchor follows its own ankle's picture x, scaled into the rig, from the frame it planted, and keeps its y on the one floor. No step releases a foot, since a foot that follows its track has no plant to drag. Every foot may take support, and the lower one does, by the rules above. The loop is then closed: the hip's x on frame i moves by −drift · i / (n − 1), where drift is its x on the last frame less its x on the first. The last frame's hip x is then the first's, and the loop carries no net travel. The y is the floor's and is not touched. `FEET` prints the drift closed, in place of the steps line, and the report's `feet.closure` carries `{ drift, frames }` (`null` otherwise). On the tests' synthetic in-place loop (40 frames, each supporting ankle sliding back 2 px a frame, the hips still), the hip moves less than 10⁻⁹ px. The same joints without the declaration put it 68 px forward over the loop. With a slide of 0.04 leg lengths a frame, both feet are released as stepping and never re-plant without the declaration, and the supports alternate with it. Jumps are read as before. **What planting is and is not needed for** ([#242](https://github.com/rig-c/pose/issues/242)): the declaration is the sequence's, so the support reading (no step releases a foot), the camera-zoom normalisations and the head nod (no detrend window, no clamp) take it with or without `--plant-feet`. Planting is needed only for what acts on a planted hip: the anchor following its ankle's track and the loop closure, since without a plant there is no anchor to follow and the hip is the pose's own travel. Without `--plant-feet` the run says so in a `TRAVEL` line: `sequence declares travel: in-place but --plant-feet is off — its supports and the head nod are read in place as with it; the loop closure is --plant-feet's (it closes the planted hip), so the hip follows the pose's own travel: x +0.00 units from the first frame to the last, not closed` (the figure is the hip's measured x drift; `and the map translates no bone` when nothing is translated). On the tests' synthetic walk in place (7 cycles of 24 frames, the supporting ankle sliding up to 0.09 leg lengths a frame, the eyes bobbing 3 px twice a cycle), without `--plant-feet` the supports are 15 spans covering all 168 frames (the seam splits the first) and the head's rotate key and nod are the same on cycle 1 and cycle 7 to 0.001; reading the declaration only under `--plant-feet`, as before, they were 2 spans covering 3 frames, and the head's cycle 7 differed from cycle 1 by 1.057° and 0.661 units. That head figure came from the nod's detrend window cut at the clip's ends, which [#241](https://github.com/rig-c/pose/issues/241) wraps on a loop, so it no longer reproduces under that reading either.
@@ -265,6 +326,7 @@ rig-c's `scalex` multiplies the bone's x axis, and a child with the `normal` inh
265
326
  - **Read from the lower ankle** ([#113](https://github.com/rig-c/pose/issues/113), [#119](https://github.com/rig-c/pose/issues/119)). On a sequence not declared `"in-place"`, a foot whose ankle is at least **0.02 leg lengths** higher in the picture than the other's never takes support: not when a plant starts, and not when a step releases the other foot. Before, a step handed support to the only foot free to take it, even a folded, lifted one, and the hip was then placed under a foot in the air. A supporting foot that the other ankle has been that much lower than for **0.1 s** hands support to it when it may take it, as before; when the lower foot may not (it is released and not yet still, or moving), the supporting foot is released, and the hip follows the pose's travel until a foot may take support. On a frame after a jump or a step's release where the only foot free under #113 is the higher one (`cause: "higher"`), the lower foot supports **tracked, not planted** ([#119](https://github.com/rig-c/pose/issues/119)): the hip puts it on #85's floor and follows its scaled ankle track (`FEET tracked`, `feet.tracked`) until it has been still for 2 frames (`< 0.01`), when the normal plant takes over at that position. Steps and jumps are read as before, and under `"in-place"` #92's reading stands. `FEET support read from the lower ankle` names every run of frames whose support differs from the reading before #113, the side before and after (`none`: no foot supports), and for each `none` why: `released`, the supporting foot let go because the lower ankle could not take support (released by a step, or moving), or the only foot free to take support was the higher one. The report's `feet.lowered` holds them as `{ frames, was, now, cause }` (absent in place). On the tests' synthetic step under a lifted leg (the lifted ankle 0.2 leg lengths up, the grounded one stepping 0.05 leg lengths on frame 10 and still from 11), the lifted leg took support on frames 10–11 before; now frames 10–11 track the lower foot on the floor (`feet.tracked`) and the grounded one is planted from 12, when it is still again.
266
327
  - **One floor** ([#85](https://github.com/rig-c/pose/issues/85)). A 2D rig has no depth, so its floor is one line, and a planted foot is on it. The picture disagrees: a step toward the camera puts the foot lower in the frame, and a camera residual puts a whole span higher, so a plant held where the picture puts each span leaves the floor stepping between spans. Every support span's supporting ankle is therefore moved in y onto one floor, the hip and everything above with it. A span's *planted height* is the world y (on the rig, posed by rig-c) its supporting ankle would be held at; the floor is the median of the spans' planted heights, or **`--floor <y>`** (world units, y up; only with `--plant-feet`, exit 2 without it). Each span's *shift* is floor − its planted height. A change of support still blends the hip over the 3 frames, now between two spans on the floor, and a planted foot does not leave the floor: on each blend frame the **supporting** leg is stretched along its chain about its root, by the factor `--stance` uses, k = (root y − floor y) / (root y − ankle y), held within **[0.92, 1.08]**, its lower bound on the frame the supporting leg's own range as `--stance`'s ([#95](https://github.com/rig-c/pose/issues/95); where that went below 0.92, a `FEET bound from the leg's own range` line, the report's `feet.blend.range`, and each blend run's `lower`). A frame that needs more is held at the bound, the rest left to the blend, and listed `FEET past the bounds` with the k needed and how far the foot was left from the floor. An ankle already within 0.01 units (the landing check's origin tolerance) is not stretched. The stretched scales are scale targets, so the landing check holds them; `--stance` then stretches the free leg, a different chain, to the supporting ankle. A jump follows the pose's travel from the hip it left, which is on the floor, carried linearly from take-off so the landing frame meets the floor and clamped so no frame inside the flight puts a foot below it ([#103](https://github.com/rig-c/pose/issues/103)), and is held the same way over its first 3 planted frames, with the flight's carry held across the landing blend so it fades out with the blend weights rather than dropping on the first frame after landing ([#147](https://github.com/rig-c/pose/issues/147)). `--stance` is unchanged: it stretches the free leg to the supporting ankle, which is on the floor. On the tests' synthetic stick, three spans 0, 6 and 1 px apart in the picture (0, 9 and 1.5 units on the rig) plant within 0.0001 units of one floor. With the shift forced to 0 they stay 9 units apart; without the stretch, the supporting ankle leaves the floor by 6.75 units on the first blend frame.
267
328
  - **A loop's planted heights** ([#285](https://github.com/rig-c/pose/issues/285)). Support passes on the frame the incoming ankle first reads 0.02 leg lengths lower than the outgoing one, and each span's planted height is read through that hand-over from the span before it, so every hand-over steps the planted height the same way: over identical cycles the heights descend as a staircase while the picture's floor stays where it is. Two things keep a loop's floor a loop's. Under `--leg-scale` the outgoing plant, on the hand-over frame and the blend after it, holds its ankle with its leg at the scale the pose asked on its last supporting frame: by the hand-over frame the picture already draws that leg shortened by the lift, and posing it so read the lift as the floor dropping under the incoming foot. And on a sequence declared `"in-place"` the heights' drift over the cycles is closed before the floor is read from them, as the hip's x is: the first and the last span of the side with more spans (not counting one starting on frame 0) are a whole number of cycles apart, and the height difference between them is taken out linearly by each span's first frame, so a span and the span a cycle later have one height. A sequence with fewer than two such spans has nothing to close. On the tests' synthetic run in place (10 cycles of 19 frames, the swinging thigh foreshortened so the foot leaving the floor rises 0.08–0.12 leg lengths in the picture on its first frame off it) with `--leg-scale 0.4,1.05`, the shifts ranged over 2.4047 leg lengths, cycle 10's planted heights lay 323.99 units below cycle 1's, and every hand-over's blend needed a stretch past 0.92 (20 runs `past the bounds`); now the shifts range over 0.0033 leg lengths, cycle 10's heights are cycle 1's to under 0.0001 units, and the blend stretches by 0.9957–0.9979, the same at `--leg-scale 0.8,1.05` and as without `--leg-scale`. Without `--leg-scale` the same run's shifts ranged over 0.0695 leg lengths, 9.37 units from cycle 1 to cycle 10, and now over 0.0033.
329
+ - **A loop's floor** ([#285](https://github.com/rig-c/pose/issues/285)). On an in-place loop the floor is the median of the spans' planted heights after their drift is closed, and the closure is anchored at frame 0: every cycle is read at the first cycle's level. Before 0.38 the floor was the median of the uncorrected staircase, so a body can sit higher by a constant between versions; that constant is this definition, not drift.
268
330
  - `FEET` lists the supports, the jumps and the steps (each with the side, its frames, who took support and where it re-planted; in place, the loop's closure instead), and with `--plant-feet` the floor: its world y and where it came from, each span's shift in units and in that side's rig leg at setup (thigh origin → ankle), and the shifts' range, then one line per run of blend frames whose supporting leg was stretched (`blend held on the floor`) and per run `past the bounds`. The report carries them under `feet` (`supports`, `jumps`, `steps`, `changes`, `floor`, `shifts`, `blend`, `closure`).
269
331
  - **`--stance <leg lengths>`** (only with `--plant-feet`, which it is refused without; off by default) plants **both** feet when the free one is near the floor ([#60](https://github.com/rig-c/pose/issues/60)). Its unit is `FEET`'s leg length, each side's **median** picture length hip → ankle — not `--leg-scale`'s straight length, a 95th percentile — and `STANCE`'s `unit` line prints it in px and as a share of the straight leg ([#237](https://github.com/rig-c/pose/issues/237)). The card's `0.07` was measured on a dance, on a span where both shoes stood flat (`dance-a`, below); **keep `--stance` below the lifted-foot threshold the material is graded at**, since a lifted foot within the tolerance is planted: a walk graded with a 0.04 threshold plants its lifted feet under `--stance 0.07`. On a frame with a supporting side (so outside every jump), the free ankle's height above the supporting one is read in the picture, in the free side's leg length — the leg length `FEET` uses. The tolerance is a **distance** ([#114](https://github.com/rig-c/pose/issues/114)): within it, above the supporting ankle or below it, the frame is **planted both**: every bone of the free leg's chain has its `scalex` key multiplied by k = (root y − floor y) / (root y − ankle y) on the posed rig, the root being the chain's first bone's origin and the floor the supporting ankle, so the foot reaches the floor. A free ankle within the tolerance below the supporting one is planted too: its k is under 1, since a foot slightly under the floor is the same fault as one floating above it. A free ankle **past the tolerance below** the supporting one is not a stance but a support error (the foot read as supporting is the one in the air), so that frame is never planted both and keeps the pose's height; it is named on a `STANCE past the tolerance below` line with the side, the frames and the largest distance, and read `"below"` in the report's `stance.runs`. Before #114 the gap was signed, so any free ankle below the supporting one counted as within, and a lifted foot read as supporting had the grounded leg shrunk up to it. On the tests' synthetic stance, a free ankle 0.5 leg lengths below is read below and not planted; one 0.03 below is planted with k under 1. k is held within **[0.92, 1.08]**, except that the lower bound comes from the leg's own range in the sequence ([#95](https://github.com/rig-c/pose/issues/95)): per side, r = the leg's picture length hip → ankle on the frame ÷ its straight length (the 95th percentile of that length, as `--leg-scale` reads a segment's), and the frame's lower bound is min(0.92, r), so the rig leg may fold as far as the picture folds it on that frame and never less far than 0.92. The upper bound stays 1.08, since a picture leg does not read longer than straight. A sequence whose legs never fold below 0.92 keeps exactly [0.92, 1.08]. Where the range set a bound below 0.92 on a planted-both frame, one `STANCE bound from the leg's own range` line gives each side's lowest, as does the report's `stance.range`. A frame past its bound is listed `past the bounds` with the bound it had, held at the bound and not forced. Past the tolerance above, the foot is **lifted** and keeps the pose's height — unless a `--leg-scale` clamp drew that leg at another length on the frame, which moves its ankle: `STANCE` then says on how many of the run's frames the height was kept, and on which frames, and by how much, the clamp moved it (`stance.runs[].moved`, [#237](https://github.com/rig-c/pose/issues/237)). The state is read with **hysteresis** and a **shortest run** ([#87](https://github.com/rig-c/pose/issues/87)), on the distance: a planted foot leaves the stance when its distance from the supporting one goes past the tolerance, and one past it is planted again only under **0.8** of it (both figures are on the `STANCE` line). A change of state takes effect only when its condition holds for **0.1 s** (by the frames' times). A shorter crossing with the other state on both sides of it keeps their state and is listed `absorbed` with its frames and the height that crossed (`absorbed: true` in the report's `stance.runs`, beside the run that took it in). One that runs to the end of a support span is taken, since nothing of the other state follows it. A threshold read frame by frame turned noise at the threshold into a pop: one frame at 0.0708 against a 0.07 threshold dropped the stretch for one frame and the foot jumped up and back. The stretched scales are the scale targets, so the landing check holds them. Measured on `dance-a`, frames 94–175 outside the jumps: the free ankle's height above the supporting one on the posed build went from median 18.3 and p90 25.4 units to 0.0 and 0.0. On `dance-b` (492 frames at 30 fps) the 29 frames labelled by eye as both feet flat, outside the jumps, read 0.0 and 0.0. Of the 6 labelled one foot lifted, 5 stay above the tolerance; the sixth (frame 360) was 0.042 leg lengths up in the picture, within the tolerance, and so planted; without `--stance` it stood 6.5 units up, also under the tolerance (22.6 units there). That is 83 %, under the 90 % the card asked.
270
332
  - The map needs one chain ending at each ankle whose last bone is aimed `"x"` (the ankle is that bone's tip), and a `translate` the legs hang from (`LEGS_MAP`). A `range`, or a set's `turn` or `offsets`, on a bone between the hip and an ankle is refused (`LEGS_RANGE`): the plant is derived before the range clamp, and a clamp after it would move the planted foot.
@@ -334,6 +396,8 @@ Exit codes: `0` written, `1` refused (every reason printed, nothing written), `2
334
396
  - Joints are body-18 (`nose neck r_shoulder r_elbow r_wrist l_shoulder l_elbow l_wrist r_hip r_knee r_ankle l_hip l_knee l_ankle r_eye l_eye r_ear l_ear`); `r`/`l` are the **subject's** sides. `state` is `observed`, `occluded` or `missing`, as in the keypoints file, or `inferred` ([#63](https://github.com/rig-c/pose/issues/63)): a position the detector supplied but did not see. An `inferred` joint carries `at` (one without it is refused, `POSE_JOINT_AT`), and `retarget` keys it as it keys `observed`. An unlisted joint is missing.
335
397
  - Two differences from the keypoints file: the unit is `picture-px`, and `at` may lie outside the picture (a hand leaves the frame; the score says how much to believe it).
336
398
  - More than one person: `--person <id>`.
399
+ - **The pelvis track** ([#288](https://github.com/rig-c/pose/issues/288)) is the minor `rig-pose-sequence/1.1`: a `pelvisTilt` on every frame (beside `t` and `people`; Spine degrees in (−180, 180] of the direction r_hip → l_hip, or `null` where the reading declined the frame) and `pelvisSource` on the sequence (`"silhouette"`, `"waistband"` or `"keys"`), written by `rig-pose pelvis` ([above](#the-pelvis-track-rig-pose-pelvis)). A sequence carrying them holds one person; a track on some frames only, a source without a track, or a track without a source is refused by name (`POSE_PELVIS`). The writer writes the minor the content needs — `/1.1` with a track, `/1` without — so a sequence without one stays readable by a reader of `/1`; a `/1` file carrying a 1.1 key is refused naming the minor (`POSE_SPEC`). The spec string is what the file records of the version that wrote it: a file of a newer minor than this reader's is read, and each key it does not know is refused with the minor the file says it was written as (`… the file says it was written as "rig-pose-sequence/1.3", a newer minor than this reader's "rig-pose-sequence/1.2" — read it with a rig-pose that reads that minor`). A reader of `/1` (rig-pose 0.37 and before) refuses a `/1.1` file by its spec (`POSE_SPEC`, naming `"rig-pose-sequence/1.1"`) and its keys (`POSE_KEYS`).
400
+ - **Depth** ([#281](https://github.com/rig-c/pose/issues/281)) is the minor `rig-pose-sequence/1.2`: a `depth` on every frame (beside `t` and `people`), `{ "<segment>": { "ratio", "sign", "source" } }` — the segments `r_upper_arm`, `r_forearm`, `r_thigh`, `r_shin`, `l_upper_arm`, `l_forearm`, `l_thigh`, `l_shin`, each present where its two joints are placed on the frame (`{}` where none is); `ratio` a number ≥ 0, `sign` −1, 0 or 1, `source` `"derived"` (`clean --depth`, [above](#derived-depth-clean---depth)) or `"measured"` (a 3D detector). A minor includes the keys of the minors before it, so a sequence with depth and a pelvis track is `/1.2`, and one with the track alone stays `/1.1`. A sequence with depth holds one person; depth on some frames only, a malformed entry, an unknown segment, derived and measured depth together, and derived depth without the `cleaned` record's `"depth": { "gait" }` (or that record without derived depth on every frame) are refused by name (`POSE_DEPTH`); a `/1` or `/1.1` file carrying `depth` is refused naming the minor (`POSE_SPEC`).
337
401
  - `travel` (optional, [#92](https://github.com/rig-c/pose/issues/92)) is the clip's frame of reference. Its one value is `"in-place"`: a gait filmed without the figure travelling, such as a treadmill or reel walk, or a loop. `clean --loop` writes it, and it may be written by hand on any sequence. Any other value is refused by name (`POSE_TRAVEL`). Without it the figure's travel in the picture is its travel. It is a property of the sequence, not of a flag ([#242](https://github.com/rig-c/pose/issues/242)): with or without `--plant-feet`, retarget reads the supports in place, skips the camera-zoom normalisations (a loop has no zoom, and their rolling windows do not wrap) and reads the head nod without its detrend window or clamp. Only the following foot and the loop closure need `--plant-feet` ([below](#the-legs-foreshortening-and-planted-feet)).
338
402
  - `score` is whatever the pose model gives. RTMPose's goes above 1, so `--min-score` is a cut on that model's scale, not a probability.
339
403
 
@@ -412,10 +476,13 @@ Bone rotation cannot carry every pose: an arm behind the head, a forearm toward
412
476
  - `symmetric` (optional, [#188](https://github.com/rig-c/pose/issues/188), [#206](https://github.com/rig-c/pose/issues/206)): `true` normalises a two-arm symmetric gesture set (such as hands behind the head) to its `drawn` symmetry centre so both elbows sit at the same world height and neither wrist extends past the head silhouette ([below](#alternate-drawings-alternates)), ramping in and out in joint space across any adjacent bridge frames where the set's slots remain shown. Requires two 2-bone chains with `drawn` angles on all four bones (`MAP_ALT_SYMMETRIC`).
413
477
  - `support` (optional, [#188](https://github.com/rig-c/pose/issues/188), [#206](https://github.com/rig-c/pose/issues/206)): a chain name or non-empty list of chain names of the set (`"support": "arm_l"` or `["arm_l"]`) whose bones carry a stationary support arm (such as a waist-resting hand while the other arm gestures); while the set's slots show (`loShow..hiShow`), each support chain's bones anchor to `drawn` within `±2.5°` (`≤ ±3°`) of the pose's delta relative to the set's reference pose (bridging in joint space across any preceding gap frames where the leaving set's slots still show), and an unplaced active raised arm in the same set clears the head/bun outward along the 2D shoulder→wrist direction (`MAP_ALT_SUPPORT`).
414
478
  - `anchor` (optional, [#191](https://github.com/rig-c/pose/issues/191)): a chain name or non-empty list of chain names of the set (`"anchor": "arm_l"` or `["arm_l"]`) whose hand is anchored to the set's drawn hand-tip-to-head offset in the `head` bone's local frame (`MAP_ALT_ANCHOR`, `ANCHOR_HEAD`); while the set's slots for that chain show, the target hand tip is `head_posed_origin + rotate(W_drawn − H_setup, head_posed_rot − head_drawn_rot)` and the 2-bone arm is solved by two-circle intersection (`twoBone` / `reachLimit`) inside the set's `turn`, listing any frame where `turn` saturates and leaves a residual hand-tip shortfall (`saturated: [{ frame, residual }]` on `raised`, printed as `; residual f46 9.68u, …` on `RAISED`).
415
- - Each name resolves against the built rig and the map, and a miss is refused by name, listing what exists: `MAP_ALT_SLOT`, `MAP_ALT_ATTACHMENT`, `MAP_ALT_HIDES`, `MAP_ALT_CHAIN`, `MAP_ALT_JOINT`, `MAP_ALT_POSE`, `MAP_ALT_DRAWN`, `MAP_ALT_OFFSET` (a bone that is not a bone of the set's chains), `MAP_ALT_PLACE`, `MAP_ALT_SYMMETRIC`, `MAP_ALT_SUPPORT` and `MAP_ALT_ANCHOR`.
479
+ - `phase` (optional, [#286](https://github.com/rig-c/pose/issues/286)): `"left-step"` or `"right-step"`, the stepping (swinging) foot a gait set is drawn for and named by ([below](#alternate-drawings-alternates)). left-step: the left foot is stepping; shown while the right foot supports. A right-step drawing mirrors it: the right foot is stepping, and it shows while the left foot supports. Refused with any other value, or beside `frames` (`MAP_ALT_PHASE`).
480
+ - Each name resolves against the built rig and the map, and a miss is refused by name, listing what exists: `MAP_ALT_SLOT`, `MAP_ALT_ATTACHMENT`, `MAP_ALT_HIDES`, `MAP_ALT_CHAIN`, `MAP_ALT_JOINT`, `MAP_ALT_POSE`, `MAP_ALT_DRAWN`, `MAP_ALT_OFFSET` (a bone that is not a bone of the set's chains), `MAP_ALT_PLACE`, `MAP_ALT_SYMMETRIC`, `MAP_ALT_SUPPORT`, `MAP_ALT_ANCHOR` and `MAP_ALT_PHASE`.
416
481
 
417
482
  **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, the short hold, the flip guard and the hinge check; before any range clamp) and the same segment in its key skeleton, with a foreshortened segment (`SHORT`) weighted at `0.5 ×` (`SHORT_SWITCH_WEIGHT`, [#180](https://github.com/rig-c/pose/issues/180)). On each frame the nearest set within `--switch-tolerance` (default 25°) shows, ties going to the map's order; once entered, a set stays active until its distance exceeds `--switch-tolerance + 15°` (`SWITCH_LEAVE_HYSTERESIS`, [#180](https://github.com/rig-c/pose/issues/180)) for at least `--switch-min` (default 0.25 s) or a competing set with a lower distance enters within `--switch-tolerance`; a run of one set shorter than `--switch-min` is dropped and the base art shows there. 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.
418
483
 
484
+ **A gait set: `phase`.** A walk's drawings are painted per step side, and which one is right depends on which foot is planted, not on which key skeleton is nearest: on a walk the nearest-angle set can be the wrong side for half a stride ([#286](https://github.com/rig-c/pose/issues/286); measured on a synthetic walk in place, the two sets without `phase` showed one drawing on all 168 frames, 84 of them on the wrong support). A set with a `phase` is named by the foot that is stepping — left-step: the left foot is stepping; shown while the right foot supports — and is not selected by distance: it shows exactly on its supporting foot's spans, the `feet.supports` the `FEET` line prints, and switches on the hand-over — no tolerance, no `--switch-min`, no hysteresis, no bridging, and `--switch-at peak` does not move its boundaries. On a frame where no foot supports (a jump, a released step) neither shows and the base art does. The supports are read with or without `--plant-feet` (with it, from the lower ankle, [#113](https://github.com/rig-c/pose/issues/113)); a run that reads no feet — a map without both legs, a pose without both hips and ankles — is refused naming the sets (`PHASE_FEET`). Sets that compete (a common slot or offset bone) must all declare a `phase` or none, and at most one of each side (`PHASE_GROUP`): nothing would decide between a phase set and an angle set, or two drawings of one step. Sets without `phase` are selected as above, byte for byte.
485
+
419
486
  **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.
420
487
 
421
488
  The bound exists because speed alone rewarded showing art far from its pose. Unbounded, on `dance-a`, 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).
package/cli.ts CHANGED
@@ -9,22 +9,53 @@
9
9
  import { readFileSync, writeFileSync } from 'node:fs';
10
10
  import pkg from './package.json' with { type: 'json' };
11
11
  import { clean, cleanLine, holdRegions, unheldText, untestedText, HOLD_BLEND, HOLD_MIN_SECONDS, HOLD_REACH, HOLD_SPEED, HOLD_SPEED_WINDOW, LEGS, OCCLUSION_SCORE, OCCLUSION_WINDOW, SPIKE_TRUNKS, SPIKE_WINDOW, windowProblem } from './src/clean.ts';
12
- import { convertCoco17, readPoseSequence, writePoseSequence } from './src/pose.ts';
12
+ import { convertCoco17, DEPTH_GAITS, readPoseSequence, writePoseSequence, type DepthGait } from './src/pose.ts';
13
+ import { FLAT_BAND, SIDE_ON } from './src/depth.ts';
13
14
  import { parseWindows } from './src/compare.ts';
14
- import { lineOf, Refusal } from './src/problems.ts';
15
+ import { lineOf, Problems, Refusal } from './src/problems.ts';
16
+ import { makeOutDirs, planOutDirs } from './src/outdir.ts';
15
17
  import { runCompare, type CompareArgs } from './src/runcompare.ts';
18
+ import { PELVIS_MODES, runPelvis, type PelvisMode } from './src/pelvis.ts';
16
19
  import { PROVENANCE_FILE } from './src/provenance.ts';
17
20
  import { runRetarget, type RetargetArgs } from './src/run.ts';
18
21
 
19
22
  const HELP = `rig-pose ${pkg.version} — motion from a pose sequence onto a rig-c rig
20
23
 
21
24
  Usage:
22
- rig-pose clean --pose <pose.json> --out <pose.clean.json> [--loop] [--legs 5] [--rest 7] [--hold off] [--coco] [--person <id>]
25
+ rig-pose pelvis --pose <pose.json> --out <pose.pelvis.json> (--frames <dir> [--mode silhouette|waistband] | --pelvis-keys <keys.json>) [--person <id>]
26
+ rig-pose clean --pose <pose.json> --out <pose.clean.json> [--loop] [--legs 5] [--rest 7] [--hold off] [--depth walk|run] [--coco] [--person <id>]
23
27
  rig-pose retarget --rig <rig> --map <map.json> --sequence <pose.json> --out <motion.json> [--animation dance] [flags]
24
28
  rig-pose convert --coco <coco17.json> --out <pose.json>
25
29
  rig-pose compare --reference <pose.json> --render <pose.json> --map <map.json> [flags]
26
30
  rig-pose --version | --help
27
31
 
32
+ pelvis (before clean; rig-c/pose#288)
33
+ --pose <file> rig-pose-sequence/1 as a scan wrote it, before clean (a cleaned one is refused,
34
+ PELVIS_AFTER_CLEAN: clean must smooth the track this writes)
35
+ --out <file> the same sequence with the pelvis track: "pelvisTilt" on every frame and "pelvisSource",
36
+ written as rig-pose-sequence/1.1. pelvisTilt is Spine degrees (counter-clockwise, y up) of
37
+ the direction from the figure's right hip to its left — the angle retarget reads off
38
+ r_hip → l_hip, about 0 facing the camera — or null on a frame the reading declined
39
+ --frames <dir> the picture: one PNG per sequence frame, in file-name order, the sequence's size (no video;
40
+ extract the frames first). The hip keypoints place the reading; their angle is not read
41
+ --mode silhouette|waistband
42
+ silhouette (default): the straightest edge of the drawn figure across the hip span (between
43
+ the two hip keypoints), within 40° of level and one hip width of the hips' height — a drawn
44
+ pelvis line or the edge of a drawn hip. waistband: the top edge of a garment covering the hips
45
+ (skirt, shorts) — the colour at the hips' midpoint, its first row in each column of the span.
46
+ Forearms (elbow → wrist) and hands are masked. A frame either cannot read (the pelvis blank,
47
+ no garment, no straight edge over 60% of the span) is declined, named on a DECLINED line,
48
+ and written null: marked, not guessed
49
+ --pelvis-keys <file> the hand override, instead of --frames: rig-pose-pelvis-keys/1, { "spec":
50
+ "rig-pose-pelvis-keys/1", "keys": [{ "frame", "deg" }], "note"? } — the tilt in pelvisTilt's
51
+ sense at frames, linear between keys (the short way round) and held outside them;
52
+ pelvisSource "keys". A frame may be fractional (a drawn key between scan frames,
53
+ rig-c/pose#294); frames strictly increase within [0, last frame], one outside is refused
54
+ --person <id> the person to read when the sequence holds more than one; only that person is written
55
+
56
+ clean then smooths pelvisTilt as its own track (the legs' window, wrapped with --loop), and retarget keys
57
+ the map's pelvis chain (one bone, joints r_hip → l_hip) from it, printing PELVIS: from <source>, swing N°.
58
+
28
59
  clean (required before retarget; rig-c/pose#67)
29
60
  --pose <file> rig-pose-sequence/1 as a scan wrote it, or COCO-17 with --coco
30
61
  --out <file> the cleaned rig-pose-sequence/1, carrying the record of what was done ("cleaned")
@@ -43,6 +74,17 @@ clean (required before retarget; rig-c/pose#67)
43
74
  --hold off turn the stillness hold off (rig-c/pose#91); recorded as hold: "off"
44
75
  --hold-speed <n> the hold's speed limit, trunk lengths per frame (default: 0.015)
45
76
  --hold-reach <n> how far a held joint may lie from its span median, trunk lengths (default: 0.03)
77
+ --depth walk|run derive each limb segment's depth on every frame (rig-c/pose#281; off by default, and
78
+ without it the output is unchanged): "depth": { "<segment>": { ratio, sign, source:
79
+ "derived" } }, written as rig-pose-sequence/1.2. ratio = drawn length ÷ rest length (the
80
+ larger side's 95th-percentile length over the clip); sign +1 when the segment's far end is
81
+ nearer the camera, -1 farther, 0 flat (ratio within 0.95–1.05, or above it) or undecided
82
+ (the trunk side-on). Which way a foreshortened segment leans comes from the body, with
83
+ "forward" the trunk's facing: thigh and forearm forward (a hip flexes forward, an elbow
84
+ bends back); an upper arm with the opposite leg (forward when the other thigh is the more
85
+ foreshortened); a shin forward in a walk (the lifted foot comes forward) and back in a run
86
+ (it is kicked back) — the gait is this flag's value. tools/skeleton.ts colours the segments
87
+ by it, and retarget --report orders the limbs front to back by it (layers)
46
88
 
47
89
  In order, each step printed with its figures: (1) spikes — a joint more than 0.2 trunk lengths
48
90
  (neck → mid_hip, the median over the clip) from its 5-frame median takes the median, each listed by
@@ -486,10 +528,15 @@ function runsOf(frames: number[]): string {
486
528
 
487
529
  /** `rig-pose clean` (#67): read, clean, print each step's figures, write. */
488
530
  function runClean(argv: string[], say: (l: string) => void): void {
489
- const f = parseFlags(argv, ['pose', 'out', 'legs', 'rest', 'person', 'hold', 'hold-speed', 'hold-reach'], ['loop', 'coco', 'hips-inferred']);
531
+ const f = parseFlags(argv, ['pose', 'out', 'legs', 'rest', 'person', 'hold', 'hold-speed', 'hold-reach', 'depth'], ['loop', 'coco', 'hips-inferred']);
490
532
  const input = need(f, 'pose');
491
533
  const out = need(f, 'out');
492
- const params = { loop: f.has('loop'), legs: windowOf(f, 'legs', 5), rest: windowOf(f, 'rest', 7), hipsInferred: f.has('hips-inferred'), hold: holdOf(f) };
534
+ const gait = f.get('depth');
535
+ if (gait !== undefined && !(DEPTH_GAITS as readonly string[]).includes(gait)) throw new Usage(`--depth is "${gait}"; it must be ${DEPTH_GAITS.join(' or ')} (the gait decides which way a foreshortened shin leans: forward in a walk, back in a run)`);
536
+ const outPath = new Problems();
537
+ const outDirs = planOutDirs([['--out', out]], outPath);
538
+ outPath.throwIfAny();
539
+ const params = { loop: f.has('loop'), legs: windowOf(f, 'legs', 5), rest: windowOf(f, 'rest', 7), hipsInferred: f.has('hips-inferred'), hold: holdOf(f), ...(gait !== undefined ? { depth: gait as DepthGait } : {}) };
493
540
  const text = readFileSync(input, 'utf8');
494
541
  const seq = f.has('coco') ? convertCoco17(text, input) : readPoseSequence(text, input);
495
542
  const c = clean(seq, params, f.get('person'), input);
@@ -506,6 +553,7 @@ function runClean(argv: string[], say: (l: string) => void): void {
506
553
  for (const m of c.missing) say(` MISSING joint "${m.joint}" frame${m.from === m.to ? ` ${m.from}` : `s ${m.from}–${m.to}`}: no position; left without one (retarget holds it)`);
507
554
  const sg = (w: number) => (w === 1 ? '1 frame (none)' : `${w} frames`);
508
555
  say(` SMOOTH quadratic Savitzky–Golay per joint: legs (${LEGS.join(', ')}) over ${sg(params.legs)}, the rest over ${sg(params.rest)}; ${params.loop ? 'circular' : 'the window cut at the ends'}`);
556
+ if (c.pelvis !== null) say(` PELVIS pelvisTilt (${c.pelvis.source}) smoothed as its own track over ${sg(c.pelvis.window)} (the legs' window), ${params.loop ? 'circular' : 'the window cut at the ends'}, on the ${c.pelvis.read} of ${c.pelvis.frames} frame(s) that carry a reading (a null stays null); largest change ${fmt(c.pelvis.moved, 3)}°`);
509
557
  if (params.loop) {
510
558
  if (c.floor === null) say(' FLOOR no ankle has a position on any frame; not re-pinned');
511
559
  else say(` FLOOR the lowest ankle over the cycle: y ${fmt(c.floor.before)} before smoothing, ${fmt(c.floor.after)} after; every joint moved ${c.floor.shift >= 0 ? '+' : ''}${fmt(c.floor.shift, 3)} px in y`);
@@ -525,7 +573,13 @@ function runClean(argv: string[], say: (l: string) => void): void {
525
573
  }
526
574
  say(` HOLDS ${c.holds.length} found over ${new Set(c.holds.map((x) => x.joint)).size} joint(s) (per joint: a span ≥ ${HOLD_MIN_SECONDS} s still by speed < ${h.speed} trunk/frame and reach ≤ ${h.reach} trunk; ${HOLD_BLEND}-frame blend in and out)`);
527
575
  }
576
+ if (c.depth !== null) {
577
+ const d = c.depth;
578
+ const rest = (['upper_arm', 'forearm', 'thigh', 'shin'] as const).map((k) => `${k} ${d.rest[k] === null ? 'not placed' : fmt(d.rest[k]!)}`).join(', ');
579
+ say(` DEPTH derived per segment for a ${params.depth} (rig-c/pose#281): ratio = drawn ÷ rest, rest the larger side's 95th-percentile length (${rest} px); flat within ${FLAT_BAND[0]}–${FLAT_BAND[1]}; toward ${d.signs.toward}, away ${d.signs.away}, flat or undecided ${d.signs.flat} segment-frame(s); the trunk faced the camera on ${d.facing.toward}, away on ${d.facing.away}, side-on (under ${SIDE_ON} trunk wide, sign 0) on ${d.facing.side} frame(s)`);
580
+ } else if (c.seq.frames.some((x) => x.depth !== undefined)) say(' DEPTH measured depth (a 3D detector\'s) carried through as read');
528
581
  if (c.inferred !== null) say(` INFERRED r_hip on ${c.inferred.r_hip} frame(s), l_hip on ${c.inferred.l_hip} frame(s) of ${n} marked "inferred" (every frame where it has a position): declared at clean (--hips-inferred); positions and scores unchanged`);
582
+ makeOutDirs(outDirs, say);
529
583
  writeFileSync(out, writePoseSequence(c.seq));
530
584
  say(` CLEAN ${cleanLine(c.record)}`);
531
585
  say(` wrote ${out}`);
@@ -562,16 +616,29 @@ export function main(argv: string[]): number {
562
616
  runClean(rest, (l) => process.stdout.write(`${l}\n`));
563
617
  return 0;
564
618
  }
619
+ if (cmd === 'pelvis') {
620
+ const f = parseFlags(rest, ['pose', 'out', 'frames', 'mode', 'pelvis-keys', 'person'], []);
621
+ if (f.has('frames') === f.has('pelvis-keys')) throw new Usage(f.has('frames') ? '--frames and --pelvis-keys are both given; the track is read off the frames or taken from the hand keys, so give one' : '--frames <dir> (the picture to read) or --pelvis-keys <file> (the hand override) is required');
622
+ const mode = f.get('mode') ?? 'silhouette';
623
+ if (f.has('mode') && f.has('pelvis-keys')) throw new Usage('--mode is given with --pelvis-keys; the mode names a reader of the frames, and the hand keys read none');
624
+ if (!(PELVIS_MODES as readonly string[]).includes(mode)) throw new Usage(`--mode is "${mode}"; it must be ${PELVIS_MODES.join(' or ')}`);
625
+ runPelvis({ pose: need(f, 'pose'), out: need(f, 'out'), frames: f.get('frames'), mode: mode as PelvisMode, keys: f.get('pelvis-keys'), person: f.get('person') }, (l) => process.stdout.write(`${l}\n`));
626
+ return 0;
627
+ }
565
628
  if (cmd === 'convert') {
566
629
  const f = parseFlags(rest, ['coco', 'out'], []);
567
630
  const input = need(f, 'coco');
568
631
  const out = need(f, 'out');
632
+ const p = new Problems();
633
+ const outDirs = planOutDirs([['--out', out]], p);
634
+ p.throwIfAny();
569
635
  const seq = convertCoco17(readFileSync(input, 'utf8'), input);
636
+ makeOutDirs(outDirs, (l) => process.stdout.write(`${l}\n`));
570
637
  writeFileSync(out, writePoseSequence(seq));
571
638
  process.stdout.write(`rig-pose convert: ${seq.frames.length} frame(s) of COCO-17 → body-18; wrote ${out}\n`);
572
639
  return 0;
573
640
  }
574
- process.stderr.write(`rig-pose: unknown command "${cmd}" — the commands are: clean, retarget, convert, compare\n`);
641
+ process.stderr.write(`rig-pose: unknown command "${cmd}" — the commands are: pelvis, clean, retarget, convert, compare\n`);
575
642
  return 2;
576
643
  } catch (e) {
577
644
  if (e instanceof Usage) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/pose",
3
- "version": "0.37.1",
3
+ "version": "0.39.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": {
package/src/clean.ts CHANGED
@@ -65,7 +65,8 @@
65
65
  * Pure: no clock, no randomness, no disk.
66
66
  */
67
67
  import { BODY18, type Body18 } from './joints.ts';
68
- import type { CleanRecord, CleanSeam, CleanSpike, CleanUnheld, Joint, PoseSequence } from './pose.ts';
68
+ import { deriveDepth, depthSourceOf, type DerivedDepth } from './depth.ts';
69
+ import type { CleanRecord, CleanSeam, CleanSpike, CleanUnheld, DepthGait, Joint, PoseFrame, PoseSequence } from './pose.ts';
69
70
 
70
71
  /** A joint not held inside a region where others are (#91): the test it fails there and its figure. */
71
72
  export type Unheld = CleanUnheld;
@@ -114,6 +115,12 @@ export interface CleanParams {
114
115
  * always passes one.
115
116
  */
116
117
  hold?: { speed: number; reach: number } | 'off';
118
+ /**
119
+ * rig-c/pose#281: derive each frame's depth (`src/depth.ts`) for this gait, on
120
+ * the cleaned positions. Absent, no depth is derived and the output is what it
121
+ * was before #281.
122
+ */
123
+ depth?: DepthGait;
117
124
  }
118
125
 
119
126
  /** The joints `--hips-inferred` marks. */
@@ -185,6 +192,35 @@ export interface Cleaned {
185
192
  unheld: Unheld[];
186
193
  /** Back-to-back holds of one joint, cross-faded, in frame then body-18 order. */
187
194
  seams: Seam[];
195
+ /**
196
+ * rig-c/pose#288: the sequence's pelvis track, smoothed as its own track over the
197
+ * legs' window (wrapped with loop): its source, how many frames carry a reading,
198
+ * and the largest change smoothing made, degrees; null without a track.
199
+ */
200
+ pelvis: { source: string; window: number; read: number; frames: number; moved: number } | null;
201
+ /** rig-c/pose#281: the depth derived with `depth` (its rest lengths and counts); null without it. */
202
+ depth: DerivedDepth | null;
203
+ }
204
+
205
+ /**
206
+ * rig-c/pose#288: a pelvis track smoothed as its own track — the quadratic
207
+ * Savitzky–Golay over `window` frames (circular with loop), on the frames that
208
+ * carry a reading; a declined frame (null) stays null and is not read. The angles
209
+ * are taken about the first reading (each brought within ±180° of it) so a track
210
+ * near ±180° (a figure facing away) is smoothed without the wrap, and wrapped
211
+ * back into (-180, 180] after.
212
+ */
213
+ export function smoothPelvis(tilt: ReadonlyArray<number | null>, window: number, loop: boolean): Array<number | null> {
214
+ const first = tilt.find((v): v is number => v !== null);
215
+ if (first === undefined) return tilt.map(() => null);
216
+ const wrap = (d: number) => {
217
+ let x = d % 360;
218
+ if (x > 180) x -= 360;
219
+ if (x <= -180) x += 360;
220
+ return x;
221
+ };
222
+ const about: Series = tilt.map((v) => (v === null ? null : [first + wrap(v - first), 0]));
223
+ return savitzkyGolay(about, window, loop).map((q) => (q === null ? null : wrap(q[0])));
188
224
  }
189
225
 
190
226
  /**
@@ -641,6 +677,10 @@ export function clean(seq: PoseSequence, params: CleanParams, wanted: string | u
641
677
  }
642
678
  const track = personTrack(seq, wanted, where);
643
679
  const n = track.length;
680
+ // rig-c/pose#281: depth a sequence already carries — a 3D detector's is carried through, clean's own is read again or refused.
681
+ const carried = depthSourceOf(seq.frames);
682
+ if (carried === 'measured' && params.depth !== undefined) p.add('CLEAN_DEPTH', `${where} carries measured depth (a 3D detector's) and --depth ${params.depth} asks to derive it; a derived reading would replace a measurement — clean without --depth, which carries the measured depth through`);
683
+ if (carried === 'derived' && params.depth === undefined) p.add('CLEAN_DEPTH', `${where} carries derived depth (clean --depth), read off positions this clean moves; give --depth walk|run to read it again on the cleaned positions`);
644
684
  const person = wanted ?? [...new Set(seq.frames.flatMap((f) => f.people.map((q) => q.id)))].sort()[0]!;
645
685
  if (params.loop) {
646
686
  const widest = Math.max(SPIKE_WINDOW, params.legs, params.rest);
@@ -765,7 +805,7 @@ export function clean(seq: PoseSequence, params: CleanParams, wanted: string | u
765
805
  }),
766
806
  };
767
807
  const inferred = params.hipsInferred === true ? { r_hip: 0, l_hip: 0 } : null;
768
- const frames = seq.frames.map((f, i) => {
808
+ const frames: PoseFrame[] = seq.frames.map((f, i) => {
769
809
  const q = f.people.find((x) => x.id === person);
770
810
  if (q === undefined) return { t: f.t, people: [] };
771
811
  const joints: Partial<Record<Body18, Joint>> = {};
@@ -780,6 +820,31 @@ export function clean(seq: PoseSequence, params: CleanParams, wanted: string | u
780
820
  }
781
821
  return { t: f.t, people: [{ id: person, joints: sortedJoints(joints) }] };
782
822
  });
823
+ // rig-c/pose#288: the pelvis track, its own track over the legs' window (the hips' window), not re-read from the hips.
824
+ let pelvis: Cleaned['pelvis'] = null;
825
+ if (seq.pelvisSource !== undefined) {
826
+ const tilt = seq.frames.map((f) => f.pelvisTilt ?? null);
827
+ const smooth = smoothPelvis(tilt, params.legs, params.loop);
828
+ let moved = 0;
829
+ tilt.forEach((v, i) => {
830
+ const w = smooth[i];
831
+ if (v !== null && w !== null && w !== undefined) {
832
+ let d = Math.abs(w - v) % 360;
833
+ if (d > 180) d = 360 - d;
834
+ moved = Math.max(moved, d);
835
+ }
836
+ });
837
+ smooth.forEach((v, i) => (frames[i]!.pelvisTilt = v));
838
+ record.params.pelvis = { window: params.legs };
839
+ pelvis = { source: seq.pelvisSource, window: params.legs, read: tilt.filter((v) => v !== null).length, frames: n, moved };
840
+ }
841
+ // rig-c/pose#281: derived on the cleaned positions, or a measured depth carried through as read.
842
+ let depth: DerivedDepth | null = null;
843
+ if (params.depth !== undefined) {
844
+ depth = deriveDepth(frames.map((f) => f.people[0]?.joints ?? {}), params.depth);
845
+ depth.frames.forEach((d, i) => (frames[i]!.depth = d));
846
+ record.params.depth = { gait: params.depth };
847
+ } else if (carried === 'measured') seq.frames.forEach((f, i) => (frames[i]!.depth = f.depth ?? {}));
783
848
  const { cleaned: _was, ...rest } = seq;
784
849
  return {
785
850
  // #92: a loop is one cycle of a gait repeated in place, so it carries no travel of its own.
@@ -797,6 +862,8 @@ export function clean(seq: PoseSequence, params: CleanParams, wanted: string | u
797
862
  holds,
798
863
  unheld,
799
864
  seams,
865
+ pelvis,
866
+ depth,
800
867
  };
801
868
  }
802
869
 
@@ -815,7 +882,9 @@ export function cleanLine(r: CleanRecord, hold: (r: CleanRecord) => string = hol
815
882
  const spikes = `${found.length === 0 ? 'no spike' : `${found.length} spike(s) replaced (${shown}${found.length > 8 ? `, … ${found.length - 8} more` : ''})`}, ${untestedText(r.spikes.untested)}`;
816
883
  const jit = (v: number | null) => (v === null ? 'not measured' : v.toFixed(2));
817
884
  const hips = r.params.hipsInferred === undefined ? '' : `; hips inferred (${inferredSource(r)})`;
818
- return `${r.spec}: ${windows}, ${loop ? 'loop' : 'no loop'}; ${spikes}; jitter (mean |second difference| of y) ${jit(r.jitter.before)} → ${jit(r.jitter.after)} px/f²${hold(r)}${hips}`;
885
+ const pelvis = r.params.pelvis === undefined ? '' : `; pelvisTilt smoothed as its own track over ${r.params.pelvis.window === 1 ? '1 frame (none)' : `${r.params.pelvis.window} frames`}`;
886
+ const depth = r.params.depth === undefined ? '' : `; depth derived per segment (gait ${r.params.depth.gait})`;
887
+ return `${r.spec}: ${windows}, ${loop ? 'loop' : 'no loop'}; ${spikes}; jitter (mean |second difference| of y) ${jit(r.jitter.before)} → ${jit(r.jitter.after)} px/f²${hold(r)}${hips}${pelvis}${depth}`;
819
888
  }
820
889
 
821
890
  /**