@rig-c/pose 0.18.0 → 0.19.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 +8 -8
- package/package.json +1 -1
- package/src/clean.ts +8 -1
- package/src/legs.ts +30 -11
- package/src/pose.ts +71 -29
- package/src/verify.ts +66 -16
package/README.md
CHANGED
|
@@ -83,11 +83,11 @@ A frame gives no reading, declined with the reason, when:
|
|
|
83
83
|
- the edge is too broken or too far from a line;
|
|
84
84
|
- the reading is over 25° from level, or a lone outlier: further, per frame, than the misread guard allows from both the read before it and the reading after it.
|
|
85
85
|
|
|
86
|
-
The tool never draws a line through a hand in front.
|
|
86
|
+
The tool never draws a line through a hand in front. If two reads lie close in time on either side (at most 1 s apart), a declined frame takes the linear interpolation between them. Over the clip's unheld read frames (or all read frames when fewer than two are unheld), the tool fits a zero-intercept gain (`fit.gain`) and correlation (`fit.corr`) between the detector's hip angle and the read waistband angle ([#106](https://github.com/rig-c/pose/issues/106)). On a declined run longer than 1 s (or at the clip's start or end), when `fit.corr >= 0.9` the frame takes the detector's hip angle scaled by `fit.gain` (`scaled`); when `fit.corr < 0.9` the detector line does not track the waistband on that clip, so the run is marked `unmeasured` (keeping the detector's hip pair, refused nowhere, and named on the line).
|
|
87
87
|
|
|
88
|
-
Both hips are marked `inferred` on every frame and turned about their midpoint to the frame's angle, keeping the midpoint and the width. The record says which frame is which: `"hipsInferred": { "by": "measured", "method", "read", "interpolated", "held": [[first, last], …], "declined": [{ "frames": [first, last], "why" }] }`. Every frame is in exactly one of `read`, `interpolated` and `
|
|
88
|
+
Both hips are marked `inferred` on every frame and turned about their midpoint to the frame's angle, keeping the midpoint and the width. The record says which frame is which: `"hipsInferred": { "by": "measured", "method", "fit"?: { "frames", "gain", "corr" }, "read", "interpolated", "scaled"?: [[first, last], …], "held": [[first, last], …], "unmeasured"?: [[first, last], …], "declined": [{ "frames": [first, last], "why" }] }`. Every frame is in exactly one of `read`, `interpolated`, `scaled`, `held` and `unmeasured`, and `declined` lists exactly the non-read frames. Runs are frame indices inside the sequence, increasing and disjoint. A malformed record is refused by name (`POSE_CLEANED`).
|
|
89
89
|
|
|
90
|
-
`tools/pelvis_selftest.py` is its synthetic check. A planted wrist on a hand in front of the waist reads "covered", and with the hand moved away the edge's angle is read again. The misread guard, the interpolation and the
|
|
90
|
+
`tools/pelvis_selftest.py` is its synthetic check. A planted wrist on a hand in front of the waist reads "covered", and with the hand moved away the edge's angle is read again. The misread guard, the interpolation, the detector-gain scaling on long covered spans (`#106`, with `--hold-covered` as its planted mutant) and the `unmeasured` path (`r < 0.9`) are checked frame by frame. Its cover mutant (`--no-cover-check`) must read a line through the hand, and the check asserts that it does.
|
|
91
91
|
|
|
92
92
|
**Measured** on HQ's references ([#67](https://github.com/rig-c/pose/issues/67)'s bars):
|
|
93
93
|
|
|
@@ -128,7 +128,7 @@ and what the run cannot decide for you it lists:
|
|
|
128
128
|
- `FLIP` — a segment that turns more than `--flip` degrees in one frame (with `--flip <deg>`; off by default) is spread over the frames around the jump; one line per spread, with the chain, the segment, the jump, the frames and h. Below.
|
|
129
129
|
- `BEND` — with `--bends clamp`, a hinge bent the wrong way is straightened on that frame instead of refusing.
|
|
130
130
|
- `FOLD` — `rigc render --folds` over the new animation: every mesh that folds, and when. A reading, not a gate.
|
|
131
|
-
- `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)), the read, interpolated and held frames are the measurement, so the tilt is read on all
|
|
131
|
+
- `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.
|
|
132
132
|
- `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)).
|
|
133
133
|
- `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.
|
|
134
134
|
- `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.
|
|
@@ -201,12 +201,12 @@ rig-c's `scalex` multiplies the bone's x axis, and a child with the `normal` inh
|
|
|
201
201
|
|
|
202
202
|
**`--plant-feet`** places the hip so the supporting foot stays where it took support, through the posed (and scaled) legs, instead of moving it by the pose's mid_hip travel:
|
|
203
203
|
|
|
204
|
-
- 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); a leg length is each side's median picture length hip → ankle, and the switch compares against their mean.
|
|
205
|
-
- The new support is held where it is on the frame it takes over, and the hip blends from the old derivation to the new over **3 frames
|
|
204
|
+
- 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.
|
|
205
|
+
- 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.
|
|
206
206
|
- 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** — releases the plant: the hip follows the pose's travel from where it was, and re-plants on landing (blended the same way). The body has to lift: an ankle's picture y drifts with depth, and under one floor for the whole sequence HQ's dancer 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.
|
|
207
|
-
- **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
|
|
207
|
+
- **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.
|
|
208
208
|
- A **step** releases the plant too: while the supporting ankle moves more than **0.03** leg lengths a frame, the other foot takes support if it is planted (not itself released or moving), and otherwise the hip follows the pose's travel. The released ankle may support again once it has moved less than **0.01** leg lengths a frame for **2 frames**. On the dancer the plant held the supporting ankle through a real step (frames 176–181, 579 → 718 px) and threw the hip sideways.
|
|
209
|
-
- **Read from the lower ankle** ([#113](https://github.com/rig-c/pose/issues/113)). 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. 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–
|
|
209
|
+
- **Read from the lower ankle** ([#113](https://github.com/rig-c/pose/issues/113)). 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. 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 have no supporting foot and the grounded one supports from 12, when it is still again.
|
|
210
210
|
- **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, and lands on the floor, held the same way over its first 3 planted frames. `--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.
|
|
211
211
|
- `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`).
|
|
212
212
|
- **`--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)). 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. 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 0.07 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 with `--stance 0.07` and HQ's flags (`--smooth 7 --flip 90 --short 0.35 --bends clamp --leg-scale 0.8,1.05 --plant-feet --carry-idle`): on HQ's dancer, 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 a second sequence (Toca, 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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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
|
@@ -780,5 +780,12 @@ export function unheldText(h: { speed: number; reach: number; minSeconds?: numbe
|
|
|
780
780
|
export function inferredSource(r: CleanRecord | undefined): string {
|
|
781
781
|
const h = r?.params.hipsInferred;
|
|
782
782
|
if (h === undefined) return 'source not stated';
|
|
783
|
-
|
|
783
|
+
if (h.by === 'declared') return 'declared at clean';
|
|
784
|
+
const spans = (rs: Array<[number, number]>) => rs.map(([a, b]) => (a === b ? `${a}` : `${a}–${b}`)).join(', ');
|
|
785
|
+
const scaled = h.scaled !== undefined && h.scaled.length > 0 ? `, scaled ${spans(h.scaled)}${h.fit?.gain !== undefined && h.fit.gain !== null && h.fit.corr !== null ? ` (gain ${h.fit.gain.toFixed(2)}×, r=${h.fit.corr.toFixed(3)})` : ''}` : '';
|
|
786
|
+
const unmeasured =
|
|
787
|
+
h.unmeasured !== undefined && (h.unmeasured.length > 0 || (h.fit !== undefined && (h.fit.corr === null || h.fit.corr < 0.9)))
|
|
788
|
+
? `, unmeasured ${h.unmeasured.length === 0 ? 'none' : spans(h.unmeasured)}${h.fit !== undefined ? ` (r=${h.fit.corr === null ? 'none' : h.fit.corr.toFixed(3)} < 0.9)` : ''}`
|
|
789
|
+
: '';
|
|
790
|
+
return `measured by ${h.method}${scaled}${unmeasured}`;
|
|
784
791
|
}
|
package/src/legs.ts
CHANGED
|
@@ -24,11 +24,13 @@
|
|
|
24
24
|
* **Planted feet.** The hip is no longer the pose's mid_hip travel scaled. On
|
|
25
25
|
* every frame one foot supports: the one whose reference ankle is lower in the
|
|
26
26
|
* picture, changing only when the other ankle has been ≥ `PLANT_SWITCH` leg
|
|
27
|
-
* lengths lower for ≥ `PLANT_SWITCH_SECONDS
|
|
27
|
+
* lengths lower for ≥ `PLANT_SWITCH_SECONDS`, and taking support from the frame
|
|
28
|
+
* it crossed that margin once confirmed (rig-c/pose#102). The supporting foot is held where
|
|
28
29
|
* it was on the frame it took support, moved in y onto the one floor every span
|
|
29
30
|
* shares (rig-c/pose#85, `plantHip`), and the hip is placed so it stays there,
|
|
30
31
|
* through the posed (scaled) legs. A change of support blends the two
|
|
31
|
-
* derivations over `PLANT_BLEND` frames
|
|
32
|
+
* derivations in y over `PLANT_BLEND` frames while taking the new derivation's x
|
|
33
|
+
* immediately (rig-c/pose#102). A jump — both ankles and mid_hip more
|
|
32
34
|
* than `JUMP_RISE` leg lengths above their rolling floor (the 95th-percentile
|
|
33
35
|
* picture y over ± `FLOOR_SECONDS`), for ≥ `JUMP_FRAMES` frames — releases the
|
|
34
36
|
* plant: the hip follows the pose's travel from where it was, and re-plants on
|
|
@@ -435,6 +437,7 @@ function readSupports(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, read
|
|
|
435
437
|
const open: Partial<Record<Side, Step>> = {};
|
|
436
438
|
let cur: Side | null = null;
|
|
437
439
|
let lowerFrom = -1;
|
|
440
|
+
let freeFrom = -1;
|
|
438
441
|
const released: number[] = [];
|
|
439
442
|
const lower = (cands: Side[], i: number): Side => (cands.length === 2 ? (ankle.l[i]![1] > ankle.r[i]![1] ? 'l' : 'r') : cands[0]!);
|
|
440
443
|
for (let i = 0; i < n; i++) {
|
|
@@ -450,6 +453,7 @@ function readSupports(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, read
|
|
|
450
453
|
if (jump[i]) {
|
|
451
454
|
cur = null;
|
|
452
455
|
lowerFrom = -1;
|
|
456
|
+
freeFrom = -1;
|
|
453
457
|
continue;
|
|
454
458
|
}
|
|
455
459
|
const free = (s: Side) => open[s] === undefined && !moving(s, i);
|
|
@@ -465,28 +469,41 @@ function readSupports(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, read
|
|
|
465
469
|
if (to !== null) changes.push({ frame: i, from: cur, to });
|
|
466
470
|
cur = to;
|
|
467
471
|
lowerFrom = -1;
|
|
472
|
+
freeFrom = -1;
|
|
468
473
|
} else if (cur === null) {
|
|
469
474
|
const cands = SIDES.filter(may);
|
|
470
475
|
if (cands.length > 0) {
|
|
471
476
|
cur = lower(cands, i);
|
|
472
477
|
changes.push({ frame: i, from: null, to: cur });
|
|
473
478
|
lowerFrom = -1;
|
|
479
|
+
freeFrom = -1;
|
|
474
480
|
}
|
|
475
481
|
} else {
|
|
476
482
|
const other: Side = cur === 'r' ? 'l' : 'r';
|
|
477
483
|
if ((free(other) || lowerAnkle) && higher(cur)) {
|
|
478
484
|
if (lowerFrom < 0) lowerFrom = i;
|
|
479
|
-
if (
|
|
480
|
-
if (
|
|
481
|
-
|
|
485
|
+
if (free(other)) {
|
|
486
|
+
if (freeFrom < 0) freeFrom = i;
|
|
487
|
+
// #102: once confirmed over PLANT_SWITCH_SECONDS, the incoming foot takes support from the landing frame it crossed the margin on.
|
|
488
|
+
if (secondsOf([freeFrom, i]) >= PLANT_SWITCH_SECONDS - 1e-9) {
|
|
489
|
+
for (let k = freeFrom; k < i; k++) support[k] = other;
|
|
490
|
+
changes.push({ frame: freeFrom, from: cur, to: other });
|
|
482
491
|
cur = other;
|
|
483
|
-
|
|
492
|
+
lowerFrom = -1;
|
|
493
|
+
freeFrom = -1;
|
|
494
|
+
}
|
|
495
|
+
} else {
|
|
496
|
+
freeFrom = -1;
|
|
497
|
+
if (secondsOf([lowerFrom, i]) >= PLANT_SWITCH_SECONDS - 1e-9) {
|
|
484
498
|
cur = null; // #113: the lower foot may not take support (released, or moving); neither foot supports
|
|
485
499
|
released.push(i);
|
|
500
|
+
lowerFrom = -1;
|
|
486
501
|
}
|
|
487
|
-
lowerFrom = -1;
|
|
488
502
|
}
|
|
489
|
-
} else
|
|
503
|
+
} else {
|
|
504
|
+
lowerFrom = -1;
|
|
505
|
+
freeFrom = -1;
|
|
506
|
+
}
|
|
490
507
|
}
|
|
491
508
|
support[i] = cur;
|
|
492
509
|
}
|
|
@@ -528,8 +545,9 @@ export interface Floor {
|
|
|
528
545
|
* floor is `floor` when given (`--floor`, world units), and otherwise the median
|
|
529
546
|
* of the spans' planted heights; each span's anchor moves in y by floor − its
|
|
530
547
|
* planted height, and the hip with it, so everything above follows. A change of
|
|
531
|
-
* support still blends the two derivations over `PLANT_BLEND` frames, now
|
|
532
|
-
* between two spans on the floor
|
|
548
|
+
* support still blends the two derivations' y over `PLANT_BLEND` frames, now
|
|
549
|
+
* between two spans on the floor, while holding the planted foot's x from the
|
|
550
|
+
* frame it lands on (#102); a jump follows the pose's travel from the hip
|
|
533
551
|
* it left (on the floor) and lands on the floor. On a blend frame the hip is
|
|
534
552
|
* between the two derivations, so the supporting ankle is off its anchor:
|
|
535
553
|
* `blends` lists those frames with the anchor's y, and `holdBlend` stretches
|
|
@@ -634,7 +652,8 @@ function placeHip(feet: Feet, offset: (i: number, s: Side) => Pt, travel: (i: nu
|
|
|
634
652
|
if (from !== null && i - blendAt < PLANT_BLEND) {
|
|
635
653
|
const w = (i - blendAt + 1) / (PLANT_BLEND + 1);
|
|
636
654
|
const f = of(from, i);
|
|
637
|
-
|
|
655
|
+
// #102: blend y only; x takes the new derivation immediately so the planted foot does not slide across the blend.
|
|
656
|
+
hip.push([to[0], f[1] + (to[1] - f[1]) * w]);
|
|
638
657
|
const m = mode!;
|
|
639
658
|
if (m.kind === 'plant') blends.push({ frame: i, side: m.side, y: m.anchor[1] });
|
|
640
659
|
} else {
|
package/src/pose.ts
CHANGED
|
@@ -74,36 +74,63 @@ export interface CleanSpike {
|
|
|
74
74
|
* pelvis line off the picture (`tools/pelvis.py`, after clean) and turned the
|
|
75
75
|
* hip pair to a measured angle on every frame. The exact shape:
|
|
76
76
|
*
|
|
77
|
-
* { by: "measured", method: string,
|
|
78
|
-
* read: Run[], interpolated: Run[], held: Run[],
|
|
77
|
+
* { by: "measured", method: string, fit?: { frames, gain, corr },
|
|
78
|
+
* read: Run[], interpolated: Run[], scaled?: Run[], held: Run[], unmeasured?: Run[],
|
|
79
79
|
* declined: Array<{ frames: Run, why: string }> } Run = [first, last]
|
|
80
80
|
*
|
|
81
81
|
* `method` names the reading. Runs are integer frame indices inside the
|
|
82
82
|
* sequence, first ≤ last, increasing and not overlapping within each list.
|
|
83
83
|
* Every frame of the sequence is in exactly one of `read` (the frame's own
|
|
84
84
|
* reading), `interpolated` (a declined frame between two reads close in time,
|
|
85
|
-
* the angle interpolated)
|
|
86
|
-
*
|
|
87
|
-
*
|
|
85
|
+
* the angle interpolated), `scaled` (#106: a longer declined run taking the
|
|
86
|
+
* detector hip line scaled by `fit.gain`), `held` (the earlier read carried to
|
|
87
|
+
* the end of its run; at the clip's start, the later read) and `unmeasured`
|
|
88
|
+
* (#106: a longer declined run when `fit.corr` < 0.9). `declined` lists, with
|
|
89
|
+
* the reason, exactly the non-read frames. Anything else is refused by name
|
|
90
|
+
* (POSE_CLEANED).
|
|
88
91
|
*/
|
|
89
92
|
export type Run = [number, number];
|
|
93
|
+
export interface PelvisFit {
|
|
94
|
+
frames: number;
|
|
95
|
+
gain: number | null;
|
|
96
|
+
corr: number | null;
|
|
97
|
+
}
|
|
90
98
|
export type HipsInferred =
|
|
91
99
|
| { by: 'declared' }
|
|
92
|
-
| {
|
|
100
|
+
| {
|
|
101
|
+
by: 'measured';
|
|
102
|
+
method: string;
|
|
103
|
+
fit?: PelvisFit;
|
|
104
|
+
read: Run[];
|
|
105
|
+
interpolated: Run[];
|
|
106
|
+
scaled?: Run[];
|
|
107
|
+
held: Run[];
|
|
108
|
+
unmeasured?: Run[];
|
|
109
|
+
declined: Array<{ frames: Run; why: string }>;
|
|
110
|
+
};
|
|
93
111
|
|
|
94
112
|
/** A copy of the record, in the writer's key order. */
|
|
95
113
|
export function copyHipsInferred(h: HipsInferred): HipsInferred {
|
|
96
114
|
const runs = (rs: Run[]): Run[] => rs.map(([a, b]) => [a, b]);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
115
|
+
if (h.by === 'declared') return { by: 'declared' };
|
|
116
|
+
return {
|
|
117
|
+
by: 'measured',
|
|
118
|
+
method: h.method,
|
|
119
|
+
...(h.fit !== undefined ? { fit: { frames: h.fit.frames, gain: h.fit.gain, corr: h.fit.corr } } : {}),
|
|
120
|
+
read: runs(h.read),
|
|
121
|
+
interpolated: runs(h.interpolated),
|
|
122
|
+
...(h.scaled !== undefined ? { scaled: runs(h.scaled) } : {}),
|
|
123
|
+
held: runs(h.held),
|
|
124
|
+
...(h.unmeasured !== undefined ? { unmeasured: runs(h.unmeasured) } : {}),
|
|
125
|
+
declined: h.declined.map((d) => ({ frames: [d.frames[0], d.frames[1]], why: d.why })),
|
|
126
|
+
};
|
|
100
127
|
}
|
|
101
128
|
|
|
102
|
-
/** The frames a `measured` record gives an angle on (read, interpolated, held), as a set; null for any other record or none. */
|
|
129
|
+
/** The frames a `measured` record gives an angle on (read, interpolated, scaled, held), as a set; null for any other record or none. */
|
|
103
130
|
export function measuredFrames(h: HipsInferred | undefined): Set<number> | null {
|
|
104
131
|
if (h === undefined || h.by !== 'measured') return null;
|
|
105
132
|
const out = new Set<number>();
|
|
106
|
-
for (const rs of [h.read, h.interpolated, h.held]) for (const [a, b] of rs) for (let f = a; f <= b; f++) out.add(f);
|
|
133
|
+
for (const rs of [h.read, h.interpolated, h.scaled ?? [], h.held]) for (const [a, b] of rs) for (let f = a; f <= b; f++) out.add(f);
|
|
107
134
|
return out;
|
|
108
135
|
}
|
|
109
136
|
|
|
@@ -489,10 +516,22 @@ function readHipsInferred(p: Problems, hw: string, h: unknown, frames: number |
|
|
|
489
516
|
return;
|
|
490
517
|
}
|
|
491
518
|
const before = p.list.length;
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
519
|
+
const REQ_CLASSES = ['read', 'interpolated', 'held'] as const;
|
|
520
|
+
const ALL_CLASSES = ['read', 'interpolated', 'scaled', 'held', 'unmeasured'] as const;
|
|
521
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', hw, h, ['by', 'method', 'fit', ...ALL_CLASSES, 'declined']);
|
|
522
|
+
for (const k of ['method', ...REQ_CLASSES, 'declined']) if (!(k in h)) p.add('POSE_CLEANED', `${hw} has no "${k}"; a measured record is ${measuredShape}`);
|
|
495
523
|
if ('method' in h && !(typeof h.method === 'string' && h.method.trim() !== '')) p.add('POSE_CLEANED', `${hw} "method" is ${typeName(h.method)}; it must be a non-empty string naming the reading`);
|
|
524
|
+
if ('fit' in h) {
|
|
525
|
+
const f = h.fit;
|
|
526
|
+
const fw = `${hw} "fit"`;
|
|
527
|
+
if (!isObject(f)) p.add('POSE_CLEANED', `${fw} is ${typeName(f)}; it must be { frames, gain, corr }`);
|
|
528
|
+
else {
|
|
529
|
+
refuseUnknownKeys(p, 'POSE_CLEANED', fw, f, ['frames', 'gain', 'corr']);
|
|
530
|
+
if (!(Number.isInteger(f.frames) && (f.frames as number) >= 0)) p.add('POSE_CLEANED', `${fw} "frames" is ${typeName(f.frames)}; it must be a whole number of frames ≥ 0`);
|
|
531
|
+
if (!(f.gain === null || (typeof f.gain === 'number' && Number.isFinite(f.gain)))) p.add('POSE_CLEANED', `${fw} "gain" is ${typeName(f.gain)}; it must be a finite number or null`);
|
|
532
|
+
if (!(f.corr === null || (typeof f.corr === 'number' && Number.isFinite(f.corr) && f.corr >= -1 && f.corr <= 1))) p.add('POSE_CLEANED', `${fw} "corr" is ${typeName(f.corr)}; it must be a correlation in [-1, 1] or null`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
496
535
|
const range = `[0, ${frames === null ? 'the frame count' : frames - 1}]`;
|
|
497
536
|
/** A run [first, last], or null after naming what is wrong with it. */
|
|
498
537
|
const run = (v: unknown, rw: string): Run | null => {
|
|
@@ -524,8 +563,8 @@ function readHipsInferred(p: Problems, hw: string, h: unknown, frames: number |
|
|
|
524
563
|
if (cur.r[0] <= prev.r[1]) p.add('POSE_CLEANED', `${cur.w} [${cur.r[0]}, ${cur.r[1]}] starts at or before the end of ${prev.w} [${prev.r[0]}, ${prev.r[1]}]; ${what} runs must increase and not overlap`);
|
|
525
564
|
}
|
|
526
565
|
};
|
|
527
|
-
const classes: Record<(typeof
|
|
528
|
-
for (const c of
|
|
566
|
+
const classes: Record<(typeof ALL_CLASSES)[number], Named[]> = { read: [], interpolated: [], scaled: [], held: [], unmeasured: [] };
|
|
567
|
+
for (const c of ALL_CLASSES) {
|
|
529
568
|
if (!(c in h)) continue;
|
|
530
569
|
const list = h[c];
|
|
531
570
|
if (!Array.isArray(list)) {
|
|
@@ -560,12 +599,15 @@ function readHipsInferred(p: Problems, hw: string, h: unknown, frames: number |
|
|
|
560
599
|
});
|
|
561
600
|
ordered(declined, 'declined');
|
|
562
601
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
602
|
+
const has106 = 'scaled' in h || 'unmeasured' in h || 'fit' in h;
|
|
603
|
+
const classListLabel = has106 ? 'read, interpolated, scaled, held and unmeasured' : 'read, interpolated and held';
|
|
604
|
+
const filledLabel = has106 ? 'interpolated, scaled, held or unmeasured' : 'interpolated or held';
|
|
605
|
+
// Across lists: the classes are disjoint, they cover every frame, and declined is exactly the non-read classes.
|
|
606
|
+
for (let x = 0; x < ALL_CLASSES.length; x++) {
|
|
607
|
+
for (let y = x + 1; y < ALL_CLASSES.length; y++) {
|
|
608
|
+
for (const a of classes[ALL_CLASSES[x]!]) {
|
|
609
|
+
for (const b of classes[ALL_CLASSES[y]!]) {
|
|
610
|
+
if (a.r[0] <= b.r[1] && b.r[0] <= a.r[1]) p.add('POSE_CLEANED', `${a.w} [${a.r[0]}, ${a.r[1]}] and ${b.w} [${b.r[0]}, ${b.r[1]}] share frame(s) ${Math.max(a.r[0], b.r[0])}–${Math.min(a.r[1], b.r[1])}; a frame is one of ${classListLabel}`);
|
|
569
611
|
}
|
|
570
612
|
}
|
|
571
613
|
}
|
|
@@ -586,18 +628,18 @@ function readHipsInferred(p: Problems, hw: string, h: unknown, frames: number |
|
|
|
586
628
|
return out.map(([a, b]) => (a === b ? `${a}` : `${a}–${b}`)).join(', ');
|
|
587
629
|
};
|
|
588
630
|
const read = set(classes.read);
|
|
589
|
-
const
|
|
631
|
+
const nonRead = set([...classes.interpolated, ...classes.scaled, ...classes.held, ...classes.unmeasured]);
|
|
590
632
|
const dec = set(declined);
|
|
591
633
|
if (frames !== null) {
|
|
592
|
-
const none = Array.from({ length: frames }, (_, f) => f).filter((f) => !read.has(f) && !
|
|
593
|
-
if (none.length > 0) p.add('POSE_CLEANED', `${hw}: frame(s) ${spans(none)} are in none of
|
|
634
|
+
const none = Array.from({ length: frames }, (_, f) => f).filter((f) => !read.has(f) && !nonRead.has(f));
|
|
635
|
+
if (none.length > 0) p.add('POSE_CLEANED', `${hw}: frame(s) ${spans(none)} are in none of ${classListLabel}; every frame of the sequence is one of them`);
|
|
594
636
|
}
|
|
595
637
|
const both = [...dec].filter((f) => read.has(f)).sort((a, b) => a - b);
|
|
596
638
|
if (both.length > 0) p.add('POSE_CLEANED', `${hw}: frame(s) ${spans(both)} are both read and declined; a frame is read or declined, not both`);
|
|
597
|
-
const unfilled = [...dec].filter((f) => !read.has(f) && !
|
|
598
|
-
if (unfilled.length > 0) p.add('POSE_CLEANED', `${hw}: frame(s) ${spans(unfilled)} are declined but neither
|
|
599
|
-
const undeclared = [...
|
|
600
|
-
if (undeclared.length > 0) p.add('POSE_CLEANED', `${hw}: frame(s) ${spans(undeclared)} are
|
|
639
|
+
const unfilled = [...dec].filter((f) => !read.has(f) && !nonRead.has(f)).sort((a, b) => a - b);
|
|
640
|
+
if (unfilled.length > 0) p.add('POSE_CLEANED', `${hw}: frame(s) ${spans(unfilled)} are declined but neither ${filledLabel}; a declined frame takes one of them`);
|
|
641
|
+
const undeclared = [...nonRead].filter((f) => !dec.has(f)).sort((a, b) => a - b);
|
|
642
|
+
if (undeclared.length > 0) p.add('POSE_CLEANED', `${hw}: frame(s) ${spans(undeclared)} are ${filledLabel} but not declined; only a frame the reading declined is filled, and "declined" says why`);
|
|
601
643
|
}
|
|
602
644
|
const holdParamsOf = (h: HoldParams | 'off'): HoldParams | 'off' => (h === 'off' ? 'off' : { speed: h.speed, reach: h.reach, minSeconds: h.minSeconds, blend: h.blend });
|
|
603
645
|
const copyHold = (h: CleanHold): CleanHold => ({ joint: h.joint, frames: [h.frames[0], h.frames[1]], jitter: { before: h.jitter.before, after: h.jitter.after } });
|
package/src/verify.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { poseRawAnimation, poseRawSetup, type CompiledDocument, type RawBone } from 'rig-c/core';
|
|
12
12
|
import type { Body18 } from './joints.ts';
|
|
13
13
|
import { aimAngle, pointAt, type JointMap } from './map.ts';
|
|
14
|
-
import type { HipsInferred, Joint, Run } from './pose.ts';
|
|
14
|
+
import type { HipsInferred, Joint, PelvisFit, Run } from './pose.ts';
|
|
15
15
|
import type { Retarget } from './retarget.ts';
|
|
16
16
|
import { pictureAngle, pointOn, wrap } from './retarget.ts';
|
|
17
17
|
|
|
@@ -260,13 +260,25 @@ export interface Posture {
|
|
|
260
260
|
*/
|
|
261
261
|
hipsInferred?: { frames: number; source: string };
|
|
262
262
|
/**
|
|
263
|
-
* Present only when the hips were measured (#86, `hipsInferred.by` is
|
|
263
|
+
* Present only when the hips were measured (#86, #106, `hipsInferred.by` is
|
|
264
264
|
* `measured`): the method, how many frames the tilt was read on — the
|
|
265
|
-
* record's read, interpolated and held frames with both hips good,
|
|
266
|
-
* class counted — and the record's
|
|
267
|
-
* inferred hip is left out
|
|
265
|
+
* record's read, interpolated, scaled and held frames with both hips good,
|
|
266
|
+
* each class counted — and the record's spans. Any `unmeasured` frame (corr
|
|
267
|
+
* < 0.9) or other frame with an inferred hip is left out of `tilt`.
|
|
268
268
|
*/
|
|
269
|
-
tiltMeasured?: {
|
|
269
|
+
tiltMeasured?: {
|
|
270
|
+
method: string;
|
|
271
|
+
frames: number;
|
|
272
|
+
read: number;
|
|
273
|
+
interpolated: number;
|
|
274
|
+
held: number;
|
|
275
|
+
heldSpans: Run[];
|
|
276
|
+
scaled?: number;
|
|
277
|
+
scaledSpans?: Run[];
|
|
278
|
+
unmeasured?: number;
|
|
279
|
+
unmeasuredSpans?: Run[];
|
|
280
|
+
fit?: PelvisFit;
|
|
281
|
+
};
|
|
270
282
|
/**
|
|
271
283
|
* Sway: the x-range of mid_hip − mid_shoulder, in rig units (the reference's
|
|
272
284
|
* picture px × the figure scale) and in each body's own shoulder widths.
|
|
@@ -311,11 +323,12 @@ const medianOf = (xs: number[]): number => {
|
|
|
311
323
|
* and the line counts the frames left out. Sway is read as before, inferred
|
|
312
324
|
* hips included: it is a different measure, and the card leaves it as it is.
|
|
313
325
|
*
|
|
314
|
-
* Measured hips (#86, `measured`): the hips are inferred on every frame,
|
|
315
|
-
* turned to a measured angle: read off the picture, interpolated between two
|
|
316
|
-
* close reads,
|
|
317
|
-
*
|
|
318
|
-
*
|
|
326
|
+
* Measured hips (#86, #106, `measured`): the hips are inferred on every frame,
|
|
327
|
+
* and turned to a measured angle: read off the picture, interpolated between two
|
|
328
|
+
* close reads, scaled from the detector's hip line when `fit.corr >= 0.9`, or
|
|
329
|
+
* held from the earlier read. Those four are the measurement, so the tilt is
|
|
330
|
+
* read on them; any `unmeasured` frame (`fit.corr < 0.9`) or other frame with
|
|
331
|
+
* an inferred hip is left out and counted.
|
|
319
332
|
*/
|
|
320
333
|
export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: ReadonlyMap<string, number>, map: JointMap, r: Retarget, frames: ReadonlyArray<Partial<Record<Body18, Joint>>>, minScore: number, inferredSource = 'source not stated', measured: Extract<HipsInferred, { by: 'measured' }> | null = null): Posture | null {
|
|
321
334
|
const pelvis = map.chains.find((c) => c.joints.length === 2 && c.joints[0] === 'r_hip' && c.joints[1] === 'l_hip');
|
|
@@ -330,16 +343,23 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
|
|
|
330
343
|
const refSw: number[] = [];
|
|
331
344
|
let inferred = 0;
|
|
332
345
|
const inClass = (rs: Run[], i: number) => rs.some(([a, b]) => a <= i && i <= b);
|
|
333
|
-
const counts = { read: 0, interpolated: 0, held: 0 };
|
|
346
|
+
const counts = { read: 0, interpolated: 0, held: 0, scaled: 0, unmeasured: 0 };
|
|
334
347
|
frames.forEach((f, i) => {
|
|
335
348
|
const rh = at(i, 'r_hip');
|
|
336
349
|
const lh = at(i, 'l_hip');
|
|
337
350
|
if (rh === null || lh === null) return;
|
|
338
351
|
const isInferred = f.r_hip?.state === 'inferred' || f.l_hip?.state === 'inferred';
|
|
339
352
|
const cls = measured === null ? undefined : (['read', 'interpolated', 'held'] as const).find((c) => inClass(measured[c], i));
|
|
353
|
+
const isScaled = measured !== null && measured.scaled !== undefined && inClass(measured.scaled, i);
|
|
354
|
+
const isUnmeasured = measured !== null && measured.unmeasured !== undefined && inClass(measured.unmeasured, i);
|
|
340
355
|
if (isInferred && cls !== undefined) {
|
|
341
356
|
counts[cls]++;
|
|
342
357
|
refTilt.push(pictureAngle(rh, lh));
|
|
358
|
+
} else if (isInferred && isScaled) {
|
|
359
|
+
counts.scaled++;
|
|
360
|
+
refTilt.push(pictureAngle(rh, lh));
|
|
361
|
+
} else if (isInferred && isUnmeasured) {
|
|
362
|
+
counts.unmeasured++;
|
|
343
363
|
} else if (isInferred) inferred++;
|
|
344
364
|
else refTilt.push(pictureAngle(rh, lh));
|
|
345
365
|
const rs = at(i, 'r_shoulder');
|
|
@@ -352,7 +372,7 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
|
|
|
352
372
|
const aim = pelvis.aim[0]!;
|
|
353
373
|
let tilt: Posture['tilt'] = null;
|
|
354
374
|
const why = `hips inferred (${inferredSource})`;
|
|
355
|
-
if (inferred === 0 || refTilt.length > 0) {
|
|
375
|
+
if ((inferred === 0 && counts.unmeasured === 0) || refTilt.length > 0) {
|
|
356
376
|
const rigTilt = posed.map((b) => aimAngle(b, lengths, bone, aim));
|
|
357
377
|
tilt = { reference: rangeOf(unwrapped(refTilt)), rig: rangeOf(unwrapped(rigTilt)), ratio: 0 };
|
|
358
378
|
tilt.ratio = tilt.rig.range / tilt.reference.range;
|
|
@@ -387,6 +407,7 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
|
|
|
387
407
|
ratioSw: rig === null || rig.rangeSw === null ? null : rig.rangeSw / reference.rangeSw,
|
|
388
408
|
};
|
|
389
409
|
}
|
|
410
|
+
const has106 = measured !== null && (measured.scaled !== undefined || measured.unmeasured !== undefined || measured.fit !== undefined);
|
|
390
411
|
return {
|
|
391
412
|
pelvis: pelvis.name,
|
|
392
413
|
rigHip: r.origin.bone,
|
|
@@ -395,7 +416,27 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
|
|
|
395
416
|
tilt,
|
|
396
417
|
...(tilt === null ? { tiltWhy: why } : {}),
|
|
397
418
|
...(inferred > 0 ? { hipsInferred: { frames: inferred, source: inferredSource } } : {}),
|
|
398
|
-
...(measured !== null
|
|
419
|
+
...(measured !== null
|
|
420
|
+
? {
|
|
421
|
+
tiltMeasured: {
|
|
422
|
+
method: measured.method,
|
|
423
|
+
frames: counts.read + counts.interpolated + counts.scaled + counts.held,
|
|
424
|
+
read: counts.read,
|
|
425
|
+
interpolated: counts.interpolated,
|
|
426
|
+
held: counts.held,
|
|
427
|
+
heldSpans: measured.held.map(([a, b]): Run => [a, b]),
|
|
428
|
+
...(has106
|
|
429
|
+
? {
|
|
430
|
+
scaled: counts.scaled,
|
|
431
|
+
scaledSpans: (measured.scaled ?? []).map(([a, b]): Run => [a, b]),
|
|
432
|
+
unmeasured: counts.unmeasured,
|
|
433
|
+
unmeasuredSpans: (measured.unmeasured ?? []).map(([a, b]): Run => [a, b]),
|
|
434
|
+
...(measured.fit !== undefined ? { fit: { frames: measured.fit.frames, gain: measured.fit.gain, corr: measured.fit.corr } } : {}),
|
|
435
|
+
}
|
|
436
|
+
: {}),
|
|
437
|
+
},
|
|
438
|
+
}
|
|
439
|
+
: {}),
|
|
399
440
|
sway,
|
|
400
441
|
};
|
|
401
442
|
}
|
|
@@ -403,14 +444,23 @@ export function measurePosture(posed: Array<Map<string, RawBone>>, lengths: Read
|
|
|
403
444
|
const f1 = (x: number) => x.toFixed(1);
|
|
404
445
|
const f3 = (x: number) => x.toFixed(3);
|
|
405
446
|
const sg = (x: number) => `${x >= 0 ? '+' : '−'}${Math.abs(x).toFixed(1)}`;
|
|
447
|
+
const spansText = (rs: Run[] | undefined) => (rs === undefined || rs.length === 0 ? 'none' : rs.map(([a, b]) => (a === b ? `${a}` : `${a}–${b}`)).join(', '));
|
|
406
448
|
|
|
407
449
|
/** The POSTURE lines. */
|
|
408
450
|
export function postureLines(q: Posture): string[] {
|
|
409
451
|
const over = (k: number) => (k === q.frames.of ? '' : ` over ${k} of ${q.frames.of} frames`);
|
|
410
452
|
const t = q.tilt;
|
|
411
453
|
const m = q.tiltMeasured;
|
|
412
|
-
const heldSpans = m
|
|
413
|
-
const
|
|
454
|
+
const heldSpans = spansText(m?.heldSpans);
|
|
455
|
+
const scaledPart =
|
|
456
|
+
m?.scaled !== undefined && m.scaled > 0
|
|
457
|
+
? `, ${m.scaled} scaled (scaled: ${spansText(m.scaledSpans)}${m.fit?.gain !== undefined && m.fit.gain !== null && m.fit.corr !== null ? `, gain ${m.fit.gain.toFixed(2)}×, r=${m.fit.corr.toFixed(3)}` : ''})`
|
|
458
|
+
: '';
|
|
459
|
+
const unmeasuredPart =
|
|
460
|
+
m?.unmeasured !== undefined && (m.unmeasured > 0 || (m.fit !== undefined && (m.fit.corr === null || m.fit.corr < 0.9)))
|
|
461
|
+
? `; ${m.unmeasured} unmeasured (${spansText(m.unmeasuredSpans)}${m.fit !== undefined ? `, r=${m.fit.corr === null ? 'none' : m.fit.corr.toFixed(3)} < 0.9` : ''})`
|
|
462
|
+
: '';
|
|
463
|
+
const measured = m === undefined ? '' : `; tilt measured (${m.method}) on ${m.frames} frame(s): ${m.read} read, ${m.interpolated} interpolated${scaledPart}, ${m.held} held (held: ${heldSpans})${unmeasuredPart}`;
|
|
414
464
|
const left = q.hipsInferred === undefined ? '' : `; ${q.hipsInferred.frames} frame(s) with an inferred hip not read (${q.hipsInferred.source})`;
|
|
415
465
|
const out = [
|
|
416
466
|
t === null
|