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,354 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
import torch
|
|
6
|
+
|
|
7
|
+
import scene
|
|
8
|
+
from scene import SceneError
|
|
9
|
+
|
|
10
|
+
SCENE_JSON = { # evidence/exp3/gt/bump/scene.json as written by tools/gt-render/scene-box.mjs
|
|
11
|
+
"kind": "cube",
|
|
12
|
+
"placement": {"x": -0.46474942564964294, "z": 4.095341396331787, "rot": 0, "sx": 1.5, "sy": 1.5, "sz": 0.4},
|
|
13
|
+
"centre": [-0.46474942564964294, 0.75, 4.095341396331787],
|
|
14
|
+
"halfExtents": [0.75, 0.75, 0.2],
|
|
15
|
+
"yawDeg": 0,
|
|
16
|
+
"min": [-1.214749425649643, 0, 3.895341396331787],
|
|
17
|
+
"max": [0.28525057435035706, 1.5, 4.2953413963317875],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def boxes_of(records, frames=1, dtype=torch.float64):
|
|
22
|
+
return scene.box_tensors(scene.parse_scene(records), frames, dtype=dtype)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def pts(*xyz):
|
|
26
|
+
return torch.tensor(np.array([xyz], dtype=np.float64)) # (T=1, N, 3)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def ry(yaw): # three.js rotation about +Y (tools/gt-render/scene-box.mjs boxCorners)
|
|
30
|
+
c, s = math.cos(yaw), math.sin(yaw)
|
|
31
|
+
return np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def reference_sdf(p, center, half, yaw):
|
|
35
|
+
local = ry(yaw).T @ (np.asarray(p) - center)
|
|
36
|
+
q = np.abs(local) - half
|
|
37
|
+
if np.all(q <= 0):
|
|
38
|
+
return float(q.max())
|
|
39
|
+
return float(np.linalg.norm(local - np.clip(local, -half, half)))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# --- parsing ---------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
def test_parses_bench_scene_json_min_max():
|
|
45
|
+
[box] = scene.parse_scene({"min": [0, 0, 0], "max": [2, 1, 4]})
|
|
46
|
+
np.testing.assert_allclose(box.center, [1, 0.5, 2])
|
|
47
|
+
np.testing.assert_allclose(box.half_extents, [1, 0.5, 2])
|
|
48
|
+
assert box.yaw == 0 and box.per_frame_center is None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_parses_real_scene_json_and_boxes_list():
|
|
52
|
+
[box] = scene.parse_scene(SCENE_JSON)
|
|
53
|
+
np.testing.assert_allclose(box.center, SCENE_JSON["centre"])
|
|
54
|
+
np.testing.assert_allclose(box.half_extents, SCENE_JSON["halfExtents"])
|
|
55
|
+
two = scene.parse_scene({"boxes": [{"min": SCENE_JSON["min"], "max": SCENE_JSON["max"]}, {"center": [0, 1, 0], "halfExtents": [1, 1, 1], "yawDeg": 90}]})
|
|
56
|
+
assert len(two) == 2 and two[1].yaw == pytest.approx(math.pi / 2)
|
|
57
|
+
assert scene.parse_scene(None) == []
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_parses_shot_context_obb_with_per_frame():
|
|
61
|
+
[box] = scene.parse_scene([{"center": [0, 0.5, 0], "halfExtents": [0.5, 0.5, 0.5], "yaw": 0.3,
|
|
62
|
+
"perFrame": [[0, 0.5, 0], {"center": [1, 0.5, 0], "yaw": 0.6}, {"centre": [2, 0.5, 0]}]}])
|
|
63
|
+
t = scene.box_tensors([box], 3, dtype=torch.float64)
|
|
64
|
+
np.testing.assert_allclose(t.centers[0, :, 0].numpy(), [0, 1, 2])
|
|
65
|
+
np.testing.assert_allclose(t.yaw[0].numpy(), [0.3, 0.6, 0.3])
|
|
66
|
+
with pytest.raises(SceneError) as err:
|
|
67
|
+
scene.box_tensors([box], 4)
|
|
68
|
+
assert err.value.code == "box-frames-mismatch"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.mark.parametrize("record", [
|
|
72
|
+
{"min": [0, 0, 0], "max": [0, 0, 0]},
|
|
73
|
+
{"min": [0, 0, 0], "max": [1, 0, 1]},
|
|
74
|
+
{"min": [1, 0, 0], "max": [0, 1, 1]},
|
|
75
|
+
{"center": [0, 0, 0], "halfExtents": [0.5, 0, 0.5]},
|
|
76
|
+
])
|
|
77
|
+
def test_zero_or_negative_size_is_degenerate_box(record):
|
|
78
|
+
with pytest.raises(SceneError) as err:
|
|
79
|
+
scene.parse_scene(record)
|
|
80
|
+
assert err.value.code == "degenerate-box"
|
|
81
|
+
assert str(err.value).startswith("degenerate-box")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.mark.parametrize("record", [
|
|
85
|
+
{"min": [0, float("nan"), 0], "max": [1, 1, 1]},
|
|
86
|
+
{"center": [0, 0, 0], "halfExtents": [0.5, float("inf"), 0.5]},
|
|
87
|
+
{"center": [0, 0, 0], "halfExtents": [0.5, 0.5, 0.5], "yaw": float("nan")},
|
|
88
|
+
{"center": [0, 0, 0], "halfExtents": [0.5, 0.5, 0.5], "perFrame": [[0, float("nan"), 0]]},
|
|
89
|
+
{"center": [0, 0, 0]},
|
|
90
|
+
{"min": [0, 0], "max": [1, 1]},
|
|
91
|
+
"box",
|
|
92
|
+
])
|
|
93
|
+
def test_malformed_box_is_rejected(record):
|
|
94
|
+
with pytest.raises(SceneError) as err:
|
|
95
|
+
scene.parse_scene(record)
|
|
96
|
+
assert err.value.code == "bad-box"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# --- signed distance ---------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
def test_sdf_unit_box_sign_and_magnitude():
|
|
102
|
+
boxes = boxes_of({"min": [-0.5, -0.5, -0.5], "max": [0.5, 0.5, 0.5]})
|
|
103
|
+
sd = scene.box_sdf(pts([0, 0, 0], [0.4, 0, 0], [0.5, 0, 0], [1, 0, 0], [1, 1, 0], [1, 1, 1], [0, -2, 0]), boxes)[0, :, 0]
|
|
104
|
+
expected = [-0.5, -0.1, 0.0, 0.5, math.sqrt(0.5), math.sqrt(0.75), 1.5]
|
|
105
|
+
np.testing.assert_allclose(sd.numpy(), expected, atol=1e-12)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_sdf_rotated_and_translated_box():
|
|
109
|
+
center, half, yaw = np.array([2.0, 0.75, -3.0]), np.array([1.0, 0.75, 0.2]), math.radians(90)
|
|
110
|
+
boxes = boxes_of({"center": center.tolist(), "halfExtents": half.tolist(), "yawDeg": 90})
|
|
111
|
+
# Rotated 90 deg the long (x) side lies along world z: 0.9 m along z is inside, 0.5 m along x is outside.
|
|
112
|
+
sd = scene.box_sdf(pts(center + [0, 0, 0.9], center + [0.5, 0, 0], center), boxes)[0, :, 0].numpy()
|
|
113
|
+
np.testing.assert_allclose(sd, [-0.1, 0.3, -0.2], atol=1e-12)
|
|
114
|
+
corners = [center + ry(yaw) @ (half * [sx, sy, sz]) for sx in (-1, 1) for sy in (-1, 1) for sz in (-1, 1)]
|
|
115
|
+
np.testing.assert_allclose(scene.box_sdf(pts(*corners), boxes)[0, :, 0].numpy(), 0, atol=1e-12)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_sdf_matches_reference_on_random_points():
|
|
119
|
+
rng = np.random.default_rng(8)
|
|
120
|
+
center, half, yaw = np.array([-0.46, 0.75, 4.1]), np.array([0.75, 0.75, 0.2]), 0.7
|
|
121
|
+
boxes = boxes_of({"center": center.tolist(), "halfExtents": half.tolist(), "yaw": yaw})
|
|
122
|
+
points = center + rng.uniform(-1.5, 1.5, size=(500, 3))
|
|
123
|
+
sd = scene.box_sdf(torch.tensor(points[None]), boxes)[0, :, 0].numpy()
|
|
124
|
+
ref = np.array([reference_sdf(p, center, half, yaw) for p in points])
|
|
125
|
+
np.testing.assert_allclose(sd, ref, atol=1e-10)
|
|
126
|
+
assert (sd < 0).any() and (sd > 0).any()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_sdf_follows_per_frame_box():
|
|
130
|
+
boxes = boxes_of({"center": [0, 0.5, 0], "halfExtents": [0.5, 0.5, 0.5], "perFrame": [[0, 0.5, 0], [3, 0.5, 0]]}, frames=2)
|
|
131
|
+
sd = scene.box_sdf(torch.tensor([[[0, 0.5, 0]], [[0, 0.5, 0]]], dtype=torch.float64), boxes)[:, 0, 0]
|
|
132
|
+
np.testing.assert_allclose(sd.numpy(), [-0.5, 2.5])
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_penetration_loss_values_and_finite_gradients():
|
|
136
|
+
boxes = boxes_of({"min": [-0.5, 0, -0.5], "max": [0.5, 1, 0.5]})
|
|
137
|
+
# inside, on a face, on an edge, on a corner, at the box centre, outside, below the floor
|
|
138
|
+
points = pts([0.4, 0.5, 0], [0.5, 0.5, 0], [0.5, 1, 0], [0.5, 1, 0.5], [0, 0.5, 0], [2, 0.5, 0], [3, -0.02, 0]).requires_grad_(True)
|
|
139
|
+
loss = scene.penetration_loss(points, boxes)
|
|
140
|
+
assert loss["box"].item() == pytest.approx(0.1 ** 2 + 0.5 ** 2)
|
|
141
|
+
assert loss["floor"].item() == pytest.approx(0.02 ** 2)
|
|
142
|
+
(loss["box"] + loss["floor"]).backward()
|
|
143
|
+
grad = points.grad[0]
|
|
144
|
+
assert torch.isfinite(grad).all()
|
|
145
|
+
assert grad[0, 0] < 0 # descending pushes the penetrating point out through the +x face
|
|
146
|
+
assert grad[6, 1] < 0 # and the sunken point up through the floor
|
|
147
|
+
assert torch.all(grad[5] == 0)
|
|
148
|
+
sd_points = pts([0.5, 1, 0.5], [2, 3, 4]).requires_grad_(True)
|
|
149
|
+
scene.box_sdf(sd_points, boxes).sum().backward()
|
|
150
|
+
assert torch.isfinite(sd_points.grad).all()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_penetration_stats_and_empty_scene():
|
|
154
|
+
boxes = boxes_of({"min": [-0.5, 0, -0.5], "max": [0.5, 1, 0.5]}, frames=3)
|
|
155
|
+
points = torch.tensor([[[2, 0.5, 0]], [[0.45, 0.5, 0]], [[2, -0.01, 0]]], dtype=torch.float64)
|
|
156
|
+
stats = scene.penetration_stats(points, boxes)
|
|
157
|
+
assert stats["maxBoxCm"] == pytest.approx(5.0)
|
|
158
|
+
assert stats["maxFloorCm"] == pytest.approx(1.0)
|
|
159
|
+
assert stats["frames"] == [1, 2]
|
|
160
|
+
empty = scene.box_tensors([], 3, dtype=torch.float64)
|
|
161
|
+
assert scene.penetration_loss(points, empty)["box"].item() == 0
|
|
162
|
+
assert scene.penetration_stats(points, empty)["maxBoxCm"] == 0
|
|
163
|
+
assert not scene.ray_occlusion(points, [0, 1, 5], empty).any()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# --- stance HMM and skate ------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
def synthetic_walk(frames=150, fps=30.0, period=1.0, duty=0.6, stride=1.2, seed=0, ground=None):
|
|
169
|
+
"""Ankle+toe of two feet, (T, 2, 2, 3), and the true stance (T, 2)."""
|
|
170
|
+
rng = np.random.default_rng(seed)
|
|
171
|
+
feet, stance = np.zeros((frames, 2, 2, 3)), np.zeros((frames, 2), dtype=bool)
|
|
172
|
+
for f, (phase, x_side) in enumerate(((0.0, 0.1), (0.5, -0.1))):
|
|
173
|
+
for t in range(frames):
|
|
174
|
+
cycle = t / fps / period + phase
|
|
175
|
+
k, u = math.floor(cycle), cycle - math.floor(cycle)
|
|
176
|
+
if u < duty:
|
|
177
|
+
s, lift = 0.0, 0.0
|
|
178
|
+
stance[t, f] = True
|
|
179
|
+
else:
|
|
180
|
+
w = (u - duty) / (1 - duty)
|
|
181
|
+
s, lift = (1 - math.cos(math.pi * w)) / 2, math.sin(math.pi * w)
|
|
182
|
+
z = (k + s) * stride + phase * stride
|
|
183
|
+
base = 0.0 if ground is None else ground(z)
|
|
184
|
+
feet[t, f, 0] = [x_side, base + 0.08 + 0.12 * lift, z]
|
|
185
|
+
feet[t, f, 1] = [x_side, base + 0.02 + 0.10 * lift, z + 0.15]
|
|
186
|
+
return feet + rng.normal(0, 0.004, feet.shape), stance, fps
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_stance_hmm_recovers_walk():
|
|
190
|
+
feet, truth, fps = synthetic_walk()
|
|
191
|
+
got = scene.stance_hmm(torch.tensor(feet), fps)
|
|
192
|
+
assert got.shape == truth.shape
|
|
193
|
+
agreement = (got == truth).mean(axis=0)
|
|
194
|
+
assert np.all(agreement >= 0.9), agreement
|
|
195
|
+
# every true stance run is found and no spurious one-frame toggles appear
|
|
196
|
+
for f in range(2):
|
|
197
|
+
runs = np.flatnonzero(np.diff(got[:, f].astype(int)) != 0)
|
|
198
|
+
assert len(runs) <= len(np.flatnonzero(np.diff(truth[:, f].astype(int)) != 0)) + 1
|
|
199
|
+
explicit = scene.stance_hmm(feet, fps, contact_height=[[0.08, 0.02], [0.08, 0.02]])
|
|
200
|
+
assert np.all((explicit == truth).mean(axis=0) >= 0.9)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_stance_hmm_uses_box_top_as_support():
|
|
204
|
+
top = 0.4
|
|
205
|
+
boxes = boxes_of({"min": [-1, 0, 2.0], "max": [1, top, 20]})
|
|
206
|
+
feet, truth, fps = synthetic_walk(ground=lambda z: top if z >= 2.0 else 0.0)
|
|
207
|
+
ref = [[0.08, 0.02], [0.08, 0.02]]
|
|
208
|
+
with_box = scene.stance_hmm(feet, fps, boxes, contact_height=ref)
|
|
209
|
+
assert np.all((with_box == truth).mean(axis=0) >= 0.9)
|
|
210
|
+
on_box = feet[:, :, 0, 2] > 2.2
|
|
211
|
+
floor_only = scene.stance_hmm(feet, fps, contact_height=ref)
|
|
212
|
+
assert not floor_only[on_box & truth].any() # without the box a planted foot on it looks airborne
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_support_height_floor_box_top_and_step_tolerance():
|
|
216
|
+
boxes = boxes_of({"center": [0, 0.2, 0], "halfExtents": [0.5, 0.2, 0.5], "yawDeg": 45})
|
|
217
|
+
# beside the box, on its top, 5 cm sunk into its top, deep inside it, inside the unrotated
|
|
218
|
+
# footprint's corner but outside the 45-degree footprint
|
|
219
|
+
points = pts([2, 0.1, 0], [0, 0.45, 0], [0.1, 0.35, 0], [0, 0.1, 0], [0.45, 0.45, 0.45])
|
|
220
|
+
np.testing.assert_allclose(scene.support_height(points, boxes)[0].numpy(), [0, 0.4, 0.4, 0, 0])
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_stance_hmm_rejects_nan():
|
|
224
|
+
feet, _, fps = synthetic_walk(frames=10)
|
|
225
|
+
feet[4, 0, 0, 1] = np.nan
|
|
226
|
+
with pytest.raises(SceneError) as err:
|
|
227
|
+
scene.stance_hmm(feet, fps)
|
|
228
|
+
assert err.value.code == "non-finite"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_skate_loss_only_on_stance_frames_and_differentiable():
|
|
232
|
+
fps = 30.0
|
|
233
|
+
joints = torch.zeros(4, 27, 3, dtype=torch.float64)
|
|
234
|
+
joints[:, 25, 0] = torch.tensor([0.0, 0.01, 0.02, 0.5]) # left ankle slides 1 cm/frame, then jumps
|
|
235
|
+
joints[:, 21, 1] = torch.tensor([0.0, 0.5, 1.0, 1.5]) # right ankle moves vertically only
|
|
236
|
+
joints.requires_grad_(True)
|
|
237
|
+
stance = np.array([[1, 1], [1, 1], [1, 1], [0, 1]], dtype=bool)
|
|
238
|
+
loss = scene.skate_loss(scene.foot_points(joints), stance, fps)
|
|
239
|
+
assert loss.item() == pytest.approx(2 * (0.01 * fps) ** 2)
|
|
240
|
+
loss.backward()
|
|
241
|
+
assert torch.isfinite(joints.grad).all()
|
|
242
|
+
assert joints.grad[3, 25, 0] == 0
|
|
243
|
+
assert scene.skate_loss(scene.foot_points(joints), np.zeros((4, 2), dtype=bool), fps).item() == 0
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# --- occlusion -------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
def test_camera_center_from_world_to_camera():
|
|
249
|
+
yaw = 0.5235987755982988
|
|
250
|
+
r = ry(yaw).T * np.array([[1], [-1], [-1]]) # OpenCV y down, z forward from a three.js camera
|
|
251
|
+
position = np.array([1.72, 1.13, 7.13])
|
|
252
|
+
w2c = np.eye(4)
|
|
253
|
+
w2c[:3, :3], w2c[:3, 3] = r, -r @ position
|
|
254
|
+
np.testing.assert_allclose(scene.camera_center({"worldToCamera": w2c.tolist()}), position, atol=1e-12)
|
|
255
|
+
with pytest.raises(SceneError):
|
|
256
|
+
scene.camera_center(np.full((4, 4), np.nan))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_ray_occlusion_behind_vs_in_front():
|
|
260
|
+
cam = [0, 1, 5]
|
|
261
|
+
boxes = boxes_of({"min": [-0.5, 0.5, -0.5], "max": [0.5, 1.5, 0.5]})
|
|
262
|
+
points = pts([0, 1, -2], [0, 1, 2], [2, 1, -2], [0, 1, 0], [0, 1, 0.5], [0, 2, -2])
|
|
263
|
+
np.testing.assert_array_equal(scene.ray_occlusion(points, cam, boxes)[0].numpy(), [True, False, False, True, False, False])
|
|
264
|
+
# a box behind the camera hides nothing in front of it
|
|
265
|
+
behind_camera = boxes_of({"min": [-0.5, 0.5, 7], "max": [0.5, 1.5, 8]})
|
|
266
|
+
assert not scene.ray_occlusion(points, cam, behind_camera).any()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def test_ray_occlusion_rotated_and_moving_box():
|
|
270
|
+
cam = [0, 1, 5]
|
|
271
|
+
# thin wall rotated 90 deg: its long side spans world z, so it only blocks lines of sight near x=0
|
|
272
|
+
wall = {"center": [0, 1, 0], "halfExtents": [1.0, 0.5, 0.05], "yawDeg": 90}
|
|
273
|
+
points = pts([0, 1, -2], [0.3, 1, -2])
|
|
274
|
+
np.testing.assert_array_equal(scene.ray_occlusion(points, cam, boxes_of(wall))[0].numpy(), [True, False])
|
|
275
|
+
np.testing.assert_array_equal(scene.ray_occlusion(points, cam, boxes_of({**wall, "yawDeg": 0}))[0].numpy(), [True, True])
|
|
276
|
+
moving = boxes_of({"center": [0, 1, 0], "halfExtents": [0.5, 0.5, 0.5], "perFrame": [[0, 1, 0], [3, 1, 0]]}, frames=2)
|
|
277
|
+
behind = torch.tensor([[[0, 1, -2]], [[0, 1, -2]]], dtype=torch.float64)
|
|
278
|
+
np.testing.assert_array_equal(scene.ray_occlusion(behind, cam, moving)[:, 0].numpy(), [True, False])
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# --- device / dtype boundaries ----------------------------------------------------
|
|
282
|
+
|
|
283
|
+
def test_float32_points_with_float64_boxes():
|
|
284
|
+
boxes = boxes_of({"min": [-0.5, 0, -0.5], "max": [0.5, 1, 0.5]}) # float64
|
|
285
|
+
points = pts([0.4, 0.5, 0], [0, 0.5, -2]).float().requires_grad_(True)
|
|
286
|
+
sd = scene.box_sdf(points, boxes)
|
|
287
|
+
assert sd.dtype == torch.float32
|
|
288
|
+
scene.penetration_loss(points, boxes)["box"].backward()
|
|
289
|
+
assert torch.isfinite(points.grad).all()
|
|
290
|
+
assert scene.support_height(points, boxes).dtype == torch.float32
|
|
291
|
+
np.testing.assert_array_equal(scene.ray_occlusion(points, torch.tensor([0.0, 0.5, 5]), boxes)[0].numpy(), [True, True])
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
needs_cuda = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA-origin inputs need a CUDA device")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def cuda_boxes(boxes):
|
|
298
|
+
return scene.SceneBoxes(*(v.cuda() for v in boxes))
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@needs_cuda
|
|
302
|
+
def test_cuda_stance_hmm_matches_cpu_with_and_without_boxes():
|
|
303
|
+
feet, truth, fps = synthetic_walk(seed=0)
|
|
304
|
+
cpu = scene.stance_hmm(feet, fps)
|
|
305
|
+
gpu_feet = torch.tensor(feet, device="cuda")
|
|
306
|
+
np.testing.assert_array_equal(scene.stance_hmm(gpu_feet, fps), cpu)
|
|
307
|
+
np.testing.assert_array_equal(scene.stance_hmm(gpu_feet.float(), fps), scene.stance_hmm(torch.tensor(feet).float(), fps))
|
|
308
|
+
top = 0.4
|
|
309
|
+
boxes = boxes_of({"min": [-1, 0, 2.0], "max": [1, top, 20]})
|
|
310
|
+
step, step_truth, _ = synthetic_walk(ground=lambda z: top if z >= 2.0 else 0.0)
|
|
311
|
+
ref = [[0.08, 0.02], [0.08, 0.02]]
|
|
312
|
+
cpu_box = scene.stance_hmm(step, fps, boxes, contact_height=ref)
|
|
313
|
+
gpu_step = torch.tensor(step, device="cuda")
|
|
314
|
+
for support in (boxes, cuda_boxes(boxes)):
|
|
315
|
+
for height in (ref, torch.tensor(ref, device="cuda")):
|
|
316
|
+
got = scene.stance_hmm(gpu_step, fps, support, contact_height=height)
|
|
317
|
+
np.testing.assert_array_equal(got, cpu_box)
|
|
318
|
+
assert np.all((cpu_box == step_truth).mean(axis=0) >= 0.9)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@needs_cuda
|
|
322
|
+
def test_cuda_points_with_cpu_or_cuda_boxes():
|
|
323
|
+
cam = [0, 1, 5]
|
|
324
|
+
boxes = boxes_of({"center": [0, 1, 0], "halfExtents": [0.5, 0.5, 0.5], "yawDeg": 30, "perFrame": [[0, 1, 0], [3, 1, 0]]}, frames=2)
|
|
325
|
+
cpu_points = torch.tensor([[[0, 1, -2], [0.3, 1, 0.2], [2, -0.01, 0]], [[0, 1, -2], [0.3, 1, 0.2], [2, 0.5, 0]]], dtype=torch.float64)
|
|
326
|
+
cpu_sd = scene.box_sdf(cpu_points, boxes)
|
|
327
|
+
cpu_occ = scene.ray_occlusion(cpu_points, cam, boxes)
|
|
328
|
+
cpu_support = scene.support_height(cpu_points, boxes)
|
|
329
|
+
cpu_stats = scene.penetration_stats(cpu_points, boxes)
|
|
330
|
+
for support in (boxes, cuda_boxes(boxes)):
|
|
331
|
+
points = cpu_points.cuda().requires_grad_(True)
|
|
332
|
+
sd = scene.box_sdf(points, support)
|
|
333
|
+
assert sd.device.type == "cuda"
|
|
334
|
+
torch.testing.assert_close(sd.cpu(), cpu_sd)
|
|
335
|
+
loss = scene.penetration_loss(points, support)
|
|
336
|
+
(loss["box"] + loss["floor"]).backward()
|
|
337
|
+
assert points.grad.device.type == "cuda" and torch.isfinite(points.grad).all() and points.grad.abs().sum() > 0
|
|
338
|
+
for camera in (cam, torch.tensor(cam, dtype=torch.float64, device="cuda")):
|
|
339
|
+
occ = scene.ray_occlusion(points.detach(), camera, support)
|
|
340
|
+
assert occ.device.type == "cuda"
|
|
341
|
+
torch.testing.assert_close(occ.cpu(), cpu_occ)
|
|
342
|
+
height = scene.support_height(points.detach(), support)
|
|
343
|
+
assert height.device.type == "cuda"
|
|
344
|
+
torch.testing.assert_close(height.cpu(), cpu_support)
|
|
345
|
+
stats = scene.penetration_stats(points.detach(), support)
|
|
346
|
+
assert stats["frames"] == cpu_stats["frames"] == [0]
|
|
347
|
+
assert stats["maxBoxCm"] == pytest.approx(cpu_stats["maxBoxCm"]) and cpu_stats["maxBoxCm"] > 0
|
|
348
|
+
assert stats["maxFloorCm"] == pytest.approx(cpu_stats["maxFloorCm"]) == pytest.approx(1.0)
|
|
349
|
+
joints = torch.zeros(3, 27, 3, device="cuda", requires_grad=True)
|
|
350
|
+
scene.skate_loss(scene.foot_points(joints), np.ones((3, 2), dtype=bool), 30.0).backward()
|
|
351
|
+
assert joints.grad.device.type == "cuda"
|
|
352
|
+
w2c = torch.eye(4, dtype=torch.float64, device="cuda")
|
|
353
|
+
w2c[:3, 3] = torch.tensor([1.0, 2, 3])
|
|
354
|
+
np.testing.assert_allclose(scene.camera_center(w2c), [-1, -2, -3])
|