返回 JoyAI-Echo
action_camera.py
根目录 / echo_wm / helpers / action_camera.py
1 """Small, standalone WASD/IJKL camera DSL used by UCPE inference."""
2
3 from __future__ import annotations
4
5 import math
6
7 import numpy as np
8 import torch
9
10 DEFAULT_TRANSLATION_SPEED = 0.025
11 DEFAULT_ROTATION_SPEED_DEG = 0.6
12 DEFAULT_PITCH_SPEED_DEG = 0.2
13 DEFAULT_PITCH_LIMIT_DEG = 60.0
14 ALLOWED_ACTION_KEYS = frozenset("wsadikjl")
15
16
17 def parse_action_string(action: str) -> list[list[str]]:
18 cleaned = "".join(action.replace(",", ",").split())
19 if not cleaned:
20 raise ValueError("action string is empty")
21 frames: list[list[str]] = []
22 for segment in cleaned.split(","):
23 if "-" not in segment:
24 raise ValueError(f"Invalid action segment {segment!r}; expected '<keys>-<duration>'")
25 keys_part, duration = segment.rsplit("-", 1)
26 if not duration.isdigit() or int(duration) <= 0:
27 raise ValueError(f"Invalid action duration in {segment!r}")
28 keys = [] if keys_part.lower() == "none" else sorted(set(keys_part.lower()))
29 invalid = sorted(set(keys) - ALLOWED_ACTION_KEYS)
30 if invalid:
31 raise ValueError(f"Unknown action keys {invalid}; allowed: {''.join(sorted(ALLOWED_ACTION_KEYS))}")
32 frames.extend([keys] * int(duration))
33 return frames
34
35
36 def _rot_x(angle: float) -> np.ndarray:
37 c, s = math.cos(angle), math.sin(angle)
38 return np.array([[1, 0, 0], [0, c, -s], [0, s, c]], dtype=np.float64)
39
40
41 def _rot_y(angle: float) -> np.ndarray:
42 c, s = math.cos(angle), math.sin(angle)
43 return np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]], dtype=np.float64)
44
45
46 def action_string_to_c2w(
47 frames: list[list[str]], *, translation_speed: float, rotation_speed_deg: float,
48 pitch_speed_deg: float, pitch_limit_deg: float, fps: float,
49 ) -> np.ndarray:
50 pose = np.eye(4, dtype=np.float64)
51 pitch = 0.0
52 velocity = np.zeros(4, dtype=np.float64)
53 poses = [pose.copy()]
54 previous: set[str] = set()
55 dt = 1.0 / fps
56 for keys in frames:
57 current = set(keys)
58 target = np.array([
59 float("w" in current) - float("s" in current),
60 float("d" in current) - float("a" in current),
61 float("l" in current) - float("j" in current),
62 float("i" in current) - float("k" in current),
63 ])
64 target *= np.array([translation_speed, translation_speed,
65 math.radians(rotation_speed_deg), math.radians(pitch_speed_deg)])
66 if current - previous:
67 velocity = target
68 else:
69 velocity += (target - velocity) * (1.0 - math.exp(-dt / (0.45 if np.any(target) else 1.0)))
70 previous = current
71 new_pitch = np.clip(pitch + velocity[3], -math.radians(pitch_limit_deg), math.radians(pitch_limit_deg))
72 pitch_step = new_pitch - pitch
73 pitch = new_pitch
74 rotation = _rot_y(velocity[2]) @ pose[:3, :3] @ _rot_x(pitch_step)
75 forward = rotation[:, 2].copy(); forward[1] = 0
76 right = rotation[:, 0].copy(); right[1] = 0
77 forward /= max(np.linalg.norm(forward), 1e-6)
78 right /= max(np.linalg.norm(right), 1e-6)
79 pose = np.eye(4, dtype=np.float64)
80 pose[:3, :3] = rotation
81 pose[:3, 3] = poses[-1][:3, 3] + forward * velocity[0] + right * velocity[1]
82 poses.append(pose.copy())
83 return np.stack(poses).astype(np.float32)
84
85
86 def default_k_pix(width: int, height: int, fov_deg: float) -> torch.Tensor:
87 focal = (width / 2.0) / math.tan(math.radians(fov_deg) / 2.0)
88 return torch.tensor([[focal, 0.0, width / 2.0], [0.0, focal, height / 2.0], [0.0, 0.0, 1.0]])
89
90
91 def build_action_pt_from_string(
92 action: str, *, num_frames: int, image_width: int, image_height: int,
93 translation_speed: float, rotation_speed_deg: float, pitch_limit_deg: float,
94 fov_deg: float, fps: float,
95 ) -> dict[str, torch.Tensor | str]:
96 keys = parse_action_string(action)
97 c2ws = action_string_to_c2w(
98 keys, translation_speed=translation_speed, rotation_speed_deg=rotation_speed_deg,
99 pitch_speed_deg=DEFAULT_PITCH_SPEED_DEG, pitch_limit_deg=pitch_limit_deg, fps=fps,
100 )
101 if len(c2ws) < num_frames:
102 c2ws = np.concatenate([c2ws, np.repeat(c2ws[-1:], num_frames - len(c2ws), axis=0)])
103 c2ws = c2ws[:num_frames]
104 return {"c2ws_raw": torch.from_numpy(c2ws), "K_pix": default_k_pix(image_width, image_height, fov_deg),
105 "schema": "wasd_ijkl_ucpe_v3"}
106
106 lines PYTHON