@rig-c/pose 0.26.0 → 0.27.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 +1 -1
- package/package.json +1 -1
- package/src/face.ts +51 -2
package/README.md
CHANGED
|
@@ -315,7 +315,7 @@ Which bones carry which joints. You write it for your rig:
|
|
|
315
315
|
⚠️ **Seen from the front, an elbow's side is not a joint limit.** An elbow flexes toward the camera, and its picture falls on either side of the upper arm depending on how the arm is turned. On a real 13 s front-facing dance (313 frames, RTMPose), the right elbow sat on the "wrong" side of the rig's rest on 165 frames (median 23°), and the left on 200 (median 29°, up to 179° with the hands behind the head). Every one was a real elbow bend. The knees strayed at most 8.8° and 14.7°, which is pose noise around a straight leg. So for front-facing material, arms go `"free"`. Legs on `"rest"` with `--bends clamp` hold only while the knees stay near straight. On a second dance with knee bends (Toca Toca, rig-c/pose#27), that setting straightened the left knee for most of the dance: `leg_l1` was 48.3° off at p90, against 0.0–0.3° with free legs. Leave legs `"free"` unless the dance keeps its knees straight.
|
|
316
316
|
- `mid_hip` and `mid_shoulder` are the midpoints of the two hips and the two shoulders.
|
|
317
317
|
- `translate.scale` is one length measured on both: two points on the rig, two points in the pose (its median over the frames where both are good). Pick a long segment that does not foreshorten much.
|
|
318
|
-
- `face` (optional, [#138](https://github.com/rig-c/pose/issues/138), [#164](https://github.com/rig-c/pose/issues/164), [#173](https://github.com/rig-c/pose/issues/173)): `{ "yaw"?: { "bone": "yaw", "range": [-15, 15], "sets"?: [{ "set": "T1", "above": 45 }] }, "pitch"?: { "bone": "pitch", "range": [-10, 15] }, "sets"?: [{ "set": "T1", "above": 45 }] }` (`min ≤ 0 ≤ max`, `min < max`). Reads per-frame yaw and pitch from `nose` against `r_eye + l_eye` (and `r_ear + l_ear` when present), maps 1:1 inside `0.8 × range` (`[-12, 12]°` on `[-15, 15]°`) and compresses only the overshoot above the knee into the remaining 20% of the range so a single outlier frame does not crush the rest of the clip, and keys the named dial bone's `rotate` track (`FACE`). When `sets` (`[{ "set": "<name>", "above": <deg> }]`, `0 < above ≤ 90`) is declared on `face.yaw.sets` or `face.sets`, each set enters on frames where `|yaw| ≥ above` (or the highest `above` satisfied when multiple sets are declared) and leaves only when `|yaw| < above − 10°` (`10°` hysteresis), emitting stepped slot `attachment` keys for the set's `slots` and `hides` (resolved from `alternates` in the map or `sets.json` (`rig-parts-sets/1`) beside `rig.json`; an unknown set is refused by name as `MAP_SET_EXISTS`).
|
|
318
|
+
- `face` (optional, [#138](https://github.com/rig-c/pose/issues/138), [#164](https://github.com/rig-c/pose/issues/164), [#173](https://github.com/rig-c/pose/issues/173), [#189](https://github.com/rig-c/pose/issues/189)): `{ "yaw"?: { "bone": "yaw", "range": [-15, 15], "sets"?: [{ "set": "T1", "above": 45 }] }, "pitch"?: { "bone": "pitch", "range": [-10, 15] }, "sets"?: [{ "set": "T1", "above": 45 }] }` (`min ≤ 0 ≤ max`, `min < max`). Reads per-frame yaw and pitch from `nose` against `r_eye + l_eye` (and `r_ear + l_ear` when present — where the eye span is foreshortened below `0.85 × median(de)` and the ear-chord offset `xea = (mid_ears − mid_eyes) · u / da` exceeds both `1.8 × |xne|` and `0.15`, yaw blends toward the ear chord `atan(2 × xea)` with weight `w = clamp((0.85 × median(de) − de) / ((0.85 − 0.80) × median(de)), 0, 1)` so a side gaze whose 2D nose sits near the eye midpoint reaches `atan(2 × xea)` below `0.80 × median(de)` and exits without a step), maps 1:1 inside `0.8 × range` (`[-12, 12]°` on `[-15, 15]°`) and compresses only the overshoot above the knee into the remaining 20% of the range so a single outlier frame does not crush the rest of the clip, and keys the named dial bone's `rotate` track (`FACE`). When `sets` (`[{ "set": "<name>", "above": <deg> }]`, `0 < above ≤ 90`) is declared on `face.yaw.sets` or `face.sets`, each set enters on frames where `|yaw| ≥ above` (or the highest `above` satisfied when multiple sets are declared) and leaves only when `|yaw| < above − 10°` (`10°` hysteresis), emitting stepped slot `attachment` keys for the set's `slots` and `hides` (resolved from `alternates` in the map or `sets.json` (`rig-parts-sets/1`) beside `rig.json`; an unknown set is refused by name as `MAP_SET_EXISTS`).
|
|
319
319
|
- `body` (optional, [#173](https://github.com/rig-c/pose/issues/173)): `{ "yaw"?: { "bone": "body_yaw", "range": [-30, 30], "sets"?: [{ "set": "T8", "above": 45 }] }, "sets"?: [{ "set": "T8", "above": 45 }] }` (`min ≤ 0 ≤ max`, `min < max`). Maps the per-frame body yaw read from the camera-normalised shoulder and hip width (`BODY`) into `range` with the same `0.8 × range` knee mapping as `face`, keying the named dial bone's `rotate` track, and switches any declared `sets` (`body.sets` or `body.yaw.sets`) with the same `above` / `above − 10°` hysteresis (overriding `face` sets and arm `alternates` on slots they share while active). Without `body` in the map, `BODY` and `--report`'s `body` still report the reading while leaving every track in `motion.json` unchanged.
|
|
320
320
|
|
|
321
321
|
### A joint's range: `range`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Puts motion from a body-18 pose sequence onto a rig built with rig-c: bone keys in rig-c's motion format, checked through rig-c's gate before anything is written. A CLI for agents that cannot see the rig.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/face.ts
CHANGED
|
@@ -123,6 +123,37 @@ function corrOf(a: readonly number[], b: readonly number[]): number {
|
|
|
123
123
|
return den > 1e-9 ? r3(num / den) : 1;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Eye-span foreshortening entry threshold `de < EAR_CHORD_K_DE * median(de)` (`rig-c/pose#189`):
|
|
128
|
+
* derived from $\cos(31.79^\circ) = 0.85$ ($\approx 32^\circ$ head turn), the foreshortening angle
|
|
129
|
+
* beyond which the near-planar 2D nose point begins to sit trapped between the foreshortened eyes.
|
|
130
|
+
*/
|
|
131
|
+
export const EAR_CHORD_K_DE = 0.85;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Eye-span foreshortening full-weight threshold `de <= EAR_CHORD_K_LO * median(de)` (`rig-c/pose#189`):
|
|
135
|
+
* derived from $\cos(36.87^\circ) = 0.80$ ($4/5$, $\approx 37^\circ$ head turn), where the foreshortening
|
|
136
|
+
* blend weight `w = clamp((EAR_CHORD_K_DE * de0 - de) / ((EAR_CHORD_K_DE - EAR_CHORD_K_LO) * de0), 0, 1)`
|
|
137
|
+
* reaches `1.0` so the exit from a foreshortened side gaze ramps smoothly instead of snapping at `EAR_CHORD_K_DE`.
|
|
138
|
+
*/
|
|
139
|
+
export const EAR_CHORD_K_LO = 0.8;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Ear-chord to nose-cue disagreement ratio `|xea| > EAR_CHORD_K_RATIO * |xne|` (`rig-c/pose#189`):
|
|
143
|
+
* derived from the 95th-percentile of `|xea| / |xne|` on confident frames (`|de - median(de)| <= 15%`)
|
|
144
|
+
* above `EAR_CHORD_K_MIN`, measured at `p95 = 1.7352` on Dancer (`37` frames) and `p95 = 1.1937`
|
|
145
|
+
* (`p100 = 1.2867`) on Toca (`58` frames) -> `1.8`.
|
|
146
|
+
*/
|
|
147
|
+
export const EAR_CHORD_K_RATIO = 1.8;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Minimum ear-to-eye-midpoint offset `|xea| > EAR_CHORD_K_MIN` (`rig-c/pose#189`):
|
|
151
|
+
* derived from the 100th-percentile (maximum) detector ear-placement noise `|xea - xne|` on
|
|
152
|
+
* confident frames (`|de - median(de)| <= 15%`), measured at `p99 = 0.0949, p100 = 0.1094` on Dancer
|
|
153
|
+
* (`231` frames) and `p99 = 0.1178, p100 = 0.1450` on Toca (`373` frames) -> `0.15` (`16.70°`).
|
|
154
|
+
*/
|
|
155
|
+
export const EAR_CHORD_K_MIN = 0.15;
|
|
156
|
+
|
|
126
157
|
/**
|
|
127
158
|
* Read per-frame yaw and pitch (degrees) from `nose`, `r_eye`, `l_eye`, and
|
|
128
159
|
* (when both are present) `r_ear` and `l_ear`.
|
|
@@ -140,8 +171,10 @@ export function readFaceAngles(
|
|
|
140
171
|
): { ears: boolean; yaw: number[]; pitch: number[] } {
|
|
141
172
|
const n = nose.length;
|
|
142
173
|
const ears = rEar !== null && lEar !== null && rEar.length === n && lEar.length === n;
|
|
174
|
+
const des: number[] = [];
|
|
143
175
|
const xne: number[] = [];
|
|
144
176
|
const xna: number[] = [];
|
|
177
|
+
const xea: number[] = [];
|
|
145
178
|
const yne: number[] = [];
|
|
146
179
|
const yna: number[] = [];
|
|
147
180
|
|
|
@@ -153,14 +186,17 @@ export function readFaceAngles(
|
|
|
153
186
|
const ey = El[1] - Er[1];
|
|
154
187
|
const de = Math.hypot(ex, ey);
|
|
155
188
|
if (!(de > 1e-9)) {
|
|
189
|
+
des.push(des[i - 1] ?? 0);
|
|
156
190
|
xne.push(xne[i - 1] ?? 0);
|
|
157
191
|
yne.push(yne[i - 1] ?? 0);
|
|
158
192
|
if (ears) {
|
|
159
193
|
xna.push(xna[i - 1] ?? 0);
|
|
194
|
+
xea.push(xea[i - 1] ?? 0);
|
|
160
195
|
yna.push(yna[i - 1] ?? 0);
|
|
161
196
|
}
|
|
162
197
|
continue;
|
|
163
198
|
}
|
|
199
|
+
des.push(de);
|
|
164
200
|
const ux = ex / de;
|
|
165
201
|
const uy = ey / de;
|
|
166
202
|
const vx = -uy;
|
|
@@ -176,18 +212,31 @@ export function readFaceAngles(
|
|
|
176
212
|
const may = 0.5 * (Ar[1] + Al[1]);
|
|
177
213
|
const da = Math.max(de, (Al[0] - Ar[0]) * ux + (Al[1] - Ar[1]) * uy);
|
|
178
214
|
xna.push(((max - N[0]) * ux + (may - N[1]) * uy) / da);
|
|
215
|
+
xea.push(((max - mex) * ux + (may - mey) * uy) / da);
|
|
179
216
|
yna.push(((N[0] - max) * vx + (N[1] - may) * vy) / de);
|
|
180
217
|
}
|
|
181
218
|
}
|
|
182
219
|
|
|
220
|
+
const de0 = n > 0 ? median(des) : 0;
|
|
183
221
|
const yne0 = median(yne);
|
|
184
222
|
const yna0 = ears ? median(yna) : 0;
|
|
185
223
|
const yaw: number[] = [];
|
|
186
224
|
const pitch: number[] = [];
|
|
187
225
|
for (let i = 0; i < n; i++) {
|
|
188
|
-
const
|
|
226
|
+
const earChord =
|
|
227
|
+
ears &&
|
|
228
|
+
de0 > 1e-9 &&
|
|
229
|
+
des[i]! < EAR_CHORD_K_DE * de0 &&
|
|
230
|
+
Math.abs(xea[i]!) > EAR_CHORD_K_RATIO * Math.abs(xne[i]!) &&
|
|
231
|
+
Math.abs(xea[i]!) > EAR_CHORD_K_MIN;
|
|
232
|
+
const w = earChord
|
|
233
|
+
? EAR_CHORD_K_DE > EAR_CHORD_K_LO
|
|
234
|
+
? Math.max(0, Math.min(1, (EAR_CHORD_K_DE * de0 - des[i]!) / ((EAR_CHORD_K_DE - EAR_CHORD_K_LO) * de0)))
|
|
235
|
+
: 1
|
|
236
|
+
: 0;
|
|
237
|
+
const rawYaw = Math.atan(ears ? (2 * xne[i]! + xna[i]!) / 2 : 2 * xne[i]!) * DEG;
|
|
189
238
|
const tp = ears ? (yne[i]! - yne0 + (yna[i]! - yna0)) / 2 : yne[i]! - yne0;
|
|
190
|
-
yaw.push(Math.atan(
|
|
239
|
+
yaw.push(w > 0 ? w * (Math.atan(2 * xea[i]!) * DEG) + (1 - w) * rawYaw : rawYaw);
|
|
191
240
|
pitch.push(Math.atan(tp) * DEG);
|
|
192
241
|
}
|
|
193
242
|
return { ears, yaw, pitch };
|