@rig-c/pose 0.24.0 → 0.24.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 +2 -2
- package/package.json +1 -1
- package/src/retarget.ts +104 -11
- package/src/run.ts +1 -1
package/README.md
CHANGED
|
@@ -182,7 +182,7 @@ The threshold was fixed by measuring first ([#15](https://github.com/rig-c/pose/
|
|
|
182
182
|
|
|
183
183
|
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.
|
|
184
184
|
|
|
185
|
-
**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
|
|
185
|
+
**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 mapped segment is measured on every frame (not a `reach`) — 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. A horizontal line across the body from a left joint to its right twin (`a>b`, such as the hip line or the eye line) also holds its last confident angle and interpolates across the span whenever its picture length falls under `--short` or its 2D reading reverses sign within one frame past `--flip` (when the left and right joints cross in 2D during a full side turn, [#184](https://github.com/rig-c/pose/issues/184)); its hold is printed as `HOLD chain "..."` and is not counted toward limb `END-ON` runs. The flip guard, the hinge check and the keys all see the held angle. `--short off` (the default) reads every frame unless an across-body line reverses past `--flip`. The report lists each span under `short` (chain, bone, segment, first and last frame, the lowest fraction, and `across` / `reversed` when across-body).
|
|
186
186
|
|
|
187
187
|
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:
|
|
188
188
|
|
|
@@ -191,7 +191,7 @@ The cutoff was fixed by measuring first ([#22](https://github.com/rig-c/pose/iss
|
|
|
191
191
|
|
|
192
192
|
Both have a meaningless direction; only the first is a pose the rig cannot draw. **0.35** is the smallest round value that takes in every end-on frame (the highest is 0.35, on the dance) without reaching normal motion (the 5th percentiles are 0.75 and 0.82). A run under the cutoff — spans merged across a gap of one frame — lasting **≥ 0.15 s** (3.6 frames at 24 fps, 4.5 at 30) is the end-on kind: it excludes every blur collapse above (at most 3 frames, 0.10 s at 30 fps) and keeps every end-on span (6–8 frames). The run prints it as `END-ON` and the report lists it under `endOn` (chain, bone, segment, frames, seconds, the lowest fraction): it is the list of moments that need an alternate drawing ([#6](https://github.com/rig-c/pose/issues/6)).
|
|
193
193
|
|
|
194
|
-
Re-checked on the exam01 test rig with `--short 0.35 --flip 90 --bends clamp`. On the second sequence the three spreads the flip guard made out of end-on segments are SHORT holds now, not FLIPs: right upper arm 399–400, right thigh 368–369, right shin 368–371. Of the flip guard's other spreads only one is left, the left forearm at 322–323; every other one fell on frames under the cutoff and is a SHORT hold instead. No guarded bone of the gated build turns more than 81.4° in one frame (85.1° with `--flip 90` alone), and every bone lands. `endOn` there lists the right upper arm (395–401) and the right shin (365–370). It does not list the right thigh, which is under 0.35 on one frame only (369, 0.16). The plan before the run expected the thigh too, and that expectation was wrong. On the dance `endOn` lists exactly S2's left upper arm (30–37); the three FLIPs are #15's, the largest one-frame turn is 78.2°, and every bone lands.
|
|
194
|
+
Re-checked on the exam01 test rig with `--short 0.35 --flip 90 --bends clamp`. On the second sequence the three spreads the flip guard made out of end-on segments are SHORT holds now, not FLIPs: right upper arm 399–400, right thigh 368–369, right shin 368–371. Of the flip guard's other spreads only one is left, the left forearm at 322–323; every other one fell on frames under the cutoff and is a SHORT hold instead. No guarded bone of the gated build turns more than 81.4° in one frame (85.1° with `--flip 90` alone), and every bone lands. `endOn` there lists the right upper arm (395–401) and the right shin (365–370). It does not list the right thigh, which is under 0.35 on one frame only (369, 0.16). The plan before the run expected the thigh too, and that expectation was wrong. On the dance `endOn` lists exactly S2's left upper arm (30–37); the three FLIPs are #15's, the largest one-frame turn is 78.2°, and every bone lands. When the second sequence's pelvis is mapped (`r_hip → l_hip`), its side-on turn (frames 463–491, body yaw +79.5°) foreshortens the hip line to 0.17 of standing and crosses `r_hip.x` past `l_hip.x` on frames 482–484 (+177.4° / −179.4°); `holdAcross` ([#184](https://github.com/rig-c/pose/issues/184)) holds `hip.rotate` across the unconfident span (`[−2.2°, +2.6°]` over 463–491, `0` `FLIP`s) without listing horizontal lines as limb `END-ON`. The default stays off: a default needs its own criterion and a third sequence.
|
|
195
195
|
|
|
196
196
|
### The legs: foreshortening and planted feet
|
|
197
197
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/pose",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.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/retarget.ts
CHANGED
|
@@ -182,6 +182,10 @@ export interface Short {
|
|
|
182
182
|
/** Present when a foreshortened segment (< `SHORT_STEP_FRACTION`) had its per-frame step capped (#110): the one-frame step after capping and the largest step asked, degrees, signed. */
|
|
183
183
|
step?: number;
|
|
184
184
|
asked?: number;
|
|
185
|
+
/** Present when a horizontal chain across the body (`isAcross`) held its last confident angle (#184). */
|
|
186
|
+
across?: boolean;
|
|
187
|
+
/** Largest one-frame turn (degrees, signed) inside a held horizontal-chain span when it reversed beyond `--flip` (#184). */
|
|
188
|
+
reversed?: number;
|
|
185
189
|
}
|
|
186
190
|
|
|
187
191
|
/** A limb segment shorter than this fraction of its median picture length has its per-frame angle step capped at `SHORT_STEP_CAP` degrees (rig-c/pose#110). */
|
|
@@ -610,30 +614,40 @@ export function retarget(rest: Rest, map: JointMap, frames: ReadonlyArray<Partia
|
|
|
610
614
|
}
|
|
611
615
|
p.throwIfAny();
|
|
612
616
|
|
|
613
|
-
// The short hold, on every
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
//
|
|
617
|
+
// The short hold, on every segment the pose aims directly (not a reach): its
|
|
618
|
+
// picture length against its own median, on the points the angle was read from.
|
|
619
|
+
// A horizontal line across the body (`isAcross`: pelvis, shoulders, head line)
|
|
620
|
+
// holds and interpolates across spans under `--short` or reversed within a frame
|
|
621
|
+
// beyond `--flip` (#184), without listing the run as `endOn` (`TURN` reads turned
|
|
622
|
+
// body/head lines).
|
|
618
623
|
const short: Short[] = [];
|
|
619
624
|
const endOn: EndOn[] = [];
|
|
620
625
|
const fractions = new Map<string, { chain: string; joints: [string, string]; fraction: number[] }>();
|
|
621
|
-
if (opts.short !== 'off') {
|
|
622
|
-
const cutoff = opts.short;
|
|
626
|
+
if (opts.short !== 'off' || opts.flip !== 'off') {
|
|
623
627
|
for (const c of map.chains) {
|
|
624
628
|
if (c.reach) continue;
|
|
625
629
|
c.bones.forEach((b, k) => {
|
|
626
630
|
const joints: [string, string] = [c.joints[k]!, c.joints[k + 1]!];
|
|
627
|
-
|
|
631
|
+
const across = isAcross(joints[0], joints[1]);
|
|
632
|
+
if (!across && opts.short === 'off') return;
|
|
628
633
|
const from = series.get(joints[0])!;
|
|
629
634
|
const to = series.get(joints[1])!;
|
|
630
635
|
const lengths = from.map((f, i) => Math.hypot(to[i]![0] - f[0], to[i]![1] - f[1]));
|
|
631
636
|
const med = median(lengths);
|
|
632
637
|
if (med < 1e-6) {
|
|
633
|
-
|
|
638
|
+
if (opts.short !== 'off') {
|
|
639
|
+
p.add('POSE_SHORT_MEDIAN', `chain "${c.name}" — bone "${b}"'s segment ${joints[0]} → ${joints[1]} has median picture length ${med}px over the ${n} frames; --short measures a frame's length against that median, and there is none to measure against`);
|
|
640
|
+
}
|
|
634
641
|
return;
|
|
635
642
|
}
|
|
636
643
|
const fraction = lengths.map((l) => l / med);
|
|
644
|
+
if (across) {
|
|
645
|
+
const restDir = joints[0].startsWith('r_') ? 0 : 180;
|
|
646
|
+
const spans = holdAcross(targets.get(b)!, fraction, opts.short, opts.flip, restDir);
|
|
647
|
+
for (const s of spans) short.push({ chain: c.name, bone: b, joints, ...s });
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const cutoff = opts.short as number;
|
|
637
651
|
fractions.set(b, { chain: c.name, joints, fraction });
|
|
638
652
|
const spans = holdShort(targets.get(b)!, fraction, cutoff);
|
|
639
653
|
for (const s of spans) short.push({ chain: c.name, bone: b, joints, ...s });
|
|
@@ -1912,6 +1926,79 @@ export function holdShort(angles: number[], fraction: readonly number[], cutoff:
|
|
|
1912
1926
|
return out;
|
|
1913
1927
|
}
|
|
1914
1928
|
|
|
1929
|
+
/**
|
|
1930
|
+
* Hold and interpolate across unconfident spans on a horizontal chain (`isAcross`,
|
|
1931
|
+
* e.g. `r_hip → l_hip`, `r_eye → l_eye`, `r_shoulder → l_shoulder`, rig-c/pose#184),
|
|
1932
|
+
* in place. A frame is unconfident when its picture length over its median is under
|
|
1933
|
+
* `shortCutoff` (with `--short`) or when its angle turns more than `flipThreshold`
|
|
1934
|
+
* from the last confident angle or reverses more than 90° from the line's rest
|
|
1935
|
+
* direction `restDir` (with `--flip`). Each contiguous unconfident run has its
|
|
1936
|
+
* angles interpolated between the confident frames either side (flat at the ends),
|
|
1937
|
+
* the short way round, exactly as `holdShort` does on limb chains.
|
|
1938
|
+
*/
|
|
1939
|
+
export function holdAcross(
|
|
1940
|
+
angles: number[],
|
|
1941
|
+
fraction: readonly number[],
|
|
1942
|
+
shortCutoff: number | 'off',
|
|
1943
|
+
flipThreshold: number | 'off',
|
|
1944
|
+
restDir = 0,
|
|
1945
|
+
): ShortSpan[] {
|
|
1946
|
+
const n = angles.length;
|
|
1947
|
+
if (n === 0 || (shortCutoff === 'off' && flipThreshold === 'off')) return [];
|
|
1948
|
+
const raw = [...angles];
|
|
1949
|
+
const unconf = new Array<boolean>(n).fill(false);
|
|
1950
|
+
let lastConfident = raw[0]!;
|
|
1951
|
+
for (let i = 0; i < n; i++) {
|
|
1952
|
+
const s = shortCutoff !== 'off' && fraction[i]! < shortCutoff;
|
|
1953
|
+
const rev = Math.abs(wrap(raw[i]! - restDir)) > 90;
|
|
1954
|
+
if (!s && !rev) {
|
|
1955
|
+
lastConfident = raw[i]!;
|
|
1956
|
+
break;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
for (let i = 0; i < n; i++) {
|
|
1960
|
+
const isShort = shortCutoff !== 'off' && fraction[i]! < shortCutoff;
|
|
1961
|
+
const isFlipped =
|
|
1962
|
+
flipThreshold !== 'off' &&
|
|
1963
|
+
(Math.abs(wrap(raw[i]! - lastConfident)) > flipThreshold || Math.abs(wrap(raw[i]! - restDir)) > 90);
|
|
1964
|
+
if (isShort || isFlipped) {
|
|
1965
|
+
unconf[i] = true;
|
|
1966
|
+
} else {
|
|
1967
|
+
lastConfident = raw[i]!;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
const out: ShortSpan[] = [];
|
|
1971
|
+
for (let i = 0; i < n; i++) {
|
|
1972
|
+
if (!unconf[i]) continue;
|
|
1973
|
+
let j = i;
|
|
1974
|
+
while (j + 1 < n && unconf[j + 1]) j++;
|
|
1975
|
+
const lo = i - 1;
|
|
1976
|
+
const hi = j + 1;
|
|
1977
|
+
let minFraction = fraction[i]!;
|
|
1978
|
+
for (let k = i + 1; k <= j; k++) minFraction = Math.min(minFraction, fraction[k]!);
|
|
1979
|
+
let maxTurn = 0;
|
|
1980
|
+
for (let k = Math.max(1, i); k <= Math.min(n - 1, j + 1); k++) {
|
|
1981
|
+
const jmp = wrap(raw[k]! - raw[k - 1]!);
|
|
1982
|
+
if (Math.abs(jmp) > Math.abs(maxTurn)) maxTurn = jmp;
|
|
1983
|
+
}
|
|
1984
|
+
if (lo >= 0 || hi < n) {
|
|
1985
|
+
const a = lo >= 0 ? angles[lo]! : angles[hi]!;
|
|
1986
|
+
const b = hi < n ? angles[hi]! : a;
|
|
1987
|
+
const turn = wrap(b - a);
|
|
1988
|
+
for (let k = i; k <= j; k++) angles[k] = lo < 0 ? b : wrap(a + (turn * (k - lo)) / (hi - lo));
|
|
1989
|
+
}
|
|
1990
|
+
const revThreshold = flipThreshold !== 'off' ? flipThreshold : 90;
|
|
1991
|
+
out.push({
|
|
1992
|
+
frames: [i, j],
|
|
1993
|
+
minFraction,
|
|
1994
|
+
across: true,
|
|
1995
|
+
...(Math.abs(maxTurn) > revThreshold ? { reversed: r3(maxTurn) } : {}),
|
|
1996
|
+
});
|
|
1997
|
+
i = j;
|
|
1998
|
+
}
|
|
1999
|
+
return out;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
1915
2002
|
/** Short spans `gap` frames apart or fewer, as one run each: its first and last frame, and the lowest fraction. */
|
|
1916
2003
|
export function mergeRuns(spans: readonly ShortSpan[], gap: number): ShortSpan[] {
|
|
1917
2004
|
const out: ShortSpan[] = [];
|
|
@@ -2011,9 +2098,9 @@ export function capShortSteps(
|
|
|
2011
2098
|
const spanOf = (frames: [number, number], times: readonly number[]): string =>
|
|
2012
2099
|
frames[0] === frames[1] ? `frame ${frames[0]} (t=${times[frames[0]]!.toFixed(3)}s)` : `frames ${frames[0]}–${frames[1]} (t=${times[frames[0]]!.toFixed(3)}–${times[frames[1]]!.toFixed(3)}s)`;
|
|
2013
2100
|
|
|
2014
|
-
export function shortLine(s: Short, times: readonly number[], cutoff: number): string {
|
|
2101
|
+
export function shortLine(s: Short, times: readonly number[], cutoff: number | 'off'): string {
|
|
2102
|
+
const sg = (v: number) => `${v >= 0 ? '+' : '−'}${Math.abs(v).toFixed(1)}°`;
|
|
2015
2103
|
if (s.step !== undefined && s.asked !== undefined) {
|
|
2016
|
-
const sg = (v: number) => `${v >= 0 ? '+' : '−'}${Math.abs(v).toFixed(1)}°`;
|
|
2017
2104
|
return `chain "${s.chain}" bone "${s.bone}" (${s.joints[0]} → ${s.joints[1]}) ${spanOf(s.frames, times)}: picture length down to ${s.minFraction.toFixed(2)} of its median, under ${SHORT_STEP_FRACTION.toFixed(2)}, turned ${sg(s.asked)} in one frame (over ${SHORT_STEP_CAP}°); spread to ${sg(s.step)} per frame`;
|
|
2018
2105
|
}
|
|
2019
2106
|
const lo = s.frames[0] - 1;
|
|
@@ -2023,6 +2110,12 @@ export function shortLine(s: Short, times: readonly number[], cutoff: number): s
|
|
|
2023
2110
|
: lo < 0 ? `held flat at frame ${hi}'s angle (the pose starts short)`
|
|
2024
2111
|
: hi >= times.length ? `held flat at frame ${lo}'s angle (the pose ends short)`
|
|
2025
2112
|
: `interpolated from frame ${lo}'s angle to frame ${hi}'s, the short way round`;
|
|
2113
|
+
if (s.across === true) {
|
|
2114
|
+
const shortWhy = cutoff !== 'off' && s.minFraction < cutoff ? `picture length down to ${s.minFraction.toFixed(2)} of its median, under ${cutoff}` : null;
|
|
2115
|
+
const revWhy = s.reversed !== undefined ? `reversed ${sg(s.reversed)} in one frame` : null;
|
|
2116
|
+
const why = shortWhy !== null && revWhy !== null ? `${shortWhy}, ${revWhy}` : shortWhy ?? `${revWhy ?? 'reversed'} (picture length down to ${s.minFraction.toFixed(2)} of its median)`;
|
|
2117
|
+
return `chain "${s.chain}" bone "${s.bone}" (${s.joints[0]} → ${s.joints[1]}) ${spanOf(s.frames, times)}: ${why}; its direction is not read — ${held}`;
|
|
2118
|
+
}
|
|
2026
2119
|
return `chain "${s.chain}" bone "${s.bone}" (${s.joints[0]} → ${s.joints[1]}) ${spanOf(s.frames, times)}: picture length down to ${s.minFraction.toFixed(2)} of its median, under ${cutoff}; its direction is not read — ${held}`;
|
|
2027
2120
|
}
|
|
2028
2121
|
|
package/src/run.ts
CHANGED
|
@@ -135,8 +135,8 @@ export function runRetarget(a: RetargetArgs, log: (line: string) => void): RunRe
|
|
|
135
135
|
else if (a.plantFeet === true) say(' CAMERA none in the sequence: --plant-feet reads the picture as a fixed camera, so a zoom or a pan in the video moves the floor with it and the floor may step with the picture (tools/scan.py reads the camera, rig-c/pose#63)');
|
|
136
136
|
if (r.scale !== null) say(` SCALE ${r.scale.factor.toFixed(5)} rig units per picture px (rig ${r.scale.rig.toFixed(2)} / pose median ${r.scale.pose.toFixed(2)})`);
|
|
137
137
|
for (const h of r.holds) say(` HOLD ${holdLine(h, r.times)}`);
|
|
138
|
+
for (const s of r.short) say(` ${s.across === true ? 'HOLD ' : 'SHORT '} ${shortLine(s, r.times, a.short)}`);
|
|
138
139
|
if (a.short !== 'off') {
|
|
139
|
-
for (const s of r.short) say(` SHORT ${shortLine(s, r.times, a.short)}`);
|
|
140
140
|
for (const e of r.endOn) say(` END-ON ${endOnLine(e, r.times, a.short)}`);
|
|
141
141
|
}
|
|
142
142
|
if (a.flip !== 'off') for (const fl of r.flips) say(` FLIP ${flipLine(fl, r.times, a.flip)}`);
|