cozyclay 1.9.0 → 2.0.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/CHANGELOG.md +171 -0
- package/bin/agent/agent-routes.mjs +74 -70
- package/bin/agent/agent-runner.mjs +10 -3
- package/bin/agent/motion-runtime.mjs +32 -260
- package/bin/agent/providers.mjs +46 -3
- package/bin/agent/session-store.mjs +41 -5
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-tools.mjs +48 -11
- package/bin/cozyclay.mjs +26 -8
- package/bin/live/cli.mjs +22 -3
- package/bin/mcp-runtime.mjs +35 -20
- package/bin/telemetry-state.mjs +5 -2
- package/bin/update-check.mjs +15 -0
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-MVZOEW8O.js +1 -0
- package/dist/assets/app-C1sdVbg4.css +1 -0
- package/dist/assets/app-CA55tcI3.js +4876 -0
- package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
- package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
- package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
- package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
- package/dist/assets/shot-prompt-Yrt99wZx.js +17 -0
- package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
- package/dist/assets/workflow-DRdwoDBT.css +1 -0
- package/dist/assets/workflow-dNPGY9xz.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
- package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
- package/dist/fonts/README.md +13 -9
- package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
- package/dist/index.html +7 -7
- package/dist/privacy/index.html +13 -8
- package/dist/sitemap.xml +6 -6
- package/dist/workflow/index.html +5 -5
- package/mcp/LIVE-PROTOCOL.md +1 -1
- package/mcp/live-hub.mjs +35 -93
- package/mcp/mesh-file.mjs +48 -0
- package/mcp/server.mjs +6 -77
- package/mcp/tool-handlers.mjs +224 -377
- package/package.json +2 -1
- package/src/App.jsx +1273 -9075
- package/src/analytics.js +393 -9
- package/src/app-context.js +211 -0
- package/src/app-stage.jsx +25 -24
- package/src/ardy/auto-fix-panel.css +108 -0
- package/src/ardy/collision-blockers.js +9 -3
- package/src/ardy/fix-collisions.js +10 -66
- package/src/ardy/ground.js +4 -4
- package/src/ardy/ik-drag.js +101 -0
- package/src/ardy/ik-key-json.js +43 -0
- package/src/ardy/ik.js +122 -76
- package/src/ardy/physics-panel.css +2 -0
- package/src/ardy/physics-panel.jsx +17 -5
- package/src/ardy/platform-fit-panel.css +64 -0
- package/src/ardy/platform-fit-panel.jsx +40 -0
- package/src/ardy/platform-fit.js +326 -0
- package/src/ardy/playback.js +166 -20
- package/src/ardy/range-pin.js +299 -0
- package/src/ardy/timeline-coordinates.js +26 -0
- package/src/ardy/timeline.css +1199 -0
- package/src/ardy/timeline.jsx +149 -41
- package/src/ardy/waypoints.js +2 -2
- package/src/asset-pane.css +462 -0
- package/src/asset-pane.jsx +461 -232
- package/src/command-bus.js +349 -0
- package/src/commands/ai.js +31 -0
- package/src/commands/cast.js +140 -0
- package/src/commands/elements/character.js +13 -0
- package/src/commands/elements/motion.js +14 -0
- package/src/commands/elements/object.js +22 -0
- package/src/commands/elements/scene.js +16 -0
- package/src/commands/elements/shot.js +9 -0
- package/src/commands/elements/stage.js +11 -0
- package/src/commands/elements.js +155 -0
- package/src/commands/export.js +20 -0
- package/src/commands/index.js +40 -0
- package/src/commands/motion.js +231 -0
- package/src/commands/objects.js +174 -0
- package/src/commands/project.js +53 -0
- package/src/commands/scene.js +81 -0
- package/src/commands/shared.js +19 -0
- package/src/commands/shot.js +103 -0
- package/src/commands/stage.js +17 -0
- package/src/commands/view.js +42 -0
- package/src/document-store.js +144 -0
- package/src/domains/cast.js +1007 -0
- package/src/domains/motion.js +3219 -0
- package/src/domains/objects.js +932 -0
- package/src/domains/scenes.js +817 -0
- package/src/domains/shots.js +404 -0
- package/src/domains/stage.js +93 -0
- package/src/dualview.jsx +113 -57
- package/src/facing-marks.js +3 -0
- package/src/first-success-guide.jsx +14 -14
- package/src/grid-view.js +18 -5
- package/src/hierarchy-model.js +24 -16
- package/src/hierarchy-panel.css +386 -0
- package/src/hierarchy-panel.jsx +150 -21
- package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
- package/src/i2v-motion-studio.jsx +180 -0
- package/src/ik-camera.js +3 -0
- package/src/main.jsx +6 -0
- package/src/motion/generation.js +105 -0
- package/src/motion-readiness-ui.jsx +4 -4
- package/src/motion-readiness.js +11 -0
- package/src/motion-trail.js +366 -9
- package/src/object-gizmo.jsx +7 -1
- package/src/otio.js +2 -1
- package/src/panels/CameraPanel.jsx +60 -0
- package/src/panels/CharacterTransformPanel.jsx +48 -0
- package/src/panels/EnvironmentPanel.jsx +40 -0
- package/src/panels/Foldout.jsx +32 -0
- package/src/panels/LightPanel.jsx +24 -0
- package/src/panels/ObjectTransformPanel.jsx +554 -0
- package/src/panels/PosePanel.jsx +97 -0
- package/src/panels/ProjectPanel.jsx +33 -0
- package/src/panels/PromptBlocksPanel.jsx +442 -0
- package/src/panels/PropsPanel.jsx +70 -0
- package/src/panels/ReferenceImageField.jsx +91 -0
- package/src/panels/RigControlPanel.jsx +78 -0
- package/src/panels/RigPanel.jsx +37 -0
- package/src/panels/SubjectBox.jsx +47 -0
- package/src/panels/SubjectsPanel.jsx +39 -0
- package/src/panels/VideoCapturePanel.jsx +180 -0
- package/src/panels/details.css +1171 -0
- package/src/panels/motion.css +197 -0
- package/src/panels/pose.css +39 -0
- package/src/planview.jsx +60 -31
- package/src/posestudio.jsx +59 -4
- package/src/project-browser.css +1073 -0
- package/src/project-browser.jsx +284 -123
- package/src/range-pin-object-transform.js +19 -0
- package/src/range-pin-panel.css +518 -0
- package/src/range-pin-panel.jsx +344 -0
- package/src/result-modal.jsx +5 -5
- package/src/room.jsx +79 -51
- package/src/scene-objects.js +37 -1
- package/src/scenes.js +5 -0
- package/src/semantic-edit.js +2 -0
- package/src/settings-menu.jsx +39 -105
- package/src/shell/BottomDock.jsx +450 -0
- package/src/shell/DetailsSlot.jsx +513 -0
- package/src/shell/LibrarySlot.jsx +86 -0
- package/src/shell/MenuBar.jsx +601 -0
- package/src/shell/OutlinerSlot.jsx +50 -0
- package/src/shell/PreferencesDialog.jsx +538 -0
- package/src/shell/PreferencesSlot.jsx +40 -0
- package/src/shell/StatusBar.jsx +125 -0
- package/src/shell/StudioShell.jsx +92 -0
- package/src/shell/TopBar.jsx +129 -0
- package/src/shell/ViewportToolbar.jsx +656 -0
- package/src/shell/agent-glass.css +296 -0
- package/src/shell/dock.css +93 -0
- package/src/shell/glass-regions.css +985 -0
- package/src/shell/glass.css +687 -0
- package/src/shell/log-store.js +52 -0
- package/src/shell/mode.css +9 -0
- package/src/shell/preferences.css +651 -0
- package/src/shell/shell.css +282 -0
- package/src/shell/studio-shell-context.js +19 -0
- package/src/shell/topbar.css +435 -0
- package/src/shell/viewport.css +451 -0
- package/src/store/authored-intent.js +22 -0
- package/src/store/runtime-adapters.js +92 -0
- package/src/store/scene-stage.js +21 -0
- package/src/store/use-document-store.js +14 -0
- package/src/studio-actions.js +210 -0
- package/src/studio-agent-commands.js +41 -271
- package/src/studio-agent-context.js +39 -10
- package/src/studio-agent-motion.js +122 -373
- package/src/studio-agent-protocol.js +67 -31
- package/src/studio-app-binding.js +379 -0
- package/src/studio-contact-sheet.js +73 -0
- package/src/studio-elements.js +58 -51
- package/src/styles/themes.css +200 -0
- package/src/styles/tokens.css +69 -0
- package/src/styles.css +460 -1742
- package/src/theme.js +44 -0
- package/src/timeline-extent.js +16 -0
- package/src/trail-key-conflicts.js +39 -0
- package/src/trail-pick.js +59 -0
- package/src/ui.jsx +8 -8
- package/src/use-case-question.jsx +66 -0
- package/src/workflow/AgentPanel.jsx +82 -69
- package/src/workflow/agent-client.js +12 -2
- package/src/workflow/agent-panel.css +34 -32
- package/src/workflow/cozy-scene-node.css +2 -0
- package/src/workflow/workflow.css +2 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/ardy/bridge.mjs +164 -146
- package/tools/ardy/visual-qa.mjs +5 -5
- package/tools/bench/EXP3.md +61 -0
- package/tools/bench/cclay_bench_extract_incam.py +125 -0
- package/tools/bench/cclay_bench_extract_obs.py +204 -0
- package/tools/bench/cclay_bench_runner.py +42 -0
- package/tools/bench/cube-contact.mjs +162 -0
- package/tools/bench/exp3.mjs +152 -0
- package/tools/bench/extract-bench-lib.mjs +216 -0
- package/tools/bench/extract-bench.mjs +302 -0
- package/tools/bench/fal-generate.mjs +88 -0
- package/tools/bench/fit/README.md +189 -0
- package/tools/bench/fit/camera.mjs +59 -0
- package/tools/bench/fit/contact.mjs +419 -0
- package/tools/bench/fit/footlock.mjs +234 -0
- package/tools/bench/fit/motion.mjs +85 -0
- package/tools/bench/fit/pin.mjs +27 -0
- package/tools/bench/fit/remote.mjs +105 -0
- package/tools/bench/fit-bench.mjs +126 -0
- package/tools/bench/fit-sanity.mjs +45 -0
- package/tools/bench/metrics.mjs +313 -0
- package/tools/bench/obs/depth.mjs +258 -0
- package/tools/bench/obs/extrinsics.mjs +175 -0
- package/tools/bench/obs/fit_mannequin_betas.py +340 -0
- package/tools/bench/obs/ground.mjs +294 -0
- package/tools/bench/obs/heading.mjs +151 -0
- package/tools/bench/obs/ladder.mjs +555 -0
- package/tools/bench/obs/mannequin-betas.json +124 -0
- package/tools/bench/obs/remote.mjs +83 -0
- package/tools/bench/obs/rest_joints.py +52 -0
- package/tools/bench/obs/ybot-targets.mjs +49 -0
- package/tools/bench/obs-bench.mjs +520 -0
- package/tools/bench/score.mjs +343 -0
- package/tools/bench/summarize.mjs +86 -0
- package/tools/dev/pages/privacy.html +13 -8
- package/tools/gt-render/browser.mjs +159 -0
- package/tools/gt-render/camera-math.mjs +276 -0
- package/tools/gt-render/page.mjs +267 -0
- package/tools/gt-render/render.mjs +514 -0
- package/tools/gt-render/scene-box.mjs +31 -0
- package/tools/gt-render/take-transform.mjs +87 -0
- package/tools/morphgs/assets/gen_truth.py +39 -0
- package/tools/morphgs/assets/mesh_ori_rig.txt +27 -0
- package/tools/morphgs/assets/playback-check.mjs +30 -0
- package/tools/morphgs/demo-gate.sh +26 -0
- package/tools/morphgs/fbx2morphgs.mjs +68 -0
- package/tools/morphgs/morphgs-to-cskel27.mjs +105 -0
- package/tools/morphgs/patches/preprocess_src-none-mode.patch +76 -0
- package/tools/morphgs/setup-on-cluster.sh +126 -0
- package/tools/qa/css-rule-usage.mjs +283 -0
- package/tools/qa/studio-control-count.mjs +70 -32
- package/tools/run-tests.mjs +134 -12
- package/tools/track/DESIGN.md +513 -0
- package/tools/track/backfill-provenance.mjs +116 -0
- package/tools/track/budget.mjs +8 -0
- package/tools/track/check-rig.mjs +209 -0
- package/tools/track/diagnostics.schema.json +60 -0
- package/tools/track/export-rig.mjs +256 -0
- package/tools/track/fallback.mjs +7 -0
- package/tools/track/fk-parity-fixture.mjs +161 -0
- package/tools/track/gate.mjs +349 -0
- package/tools/track/masks.mjs +318 -0
- package/tools/track/metrics.mjs +201 -0
- package/tools/track/publish-obs.mjs +108 -0
- package/tools/track/py/check_env.py +61 -0
- package/tools/track/py/eval_lr.py +229 -0
- package/tools/track/py/lr_viterbi.py +294 -0
- package/tools/track/py/masks.py +405 -0
- package/tools/track/py/objective.py +382 -0
- package/tools/track/py/rig.py +508 -0
- package/tools/track/py/scene.py +326 -0
- package/tools/track/py/test_joint_indices.py +123 -0
- package/tools/track/py/test_lr_viterbi.py +195 -0
- package/tools/track/py/test_masks.py +229 -0
- package/tools/track/py/test_rig.py +288 -0
- package/tools/track/py/test_scene.py +354 -0
- package/tools/track/py/test_track.py +328 -0
- package/tools/track/py/track.py +411 -0
- package/tools/track/remote.mjs +140 -0
- package/tools/track/rig-dump.mjs +227 -0
- package/tools/track/run-box-tests.mjs +14 -0
- package/tools/track/run-box.mjs +168 -0
- package/tools/track/setup-box.sh +59 -0
- package/tools/track/study-2d.mjs +728 -0
- package/dist/assets/analytics-B1hnH66c.js +0 -1
- package/dist/assets/app-BWusbqgO.js +0 -4861
- package/dist/assets/app-qKDo4PBX.css +0 -1
- package/dist/assets/shot-prompt-CHUtw6af.js +0 -17
- package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
- package/dist/assets/workflow-CVwzMhz2.js +0 -179
- package/dist/assets/workflow-DsudxKHF.css +0 -1
- package/src/fal-motion-studio.jsx +0 -180
- package/src/scene-history.js +0 -129
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure scoring math for the mocap bench (#431): rigid/similarity alignment,
|
|
3
|
+
* pose and trajectory errors, mask IoU, box contact and timeline resampling.
|
|
4
|
+
* No DOM, no Three, no I/O; unit-tested by test/verify-bench-metrics.mjs.
|
|
5
|
+
*
|
|
6
|
+
* Conventions: points are [x, y, z] in the Studio scene (metres, +Y up).
|
|
7
|
+
* A yaw is a rotation about +Y with the same sign as src/ardy/motion-
|
|
8
|
+
* calibration.js: x' = cos*x + sin*z, z' = -sin*x + cos*z.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const DEG = Math.PI / 180;
|
|
12
|
+
|
|
13
|
+
export function rotateYaw([x, y, z], yawRad) {
|
|
14
|
+
const c = Math.cos(yawRad);
|
|
15
|
+
const s = Math.sin(yawRad);
|
|
16
|
+
return [c * x + s * z, y, -s * x + c * z];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
20
|
+
const dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
21
|
+
|
|
22
|
+
function centroid(points) {
|
|
23
|
+
const c = [0, 0, 0];
|
|
24
|
+
for (const p of points) {
|
|
25
|
+
c[0] += p[0];
|
|
26
|
+
c[1] += p[1];
|
|
27
|
+
c[2] += p[2];
|
|
28
|
+
}
|
|
29
|
+
return c.map((value) => value / points.length);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Least-squares yaw (about +Y) and ground-plane translation mapping `src`
|
|
34
|
+
* onto `dst` (paired points): dst ~ rotateYaw(src, yaw) + [tx, 0, tz].
|
|
35
|
+
* Height is neither rotated nor translated: both takes stand on the same
|
|
36
|
+
* floor, so a height error stays an error.
|
|
37
|
+
*/
|
|
38
|
+
export function fitYawTranslation(src, dst) {
|
|
39
|
+
if (src.length !== dst.length || !src.length) throw new Error("fitYawTranslation needs paired, non-empty point sets");
|
|
40
|
+
const cs = centroid(src);
|
|
41
|
+
const cd = centroid(dst);
|
|
42
|
+
// Maximise sum b . R(a) over theta: C = sum(ax bx + az bz), S = sum(az bx - ax bz).
|
|
43
|
+
let C = 0;
|
|
44
|
+
let S = 0;
|
|
45
|
+
for (let i = 0; i < src.length; i += 1) {
|
|
46
|
+
const ax = src[i][0] - cs[0];
|
|
47
|
+
const az = src[i][2] - cs[2];
|
|
48
|
+
const bx = dst[i][0] - cd[0];
|
|
49
|
+
const bz = dst[i][2] - cd[2];
|
|
50
|
+
C += ax * bx + az * bz;
|
|
51
|
+
S += az * bx - ax * bz;
|
|
52
|
+
}
|
|
53
|
+
const yawRad = C === 0 && S === 0 ? 0 : Math.atan2(S, C);
|
|
54
|
+
const rc = rotateYaw(cs, yawRad);
|
|
55
|
+
return { yawRad, yawDeg: yawRad / DEG, tx: cd[0] - rc[0], tz: cd[2] - rc[2] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function applyYawTranslation(point, { yawRad, tx, tz }) {
|
|
59
|
+
const r = rotateYaw(point, yawRad);
|
|
60
|
+
return [r[0] + tx, r[1], r[2] + tz];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Eigenvector of the largest eigenvalue of a symmetric 4x4 (cyclic Jacobi). */
|
|
64
|
+
function dominantEigenvector4(input) {
|
|
65
|
+
const a = input.map((row) => row.slice());
|
|
66
|
+
const v = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]];
|
|
67
|
+
for (let sweep = 0; sweep < 64; sweep += 1) {
|
|
68
|
+
let off = 0;
|
|
69
|
+
for (let p = 0; p < 4; p += 1) for (let q = p + 1; q < 4; q += 1) off += a[p][q] * a[p][q];
|
|
70
|
+
if (off < 1e-30) break;
|
|
71
|
+
for (let p = 0; p < 4; p += 1) {
|
|
72
|
+
for (let q = p + 1; q < 4; q += 1) {
|
|
73
|
+
if (Math.abs(a[p][q]) < 1e-300) continue;
|
|
74
|
+
const theta = (a[q][q] - a[p][p]) / (2 * a[p][q]);
|
|
75
|
+
const t = Math.sign(theta || 1) / (Math.abs(theta) + Math.sqrt(theta * theta + 1));
|
|
76
|
+
const c = 1 / Math.sqrt(t * t + 1);
|
|
77
|
+
const s = t * c;
|
|
78
|
+
for (let k = 0; k < 4; k += 1) {
|
|
79
|
+
const akp = a[k][p];
|
|
80
|
+
const akq = a[k][q];
|
|
81
|
+
a[k][p] = c * akp - s * akq;
|
|
82
|
+
a[k][q] = s * akp + c * akq;
|
|
83
|
+
}
|
|
84
|
+
for (let k = 0; k < 4; k += 1) {
|
|
85
|
+
const apk = a[p][k];
|
|
86
|
+
const aqk = a[q][k];
|
|
87
|
+
a[p][k] = c * apk - s * aqk;
|
|
88
|
+
a[q][k] = s * apk + c * aqk;
|
|
89
|
+
}
|
|
90
|
+
for (let k = 0; k < 4; k += 1) {
|
|
91
|
+
const vkp = v[k][p];
|
|
92
|
+
const vkq = v[k][q];
|
|
93
|
+
v[k][p] = c * vkp - s * vkq;
|
|
94
|
+
v[k][q] = s * vkp + c * vkq;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
let best = 0;
|
|
100
|
+
for (let i = 1; i < 4; i += 1) if (a[i][i] > a[best][best]) best = i;
|
|
101
|
+
return [v[0][best], v[1][best], v[2][best], v[3][best]];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Similarity Procrustes (Horn's quaternion method, proper rotation only):
|
|
106
|
+
* scale, R, t minimising sum |s R src_i + t - dst_i|^2.
|
|
107
|
+
* Returns { scale, R (row-major 3x3), t, aligned }.
|
|
108
|
+
*/
|
|
109
|
+
export function similarityAlign(src, dst, { allowScale = true } = {}) {
|
|
110
|
+
if (src.length !== dst.length || !src.length) throw new Error("similarityAlign needs paired, non-empty point sets");
|
|
111
|
+
const cs = centroid(src);
|
|
112
|
+
const cd = centroid(dst);
|
|
113
|
+
const S = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
|
|
114
|
+
let srcNorm = 0;
|
|
115
|
+
for (let i = 0; i < src.length; i += 1) {
|
|
116
|
+
const a = sub(src[i], cs);
|
|
117
|
+
const b = sub(dst[i], cd);
|
|
118
|
+
srcNorm += a[0] * a[0] + a[1] * a[1] + a[2] * a[2];
|
|
119
|
+
for (let r = 0; r < 3; r += 1) for (let c = 0; c < 3; c += 1) S[r][c] += a[r] * b[c];
|
|
120
|
+
}
|
|
121
|
+
const [[xx, xy, xz], [yx, yy, yz], [zx, zy, zz]] = S;
|
|
122
|
+
const N = [
|
|
123
|
+
[xx + yy + zz, yz - zy, zx - xz, xy - yx],
|
|
124
|
+
[yz - zy, xx - yy - zz, xy + yx, zx + xz],
|
|
125
|
+
[zx - xz, xy + yx, -xx + yy - zz, yz + zy],
|
|
126
|
+
[xy - yx, zx + xz, yz + zy, -xx - yy + zz],
|
|
127
|
+
];
|
|
128
|
+
const [w, x, y, z] = dominantEigenvector4(N);
|
|
129
|
+
const n = Math.hypot(w, x, y, z) || 1;
|
|
130
|
+
const q = [w / n, x / n, y / n, z / n];
|
|
131
|
+
const R = [
|
|
132
|
+
1 - 2 * (q[2] * q[2] + q[3] * q[3]), 2 * (q[1] * q[2] - q[0] * q[3]), 2 * (q[1] * q[3] + q[0] * q[2]),
|
|
133
|
+
2 * (q[1] * q[2] + q[0] * q[3]), 1 - 2 * (q[1] * q[1] + q[3] * q[3]), 2 * (q[2] * q[3] - q[0] * q[1]),
|
|
134
|
+
2 * (q[1] * q[3] - q[0] * q[2]), 2 * (q[2] * q[3] + q[0] * q[1]), 1 - 2 * (q[1] * q[1] + q[2] * q[2]),
|
|
135
|
+
];
|
|
136
|
+
const rot = (p) => [R[0] * p[0] + R[1] * p[1] + R[2] * p[2], R[3] * p[0] + R[4] * p[1] + R[5] * p[2], R[6] * p[0] + R[7] * p[1] + R[8] * p[2]];
|
|
137
|
+
let dot = 0;
|
|
138
|
+
for (let i = 0; i < src.length; i += 1) {
|
|
139
|
+
const ra = rot(sub(src[i], cs));
|
|
140
|
+
const b = sub(dst[i], cd);
|
|
141
|
+
dot += ra[0] * b[0] + ra[1] * b[1] + ra[2] * b[2];
|
|
142
|
+
}
|
|
143
|
+
const scale = allowScale && srcNorm > 0 ? dot / srcNorm : 1;
|
|
144
|
+
const rcs = rot(cs);
|
|
145
|
+
const t = [cd[0] - scale * rcs[0], cd[1] - scale * rcs[1], cd[2] - scale * rcs[2]];
|
|
146
|
+
const aligned = src.map((p) => {
|
|
147
|
+
const r = rot(p);
|
|
148
|
+
return [scale * r[0] + t[0], scale * r[1] + t[1], scale * r[2] + t[2]];
|
|
149
|
+
});
|
|
150
|
+
return { scale, R, t, aligned };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Mean per-joint Euclidean error of two paired joint sets. */
|
|
154
|
+
export function meanJointError(pred, gt) {
|
|
155
|
+
if (pred.length !== gt.length || !pred.length) throw new Error("meanJointError needs paired, non-empty joint sets");
|
|
156
|
+
let total = 0;
|
|
157
|
+
for (let i = 0; i < pred.length; i += 1) total += dist(pred[i], gt[i]);
|
|
158
|
+
return total / pred.length;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** MPJPE after subtracting each set's own root joint. */
|
|
162
|
+
export function rootRelativeError(pred, gt, rootIndex = 0) {
|
|
163
|
+
const pr = pred[rootIndex];
|
|
164
|
+
const gr = gt[rootIndex];
|
|
165
|
+
return meanJointError(pred.map((p) => sub(p, pr)), gt.map((g) => sub(g, gr)));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** MPJPE after per-frame similarity Procrustes of pred onto gt. */
|
|
169
|
+
export function procrustesError(pred, gt) {
|
|
170
|
+
return meanJointError(similarityAlign(pred, gt).aligned, gt);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Per-frame position errors of two paired trajectories, summarised. */
|
|
174
|
+
export function trajectoryError(pred, gt) {
|
|
175
|
+
if (pred.length !== gt.length || !pred.length) throw new Error("trajectoryError needs paired, non-empty trajectories");
|
|
176
|
+
const errors = pred.map((p, i) => dist(p, gt[i]));
|
|
177
|
+
const mean = errors.reduce((a, b) => a + b, 0) / errors.length;
|
|
178
|
+
const rmse = Math.sqrt(errors.reduce((a, b) => a + b * b, 0) / errors.length);
|
|
179
|
+
return { errors, mean, rmse, max: Math.max(...errors), final: errors[errors.length - 1] };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Summed ground-plane (XZ) step length of a trajectory. */
|
|
183
|
+
export function pathLengthXZ(points) {
|
|
184
|
+
let total = 0;
|
|
185
|
+
for (let i = 1; i < points.length; i += 1) total += Math.hypot(points[i][0] - points[i - 1][0], points[i][2] - points[i - 1][2]);
|
|
186
|
+
return total;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Ground-plane (XZ) distance from the first to the last point. */
|
|
190
|
+
export function displacementXZ(points) {
|
|
191
|
+
const a = points[0];
|
|
192
|
+
const b = points[points.length - 1];
|
|
193
|
+
return Math.hypot(b[0] - a[0], b[2] - a[2]);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** pred / gt, or null when gt is 0 (a ratio of nothing is not a number). */
|
|
197
|
+
export function ratio(pred, gt) {
|
|
198
|
+
return gt > 0 ? pred / gt : null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Intersection over union of two 8-bit masks (pixel set where value >=
|
|
203
|
+
* threshold). Two empty masks agree perfectly: IoU 1.
|
|
204
|
+
*/
|
|
205
|
+
export function maskIoU(a, b, threshold = 128) {
|
|
206
|
+
if (a.length !== b.length) throw new Error(`maskIoU: mask sizes differ (${a.length} vs ${b.length})`);
|
|
207
|
+
let intersection = 0;
|
|
208
|
+
let union = 0;
|
|
209
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
210
|
+
const x = a[i] >= threshold;
|
|
211
|
+
const y = b[i] >= threshold;
|
|
212
|
+
if (x && y) intersection += 1;
|
|
213
|
+
if (x || y) union += 1;
|
|
214
|
+
}
|
|
215
|
+
return { iou: union ? intersection / union : 1, intersection, union };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Signed distance of every point of a flat [x0, y0, z0, ...] array to an
|
|
220
|
+
* axis-aligned box { min: [x, y, z], max: [x, y, z] }: positive outside
|
|
221
|
+
* (Euclidean distance to the box), negative inside (minus the distance to
|
|
222
|
+
* the nearest face). Reduced to the minimum and the count inside.
|
|
223
|
+
*
|
|
224
|
+
* Self-contained on purpose (no outer references): render.mjs injects its
|
|
225
|
+
* source into the Studio page with Function.prototype.toString.
|
|
226
|
+
*/
|
|
227
|
+
export function boxContactValues(points, box) {
|
|
228
|
+
const [x0, y0, z0] = box.min;
|
|
229
|
+
const [x1, y1, z1] = box.max;
|
|
230
|
+
let minSigned = Infinity;
|
|
231
|
+
let closest = -1;
|
|
232
|
+
let inside = 0;
|
|
233
|
+
const count = Math.floor(points.length / 3);
|
|
234
|
+
for (let i = 0; i < count; i += 1) {
|
|
235
|
+
const x = points[i * 3];
|
|
236
|
+
const y = points[i * 3 + 1];
|
|
237
|
+
const z = points[i * 3 + 2];
|
|
238
|
+
const dx = Math.max(x0 - x, x - x1);
|
|
239
|
+
const dy = Math.max(y0 - y, y - y1);
|
|
240
|
+
const dz = Math.max(z0 - z, z - z1);
|
|
241
|
+
let signed;
|
|
242
|
+
if (dx <= 0 && dy <= 0 && dz <= 0) {
|
|
243
|
+
signed = Math.max(dx, dy, dz);
|
|
244
|
+
inside += 1;
|
|
245
|
+
} else {
|
|
246
|
+
signed = Math.hypot(Math.max(dx, 0), Math.max(dy, 0), Math.max(dz, 0));
|
|
247
|
+
}
|
|
248
|
+
if (signed < minSigned) {
|
|
249
|
+
minSigned = signed;
|
|
250
|
+
closest = i;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return { minSignedDistance: minSigned, closestIndex: closest, insideCount: inside, count };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Contact summary of one frame: closest approach (0 when touching or
|
|
257
|
+
* inside) and deepest penetration (0 when nothing is inside). */
|
|
258
|
+
export function contactFromSigned(minSignedDistance) {
|
|
259
|
+
return { minDistanceM: Math.max(0, minSignedDistance), maxPenetrationM: Math.max(0, -minSignedDistance) };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Validate a box given as { min: [x,y,z], max: [x,y,z] }. */
|
|
263
|
+
export function parseBox(value) {
|
|
264
|
+
const box = typeof value === "string" ? JSON.parse(value) : value;
|
|
265
|
+
const ok = (v) => Array.isArray(v) && v.length === 3 && v.every(Number.isFinite);
|
|
266
|
+
if (!box || !ok(box.min) || !ok(box.max)) throw new Error("box must be {\"min\":[x,y,z],\"max\":[x,y,z]} in scene metres");
|
|
267
|
+
if (box.min.some((v, i) => v > box.max[i])) throw new Error("box min must not exceed max on any axis");
|
|
268
|
+
return { min: box.min.slice(), max: box.max.slice() };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Map every GT timeline frame to the nearest-in-time pred frame. A GT frame
|
|
273
|
+
* whose time lies past the pred's last frame (by more than half a pred frame)
|
|
274
|
+
* is not scored. Returns { predIndex: (number|null)[], scored, dropped }.
|
|
275
|
+
*/
|
|
276
|
+
export function resampleToTimeline({ gtFrames, gtFps, predFrames, predFps }) {
|
|
277
|
+
if (!(gtFps > 0) || !(predFps > 0)) throw new Error("resampleToTimeline needs positive frame rates");
|
|
278
|
+
const predIndex = [];
|
|
279
|
+
let scored = 0;
|
|
280
|
+
for (let i = 0; i < gtFrames; i += 1) {
|
|
281
|
+
const k = Math.round((i / gtFps) * predFps);
|
|
282
|
+
if (k <= predFrames - 1) {
|
|
283
|
+
predIndex.push(k);
|
|
284
|
+
scored += 1;
|
|
285
|
+
} else {
|
|
286
|
+
predIndex.push(null);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return { predIndex, scored, dropped: gtFrames - scored, exact: gtFps === predFps && gtFrames === predFrames };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The scene calibration (Studio sceneCalibration semantics: yaw about the
|
|
294
|
+
* take's anchor A, then offset) that equals applying G(p) = rotateYaw(p, g.yaw)
|
|
295
|
+
* + [g.tx, 0, g.tz] after an existing calibration { yawDeg, offsetX, offsetZ }:
|
|
296
|
+
* G(A + o + Ry(psi)(U - A)) = A + o' + Ry(psi + theta)(U - A),
|
|
297
|
+
* o' = Ry(theta)(A + o) + t - A.
|
|
298
|
+
*/
|
|
299
|
+
export function composeCalibration(calibration, anchor, g) {
|
|
300
|
+
const base = [anchor.x + (calibration.offsetX ?? 0), 0, anchor.z + (calibration.offsetZ ?? 0)];
|
|
301
|
+
const moved = applyYawTranslation(base, g);
|
|
302
|
+
return {
|
|
303
|
+
...calibration,
|
|
304
|
+
yawDeg: (calibration.yawDeg ?? 0) + g.yawDeg,
|
|
305
|
+
offsetX: moved[0] - anchor.x,
|
|
306
|
+
offsetZ: moved[2] - anchor.z,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function mean(values) {
|
|
311
|
+
const finite = values.filter((v) => v !== null && Number.isFinite(v));
|
|
312
|
+
return finite.length ? finite.reduce((a, b) => a + b, 0) / finite.length : null;
|
|
313
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
const COCO_TO_SMPL = [
|
|
2
|
+
[5, 16], [6, 17], [7, 18], [8, 19], [9, 20], [10, 21],
|
|
3
|
+
[11, 1], [12, 2], [13, 4], [14, 5], [15, 7], [16, 8],
|
|
4
|
+
];
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MIN_CONF = 0.3;
|
|
7
|
+
const DEFAULT_SIGMA = 2;
|
|
8
|
+
const EPSILON = 1e-10;
|
|
9
|
+
|
|
10
|
+
function valuesOf(value) {
|
|
11
|
+
return value?.data ?? value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function shapeOf(value) {
|
|
15
|
+
return value?.shape ? Array.from(value.shape) : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function flatValues(value, name) {
|
|
19
|
+
const data = valuesOf(value);
|
|
20
|
+
if (ArrayBuffer.isView(data) || Array.isArray(data)) return data;
|
|
21
|
+
throw new TypeError(`${name}: expected an array or a shaped array`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function frameRows(value, frames, width, components, name) {
|
|
25
|
+
const shape = shapeOf(value);
|
|
26
|
+
const data = flatValues(value, name);
|
|
27
|
+
if (shape) {
|
|
28
|
+
const expected = [frames, width, components];
|
|
29
|
+
if (shape.length !== 3 || shape.some((n, i) => n !== expected[i])) {
|
|
30
|
+
throw new Error(`${name}: expected shape [${expected.join(", ")}]`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(data) && data.length === frames && data.every(row => Array.isArray(row))) {
|
|
34
|
+
return data.map((row, frame) => {
|
|
35
|
+
if (row.length !== width || row.some(point => !Array.isArray(point) && !ArrayBuffer.isView(point))) {
|
|
36
|
+
throw new Error(`${name}[${frame}]: expected [${width}, ${components}]`);
|
|
37
|
+
}
|
|
38
|
+
return row.map((point, index) => {
|
|
39
|
+
if (point.length !== components) throw new Error(`${name}[${frame}][${index}]: expected ${components} values`);
|
|
40
|
+
return Array.from(point, Number);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (data.length !== frames * width * components) {
|
|
45
|
+
throw new Error(`${name}: expected ${frames * width * components} values`);
|
|
46
|
+
}
|
|
47
|
+
return Array.from({ length: frames }, (_, frame) => Array.from({ length: width }, (_, index) => {
|
|
48
|
+
const offset = (frame * width + index) * components;
|
|
49
|
+
return Array.from(data.slice(offset, offset + components), Number);
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function vectorFrames(value, frames, width, name) {
|
|
54
|
+
const shape = shapeOf(value);
|
|
55
|
+
const data = flatValues(value, name);
|
|
56
|
+
if (shape) {
|
|
57
|
+
const expected = [frames, width];
|
|
58
|
+
if (shape.length !== 2 || shape.some((n, i) => n !== expected[i])) {
|
|
59
|
+
throw new Error(`${name}: expected shape [${expected.join(", ")}]`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(data) && data.length === frames && data.every(row => Array.isArray(row) || ArrayBuffer.isView(row))) {
|
|
63
|
+
return data.map((row, frame) => {
|
|
64
|
+
if (row.length !== width) throw new Error(`${name}[${frame}]: expected ${width} values`);
|
|
65
|
+
return Array.from(row, Number);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (data.length !== frames * width) throw new Error(`${name}: expected ${frames * width} values`);
|
|
69
|
+
return Array.from({ length: frames }, (_, frame) => Array.from(data.slice(frame * width, (frame + 1) * width), Number));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function matrix3(K) {
|
|
73
|
+
const data = valuesOf(K);
|
|
74
|
+
const shape = shapeOf(K);
|
|
75
|
+
if (shape && (shape.length !== 2 || shape[0] !== 3 || shape[1] !== 3)) throw new Error("K: expected shape [3, 3]");
|
|
76
|
+
const matrix = Array.isArray(data) && data.length === 3 && data.every(row => Array.isArray(row) || ArrayBuffer.isView(row))
|
|
77
|
+
? data.map(row => Array.from(row, Number))
|
|
78
|
+
: data?.length === 9 ? Array.from({ length: 3 }, (_, row) => Array.from(data.slice(row * 3, row * 3 + 3), Number)) : null;
|
|
79
|
+
if (!matrix || matrix.some(row => row.length !== 3 || row.some(value => !Number.isFinite(value)))) throw new Error("K: expected a finite 3x3 matrix");
|
|
80
|
+
if (!(Math.abs(matrix[0][0]) > EPSILON) || !(Math.abs(matrix[1][1]) > EPSILON)) throw new Error("K: fx and fy must be nonzero");
|
|
81
|
+
return matrix;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function finiteVector(value, fallback = [0, 0, 0]) {
|
|
85
|
+
return (Array.isArray(value) || ArrayBuffer.isView(value)) && value.length === 3 && Array.from(value).every(Number.isFinite)
|
|
86
|
+
? Array.from(value, Number) : fallback.slice();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function addNormal(normal, rhs, row, target, weight) {
|
|
90
|
+
for (let i = 0; i < 3; i++) {
|
|
91
|
+
for (let j = 0; j < 3; j++) normal[i][j] += weight * row[i] * row[j];
|
|
92
|
+
rhs[i] += weight * row[i] * target;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function solve3(normal, rhs) {
|
|
97
|
+
const a = normal.map((row, i) => [...row, rhs[i]]);
|
|
98
|
+
for (let column = 0; column < 3; column++) {
|
|
99
|
+
let pivot = column;
|
|
100
|
+
for (let row = column + 1; row < 3; row++) {
|
|
101
|
+
if (Math.abs(a[row][column]) > Math.abs(a[pivot][column])) pivot = row;
|
|
102
|
+
}
|
|
103
|
+
if (Math.abs(a[pivot][column]) <= EPSILON) return null;
|
|
104
|
+
[a[column], a[pivot]] = [a[pivot], a[column]];
|
|
105
|
+
for (let row = column + 1; row < 3; row++) {
|
|
106
|
+
const scale = a[row][column] / a[column][column];
|
|
107
|
+
for (let col = column; col < 4; col++) a[row][col] -= scale * a[column][col];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const answer = [0, 0, 0];
|
|
111
|
+
for (let row = 2; row >= 0; row--) {
|
|
112
|
+
let value = a[row][3];
|
|
113
|
+
for (let col = row + 1; col < 3; col++) value -= a[row][col] * answer[col];
|
|
114
|
+
answer[row] = value / a[row][row];
|
|
115
|
+
}
|
|
116
|
+
return answer.every(Number.isFinite) ? answer : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function observations(joints, keypoints, K, minConf) {
|
|
120
|
+
const fx = K[0][0], fy = K[1][1], cx = K[0][2], cy = K[1][2];
|
|
121
|
+
return COCO_TO_SMPL.flatMap(([coco, smpl]) => {
|
|
122
|
+
const point = keypoints[coco], joint = joints[smpl];
|
|
123
|
+
if (!point || !joint || point.length < 3 || joint.length < 3) return [];
|
|
124
|
+
const confidence = Number(point[2]);
|
|
125
|
+
if (!(confidence >= minConf) || !joint.every(Number.isFinite) || !point.slice(0, 2).every(Number.isFinite)) return [];
|
|
126
|
+
return [{ X: joint[0], Y: joint[1], Z: joint[2], u: point[0], v: point[1], confidence }];
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function linearTranslation(items, K) {
|
|
131
|
+
const fx = K[0][0], fy = K[1][1], cx = K[0][2], cy = K[1][2];
|
|
132
|
+
const normal = Array.from({ length: 3 }, () => [0, 0, 0]);
|
|
133
|
+
const rhs = [0, 0, 0];
|
|
134
|
+
for (const item of items) {
|
|
135
|
+
const weight = item.confidence;
|
|
136
|
+
// fx*(X+tx) + (cx-u)*(Z+tz) = 0, and the equivalent y equation.
|
|
137
|
+
addNormal(normal, rhs, [fx, 0, cx - item.u], item.u * item.Z - fx * item.X, weight);
|
|
138
|
+
addNormal(normal, rhs, [0, fy, cy - item.v], item.v * item.Z - fy * item.Y, weight);
|
|
139
|
+
}
|
|
140
|
+
return solve3(normal, rhs);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function refineTranslation(start, items, K, iterations = 5) {
|
|
144
|
+
const fx = K[0][0], fy = K[1][1];
|
|
145
|
+
const translation = start.slice();
|
|
146
|
+
for (let iteration = 0; iteration < iterations; iteration++) {
|
|
147
|
+
const normal = Array.from({ length: 3 }, () => [0, 0, 0]);
|
|
148
|
+
const rhs = [0, 0, 0];
|
|
149
|
+
let rows = 0;
|
|
150
|
+
for (const item of items) {
|
|
151
|
+
const x = item.X + translation[0], y = item.Y + translation[1], z = item.Z + translation[2];
|
|
152
|
+
if (!(z > EPSILON)) continue;
|
|
153
|
+
const u = fx * x / z + K[0][2], v = fy * y / z + K[1][2];
|
|
154
|
+
const du = u - item.u, dv = v - item.v;
|
|
155
|
+
const ju = [fx / z, 0, -fx * x / (z * z)];
|
|
156
|
+
const jv = [0, fy / z, -fy * y / (z * z)];
|
|
157
|
+
addNormal(normal, rhs, ju, -du, item.confidence);
|
|
158
|
+
addNormal(normal, rhs, jv, -dv, item.confidence);
|
|
159
|
+
rows += 2;
|
|
160
|
+
}
|
|
161
|
+
if (rows < 3) break;
|
|
162
|
+
const delta = solve3(normal, rhs);
|
|
163
|
+
if (!delta) break;
|
|
164
|
+
translation[0] += delta[0];
|
|
165
|
+
translation[1] += delta[1];
|
|
166
|
+
translation[2] += delta[2];
|
|
167
|
+
if (Math.hypot(...delta) < 1e-8) break;
|
|
168
|
+
}
|
|
169
|
+
return translation;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function residual(translation, items, K) {
|
|
173
|
+
let error = 0;
|
|
174
|
+
let weight = 0;
|
|
175
|
+
for (const item of items) {
|
|
176
|
+
const z = item.Z + translation[2];
|
|
177
|
+
if (!(z > EPSILON)) continue;
|
|
178
|
+
const u = K[0][0] * (item.X + translation[0]) / z + K[0][2];
|
|
179
|
+
const v = K[1][1] * (item.Y + translation[1]) / z + K[1][2];
|
|
180
|
+
error += item.confidence * ((u - item.u) ** 2 + (v - item.v) ** 2);
|
|
181
|
+
weight += item.confidence;
|
|
182
|
+
}
|
|
183
|
+
return weight > 0 ? Math.sqrt(error / weight) : Infinity;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function gaussianSmooth(translations, sigma) {
|
|
187
|
+
if (!(sigma > 0) || translations.length < 2) return translations.map(translation => translation.slice());
|
|
188
|
+
const radius = Math.ceil(3 * sigma);
|
|
189
|
+
return translations.map((_, frame) => {
|
|
190
|
+
const out = [0, 0, 0];
|
|
191
|
+
let total = 0;
|
|
192
|
+
for (let other = Math.max(0, frame - radius); other <= Math.min(translations.length - 1, frame + radius); other++) {
|
|
193
|
+
const distance = other - frame;
|
|
194
|
+
const weight = Math.exp(-(distance * distance) / (2 * sigma * sigma));
|
|
195
|
+
total += weight;
|
|
196
|
+
for (let axis = 0; axis < 3; axis++) out[axis] += weight * translations[other][axis];
|
|
197
|
+
}
|
|
198
|
+
return out.map(value => value / total);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function guardJumps(translations, maxJump) {
|
|
203
|
+
if (!(maxJump > 0) || !Number.isFinite(maxJump)) return translations;
|
|
204
|
+
const guarded = [];
|
|
205
|
+
for (const translation of translations) {
|
|
206
|
+
if (!guarded.length) {
|
|
207
|
+
guarded.push(translation.slice());
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const previous = guarded.at(-1);
|
|
211
|
+
const delta = translation.map((value, axis) => value - previous[axis]);
|
|
212
|
+
const distance = Math.hypot(...delta);
|
|
213
|
+
if (distance <= maxJump) guarded.push(translation.slice());
|
|
214
|
+
else {
|
|
215
|
+
const scale = maxJump / distance;
|
|
216
|
+
guarded.push(previous.map((value, axis) => value + delta[axis] * scale));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return guarded;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Recover per-frame camera translation from known-size, root-relative SMPL-X
|
|
224
|
+
* joints and COCO-17 observations. `incamTransl` is accepted as the fallback
|
|
225
|
+
* translation because the root-relative joints alone do not contain one.
|
|
226
|
+
*/
|
|
227
|
+
export function solveTranslations({ jointsRel, kp2d, K, minConf = DEFAULT_MIN_CONF, sigma = DEFAULT_SIGMA, incamTransl, maxJump = Infinity } = {}) {
|
|
228
|
+
const keypointData = flatValues(kp2d, "kp2d");
|
|
229
|
+
const keypointShape = shapeOf(kp2d);
|
|
230
|
+
const frames = keypointShape?.[0] ?? (Array.isArray(keypointData) && keypointData.every(row => Array.isArray(row) || ArrayBuffer.isView(row)) ? keypointData.length : Math.floor(keypointData.length / (17 * 3)));
|
|
231
|
+
if (!Number.isInteger(frames) || frames < 1) throw new Error("kp2d: expected at least one frame");
|
|
232
|
+
const joints = frameRows(jointsRel, frames, 22, 3, "jointsRel");
|
|
233
|
+
const keypoints = frameRows(kp2d, frames, 17, 3, "kp2d");
|
|
234
|
+
const intrinsics = matrix3(K);
|
|
235
|
+
if (!(Number.isFinite(minConf) && minConf >= 0)) throw new Error("minConf: expected a nonnegative number");
|
|
236
|
+
if (!(Number.isFinite(sigma) && sigma >= 0)) throw new Error("sigma: expected a nonnegative number");
|
|
237
|
+
const fallback = incamTransl === undefined
|
|
238
|
+
? Array.from({ length: frames }, () => [0, 0, 0])
|
|
239
|
+
: vectorFrames(incamTransl, frames, 3, "incamTransl");
|
|
240
|
+
const rawTranslations = [];
|
|
241
|
+
const flags = [];
|
|
242
|
+
const frameItems = [];
|
|
243
|
+
for (let frame = 0; frame < frames; frame++) {
|
|
244
|
+
const items = observations(joints[frame], keypoints[frame], intrinsics, minConf);
|
|
245
|
+
let translation = items.length >= 4 ? linearTranslation(items, intrinsics) : null;
|
|
246
|
+
const usedFallback = items.length < 4 || !translation;
|
|
247
|
+
if (usedFallback) translation = finiteVector(fallback[frame]);
|
|
248
|
+
else translation = refineTranslation(translation, items, intrinsics);
|
|
249
|
+
rawTranslations.push(translation);
|
|
250
|
+
flags.push(usedFallback);
|
|
251
|
+
frameItems.push(items);
|
|
252
|
+
}
|
|
253
|
+
const translations = guardJumps(gaussianSmooth(rawTranslations, sigma), maxJump);
|
|
254
|
+
const residualPx = translations.map((translation, frame) => residual(translation, frameItems[frame], intrinsics));
|
|
255
|
+
return { transl: translations, flags, residualPx };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export { COCO_TO_SMPL };
|