@rig-c/pose 0.19.0 → 0.20.1
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 +11 -11
- package/cli.ts +1 -1
- package/package.json +1 -1
- package/src/clean.ts +107 -20
- package/src/legs.ts +252 -35
- package/src/pose.ts +11 -4
- package/src/retarget.ts +206 -13
- package/src/run.ts +15 -3
- package/src/switch.ts +33 -5
- package/src/verify.ts +155 -1
package/README.md
CHANGED
|
@@ -53,15 +53,15 @@ The output carries the record, here the dancer's from the table below:
|
|
|
53
53
|
|
|
54
54
|
**`--hips-inferred`** ([#63](https://github.com/rig-c/pose/issues/63)) declares that the hips are not seen: under a skirt or a cloak the pose model still places them, but where it puts them is its guess. After the steps, `r_hip` and `l_hip` are marked `inferred` on every frame where they have a position (`INFERRED`), and the record's params carry `"hipsInferred": { "by": "declared" }` — the person running `clean` said so. Positions and scores are unchanged, so `retarget`'s keys are the same with the flag and without it. What changes is the `POSTURE` line, which does not read a hip tilt off inferred hips (below), and the `CLEAN` line, which ends `hips inferred (declared at clean)`.
|
|
55
55
|
|
|
56
|
-
**The stillness hold** ([#91](https://github.com/rig-c/pose/issues/91)) runs on the cleaned positions, after the smoothing and the floor, **joint by joint
|
|
56
|
+
**The stillness hold** ([#91](https://github.com/rig-c/pose/issues/91), [#108](https://github.com/rig-c/pose/issues/108), [#109](https://github.com/rig-c/pose/issues/109)) runs on the cleaned positions, after the smoothing and the floor, **joint by joint** — except the five **face** joints (`nose`, `r_eye`, `l_eye`, `r_ear`, `l_ear`), which are held as one group (all of them or none, over the span where all five pass, [#109](https://github.com/rig-c/pose/issues/109)), so a piecewise hold does not turn the head. Stillness is a property of a joint (or of the face as a whole), so a hand that has stopped is held while the elbow beside it still settles. A joint's speed on a frame is its movement from the frame before, in trunk lengths, taken as a 5-frame median. A **hold** of a joint is a span of at least 0.5 s (the first frame's time to the end of the last) on every frame of which its median speed is under `--hold-speed` (default 0.015 trunk lengths a frame), and over which it stays within `--hold-reach` (default 0.03 trunk lengths) of its own span median. Speed alone cannot tell a slow drift from stillness, because a drift accumulates. For each joint (and for the face group together) the span is chosen greedily, left to right: from a still frame it grows one frame at a time while the reach holds on the grown span, its median read again each time, and stops at the first frame that breaks it. If what it reached lasts 0.5 s it is a hold and the search resumes after it; if not, the search moves one frame on. A still run not held by that greedy pass that lasts at least 0.5 s and fails the reach only on one interior run of at most 6 frames stays held around that **excursion** ([#108](https://github.com/rig-c/pose/issues/108)), provided the held frames themselves last at least 0.5 s and stay within the reach of their own median: the excursion is kept (blended from its cleaned positions toward the span median over the 6 frames from either held side), not erased, and the joint is not released for the whole span. A span does not cross a loop's seam.
|
|
57
57
|
|
|
58
|
-
On every frame of a hold the joint takes its per-coordinate median over the span. It blends in linearly over the 6 frames before it (the 6th stays as cleaned, the frame before the span is ⅚ of the way) and out over the 6 frames after it, mirrored. A blend frame inside another hold of the same joint is not touched; where two blends of one joint meet, the larger weight wins. Where one hold of a joint ends on frame k and the next starts on k + 1 + g, with a gap g of under 6 frames (a **seam**: the reach broke while the joint was still moving slowly, or a few frames between two holds failed a test), the two medians differ, and the joint would jump between them. A seam cross-fades over 6 + g frames, symmetric about the gap's centre: frames k − 2 … k + 3 + g take 1/(2(6 + g)), 3/(2(6 + g)), … of the way from the first median to the second (for g = 0: 1/12, 3/12, … 11/12, the blend's sixths placed symmetric about the seam), so no frame moves more than 1/(6 + g) of the step. The cross-fade replaces the blends on the gap's frames. Each seam is printed (`SEAM`) and recorded with the joint, the second hold's first frame, the step in trunk lengths and the gap. A hold that carries a cross-fade has a non-zero jitter after: the ramp's own curvature, printed as `(ramp)`, not noise. A joint that fails keeps its cleaned track. A joint with no position on a frame gets none, and states and scores are kept.
|
|
58
|
+
On every held frame of a hold the joint takes its per-coordinate median over the span. It blends in linearly over the 6 frames before it (the 6th stays as cleaned, the frame before the span is ⅚ of the way) and out over the 6 frames after it, mirrored. A blend frame inside another hold of the same joint is not touched; where two blends of one joint meet, the larger weight wins. Where one hold of a joint ends on frame k and the next starts on k + 1 + g, with a gap g of under 6 frames (a **seam**: the reach broke while the joint was still moving slowly, or a few frames between two holds failed a test), the two medians differ, and the joint would jump between them. A seam cross-fades over 6 + g frames, symmetric about the gap's centre: frames k − 2 … k + 3 + g take 1/(2(6 + g)), 3/(2(6 + g)), … of the way from the first median to the second (for g = 0: 1/12, 3/12, … 11/12, the blend's sixths placed symmetric about the seam), so no frame moves more than 1/(6 + g) of the step. The cross-fade replaces the blends on the gap's frames. Each seam is printed (`SEAM`) and recorded with the joint, the second hold's first frame, the step in trunk lengths and the gap. A hold that carries a cross-fade or an excursion has a non-zero jitter after: the ramp's or excursion's own curvature, printed as `(ramp)` or `(excursion)`, not noise. A joint that fails keeps its cleaned track. A joint with no position on a frame gets none, and states and scores are kept.
|
|
59
59
|
|
|
60
|
-
Each hold is printed (`HOLD`, joint by joint, with its largest speed and reach) and recorded with the joint's jitter inside it, before and after. Where some joints are held, every run of the same frames (the held spans joined where they overlap or touch) on which a joint has no hold is named with the test it fails there and its figure (`UNHELD`, and the record's `unheld`): `speed` (its largest median speed), else `reach` (its largest distance from its median over the run), else `short` (it is still, but for under 0.5 s; the figure is its seconds), else `span` (it passes all three, and its spans broke elsewhere).
|
|
60
|
+
Each hold is printed (`HOLD`, joint by joint, with its largest speed and reach, and its `excursion` when one was kept) and recorded with the joint's jitter inside it, before and after. Where some joints are held, every run of the same frames (the held spans joined where they overlap or touch) on which a joint has no hold is named with the test it fails there and its figure (`UNHELD`, and the record's `unheld`): `speed` (its largest median speed), else `reach` (its largest distance from its median over the run), else `short` (it is still, but for under 0.5 s; the figure is its seconds), else `span` (it passes all three, and its spans broke elsewhere).
|
|
61
61
|
|
|
62
62
|
```json
|
|
63
63
|
"params": { "loop": false, "legs": 5, "rest": 7, "hold": { "speed": 0.015, "reach": 0.03, "minSeconds": 0.5, "blend": 6 } },
|
|
64
|
-
"holds": [{ "joint": "nose", "frames": [first, last], "jitter": { "before": before, "after": 0 } }, …],
|
|
64
|
+
"holds": [{ "joint": "nose", "frames": [first, last], "excursion"?: [first, last], "jitter": { "before": before, "after": 0 } }, …],
|
|
65
65
|
"seams": [{ "joint": "neck", "frame": second hold's first frame, "step": trunk lengths, "gap": frames }, …],
|
|
66
66
|
"unheld": [{ "joint": "l_elbow", "frames": [first, last], "test": "reach", "value": figure }, …]
|
|
67
67
|
```
|
|
@@ -107,7 +107,7 @@ A retarget of the cleaned dancer, with `--flip 90 --short 0.35 --bends clamp --l
|
|
|
107
107
|
rig-pose retarget --rig out/rig --map map.json --pose dance.pose.json --out motion.json --name dance
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
For every frame of the pose and every bone the map names, the segment that bone carries (an upper arm: shoulder → elbow) points somewhere in the picture. The bone is turned so its segment points the same way: the picture angle, flipped into rig-c's y-up world, minus the bone's angle at the rig's setup pose (read through rig-c's own poser), less the turn of the mapped bone above it — because a `rotate` key in rig-c adds to the setup rotation and a child turns with its parent. The travel of the pelvis midpoint, scaled by a length measured on both the rig and the pose, moves the hip. The result is one animation in rig-c's motion format (`rigc-motion/1`): a `rotate` track per mapped bone, a `translate` track on the hip, one key per pose frame — and, when the map lists alternate drawings, an `attachment` track per slot they switch (and a `translate` track per bone a set offsets).
|
|
110
|
+
For every frame of the pose and every bone the map names, the segment that bone carries (an upper arm: shoulder → elbow) points somewhere in the picture. The bone is turned so its segment points the same way: the picture angle, flipped into rig-c's y-up world, minus the bone's angle at the rig's setup pose (read through rig-c's own poser), less the turn of the mapped bone above it — because a `rotate` key in rig-c adds to the setup rotation and a child turns with its parent. The travel of the pelvis midpoint, scaled by a length measured on both the rig and the pose, moves the hip. When a head chain carries the eye line (`r_eye → l_eye`), the eyes' vertical travel against `neck` in the reference, scaled into the rig's units, keys a vertical `translate` nod channel on the head bone ([#111](https://github.com/rig-c/pose/issues/111)), leaving its `rotate` (roll) track unchanged. The result is one animation in rig-c's motion format (`rigc-motion/1`): a `rotate` track per mapped bone, a `translate` track on the hip (and on the head when the reference nods), one key per pose frame — and, when the map lists alternate drawings, an `attachment` track per slot they switch (and a `translate` track per bone a set offsets).
|
|
111
111
|
|
|
112
112
|
Nothing is written until every check passes:
|
|
113
113
|
|
|
@@ -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), [#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.
|
|
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. With `--plant-feet`, `POSTURE` also prints one `bounce` line per support span ([#107](https://github.com/rig-c/pose/issues/107), `posture.bounce` in the report): the detrended (centred ±12-frame moving mean removed) vertical standard deviation, rig/reference ratio and correlation for the hip, the neck and `eyes − neck`.
|
|
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.
|
|
@@ -136,7 +136,7 @@ and what the run cannot decide for you it lists:
|
|
|
136
136
|
- `RAISED` — with `--raised-by-head` ([below](#alternate-drawings-alternates)): one line per span of a set that draws an arm raised, per chain. It gives the frames and the median eye distances (the rig's in units, the reference's in px). It gives the wrist's offset from the head centre in eye distances: the median of the reference's, of where the wrist was placed, of the rig's on the pose's angles, and of the rig's as keyed. It gives the worst |rig − reference| before and after, the worst |after − placed|, and the frames whose upper arm the set's `turn` held. `RAISED none` when the flag is given and no such set showed.
|
|
137
137
|
- `BY-HIP` — with a set's `"place": "by-hip"` ([below](#alternate-drawings-alternates)): the `RAISED` line against the hip, one per span of such a set per chain it places. The unit is the hip width, the offset is from the mid-hip. `BY-HIP none` when the map has such a set and none of its spans showed.
|
|
138
138
|
- `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.
|
|
139
|
-
- `LEG-SCALE` — with `--leg-scale` ([below](#the-legs-foreshortening-and-planted-feet)): per leg segment, its straight length, the spread of its picture length over it,
|
|
139
|
+
- `LEG-SCALE` — with `--leg-scale` ([below](#the-legs-foreshortening-and-planted-feet)): per leg segment, its straight length, the spread of its picture length over it, the frames clamped at each limit, 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).
|
|
140
140
|
- `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.
|
|
141
141
|
- `FEET` — when the map has both legs: 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.
|
|
142
142
|
- `STANCE` — with `--stance` ([below](#the-legs-foreshortening-and-planted-feet)): one line per run of frames planted both (with the stretch keyed, its largest k), lifted, or past the tolerance below (the pose's height kept, [#114](https://github.com/rig-c/pose/issues/114)), with the free side, the frames and the tolerance; and one per run whose stretch fell past its bounds (`past the bounds`), with the k the floor needed, the bound it had and how far the foot was left from it; and, where the leg's own range set a lower bound below 0.92, one `bound from the leg's own range` line with each side's lowest.
|
|
@@ -199,15 +199,15 @@ A knee bending toward the camera is seen from the front only as a shorter leg, a
|
|
|
199
199
|
|
|
200
200
|
rig-c's `scalex` multiplies the bone's x axis, and a child with the `normal` inherit mode takes its parent's whole world matrix. So under a thigh scaled by s, a shin at angle φ in the thigh's axes points at atan2(sin φ, s·cos φ), and is itself scaled by |(s·cos φ, sin φ)|. The rotate key `turn − turn above` assumes unit scale. A mapped bone under a scaled bone is therefore keyed through its parent's posed matrix: a `rotate` key k and a `scalex` key s' make its local matrix R(k) · L · diag(s', 1), L its setup local matrix, and k and s' are solved so its world x axis lands on its target at the scale asked. On HQ's dancer the unit-scale formula missed the shins by up to 2.4°, their length by 0.16 of setup and the ankles by 38 units; solved, every bone lands. The child's art is still sheared by its parent's scale, and a bone under it (a foot) is squashed along the shin: `SHEAR` reads both.
|
|
201
201
|
|
|
202
|
-
**`--plant-feet`** places the hip so the supporting foot stays where it took support, through the posed (and
|
|
202
|
+
**`--plant-feet`** places the hip so the supporting foot stays where it took support, through the posed legs at the scale the pose asked for (when a segment asks for a scale below `--leg-scale`'s clamp floor, the clamp does not lift the hip by what it withheld, [#107](https://github.com/rig-c/pose/issues/107); on a supporting leg, the segment's `scalex` follows the asked scale down to the bound #95 derives from the leg's own range so the supporting ankle stays on #85's floor while the free leg keeps the configured clamp, and any frame clamped at #95's bound lifts the hip by the remainder so the ankle never sinks, listed as `LEG-SCALE past the floor (support)`, [#133](https://github.com/rig-c/pose/issues/133)), instead of moving it by the pose's mid_hip travel:
|
|
203
203
|
|
|
204
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
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
|
-
- 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.
|
|
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). 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 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
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–11 before; now frames 10–11
|
|
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
|
|
209
|
+
- **Read from the lower ankle** ([#113](https://github.com/rig-c/pose/issues/113), [#119](https://github.com/rig-c/pose/issues/119)). On a sequence not declared `"in-place"`, a foot whose ankle is at least **0.02 leg lengths** higher in the picture than the other's never takes support: not when a plant starts, and not when a step releases the other foot. Before, a step handed support to the only foot free to take it, even a folded, lifted one, and the hip was then placed under a foot in the air. A supporting foot that the other ankle has been that much lower than for **0.1 s** hands support to it when it may take it, as before; when the lower foot may not (it is released and not yet still, or moving), the supporting foot is released, and the hip follows the pose's travel until a foot may take support. On a frame after a jump or a step's release where the only foot free under #113 is the higher one (`cause: "higher"`), the lower foot supports **tracked, not planted** ([#119](https://github.com/rig-c/pose/issues/119)): the hip puts it on #85's floor and follows its scaled ankle track (`FEET tracked`, `feet.tracked`) until it has been still for 2 frames (`< 0.01`), when the normal plant takes over at that position. Steps and jumps are read as before, and under `"in-place"` #92's reading stands. `FEET support read from the lower ankle` names every run of frames whose support differs from the reading before #113, the side before and after (`none`: no foot supports), and for each `none` why: `released`, the supporting foot let go because the lower ankle could not take support (released by a step, or moving), or the only foot free to take support was the higher one. The report's `feet.lowered` holds them as `{ frames, was, now, cause }` (absent in place). On the tests' synthetic step under a lifted leg (the lifted ankle 0.2 leg lengths up, the grounded one stepping 0.05 leg lengths on frame 10 and still from 11), the lifted leg took support on frames 10–11 before; now frames 10–11 track the lower foot on the floor (`feet.tracked`) and the grounded one is planted from 12, when it is still again.
|
|
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, carried linearly from take-off so the landing frame meets the floor and clamped so no frame inside the flight puts a foot below it ([#103](https://github.com/rig-c/pose/issues/103)), and is held the same way over its first 3 planted frames. `--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.
|
|
213
213
|
- The map needs one chain ending at each ankle whose last bone is aimed `"x"` (the ankle is that bone's tip), and a `translate` the legs hang from (`LEGS_MAP`). A `range`, or a set's `turn` or `offsets`, on a bone between the hip and an ankle is refused (`LEGS_RANGE`): the plant is derived before the range clamp, and a clamp after it would move the planted foot.
|
package/cli.ts
CHANGED
|
@@ -437,7 +437,7 @@ function runClean(argv: string[], say: (l: string) => void): void {
|
|
|
437
437
|
else {
|
|
438
438
|
const h = params.hold;
|
|
439
439
|
const span = (r: [number, number] | null, what: string) => (r === null ? `nothing ${what}` : `${what} ${r[0] === r[1] ? `frame ${r[0]}` : `frames ${r[0]}–${r[1]}`}`);
|
|
440
|
-
for (const x of c.holds) say(` HOLD joint "${x.joint}" frames ${x.frames[0]}–${x.frames[1]} (${tOf(x.frames[0])}–${tOf(x.frames[1])}, ${fmt(x.seconds, 3)} s): ${HOLD_SPEED_WINDOW}-frame median speed under ${h.speed} trunk/frame (largest ${fmt(x.speed, 4)}), within ${h.reach} trunk of its span median (largest ${fmt(x.reach, 4)}); written as that median, blended in over ${span(x.blendIn, 'before').replace('nothing before', 'nothing before it')}, out over ${span(x.blendOut, 'after').replace('nothing after', 'nothing after it')}; jitter inside ${jit(x.jitter.before)} → ${jit(x.jitter.after)} px/f²${x.jitter.after !== null && x.jitter.after > 0 ? '
|
|
440
|
+
for (const x of c.holds) say(` HOLD joint "${x.joint}" frames ${x.frames[0]}–${x.frames[1]} (${tOf(x.frames[0])}–${tOf(x.frames[1])}, ${fmt(x.seconds, 3)} s): ${HOLD_SPEED_WINDOW}-frame median speed under ${h.speed} trunk/frame (largest ${fmt(x.speed, 4)}), ${x.excursion === null ? `within ${h.reach} trunk of its span median (largest ${fmt(x.reach, 4)}); written as that median` : `within ${h.reach} trunk of its span median outside excursion ${x.excursion[0] === x.excursion[1] ? `frame ${x.excursion[0]}` : `frames ${x.excursion[0]}–${x.excursion[1]}`} (reach ${fmt(x.reach, 4)} with excursion); held around the excursion, which is blended in and out`}, blended in over ${span(x.blendIn, 'before').replace('nothing before', 'nothing before it')}, out over ${span(x.blendOut, 'after').replace('nothing after', 'nothing after it')}; jitter inside ${jit(x.jitter.before)} → ${jit(x.jitter.after)} px/f²${x.jitter.after !== null && x.jitter.after > 0 ? ` (${x.excursion !== null ? 'excursion' : 'ramp'})` : ''}`);
|
|
441
441
|
for (const x of c.seams) say(` SEAM joint "${x.joint}" frame ${x.frame} (${tOf(x.frame)}): two holds ${x.gap === 0 ? 'back to back' : `${x.gap} frame(s) apart`}, their medians ${fmt(x.step, 4)} trunk (${fmt(x.step * c.trunk.length)} px) apart; cross-faded over frames ${x.frame - x.gap - HOLD_BLEND / 2}–${x.frame + HOLD_BLEND / 2 - 1}, no frame moving more than 1/${HOLD_BLEND + x.gap} of it`);
|
|
442
442
|
for (const [a, b] of holdRegions(c.holds)) {
|
|
443
443
|
const not = c.unheld.filter((u) => u.frames[0] >= a && u.frames[1] <= b);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.1",
|
|
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
|
@@ -94,6 +94,8 @@ export const HOLD_REACH = 0.03;
|
|
|
94
94
|
export const HOLD_MIN_SECONDS = 0.5;
|
|
95
95
|
/** The frames before a hold over which the positions blend into it. */
|
|
96
96
|
export const HOLD_BLEND = 6;
|
|
97
|
+
/** The longest excursion inside a hold (#108), frames: a run over the reach that is kept while the span around it stays held. */
|
|
98
|
+
export const HOLD_EXCURSION = 6;
|
|
97
99
|
/** The speed's median window, frames. */
|
|
98
100
|
export const HOLD_SPEED_WINDOW = 5;
|
|
99
101
|
|
|
@@ -144,6 +146,8 @@ export interface HoldFound {
|
|
|
144
146
|
joint: Body18;
|
|
145
147
|
/** The span, inclusive. */
|
|
146
148
|
frames: [number, number];
|
|
149
|
+
/** A short run inside the span that left the reach and was kept, not held (#108); null when every frame was held. */
|
|
150
|
+
excursion: [number, number] | null;
|
|
147
151
|
/** First frame's time to the end of the last, seconds. */
|
|
148
152
|
seconds: number;
|
|
149
153
|
/** The joint's largest 5-frame median speed on the span, trunk lengths per frame. */
|
|
@@ -330,8 +334,9 @@ export function jitterOf(all: readonly Series[], loop: boolean): { value: number
|
|
|
330
334
|
|
|
331
335
|
/**
|
|
332
336
|
* The stillness hold (#91), on the cleaned positions (after the smoothing and
|
|
333
|
-
* the floor), joint by joint
|
|
334
|
-
*
|
|
337
|
+
* the floor), joint by joint — except the face (`FACE`: nose, eyes, ears, #109),
|
|
338
|
+
* which is held as one group (all of them or none, over the span where all of
|
|
339
|
+
* them pass), so a piecewise hold does not turn the head.
|
|
335
340
|
*
|
|
336
341
|
* A joint's speed on frame i is its movement from frame i − 1 in trunk lengths
|
|
337
342
|
* (wrapping with `loop`; none where either frame has no position), taken as a
|
|
@@ -346,10 +351,16 @@ export function jitterOf(all: readonly Series[], loop: boolean): { value: number
|
|
|
346
351
|
* from a start it grows one frame at a time while the reach holds on the grown
|
|
347
352
|
* span (its median re-read each time) and stops at the first frame that breaks
|
|
348
353
|
* it; if what it reached lasts long enough it is a hold and the next start is
|
|
349
|
-
* the frame after it, else the start moves one frame on. A
|
|
350
|
-
*
|
|
354
|
+
* the frame after it, else the start moves one frame on. A still run not held
|
|
355
|
+
* by that greedy pass that lasts at least `HOLD_MIN_SECONDS` and fails `reach`
|
|
356
|
+
* only on one interior run of at most `HOLD_EXCURSION` (6) frames stays held
|
|
357
|
+
* around that excursion (#108), provided the held frames themselves last at
|
|
358
|
+
* least `HOLD_MIN_SECONDS` and stay within `reach` of their own median; the
|
|
359
|
+
* excursion's frames blend from their cleaned positions toward the span median
|
|
360
|
+
* over the `HOLD_BLEND` frames from either held side. A span never crosses the
|
|
361
|
+
* loop's seam.
|
|
351
362
|
*
|
|
352
|
-
* On every frame of a hold the joint takes its span median. It blends in over
|
|
363
|
+
* On every held frame of a hold the joint takes its span median. It blends in over
|
|
353
364
|
* the `HOLD_BLEND` (6) frames before (p + w (held − p), w = (6 − k)/6 on the
|
|
354
365
|
* k-th frame before: the 6th stays as cleaned, the frame before is 5/6 of the
|
|
355
366
|
* way) and out over the 6 frames after, mirrored (the frame after is 5/6 of the
|
|
@@ -388,12 +399,16 @@ export function jitterOf(all: readonly Series[], loop: boolean): { value: number
|
|
|
388
399
|
* passes all three over the run, and its greedy spans broke elsewhere; the
|
|
389
400
|
* figure is its reach).
|
|
390
401
|
*/
|
|
402
|
+
/** The face joints (#109): held as one group (all of them or none, over the span where all of them pass), so a piecewise hold does not turn the head. */
|
|
403
|
+
export const FACE: readonly Body18[] = ['nose', 'r_eye', 'l_eye', 'r_ear', 'l_ear'];
|
|
404
|
+
|
|
391
405
|
export function holdStill(series: ReadonlyMap<Body18, Series>, times: readonly number[], trunk: number, speed: number, reach: number, loop: boolean, minSeconds = HOLD_MIN_SECONDS): { held: Map<Body18, Series>; holds: HoldFound[]; unheld: Unheld[]; seams: Seam[] } {
|
|
392
406
|
const n = times.length;
|
|
393
407
|
const names = BODY18.filter((name) => series.has(name));
|
|
394
408
|
const endOf = (i: number): number => (i + 1 < n ? times[i + 1]! : n > 1 ? 2 * times[i]! - times[i - 1]! : times[i]!);
|
|
395
409
|
const secondsOf = (a: number, b: number): number => Math.round((endOf(b) - times[a]!) * 1e6) / 1e6;
|
|
396
410
|
const med = new Map<Body18, Array<number | null>>();
|
|
411
|
+
const stillByJoint = new Map<Body18, boolean[]>();
|
|
397
412
|
for (const name of names) {
|
|
398
413
|
const xs = series.get(name)!;
|
|
399
414
|
const v: Array<number | null> = xs.map((p, i) => {
|
|
@@ -403,14 +418,18 @@ export function holdStill(series: ReadonlyMap<Body18, Series>, times: readonly n
|
|
|
403
418
|
if (q === null || q === undefined) return null;
|
|
404
419
|
return Math.hypot(p[0] - q[0], p[1] - q[1]) / trunk;
|
|
405
420
|
});
|
|
406
|
-
|
|
421
|
+
const sp = v.map((_, i) => {
|
|
407
422
|
const w = windowAt(i, n, HOLD_SPEED_WINDOW, loop).map((j) => v[j]).filter((x): x is number => x !== null && x !== undefined);
|
|
408
423
|
return w.length === 0 ? null : median1(w);
|
|
409
|
-
})
|
|
424
|
+
});
|
|
425
|
+
med.set(name, sp);
|
|
426
|
+
stillByJoint.set(name, xs.map((p, i) => p !== null && sp[i] !== null && sp[i] !== undefined && sp[i]! < speed));
|
|
410
427
|
}
|
|
411
428
|
/** One joint's distance (largest) from its per-coordinate median over a..b, trunk lengths, and that median; null where it has no position there. */
|
|
412
429
|
const spanOf = (name: Body18, a: number, b: number): { reach: number; at: Pt } | null => {
|
|
413
|
-
const
|
|
430
|
+
const s = series.get(name);
|
|
431
|
+
if (s === undefined) return null;
|
|
432
|
+
const pts = s.slice(a, b + 1).filter((p): p is Pt => p !== null);
|
|
414
433
|
if (pts.length === 0) return null;
|
|
415
434
|
const m: Pt = [median1(pts.map((p) => p[0])), median1(pts.map((p) => p[1]))];
|
|
416
435
|
let worst = 0;
|
|
@@ -423,8 +442,10 @@ export function holdStill(series: ReadonlyMap<Body18, Series>, times: readonly n
|
|
|
423
442
|
for (const name of names) {
|
|
424
443
|
const xs = series.get(name)!;
|
|
425
444
|
const sp = med.get(name)!;
|
|
426
|
-
|
|
427
|
-
const
|
|
445
|
+
// #109: the face joints (nose, eyes, ears) are held as one group — all of them or none, over the span where all of them pass.
|
|
446
|
+
const group: readonly Body18[] = FACE.includes(name) ? FACE : [name];
|
|
447
|
+
const still = Array.from({ length: n }, (_, i) => group.every((g) => stillByJoint.get(g)?.[i] === true));
|
|
448
|
+
const spans: Array<{ a: number; b: number; exc: [number, number] | null }> = [];
|
|
428
449
|
let s = 0;
|
|
429
450
|
while (s < n) {
|
|
430
451
|
if (!still[s]) {
|
|
@@ -432,18 +453,61 @@ export function holdStill(series: ReadonlyMap<Body18, Series>, times: readonly n
|
|
|
432
453
|
continue;
|
|
433
454
|
}
|
|
434
455
|
let e = s;
|
|
435
|
-
while (e + 1 < n && still[e + 1] && spanOf(
|
|
456
|
+
while (e + 1 < n && still[e + 1] && group.every((g) => spanOf(g, s, e + 1)!.reach <= reach)) e++;
|
|
436
457
|
if (secondsOf(s, e) >= minSeconds - 1e-9) {
|
|
437
|
-
spans.push(
|
|
458
|
+
spans.push({ a: s, b: e, exc: null });
|
|
438
459
|
s = e + 1;
|
|
439
460
|
} else s++;
|
|
440
461
|
}
|
|
462
|
+
// #108: a still run not held above that fails the reach only on one interior run of ≤ HOLD_EXCURSION frames
|
|
463
|
+
// stays held around that excursion, provided the held frames themselves last ≥ minSeconds and stay within reach.
|
|
464
|
+
const coveredByPass1 = (i: number) => spans.some((x) => i >= x.a && i <= x.b);
|
|
465
|
+
let u = 0;
|
|
466
|
+
while (u < n) {
|
|
467
|
+
if (!still[u] || coveredByPass1(u)) {
|
|
468
|
+
u++;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
let v = u;
|
|
472
|
+
while (v + 1 < n && still[v + 1] && !coveredByPass1(v + 1)) v++;
|
|
473
|
+
if (secondsOf(u, v) >= minSeconds - 1e-9) {
|
|
474
|
+
const ats = new Map(group.map((g) => [g, spanOf(g, u, v)!.at]));
|
|
475
|
+
let e0 = -1;
|
|
476
|
+
let e1 = -1;
|
|
477
|
+
let single = true;
|
|
478
|
+
for (let i = u; i <= v; i++) {
|
|
479
|
+
const over = group.some((g) => {
|
|
480
|
+
const p = series.get(g)![i]!;
|
|
481
|
+
const at = ats.get(g)!;
|
|
482
|
+
return Math.hypot(p[0] - at[0], p[1] - at[1]) / trunk > reach;
|
|
483
|
+
});
|
|
484
|
+
if (over) {
|
|
485
|
+
if (e0 === -1) e0 = i;
|
|
486
|
+
else if (i > e1 + 1) {
|
|
487
|
+
single = false;
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
e1 = i;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (single && e0 > u && e1 < v && e1 - e0 + 1 <= HOLD_EXCURSION && secondsOf(u, e0 - 1) + secondsOf(e1 + 1, v) >= minSeconds - 1e-9) {
|
|
494
|
+
const ok = group.every((g) => {
|
|
495
|
+
const kept = series.get(g)!.slice(u, v + 1).filter((p, k): p is Pt => p !== null && (u + k < e0 || u + k > e1));
|
|
496
|
+
const km: Pt = [median1(kept.map((p) => p[0])), median1(kept.map((p) => p[1]))];
|
|
497
|
+
return kept.every((p) => Math.hypot(p[0] - km[0], p[1] - km[1]) / trunk <= reach);
|
|
498
|
+
});
|
|
499
|
+
if (ok) spans.push({ a: u, b: v, exc: [e0, e1] });
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
u = v + 1;
|
|
503
|
+
}
|
|
504
|
+
spans.sort((x, y) => x.a - y.a);
|
|
441
505
|
const out: Series = xs.map((p) => (p === null ? null : [p[0], p[1]] as Pt));
|
|
442
|
-
const inHold = (i: number) => spans.some((
|
|
506
|
+
const inHold = (i: number) => spans.some(({ a, b, exc }) => i >= a && i <= b && (exc === null || i < exc[0] || i > exc[1]));
|
|
443
507
|
// The blends: on each frame outside a hold, the largest weight any hold of this joint gives it (the earlier hold on a tie).
|
|
444
508
|
const blend = new Map<number, { w: number; at: Pt }>();
|
|
445
|
-
const found: Array<{ a: number; b: number; at: Pt; in: number[]; out: number[] }> = [];
|
|
446
|
-
for (const
|
|
509
|
+
const found: Array<{ a: number; b: number; exc: [number, number] | null; at: Pt; in: number[]; out: number[] }> = [];
|
|
510
|
+
for (const { a, b, exc } of spans) {
|
|
447
511
|
const { at } = spanOf(name, a, b)!;
|
|
448
512
|
const ins: number[] = [];
|
|
449
513
|
const outs: number[] = [];
|
|
@@ -456,9 +520,25 @@ export function holdStill(series: ReadonlyMap<Body18, Series>, times: readonly n
|
|
|
456
520
|
if (was === undefined || w > was.w) blend.set(i, { w, at });
|
|
457
521
|
}
|
|
458
522
|
}
|
|
459
|
-
|
|
523
|
+
if (exc !== null) {
|
|
524
|
+
const [e0, e1] = exc;
|
|
525
|
+
for (let k = 1; k < HOLD_BLEND; k++) {
|
|
526
|
+
const w = (HOLD_BLEND - k) / HOLD_BLEND;
|
|
527
|
+
for (const i of [e0 - 1 + k, e1 + 1 - k]) {
|
|
528
|
+
if (i < e0 || i > e1) continue;
|
|
529
|
+
const was = blend.get(i);
|
|
530
|
+
if (was === undefined || w > was.w) blend.set(i, { w, at });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
found.push({ a, b, exc, at, in: ins, out: outs });
|
|
535
|
+
}
|
|
536
|
+
for (const { a, b, exc, at } of found) {
|
|
537
|
+
for (let i = a; i <= b; i++) {
|
|
538
|
+
if (exc !== null && i >= exc[0] && i <= exc[1]) continue;
|
|
539
|
+
if (out[i] !== null) out[i] = [at[0], at[1]];
|
|
540
|
+
}
|
|
460
541
|
}
|
|
461
|
-
for (const { a, b, at } of found) for (let i = a; i <= b; i++) if (out[i] !== null) out[i] = [at[0], at[1]];
|
|
462
542
|
for (const [i, { w, at }] of blend) {
|
|
463
543
|
const p = xs[i];
|
|
464
544
|
if (p === null || p === undefined) continue;
|
|
@@ -488,6 +568,7 @@ export function holdStill(series: ReadonlyMap<Body18, Series>, times: readonly n
|
|
|
488
568
|
holds.push({
|
|
489
569
|
joint: name,
|
|
490
570
|
frames: [f.a, f.b],
|
|
571
|
+
excursion: f.exc,
|
|
491
572
|
seconds: secondsOf(f.a, f.b),
|
|
492
573
|
speed: fastest,
|
|
493
574
|
reach: spanOf(name, f.a, f.b)!.reach,
|
|
@@ -673,7 +754,12 @@ export function clean(seq: PoseSequence, params: CleanParams, wanted: string | u
|
|
|
673
754
|
spikes: { found: spikes.map((s) => ({ frame: s.frame, joint: s.joint })), untested },
|
|
674
755
|
jitter: { before: before.value, after: after.value },
|
|
675
756
|
...(hold === undefined || hold === 'off' ? {} : {
|
|
676
|
-
holds: holds.map((h) => ({
|
|
757
|
+
holds: holds.map((h) => ({
|
|
758
|
+
joint: h.joint,
|
|
759
|
+
frames: [h.frames[0], h.frames[1]] as [number, number],
|
|
760
|
+
...(h.excursion === null ? {} : { excursion: [h.excursion[0], h.excursion[1]] as [number, number] }),
|
|
761
|
+
jitter: { before: h.jitter.before, after: h.jitter.after },
|
|
762
|
+
})),
|
|
677
763
|
unheld: unheld.map((u) => ({ joint: u.joint, frames: [u.frames[0], u.frames[1]] as [number, number], test: u.test, value: u.value })),
|
|
678
764
|
seams: seams.map((x) => ({ joint: x.joint, frame: x.frame, step: x.step, gap: x.gap })),
|
|
679
765
|
}),
|
|
@@ -748,13 +834,14 @@ function holdText(r: CleanRecord): string {
|
|
|
748
834
|
const groups = new Map<string, string[]>();
|
|
749
835
|
for (const x of holds) {
|
|
750
836
|
if (x.frames[0] > b || x.frames[1] < a) continue;
|
|
751
|
-
const
|
|
837
|
+
const exc = x.excursion === undefined ? '' : ` (excursion ${x.excursion[0] === x.excursion[1] ? `${x.excursion[0]}` : `${x.excursion[0]}–${x.excursion[1]}`} kept)`;
|
|
838
|
+
const k = `${x.frames[0]}–${x.frames[1]}${exc}`;
|
|
752
839
|
groups.set(k, [...(groups.get(k) ?? []), x.joint]);
|
|
753
840
|
}
|
|
754
841
|
const held = [...groups].map(([k, js]) => `${js.join(', ')} ${k}`).join('; ');
|
|
755
842
|
const not = (r.unheld ?? []).filter((u) => u.frames[0] >= a && u.frames[1] <= b).map(unheldText(h));
|
|
756
843
|
const sm = (r.seams ?? []).filter((x) => x.frame >= a && x.frame <= b).map((x) => `${x.joint} ${x.frame} (step ${x.step.toFixed(4)}${x.gap > 0 ? `, gap ${x.gap}` : ''})`);
|
|
757
|
-
const ramps = holds.filter((x) => x.frames[0] <= b && x.frames[1] >= a && x.jitter.after !== null && x.jitter.after > 0).map((x) => `${x.joint} ${x.frames[0]}–${x.frames[1]} ${x.jitter.after!.toFixed(4)} px/f² (ramp)`);
|
|
844
|
+
const ramps = holds.filter((x) => x.frames[0] <= b && x.frames[1] >= a && x.jitter.after !== null && x.jitter.after > 0).map((x) => `${x.joint} ${x.frames[0]}–${x.frames[1]} ${x.jitter.after!.toFixed(4)} px/f² (${x.excursion !== undefined ? 'excursion' : 'ramp'})`);
|
|
758
845
|
return `; HOLD frames ${a}–${b}: held ${held}${sm.length === 0 ? '' : `; seams cross-faded: ${sm.join(', ')}`}${ramps.length === 0 ? '' : `; jitter after ${ramps.join(', ')}`}${not.length === 0 ? '; every joint held' : `; not held: ${not.join(', ')}`}`;
|
|
759
846
|
}).join('');
|
|
760
847
|
}
|