@rig-c/pose 0.37.1 → 0.39.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 +73 -6
- package/cli.ts +73 -6
- package/package.json +1 -1
- package/src/clean.ts +72 -3
- package/src/depth.ts +190 -0
- package/src/legs.ts +189 -0
- package/src/map.ts +24 -3
- package/src/outdir.ts +54 -0
- package/src/pelvis.ts +498 -0
- package/src/pose.ts +258 -23
- package/src/problems.ts +6 -3
- package/src/retarget.ts +98 -3
- package/src/run.ts +80 -21
- package/src/runcompare.ts +6 -7
- package/src/switch.ts +109 -21
- package/src/verify.ts +90 -9
package/src/depth.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derived depth (rig-c/pose#281): which way each limb segment leans out of the
|
|
3
|
+
* picture, read off its 2D length, and the limbs' front-to-back order from it.
|
|
4
|
+
*
|
|
5
|
+
* A sequence carries x and y only, so nothing downstream can say which limb is
|
|
6
|
+
* nearer the camera. A segment drawn shorter than its rest length is tilted out
|
|
7
|
+
* of the picture plane, and by how much its length says: `ratio` = drawn length
|
|
8
|
+
* ÷ rest length, the rest length being the larger of the two sides' 95th
|
|
9
|
+
* percentile picture lengths of that segment over the clip — the straight length
|
|
10
|
+
* the leg scale reads (`STRAIGHT_PERCENTILE`, #148), per segment. Which way it
|
|
11
|
+
* tilts the length cannot say; the sign comes from the body:
|
|
12
|
+
*
|
|
13
|
+
* | segment | sense when foreshortened | why |
|
|
14
|
+
* | --------- | ------------------------ | --- |
|
|
15
|
+
* | thigh | forward | a hip flexes forward; it extends back too little to foreshorten past the band |
|
|
16
|
+
* | shin | walk: forward · run: back | the gait rule (rig-c/pose#281, HQ): a walk's lifted foot comes forward, a run's is kicked back — the knee alone cannot say, since it bends forward in both |
|
|
17
|
+
* | upper arm | with the opposite leg | in a gait each arm swings with the opposite leg: forward when the other side's thigh is the more foreshortened of the two, back when its own is; forward (a shoulder flexes forward) when neither thigh is foreshortened |
|
|
18
|
+
* | forearm | forward | an elbow bends toward the back of the body, so the forearm folds forward |
|
|
19
|
+
*
|
|
20
|
+
* "Forward" is the trunk's facing: toward the camera (+1) when the figure faces
|
|
21
|
+
* it (its left shoulder and hip drawn right of its right ones), away (−1) when
|
|
22
|
+
* it faces away, and undecided (sign 0) when the trunk is side-on — shoulders
|
|
23
|
+
* and hips drawn less than `SIDE_ON` trunk lengths wide — since forward is then
|
|
24
|
+
* across the picture, not into it. A ratio within `FLAT_BAND` is flat, sign 0,
|
|
25
|
+
* and so is one above it (drawn longer than rest is not a tilt).
|
|
26
|
+
*
|
|
27
|
+
* The layers: each joint's depth is its parent's plus its segment's,
|
|
28
|
+
* sign × rest × √(1 − ratio²) px (rest = drawn ÷ ratio), from the shoulders and
|
|
29
|
+
* hips at 0; a limb's depth is the mean of its middle and end joints'; the limbs
|
|
30
|
+
* are ordered nearest first, equal depths (to 0.01 px) sharing a layer. A limb
|
|
31
|
+
* whose segments are not both named on a frame is not ordered on it.
|
|
32
|
+
*
|
|
33
|
+
* Pure: no clock, no randomness, no disk.
|
|
34
|
+
*/
|
|
35
|
+
import type { Body18 } from './joints.ts';
|
|
36
|
+
import { percentile, STRAIGHT_PERCENTILE } from './legs.ts';
|
|
37
|
+
import { DEPTH_SEGMENT_NAMES, DEPTH_SEGMENTS, type DepthGait, type DepthSegment, type DepthSource, type FrameDepth, type Joint, type SegmentDepth } from './pose.ts';
|
|
38
|
+
|
|
39
|
+
/** A ratio within this band is flat (sign 0): cos 18.2° = 0.95, so a segment within about 18° of the picture plane reads flat. */
|
|
40
|
+
export const FLAT_BAND: readonly [number, number] = [0.95, 1.05];
|
|
41
|
+
/** The trunk is side-on when its shoulders and hips are drawn, on average, less than this many trunk lengths (neck → mid_hip) wide. */
|
|
42
|
+
export const SIDE_ON = 0.1;
|
|
43
|
+
/** The limbs `layers` orders, each by its segments from the trunk out. */
|
|
44
|
+
export const LIMBS = {
|
|
45
|
+
r_arm: ['r_upper_arm', 'r_forearm'],
|
|
46
|
+
l_arm: ['l_upper_arm', 'l_forearm'],
|
|
47
|
+
r_leg: ['r_thigh', 'r_shin'],
|
|
48
|
+
l_leg: ['l_thigh', 'l_shin'],
|
|
49
|
+
} as const satisfies Record<string, readonly [DepthSegment, DepthSegment]>;
|
|
50
|
+
export type Limb = keyof typeof LIMBS;
|
|
51
|
+
export const LIMB_NAMES = Object.keys(LIMBS) as Limb[];
|
|
52
|
+
|
|
53
|
+
type Pt = [number, number];
|
|
54
|
+
type Joints = Partial<Record<Body18, Joint>>;
|
|
55
|
+
const at = (j: Joint | undefined): Pt | null => (j === undefined || j.state === 'missing' || j.at === undefined ? null : j.at);
|
|
56
|
+
const kindOf = (s: DepthSegment) => s.slice(2) as 'upper_arm' | 'forearm' | 'thigh' | 'shin';
|
|
57
|
+
const other = (s: DepthSegment): DepthSegment => `${s.startsWith('r_') ? 'l' : 'r'}_${s.slice(2)}` as DepthSegment;
|
|
58
|
+
|
|
59
|
+
/** A segment's drawn length on a frame; null where a joint is not placed. */
|
|
60
|
+
function drawn(joints: Joints, s: DepthSegment): number | null {
|
|
61
|
+
const [a, b] = DEPTH_SEGMENTS[s];
|
|
62
|
+
const pa = at(joints[a]);
|
|
63
|
+
const pb = at(joints[b]);
|
|
64
|
+
return pa === null || pb === null ? null : Math.hypot(pb[0] - pa[0], pb[1] - pa[1]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The trunk's facing on a frame: +1 toward the camera, −1 away, 0 side-on or not placed. */
|
|
68
|
+
export function facingOf(joints: Joints): -1 | 0 | 1 {
|
|
69
|
+
const p = (n: Body18) => at(joints[n]);
|
|
70
|
+
const [rs, ls, rh, lh, neck] = [p('r_shoulder'), p('l_shoulder'), p('r_hip'), p('l_hip'), p('neck')];
|
|
71
|
+
if (rs === null || ls === null || rh === null || lh === null || neck === null) return 0;
|
|
72
|
+
const trunk = Math.hypot(neck[0] - (rh[0] + lh[0]) / 2, neck[1] - (rh[1] + lh[1]) / 2);
|
|
73
|
+
if (!(trunk > 0)) return 0;
|
|
74
|
+
const width = (ls[0] - rs[0] + (lh[0] - rh[0])) / 2 / trunk;
|
|
75
|
+
return width > SIDE_ON ? 1 : width < -SIDE_ON ? -1 : 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface DerivedDepth {
|
|
79
|
+
/** Per frame, each segment whose joints are placed. */
|
|
80
|
+
frames: FrameDepth[];
|
|
81
|
+
/** Each segment kind's rest length, px (the larger side's 95th percentile); null where neither side is ever placed. */
|
|
82
|
+
rest: Record<'upper_arm' | 'forearm' | 'thigh' | 'shin', number | null>;
|
|
83
|
+
/** Frames the trunk faced the camera, faced away, and was side-on (or not placed). */
|
|
84
|
+
facing: { toward: number; away: number; side: number };
|
|
85
|
+
/** Segment-frames by sign. */
|
|
86
|
+
signs: { toward: number; away: number; flat: number };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Each frame's depth, derived from the picture's lengths for `gait` (the table above). */
|
|
90
|
+
export function deriveDepth(track: readonly Joints[], gait: DepthGait): DerivedDepth {
|
|
91
|
+
const lengths = new Map(DEPTH_SEGMENT_NAMES.map((s) => [s, track.map((j) => drawn(j, s))]));
|
|
92
|
+
const rest = {} as DerivedDepth['rest'];
|
|
93
|
+
for (const k of ['upper_arm', 'forearm', 'thigh', 'shin'] as const) {
|
|
94
|
+
const sides = (['r', 'l'] as const).map((side) => percentile(lengths.get(`${side}_${k}`)!.filter((v): v is number => v !== null), STRAIGHT_PERCENTILE)).filter((v) => Number.isFinite(v) && v > 0);
|
|
95
|
+
rest[k] = sides.length === 0 ? null : Math.max(...sides);
|
|
96
|
+
}
|
|
97
|
+
const facing = { toward: 0, away: 0, side: 0 };
|
|
98
|
+
const signs = { toward: 0, away: 0, flat: 0 };
|
|
99
|
+
const frames = track.map((joints, i) => {
|
|
100
|
+
const face = facingOf(joints);
|
|
101
|
+
if (face === 1) facing.toward++;
|
|
102
|
+
else if (face === -1) facing.away++;
|
|
103
|
+
else facing.side++;
|
|
104
|
+
const ratio = (s: DepthSegment): number | null => {
|
|
105
|
+
const d = lengths.get(s)![i];
|
|
106
|
+
const r = rest[kindOf(s)];
|
|
107
|
+
return d === null || d === undefined || r === null ? null : d / r;
|
|
108
|
+
};
|
|
109
|
+
const out: FrameDepth = {};
|
|
110
|
+
for (const s of DEPTH_SEGMENT_NAMES) {
|
|
111
|
+
const r = ratio(s);
|
|
112
|
+
if (r === null) continue;
|
|
113
|
+
let sense: -1 | 1;
|
|
114
|
+
const kind = kindOf(s);
|
|
115
|
+
if (kind === 'shin') sense = gait === 'walk' ? 1 : -1;
|
|
116
|
+
else if (kind === 'upper_arm') {
|
|
117
|
+
// The arm swings with the opposite leg: its own thigh and the other side's, by how foreshortened each is.
|
|
118
|
+
const own = ratio(`${s.slice(0, 2)}thigh` as DepthSegment);
|
|
119
|
+
const opp = ratio(other(`${s.slice(0, 2)}thigh` as DepthSegment));
|
|
120
|
+
sense = own === null || opp === null || Math.min(own, opp) >= FLAT_BAND[0] || own === opp ? 1 : opp < own ? 1 : -1;
|
|
121
|
+
} else sense = 1;
|
|
122
|
+
const sign = (r < FLAT_BAND[0] ? face * sense : 0) as -1 | 0 | 1;
|
|
123
|
+
if (sign === 1) signs.toward++;
|
|
124
|
+
else if (sign === -1) signs.away++;
|
|
125
|
+
else signs.flat++;
|
|
126
|
+
out[s] = { ratio: r, sign: sign === 0 ? 0 : sign, source: 'derived' };
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
});
|
|
130
|
+
return { frames, rest, facing, signs };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** One frame's layers: the limbs nearest first, equal depths sharing a layer, and each limb's depth (px, + toward the camera). */
|
|
134
|
+
export interface FrameLayers {
|
|
135
|
+
order: Limb[][];
|
|
136
|
+
depth: Partial<Record<Limb, number>>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The depth one segment adds to its distal joint over its proximal one, px: sign × rest × √(1 − ratio²). */
|
|
140
|
+
export function segmentDepth(d: SegmentDepth, length: number): number {
|
|
141
|
+
if (d.sign === 0 || !(d.ratio > 0)) return 0;
|
|
142
|
+
const r = Math.min(d.ratio, 1);
|
|
143
|
+
return d.sign * (length / d.ratio) * Math.sqrt(1 - r * r);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The limbs' front-to-back order on one frame, from its depth and its joints' drawn lengths. */
|
|
147
|
+
export function layersOf(joints: Joints, depth: FrameDepth): FrameLayers {
|
|
148
|
+
const z: Partial<Record<Limb, number>> = {};
|
|
149
|
+
for (const limb of LIMB_NAMES) {
|
|
150
|
+
const [upper, lower] = LIMBS[limb];
|
|
151
|
+
const du = depth[upper];
|
|
152
|
+
const dl = depth[lower];
|
|
153
|
+
const lu = drawn(joints, upper);
|
|
154
|
+
const ll = drawn(joints, lower);
|
|
155
|
+
if (du === undefined || dl === undefined || lu === null || ll === null) continue;
|
|
156
|
+
const mid = segmentDepth(du, lu);
|
|
157
|
+
const end = mid + segmentDepth(dl, ll);
|
|
158
|
+
z[limb] = Math.round(((mid + end) / 2) * 100) / 100;
|
|
159
|
+
}
|
|
160
|
+
const placed = LIMB_NAMES.filter((l) => z[l] !== undefined).sort((a, b) => z[b]! - z[a]! || LIMB_NAMES.indexOf(a) - LIMB_NAMES.indexOf(b));
|
|
161
|
+
const order: Limb[][] = [];
|
|
162
|
+
for (const l of placed) {
|
|
163
|
+
const last = order[order.length - 1];
|
|
164
|
+
if (last !== undefined && z[last[0]!] === z[l]) last.push(l);
|
|
165
|
+
else order.push([l]);
|
|
166
|
+
}
|
|
167
|
+
return { order, depth: z };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** A frame's layers as text: "r_arm > l_arm = r_leg > l_leg". */
|
|
171
|
+
export const orderText = (order: Limb[][]): string => order.map((g) => g.join(' = ')).join(' > ');
|
|
172
|
+
|
|
173
|
+
/** The source a sequence's depth came from, or null when its frames carry none. */
|
|
174
|
+
export function depthSourceOf(frames: ReadonlyArray<{ depth?: FrameDepth }>): DepthSource | null {
|
|
175
|
+
for (const f of frames) for (const d of Object.values(f.depth ?? {})) return d.source;
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* retarget's DEPTH line (rig-c/pose#281): where the depth came from, on how many
|
|
181
|
+
* frames the limbs were ordered, and which limb was in front alone how often.
|
|
182
|
+
*/
|
|
183
|
+
export function depthLine(layers: readonly FrameLayers[], frames: ReadonlyArray<{ depth?: FrameDepth }>, gait: DepthGait | null): string {
|
|
184
|
+
const source = depthSourceOf(frames);
|
|
185
|
+
const from = source === null ? 'the sequence\'s depth (no segment named)' : source === 'derived' ? `the sequence's derived depth (clean --depth ${gait ?? '?'})` : 'the sequence\'s measured depth';
|
|
186
|
+
const ordered = layers.filter((l) => l.order.length > 0).length;
|
|
187
|
+
const front = LIMB_NAMES.map((limb) => `${limb} ${layers.filter((l) => l.order[0]?.length === 1 && l.order[0][0] === limb).length}`).join(', ');
|
|
188
|
+
const tied = layers.filter((l) => (l.order[0]?.length ?? 0) > 1).length;
|
|
189
|
+
return `DEPTH layers from ${from}: the limbs ordered front to back on ${ordered} of ${layers.length} frame(s); in front alone: ${front}; tied in front on ${tied} (the report's layers)`;
|
|
190
|
+
}
|
package/src/legs.ts
CHANGED
|
@@ -516,6 +516,12 @@ export interface Feet {
|
|
|
516
516
|
* rule does not apply.
|
|
517
517
|
*/
|
|
518
518
|
tracked?: Array<{ side: Side; frames: [number, number] }>;
|
|
519
|
+
/**
|
|
520
|
+
* rig-c/pose#280: the second support signal (the ankle's vertical speed), how
|
|
521
|
+
* often it contradicts ankle height, and which one decided each frame. Set by
|
|
522
|
+
* `supportsOf` on every reading.
|
|
523
|
+
*/
|
|
524
|
+
signal?: SupportSignal;
|
|
519
525
|
}
|
|
520
526
|
|
|
521
527
|
/** A run of frames whose support the lower-ankle rule (rig-c/pose#113) reads differently from the reading before it. */
|
|
@@ -559,8 +565,17 @@ export function rollingFloor(ys: readonly number[], times: readonly number[]): n
|
|
|
559
565
|
* The frames whose support this changes are `lowered`. On a frame where no foot
|
|
560
566
|
* is planted because the only foot free is the higher one (rig-c/pose#119), the
|
|
561
567
|
* lower foot is tracked (`tracked`).
|
|
568
|
+
*
|
|
569
|
+
* rig-c/pose#280: the ankle's vertical speed is read beside ankle height on
|
|
570
|
+
* every clip (`speedSignalOf`), and on a frontal view it decides the support
|
|
571
|
+
* (`frontalOf`); `signal` says which signal decided each frame.
|
|
562
572
|
*/
|
|
563
573
|
export function supportsOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, readonly Pt[]>, times: readonly number[], secondsOf: (run: [number, number]) => number, inPlace = false, lowerAnkle = false): Feet {
|
|
574
|
+
return frontalOf(ankle, hip, heightSupportsOf(ankle, hip, times, secondsOf, inPlace, lowerAnkle));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** The support read from ankle height alone: the rules above, #113's included (the whole reading before rig-c/pose#280). */
|
|
578
|
+
function heightSupportsOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, readonly Pt[]>, times: readonly number[], secondsOf: (run: [number, number]) => number, inPlace: boolean, lowerAnkle: boolean): Feet {
|
|
564
579
|
if (inPlace || !lowerAnkle) return readSupports(ankle, hip, times, secondsOf, inPlace, false).feet;
|
|
565
580
|
const { feet, released } = readSupports(ankle, hip, times, secondsOf, false, true);
|
|
566
581
|
const before = readSupports(ankle, hip, times, secondsOf, false, false).feet.support;
|
|
@@ -727,6 +742,179 @@ function readSupports(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, read
|
|
|
727
742
|
return { feet: { legLength, support, supports, jumps, steps, changes, ...(tracked.length > 0 ? { tracked } : {}) }, released };
|
|
728
743
|
}
|
|
729
744
|
|
|
745
|
+
// --- the second signal: the ankle's vertical speed against the ground (rig-c/pose#280) ---
|
|
746
|
+
|
|
747
|
+
/** A foot's contact on one frame, read from the ankles' vertical speed: one side, both, or neither (null). */
|
|
748
|
+
export type Contact = Side | 'both' | null;
|
|
749
|
+
|
|
750
|
+
export interface SpeedSignal {
|
|
751
|
+
/** Each side's ground speed: the median of its ankle's vertical step (picture px a frame, y down) over the frames outside every jump. */
|
|
752
|
+
ground: Record<Side, number>;
|
|
753
|
+
/** Per frame: the feet that move with the ground, on the step into the frame or the step out of it (null on a jump frame or where neither does). */
|
|
754
|
+
contact: Contact[];
|
|
755
|
+
/** The frames the two signals contradict: the lower ankle (by ≥ `PLANT_SWITCH` leg lengths) leaves the ground's speed while the higher moves with it. */
|
|
756
|
+
contradict: number[];
|
|
757
|
+
/** The frames both signals read: outside every jump. */
|
|
758
|
+
read: number;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* The ankle's vertical speed as a support signal (rig-c/pose#280). A planted
|
|
763
|
+
* foot moves with the ground: still under a fixed camera, or sliding at the
|
|
764
|
+
* ground's speed in a gait drawn in place. Each side's ground speed is the
|
|
765
|
+
* median of its ankle's vertical step over the frames outside every jump —
|
|
766
|
+
* a walking foot is down on more than half of them — and a foot moves with it
|
|
767
|
+
* on a step that departs from it by less than `STILL_SPEED` of that side's leg
|
|
768
|
+
* length. A frame's foot is down when either the step into the frame or the
|
|
769
|
+
* step out of it moves with the ground, so a drawing a foot lands or lifts on
|
|
770
|
+
* reads it down. `contradict` lists the frames where this and ankle height
|
|
771
|
+
* disagree in the way a frontal view's depth makes them: the lower ankle is
|
|
772
|
+
* the moving one, the higher the one moving with the ground.
|
|
773
|
+
*/
|
|
774
|
+
export function speedSignalOf(ankle: Record<Side, readonly Pt[]>, feet: Pick<Feet, 'legLength' | 'jumps'>): SpeedSignal {
|
|
775
|
+
const n = ankle.r.length;
|
|
776
|
+
const jump = new Array<boolean>(n).fill(false);
|
|
777
|
+
for (const [a, b] of feet.jumps) for (let i = a; i <= b; i++) jump[i] = true;
|
|
778
|
+
const step = (s: Side, i: number): number | null => (i < 1 || i >= n || jump[i] || jump[i - 1] ? null : ankle[s][i]![1] - ankle[s][i - 1]![1]);
|
|
779
|
+
const ground = {} as Record<Side, number>;
|
|
780
|
+
for (const s of SIDES) {
|
|
781
|
+
const xs: number[] = [];
|
|
782
|
+
for (let i = 1; i < n; i++) {
|
|
783
|
+
const v = step(s, i);
|
|
784
|
+
if (v !== null) xs.push(v);
|
|
785
|
+
}
|
|
786
|
+
ground[s] = xs.length === 0 ? 0 : percentile(xs, 0.5);
|
|
787
|
+
}
|
|
788
|
+
const withGround = (s: Side, i: number): boolean => {
|
|
789
|
+
const v = step(s, i);
|
|
790
|
+
return v !== null && Math.abs(v - ground[s]) < STILL_SPEED * feet.legLength[s];
|
|
791
|
+
};
|
|
792
|
+
const down = (s: Side, i: number) => withGround(s, i) || withGround(s, i + 1);
|
|
793
|
+
const mean = (feet.legLength.r + feet.legLength.l) / 2;
|
|
794
|
+
const contact: Contact[] = [];
|
|
795
|
+
const contradict: number[] = [];
|
|
796
|
+
let read = 0;
|
|
797
|
+
for (let i = 0; i < n; i++) {
|
|
798
|
+
if (jump[i]) {
|
|
799
|
+
contact.push(null);
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
read++;
|
|
803
|
+
const r = down('r', i);
|
|
804
|
+
const l = down('l', i);
|
|
805
|
+
contact.push(r && l ? 'both' : r ? 'r' : l ? 'l' : null);
|
|
806
|
+
const gap = ankle.l[i]![1] - ankle.r[i]![1];
|
|
807
|
+
const lower: Side | null = Math.abs(gap) < PLANT_SWITCH * mean ? null : gap > 0 ? 'l' : 'r';
|
|
808
|
+
if (lower !== null && !down(lower, i) && down(lower === 'r' ? 'l' : 'r', i)) contradict.push(i);
|
|
809
|
+
}
|
|
810
|
+
return { ground, contact, contradict, read };
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* A clip whose two support signals contradict on more than this share of the
|
|
815
|
+
* frames both read is read as a frontal view (rig-c/pose#280), when its stride
|
|
816
|
+
* also shows in depth (`strideInDepth`). A frontal walk contradicts at every
|
|
817
|
+
* hand-over, for as long as the nearer foot is in the air below the planted one:
|
|
818
|
+
* 18 of 96 frames (18.8 %) on the tests' synthetic frontal walk, where ankle
|
|
819
|
+
* height alone hands support to the nearer foot 1 or 2 frames before it lands
|
|
820
|
+
* (16 frames in all, declared in place). Every clip the other tests read with
|
|
821
|
+
* its stride in depth contradicts on none; the shortest contradiction a walk
|
|
822
|
+
* can make, one frame a step, is 8.3 % of a 1 s cycle at 24 fps (2 of 24).
|
|
823
|
+
*/
|
|
824
|
+
export const FRONTAL_DISAGREE = 0.05;
|
|
825
|
+
|
|
826
|
+
/** rig-c/pose#280: the two support signals of a clip, and which one decided each frame's support. */
|
|
827
|
+
export interface SupportSignal {
|
|
828
|
+
/** The frames the signals contradict (`speedSignalOf`), of `read` frames both read. */
|
|
829
|
+
contradict: number[];
|
|
830
|
+
read: number;
|
|
831
|
+
/** Each side's ankle range in the picture, relative to its hip (5th to 95th percentile), px: the stride shows in y on a frontal view and in x on a side view. */
|
|
832
|
+
range: Record<Side, { x: number; y: number }>;
|
|
833
|
+
/** The stride shows in depth (y range over x range on both sides) and the signals contradict on more than `FRONTAL_DISAGREE` of the frames: the support comes from the vertical speed. */
|
|
834
|
+
frontal: boolean;
|
|
835
|
+
/** Per frame, the vertical speed's reading: the side down, both, or null (a jump, or neither foot moving with the ground). */
|
|
836
|
+
contact: Contact[];
|
|
837
|
+
/** Per frame, ankle height's reading (the support before #280). */
|
|
838
|
+
height: Array<Side | null>;
|
|
839
|
+
/** Per frame, the signal that decided the support: `speed`, `height`, or null where no foot supports. */
|
|
840
|
+
by: Array<'speed' | 'height' | null>;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/** Each side's ankle range relative to its hip, px, x and y (5th to 95th percentile). */
|
|
844
|
+
function rangeOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, readonly Pt[]>): Record<Side, { x: number; y: number }> {
|
|
845
|
+
const span = (xs: number[]) => percentile(xs, 0.95) - percentile(xs, 0.05);
|
|
846
|
+
const out = {} as Record<Side, { x: number; y: number }>;
|
|
847
|
+
for (const s of SIDES) out[s] = { x: span(ankle[s].map((a, i) => a[0] - hip[s][i]![0])), y: span(ankle[s].map((a, i) => a[1] - hip[s][i]![1])) };
|
|
848
|
+
return out;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* rig-c/pose#280: the support from the ankle's vertical speed, on a frontal
|
|
853
|
+
* view. Seen from the front, a foot's depth is drawn as height, so the lower
|
|
854
|
+
* ankle is the nearer foot, which may be the one in the air. A clip is read as
|
|
855
|
+
* frontal when both hold: its stride shows in depth — on each side the ankle's
|
|
856
|
+
* range relative to its hip is larger in y than in x (a side view's stride is
|
|
857
|
+
* in x) — and the two signals contradict on more than `FRONTAL_DISAGREE` of the
|
|
858
|
+
* frames both read. Then each frame's support is the vertical speed's: the one
|
|
859
|
+
* foot down; on frames both are down, the foot that landed (the other side from
|
|
860
|
+
* the support before the run, or, at the clip's start, the side that stays down
|
|
861
|
+
* after it), so support passes on the frame the incoming foot lands (as #102
|
|
862
|
+
* takes it from the landing frame); and ankle height's reading where neither
|
|
863
|
+
* foot moves with the ground. A plant ends where the other foot lands, so no
|
|
864
|
+
* step releases one and none is tracked (#113's `lowered` and #119's `tracked`
|
|
865
|
+
* are not read). Otherwise the feet are ankle height's, unchanged.
|
|
866
|
+
*/
|
|
867
|
+
function frontalOf(ankle: Record<Side, readonly Pt[]>, hip: Record<Side, readonly Pt[]>, feet: Feet): Feet {
|
|
868
|
+
const sig = speedSignalOf(ankle, feet);
|
|
869
|
+
const range = rangeOf(ankle, hip);
|
|
870
|
+
const strideInDepth = SIDES.every((s) => range[s].y > range[s].x);
|
|
871
|
+
const frontal = strideInDepth && sig.read > 0 && sig.contradict.length > FRONTAL_DISAGREE * sig.read;
|
|
872
|
+
const height = feet.support.map((s) => s ?? null);
|
|
873
|
+
const signal = (by: SupportSignal['by']): SupportSignal => ({ contradict: sig.contradict, read: sig.read, range, frontal, contact: sig.contact, height, by });
|
|
874
|
+
if (!frontal) return { ...feet, signal: signal(height.map((s) => (s === null ? null : 'height'))) };
|
|
875
|
+
const n = height.length;
|
|
876
|
+
const other = (s: Side): Side => (s === 'r' ? 'l' : 'r');
|
|
877
|
+
const support: Array<Side | null> = new Array<Side | null>(n).fill(null);
|
|
878
|
+
const by: SupportSignal['by'] = new Array<'speed' | 'height' | null>(n).fill(null);
|
|
879
|
+
const fromHeight = (i: number) => {
|
|
880
|
+
support[i] = height[i]!;
|
|
881
|
+
by[i] = height[i] === null ? null : 'height';
|
|
882
|
+
};
|
|
883
|
+
for (let i = 0; i < n; i++) {
|
|
884
|
+
const c = sig.contact[i] ?? null;
|
|
885
|
+
if (c === 'r' || c === 'l') {
|
|
886
|
+
support[i] = c;
|
|
887
|
+
by[i] = 'speed';
|
|
888
|
+
} else if (c === 'both') {
|
|
889
|
+
let b = i;
|
|
890
|
+
while (b + 1 < n && sig.contact[b + 1] === 'both') b++;
|
|
891
|
+
const before = i > 0 ? (support[i - 1] ?? null) : null;
|
|
892
|
+
const after = b + 1 < n ? (sig.contact[b + 1] ?? null) : null;
|
|
893
|
+
const landed: Side | null = before !== null ? other(before) : after === 'r' || after === 'l' ? after : null;
|
|
894
|
+
for (let k = i; k <= b; k++) {
|
|
895
|
+
if (landed === null) fromHeight(k);
|
|
896
|
+
else {
|
|
897
|
+
support[k] = landed;
|
|
898
|
+
by[k] = 'speed';
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
i = b;
|
|
902
|
+
} else fromHeight(i);
|
|
903
|
+
}
|
|
904
|
+
const supports: Feet['supports'] = [];
|
|
905
|
+
const changes: Feet['changes'] = [];
|
|
906
|
+
for (let i = 0; i < n; i++) {
|
|
907
|
+
const s = support[i];
|
|
908
|
+
if (s === null || s === undefined) continue;
|
|
909
|
+
const prev = i > 0 ? (support[i - 1] ?? null) : null;
|
|
910
|
+
if (prev !== s) changes.push({ frame: i, from: prev, to: s });
|
|
911
|
+
const last = supports[supports.length - 1];
|
|
912
|
+
if (last !== undefined && last.side === s && last.frames[1] === i - 1) last.frames[1] = i;
|
|
913
|
+
else supports.push({ side: s, frames: [i, i] });
|
|
914
|
+
}
|
|
915
|
+
return { legLength: feet.legLength, support, supports, jumps: feet.jumps, steps: [], changes, signal: signal(by) };
|
|
916
|
+
}
|
|
917
|
+
|
|
730
918
|
/** One support span's shift onto the floor (rig-c/pose#85): its side, its frames, and floor − its planted height, world units. */
|
|
731
919
|
export interface FloorShift {
|
|
732
920
|
side: Side;
|
|
@@ -1584,3 +1772,4 @@ export function holdBlend(blends: readonly BlendFrame[], n: number, posed: (i: n
|
|
|
1584
1772
|
const range = rangeUsed(lower, held);
|
|
1585
1773
|
return { bounds: STANCE_BOUNDS, ...(range === null ? {} : { range }), runs, over, stretch };
|
|
1586
1774
|
}
|
|
1775
|
+
|
package/src/map.ts
CHANGED
|
@@ -58,6 +58,11 @@
|
|
|
58
58
|
* `place: "by-hip"` (rig-c/pose#93) places the wrist of each of its chains that
|
|
59
59
|
* `pose` puts within one hip width of the mid-hip by its offset from the hip
|
|
60
60
|
* (`byHipChains`; the retarget solves it).
|
|
61
|
+
* `phase: "left-step" | "right-step"` (rig-c/pose#286) declares a gait set drawn
|
|
62
|
+
* for the foot that is stepping (swinging), named by it — left-step: the left foot is stepping; shown while the right foot supports (right-step
|
|
63
|
+
* mirrors it). It shows exactly on the report's `feet.supports` spans of the
|
|
64
|
+
* supporting foot and is switched on the support hand-over, not on angle
|
|
65
|
+
* distance (`PHASE_SIDE`).
|
|
61
66
|
* `switch.ts` decides when each one shows.
|
|
62
67
|
*/
|
|
63
68
|
import type { RawBone } from 'rig-c/core';
|
|
@@ -86,6 +91,9 @@ export interface Translate {
|
|
|
86
91
|
joint: string;
|
|
87
92
|
scale: { pose: [string, string]; rig: [string, string] };
|
|
88
93
|
}
|
|
94
|
+
/** The step sides a set may declare as its `phase` (rig-c/pose#286), and the supporting foot each one shows on: left-step: the left foot is stepping; shown while the right foot supports. */
|
|
95
|
+
export const PHASE_SIDE = { 'left-step': 'r', 'right-step': 'l' } as const;
|
|
96
|
+
export type Phase = keyof typeof PHASE_SIDE;
|
|
89
97
|
/** An alternate drawing: a set of slots switched together, and the key skeleton it is selected by. */
|
|
90
98
|
export interface AlternateSet {
|
|
91
99
|
name: string;
|
|
@@ -113,6 +121,8 @@ export interface AlternateSet {
|
|
|
113
121
|
support?: string[];
|
|
114
122
|
/** `anchor` (rig-c/pose#191): the set's two-bone arm chains whose hand-tip is anchored to the set's `drawn` head-local offset while the set shows. */
|
|
115
123
|
anchor?: string[];
|
|
124
|
+
/** `phase` (rig-c/pose#286): the stepping foot the set is drawn for (left-step: the left foot is stepping; shown while the right foot supports); it shows exactly on that supporting foot's `feet.supports` spans, switched on the hand-over. Absent: the angle rule. */
|
|
125
|
+
phase?: Phase;
|
|
116
126
|
}
|
|
117
127
|
export interface YawSet {
|
|
118
128
|
set: string;
|
|
@@ -423,7 +433,7 @@ function readBodyMap(p: Problems, where: string, raw: unknown): BodyMap | undefi
|
|
|
423
433
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
424
434
|
}
|
|
425
435
|
|
|
426
|
-
const SET_SHAPE = '{ name, slots: { "<slot>": "<attachment>" }, hides?: [slot], chains: [chain], pose: { "<joint>": [x, y] }, drawn?: { "<bone>": deg }, offsets?: { "<bone>": [dx, dy] }, turn?: { "<bone>": [min, max] }, frames?: [[first, last], …], place?: "by-hip" }';
|
|
436
|
+
const SET_SHAPE = '{ name, slots: { "<slot>": "<attachment>" }, hides?: [slot], chains: [chain], pose: { "<joint>": [x, y] }, drawn?: { "<bone>": deg }, offsets?: { "<bone>": [dx, dy] }, turn?: { "<bone>": [min, max] }, frames?: [[first, last], …], place?: "by-hip", phase?: "left-step" | "right-step" }';
|
|
427
437
|
|
|
428
438
|
/** The joints a wrist is, for `place: "by-hip"`: a chain whose last joint is one of them carries a wrist. */
|
|
429
439
|
export const WRISTS: readonly string[] = ['r_wrist', 'l_wrist'];
|
|
@@ -472,7 +482,7 @@ function readAlternates(p: Problems, where: string, raw: unknown, chains: readon
|
|
|
472
482
|
p.add('MAP_ALT_SHAPE', `${aw} is ${typeName(a)}; a set is ${SET_SHAPE}`);
|
|
473
483
|
return;
|
|
474
484
|
}
|
|
475
|
-
refuseUnknownKeys(p, 'MAP_KEYS', aw, a, ['name', 'note', 'slots', 'hides', 'chains', 'pose', 'drawn', 'offsets', 'turn', 'frames', 'place', 'symmetric', 'support', 'anchor']);
|
|
485
|
+
refuseUnknownKeys(p, 'MAP_KEYS', aw, a, ['name', 'note', 'slots', 'hides', 'chains', 'pose', 'drawn', 'offsets', 'turn', 'frames', 'place', 'symmetric', 'support', 'anchor', 'phase']);
|
|
476
486
|
const name = typeof a.name === 'string' && a.name !== '' ? a.name : null;
|
|
477
487
|
if (name === null) p.add('MAP_ALT_NAME', `${aw} "name" is ${typeName(a.name)}; it must be a non-empty string`);
|
|
478
488
|
else if (names.has(name)) p.add('MAP_ALT_NAME', `${aw} "name" "${name}" names two sets`);
|
|
@@ -676,7 +686,18 @@ function readAlternates(p: Problems, where: string, raw: unknown, chains: readon
|
|
|
676
686
|
if (ok) anchor = anc;
|
|
677
687
|
}
|
|
678
688
|
}
|
|
679
|
-
|
|
689
|
+
let phase: Phase | undefined;
|
|
690
|
+
if ('phase' in a) {
|
|
691
|
+
const phases = Object.keys(PHASE_SIDE);
|
|
692
|
+
if (typeof a.phase !== 'string' || !phases.includes(a.phase)) {
|
|
693
|
+
p.add('MAP_ALT_PHASE', `${label} "phase" is ${JSON.stringify(a.phase)}; it is the stepping foot the set is drawn for, one of ${phases.map((x) => `"${x}"`).join(', ')} (left-step: the left foot is stepping; shown while the right foot supports)`);
|
|
694
|
+
ok = false;
|
|
695
|
+
} else if ('frames' in a) {
|
|
696
|
+
p.add('MAP_ALT_PHASE', `${label} names both "phase" and "frames"; a phase set shows on its supporting foot's spans, and a window would cut them — drop one`);
|
|
697
|
+
ok = false;
|
|
698
|
+
} else phase = a.phase as Phase;
|
|
699
|
+
}
|
|
700
|
+
if (ok && name !== null && cs !== null) out.push({ name, slots, hides, chains: cs, pose, drawn, offsets, turn, frames, ...(place === null ? {} : { place }), ...(symmetric === undefined ? {} : { symmetric }), ...(support === undefined ? {} : { support }), ...(anchor === undefined ? {} : { anchor }), ...(phase === undefined ? {} : { phase }) });
|
|
680
701
|
});
|
|
681
702
|
return out;
|
|
682
703
|
}
|
package/src/outdir.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a command's output goes (rig-c/pose#239, #284). A file a command writes goes into a
|
|
3
|
+
* directory the user named: one that is not there yet is made just before the write (mkdir -p),
|
|
4
|
+
* and a `made directory` line says so and which paths it holds. A file where a directory has to
|
|
5
|
+
* be made, or a directory that cannot be written into, is refused by name (`ARG_PATH`) before
|
|
6
|
+
* anything is read or written.
|
|
7
|
+
*/
|
|
8
|
+
import { accessSync, constants, existsSync, mkdirSync, statSync } from 'node:fs';
|
|
9
|
+
import { dirname } from 'node:path';
|
|
10
|
+
import type { Problems } from './problems.ts';
|
|
11
|
+
|
|
12
|
+
/** The directories to make before writing, each with the `<flag> <path>`s written into it, in the order named. */
|
|
13
|
+
export type OutDirs = Map<string, string[]>;
|
|
14
|
+
|
|
15
|
+
const writable = (dir: string): boolean => {
|
|
16
|
+
try {
|
|
17
|
+
accessSync(dir, constants.W_OK);
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check where each named output lands: its directory is there, or the nearest existing ancestor
|
|
26
|
+
* is a directory this process can write into. A miss is added to `p`; a directory still to make
|
|
27
|
+
* is added to `dirs`. A directory a flag names itself (--build, --render) is checked by its parent.
|
|
28
|
+
*/
|
|
29
|
+
export function planOutDirs(outputs: ReadonlyArray<readonly [string, string | undefined]>, p: Problems, dirs: OutDirs = new Map()): OutDirs {
|
|
30
|
+
for (const [flag, path] of outputs) {
|
|
31
|
+
if (path === undefined) continue;
|
|
32
|
+
const dir = dirname(path);
|
|
33
|
+
let up = dir;
|
|
34
|
+
while (!existsSync(up) && dirname(up) !== up) up = dirname(up);
|
|
35
|
+
if (!statSync(up).isDirectory()) p.add('ARG_PATH', up === dir ? `${flag} ${path} — ${dir} is not a directory` : `${flag} ${path} — ${up} is a file, so directory ${dir} cannot be made under it`);
|
|
36
|
+
else if (up !== dir && !writable(up)) p.add('ARG_PATH', `${flag} ${path} — ${up} is not writable, so directory ${dir} cannot be made under it`);
|
|
37
|
+
else if (up !== dir) dirs.set(dir, [...(dirs.get(dir) ?? []), `${flag} ${path}`]);
|
|
38
|
+
}
|
|
39
|
+
return dirs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Make each planned directory (mkdir -p) and print its line; run after the command is green, just before it writes. */
|
|
43
|
+
export function makeOutDirs(dirs: OutDirs, tell: (l: string) => void): void {
|
|
44
|
+
for (const [dir, flags] of dirs) {
|
|
45
|
+
if (existsSync(dir)) continue;
|
|
46
|
+
mkdirSync(dir, { recursive: true });
|
|
47
|
+
tell(madeLine(dir, flags));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** rig-c/pose#239: the line a directory this run made prints — the directory, and the paths that named it. */
|
|
52
|
+
export function madeLine(dir: string, flags: readonly string[]): string {
|
|
53
|
+
return ` made directory ${dir} — it did not exist; ${flags.join(' and ')} ${flags.length === 1 ? 'is' : 'are'} written into it`;
|
|
54
|
+
}
|