@rig-c/pose 0.13.0 → 0.15.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 +59 -15
- package/cli.ts +101 -14
- package/package.json +1 -1
- package/src/clean.ts +448 -0
- package/src/pose.ts +128 -4
- package/src/retarget.ts +7 -68
- package/src/run.ts +21 -3
- package/src/switch.ts +1 -1
- package/src/verify.ts +49 -10
package/README.md
CHANGED
|
@@ -16,12 +16,54 @@ Part of the rig-c toolchain: [core](https://github.com/rig-c/core) compiles and
|
|
|
16
16
|
rig-pose retarget --rig examples/wave/rig --map examples/wave/map.json --pose examples/wave/pose.json --out wave.motion.json --build wave-build
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
It prints the gate's verdict and where every mapped bone landed, then writes `wave.motion.json` and the gated build. `rigc preview --candidate wave-build` plays it in the Spine Web Player. A rig with no art shows only its bones.
|
|
19
|
+
`pose.json` is already cleaned (`make-pose.ts` passes it through `clean`). It prints the gate's verdict and where every mapped bone landed, then writes `wave.motion.json` and the gated build. `rigc preview --candidate wave-build` plays it in the Spine Web Player. A rig with no art shows only its bones.
|
|
20
20
|
|
|
21
21
|
## What the rig needs
|
|
22
22
|
|
|
23
23
|
The map names **chains**: each limb as bones that descend from one another, parent to child. That means a thigh with its shin below it, and an upper arm with its forearm below it, and likewise for the trunk and the head. A rig whose arm is one bone, or that has no leg bones, cannot take a pose: the map is refused by name (`MAP_*`). [@rig-c/parts](https://github.com/rig-c/parts) builds such a rig from a painting's keypoints with `propose --keypoints`. A rig drawn by hand needs the same chains.
|
|
24
24
|
|
|
25
|
+
## Clean first: `rig-pose clean`
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
rig-pose clean --pose dance.scan.json --out dance.pose.json [--loop] [--legs 5] [--rest 7] [--hips-inferred]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`retarget` reads only a sequence that `clean` wrote ([#67](https://github.com/rig-c/pose/issues/67)). A pose model reads each frame alone, so a hanging wrist moves ±20 px a frame and a cloak-hidden leg jumps 100+ px, and every key carries that shake. `clean` takes it out once, at the sequence, where it can be inspected. A sequence without the record is refused by name (`POSE_NOT_CLEANED`), and the message is the `clean` command to run on that file. There is no flag to skip the step. Windows of 1 (`--legs 1 --rest 1`) are a legitimate run that smooths nothing, and they are recorded as such.
|
|
32
|
+
|
|
33
|
+
The steps, in this order, each printed with its figures:
|
|
34
|
+
|
|
35
|
+
1. **Spikes.** A joint more than 0.2 trunk lengths from its 5-frame median takes the median. The trunk is neck → mid-hip, the median over the clip. Each replacement is listed by frame and joint (`SPIKE`). Without `--loop`, a clip end cuts the window of the first and last two frames. There the median is a neighbour's value, and a joint moving steadily would read as a spike. Those frames are **not tested**, and the `SPIKES` line and the record name them. Reflecting the window does not help: the median of `[x2, x1, x0, x1, x2]` is still `x1`.
|
|
36
|
+
2. **Occlusion.** A joint whose state is not `observed` (or `inferred`), or whose score is under 0.3, takes its 3-frame median (`OCCLUDED`).
|
|
37
|
+
3. **Smoothing.** A quadratic Savitzky–Golay per joint: the legs (both hips, knees and ankles) over `--legs` frames (default 5), every other joint over `--rest` (default 7). A window is 1 (none) or odd and at least 5. 3 is refused because a quadratic through 3 points passes through all three. Without `--loop`, the window is cut at the ends. Where fewer than three frames have a position, the degree drops to fit the ones that do.
|
|
38
|
+
4. **Floor.** Only with `--loop`: the whole figure moves in y by one constant, so that the lowest ankle over the cycle is where it was before smoothing.
|
|
39
|
+
|
|
40
|
+
A **median** is taken per coordinate (x and y apart), over the window's frames on which the joint has a position, its own frame included, whatever its state. An even count takes the mean of the middle two. **`--loop`** says the sequence is one cycle, such as a gait from strike to strike. Every window then wraps, the last frame running into the first, so nothing is untested; a cycle shorter than its widest window is refused (`CLEAN_LOOP`). **Nothing is invented.** A joint with no position on a frame gets none: `retarget` holds it and prints a `HOLD` line. States and scores are written as they were read. `--coco` reads COCO-17 (below) and converts it first. `--person` picks the person when there is more than one, and only that person is written.
|
|
41
|
+
|
|
42
|
+
The output carries the record, here the dancer's from the table below:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
"cleaned": { "spec": "rig-pose-clean/1",
|
|
46
|
+
"params": { "loop": false, "legs": 5, "rest": 7 },
|
|
47
|
+
"spikes": { "found": [], "untested": [0, 1, 311, 312] },
|
|
48
|
+
"jitter": { "before": 7.9123, "after": 4.6225 } }
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`jitter` is the mean |second difference| of y, in px/f², over every joint and every three consecutive frames that have a position, wrapping with `--loop`. It is measured on the sequence as read and on the cleaned positions. `retarget` prints the record as its `CLEAN` line and copies it into `--report`.
|
|
52
|
+
|
|
53
|
+
**`--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 four 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)`.
|
|
54
|
+
|
|
55
|
+
**Measured** on HQ's references ([#67](https://github.com/rig-c/pose/issues/67)'s bars):
|
|
56
|
+
|
|
57
|
+
| Clip | Run | Jitter, px/f² | Against |
|
|
58
|
+
| --- | --- | --- | --- |
|
|
59
|
+
| The 13 s dancer | defaults | 7.91 → 4.62 | HQ's hand-cleaned file: 4.62; positions within 0.071 px |
|
|
60
|
+
| armor_walk, one 32-frame cycle | `--loop` | 9.43 → 3.16; 3.155 without the four arm joints | HQ's file without the arm joints: 3.156 (HQ's hand step smoothed the arms a second time) |
|
|
61
|
+
| armor_run, one 20-frame cycle | `--loop --rest 5` | 17.06 → 7.99 | HQ's file: 8.17 |
|
|
62
|
+
| school_walk, 90 frames, held out | defaults | 5.99 → 1.95 (−67 %) | the bar: −30 % |
|
|
63
|
+
| school_run, 90 frames, held out | defaults | 47.00 → 11.54 (−75 %) | the bar: −30 % |
|
|
64
|
+
|
|
65
|
+
A retarget of the cleaned dancer, with `--flip 90 --short 0.35 --bends clamp --leg-scale 0.8,1.05 --plant-feet`, finds the same jumps (`FEET`) as the same run on HQ's hand-cleaned file. Its hip rotate sd over frames 94–175 is 4.574, against HQ's 4.58.
|
|
66
|
+
|
|
25
67
|
## What it does
|
|
26
68
|
|
|
27
69
|
```sh
|
|
@@ -49,7 +91,7 @@ and what the run cannot decide for you it lists:
|
|
|
49
91
|
- `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.
|
|
50
92
|
- `BEND` — with `--bends clamp`, a hinge bent the wrong way is straightened on that frame instead of refusing.
|
|
51
93
|
- `FOLD` — `rigc render --folds` over the new animation: every mesh that folds, and when. A reading, not a gate.
|
|
52
|
-
- `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.
|
|
94
|
+
- `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. The sway reads the hips as before.
|
|
53
95
|
- `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)).
|
|
54
96
|
- `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.
|
|
55
97
|
- `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.
|
|
@@ -84,19 +126,19 @@ The demand lists:
|
|
|
84
126
|
|
|
85
127
|
Runs in `turns`, `range` and `artRange` merge across a gap of one frame, as `endOn` does; `seconds` runs from the first frame's key time to the end of the last frame.
|
|
86
128
|
|
|
87
|
-
`--smooth <n>`
|
|
129
|
+
`--smooth <n>` is an alias kept since [#67](https://github.com/rig-c/pose/issues/67). The smoothing is `clean`'s (`--legs`, `--rest`), so `retarget` prints `--smooth is carried by clean; ignored` and smooths nothing: `--smooth 7` writes the same bytes as `--smooth 1`. The three paragraphs below were measured before #67, when `retarget` smoothed with a cubic Savitzky–Golay over `--smooth` frames. They are kept as the record of why the smoothing moved to the sequence.
|
|
88
130
|
|
|
89
131
|
**`--smooth 7` flattens the pelvis's rebound.** Measured on HQ's dancer with `--leg-scale 0.8,1.05 --plant-feet` ([#47](https://github.com/rig-c/pose/issues/47)): the rebound — the hip's peak upward acceleration at the bottom of each bounce, rig ÷ reference, median over a window, bar ≥ 0.7 — reads 0.40–0.52 with `--smooth 7` in every configuration measured (0.461 and 0.501 in the two windows, where `--smooth 1` reads 0.648 and 0.942). So the pelvis is read at `--smooth 1`, and `--smooth 7` is not advised for it. What follows is the earlier advice, for the limbs.
|
|
90
132
|
|
|
91
133
|
**`--smooth 7` at 24 fps, for the limbs.** It was measured on a 13 s dance on a @rig-c/parts rig, read back from the gated build: the motion's timing did not move (speed cross-correlation lag 0 on the wrists, ankles and hip), the frame-to-frame second difference dropped 2.5× on the wrists, 1.7–2.1× on the ankles and 1.5× on the hip, and the hip's 95th-percentile speed fell to 0.85×. The criterion set beforehand, a 2× drop on every point, was not met on the hip. That criterion was flawed: the hip's second difference is mostly real motion, while the filter cuts white noise 4.6×. A measure that isolates noise is a later card, and the default stays off until it passes.
|
|
92
134
|
|
|
93
|
-
**The flip guard.** A pose model can put a fast sweep of a limb into one frame — under motion blur it takes the elbow or the wrist for another point — and the rig then whips the limb round between two keys. Position smoothing cannot remove a step like that. So after holds
|
|
135
|
+
**The flip guard.** A pose model can put a fast sweep of a limb into one frame — under motion blur it takes the elbow or the wrist for another point — and the rig then whips the limb round between two keys. Position smoothing cannot remove a step like that. So after holds, and before the hinge check, every segment the map aims (not a `reach`, which is aimed later from posed positions) is read frame to frame: a turn greater than `--flip <deg>` from frame k−1 to frame k is spread. For the smallest h ≥ 1 that keeps every one-frame turn at or under the threshold, frames k−h … k+h−1 are replaced by the straight line from frame k−h−1 to frame k+h, the turn's own way round. It is centred, so the step happens when the pose put it, over 2h frames, and both ends keep the pose's angles. A window that would cross the first or last frame stops there and the line says so; two windows that overlap become one. `--flip off` keeps every turn. The report lists each spread under `flips`.
|
|
94
136
|
|
|
95
137
|
The threshold was fixed by measuring first ([#15](https://github.com/rig-c/pose/issues/15)). On the 13 s dance (313 frames, RTMPose), the one-frame turns of the picture angle of 10 segments — both upper arms, forearms, thighs and shins, the eye line and the hip line, 3,120 turns — run p50 1.0°, p90 5.0°, p99 23.1°, p99.5 33.4°, p99.9 78.2°, max 148.6°. 90° is the p99.9 rounded up past a margin, and marks the 3 turns above it (0.1%): both forearms between frames 181 and 182 (148.6° and 114.4°, a motion-blurred stretch) and the left forearm between 53 and 54 (99.2°, the forearm pointing at the camera). None of them is a spike that comes back: each keeps the direction the limb was already turning, and the new angle holds — a real sweep put into one frame. Holding the last angle would be wrong a frame later, so the guard spreads the step instead. With it, on the @rig-c/parts rig #11 measured, the largest one-frame turn of any guarded bone in the gated build, posed by rig-c, is 78.2° (148.6° without it), and every bone still lands.
|
|
96
138
|
|
|
97
139
|
Re-measured with the guard on, against the guarded unsmoothed run, on #11's criteria (power above 6 Hz down ≥ 2× on every point, timing lag 0, p95 speed ≥ 0.85×): no window passes. `--smooth 7` comes closest — the left wrist's power drops 1.99× and the hip's p95 speed is 0.847× — so `--smooth` stays off by default, and so does the guard: the rule posted before the run made a default for either one depend on a window passing. **Use `--flip 90`**: with it, no bone of the gated build turns more than 90° in one frame (without it, 148.6°), and every bone still lands.
|
|
98
140
|
|
|
99
|
-
**The short hold.** A segment pointing at the camera is short in the picture, and its picture direction is then detector noise: the rig turns the bone to follow it. So after holds
|
|
141
|
+
**The short hold.** A segment pointing at the camera is short in the picture, and its picture direction is then detector noise: the rig turns the bone to follow it. So after holds, and before the flip guard, every limb segment the map aims is measured on every frame (not a `reach`, and not a line across the body from a left joint to its right twin, such as the hip line or the eye line: those shorten when the body or head turns, which is a different thing, and the cutoff below was measured on limbs only) — its picture length, between the same two points its angle is read from, over its own median length across the pose. On a frame under `--short <fraction>` its angle is not read from the pose: it is interpolated, the short way round, from the last frame at or above the cutoff to the next one, and held flat at the first or last frame of the pose. The flip guard, the hinge check and the keys all see the held angle. `--short off` (the default) reads every frame. The report lists each span under `short` (chain, bone, segment, first and last frame, the lowest fraction).
|
|
100
142
|
|
|
101
143
|
The cutoff was fixed by measuring first ([#22](https://github.com/rig-c/pose/issues/22)), over both upper arms, forearms, thighs and shins. On the 13 s dance (24 fps, 2,504 segment-frames) the fraction's 0.5th percentile is 0.51, its 5th 0.82, and 7 segment-frames (0.28%) are under 0.35; on a 16 s second sequence (30 fps, 3,936) they are 0.27 and 0.75, and 34 (0.86%). Every frame under 0.35 was looked at, and was one of two kinds:
|
|
102
144
|
|
|
@@ -142,7 +184,7 @@ A rig's motion spec usually has an `idle` — a blink, a breath, hair and hem sw
|
|
|
142
184
|
|
|
143
185
|
Every bone still has to land with the carried tracks in. A carried translate above the translated bone moves its origin off the pose's travel, and the run refuses with `POSE_LANDS`, naming the carried tracks above the bone that missed.
|
|
144
186
|
|
|
145
|
-
On HQ's dancer (`--flip 90 --short 0.35 --bends clamp --carry-idle`) the idle has 26 tracks. Kept out: the four arm rotates, which the dance keys, and
|
|
187
|
+
On HQ's dancer (`--flip 90 --short 0.35 --bends clamp --carry-idle`) the idle has 26 tracks. Kept out: the four arm rotates, which the dance keys, and a control bone's scalex above the chest, the neck's rotate and a head control's rotate, which sit above the mapped chest, head and arms — carried, they missed the chest, the head and the arms by 0.16°–1.37°. Carried: the other 19, the blink (an eyes group's scaley) and the breath (the chest control's translatey) among them. The gate is green, every bone lands (worst 0.00167°), and rig-c's poser shows the blink at every cycle (scaleY 0.12 at 2.37 s, 5.70 s, 9.08 s and 12.41 s, against the idle's at t mod 3.333 s within 8.9e-6) and the breath's offset (5.02 at 0.833, 4.167, 7.5, 10.833 s; 0 at 2.5, 5.833, 9.167, 12.5 s; 1.035 at the cut, 13 s).
|
|
146
188
|
|
|
147
189
|
**`--merge <file>`** merges a timed track fragment into the new animation, at absolute times from its start — blinks read off the video, for example:
|
|
148
190
|
|
|
@@ -182,12 +224,12 @@ Exit codes: `0` written, `1` refused (every reason printed, nothing written), `2
|
|
|
182
224
|
```
|
|
183
225
|
|
|
184
226
|
- `t` is seconds and strictly increases; the animation's keys are at `t − t₀`.
|
|
185
|
-
- Joints are body-18 (`nose neck r_shoulder r_elbow r_wrist l_shoulder l_elbow l_wrist r_hip r_knee r_ankle l_hip l_knee l_ankle r_eye l_eye r_ear l_ear`); `r`/`l` are the **subject's** sides. `state` is `observed`, `occluded` or `missing`, as in the keypoints file. An unlisted joint is missing.
|
|
227
|
+
- Joints are body-18 (`nose neck r_shoulder r_elbow r_wrist l_shoulder l_elbow l_wrist r_hip r_knee r_ankle l_hip l_knee l_ankle r_eye l_eye r_ear l_ear`); `r`/`l` are the **subject's** sides. `state` is `observed`, `occluded` or `missing`, as in the keypoints file, or `inferred` ([#63](https://github.com/rig-c/pose/issues/63)): a position the detector supplied but did not see. An `inferred` joint carries `at` (one without it is refused, `POSE_JOINT_AT`), and `retarget` keys it as it keys `observed`. An unlisted joint is missing.
|
|
186
228
|
- Two differences from the keypoints file: the unit is `picture-px`, and `at` may lie outside the picture (a hand leaves the frame; the score says how much to believe it).
|
|
187
229
|
- More than one person: `--person <id>`.
|
|
188
230
|
- `score` is whatever the pose model gives. RTMPose's goes above 1, so `--min-score` is a cut on that model's scale, not a probability.
|
|
189
231
|
|
|
190
|
-
**From COCO-17** — the layout most pose models and datasets use — `rig-pose
|
|
232
|
+
**From COCO-17** — the layout most pose models and datasets use — `rig-pose clean --coco in.json --out pose.json`, which converts and cleans in one step. `rig-pose convert --coco` converts only, and its output still needs `clean`. `retarget --coco` is refused (`POSE_NOT_CLEANED`), naming the `clean --coco` command. The input is `{ "format": "coco17-sequence/1", "width", "height", "source", "frames": [{ "t", "keypoints" }] }`, keypoints in COCO's order as 17 `[x, y, score]` or 51 numbers, or `null` for a frame with nobody. The mapping: COCO's `left_*`/`right_*` → body-18's `l_*`/`r_*` (both are the subject's sides), a score of 0 → `missing`, and body-18's `neck`, which COCO lacks, is the shoulders' midpoint scored the lower of the two (OpenPose's definition).
|
|
191
233
|
|
|
192
234
|
|
|
193
235
|
**The camera** ([#63](https://github.com/rig-c/pose/issues/63)). A sequence may carry an optional `camera`, one `{ "s", "tx", "ty", "inliers" }` per frame: the picture's scale and translation against frame 0's, so a point p₀ of frame 0's picture appears at `s · p₀ + (tx, ty)` on that frame. Its joints are then already normalised by it, `p′ = (p − t) / s`, and `inliers` is how many background features the fit kept. `s` must be > 0, `tx` and `ty` finite, `inliers` an integer ≥ 0, and there is one entry per frame; anything else is refused by name (`POSE_CAMERA`). The retarget does not apply it again: it prints a `CAMERA` line with the range of `s` and the three frames where it changes fastest. With `--plant-feet` and no `camera`, the `CAMERA` line says the floor may step with the picture, since a zoom or a pan in the video moves the floor with it. An optional `cameraSource` string says where the camera came from: `"tracked"` or `"hand-labelled, N keys"`. It is refused without a `camera`, or when it is not a non-empty string (`POSE_CAMERA_SOURCE`), and the `CAMERA` line starts with it (`CAMERA tracked: …`; a sequence with a `camera` and no `cameraSource` prints `CAMERA (source not stated): …`). `tools/scan.py` (not shipped) reads the camera off the video's background and writes the normalised sequence with `"cameraSource": "tracked"`. A frame whose fit keeps too few background features is refused by name, and nothing is written. Where the background cannot be tracked, `tools/scan.py --camera-file` takes a hand-written camera instead, a `rig-pose-camera/1` file: `{ "spec": "rig-pose-camera/1", "keys": [ { "frame", "s", "tx", "ty" } ], "note"? }`. Its frames are integers of the pose and strictly increase, `s` is > 0, and `tx` and `ty` are finite. Values are linear between keys and held before the first key and after the last. `inliers` is 0 on every frame, since no feature was fitted, and anything malformed is refused by name (`CAMERA_FILE`). `tools/scan_selftest.py` is the synthetic check of both paths.
|
|
@@ -233,23 +275,25 @@ Bone rotation cannot carry every pose: an arm behind the head, a forearm toward
|
|
|
233
275
|
|
|
234
276
|
```json
|
|
235
277
|
"alternates": [
|
|
236
|
-
{ "name": "
|
|
237
|
-
"slots": { "
|
|
278
|
+
{ "name": "raised_arms",
|
|
279
|
+
"slots": { "upperarm_r_raised": "raised/upperarm_r", "forearm_r_raised": "raised/forearm_r", "torso": "raised/torso" },
|
|
238
280
|
"hides": ["upperarm_r", "forearm_r"],
|
|
239
281
|
"chains": ["arm_r", "arm_l"],
|
|
240
|
-
"pose": { "r_shoulder": [
|
|
241
|
-
"l_shoulder": [
|
|
242
|
-
"drawn": { "sleeve_r0":
|
|
282
|
+
"pose": { "r_shoulder": [400, 600], "r_elbow": [300, 450], "r_wrist": [450, 400],
|
|
283
|
+
"l_shoulder": [600, 600], "l_elbow": [700, 450], "l_wrist": [550, 400] },
|
|
284
|
+
"drawn": { "sleeve_r0": 120, "sleeve_r1": 30 },
|
|
243
285
|
"offsets": { "sleeve_r1": [4, -2] } } ]
|
|
244
286
|
```
|
|
245
287
|
|
|
288
|
+
(The names, coordinates and angles above are illustrative.)
|
|
289
|
+
|
|
246
290
|
- `slots`: what each slot of the set shows while it shows; each attachment must be in that slot under some skin of the rig, as rig-c built it. `hides`: the slots that are empty while it shows. A slot's place in the draw order is the rig's own, so a drawing that must sit in front of the torso is an attachment in a slot that is: a switch is attachment keys only, and no draw-order key is written.
|
|
247
291
|
- `chains`: the map's chains whose pose the set depicts; it is selected by them. `pose`: the key skeleton the art was painted from, body-18 (or `mid_hip`/`mid_shoulder`) in picture px, y down, any scale and place — only its directions are read; every joint the listed chains use must be in it.
|
|
248
292
|
- `drawn` (optional): each bone's world angle (Spine degrees) as painted. It changes no key: while a set shows, a bone is keyed to the pose as always, and the art turns with it. What it gives is a figure: the art turn, how far each bone is from the angle its art was painted at (`artTurn`, and `ART-RANGE` past the bone's range).
|
|
249
293
|
- `offsets` (optional): a translate per bone, in its parent's axes, held while the set shows and 0 elsewhere (stepped keys), so the joints as painted are the pivots. A bone `translate` already keys (the hip) cannot take one.
|
|
250
294
|
- Each name resolves against the built rig and the map, and a miss is refused by name, listing what exists: `MAP_ALT_SLOT`, `MAP_ALT_ATTACHMENT`, `MAP_ALT_HIDES`, `MAP_ALT_CHAIN`, `MAP_ALT_JOINT`, `MAP_ALT_POSE`, `MAP_ALT_DRAWN` and `MAP_ALT_OFFSET` (a bone that is not a bone of the set's chains).
|
|
251
295
|
|
|
252
|
-
**When one shows.** Per frame and per set, the distance is the largest difference, in degrees, between a segment of its chains as the retarget asked for it (after holds,
|
|
296
|
+
**When one shows.** Per frame and per set, the distance is the largest difference, in degrees, between a segment of its chains as the retarget asked for it (after holds, the short hold, the flip guard and the hinge check; before any range clamp) and the same segment in its key skeleton. On each frame the nearest set within `--switch-tolerance` (default 25°) shows, ties going to the map's order; a run of one set shorter than `--switch-min` (default 0.25 s) is dropped and the base art shows there. Both figures were fixed before a run ([#6](https://github.com/rig-c/pose/issues/6)). Sets compete only with those they share a slot (shown or hidden) or an offset bone with, so a drawing of each arm can show at once; two that compete never overlap.
|
|
253
297
|
|
|
254
298
|
**Where a switch is keyed: `--switch-at`.** By default (`first`) a boundary — the frame a set starts showing on, and the frame the base art or the next set comes back on — is where the selection above puts it: the first frame within the tolerance, often a frame where the arm is slowing into the pose, so the change of drawing can be seen on a nearly still arm. `--switch-at peak` moves each boundary to the frame within ±`--switch-min`/2 (±3 frames at 24 fps with the default 0.25 s) where the chains it changes turn fastest, so the cut lands in motion ([#31](https://github.com/rig-c/pose/issues/31)). The speed on a frame is the largest |angular speed| in degrees per frame, a central difference of the target world angles (one-sided on the first and last frame), over every segment of the leaving set's and the arriving set's chains; it is read on the same targets as the distance, before the range clamp and any set's `turn`, which depend on where the spans are. The window is clipped so every span keeps `--switch-min`, a gap between two spans of one set keeps it too (the bridging holds), no boundary reaches its neighbour, and a set with `frames` shows only inside them. In it, a frame qualifies only if the set the move newly shows is within **2 × `--switch-tolerance`** of its key skeleton on every frame it newly shows it on; the boundary goes to the fastest qualifying frame, and stays when none qualifies. A tie goes to the frame nearest the original, then the earlier. A boundary between two sets moves once.
|
|
255
299
|
|
|
@@ -259,7 +303,7 @@ Either way the `SWITCH` line ends with `moved on …, off …`: each boundary's
|
|
|
259
303
|
|
|
260
304
|
**What is written.** An `attachment` track per slot whose picture changes: the set's attachment (or `null` for a hidden slot) where a span starts, and the slot's setup attachment, as the compiled model states it, where it ends. A direct switch from one set to another puts the first set's slots back to setup on the second's first frame, so an extra slot that is empty at setup goes empty. A switch onto frame *i* is keyed halfway between frames *i*−1 and *i*: a stepped key on the frame's own time fires a frame late wherever a player's accumulated time lands just below it (rig-c's docs/AUTHORING.md §4.5), and halfway both the pose's times and a renderer's `i / fps` see it on frame *i*. Before the first key a slot shows its setup attachment, so no key is written at 0 unless a span starts there. Offsets are `translate` keys at the same times, `stepped`. rig-c's gate checks these keys like any other.
|
|
261
305
|
|
|
262
|
-
**TURN.** The cutoff was measured on the three lines across the body before it was fixed ([#6](https://github.com/rig-c/pose/issues/6)): on the 13 s front-facing dance their picture length never drops under 0.81 of its median and none reverses; on a second sequence the hip line falls to 0.17 at frames 465–491 (side-on) and the eye line to 0.22 at 26–58 (head in profile), and 3 frames reverse. 0.5 lies between. A line is reversed when it points more than 90° from the subject's right joint to the left (front-facing, it points at 0°). The lines are read after holds
|
|
306
|
+
**TURN.** The cutoff was measured on the three lines across the body before it was fixed ([#6](https://github.com/rig-c/pose/issues/6)): on the 13 s front-facing dance their picture length never drops under 0.81 of its median and none reverses; on a second sequence the hip line falls to 0.17 at frames 465–491 (side-on) and the eye line to 0.22 at 26–58 (head in profile), and 3 frames reverse. 0.5 lies between. A line is reversed when it points more than 90° from the subject's right joint to the left (front-facing, it points at 0°). The lines are read after holds, held as the map's points are even where the map does not use them; a line whose points are good on no frame is named as not measured.
|
|
263
307
|
|
|
264
308
|
|
|
265
309
|
**A stopgap window: `frames`.** A set may state `"frames": [[first, last], …]`: it shows only inside those windows, and on them it is chosen over any set within tolerance that has no window. It is for two sets whose key skeletons the angles cannot tell apart. Measured on HQ's dancer, the hands clasped behind the back and the hands low in front differ by only 2–21°, and the wrist and hand-point scores do not separate them either; the cue that might is #33's depth state. It is recorded in the map as a stopgap, not a selection rule. A window **restricts** a set; it never forces one. Where the set does not show inside its window, the run says why in a `FRAMES` line: the stretch, and whether the pose there was past the tolerance (with the largest distance), or within it but the run was dropped.
|
package/cli.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
10
10
|
import pkg from './package.json' with { type: 'json' };
|
|
11
|
-
import {
|
|
11
|
+
import { clean, cleanLine, untestedText, LEGS, OCCLUSION_SCORE, OCCLUSION_WINDOW, SPIKE_TRUNKS, SPIKE_WINDOW, windowProblem } from './src/clean.ts';
|
|
12
|
+
import { convertCoco17, readPoseSequence, writePoseSequence } from './src/pose.ts';
|
|
12
13
|
import { parseWindows } from './src/compare.ts';
|
|
13
14
|
import { lineOf, Refusal } from './src/problems.ts';
|
|
14
15
|
import { runCompare, type CompareArgs } from './src/runcompare.ts';
|
|
@@ -17,29 +18,56 @@ import { runRetarget, type RetargetArgs } from './src/run.ts';
|
|
|
17
18
|
const HELP = `rig-pose ${pkg.version} — motion from a pose sequence onto a rig-c rig
|
|
18
19
|
|
|
19
20
|
Usage:
|
|
21
|
+
rig-pose clean --pose <pose.json> --out <pose.clean.json> [--loop] [--legs 5] [--rest 7] [--coco] [--person <id>]
|
|
20
22
|
rig-pose retarget --rig <rig> --map <map.json> --pose <pose.json> --out <motion.json> [--name dance] [flags]
|
|
21
23
|
rig-pose convert --coco <coco17.json> --out <pose.json>
|
|
22
24
|
rig-pose compare --reference <pose.json> --render <pose.json> --map <map.json> [flags]
|
|
23
25
|
rig-pose --version | --help
|
|
24
26
|
|
|
27
|
+
clean (required before retarget; rig-c/pose#67)
|
|
28
|
+
--pose <file> rig-pose-sequence/1 as a scan wrote it, or COCO-17 with --coco
|
|
29
|
+
--out <file> the cleaned rig-pose-sequence/1, carrying the record of what was done ("cleaned")
|
|
30
|
+
--coco read --pose as coco17-sequence/1 and convert it first (neck = the shoulders' midpoint)
|
|
31
|
+
--person <id> the person to clean when the sequence holds more than one; only that person is written
|
|
32
|
+
--loop the sequence is one cycle (a gait's strike to strike): every window wraps, last frame
|
|
33
|
+
to first, and the floor is re-pinned
|
|
34
|
+
--legs <n> Savitzky–Golay window over the legs — both hips, knees and ankles (default: 5)
|
|
35
|
+
--rest <n> Savitzky–Golay window over every other joint (default: 7)
|
|
36
|
+
n is 1 (none), or odd and ≥ 5: a quadratic through 3 points passes through all three
|
|
37
|
+
--hips-inferred declare the hips inferred (rig-c/pose#63): r_hip and l_hip are marked "inferred" — a
|
|
38
|
+
position the detector supplied but did not see — on every frame where they have one, and
|
|
39
|
+
the record says so (hipsInferred: { by: "declared" }). Positions are unchanged, so retarget's
|
|
40
|
+
keys are too; its POSTURE line does not read the hip tilt ("hip tilt not read: hips inferred")
|
|
41
|
+
|
|
42
|
+
In order, each step printed with its figures: (1) spikes — a joint more than 0.2 trunk lengths
|
|
43
|
+
(neck → mid_hip, the median over the clip) from its 5-frame median takes the median, each listed by
|
|
44
|
+
frame and joint; without --loop the first and last two frames, whose window a clip end cuts, are not
|
|
45
|
+
tested and are listed as untested; (2) occlusion — a joint not "observed", or scored under 0.3, takes its 3-frame median;
|
|
46
|
+
(3) a quadratic Savitzky–Golay per joint, the window cut at the ends (wrapped with --loop); (4) with
|
|
47
|
+
--loop, the figure moved in y so the lowest ankle over the cycle is where it was before smoothing.
|
|
48
|
+
A median is per coordinate over the window's frames where the joint has a position. A joint with no
|
|
49
|
+
position on a frame is left without one (retarget holds it). States and scores are written as read.
|
|
50
|
+
Jitter: the mean |second difference| of y over every joint, px/f², before and after.
|
|
51
|
+
|
|
25
52
|
retarget
|
|
26
53
|
--rig <path> the rig spec rigc builds from: a rig.json, or a directory holding one
|
|
27
54
|
(with its motion.json beside it — @rig-c/parts writes the pair to <out>/rig/)
|
|
28
55
|
--map <map.json> rig-pose-map/1: which bones carry which joints
|
|
29
|
-
--pose <pose.json> rig-pose-sequence/1 (body-18 per frame)
|
|
56
|
+
--pose <pose.json> rig-pose-sequence/1 (body-18 per frame) that rig-pose clean wrote; a sequence without
|
|
57
|
+
its "cleaned" record is refused (POSE_NOT_CLEANED), naming the clean command to run
|
|
30
58
|
--out <motion.json> where the motion spec goes: the rig's own animations plus the new one,
|
|
31
59
|
ready for \`rigc build --rig <rig> --motion <motion.json>\`
|
|
32
60
|
--name <name> the new animation's name (default: dance); refused if the rig has one by that name
|
|
33
|
-
--coco read --pose as coco17-sequence/1
|
|
61
|
+
--coco read --pose as coco17-sequence/1: refused (POSE_NOT_CLEANED) — clean it with
|
|
62
|
+
rig-pose clean --coco, then retarget the cleaned file
|
|
34
63
|
--person <id> the person to follow when the sequence holds more than one
|
|
35
64
|
--base-motion <path> the rig's motion spec, when it is not motion.json beside rig.json
|
|
36
65
|
--images <dir> passed to rigc build: where the rig spec's images are, when its own "images" path
|
|
37
66
|
does not reach them (a rig.json copied away from its images)
|
|
38
67
|
--min-score <s> a joint scored below s is not good on that frame and holds its last good
|
|
39
68
|
value (default: 0.3); a joint with no score is good
|
|
40
|
-
--smooth <n>
|
|
41
|
-
|
|
42
|
-
points passes through all three, so 3 would change nothing
|
|
69
|
+
--smooth <n> an alias kept since rig-c/pose#67: the smoothing is clean's (--legs, --rest); retarget
|
|
70
|
+
prints "--smooth is carried by clean; ignored" and smooths nothing
|
|
43
71
|
--flip <deg>|off a segment that turns more than <deg> degrees in one frame of the pose is
|
|
44
72
|
spread over the frames around the jump, centred on it, and listed as FLIP
|
|
45
73
|
(default: off, every turn kept as the pose gives it; 90 is the measured setting)
|
|
@@ -179,12 +207,6 @@ function num(f: Map<string, string>, name: string, dflt: number, ok: (n: number)
|
|
|
179
207
|
return n;
|
|
180
208
|
}
|
|
181
209
|
|
|
182
|
-
function smoothOf(f: Map<string, string>): number {
|
|
183
|
-
const n = num(f, 'smooth', 1, (v) => Number.isInteger(v) && v >= 1 && v % 2 === 1, '1 (none) or an odd whole number ≥ 5');
|
|
184
|
-
if (n === 3) throw new Usage('--smooth is "3"; a cubic fitted through 3 points passes through all three, so a window of 3 changes nothing — it must be 1 (none) or an odd whole number ≥ 5');
|
|
185
|
-
return n;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
210
|
function flipOf(f: Map<string, string>): number | 'off' {
|
|
189
211
|
if (!f.has('flip') || f.get('flip') === 'off') return 'off';
|
|
190
212
|
return num(f, 'flip', 90, (v) => v > 0 && v < 180, 'off, or degrees in (0, 180) — a one-frame turn is never more than 180°');
|
|
@@ -226,7 +248,7 @@ function retargetArgs(argv: string[]): RetargetArgs {
|
|
|
226
248
|
baseMotion: f.get('base-motion'),
|
|
227
249
|
images: f.get('images'),
|
|
228
250
|
minScore: num(f, 'min-score', 0.3, (n) => n >= 0, 'a number ≥ 0'),
|
|
229
|
-
|
|
251
|
+
smoothAlias: f.get('smooth'),
|
|
230
252
|
flip: flipOf(f),
|
|
231
253
|
short: shortOf(f),
|
|
232
254
|
legScale: legScaleOf(f),
|
|
@@ -299,6 +321,67 @@ function compareArgs(argv: string[]): CompareArgs {
|
|
|
299
321
|
};
|
|
300
322
|
}
|
|
301
323
|
|
|
324
|
+
/** A window flag of clean: 1, or odd and ≥ 5 (`windowProblem`). */
|
|
325
|
+
function windowOf(f: Map<string, string>, name: string, dflt: number): number {
|
|
326
|
+
const v = f.get(name);
|
|
327
|
+
if (v === undefined) return dflt;
|
|
328
|
+
const n = Number(v);
|
|
329
|
+
const why = windowProblem(`--${name}`, Number.isFinite(n) ? n : NaN);
|
|
330
|
+
if (!Number.isFinite(n) || why !== null) throw new Usage(why ?? `--${name} is "${v}"; it must be 1 (none) or an odd whole number ≥ 5`);
|
|
331
|
+
return n;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const fmt = (v: number, d = 2) => v.toFixed(d);
|
|
335
|
+
function runsOf(frames: number[]): string {
|
|
336
|
+
const out: string[] = [];
|
|
337
|
+
let a = -1;
|
|
338
|
+
let b = -1;
|
|
339
|
+
for (const f of frames) {
|
|
340
|
+
if (f === b + 1) b = f;
|
|
341
|
+
else {
|
|
342
|
+
if (a >= 0) out.push(a === b ? `${a}` : `${a}–${b}`);
|
|
343
|
+
a = f;
|
|
344
|
+
b = f;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (a >= 0) out.push(a === b ? `${a}` : `${a}–${b}`);
|
|
348
|
+
return out.join(', ');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** `rig-pose clean` (#67): read, clean, print each step's figures, write. */
|
|
352
|
+
function runClean(argv: string[], say: (l: string) => void): void {
|
|
353
|
+
const f = parseFlags(argv, ['pose', 'out', 'legs', 'rest', 'person'], ['loop', 'coco', 'hips-inferred']);
|
|
354
|
+
const input = need(f, 'pose');
|
|
355
|
+
const out = need(f, 'out');
|
|
356
|
+
const params = { loop: f.has('loop'), legs: windowOf(f, 'legs', 5), rest: windowOf(f, 'rest', 7), hipsInferred: f.has('hips-inferred') };
|
|
357
|
+
const text = readFileSync(input, 'utf8');
|
|
358
|
+
const seq = f.has('coco') ? convertCoco17(text, input) : readPoseSequence(text, input);
|
|
359
|
+
const c = clean(seq, params, f.get('person'), input);
|
|
360
|
+
const n = seq.frames.length;
|
|
361
|
+
const tOf = (i: number) => `t=${seq.frames[i]!.t.toFixed(3)}s`;
|
|
362
|
+
say(`rig-pose clean: ${n} frame(s) of person "${c.person}" from ${input}${f.has('coco') ? ' (COCO-17)' : ''}; legs ${params.legs}, rest ${params.rest}, ${params.loop ? 'loop (every window wraps)' : 'no loop (windows cut at the ends)'}`);
|
|
363
|
+
say(` TRUNK neck → mid_hip: median ${fmt(c.trunk.length)} px over ${c.trunk.frames} frame(s); a spike is > ${fmt(SPIKE_TRUNKS * c.trunk.length)} px (${SPIKE_TRUNKS} trunk) from its ${SPIKE_WINDOW}-frame median`);
|
|
364
|
+
for (const s of c.spikes) say(` SPIKE joint "${s.joint}" frame ${s.frame} (${tOf(s.frame)}): ${fmt(s.off)} px from its ${SPIKE_WINDOW}-frame median, over ${fmt(SPIKE_TRUNKS * c.trunk.length)} — took the median`);
|
|
365
|
+
say(` SPIKES ${c.spikes.length} replaced; ${untestedText(c.untested)}`);
|
|
366
|
+
const byJoint = new Map<string, typeof c.occluded>();
|
|
367
|
+
for (const o of c.occluded) byJoint.set(o.joint, [...(byJoint.get(o.joint) ?? []), o]);
|
|
368
|
+
for (const [joint, os] of byJoint) say(` OCCLUDED joint "${joint}" on ${os.length} frame(s) (${runsOf(os.map((o) => o.frame))}): not observed or scored under ${OCCLUSION_SCORE}; took its ${OCCLUSION_WINDOW}-frame median, moved up to ${fmt(Math.max(...os.map((o) => o.moved)))} px`);
|
|
369
|
+
say(` OCCLUSION ${c.occluded.length} joint-frame(s) took the median`);
|
|
370
|
+
for (const m of c.missing) say(` MISSING joint "${m.joint}" frame${m.from === m.to ? ` ${m.from}` : `s ${m.from}–${m.to}`}: no position; left without one (retarget holds it)`);
|
|
371
|
+
const sg = (w: number) => (w === 1 ? '1 frame (none)' : `${w} frames`);
|
|
372
|
+
say(` SMOOTH quadratic Savitzky–Golay per joint: legs (${LEGS.join(', ')}) over ${sg(params.legs)}, the rest over ${sg(params.rest)}; ${params.loop ? 'circular' : 'the window cut at the ends'}`);
|
|
373
|
+
if (params.loop) {
|
|
374
|
+
if (c.floor === null) say(' FLOOR no ankle has a position on any frame; not re-pinned');
|
|
375
|
+
else say(` FLOOR the lowest ankle over the cycle: y ${fmt(c.floor.before)} before smoothing, ${fmt(c.floor.after)} after; every joint moved ${c.floor.shift >= 0 ? '+' : ''}${fmt(c.floor.shift, 3)} px in y`);
|
|
376
|
+
}
|
|
377
|
+
const jit = (v: number | null) => (v === null ? 'not measured' : `${fmt(v, 3)}`);
|
|
378
|
+
say(` JITTER mean |second difference| of y over ${c.triples} joint-frame triple(s): ${jit(c.record.jitter.before)} → ${jit(c.record.jitter.after)} px/f²${c.record.jitter.before !== null && c.record.jitter.after !== null && c.record.jitter.before > 0 ? ` (${fmt(100 * (1 - c.record.jitter.after / c.record.jitter.before), 1)}% lower)` : ''}`);
|
|
379
|
+
if (c.inferred !== null) say(` INFERRED r_hip on ${c.inferred.r_hip} frame(s), l_hip on ${c.inferred.l_hip} frame(s) of ${n} marked "inferred" (every frame where it has a position): declared at clean (--hips-inferred); positions and scores unchanged`);
|
|
380
|
+
writeFileSync(out, writePoseSequence(c.seq));
|
|
381
|
+
say(` CLEAN ${cleanLine(c.record)}`);
|
|
382
|
+
say(` wrote ${out}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
302
385
|
export function main(argv: string[]): number {
|
|
303
386
|
const [cmd, ...rest] = argv;
|
|
304
387
|
if (cmd === undefined || cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
@@ -322,6 +405,10 @@ export function main(argv: string[]): number {
|
|
|
322
405
|
runCompare(compareArgs(rest), (l) => process.stdout.write(`${l}\n`));
|
|
323
406
|
return 0;
|
|
324
407
|
}
|
|
408
|
+
if (cmd === 'clean') {
|
|
409
|
+
runClean(rest, (l) => process.stdout.write(`${l}\n`));
|
|
410
|
+
return 0;
|
|
411
|
+
}
|
|
325
412
|
if (cmd === 'convert') {
|
|
326
413
|
const f = parseFlags(rest, ['coco', 'out'], []);
|
|
327
414
|
const input = need(f, 'coco');
|
|
@@ -331,7 +418,7 @@ export function main(argv: string[]): number {
|
|
|
331
418
|
process.stdout.write(`rig-pose convert: ${seq.frames.length} frame(s) of COCO-17 → body-18; wrote ${out}\n`);
|
|
332
419
|
return 0;
|
|
333
420
|
}
|
|
334
|
-
process.stderr.write(`rig-pose: unknown command "${cmd}" — the commands are: retarget, convert, compare\n`);
|
|
421
|
+
process.stderr.write(`rig-pose: unknown command "${cmd}" — the commands are: clean, retarget, convert, compare\n`);
|
|
335
422
|
return 2;
|
|
336
423
|
} catch (e) {
|
|
337
424
|
if (e instanceof Usage) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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": {
|