@rig-c/pose 0.38.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -6
- package/cli.ts +33 -5
- package/package.json +1 -1
- package/src/clean.ts +24 -2
- package/src/depth.ts +190 -0
- package/src/legs.ts +189 -0
- package/src/outdir.ts +54 -0
- package/src/pelvis.ts +100 -15
- package/src/pose.ts +150 -18
- package/src/run.ts +74 -18
- package/src/runcompare.ts +6 -7
- package/src/verify.ts +90 -9
package/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.
|
|
@@ -113,7 +113,7 @@ A pose detector keeps little of a drawn pelvis tilt ([#288](https://github.com/r
|
|
|
113
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
114
|
|
|
115
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.
|
|
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
117
|
|
|
118
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
119
|
|
|
@@ -125,7 +125,36 @@ Both pass over the forearms: pixels within 0.15 hip widths of a segment elbow
|
|
|
125
125
|
|
|
126
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
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`).
|
|
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.
|
|
129
158
|
|
|
130
159
|
## What it does
|
|
131
160
|
|
|
@@ -135,7 +164,7 @@ rig-pose retarget --rig out/rig --map map.json --sequence dance.pose.json --out
|
|
|
135
164
|
|
|
136
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.
|
|
137
166
|
|
|
138
|
-
**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.
|
|
139
168
|
|
|
140
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).
|
|
141
170
|
|
|
@@ -164,7 +193,7 @@ and what the run cannot decide for you it lists:
|
|
|
164
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`).
|
|
165
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.
|
|
166
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.
|
|
167
|
-
- `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`.
|
|
168
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)).
|
|
169
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 }`.
|
|
170
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.
|
|
@@ -174,6 +203,7 @@ and what the run cannot decide for you it lists:
|
|
|
174
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.
|
|
175
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.
|
|
176
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)`.
|
|
177
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.
|
|
178
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.
|
|
179
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.
|
|
@@ -197,6 +227,8 @@ and what the run cannot decide for you it lists:
|
|
|
197
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.
|
|
198
228
|
|
|
199
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
|
+
|
|
200
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`.
|
|
201
233
|
|
|
202
234
|
|
|
@@ -286,6 +318,7 @@ rig-c's `scalex` multiplies the bone's x axis, and a child with the `normal` inh
|
|
|
286
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:
|
|
287
319
|
|
|
288
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).
|
|
289
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.
|
|
290
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.
|
|
291
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.
|
|
@@ -363,7 +396,8 @@ Exit codes: `0` written, `1` refused (every reason printed, nothing written), `2
|
|
|
363
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.
|
|
364
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).
|
|
365
398
|
- More than one person: `--person <id>`.
|
|
366
|
-
- **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.
|
|
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`).
|
|
367
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)).
|
|
368
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.
|
|
369
403
|
|
package/cli.ts
CHANGED
|
@@ -9,9 +9,11 @@
|
|
|
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';
|
|
16
18
|
import { PELVIS_MODES, runPelvis, type PelvisMode } from './src/pelvis.ts';
|
|
17
19
|
import { PROVENANCE_FILE } from './src/provenance.ts';
|
|
@@ -21,7 +23,7 @@ const HELP = `rig-pose ${pkg.version} — motion from a pose sequence onto a rig
|
|
|
21
23
|
|
|
22
24
|
Usage:
|
|
23
25
|
rig-pose pelvis --pose <pose.json> --out <pose.pelvis.json> (--frames <dir> [--mode silhouette|waistband] | --pelvis-keys <keys.json>) [--person <id>]
|
|
24
|
-
rig-pose clean --pose <pose.json> --out <pose.clean.json> [--loop] [--legs 5] [--rest 7] [--hold off] [--coco] [--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>]
|
|
25
27
|
rig-pose retarget --rig <rig> --map <map.json> --sequence <pose.json> --out <motion.json> [--animation dance] [flags]
|
|
26
28
|
rig-pose convert --coco <coco17.json> --out <pose.json>
|
|
27
29
|
rig-pose compare --reference <pose.json> --render <pose.json> --map <map.json> [flags]
|
|
@@ -72,6 +74,17 @@ clean (required before retarget; rig-c/pose#67)
|
|
|
72
74
|
--hold off turn the stillness hold off (rig-c/pose#91); recorded as hold: "off"
|
|
73
75
|
--hold-speed <n> the hold's speed limit, trunk lengths per frame (default: 0.015)
|
|
74
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)
|
|
75
88
|
|
|
76
89
|
In order, each step printed with its figures: (1) spikes — a joint more than 0.2 trunk lengths
|
|
77
90
|
(neck → mid_hip, the median over the clip) from its 5-frame median takes the median, each listed by
|
|
@@ -515,10 +528,15 @@ function runsOf(frames: number[]): string {
|
|
|
515
528
|
|
|
516
529
|
/** `rig-pose clean` (#67): read, clean, print each step's figures, write. */
|
|
517
530
|
function runClean(argv: string[], say: (l: string) => void): void {
|
|
518
|
-
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']);
|
|
519
532
|
const input = need(f, 'pose');
|
|
520
533
|
const out = need(f, 'out');
|
|
521
|
-
const
|
|
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 } : {}) };
|
|
522
540
|
const text = readFileSync(input, 'utf8');
|
|
523
541
|
const seq = f.has('coco') ? convertCoco17(text, input) : readPoseSequence(text, input);
|
|
524
542
|
const c = clean(seq, params, f.get('person'), input);
|
|
@@ -555,7 +573,13 @@ function runClean(argv: string[], say: (l: string) => void): void {
|
|
|
555
573
|
}
|
|
556
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)`);
|
|
557
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');
|
|
558
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);
|
|
559
583
|
writeFileSync(out, writePoseSequence(c.seq));
|
|
560
584
|
say(` CLEAN ${cleanLine(c.record)}`);
|
|
561
585
|
say(` wrote ${out}`);
|
|
@@ -605,7 +629,11 @@ export function main(argv: string[]): number {
|
|
|
605
629
|
const f = parseFlags(rest, ['coco', 'out'], []);
|
|
606
630
|
const input = need(f, 'coco');
|
|
607
631
|
const out = need(f, 'out');
|
|
632
|
+
const p = new Problems();
|
|
633
|
+
const outDirs = planOutDirs([['--out', out]], p);
|
|
634
|
+
p.throwIfAny();
|
|
608
635
|
const seq = convertCoco17(readFileSync(input, 'utf8'), input);
|
|
636
|
+
makeOutDirs(outDirs, (l) => process.stdout.write(`${l}\n`));
|
|
609
637
|
writeFileSync(out, writePoseSequence(seq));
|
|
610
638
|
process.stdout.write(`rig-pose convert: ${seq.frames.length} frame(s) of COCO-17 → body-18; wrote ${out}\n`);
|
|
611
639
|
return 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
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
|
|
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. */
|
|
@@ -191,6 +198,8 @@ export interface Cleaned {
|
|
|
191
198
|
* and the largest change smoothing made, degrees; null without a track.
|
|
192
199
|
*/
|
|
193
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;
|
|
194
203
|
}
|
|
195
204
|
|
|
196
205
|
/**
|
|
@@ -668,6 +677,10 @@ export function clean(seq: PoseSequence, params: CleanParams, wanted: string | u
|
|
|
668
677
|
}
|
|
669
678
|
const track = personTrack(seq, wanted, where);
|
|
670
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`);
|
|
671
684
|
const person = wanted ?? [...new Set(seq.frames.flatMap((f) => f.people.map((q) => q.id)))].sort()[0]!;
|
|
672
685
|
if (params.loop) {
|
|
673
686
|
const widest = Math.max(SPIKE_WINDOW, params.legs, params.rest);
|
|
@@ -825,6 +838,13 @@ export function clean(seq: PoseSequence, params: CleanParams, wanted: string | u
|
|
|
825
838
|
record.params.pelvis = { window: params.legs };
|
|
826
839
|
pelvis = { source: seq.pelvisSource, window: params.legs, read: tilt.filter((v) => v !== null).length, frames: n, moved };
|
|
827
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 ?? {}));
|
|
828
848
|
const { cleaned: _was, ...rest } = seq;
|
|
829
849
|
return {
|
|
830
850
|
// #92: a loop is one cycle of a gait repeated in place, so it carries no travel of its own.
|
|
@@ -843,6 +863,7 @@ export function clean(seq: PoseSequence, params: CleanParams, wanted: string | u
|
|
|
843
863
|
unheld,
|
|
844
864
|
seams,
|
|
845
865
|
pelvis,
|
|
866
|
+
depth,
|
|
846
867
|
};
|
|
847
868
|
}
|
|
848
869
|
|
|
@@ -862,7 +883,8 @@ export function cleanLine(r: CleanRecord, hold: (r: CleanRecord) => string = hol
|
|
|
862
883
|
const jit = (v: number | null) => (v === null ? 'not measured' : v.toFixed(2));
|
|
863
884
|
const hips = r.params.hipsInferred === undefined ? '' : `; hips inferred (${inferredSource(r)})`;
|
|
864
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`}`;
|
|
865
|
-
|
|
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}`;
|
|
866
888
|
}
|
|
867
889
|
|
|
868
890
|
/**
|
package/src/depth.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derived depth (rig-c/pose#281): which way each limb segment leans out of the
|
|
3
|
+
* picture, read off its 2D length, and the limbs' front-to-back order from it.
|
|
4
|
+
*
|
|
5
|
+
* A sequence carries x and y only, so nothing downstream can say which limb is
|
|
6
|
+
* nearer the camera. A segment drawn shorter than its rest length is tilted out
|
|
7
|
+
* of the picture plane, and by how much its length says: `ratio` = drawn length
|
|
8
|
+
* ÷ rest length, the rest length being the larger of the two sides' 95th
|
|
9
|
+
* percentile picture lengths of that segment over the clip — the straight length
|
|
10
|
+
* the leg scale reads (`STRAIGHT_PERCENTILE`, #148), per segment. Which way it
|
|
11
|
+
* tilts the length cannot say; the sign comes from the body:
|
|
12
|
+
*
|
|
13
|
+
* | segment | sense when foreshortened | why |
|
|
14
|
+
* | --------- | ------------------------ | --- |
|
|
15
|
+
* | thigh | forward | a hip flexes forward; it extends back too little to foreshorten past the band |
|
|
16
|
+
* | shin | walk: forward · run: back | the gait rule (rig-c/pose#281, HQ): a walk's lifted foot comes forward, a run's is kicked back — the knee alone cannot say, since it bends forward in both |
|
|
17
|
+
* | upper arm | with the opposite leg | in a gait each arm swings with the opposite leg: forward when the other side's thigh is the more foreshortened of the two, back when its own is; forward (a shoulder flexes forward) when neither thigh is foreshortened |
|
|
18
|
+
* | forearm | forward | an elbow bends toward the back of the body, so the forearm folds forward |
|
|
19
|
+
*
|
|
20
|
+
* "Forward" is the trunk's facing: toward the camera (+1) when the figure faces
|
|
21
|
+
* it (its left shoulder and hip drawn right of its right ones), away (−1) when
|
|
22
|
+
* it faces away, and undecided (sign 0) when the trunk is side-on — shoulders
|
|
23
|
+
* and hips drawn less than `SIDE_ON` trunk lengths wide — since forward is then
|
|
24
|
+
* across the picture, not into it. A ratio within `FLAT_BAND` is flat, sign 0,
|
|
25
|
+
* and so is one above it (drawn longer than rest is not a tilt).
|
|
26
|
+
*
|
|
27
|
+
* The layers: each joint's depth is its parent's plus its segment's,
|
|
28
|
+
* sign × rest × √(1 − ratio²) px (rest = drawn ÷ ratio), from the shoulders and
|
|
29
|
+
* hips at 0; a limb's depth is the mean of its middle and end joints'; the limbs
|
|
30
|
+
* are ordered nearest first, equal depths (to 0.01 px) sharing a layer. A limb
|
|
31
|
+
* whose segments are not both named on a frame is not ordered on it.
|
|
32
|
+
*
|
|
33
|
+
* Pure: no clock, no randomness, no disk.
|
|
34
|
+
*/
|
|
35
|
+
import type { Body18 } from './joints.ts';
|
|
36
|
+
import { percentile, STRAIGHT_PERCENTILE } from './legs.ts';
|
|
37
|
+
import { DEPTH_SEGMENT_NAMES, DEPTH_SEGMENTS, type DepthGait, type DepthSegment, type DepthSource, type FrameDepth, type Joint, type SegmentDepth } from './pose.ts';
|
|
38
|
+
|
|
39
|
+
/** A ratio within this band is flat (sign 0): cos 18.2° = 0.95, so a segment within about 18° of the picture plane reads flat. */
|
|
40
|
+
export const FLAT_BAND: readonly [number, number] = [0.95, 1.05];
|
|
41
|
+
/** The trunk is side-on when its shoulders and hips are drawn, on average, less than this many trunk lengths (neck → mid_hip) wide. */
|
|
42
|
+
export const SIDE_ON = 0.1;
|
|
43
|
+
/** The limbs `layers` orders, each by its segments from the trunk out. */
|
|
44
|
+
export const LIMBS = {
|
|
45
|
+
r_arm: ['r_upper_arm', 'r_forearm'],
|
|
46
|
+
l_arm: ['l_upper_arm', 'l_forearm'],
|
|
47
|
+
r_leg: ['r_thigh', 'r_shin'],
|
|
48
|
+
l_leg: ['l_thigh', 'l_shin'],
|
|
49
|
+
} as const satisfies Record<string, readonly [DepthSegment, DepthSegment]>;
|
|
50
|
+
export type Limb = keyof typeof LIMBS;
|
|
51
|
+
export const LIMB_NAMES = Object.keys(LIMBS) as Limb[];
|
|
52
|
+
|
|
53
|
+
type Pt = [number, number];
|
|
54
|
+
type Joints = Partial<Record<Body18, Joint>>;
|
|
55
|
+
const at = (j: Joint | undefined): Pt | null => (j === undefined || j.state === 'missing' || j.at === undefined ? null : j.at);
|
|
56
|
+
const kindOf = (s: DepthSegment) => s.slice(2) as 'upper_arm' | 'forearm' | 'thigh' | 'shin';
|
|
57
|
+
const other = (s: DepthSegment): DepthSegment => `${s.startsWith('r_') ? 'l' : 'r'}_${s.slice(2)}` as DepthSegment;
|
|
58
|
+
|
|
59
|
+
/** A segment's drawn length on a frame; null where a joint is not placed. */
|
|
60
|
+
function drawn(joints: Joints, s: DepthSegment): number | null {
|
|
61
|
+
const [a, b] = DEPTH_SEGMENTS[s];
|
|
62
|
+
const pa = at(joints[a]);
|
|
63
|
+
const pb = at(joints[b]);
|
|
64
|
+
return pa === null || pb === null ? null : Math.hypot(pb[0] - pa[0], pb[1] - pa[1]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The trunk's facing on a frame: +1 toward the camera, −1 away, 0 side-on or not placed. */
|
|
68
|
+
export function facingOf(joints: Joints): -1 | 0 | 1 {
|
|
69
|
+
const p = (n: Body18) => at(joints[n]);
|
|
70
|
+
const [rs, ls, rh, lh, neck] = [p('r_shoulder'), p('l_shoulder'), p('r_hip'), p('l_hip'), p('neck')];
|
|
71
|
+
if (rs === null || ls === null || rh === null || lh === null || neck === null) return 0;
|
|
72
|
+
const trunk = Math.hypot(neck[0] - (rh[0] + lh[0]) / 2, neck[1] - (rh[1] + lh[1]) / 2);
|
|
73
|
+
if (!(trunk > 0)) return 0;
|
|
74
|
+
const width = (ls[0] - rs[0] + (lh[0] - rh[0])) / 2 / trunk;
|
|
75
|
+
return width > SIDE_ON ? 1 : width < -SIDE_ON ? -1 : 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface DerivedDepth {
|
|
79
|
+
/** Per frame, each segment whose joints are placed. */
|
|
80
|
+
frames: FrameDepth[];
|
|
81
|
+
/** Each segment kind's rest length, px (the larger side's 95th percentile); null where neither side is ever placed. */
|
|
82
|
+
rest: Record<'upper_arm' | 'forearm' | 'thigh' | 'shin', number | null>;
|
|
83
|
+
/** Frames the trunk faced the camera, faced away, and was side-on (or not placed). */
|
|
84
|
+
facing: { toward: number; away: number; side: number };
|
|
85
|
+
/** Segment-frames by sign. */
|
|
86
|
+
signs: { toward: number; away: number; flat: number };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Each frame's depth, derived from the picture's lengths for `gait` (the table above). */
|
|
90
|
+
export function deriveDepth(track: readonly Joints[], gait: DepthGait): DerivedDepth {
|
|
91
|
+
const lengths = new Map(DEPTH_SEGMENT_NAMES.map((s) => [s, track.map((j) => drawn(j, s))]));
|
|
92
|
+
const rest = {} as DerivedDepth['rest'];
|
|
93
|
+
for (const k of ['upper_arm', 'forearm', 'thigh', 'shin'] as const) {
|
|
94
|
+
const sides = (['r', 'l'] as const).map((side) => percentile(lengths.get(`${side}_${k}`)!.filter((v): v is number => v !== null), STRAIGHT_PERCENTILE)).filter((v) => Number.isFinite(v) && v > 0);
|
|
95
|
+
rest[k] = sides.length === 0 ? null : Math.max(...sides);
|
|
96
|
+
}
|
|
97
|
+
const facing = { toward: 0, away: 0, side: 0 };
|
|
98
|
+
const signs = { toward: 0, away: 0, flat: 0 };
|
|
99
|
+
const frames = track.map((joints, i) => {
|
|
100
|
+
const face = facingOf(joints);
|
|
101
|
+
if (face === 1) facing.toward++;
|
|
102
|
+
else if (face === -1) facing.away++;
|
|
103
|
+
else facing.side++;
|
|
104
|
+
const ratio = (s: DepthSegment): number | null => {
|
|
105
|
+
const d = lengths.get(s)![i];
|
|
106
|
+
const r = rest[kindOf(s)];
|
|
107
|
+
return d === null || d === undefined || r === null ? null : d / r;
|
|
108
|
+
};
|
|
109
|
+
const out: FrameDepth = {};
|
|
110
|
+
for (const s of DEPTH_SEGMENT_NAMES) {
|
|
111
|
+
const r = ratio(s);
|
|
112
|
+
if (r === null) continue;
|
|
113
|
+
let sense: -1 | 1;
|
|
114
|
+
const kind = kindOf(s);
|
|
115
|
+
if (kind === 'shin') sense = gait === 'walk' ? 1 : -1;
|
|
116
|
+
else if (kind === 'upper_arm') {
|
|
117
|
+
// The arm swings with the opposite leg: its own thigh and the other side's, by how foreshortened each is.
|
|
118
|
+
const own = ratio(`${s.slice(0, 2)}thigh` as DepthSegment);
|
|
119
|
+
const opp = ratio(other(`${s.slice(0, 2)}thigh` as DepthSegment));
|
|
120
|
+
sense = own === null || opp === null || Math.min(own, opp) >= FLAT_BAND[0] || own === opp ? 1 : opp < own ? 1 : -1;
|
|
121
|
+
} else sense = 1;
|
|
122
|
+
const sign = (r < FLAT_BAND[0] ? face * sense : 0) as -1 | 0 | 1;
|
|
123
|
+
if (sign === 1) signs.toward++;
|
|
124
|
+
else if (sign === -1) signs.away++;
|
|
125
|
+
else signs.flat++;
|
|
126
|
+
out[s] = { ratio: r, sign: sign === 0 ? 0 : sign, source: 'derived' };
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
});
|
|
130
|
+
return { frames, rest, facing, signs };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** One frame's layers: the limbs nearest first, equal depths sharing a layer, and each limb's depth (px, + toward the camera). */
|
|
134
|
+
export interface FrameLayers {
|
|
135
|
+
order: Limb[][];
|
|
136
|
+
depth: Partial<Record<Limb, number>>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The depth one segment adds to its distal joint over its proximal one, px: sign × rest × √(1 − ratio²). */
|
|
140
|
+
export function segmentDepth(d: SegmentDepth, length: number): number {
|
|
141
|
+
if (d.sign === 0 || !(d.ratio > 0)) return 0;
|
|
142
|
+
const r = Math.min(d.ratio, 1);
|
|
143
|
+
return d.sign * (length / d.ratio) * Math.sqrt(1 - r * r);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The limbs' front-to-back order on one frame, from its depth and its joints' drawn lengths. */
|
|
147
|
+
export function layersOf(joints: Joints, depth: FrameDepth): FrameLayers {
|
|
148
|
+
const z: Partial<Record<Limb, number>> = {};
|
|
149
|
+
for (const limb of LIMB_NAMES) {
|
|
150
|
+
const [upper, lower] = LIMBS[limb];
|
|
151
|
+
const du = depth[upper];
|
|
152
|
+
const dl = depth[lower];
|
|
153
|
+
const lu = drawn(joints, upper);
|
|
154
|
+
const ll = drawn(joints, lower);
|
|
155
|
+
if (du === undefined || dl === undefined || lu === null || ll === null) continue;
|
|
156
|
+
const mid = segmentDepth(du, lu);
|
|
157
|
+
const end = mid + segmentDepth(dl, ll);
|
|
158
|
+
z[limb] = Math.round(((mid + end) / 2) * 100) / 100;
|
|
159
|
+
}
|
|
160
|
+
const placed = LIMB_NAMES.filter((l) => z[l] !== undefined).sort((a, b) => z[b]! - z[a]! || LIMB_NAMES.indexOf(a) - LIMB_NAMES.indexOf(b));
|
|
161
|
+
const order: Limb[][] = [];
|
|
162
|
+
for (const l of placed) {
|
|
163
|
+
const last = order[order.length - 1];
|
|
164
|
+
if (last !== undefined && z[last[0]!] === z[l]) last.push(l);
|
|
165
|
+
else order.push([l]);
|
|
166
|
+
}
|
|
167
|
+
return { order, depth: z };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** A frame's layers as text: "r_arm > l_arm = r_leg > l_leg". */
|
|
171
|
+
export const orderText = (order: Limb[][]): string => order.map((g) => g.join(' = ')).join(' > ');
|
|
172
|
+
|
|
173
|
+
/** The source a sequence's depth came from, or null when its frames carry none. */
|
|
174
|
+
export function depthSourceOf(frames: ReadonlyArray<{ depth?: FrameDepth }>): DepthSource | null {
|
|
175
|
+
for (const f of frames) for (const d of Object.values(f.depth ?? {})) return d.source;
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* retarget's DEPTH line (rig-c/pose#281): where the depth came from, on how many
|
|
181
|
+
* frames the limbs were ordered, and which limb was in front alone how often.
|
|
182
|
+
*/
|
|
183
|
+
export function depthLine(layers: readonly FrameLayers[], frames: ReadonlyArray<{ depth?: FrameDepth }>, gait: DepthGait | null): string {
|
|
184
|
+
const source = depthSourceOf(frames);
|
|
185
|
+
const from = source === null ? 'the sequence\'s depth (no segment named)' : source === 'derived' ? `the sequence's derived depth (clean --depth ${gait ?? '?'})` : 'the sequence\'s measured depth';
|
|
186
|
+
const ordered = layers.filter((l) => l.order.length > 0).length;
|
|
187
|
+
const front = LIMB_NAMES.map((limb) => `${limb} ${layers.filter((l) => l.order[0]?.length === 1 && l.order[0][0] === limb).length}`).join(', ');
|
|
188
|
+
const tied = layers.filter((l) => (l.order[0]?.length ?? 0) > 1).length;
|
|
189
|
+
return `DEPTH layers from ${from}: the limbs ordered front to back on ${ordered} of ${layers.length} frame(s); in front alone: ${front}; tied in front on ${tied} (the report's layers)`;
|
|
190
|
+
}
|