@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 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 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).
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. A first implementation also held the lines across the body. It listed the eye line (head in profile) and the hip line (body side-on) as end-on, and holding the hip line narrowed the rig's hip tilt range to 39.3° against the pose's 125.2°. The cutoff was never measured on those lines, so they are excluded; with them out the rig reads 125.2° again. The default stays off: a default needs its own criterion and a third sequence.
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.0",
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 limb segment the pose aims directly (not a reach, and
614
- // not a line across the body from a left joint to its right twin — the hip line,
615
- // the eye line — whose shortening is the body or head turning, not a limb seen
616
- // end-on; #22's cutoff was measured on limb segments only): its picture length
617
- // against its own median, on the points the angle was read from.
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
- if (isAcross(joints[0], joints[1])) return;
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
- 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`);
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)}`);