@rig-c/pose 0.37.1 → 0.38.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 +36 -3
- package/cli.ts +40 -1
- package/package.json +1 -1
- package/src/clean.ts +50 -3
- package/src/map.ts +24 -3
- package/src/pelvis.ts +413 -0
- package/src/pose.ts +126 -23
- package/src/problems.ts +6 -3
- package/src/retarget.ts +98 -3
- package/src/run.ts +6 -3
- package/src/switch.ts +109 -21
package/README.md
CHANGED
|
@@ -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,32 @@ 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.
|
|
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`).
|
|
129
|
+
|
|
104
130
|
## What it does
|
|
105
131
|
|
|
106
132
|
```sh
|
|
@@ -140,6 +166,7 @@ and what the run cannot decide for you it lists:
|
|
|
140
166
|
- `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
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`.
|
|
142
168
|
- `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
|
+
- `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
170
|
- `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
171
|
- `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
172
|
- `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 +174,7 @@ and what the run cannot decide for you it lists:
|
|
|
147
174
|
- `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
175
|
- `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
176
|
- `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).
|
|
177
|
+
- `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
178
|
- `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
179
|
- `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
180
|
- `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 +196,7 @@ and what the run cannot decide for you it lists:
|
|
|
168
196
|
|
|
169
197
|
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
198
|
|
|
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:
|
|
199
|
+
`--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:
|
|
172
200
|
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
201
|
|
|
174
202
|
|
|
@@ -265,6 +293,7 @@ rig-c's `scalex` multiplies the bone's x axis, and a child with the `normal` inh
|
|
|
265
293
|
- **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
294
|
- **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
295
|
- **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.
|
|
296
|
+
- **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
297
|
- `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
298
|
- **`--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
299
|
- 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 +363,7 @@ Exit codes: `0` written, `1` refused (every reason printed, nothing written), `2
|
|
|
334
363
|
- 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
364
|
- 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
365
|
- 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.2", a newer minor than this reader's "rig-pose-sequence/1.1" — 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`).
|
|
337
367
|
- `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
368
|
- `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
369
|
|
|
@@ -412,10 +442,13 @@ Bone rotation cannot carry every pose: an arm behind the head, a forearm toward
|
|
|
412
442
|
- `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
443
|
- `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
444
|
- `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
|
-
-
|
|
445
|
+
- `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`).
|
|
446
|
+
- 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
447
|
|
|
417
448
|
**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
449
|
|
|
450
|
+
**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.
|
|
451
|
+
|
|
419
452
|
**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
453
|
|
|
421
454
|
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
|
@@ -13,18 +13,47 @@ import { convertCoco17, readPoseSequence, writePoseSequence } from './src/pose.t
|
|
|
13
13
|
import { parseWindows } from './src/compare.ts';
|
|
14
14
|
import { lineOf, Refusal } from './src/problems.ts';
|
|
15
15
|
import { runCompare, type CompareArgs } from './src/runcompare.ts';
|
|
16
|
+
import { PELVIS_MODES, runPelvis, type PelvisMode } from './src/pelvis.ts';
|
|
16
17
|
import { PROVENANCE_FILE } from './src/provenance.ts';
|
|
17
18
|
import { runRetarget, type RetargetArgs } from './src/run.ts';
|
|
18
19
|
|
|
19
20
|
const HELP = `rig-pose ${pkg.version} — motion from a pose sequence onto a rig-c rig
|
|
20
21
|
|
|
21
22
|
Usage:
|
|
23
|
+
rig-pose pelvis --pose <pose.json> --out <pose.pelvis.json> (--frames <dir> [--mode silhouette|waistband] | --pelvis-keys <keys.json>) [--person <id>]
|
|
22
24
|
rig-pose clean --pose <pose.json> --out <pose.clean.json> [--loop] [--legs 5] [--rest 7] [--hold off] [--coco] [--person <id>]
|
|
23
25
|
rig-pose retarget --rig <rig> --map <map.json> --sequence <pose.json> --out <motion.json> [--animation dance] [flags]
|
|
24
26
|
rig-pose convert --coco <coco17.json> --out <pose.json>
|
|
25
27
|
rig-pose compare --reference <pose.json> --render <pose.json> --map <map.json> [flags]
|
|
26
28
|
rig-pose --version | --help
|
|
27
29
|
|
|
30
|
+
pelvis (before clean; rig-c/pose#288)
|
|
31
|
+
--pose <file> rig-pose-sequence/1 as a scan wrote it, before clean (a cleaned one is refused,
|
|
32
|
+
PELVIS_AFTER_CLEAN: clean must smooth the track this writes)
|
|
33
|
+
--out <file> the same sequence with the pelvis track: "pelvisTilt" on every frame and "pelvisSource",
|
|
34
|
+
written as rig-pose-sequence/1.1. pelvisTilt is Spine degrees (counter-clockwise, y up) of
|
|
35
|
+
the direction from the figure's right hip to its left — the angle retarget reads off
|
|
36
|
+
r_hip → l_hip, about 0 facing the camera — or null on a frame the reading declined
|
|
37
|
+
--frames <dir> the picture: one PNG per sequence frame, in file-name order, the sequence's size (no video;
|
|
38
|
+
extract the frames first). The hip keypoints place the reading; their angle is not read
|
|
39
|
+
--mode silhouette|waistband
|
|
40
|
+
silhouette (default): the straightest edge of the drawn figure across the hip span (between
|
|
41
|
+
the two hip keypoints), within 40° of level and one hip width of the hips' height — a drawn
|
|
42
|
+
pelvis line or the edge of a drawn hip. waistband: the top edge of a garment covering the hips
|
|
43
|
+
(skirt, shorts) — the colour at the hips' midpoint, its first row in each column of the span.
|
|
44
|
+
Forearms (elbow → wrist) and hands are masked. A frame either cannot read (the pelvis blank,
|
|
45
|
+
no garment, no straight edge over 60% of the span) is declined, named on a DECLINED line,
|
|
46
|
+
and written null: marked, not guessed
|
|
47
|
+
--pelvis-keys <file> the hand override, instead of --frames: rig-pose-pelvis-keys/1, { "spec":
|
|
48
|
+
"rig-pose-pelvis-keys/1", "keys": [{ "frame", "deg" }], "note"? } — the tilt in pelvisTilt's
|
|
49
|
+
sense at frames, linear between keys (the short way round) and held outside them;
|
|
50
|
+
pelvisSource "keys". A frame may be fractional (a drawn key between scan frames,
|
|
51
|
+
rig-c/pose#294); frames strictly increase within [0, last frame], one outside is refused
|
|
52
|
+
--person <id> the person to read when the sequence holds more than one; only that person is written
|
|
53
|
+
|
|
54
|
+
clean then smooths pelvisTilt as its own track (the legs' window, wrapped with --loop), and retarget keys
|
|
55
|
+
the map's pelvis chain (one bone, joints r_hip → l_hip) from it, printing PELVIS: from <source>, swing N°.
|
|
56
|
+
|
|
28
57
|
clean (required before retarget; rig-c/pose#67)
|
|
29
58
|
--pose <file> rig-pose-sequence/1 as a scan wrote it, or COCO-17 with --coco
|
|
30
59
|
--out <file> the cleaned rig-pose-sequence/1, carrying the record of what was done ("cleaned")
|
|
@@ -506,6 +535,7 @@ function runClean(argv: string[], say: (l: string) => void): void {
|
|
|
506
535
|
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
536
|
const sg = (w: number) => (w === 1 ? '1 frame (none)' : `${w} frames`);
|
|
508
537
|
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'}`);
|
|
538
|
+
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
539
|
if (params.loop) {
|
|
510
540
|
if (c.floor === null) say(' FLOOR no ankle has a position on any frame; not re-pinned');
|
|
511
541
|
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`);
|
|
@@ -562,6 +592,15 @@ export function main(argv: string[]): number {
|
|
|
562
592
|
runClean(rest, (l) => process.stdout.write(`${l}\n`));
|
|
563
593
|
return 0;
|
|
564
594
|
}
|
|
595
|
+
if (cmd === 'pelvis') {
|
|
596
|
+
const f = parseFlags(rest, ['pose', 'out', 'frames', 'mode', 'pelvis-keys', 'person'], []);
|
|
597
|
+
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');
|
|
598
|
+
const mode = f.get('mode') ?? 'silhouette';
|
|
599
|
+
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');
|
|
600
|
+
if (!(PELVIS_MODES as readonly string[]).includes(mode)) throw new Usage(`--mode is "${mode}"; it must be ${PELVIS_MODES.join(' or ')}`);
|
|
601
|
+
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`));
|
|
602
|
+
return 0;
|
|
603
|
+
}
|
|
565
604
|
if (cmd === 'convert') {
|
|
566
605
|
const f = parseFlags(rest, ['coco', 'out'], []);
|
|
567
606
|
const input = need(f, 'coco');
|
|
@@ -571,7 +610,7 @@ export function main(argv: string[]): number {
|
|
|
571
610
|
process.stdout.write(`rig-pose convert: ${seq.frames.length} frame(s) of COCO-17 → body-18; wrote ${out}\n`);
|
|
572
611
|
return 0;
|
|
573
612
|
}
|
|
574
|
-
process.stderr.write(`rig-pose: unknown command "${cmd}" — the commands are: clean, retarget, convert, compare\n`);
|
|
613
|
+
process.stderr.write(`rig-pose: unknown command "${cmd}" — the commands are: pelvis, clean, retarget, convert, compare\n`);
|
|
575
614
|
return 2;
|
|
576
615
|
} catch (e) {
|
|
577
616
|
if (e instanceof Usage) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.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,7 @@
|
|
|
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 type { CleanRecord, CleanSeam, CleanSpike, CleanUnheld, Joint, PoseFrame, PoseSequence } from './pose.ts';
|
|
69
69
|
|
|
70
70
|
/** A joint not held inside a region where others are (#91): the test it fails there and its figure. */
|
|
71
71
|
export type Unheld = CleanUnheld;
|
|
@@ -185,6 +185,33 @@ export interface Cleaned {
|
|
|
185
185
|
unheld: Unheld[];
|
|
186
186
|
/** Back-to-back holds of one joint, cross-faded, in frame then body-18 order. */
|
|
187
187
|
seams: Seam[];
|
|
188
|
+
/**
|
|
189
|
+
* rig-c/pose#288: the sequence's pelvis track, smoothed as its own track over the
|
|
190
|
+
* legs' window (wrapped with loop): its source, how many frames carry a reading,
|
|
191
|
+
* and the largest change smoothing made, degrees; null without a track.
|
|
192
|
+
*/
|
|
193
|
+
pelvis: { source: string; window: number; read: number; frames: number; moved: number } | null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* rig-c/pose#288: a pelvis track smoothed as its own track — the quadratic
|
|
198
|
+
* Savitzky–Golay over `window` frames (circular with loop), on the frames that
|
|
199
|
+
* carry a reading; a declined frame (null) stays null and is not read. The angles
|
|
200
|
+
* are taken about the first reading (each brought within ±180° of it) so a track
|
|
201
|
+
* near ±180° (a figure facing away) is smoothed without the wrap, and wrapped
|
|
202
|
+
* back into (-180, 180] after.
|
|
203
|
+
*/
|
|
204
|
+
export function smoothPelvis(tilt: ReadonlyArray<number | null>, window: number, loop: boolean): Array<number | null> {
|
|
205
|
+
const first = tilt.find((v): v is number => v !== null);
|
|
206
|
+
if (first === undefined) return tilt.map(() => null);
|
|
207
|
+
const wrap = (d: number) => {
|
|
208
|
+
let x = d % 360;
|
|
209
|
+
if (x > 180) x -= 360;
|
|
210
|
+
if (x <= -180) x += 360;
|
|
211
|
+
return x;
|
|
212
|
+
};
|
|
213
|
+
const about: Series = tilt.map((v) => (v === null ? null : [first + wrap(v - first), 0]));
|
|
214
|
+
return savitzkyGolay(about, window, loop).map((q) => (q === null ? null : wrap(q[0])));
|
|
188
215
|
}
|
|
189
216
|
|
|
190
217
|
/**
|
|
@@ -765,7 +792,7 @@ export function clean(seq: PoseSequence, params: CleanParams, wanted: string | u
|
|
|
765
792
|
}),
|
|
766
793
|
};
|
|
767
794
|
const inferred = params.hipsInferred === true ? { r_hip: 0, l_hip: 0 } : null;
|
|
768
|
-
const frames = seq.frames.map((f, i) => {
|
|
795
|
+
const frames: PoseFrame[] = seq.frames.map((f, i) => {
|
|
769
796
|
const q = f.people.find((x) => x.id === person);
|
|
770
797
|
if (q === undefined) return { t: f.t, people: [] };
|
|
771
798
|
const joints: Partial<Record<Body18, Joint>> = {};
|
|
@@ -780,6 +807,24 @@ export function clean(seq: PoseSequence, params: CleanParams, wanted: string | u
|
|
|
780
807
|
}
|
|
781
808
|
return { t: f.t, people: [{ id: person, joints: sortedJoints(joints) }] };
|
|
782
809
|
});
|
|
810
|
+
// rig-c/pose#288: the pelvis track, its own track over the legs' window (the hips' window), not re-read from the hips.
|
|
811
|
+
let pelvis: Cleaned['pelvis'] = null;
|
|
812
|
+
if (seq.pelvisSource !== undefined) {
|
|
813
|
+
const tilt = seq.frames.map((f) => f.pelvisTilt ?? null);
|
|
814
|
+
const smooth = smoothPelvis(tilt, params.legs, params.loop);
|
|
815
|
+
let moved = 0;
|
|
816
|
+
tilt.forEach((v, i) => {
|
|
817
|
+
const w = smooth[i];
|
|
818
|
+
if (v !== null && w !== null && w !== undefined) {
|
|
819
|
+
let d = Math.abs(w - v) % 360;
|
|
820
|
+
if (d > 180) d = 360 - d;
|
|
821
|
+
moved = Math.max(moved, d);
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
smooth.forEach((v, i) => (frames[i]!.pelvisTilt = v));
|
|
825
|
+
record.params.pelvis = { window: params.legs };
|
|
826
|
+
pelvis = { source: seq.pelvisSource, window: params.legs, read: tilt.filter((v) => v !== null).length, frames: n, moved };
|
|
827
|
+
}
|
|
783
828
|
const { cleaned: _was, ...rest } = seq;
|
|
784
829
|
return {
|
|
785
830
|
// #92: a loop is one cycle of a gait repeated in place, so it carries no travel of its own.
|
|
@@ -797,6 +842,7 @@ export function clean(seq: PoseSequence, params: CleanParams, wanted: string | u
|
|
|
797
842
|
holds,
|
|
798
843
|
unheld,
|
|
799
844
|
seams,
|
|
845
|
+
pelvis,
|
|
800
846
|
};
|
|
801
847
|
}
|
|
802
848
|
|
|
@@ -815,7 +861,8 @@ export function cleanLine(r: CleanRecord, hold: (r: CleanRecord) => string = hol
|
|
|
815
861
|
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
862
|
const jit = (v: number | null) => (v === null ? 'not measured' : v.toFixed(2));
|
|
817
863
|
const hips = r.params.hipsInferred === undefined ? '' : `; hips inferred (${inferredSource(r)})`;
|
|
818
|
-
|
|
864
|
+
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
|
+
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}`;
|
|
819
866
|
}
|
|
820
867
|
|
|
821
868
|
/**
|
package/src/map.ts
CHANGED
|
@@ -58,6 +58,11 @@
|
|
|
58
58
|
* `place: "by-hip"` (rig-c/pose#93) places the wrist of each of its chains that
|
|
59
59
|
* `pose` puts within one hip width of the mid-hip by its offset from the hip
|
|
60
60
|
* (`byHipChains`; the retarget solves it).
|
|
61
|
+
* `phase: "left-step" | "right-step"` (rig-c/pose#286) declares a gait set drawn
|
|
62
|
+
* for the foot that is stepping (swinging), named by it — left-step: the left foot is stepping; shown while the right foot supports (right-step
|
|
63
|
+
* mirrors it). It shows exactly on the report's `feet.supports` spans of the
|
|
64
|
+
* supporting foot and is switched on the support hand-over, not on angle
|
|
65
|
+
* distance (`PHASE_SIDE`).
|
|
61
66
|
* `switch.ts` decides when each one shows.
|
|
62
67
|
*/
|
|
63
68
|
import type { RawBone } from 'rig-c/core';
|
|
@@ -86,6 +91,9 @@ export interface Translate {
|
|
|
86
91
|
joint: string;
|
|
87
92
|
scale: { pose: [string, string]; rig: [string, string] };
|
|
88
93
|
}
|
|
94
|
+
/** The step sides a set may declare as its `phase` (rig-c/pose#286), and the supporting foot each one shows on: left-step: the left foot is stepping; shown while the right foot supports. */
|
|
95
|
+
export const PHASE_SIDE = { 'left-step': 'r', 'right-step': 'l' } as const;
|
|
96
|
+
export type Phase = keyof typeof PHASE_SIDE;
|
|
89
97
|
/** An alternate drawing: a set of slots switched together, and the key skeleton it is selected by. */
|
|
90
98
|
export interface AlternateSet {
|
|
91
99
|
name: string;
|
|
@@ -113,6 +121,8 @@ export interface AlternateSet {
|
|
|
113
121
|
support?: string[];
|
|
114
122
|
/** `anchor` (rig-c/pose#191): the set's two-bone arm chains whose hand-tip is anchored to the set's `drawn` head-local offset while the set shows. */
|
|
115
123
|
anchor?: string[];
|
|
124
|
+
/** `phase` (rig-c/pose#286): the stepping foot the set is drawn for (left-step: the left foot is stepping; shown while the right foot supports); it shows exactly on that supporting foot's `feet.supports` spans, switched on the hand-over. Absent: the angle rule. */
|
|
125
|
+
phase?: Phase;
|
|
116
126
|
}
|
|
117
127
|
export interface YawSet {
|
|
118
128
|
set: string;
|
|
@@ -423,7 +433,7 @@ function readBodyMap(p: Problems, where: string, raw: unknown): BodyMap | undefi
|
|
|
423
433
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
424
434
|
}
|
|
425
435
|
|
|
426
|
-
const SET_SHAPE = '{ name, slots: { "<slot>": "<attachment>" }, hides?: [slot], chains: [chain], pose: { "<joint>": [x, y] }, drawn?: { "<bone>": deg }, offsets?: { "<bone>": [dx, dy] }, turn?: { "<bone>": [min, max] }, frames?: [[first, last], …], place?: "by-hip" }';
|
|
436
|
+
const SET_SHAPE = '{ name, slots: { "<slot>": "<attachment>" }, hides?: [slot], chains: [chain], pose: { "<joint>": [x, y] }, drawn?: { "<bone>": deg }, offsets?: { "<bone>": [dx, dy] }, turn?: { "<bone>": [min, max] }, frames?: [[first, last], …], place?: "by-hip", phase?: "left-step" | "right-step" }';
|
|
427
437
|
|
|
428
438
|
/** The joints a wrist is, for `place: "by-hip"`: a chain whose last joint is one of them carries a wrist. */
|
|
429
439
|
export const WRISTS: readonly string[] = ['r_wrist', 'l_wrist'];
|
|
@@ -472,7 +482,7 @@ function readAlternates(p: Problems, where: string, raw: unknown, chains: readon
|
|
|
472
482
|
p.add('MAP_ALT_SHAPE', `${aw} is ${typeName(a)}; a set is ${SET_SHAPE}`);
|
|
473
483
|
return;
|
|
474
484
|
}
|
|
475
|
-
refuseUnknownKeys(p, 'MAP_KEYS', aw, a, ['name', 'note', 'slots', 'hides', 'chains', 'pose', 'drawn', 'offsets', 'turn', 'frames', 'place', 'symmetric', 'support', 'anchor']);
|
|
485
|
+
refuseUnknownKeys(p, 'MAP_KEYS', aw, a, ['name', 'note', 'slots', 'hides', 'chains', 'pose', 'drawn', 'offsets', 'turn', 'frames', 'place', 'symmetric', 'support', 'anchor', 'phase']);
|
|
476
486
|
const name = typeof a.name === 'string' && a.name !== '' ? a.name : null;
|
|
477
487
|
if (name === null) p.add('MAP_ALT_NAME', `${aw} "name" is ${typeName(a.name)}; it must be a non-empty string`);
|
|
478
488
|
else if (names.has(name)) p.add('MAP_ALT_NAME', `${aw} "name" "${name}" names two sets`);
|
|
@@ -676,7 +686,18 @@ function readAlternates(p: Problems, where: string, raw: unknown, chains: readon
|
|
|
676
686
|
if (ok) anchor = anc;
|
|
677
687
|
}
|
|
678
688
|
}
|
|
679
|
-
|
|
689
|
+
let phase: Phase | undefined;
|
|
690
|
+
if ('phase' in a) {
|
|
691
|
+
const phases = Object.keys(PHASE_SIDE);
|
|
692
|
+
if (typeof a.phase !== 'string' || !phases.includes(a.phase)) {
|
|
693
|
+
p.add('MAP_ALT_PHASE', `${label} "phase" is ${JSON.stringify(a.phase)}; it is the stepping foot the set is drawn for, one of ${phases.map((x) => `"${x}"`).join(', ')} (left-step: the left foot is stepping; shown while the right foot supports)`);
|
|
694
|
+
ok = false;
|
|
695
|
+
} else if ('frames' in a) {
|
|
696
|
+
p.add('MAP_ALT_PHASE', `${label} names both "phase" and "frames"; a phase set shows on its supporting foot's spans, and a window would cut them — drop one`);
|
|
697
|
+
ok = false;
|
|
698
|
+
} else phase = a.phase as Phase;
|
|
699
|
+
}
|
|
700
|
+
if (ok && name !== null && cs !== null) out.push({ name, slots, hides, chains: cs, pose, drawn, offsets, turn, frames, ...(place === null ? {} : { place }), ...(symmetric === undefined ? {} : { symmetric }), ...(support === undefined ? {} : { support }), ...(anchor === undefined ? {} : { anchor }), ...(phase === undefined ? {} : { phase }) });
|
|
680
701
|
});
|
|
681
702
|
return out;
|
|
682
703
|
}
|