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,382 @@
|
|
|
1
|
+
"""Image and physical terms for the deterministic cskel27 clip fit.
|
|
2
|
+
|
|
3
|
+
No image renderer is needed: exterior signed-DT samples prevent spill, and a
|
|
4
|
+
foreground-to-surface Chamfer prevents shrinkage. Nearest-neighbour selection
|
|
5
|
+
is discrete; its selected squared distance retains the exact local gradient.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import torch
|
|
11
|
+
from scipy.ndimage import distance_transform_edt
|
|
12
|
+
|
|
13
|
+
from rig import State, cskel27_fk, studio_skin
|
|
14
|
+
from scene import (FOOT_JOINTS, SceneBoxes, camera_center, foot_points, penetration_loss,
|
|
15
|
+
ray_occlusion, skate_loss, support_height)
|
|
16
|
+
|
|
17
|
+
# Face landmarks have no cskel27 counterpart and receive zero data weight.
|
|
18
|
+
COCO_JOINTS = [5, 5, 5, 5, 5, 14, 8, 15, 9, 16, 10, 23, 19, 24, 20, 25, 21]
|
|
19
|
+
# Bilateral Shoulder, Arm, ForeArm, Hand; exclude HandEnd/Thumb on both sides.
|
|
20
|
+
ARM_TRUST_JOINTS = [7, 8, 9, 10, 13, 14, 15, 16]
|
|
21
|
+
JOINT_CAPS_DEG = [180, 65, 65, 65, 90, 90, 180,
|
|
22
|
+
90, 175, 175, 110, 150, 180,
|
|
23
|
+
90, 175, 175, 110, 150, 180,
|
|
24
|
+
160, 175, 100, 80, 160, 175, 100, 80]
|
|
25
|
+
|
|
26
|
+
# --track-ablate levels. Each level adds one component to the previous one;
|
|
27
|
+
# keypoints and priors (acceleration, limits, endpoints, nuisance) are always
|
|
28
|
+
# on, and weights/schedule are identical at every level. 'occlusion' is the
|
|
29
|
+
# ray/box visibility of keypoints and surface samples in the fit.
|
|
30
|
+
ABLATIONS = {
|
|
31
|
+
'kp-only': frozenset(),
|
|
32
|
+
'silhouette': frozenset({'silhouette'}),
|
|
33
|
+
'viterbi': frozenset({'silhouette', 'viterbi'}),
|
|
34
|
+
'contacts': frozenset({'silhouette', 'viterbi', 'contacts'}),
|
|
35
|
+
'full': frozenset({'silhouette', 'viterbi', 'contacts', 'occlusion'}),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def huber(x, delta=1.0):
|
|
40
|
+
return torch.where(x <= delta, 0.5 * x.square(), delta * (x - 0.5 * delta))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def project(points, camera, nuisance):
|
|
44
|
+
"""OpenCV projection; nuisance = scale offset, yaw/pitch degrees, focal %."""
|
|
45
|
+
xyz = points @ camera[0].T + camera[1]
|
|
46
|
+
yaw, pitch = nuisance[1:3] * (np.pi / 180)
|
|
47
|
+
cy, sy, cp, sp = yaw.cos(), yaw.sin(), pitch.cos(), pitch.sin()
|
|
48
|
+
x, y, z = xyz.unbind(-1)
|
|
49
|
+
x, z = cy * x + sy * z, -sy * x + cy * z
|
|
50
|
+
y, z = cp * y - sp * z, sp * y + cp * z
|
|
51
|
+
xy = torch.stack((x, y), -1) / z.clamp_min(1e-3).unsqueeze(-1)
|
|
52
|
+
K = camera[2]
|
|
53
|
+
return xy * torch.stack((K[0, 0], K[1, 1])) * (1 + nuisance[3] / 100) + K[:2, 2], z
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def camera_tensors(camera, device):
|
|
57
|
+
w = torch.tensor(camera['worldToCamera'], dtype=torch.float32, device=device)
|
|
58
|
+
return w[:3, :3], w[:3, 3], torch.tensor(camera['K'], dtype=torch.float32, device=device)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def slice_boxes(boxes, start, end):
|
|
62
|
+
return SceneBoxes(boxes.centers[:, start:end], boxes.half, boxes.yaw[:, start:end])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class MaskEvidence:
|
|
66
|
+
def __init__(self, prob, width, height, count=1000):
|
|
67
|
+
self.binary = np.asarray(prob) >= 0.5
|
|
68
|
+
self.ratio = np.array([prob.shape[2] / width, prob.shape[1] / height], np.float32)
|
|
69
|
+
rng = np.random.default_rng(0)
|
|
70
|
+
distances, targets, valid = [], [], []
|
|
71
|
+
for mask in self.binary:
|
|
72
|
+
distances.append(distance_transform_edt(~mask) - distance_transform_edt(mask))
|
|
73
|
+
y, x = np.nonzero(mask)
|
|
74
|
+
if len(x):
|
|
75
|
+
# Stratification avoids over-representing a dense image region.
|
|
76
|
+
idx = np.minimum(((np.arange(count) + rng.random(count)) * len(x) / count).astype(int), len(x) - 1)
|
|
77
|
+
targets.append(np.stack((x[idx] + 0.5, y[idx] + 0.5), -1))
|
|
78
|
+
valid.append(True)
|
|
79
|
+
else:
|
|
80
|
+
targets.append(np.zeros((count, 2)))
|
|
81
|
+
valid.append(False)
|
|
82
|
+
self.dt = np.asarray(distances, np.float32)
|
|
83
|
+
self.targets = np.asarray(targets, np.float32)
|
|
84
|
+
self.valid = np.asarray(valid)
|
|
85
|
+
|
|
86
|
+
def window(self, start, end, device):
|
|
87
|
+
return tuple(torch.as_tensor(x, device=device) for x in
|
|
88
|
+
(self.dt[start:end], self.targets[start:end], self.valid[start:end], self.ratio))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def bilinear_dt(dt, uv):
|
|
92
|
+
"""Pixel centres are at n+0.5. Include off-image distance (no border escape)."""
|
|
93
|
+
h, w = dt.shape[-2:]
|
|
94
|
+
q = uv - 0.5
|
|
95
|
+
x, y = q[..., 0].clamp(0, w - 1.001), q[..., 1].clamp(0, h - 1.001)
|
|
96
|
+
ix, iy = x.long(), y.long()
|
|
97
|
+
fx, fy = x - ix, y - iy
|
|
98
|
+
flat = dt.flatten(1)
|
|
99
|
+
at = lambda dx, dy: flat.gather(1, (iy + dy) * w + ix + dx)
|
|
100
|
+
d = (1 - fy) * ((1 - fx) * at(0, 0) + fx * at(1, 0)) + fy * ((1 - fx) * at(0, 1) + fx * at(1, 1))
|
|
101
|
+
return d + (q - torch.stack((x, y), -1)).norm(dim=-1)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def coverage_loss(uv, visible, target, frame_valid, delta):
|
|
105
|
+
# Query tiles bound scratch to 240*64*2000 floats (<118 MiB), with no
|
|
106
|
+
# T*foreground*surface autograd graph. All 1000 queries still participate.
|
|
107
|
+
indices = []
|
|
108
|
+
with torch.no_grad():
|
|
109
|
+
detached = uv.detach()
|
|
110
|
+
vv = detached.square().sum(-1)[:, None]
|
|
111
|
+
for chunk in target.split(64, dim=1):
|
|
112
|
+
distance = chunk.square().sum(-1, keepdim=True) + vv - 2 * (chunk @ detached.transpose(1, 2))
|
|
113
|
+
distance.masked_fill_(~visible[:, None], float('inf'))
|
|
114
|
+
indices.append(distance.argmin(-1))
|
|
115
|
+
idx = torch.cat(indices, 1)
|
|
116
|
+
nearest = uv.gather(1, idx[..., None].expand(-1, -1, 2))
|
|
117
|
+
active = (visible.any(-1) & frame_valid).float()
|
|
118
|
+
loss = huber((nearest - target).norm(dim=-1) / delta).mean(-1)
|
|
119
|
+
return (loss * active).sum() / active.sum().clamp_min(1)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def surface_samples(rig, count=2000):
|
|
123
|
+
"""Fixed area-stratified face samples, reused across frames and all runs."""
|
|
124
|
+
v = rig.dec_vertices.detach().cpu().numpy()
|
|
125
|
+
f = rig.dec_faces.cpu().numpy()
|
|
126
|
+
area = np.linalg.norm(np.cross(v[f[:, 1]] - v[f[:, 0]], v[f[:, 2]] - v[f[:, 0]]), axis=-1)
|
|
127
|
+
rng = np.random.default_rng(0)
|
|
128
|
+
cdf = np.cumsum(area) / area.sum()
|
|
129
|
+
selected = np.searchsorted(cdf, (np.arange(count) + rng.random(count)) / count)
|
|
130
|
+
a, b = np.sqrt(rng.random(count)), rng.random(count)
|
|
131
|
+
bary = np.stack((1 - a, a * (1 - b), a * b), -1)
|
|
132
|
+
return (torch.as_tensor(f[selected], device=rig.dec_vertices.device),
|
|
133
|
+
torch.tensor(bary, dtype=rig.dec_vertices.dtype, device=rig.dec_vertices.device))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def trust_region_loss(root, rotations, initial_root, initial_rotations):
|
|
137
|
+
"""Bounded G5 tether: retain trajectory shape, not its placement error.
|
|
138
|
+
|
|
139
|
+
A 25 cm centred path deviation or 30 degree rotation has unit squared
|
|
140
|
+
residual. Saturation lets persistent image evidence escape a bad G5 basin.
|
|
141
|
+
"""
|
|
142
|
+
shift = root - initial_root
|
|
143
|
+
path_squared = ((shift - shift.mean(0)) / 0.25).square().sum(-1)
|
|
144
|
+
angle_squared = (rotations - initial_rotations).square().sum((-1, -2)) / (8 * np.sin(np.pi / 12) ** 2)
|
|
145
|
+
weights = rotations.new_ones(27)
|
|
146
|
+
weights[0] = 2
|
|
147
|
+
weights[ARM_TRUST_JOINTS] = 4
|
|
148
|
+
return (2 * (path_squared / (1 + path_squared)).mean()
|
|
149
|
+
+ (weights * angle_squared / (1 + angle_squared)).mean())
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@torch.no_grad()
|
|
153
|
+
def articulation_reliability(projected, assigned, seen, delta):
|
|
154
|
+
"""G5 shape disagreement, excluding common image translation and L/R swaps.
|
|
155
|
+
|
|
156
|
+
Residuals within detector noise are fully trusted. Beyond that band use
|
|
157
|
+
Geman-McClure influence weights; confidence/visibility can only reduce them.
|
|
158
|
+
These labels are frozen per block, not learned by fitting the bad keypoint.
|
|
159
|
+
"""
|
|
160
|
+
offset = projected - assigned[..., :2]
|
|
161
|
+
valid = seen & (assigned[..., 2] >= 0.5)
|
|
162
|
+
body = offset[:, 5:].masked_fill(~valid[:, 5:, None], float('inf'))
|
|
163
|
+
# Same lower median as nanmedian, without its nondeterministic CUDA
|
|
164
|
+
# indices kernel. No-evidence frames use zero common translation.
|
|
165
|
+
ordered = body.sort(dim=1, stable=True).values
|
|
166
|
+
count = valid[:, 5:].sum(-1)
|
|
167
|
+
middle = ((count - 1) // 2).clamp_min(0)
|
|
168
|
+
center = ordered.gather(1, middle[:, None, None].expand(-1, 1, 2)).squeeze(1)
|
|
169
|
+
center = torch.where(count[:, None] > 0, center, torch.zeros_like(center))
|
|
170
|
+
excess = ((offset - center[:, None]).norm(dim=-1) / delta - 1).relu() / 2
|
|
171
|
+
return assigned[..., 2] * seen / (1 + excess.square()).square()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def articulation_trust_loss(rotations, initial_rotations, uncertainty):
|
|
175
|
+
"""Local rotations only; 15-degree Huber scale, no root/placement tether."""
|
|
176
|
+
chord = (rotations[:, 1:] - initial_rotations[:, 1:]).norm(dim=(-1, -2))
|
|
177
|
+
return (huber(chord / np.sqrt(8 * np.sin(np.pi / 24) ** 2)) * uncertainty).mean()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def heading_rate_loss(root_rotations, fps):
|
|
181
|
+
"""World-Y twist increments, circularly unwrapped rather than Euler-smoothed.
|
|
182
|
+
|
|
183
|
+
Ordinary turns below 3 rad/s are free; excess rate is Huber-robust at
|
|
184
|
+
3 rad/s. Near a 180-degree tilt yaw is undefined, so omit only those pairs.
|
|
185
|
+
No absolute heading is prescribed, and L/R assignment remains independent.
|
|
186
|
+
"""
|
|
187
|
+
cs = torch.stack((root_rotations[:, 0, 0] + root_rotations[:, 2, 2],
|
|
188
|
+
root_rotations[:, 0, 2] - root_rotations[:, 2, 0]), -1)
|
|
189
|
+
defined = cs.square().sum(-1) > 1e-4
|
|
190
|
+
cs = torch.where(defined[:, None], cs, cs.new_tensor([1, 0]))
|
|
191
|
+
cosine = (cs[1:] * cs[:-1]).sum(-1)
|
|
192
|
+
sine = cs[1:, 1] * cs[:-1, 0] - cs[1:, 0] * cs[:-1, 1]
|
|
193
|
+
rate = torch.atan2(sine, cosine).abs() * fps
|
|
194
|
+
active = defined[1:] & defined[:-1]
|
|
195
|
+
return (huber((rate - 3).relu() / 3) * active).sum() / active.sum().clamp_min(1)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class ClipObjective:
|
|
199
|
+
def __init__(self, rig, bone_scale, camera, boxes, masks, kp, fps, delta_px, endpoints, initializer, components=ABLATIONS['full']):
|
|
200
|
+
self.rig, self.bone_scale = rig, bone_scale
|
|
201
|
+
self.components = components
|
|
202
|
+
self.camera = camera_tensors(camera, rig.neutral.device)
|
|
203
|
+
self.camera_pos = camera_center(camera)
|
|
204
|
+
self.boxes, self.masks, self.kp = boxes, masks, kp
|
|
205
|
+
self.fps, self.delta = fps, delta_px
|
|
206
|
+
self.endpoints = endpoints
|
|
207
|
+
self.initial_root = initializer.transl.detach().clone()
|
|
208
|
+
self.initial_rotations = initializer.local_rot_mats().detach().clone()
|
|
209
|
+
self.initial_local6d = initializer.local6d.detach().clone()
|
|
210
|
+
# An observed joint constrains the local chain leading to it. Leaves
|
|
211
|
+
# without their own keypoint inherit their parent's evidence, not root.
|
|
212
|
+
influence = np.zeros((27, 17), np.float32)
|
|
213
|
+
for coco, joint in enumerate(COCO_JOINTS[5:], start=5):
|
|
214
|
+
while joint > 0:
|
|
215
|
+
influence[joint, coco] = 1
|
|
216
|
+
joint = rig.parents[joint]
|
|
217
|
+
for joint in range(1, 27):
|
|
218
|
+
if not influence[joint].any():
|
|
219
|
+
influence[joint] = influence[rig.parents[joint]]
|
|
220
|
+
influence = influence[1:] / np.maximum(1, influence[1:].sum(-1, keepdims=True))
|
|
221
|
+
self.articulation_influence = torch.as_tensor(influence, device=rig.neutral.device)
|
|
222
|
+
self.faces, self.bary = surface_samples(rig)
|
|
223
|
+
self.foot_vertices = [torch.nonzero(
|
|
224
|
+
rig.dec_weights[:, [rig.prep_bone[j] for j in foot]].sum(-1) > 0.5).flatten()
|
|
225
|
+
for foot in FOOT_JOINTS]
|
|
226
|
+
# Generous rest-relative caps preserve stylised motion. Root is free.
|
|
227
|
+
self.caps = torch.tensor(JOINT_CAPS_DEG, device=rig.neutral.device) * (np.pi / 180)
|
|
228
|
+
|
|
229
|
+
def geometry(self, state, nuisance):
|
|
230
|
+
joints, globals_ = cskel27_fk(state, self.bone_scale, self.rig)
|
|
231
|
+
_, verts = studio_skin(globals_, joints, self.rig, bone_scale=self.bone_scale)
|
|
232
|
+
root = state.transl[:, None]
|
|
233
|
+
joints = root + (joints - root) * (1 + nuisance[0])
|
|
234
|
+
verts = root + (verts - root) * (1 + nuisance[0])
|
|
235
|
+
return joints, verts
|
|
236
|
+
|
|
237
|
+
@torch.no_grad()
|
|
238
|
+
def articulation_observation(self, state, nuisance, assigned):
|
|
239
|
+
# Keep G5 articulation but let the fitted root and global orientation
|
|
240
|
+
# explain camera placement. Never tether the global pose to this prior.
|
|
241
|
+
reference = State(state.transl, state.root6d, self.initial_local6d)
|
|
242
|
+
joints, _ = cskel27_fk(reference, self.bone_scale, self.rig)
|
|
243
|
+
joints = state.transl[:, None] + (joints - state.transl[:, None]) * (1 + nuisance[0])
|
|
244
|
+
projected, depth = project(joints[:, COCO_JOINTS], self.camera, nuisance)
|
|
245
|
+
seen = depth > 0
|
|
246
|
+
if 'occlusion' in self.components:
|
|
247
|
+
seen &= ~ray_occlusion(joints[:, COCO_JOINTS], self.camera_pos, self.boxes)
|
|
248
|
+
reliable = articulation_reliability(projected, assigned, seen, self.delta)
|
|
249
|
+
uncertainty = (1 - reliable).square() @ self.articulation_influence.T
|
|
250
|
+
# Retain a weak escape route from a wrong G5 pose; do not renormalize
|
|
251
|
+
# these attenuated data weights back to a full observation's strength.
|
|
252
|
+
return 0.1 + 0.9 * reliable, uncertainty
|
|
253
|
+
|
|
254
|
+
def sole_points(self, vertices):
|
|
255
|
+
"""Lowest four skinned foot vertices, not an ankle-as-sole offset."""
|
|
256
|
+
soles = []
|
|
257
|
+
for indices in self.foot_vertices:
|
|
258
|
+
foot = vertices[:, indices]
|
|
259
|
+
lowest = foot[..., 1].topk(4, dim=1, largest=False).indices
|
|
260
|
+
soles.append(foot.gather(1, lowest[..., None].expand(-1, -1, 3)))
|
|
261
|
+
return torch.stack(soles, 1)
|
|
262
|
+
|
|
263
|
+
@torch.no_grad()
|
|
264
|
+
def support_observation(self, state, nuisance, assigned, stance, hidden):
|
|
265
|
+
"""Freeze reliable stance/support labels between blocks; no ankle ray pin.
|
|
266
|
+
|
|
267
|
+
The HMM is relative to a clip percentile and alone can label hovering
|
|
268
|
+
or a jump apex as stance. Require absolute proximity, two quiet motion
|
|
269
|
+
intervals and a consistent confident ankle observation as well.
|
|
270
|
+
"""
|
|
271
|
+
joints, vertices = self.geometry(state, nuisance)
|
|
272
|
+
sole = self.sole_points(vertices)
|
|
273
|
+
height = support_height(sole.flatten(1, 2), self.boxes).reshape(sole.shape[:-1])
|
|
274
|
+
support = height.mean(-1)
|
|
275
|
+
clearance = sole[..., 1].mean(-1) - support
|
|
276
|
+
uv, depth = project(joints[:, [25, 21]], self.camera, nuisance)
|
|
277
|
+
ankle = assigned[:, [15, 16]]
|
|
278
|
+
active = (stance & (depth > 0) & (ankle[..., 2] >= 0.5)
|
|
279
|
+
& ((uv - ankle[..., :2]).norm(dim=-1) <= 2 * self.delta)
|
|
280
|
+
& (clearance >= -0.03) & (clearance <= 0.10)
|
|
281
|
+
& (height.amax(-1) - height.amin(-1) <= 0.02))
|
|
282
|
+
if 'occlusion' in self.components:
|
|
283
|
+
active &= ~hidden[:, [25, 21]]
|
|
284
|
+
# Stable ankle OR toe across both adjacent intervals. A moving foot
|
|
285
|
+
# is not evidence of ground contact, even if the HMM calls it stance.
|
|
286
|
+
velocity = (foot_points(joints)[1:] - foot_points(joints)[:-1]) * self.fps
|
|
287
|
+
quiet = ((velocity[..., [0, 2]].norm(dim=-1) <= 0.10)
|
|
288
|
+
& (velocity[..., 1].abs() <= 0.15)).any(-1)
|
|
289
|
+
stable = torch.zeros_like(active)
|
|
290
|
+
stable[1:-1] = (active[:-2] & active[1:-1] & active[2:]
|
|
291
|
+
& quiet[:-1] & quiet[1:])
|
|
292
|
+
return support, stable
|
|
293
|
+
|
|
294
|
+
def __call__(self, state, nuisance, start, end, assigned, stance, penetration_weight,
|
|
295
|
+
trust_weight, support=None, articulation=None):
|
|
296
|
+
joints, vertices = self.geometry(state, nuisance)
|
|
297
|
+
boxes = slice_boxes(self.boxes, start, end)
|
|
298
|
+
occlusion = 'occlusion' in self.components
|
|
299
|
+
projected, depth = project(joints[:, COCO_JOINTS], self.camera, nuisance)
|
|
300
|
+
seen = depth > 0
|
|
301
|
+
if occlusion:
|
|
302
|
+
seen = ~ray_occlusion(joints[:, COCO_JOINTS], self.camera_pos, boxes) & seen
|
|
303
|
+
weights = assigned[..., 2] * seen.float()
|
|
304
|
+
residual = (projected - assigned[..., :2]).norm(dim=-1) / self.delta
|
|
305
|
+
kp_terms = huber(residual) * weights
|
|
306
|
+
if articulation is not None:
|
|
307
|
+
kp_terms = kp_terms * articulation[0]
|
|
308
|
+
kp_loss = kp_terms.sum() / weights.sum().clamp_min(1)
|
|
309
|
+
zero = kp_loss * 0
|
|
310
|
+
silhouette = zero
|
|
311
|
+
if 'silhouette' in self.components:
|
|
312
|
+
samples = (vertices[:, self.faces] * self.bary[None, :, :, None]).sum(-2)
|
|
313
|
+
uv, sz = project(samples, self.camera, nuisance)
|
|
314
|
+
visible = sz > 0
|
|
315
|
+
if occlusion:
|
|
316
|
+
visible = ~ray_occlusion(samples, self.camera_pos, boxes) & visible
|
|
317
|
+
dt, target, valid, ratio = self.masks
|
|
318
|
+
uv = uv * ratio
|
|
319
|
+
d = bilinear_dt(dt, uv).relu() / (self.delta * ratio.mean())
|
|
320
|
+
active = visible & valid[:, None]
|
|
321
|
+
boundary = (huber(d) * active).sum() / active.sum().clamp_min(1)
|
|
322
|
+
coverage = coverage_loss(uv, visible, target, valid, self.delta * ratio.mean())
|
|
323
|
+
silhouette = (boundary + coverage) / 2
|
|
324
|
+
# Second differences at the clip's sampling rate, robust above 3 m/s^2;
|
|
325
|
+
# no velocity damping of freely moving limbs.
|
|
326
|
+
if len(joints) > 2:
|
|
327
|
+
acc = (joints[2:] - 2 * joints[1:-1] + joints[:-2]) * self.fps ** 2
|
|
328
|
+
acceleration = huber(acc.norm(dim=-1) / 3).mean()
|
|
329
|
+
else:
|
|
330
|
+
acceleration = zero
|
|
331
|
+
rotations = state.local_rot_mats()
|
|
332
|
+
heading = heading_rate_loss(rotations[:, 0], self.fps)
|
|
333
|
+
articulation_trust = zero if articulation is None else articulation_trust_loss(
|
|
334
|
+
rotations, self.initial_rotations[start:end], articulation[1])
|
|
335
|
+
trust = trust_region_loss(state.transl, rotations, self.initial_root[start:end],
|
|
336
|
+
self.initial_rotations[start:end]) if trust_weight else zero
|
|
337
|
+
cos = ((rotations.diagonal(dim1=-2, dim2=-1).sum(-1) - 1) / 2).clamp(-1 + 1e-6, 1 - 1e-6)
|
|
338
|
+
limits = (cos.acos() - self.caps).relu().square().mean()
|
|
339
|
+
skate = penetration = zero
|
|
340
|
+
if 'contacts' in self.components:
|
|
341
|
+
feet = foot_points(joints)
|
|
342
|
+
skate = skate_loss(feet, stance, self.fps) / max(1, (end - start - 1) * 4)
|
|
343
|
+
pen = penetration_loss(vertices, boxes)
|
|
344
|
+
# Metres -> a 1 cm residual; normalise by points rather than active
|
|
345
|
+
# collisions, so there is no discontinuity when a vertex exits a solid.
|
|
346
|
+
penetration = (pen['box'] + pen['floor']) / (vertices.shape[0] * vertices.shape[1] * 0.01 ** 2)
|
|
347
|
+
support_loss = zero
|
|
348
|
+
if support is not None:
|
|
349
|
+
target_height, reliable = support
|
|
350
|
+
sole_height = self.sole_points(vertices)[..., 1].mean(-1)
|
|
351
|
+
support_loss = (huber((sole_height - target_height).abs() / 0.03)
|
|
352
|
+
* reliable).sum() / reliable.sum().clamp_min(1)
|
|
353
|
+
endpoint = zero
|
|
354
|
+
for frame, pose in self.endpoints.items():
|
|
355
|
+
if start <= frame < end:
|
|
356
|
+
i = frame - start
|
|
357
|
+
endpoint = endpoint + ((state.transl[i] - pose[0]) / 0.05).square().mean() + (rotations[i] - pose[1]).square().mean()
|
|
358
|
+
nuisance_prior = ((nuisance / nuisance.new_tensor([0.01, 0.3, 0.3, 0.3])) ** 2).sum()
|
|
359
|
+
terms = dict(keypoints=kp_loss, silhouette=silhouette, acceleration=acceleration,
|
|
360
|
+
trust=trust, headingRate=heading,
|
|
361
|
+
limits=limits, skate=skate, penetration=penetration,
|
|
362
|
+
endpoints=endpoint, nuisance=nuisance_prior)
|
|
363
|
+
if articulation is not None:
|
|
364
|
+
terms['articulationTrust'] = articulation_trust
|
|
365
|
+
terms['articulationUncertainty'] = articulation[1].mean()
|
|
366
|
+
terms['keypointReliabilityWeight'] = articulation[0][:, 5:].mean()
|
|
367
|
+
if support is not None:
|
|
368
|
+
terms['supportHeight'] = support_loss
|
|
369
|
+
terms['supportFootFrames'] = support[1].sum()
|
|
370
|
+
# Disabled terms are not evaluated, so they are not reported as zeros.
|
|
371
|
+
if 'silhouette' not in self.components:
|
|
372
|
+
del terms['silhouette']
|
|
373
|
+
if 'contacts' not in self.components:
|
|
374
|
+
del terms['skate'], terms['penetration']
|
|
375
|
+
# Keep the silhouette depth support; bounded G5 trust discourages
|
|
376
|
+
# unsupported articulation/path excursions without pinning placement.
|
|
377
|
+
total = (kp_loss + 6.0 * silhouette + 0.05 * acceleration
|
|
378
|
+
+ trust_weight * trust + articulation_trust + 0.2 * heading
|
|
379
|
+
+ 0.3 * support_loss
|
|
380
|
+
+ 0.1 * limits + skate + penetration_weight * penetration
|
|
381
|
+
+ 0.2 * endpoint + nuisance_prior)
|
|
382
|
+
return total, terms
|