@rig-c/pose 0.41.0 → 0.43.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 +21 -11
- package/cli.ts +25 -8
- package/package.json +1 -1
- package/src/clean.ts +5 -4
- package/src/contacts.ts +250 -0
- package/src/depth.ts +53 -13
- package/src/draworder.ts +74 -23
- package/src/legs.ts +7 -0
- package/src/pelvis.ts +263 -4
- package/src/pose.ts +4 -3
- package/src/retarget.ts +158 -14
- package/src/run.ts +26 -9
- package/src/switch.ts +11 -4
package/README.md
CHANGED
|
@@ -104,7 +104,7 @@ A retarget of the cleaned `dance-a` fixture finds the same jumps (`FEET`) as the
|
|
|
104
104
|
## The pelvis track: `rig-pose pelvis`
|
|
105
105
|
|
|
106
106
|
```sh
|
|
107
|
-
rig-pose pelvis --pose dance.scan.json --frames frames/ --out dance.pelvis.json [--mode silhouette|waistband] [--person <id>]
|
|
107
|
+
rig-pose pelvis --pose dance.scan.json --frames frames/ --out dance.pelvis.json [--mode silhouette|waistband|below-cover] [--person <id>]
|
|
108
108
|
rig-pose pelvis --pose dance.scan.json --pelvis-keys pelvis.keys.json --out dance.pelvis.json
|
|
109
109
|
```
|
|
110
110
|
|
|
@@ -114,12 +114,13 @@ A pose detector keeps little of a drawn pelvis tilt ([#288](https://github.com/r
|
|
|
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
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
|
+
- **`below-cover`** ([#342](https://github.com/rig-c/pose/issues/342)): for a garment whose top is hidden under another — a sweater's hem lying over a skirt's waistband hangs level whatever the pelvis does, so the band-top reader reads the cover. It reads what the cover cannot hide, every colour through the 5×5 window mean, a pixel being garment when it is nearer the garment's colour than each leg's (sampled 0.7 of the way hip → knee) and the ground's: the **hem** (in each column of the span, the last garment row before three that are not; a line fitted through them), the **thigh roots** (along each hip → knee, where the garment stops for 3 px; the line from the right root to the left), and the waistband's band top only when the ground lies above it. The tilt is the mean of the cues; fewer than two, or two more than 8° apart, decline the frame by name (`too few cues below the cover`, `cues disagree`). A `CUES` line per frame prints every cue and the hip keypoints' line and sign beside the fit's; the hips are reported, not fitted (a detector's hips keep a fraction of a drawn tilt), and a `HIPS-SIGN` line names a frame where their sign is opposite the fit's. On the synthetic walk with shorts turned with the drawn pelvis under a level sweater (`sweater`), every frame reads within 1.5° of the drawn tilt; the `waistband` reader reads the sweater's level hem there. **Known limit:** a thigh drawn in front of the garment (raised toward the camera) is read as the hem — its outline over the cloth looks like the hem's edge, and the hem and that thigh's root agree on it — so those frames must be declined by hand (set their `pelvisTilt` to null, or leave them out of `--pelvis-keys`). Reading them is [#349](https://github.com/rig-c/pose/issues/349).
|
|
117
118
|
|
|
118
119
|
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
|
|
|
120
121
|
`--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
|
|
|
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
|
+
**The track** is `pelvisTilt` on every frame and `pelvisSource` (`"silhouette"`, `"waistband"`, `"below-cover"` 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
|
|
|
124
125
|
**`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
|
|
|
@@ -141,10 +142,12 @@ A sequence carries x and y only, so nothing downstream can say which limb is nea
|
|
|
141
142
|
| segment | leans | why |
|
|
142
143
|
| --- | --- | --- |
|
|
143
144
|
| thigh | forward | a hip flexes forward, and extends back too little to foreshorten past the band |
|
|
144
|
-
| shin |
|
|
145
|
+
| shin | back | the owner's walk statement ([#336](https://github.com/rig-c/pose/issues/336)): the lifted foot is behind the supporting leg. A knee folds the shin back, in a walk and in a run |
|
|
145
146
|
| 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
147
|
| forearm | forward | an elbow bends toward the back of the body, so the forearm folds forward |
|
|
147
148
|
|
|
149
|
+
The thigh's row is the owner's run statement ([#336](https://github.com/rig-c/pose/issues/336)): the lifted leg is in front, its knee protruding. The table is the same for both gaits, so `--depth walk` and `--depth run` write the same signs; the gait the flag records decides which joint `--draw-order depth` orders the legs by ([below](#draw-order-from-depth---draw-order-depth)).
|
|
150
|
+
|
|
148
151
|
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
152
|
- **`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
153
|
|
|
@@ -154,7 +157,7 @@ The record says it: `"depth": { "gait" }` in `params`, and the `CLEAN` line ends
|
|
|
154
157
|
|
|
155
158
|
**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 unless `--draw-order depth` asks ([below](#draw-order-from-depth---draw-order-depth)): without the flag the motion is byte for byte the same with and without depth.
|
|
156
159
|
|
|
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
|
|
160
|
+
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` and under `--depth walk`, as the generator draws them, and the two files differ only in the gait they record; on the frontal walk in place, the 28 shin-frames the generator draws away from the camera (the lifted foot behind) 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 48 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.
|
|
158
161
|
|
|
159
162
|
### Draw order from depth: `--draw-order depth`
|
|
160
163
|
|
|
@@ -162,13 +165,19 @@ On the synthetic fixtures ([`examples/walk/make-depth.ts`](https://github.com/ri
|
|
|
162
165
|
rig-pose retarget --rig rig/ --map map.json --sequence run.pose.json --out motion.json --name run --draw-order depth
|
|
163
166
|
```
|
|
164
167
|
|
|
165
|
-
With `--draw-order depth` (off by default; without it nothing changes) `retarget` keys the legs' draw order from the sequence's depth ([#304](https://github.com/rig-c/pose/issues/304)), so a
|
|
168
|
+
With `--draw-order depth` (off by default; without it nothing changes) `retarget` keys the legs' draw order from the sequence's depth ([#304](https://github.com/rig-c/pose/issues/304)), so a walk's lifted foot is drawn behind the supporting leg and a run's lifted leg, its knee forward, in front ([#336](https://github.com/rig-c/pose/issues/336)). It writes rig-c's draw-order timeline, `"drawOrder": [{ "t", "offsets": [{ "slot", "offset" }] }, …]` on the animation beside its `tracks`: each key moves slots against the setup order, and a key with no `offsets` is the setup order again. A key's `t` is half a frame before the frame it shows on (0 on the first frame), as the switches' attachment keys are: a key exactly on a frame time is a tie, and the render breaks it a frame late on about half of them ([#325](https://github.com/rig-c/pose/issues/325)).
|
|
166
169
|
|
|
167
170
|
- **The pair.** The legs: a leg is the map's chains naming its knee or ankle, their bones and the bones under them no other chain maps. Its slots are the slots on those bones. A slot of an alternate set (`alternates`) on none of them goes with the leg the set depicts, so a set keeps its own switch and takes the order of the leg it replaces. The legs are keyed when their slots overlap in the draw order, filling one unbroken run of the setup order. A slot of anything else drawn between them would be re-layered by the swap, and the setup order is not changed here. The pair is then not keyed, and the line names that slot. The arms across the trunk are a later card.
|
|
168
|
-
- **The rule.** On each frame the leg whose depth
|
|
171
|
+
- **The rule.** On each frame the leg whose depth is the lower is drawn behind: its slots first in the pair's run, in their setup order, then the other's. The depths must differ by at least a tie, the depth a flat chain can hold there. Inside the tie, or where a leg is not ordered, the order before holds. Before the first decided frame it is the setup order, or on a looped sequence the order the clip ends in. A key is written where the order changes.
|
|
172
|
+
- **The joint the gait names** ([#336](https://github.com/rig-c/pose/issues/336), the owner's two statements). On a sequence cleaned with `--depth walk` a leg's depth is its ankle's (the hip at 0, plus the thigh's, plus the shin's), the tie √(1 − 0.95²) × (thigh + shin) of the rest lengths (46.83 px on the fixtures): in a walk the lifted foot is behind the supporting leg. With `--depth run` it is its knee's (the thigh's), the tie √(1 − 0.95²) × thigh (24.96 px): in a run the lifted leg is in front, its knee protruding. The limb mean (`layers`, above) says neither: a lifted leg's thigh forward and shin back cancel in it, and a walk whose thighs stay inside the band leaves it at most half a shin (35 px on the fixtures), under its tie of √(1 − 0.95²) × (thigh + shin ÷ 2) (35.89 px). A measured depth names no gait and keeps the limb mean and its tie ([#304](https://github.com/rig-c/pose/issues/304)).
|
|
169
173
|
- **No depth.** On a sequence without depth the flag is refused (`DRAWORDER_NO_DEPTH`): the order would come from nowhere, and a motion written without it would read as ordered.
|
|
174
|
+
- **A walk cleaned before #336** ([#336](https://github.com/rig-c/pose/issues/336)). A derived depth whose foreshortened shin leans the way the trunk faces carries the sign table before #336, where a walk's lifted foot read forward; the ankle would draw that foot in front. The flag refuses it by name (`DEPTH_RECLEAN`), listing the frames, until it is cleaned again with this version. The table since leans the shin back in both gaits, so a record it wrote never matches. A run's signs did not change, so a run cleaned by an earlier release is read as it is, and so is a walk with no foreshortened shin (its signs read the same under both tables). Measured depth is not checked. The file format does not change.
|
|
175
|
+
|
|
176
|
+
On #281's fixtures, retargeted onto a limb-cut rig whose legs overlap (the left leg drawn first; `test/draworder.test.ts`):
|
|
170
177
|
|
|
171
|
-
|
|
178
|
+
- **The walk** (by the ankle): each of the 36 frames with one leg lifted draws that leg behind; the order changes on frames 3, 15, 27 and 39, where a lifted foot first goes back past the tie, and holds through the double support between. The generator orders the true ankles on 16 of the 48 frames, and the keyed order agrees on all 16.
|
|
179
|
+
- **The run** (by the knee): the order changes on frames 0, 10, … 50, where the other foot lands. The generator's swing thigh flexes forward from toe-off ([#343](https://github.com/rig-c/pose/issues/343)), so on each of the 42 frames with one leg lifted the lifted knee is in front of the supporting one, its thigh leaning forward past the band, and that leg is drawn in front. The generator orders the true knees on 42 of the 60 frames, and the keyed order agrees on all 42. On frame 12, on a rig that draws the right leg first, none of the 8,771 pixels the lifted right leg draws shows the standing leg, against 2,205 in the setup order.
|
|
180
|
+
- **Read otherwise**: by the limb mean or by the ankle, the run draws the lifted leg behind on 21 of its 42 lifted frames; by the limb mean or by the knee, the walk keys nothing (the setup order) and draws the lifted right leg in front on its 18 frames. Knees planted 10 px apart, inside the knee tie, hold the order before; without the tie they swap it.
|
|
172
181
|
|
|
173
182
|
## What it does
|
|
174
183
|
|
|
@@ -220,7 +229,7 @@ and what the run cannot decide for you it lists:
|
|
|
220
229
|
- `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.
|
|
221
230
|
- `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).
|
|
222
231
|
- `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)`.
|
|
223
|
-
- `DRAWORDER` — with `--draw-order depth` ([above](#draw-order-from-depth---draw-order-depth)): the keys and the spans they key, per leg drawn behind, cut to 260 characters — `DRAWORDER from depth, 6 key(s) over 3 span(s) (leg_r behind leg_l
|
|
232
|
+
- `DRAWORDER` — with `--draw-order depth` ([above](#draw-order-from-depth---draw-order-depth)): the keys and the spans they key, per leg drawn behind, cut to 260 characters — `DRAWORDER from depth by the knee (run; tie 24.96 px), 6 key(s) over 3 span(s) (leg_r behind leg_l 0–9, 20–29, 40–49)`, naming the joint and its tie for a keyed pair ([#336](https://github.com/rig-c/pose/issues/336)) — and a line per pair not keyed, saying why.
|
|
224
233
|
- `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.
|
|
225
234
|
- `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.
|
|
226
235
|
- `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.
|
|
@@ -244,7 +253,7 @@ and what the run cannot decide for you it lists:
|
|
|
244
253
|
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.
|
|
245
254
|
|
|
246
255
|
`--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`; `reduceChains` only with a per-chain `--reduce`), `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:
|
|
247
|
-
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. With `--draw-order depth` ([#304](https://github.com/rig-c/pose/issues/304)) it carries `drawOrder`: `{ source: "depth", pairs, keys, spans }`. `pairs` gives per pair `{ a, b, limbs, slots, keyed, tie }` (and `why` when not keyed), `keys` the timeline with each key's `frame
|
|
256
|
+
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. With `--draw-order depth` ([#304](https://github.com/rig-c/pose/issues/304)) it carries `drawOrder`: `{ source: "depth", pairs, keys, spans }`. `pairs` gives per pair `{ a, b, limbs, slots, keyed, tie }` (`by`, `"knee"` or `"ankle"`, when the sequence's gait names the joint the pair was read at, [#336](https://github.com/rig-c/pose/issues/336), the tie then that joint's; and `why` when not keyed), `keys` the timeline with each key's `frame` (the frame it first shows on; `t` there is that frame's time, as everywhere in the report, and the motion's key sits half a frame before it), and `spans` every span of one order `{ behind, front, frames, keyed }` (`keyed` false where the setup order already draws it so). The options carry `drawOrder: "depth"`. A run without the flag writes neither. A sequence without depth writes no `layers` key, and its report is what it was.
|
|
248
257
|
|
|
249
258
|
With a map that has a `reach` chain ([#282](https://github.com/rig-c/pose/issues/282)) the report also carries `reachOrigin`, one `{ chain, bone, joints, anchor, setup, posed: { max, frame, t }, segment, ofSegment, warn, threshold }` per reach chain in map order: the `REACH-ORIGIN` line's figures. `setup` and `posed.max` are the distance from the bone's origin to the translated bone's (where the rig puts the source joint), in rig units; `segment` the reached length; `ofSegment` the larger distance ÷ `segment`; `warn` whether it is past `threshold` (0.2). A map without one writes no `reachOrigin` key.
|
|
250
259
|
|
|
@@ -339,6 +348,7 @@ rig-c's `scalex` multiplies the bone's x axis, and a child with the `normal` inh
|
|
|
339
348
|
|
|
340
349
|
- 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.
|
|
341
350
|
- **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).
|
|
351
|
+
- **Contact keys, by hand** ([#303](https://github.com/rig-c/pose/issues/303), the route for drawn references that [#313](https://github.com/rig-c/pose/issues/313) asked for). A drawn frontal walk can defeat every reading above: its nearer foot is drawn lower, so ankle height names the foot in the air, and a slow walk can step under the vertical speed's 0.01-leg-length stillness, so #280 never reads it as frontal. A single floor line in the picture does not help either: seen from the front the ground is a band, not a line (#303's reading). The artist's key sheet already carries the contact table, so `--contact-keys <file>` takes it: `rig-pose-contact-keys/1`, `{ "spec": "rig-pose-contact-keys/1", "keys": [ { "frame", "side" } ], "note"? }`, `side` one of `l`, `r`, `both`, `none`. Frames are the sequence's, may be fractional (a key sheet timed off the frame grid, as `--pelvis-keys`, #294), and strictly increase within `[0, frames − 1]`. **A key holds until the next one**, and the last to the clip's end: contact is a state, not an angle, so there is nothing between `l` and `r` to interpolate. A fractional key takes effect on the first frame at or after it; frames before the first key are not covered and keep the reading without keys. **The precedence is contact keys, then the vertical speed (#280, on a frontal clip), then ankle height**: the keys are applied last, over whatever the readings decided, on every frame they cover. `l` or `r` is that foot; `none` is flight, no foot supports; `both` is the foot that landed, as #280 hands over on landing: the other side from the `l` or `r` key before the run, or, where the run starts the cover or follows a `none` key (flight), the side the next `l` or `r` key names (the side that stays down after). Those neighbours are read from the keys, so a key that reaches no whole frame still counts: `l@0, none@1.1, both@1.2, l@3` holds `l` on frame 2 (rig-c/pose#338). The support spans and changes are then rebuilt from the per-frame support, as #280's are; a step, #113's `lowered` or #119's `tracked` run is kept only where it starts before the cover, cut at the frame before it (a step's re-plant inside the cover is not read), and jumps are kept as read. `FEET` prints one `support: contact keys` line (the keys, the frames they cover, how many frames they changed, the flight they declare, the frames before the first key) and one `stride` line per support span naming what decided its frames and what the reading without keys would have named, e.g. `stride l 19–37: of 19 frame(s), decided by contact keys 19; the reading without keys would name r on 4 of them`; these stride lines stand in for #280's. The report gains a `contactKeys` key (the file's path and SHA-256, the key count, the first covered frame, the runs changed) and `options.contactKeys`, only with the flag. On the tests' slow synthetic frontal walk (8 drawings a stride, each held 4.75 frames at 24 fps, keyed from its third drawing on), the support equals the keys on all 142 covered frames, where the reading without keys misses them on 16; the 10 frames before the first key keep that reading (`test/contactkeys.test.ts`). A malformed file, a `side` outside the four, a frame outside the span or one not after the key before it is refused by name (`CONTACT_KEYS`), every problem in one run, and so are keys on a map whose legs give no support to decide. Without the flag nothing is read and the output is as it was.
|
|
342
352
|
- 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.
|
|
343
353
|
- 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.
|
|
344
354
|
- **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.
|
|
@@ -418,7 +428,7 @@ Exit codes: `0` written, `1` refused (every reason printed, nothing written), `2
|
|
|
418
428
|
- 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.
|
|
419
429
|
- 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).
|
|
420
430
|
- More than one person: `--person <id>`.
|
|
421
|
-
- **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`).
|
|
431
|
+
- **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"`, `"below-cover"` 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`).
|
|
422
432
|
- **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`).
|
|
423
433
|
- `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)).
|
|
424
434
|
- `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.
|
|
@@ -458,7 +468,7 @@ Which bones carry which joints. You write it for your rig:
|
|
|
458
468
|
⚠️ **Seen from the front, an elbow's side is not a joint limit.** An elbow flexes toward the camera, and its picture falls on either side of the upper arm depending on how the arm is turned. On a real 13 s front-facing dance (`dance-a`, 313 frames, RTMPose), the right elbow sat on the "wrong" side of the rig's rest on 165 frames (median 23°), and the left on 200 (median 29°, up to 179° with the hands behind the head). Every one was a real elbow bend. The knees strayed at most 8.8° and 14.7°, which is pose noise around a straight leg. So for front-facing material, arms go `"free"`. Legs on `"rest"` with `--bends clamp` hold only while the knees stay near straight. On `dance-b` (a second dance with deeper knee bends, rig-c/pose#27), that setting straightened the left knee for most of the dance: `leg_l1` was 48.3° off at p90, against 0.0–0.3° with free legs. Leave legs `"free"` unless the dance keeps its knees straight.
|
|
459
469
|
- `mid_hip` and `mid_shoulder` are the midpoints of the two hips and the two shoulders.
|
|
460
470
|
- `translate.scale` is one length measured on both: two points on the rig, two points in the pose (its median over the frames where both are good). Pick a long segment that does not foreshorten much.
|
|
461
|
-
- `face` (optional, [#138](https://github.com/rig-c/pose/issues/138), [#164](https://github.com/rig-c/pose/issues/164), [#173](https://github.com/rig-c/pose/issues/173), [#189](https://github.com/rig-c/pose/issues/189), [#190](https://github.com/rig-c/pose/issues/190), [#205](https://github.com/rig-c/pose/issues/205)): `{ "yaw"?: { "bone": "yaw", "range": [-15, 15], "sets"?: [{ "set": "T1", "above": 45 }] }, "pitch"?: { "bone": "pitch", "range": [-10, 15] }, "sets"?: [{ "set": "T1", "above": 45 }] }` (`min ≤ 0 ≤ max`, `min < max`). Reads per-frame yaw and pitch from `nose` against `r_eye + l_eye` (and `r_ear + l_ear` when present — where the eye span is foreshortened below `0.85 × median(de)` and the ear-chord offset `xea = (mid_ears − mid_eyes) · u / da` exceeds both `1.8 × |xne|` and `0.15`, yaw blends toward the ear chord `atan(2 × xea)` with weight `w = clamp((0.85 × median(de) − de) / ((0.85 − 0.80) × median(de)), 0, 1)` so a side gaze whose 2D nose sits near the eye midpoint reaches `atan(2 × xea)` below `0.80 × median(de)` and exits without a step), couples the head nod channel's clamped vertical travel (`clampedY`, `+2.8°/unit` when the `±2%` neck-length clamp acts on the sequence) into `pitch` in phase ([#190](https://github.com/rig-c/pose/issues/190), [#205](https://github.com/rig-c/pose/issues/205)), maps 1:1 inside `0.8 × range` (`[-12, 12]°` on `[-15, 15]°`) and compresses only the overshoot above the knee into the remaining 20% of the range so a single outlier frame does not crush the rest of the clip, and keys the named dial bone's `rotate` track (`FACE`). When `sets` (`[{ "set": "<name>", "above": <deg> }]`, `0 < above ≤ 90`) is declared on `face.yaw.sets` or `face.sets`, each set enters on frames where `|yaw| ≥ above` (or the highest `above` satisfied when multiple sets are declared) and leaves only when `|yaw| < above − 10°` (`10°` hysteresis), emitting stepped slot `attachment` keys for the set's `slots` and `hides
|
|
471
|
+
- `face` (optional, [#138](https://github.com/rig-c/pose/issues/138), [#164](https://github.com/rig-c/pose/issues/164), [#173](https://github.com/rig-c/pose/issues/173), [#189](https://github.com/rig-c/pose/issues/189), [#190](https://github.com/rig-c/pose/issues/190), [#205](https://github.com/rig-c/pose/issues/205)): `{ "yaw"?: { "bone": "yaw", "range": [-15, 15], "sets"?: [{ "set": "T1", "above": 45 }] }, "pitch"?: { "bone": "pitch", "range": [-10, 15] }, "sets"?: [{ "set": "T1", "above": 45 }] }` (`min ≤ 0 ≤ max`, `min < max`). Reads per-frame yaw and pitch from `nose` against `r_eye + l_eye` (and `r_ear + l_ear` when present — where the eye span is foreshortened below `0.85 × median(de)` and the ear-chord offset `xea = (mid_ears − mid_eyes) · u / da` exceeds both `1.8 × |xne|` and `0.15`, yaw blends toward the ear chord `atan(2 × xea)` with weight `w = clamp((0.85 × median(de) − de) / ((0.85 − 0.80) × median(de)), 0, 1)` so a side gaze whose 2D nose sits near the eye midpoint reaches `atan(2 × xea)` below `0.80 × median(de)` and exits without a step), couples the head nod channel's clamped vertical travel (`clampedY`, `+2.8°/unit` when the `±2%` neck-length clamp acts on the sequence) into `pitch` in phase ([#190](https://github.com/rig-c/pose/issues/190), [#205](https://github.com/rig-c/pose/issues/205)), maps 1:1 inside `0.8 × range` (`[-12, 12]°` on `[-15, 15]°`) and compresses only the overshoot above the knee into the remaining 20% of the range so a single outlier frame does not crush the rest of the clip, and keys the named dial bone's `rotate` track (`FACE`). When `sets` (`[{ "set": "<name>", "above": <deg> }]`, `0 < above ≤ 90`) is declared on `face.yaw.sets` or `face.sets`, each set enters on frames where `|yaw| ≥ above` (or the highest `above` satisfied when multiple sets are declared) and leaves only when `|yaw| < above − 10°` (`10°` hysteresis), emitting stepped slot `attachment` keys for the set's `slots` and `hides`, each half a frame before the frame it shows on as the switches' and the draw order's keys are ([#334](https://github.com/rig-c/pose/issues/334)) (resolved from `alternates` in the map or `sets.json` (`rig-parts-sets/1`) beside `rig.json`; an unknown set is refused by name as `MAP_SET_EXISTS`).
|
|
462
472
|
- `body` (optional, [#173](https://github.com/rig-c/pose/issues/173)): `{ "yaw"?: { "bone": "body_yaw", "range": [-30, 30], "sets"?: [{ "set": "T8", "above": 45 }] }, "sets"?: [{ "set": "T8", "above": 45 }] }` (`min ≤ 0 ≤ max`, `min < max`). Maps the per-frame body yaw read from the camera-normalised shoulder and hip width (`BODY`) into `range` with the same `0.8 × range` knee mapping as `face`, keying the named dial bone's `rotate` track, and switches any declared `sets` (`body.sets` or `body.yaw.sets`) with the same `above` / `above − 10°` hysteresis (overriding `face` sets and arm `alternates` on slots they share while active). Without `body` in the map, `BODY` and `--report`'s `body` still report the reading while leaving every track in `motion.json` unchanged.
|
|
463
473
|
|
|
464
474
|
### A joint's range: `range`
|
package/cli.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { runRetarget, type RetargetArgs } from './src/run.ts';
|
|
|
22
22
|
const HELP = `rig-pose ${pkg.version} — motion from a pose sequence onto a rig-c rig
|
|
23
23
|
|
|
24
24
|
Usage:
|
|
25
|
-
rig-pose pelvis --pose <pose.json> --out <pose.pelvis.json> (--frames <dir> [--mode silhouette|waistband] | --pelvis-keys <keys.json>) [--person <id>]
|
|
25
|
+
rig-pose pelvis --pose <pose.json> --out <pose.pelvis.json> (--frames <dir> [--mode silhouette|waistband|below-cover] | --pelvis-keys <keys.json>) [--person <id>]
|
|
26
26
|
rig-pose clean --pose <pose.json> --out <pose.clean.json> [--loop] [--legs 5] [--rest 7] [--hold off] [--depth walk|run] [--coco] [--person <id>]
|
|
27
27
|
rig-pose retarget --rig <rig> --map <map.json> --sequence <pose.json> --out <motion.json> [--animation dance] [flags]
|
|
28
28
|
rig-pose convert --coco <coco17.json> --out <pose.json>
|
|
@@ -38,11 +38,15 @@ pelvis (before clean; rig-c/pose#288)
|
|
|
38
38
|
r_hip → l_hip, about 0 facing the camera — or null on a frame the reading declined
|
|
39
39
|
--frames <dir> the picture: one PNG per sequence frame, in file-name order, the sequence's size (no video;
|
|
40
40
|
extract the frames first). The hip keypoints place the reading; their angle is not read
|
|
41
|
-
--mode silhouette|waistband
|
|
41
|
+
--mode silhouette|waistband|below-cover
|
|
42
42
|
silhouette (default): the straightest edge of the drawn figure across the hip span (between
|
|
43
43
|
the two hip keypoints), within 40° of level and one hip width of the hips' height — a drawn
|
|
44
44
|
pelvis line or the edge of a drawn hip. waistband: the top edge of a garment covering the hips
|
|
45
45
|
(skirt, shorts) — the colour at the hips' midpoint, its first row in each column of the span.
|
|
46
|
+
below-cover: for a garment whose top is hidden under another (a sweater's hem over a skirt)
|
|
47
|
+
— the bottomwear's lower hem across the span, where each thigh leaves it, and the band's top
|
|
48
|
+
only where nothing covers it; the mean of those cues, declined when they differ by more
|
|
49
|
+
than 8° (a CUES line per frame; the hip keypoints are printed there, not fitted).
|
|
46
50
|
Forearms (elbow → wrist) and hands are masked. A frame either cannot read (the pelvis blank,
|
|
47
51
|
no garment, no straight edge over 60% of the span) is declined, named on a DECLINED line,
|
|
48
52
|
and written null: marked, not guessed
|
|
@@ -82,8 +86,10 @@ clean (required before retarget; rig-c/pose#67)
|
|
|
82
86
|
(the trunk side-on). Which way a foreshortened segment leans comes from the body, with
|
|
83
87
|
"forward" the trunk's facing: thigh and forearm forward (a hip flexes forward, an elbow
|
|
84
88
|
bends back); an upper arm with the opposite leg (forward when the other thigh is the more
|
|
85
|
-
foreshortened); a shin
|
|
86
|
-
|
|
89
|
+
foreshortened); a shin back (a knee folds it back) — the same in a walk and a run, after
|
|
90
|
+
the owner's two statements (rig-c/pose#336): a walk's lifted foot is behind the supporting
|
|
91
|
+
leg, a run's lifted leg is in front, its knee protruding. The gait is this flag's value and
|
|
92
|
+
decides the joint --draw-order depth reads. tools/skeleton.ts colours the segments
|
|
87
93
|
by it, and retarget --report orders the limbs front to back by it (layers)
|
|
88
94
|
|
|
89
95
|
In order, each step printed with its figures: (1) spikes — a joint more than 0.2 trunk lengths
|
|
@@ -191,10 +197,20 @@ retarget
|
|
|
191
197
|
(the shortfall listed). RAISED lines (default: off; rig-c/pose#62)
|
|
192
198
|
--draw-order depth key the legs' draw order from the sequence's depth (clean --depth; rig-c/pose#304): on
|
|
193
199
|
each frame the leg whose depth reads farther from the camera is drawn behind the other,
|
|
194
|
-
a tie (equal depths) holding the order before, in rig-c's drawOrder timeline. The
|
|
200
|
+
a tie (equal depths) holding the order before, in rig-c's drawOrder timeline. The depth is
|
|
201
|
+
read at the joint the gait names (rig-c/pose#336): a walk (clean --depth walk) by the ankle,
|
|
202
|
+
the lifted foot behind; a run by the knee, the lifted leg in front. The legs'
|
|
195
203
|
slots must fill one unbroken run of the setup draw order (else the pair is not keyed and
|
|
196
204
|
the line names the slot between); a set's slot goes with the leg it depicts. Refused on
|
|
197
|
-
a sequence without depth (DRAWORDER_NO_DEPTH)
|
|
205
|
+
a sequence without depth (DRAWORDER_NO_DEPTH), and on a walk cleaned before rig-c/pose#336
|
|
206
|
+
(a shin leaning forward: DEPTH_RECLEAN; clean it again). DRAWORDER lines (default: off)
|
|
207
|
+
--contact-keys <file> which foot supports, by hand (rig-c/pose#303): rig-pose-contact-keys/1, { "spec":
|
|
208
|
+
"rig-pose-contact-keys/1", "keys": [{ "frame", "side" }], "note"? }, side l | r | both |
|
|
209
|
+
none; frames the sequence's, fractional allowed, strictly increasing in [0, N − 1]. A key
|
|
210
|
+
holds until the next one (contact is a state, not interpolated); before the first key the
|
|
211
|
+
support is read as without it. On every frame they cover the keys decide, over the
|
|
212
|
+
vertical speed (#280) and ankle height: none is flight; both, the foot that landed. Malformed
|
|
213
|
+
keys are refused by name (CONTACT_KEYS). FEET support and stride lines (default: off)
|
|
198
214
|
--idle <name> the rig's animation read as its idle (default: idle). Every idle track the new
|
|
199
215
|
animation does not key, on any channel, is listed as IDLE-DROPPED (a blink, a
|
|
200
216
|
breath); one it keys is IDLE-KEPT-OUT, naming the track that keys it, and so is a
|
|
@@ -383,7 +399,7 @@ function legScaleOf(f: Map<string, string>): [number, number] | 'off' {
|
|
|
383
399
|
const RETARGET_ALIASES = { sequence: 'pose', animation: 'name' } as const;
|
|
384
400
|
|
|
385
401
|
function retargetArgs(argv: string[]): RetargetArgs {
|
|
386
|
-
const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'images', 'min-score', 'smooth', 'flip', 'short', 'bends', 'bend-tolerance', 'switch-tolerance', 'switch-min', 'switch-at', 'profile', 'build', 'reference', 'render', 'parts', 'report', 'camera', 'leg-scale', 'idle', 'merge', 'reduce', 'reduce-hip', 'stance', 'floor', 'draw-order'], ['coco', 'plant-feet', 'carry-idle', 'raised-by-head', 'reduce-keep-planted', 'verbose', 'summary'], RETARGET_ALIASES);
|
|
402
|
+
const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'images', 'min-score', 'smooth', 'flip', 'short', 'bends', 'bend-tolerance', 'switch-tolerance', 'switch-min', 'switch-at', 'profile', 'build', 'reference', 'render', 'parts', 'report', 'camera', 'leg-scale', 'idle', 'merge', 'reduce', 'reduce-hip', 'stance', 'floor', 'draw-order', 'contact-keys'], ['coco', 'plant-feet', 'carry-idle', 'raised-by-head', 'reduce-keep-planted', 'verbose', 'summary'], RETARGET_ALIASES);
|
|
387
403
|
if (f.has('verbose') && f.has('summary')) throw new Usage('--verbose and --summary are both given; --verbose prints every line in full and --summary one line per measure, so give one');
|
|
388
404
|
if (f.has('reference') && !f.has('build')) throw new Usage(`--reference needs --build: the record it goes in is written beside the build (${PROVENANCE_FILE})`);
|
|
389
405
|
const bends = f.get('bends') ?? 'refuse';
|
|
@@ -433,6 +449,7 @@ function retargetArgs(argv: string[]): RetargetArgs {
|
|
|
433
449
|
...reduceOf(f),
|
|
434
450
|
output: f.has('verbose') ? 'verbose' : f.has('summary') ? 'summary' : 'default',
|
|
435
451
|
...(drawOrder !== undefined ? { drawOrder: 'depth' as const } : {}),
|
|
452
|
+
...(f.has('contact-keys') ? { contactKeysFile: f.get('contact-keys')! } : {}),
|
|
436
453
|
};
|
|
437
454
|
}
|
|
438
455
|
|
|
@@ -558,7 +575,7 @@ function runClean(argv: string[], say: (l: string) => void): void {
|
|
|
558
575
|
const input = need(f, 'pose');
|
|
559
576
|
const out = need(f, 'out');
|
|
560
577
|
const gait = f.get('depth');
|
|
561
|
-
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
|
|
578
|
+
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 the joint --draw-order depth orders the legs by: the ankle in a walk, the knee in a run)`);
|
|
562
579
|
const outPath = new Problems();
|
|
563
580
|
const outDirs = planOutDirs([['--out', out]], outPath);
|
|
564
581
|
outPath.throwIfAny();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.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
|
@@ -116,9 +116,10 @@ export interface CleanParams {
|
|
|
116
116
|
*/
|
|
117
117
|
hold?: { speed: number; reach: number } | 'off';
|
|
118
118
|
/**
|
|
119
|
-
* rig-c/pose#281: derive each frame's depth (`src/depth.ts`)
|
|
120
|
-
*
|
|
121
|
-
*
|
|
119
|
+
* rig-c/pose#281: derive each frame's depth (`src/depth.ts`) on the cleaned
|
|
120
|
+
* positions, and record this gait (rig-c/pose#336: it decides the joint the
|
|
121
|
+
* draw order reads, not a sign). Absent, no depth is derived and the output is
|
|
122
|
+
* what it was before #281.
|
|
122
123
|
*/
|
|
123
124
|
depth?: DepthGait;
|
|
124
125
|
}
|
|
@@ -841,7 +842,7 @@ export function clean(seq: PoseSequence, params: CleanParams, wanted: string | u
|
|
|
841
842
|
// rig-c/pose#281: derived on the cleaned positions, or a measured depth carried through as read.
|
|
842
843
|
let depth: DerivedDepth | null = null;
|
|
843
844
|
if (params.depth !== undefined) {
|
|
844
|
-
depth = deriveDepth(frames.map((f) => f.people[0]?.joints ?? {})
|
|
845
|
+
depth = deriveDepth(frames.map((f) => f.people[0]?.joints ?? {}));
|
|
845
846
|
depth.frames.forEach((d, i) => (frames[i]!.depth = d));
|
|
846
847
|
record.params.depth = { gait: params.depth };
|
|
847
848
|
} else if (carried === 'measured') seq.frames.forEach((f, i) => (frames[i]!.depth = f.depth ?? {}));
|
package/src/contacts.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--contact-keys` (rig-c/pose#303): the support declared by hand, frame by
|
|
3
|
+
* frame, for a clip no reading gets right — a drawn frontal walk. Seen from the
|
|
4
|
+
* front, depth is drawn as height, so the ground is a band in the picture and
|
|
5
|
+
* the nearer foot is drawn lower (#303's reading: no single floor line matches a
|
|
6
|
+
* contact table); a slow drawn walk can also step under the vertical speed's
|
|
7
|
+
* still tolerance, so #280 never takes over. The artist's key sheet already
|
|
8
|
+
* carries the contact table: these keys are that data, not a guess.
|
|
9
|
+
*
|
|
10
|
+
* `rig-pose-contact-keys/1`: `{ spec, keys: [{ frame, side }], note? }`, `side`
|
|
11
|
+
* one of `l | r | both | none`. Frames are the sequence's, may be fractional (a
|
|
12
|
+
* key sheet timed off the frame grid, as `--pelvis-keys`, #294), and strictly
|
|
13
|
+
* increase within [0, frames − 1].
|
|
14
|
+
*
|
|
15
|
+
* **A key holds until the next one** (a step function; the last holds to the
|
|
16
|
+
* clip's end). Contact is a state, not an angle: a foot is down or it is not, and
|
|
17
|
+
* there is no value between `l` and `r` to interpolate. A key at a fractional
|
|
18
|
+
* frame takes effect on the first frame at or after it. Frames before the first
|
|
19
|
+
* key are not covered: the support there is the reading without keys.
|
|
20
|
+
*
|
|
21
|
+
* Precedence: on every covered frame the keys decide, over #280's vertical
|
|
22
|
+
* speed (a frontal clip) and ankle height. `l`/`r`: that foot supports. `none`:
|
|
23
|
+
* flight, no foot supports. `both`: the foot that landed supports, as #280 hands
|
|
24
|
+
* over on landing. The run's neighbours are read from the keys, not the frames,
|
|
25
|
+
* so a key that reaches no frame counts (rig-c/pose#338): after an `l` or `r`
|
|
26
|
+
* key, the other side; after a `none` key (flight), or where the run starts the
|
|
27
|
+
* cover, the side the next `l` or `r` key names (the side that stays down after);
|
|
28
|
+
* else, at the cover's start, the support on the frame before (the reading
|
|
29
|
+
* without keys); a run none of those name keeps the reading without keys (`kept`).
|
|
30
|
+
*/
|
|
31
|
+
import type { Feet, Side } from './legs.ts';
|
|
32
|
+
import { isObject, parseJson, Problems, refuseUnknownKeys, typeName } from './problems.ts';
|
|
33
|
+
|
|
34
|
+
export const CONTACT_KEYS_SPEC = 'rig-pose-contact-keys/1';
|
|
35
|
+
|
|
36
|
+
/** A contact key's value: the foot down, both, or none (flight). */
|
|
37
|
+
export type ContactSide = Side | 'both' | 'none';
|
|
38
|
+
const VALUES: readonly ContactSide[] = ['l', 'r', 'both', 'none'];
|
|
39
|
+
|
|
40
|
+
export interface ContactKeys {
|
|
41
|
+
/** The file's keys, as read: strictly increasing frames within the sequence's span. */
|
|
42
|
+
keys: Array<{ frame: number; side: ContactSide }>;
|
|
43
|
+
/** Per frame, the key in force (the last key at or before the frame), or null before the first key. */
|
|
44
|
+
key: Array<ContactSide | null>;
|
|
45
|
+
/** The frames of keys no frame takes: the next key comes before the next whole frame (two keys between two frames). */
|
|
46
|
+
unreached: number[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* `rig-pose-contact-keys/1`, read against a sequence of `frames` frames. Every
|
|
51
|
+
* problem is collected and refused by name (`CONTACT_KEYS`): a file that is not
|
|
52
|
+
* JSON or not the shape, a `spec` that is not this one, a `side` outside the
|
|
53
|
+
* four, a frame outside [0, frames − 1], a frame not after the key before it.
|
|
54
|
+
*/
|
|
55
|
+
export function readContactKeys(text: string, where: string, frames: number): ContactKeys {
|
|
56
|
+
const doc = parseJson(text, where, 'CONTACT_KEYS');
|
|
57
|
+
const p = new Problems();
|
|
58
|
+
const shape = `{ "spec": "${CONTACT_KEYS_SPEC}", "keys": [{ "frame", "side" }], "note"? }`;
|
|
59
|
+
if (!isObject(doc)) {
|
|
60
|
+
p.add('CONTACT_KEYS', `${where} is ${typeName(doc)}; it must be ${shape}`);
|
|
61
|
+
p.throwIfAny();
|
|
62
|
+
throw new Error('unreachable');
|
|
63
|
+
}
|
|
64
|
+
refuseUnknownKeys(p, 'CONTACT_KEYS', where, doc, ['spec', 'keys', 'note']);
|
|
65
|
+
if (doc.spec !== CONTACT_KEYS_SPEC) p.add('CONTACT_KEYS', `${where} "spec" is ${typeName(doc.spec)}; it must be "${CONTACT_KEYS_SPEC}"`);
|
|
66
|
+
if ('note' in doc && typeof doc.note !== 'string') p.add('CONTACT_KEYS', `${where} "note" is ${typeName(doc.note)}; it must be a string`);
|
|
67
|
+
const keys: ContactKeys['keys'] = [];
|
|
68
|
+
if (!Array.isArray(doc.keys) || doc.keys.length === 0) p.add('CONTACT_KEYS', `${where} "keys" is ${typeName(doc.keys)}; it must be a non-empty array of { frame, side }`);
|
|
69
|
+
else doc.keys.forEach((k: unknown, i: number) => {
|
|
70
|
+
const kw = `${where} keys[${i}]`;
|
|
71
|
+
if (!isObject(k)) {
|
|
72
|
+
p.add('CONTACT_KEYS', `${kw} is ${typeName(k)}; a key is { frame, side }`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
refuseUnknownKeys(p, 'CONTACT_KEYS', kw, k, ['frame', 'side']);
|
|
76
|
+
const okF = typeof k.frame === 'number' && Number.isFinite(k.frame) && k.frame >= 0 && k.frame <= frames - 1;
|
|
77
|
+
const okS = typeof k.side === 'string' && (VALUES as readonly string[]).includes(k.side);
|
|
78
|
+
if (!okF) p.add('CONTACT_KEYS', `${kw} "frame" is ${typeName(k.frame)}; it must be a frame of the sequence in [0, ${frames - 1}] (fractional allowed)`);
|
|
79
|
+
if (!okS) p.add('CONTACT_KEYS', `${kw} "side" is ${typeName(k.side)}; it must be one of l, r, both, none (the foot down, both feet, or flight)`);
|
|
80
|
+
if (!okF || !okS) return;
|
|
81
|
+
const prev = keys[keys.length - 1];
|
|
82
|
+
if (prev !== undefined && (k.frame as number) <= prev.frame) p.add('CONTACT_KEYS', `${kw} "frame" is ${k.frame as number}, not after the key before it (${prev.frame}); frames strictly increase`);
|
|
83
|
+
else keys.push({ frame: k.frame as number, side: k.side as ContactSide });
|
|
84
|
+
});
|
|
85
|
+
p.throwIfAny();
|
|
86
|
+
const key: Array<ContactSide | null> = [];
|
|
87
|
+
const reached = new Set<number>();
|
|
88
|
+
let at = -1;
|
|
89
|
+
for (let i = 0; i < frames; i++) {
|
|
90
|
+
while (at + 1 < keys.length && keys[at + 1]!.frame <= i) at++;
|
|
91
|
+
key.push(at < 0 ? null : keys[at]!.side);
|
|
92
|
+
if (at >= 0) reached.add(at);
|
|
93
|
+
}
|
|
94
|
+
return { keys, key, unreached: keys.filter((_, k) => !reached.has(k)).map((k) => k.frame) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** rig-c/pose#303: what the contact keys did to a clip's support, set on `Feet` by `contactKeysOf`. */
|
|
98
|
+
export interface ContactKeysReading {
|
|
99
|
+
keys: ContactKeys['keys'];
|
|
100
|
+
/** Per frame, the key in force, or null before the first key. */
|
|
101
|
+
key: Array<ContactSide | null>;
|
|
102
|
+
/** The first frame a key covers; every frame from it to the last is covered. */
|
|
103
|
+
from: number;
|
|
104
|
+
/** Per frame, the support read without the keys (#280's or ankle height's). */
|
|
105
|
+
before: Array<Side | null>;
|
|
106
|
+
/** Per frame, whether a key decided its support (false outside the cover, and on a `both` run no side names, `kept`). */
|
|
107
|
+
keyed: boolean[];
|
|
108
|
+
/** The frames of `both` runs that keep the reading without keys: no keyed support before, no single-foot key after. */
|
|
109
|
+
kept: number[];
|
|
110
|
+
/** Runs of frames whose support the keys changed: the support without them (`was`) and with them (`now`; null: flight). */
|
|
111
|
+
changed: Array<{ frames: [number, number]; was: Side | null; now: Side | null }>;
|
|
112
|
+
/** As `ContactKeys.unreached`. */
|
|
113
|
+
unreached: number[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The support with the contact keys applied, last, over the reading `supportsOf`
|
|
118
|
+
* made (the header's precedence), then rebuilt as #280's frontal reading
|
|
119
|
+
* rebuilds it: support spans and changes from the per-frame support. Outside
|
|
120
|
+
* the cover the support is the reading without keys, unchanged. A step that
|
|
121
|
+
* released a plant (report only) and #113's `lowered` and #119's `tracked` runs
|
|
122
|
+
* are kept where they start before the cover, cut at the frame before it
|
|
123
|
+
* (rig-c/pose#338; a step's re-plant inside the cover is not read, null): inside
|
|
124
|
+
* it a plant ends where the keys hand support over. Jumps are kept as read (the
|
|
125
|
+
* hip's rise); on a jump frame a key still decides the support.
|
|
126
|
+
*/
|
|
127
|
+
export function contactKeysOf(feet: Feet, keys: ContactKeys): Feet {
|
|
128
|
+
const n = feet.support.length;
|
|
129
|
+
const before = feet.support.map((s) => s ?? null);
|
|
130
|
+
const support = [...before];
|
|
131
|
+
const keyed = new Array<boolean>(n).fill(false);
|
|
132
|
+
const kept: number[] = [];
|
|
133
|
+
const other = (s: Side): Side => (s === 'r' ? 'l' : 'r');
|
|
134
|
+
const from = keys.key.findIndex((k) => k !== null);
|
|
135
|
+
/** Per frame, the index in `keys.keys` of the key in force (−1 before the first). */
|
|
136
|
+
const at: number[] = [];
|
|
137
|
+
for (let i = 0, k = -1; i < n; i++) {
|
|
138
|
+
while (k + 1 < keys.keys.length && keys.keys[k + 1]!.frame <= i) k++;
|
|
139
|
+
at.push(k);
|
|
140
|
+
}
|
|
141
|
+
for (let i = 0; i < n; i++) {
|
|
142
|
+
const k = keys.key[i] ?? null;
|
|
143
|
+
if (k === null) continue;
|
|
144
|
+
if (k !== 'both') {
|
|
145
|
+
support[i] = k === 'none' ? null : k;
|
|
146
|
+
keyed[i] = true;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
let b = i;
|
|
150
|
+
while (b + 1 < n && keys.key[b + 1] === 'both') b++;
|
|
151
|
+
// rig-c/pose#338: the states either side come from the keys themselves, not from the frames — a key that reaches no frame (none@1.1 before both@1.2) is the state the run follows.
|
|
152
|
+
let k0 = at[i]!;
|
|
153
|
+
while (k0 > 0 && keys.keys[k0 - 1]!.side === 'both') k0--;
|
|
154
|
+
let k1 = at[b]! + 1;
|
|
155
|
+
while (k1 < keys.keys.length && keys.keys[k1]!.side === 'both') k1++;
|
|
156
|
+
const prevSide = k0 > 0 ? keys.keys[k0 - 1]!.side : null;
|
|
157
|
+
const nextSide = k1 < keys.keys.length ? keys.keys[k1]!.side : null;
|
|
158
|
+
const landed: Side | null = prevSide === 'r' || prevSide === 'l' ? other(prevSide) : nextSide === 'r' || nextSide === 'l' ? nextSide : prevSide === null && i > 0 ? support[i - 1]! : null;
|
|
159
|
+
for (let f = i; f <= b; f++) {
|
|
160
|
+
if (landed === null) kept.push(f);
|
|
161
|
+
else {
|
|
162
|
+
support[f] = landed;
|
|
163
|
+
keyed[f] = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
i = b;
|
|
167
|
+
}
|
|
168
|
+
const supports: Feet['supports'] = [];
|
|
169
|
+
const changes: Feet['changes'] = [];
|
|
170
|
+
const changed: ContactKeysReading['changed'] = [];
|
|
171
|
+
for (let i = 0; i < n; i++) {
|
|
172
|
+
const s = support[i] ?? null;
|
|
173
|
+
if (s !== before[i]) {
|
|
174
|
+
const last = changed[changed.length - 1];
|
|
175
|
+
if (last !== undefined && last.frames[1] === i - 1 && last.was === before[i] && last.now === s) last.frames[1] = i;
|
|
176
|
+
else changed.push({ frames: [i, i], was: before[i]!, now: s });
|
|
177
|
+
}
|
|
178
|
+
if (s === null) continue;
|
|
179
|
+
const prev = i > 0 ? (support[i - 1] ?? null) : null;
|
|
180
|
+
if (prev !== s) changes.push({ frame: i, from: prev, to: s });
|
|
181
|
+
const last = supports[supports.length - 1];
|
|
182
|
+
if (last !== undefined && last.side === s && last.frames[1] === i - 1) last.frames[1] = i;
|
|
183
|
+
else supports.push({ side: s, frames: [i, i] });
|
|
184
|
+
}
|
|
185
|
+
const cut = <T extends { frames: [number, number] }>(runs: readonly T[] | undefined): T[] => (runs ?? []).filter((r) => r.frames[0] < from).map((r) => ({ ...r, frames: [r.frames[0], Math.min(r.frames[1], from - 1)] as [number, number] }));
|
|
186
|
+
const lowered = feet.lowered === undefined ? undefined : cut(feet.lowered);
|
|
187
|
+
const tracked = feet.tracked === undefined ? undefined : cut(feet.tracked);
|
|
188
|
+
return {
|
|
189
|
+
legLength: feet.legLength,
|
|
190
|
+
support,
|
|
191
|
+
supports,
|
|
192
|
+
jumps: feet.jumps,
|
|
193
|
+
// rig-c/pose#338: a step running into the cover ends on the frame before it, as lowered and tracked do; a re-plant inside the cover is the keys' to decide, so it is not read (null).
|
|
194
|
+
steps: cut(feet.steps).map((s) => (s.replant !== null && s.replant >= from ? { ...s, replant: null } : s)),
|
|
195
|
+
changes,
|
|
196
|
+
...(lowered !== undefined && lowered.length > 0 ? { lowered } : {}),
|
|
197
|
+
...(tracked !== undefined && tracked.length > 0 ? { tracked } : {}),
|
|
198
|
+
...(feet.signal !== undefined ? { signal: feet.signal } : {}),
|
|
199
|
+
contactKeys: { keys: keys.keys, key: keys.key, from, before, keyed, kept, changed, unreached: keys.unreached },
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The signal names a stride line uses for the frames a key did not decide. */
|
|
204
|
+
const SIGNAL_NAMES: Record<string, string> = { speed: 'ankle vertical speed', height: 'ankle height' };
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The FEET lines of a run with contact keys (rig-c/pose#303), in place of #280's
|
|
208
|
+
* stride lines: one `support: contact keys` line — the keys, the frames they
|
|
209
|
+
* cover, how many they changed, the flight they declare, what stands outside —
|
|
210
|
+
* then one `stride` line per support span naming the signal that decided its
|
|
211
|
+
* frames (`contact keys`, else the reading without keys) and what that reading
|
|
212
|
+
* alone would have named.
|
|
213
|
+
*/
|
|
214
|
+
export function contactKeysLines(feet: Feet): string[] {
|
|
215
|
+
const c = feet.contactKeys;
|
|
216
|
+
if (c === undefined) return [];
|
|
217
|
+
const n = feet.support.length;
|
|
218
|
+
const span = (x: [number, number]) => (x[0] === x[1] ? `${x[0]}` : `${x[0]}–${x[1]}`);
|
|
219
|
+
const changed = c.changed.reduce((s, r) => s + r.frames[1] - r.frames[0] + 1, 0);
|
|
220
|
+
const flight = c.key.filter((k) => k === 'none').length;
|
|
221
|
+
const tail = [
|
|
222
|
+
...(flight > 0 ? [`${flight} keyed none (flight, no foot supports)`] : []),
|
|
223
|
+
...(c.kept.length > 0 ? [`${c.kept.length} keyed both with no side to hold (no keyed support before, no single-foot key after) keep it`] : []),
|
|
224
|
+
...(c.from > 0 ? [`frames ${span([0, c.from - 1])}, before the first key, keep it`] : []),
|
|
225
|
+
...(c.unreached.length > 0 ? [`key(s) at ${c.unreached.join(', ')} reach no frame (the next key comes first)`] : []),
|
|
226
|
+
];
|
|
227
|
+
const out = [`support: contact keys (rig-c/pose#303, --contact-keys) — ${c.keys.length} key(s) cover frames ${span([c.from, n - 1])} of ${n}, each holding until the next one; they change the reading without them on ${changed} frame(s)${tail.length > 0 ? `; ${tail.join('; ')}` : ''}`];
|
|
228
|
+
const by = feet.signal?.by;
|
|
229
|
+
for (const sp of feet.supports) {
|
|
230
|
+
let keyed = 0;
|
|
231
|
+
let differ = 0;
|
|
232
|
+
const rest = new Map<string, number>();
|
|
233
|
+
const was = new Set<string>();
|
|
234
|
+
for (let i = sp.frames[0]; i <= sp.frames[1]; i++) {
|
|
235
|
+
if (c.keyed[i]) keyed++;
|
|
236
|
+
else {
|
|
237
|
+
const name = SIGNAL_NAMES[by?.[i] ?? 'height'] ?? 'ankle height';
|
|
238
|
+
rest.set(name, (rest.get(name) ?? 0) + 1);
|
|
239
|
+
}
|
|
240
|
+
if (c.before[i] !== sp.side) {
|
|
241
|
+
differ++;
|
|
242
|
+
was.add(c.before[i] ?? 'none');
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const parts = [...(keyed > 0 ? [`contact keys ${keyed}`] : []), ...[...rest].map(([k, v]) => `${k} ${v}`)];
|
|
246
|
+
const alone = differ === 0 ? 'the reading without keys names the same' : `the reading without keys would name ${[...was].join(' or ')} on ${differ} of them`;
|
|
247
|
+
out.push(`stride ${sp.side} ${span(sp.frames)}: of ${sp.frames[1] - sp.frames[0] + 1} frame(s), decided by ${parts.join(', ')}; ${alone}`);
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|