Timsty commited on
Commit
0651b21
·
verified ·
1 Parent(s): 1f3a93e

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. GR00T-WholeBodyControl/gear_sonic/__pycache__/__init__.cpython-310.pyc +0 -0
  2. GR00T-WholeBodyControl/gear_sonic/__pycache__/__init__.cpython-311.pyc +0 -0
  3. GR00T-WholeBodyControl/gear_sonic/__pycache__/__init__.cpython-312.pyc +0 -0
  4. GR00T-WholeBodyControl/gear_sonic/__pycache__/eval_agent_trl.cpython-312.pyc +0 -0
  5. GR00T-WholeBodyControl/gear_sonic/__pycache__/train_agent_trl.cpython-311.pyc +0 -0
  6. GR00T-WholeBodyControl/gear_sonic/__pycache__/train_agent_trl.cpython-312.pyc +0 -0
  7. GR00T-WholeBodyControl/gear_sonic/__pycache__/version.cpython-310.pyc +0 -0
  8. GR00T-WholeBodyControl/gear_sonic/__pycache__/version.cpython-312.pyc +0 -0
  9. GR00T-WholeBodyControl/gear_sonic/camera/sensor.py +33 -0
  10. GR00T-WholeBodyControl/gear_sonic/camera/sensor_server.py +243 -0
  11. GR00T-WholeBodyControl/gear_sonic/data/exporter.py +472 -0
  12. GR00T-WholeBodyControl/gear_sonic/data/features_sonic_vla.py +410 -0
  13. GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/left_ankle_pitch_link.STL +0 -0
  14. GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/left_elbow_link.STL +0 -0
  15. GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/left_hand_thumb_0_link.STL +0 -0
  16. GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/right_ankle_pitch_link.STL +0 -0
  17. GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/right_elbow_link.STL +0 -0
  18. GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/right_hand_thumb_0_link.STL +0 -0
  19. GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/right_wrist_pitch_link.STL +0 -0
  20. GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/torso_constraint_L_rod_link.STL +0 -0
  21. GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/torso_constraint_R_rod_link.STL +0 -0
  22. GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/waist_roll_link.STL +0 -0
  23. GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/waist_roll_link_rev_1_0.STL +0 -0
  24. GR00T-WholeBodyControl/gear_sonic/data/video_writer.py +99 -0
  25. GR00T-WholeBodyControl/gear_sonic/data_process/convert_soma_csv_to_motion_lib.py +584 -0
  26. GR00T-WholeBodyControl/gear_sonic/data_process/extract_soma_joints_from_bvh.py +394 -0
  27. GR00T-WholeBodyControl/gear_sonic/data_process/filter_and_copy_bones_data.py +294 -0
  28. GR00T-WholeBodyControl/gear_sonic/data_process/split_pkl_files.py +193 -0
  29. GR00T-WholeBodyControl/gear_sonic/envs/__init__.py +0 -0
  30. GR00T-WholeBodyControl/gear_sonic/isaac_utils/__init__.py +0 -0
  31. GR00T-WholeBodyControl/gear_sonic/isaac_utils/maths.py +52 -0
  32. GR00T-WholeBodyControl/gear_sonic/isaac_utils/rotations.py +787 -0
  33. GR00T-WholeBodyControl/gear_sonic/utils/__init__.py +0 -0
  34. GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/__init__.cpython-310.pyc +0 -0
  35. GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/__init__.cpython-311.pyc +0 -0
  36. GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/__init__.cpython-312.pyc +0 -0
  37. GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/average_meters.cpython-311.pyc +0 -0
  38. GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/average_meters.cpython-312.pyc +0 -0
  39. GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/batch_normalizer.cpython-311.pyc +0 -0
  40. GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/batch_normalizer.cpython-312.pyc +0 -0
  41. GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/common.cpython-311.pyc +0 -0
  42. GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/common.cpython-312.pyc +0 -0
  43. GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/config_utils.cpython-311.pyc +0 -0
  44. GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/config_utils.cpython-312.pyc +0 -0
  45. GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/logging.cpython-311.pyc +0 -0
  46. GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/logging.cpython-312.pyc +0 -0
  47. GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/obs_utils.cpython-311.pyc +0 -0
  48. GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/obs_utils.cpython-312.pyc +0 -0
  49. GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/running_mean_std.cpython-311.pyc +0 -0
  50. GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/running_mean_std.cpython-312.pyc +0 -0
GR00T-WholeBodyControl/gear_sonic/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (153 Bytes). View file
 
GR00T-WholeBodyControl/gear_sonic/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (169 Bytes). View file
 
GR00T-WholeBodyControl/gear_sonic/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (157 Bytes). View file
 
GR00T-WholeBodyControl/gear_sonic/__pycache__/eval_agent_trl.cpython-312.pyc ADDED
Binary file (34.8 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/__pycache__/train_agent_trl.cpython-311.pyc ADDED
Binary file (23.8 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/__pycache__/train_agent_trl.cpython-312.pyc ADDED
Binary file (22.9 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/__pycache__/version.cpython-310.pyc ADDED
Binary file (365 Bytes). View file
 
GR00T-WholeBodyControl/gear_sonic/__pycache__/version.cpython-312.pyc ADDED
Binary file (466 Bytes). View file
 
GR00T-WholeBodyControl/gear_sonic/camera/sensor.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Base sensor abstract class.
2
+
3
+ The ``gymnasium`` dependency is lazy-imported so the camera server can
4
+ run without it.
5
+ """
6
+
7
+ from abc import abstractmethod
8
+ from typing import Any
9
+
10
+
11
+ class Sensor:
12
+ """Base class for camera / sensor implementations.
13
+
14
+ Concrete drivers (OAK, RealSense, ZED, USB, …) inherit from this and
15
+ implement at least :meth:`read` and :meth:`serialize`.
16
+ """
17
+
18
+ def read(self, **kwargs) -> Any:
19
+ """Read the current sensor value (e.g. a dict of images)."""
20
+
21
+ def observation_space(self):
22
+ """Return a ``gymnasium.Space`` describing the observation.
23
+
24
+ Only used during init to report camera capabilities to the
25
+ composed-camera orchestrator; not required for data collection.
26
+ """
27
+
28
+ @abstractmethod
29
+ def serialize(self, data: dict[str, Any]) -> dict[str, Any]:
30
+ """Serialize the sensor reading for ZMQ transmission."""
31
+
32
+ def close(self):
33
+ """Release hardware resources."""
GR00T-WholeBodyControl/gear_sonic/camera/sensor_server.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ZMQ PUB/SUB transport and image serialisation for the camera server."""
2
+
3
+ import base64
4
+ from dataclasses import dataclass, field
5
+ from enum import Enum
6
+ from typing import Any
7
+
8
+ import cv2
9
+ import msgpack
10
+ import msgpack_numpy as m
11
+ import numpy as np
12
+ import zmq
13
+
14
+
15
+ # =============================================================================
16
+ # Pose Message Schema
17
+ # =============================================================================
18
+ @dataclass
19
+ class PoseData:
20
+ """Single pose data point with quaternion orientation and translation."""
21
+
22
+ qx: float = 0.0
23
+ qy: float = 0.0
24
+ qz: float = 0.0
25
+ qw: float = 1.0
26
+ tx: float = 0.0
27
+ ty: float = 0.0
28
+ tz: float = 0.0
29
+
30
+ def to_dict(self) -> dict[str, float]:
31
+ return {
32
+ "qx": self.qx,
33
+ "qy": self.qy,
34
+ "qz": self.qz,
35
+ "qw": self.qw,
36
+ "tx": self.tx,
37
+ "ty": self.ty,
38
+ "tz": self.tz,
39
+ }
40
+
41
+ @staticmethod
42
+ def from_dict(data: dict[str, float]) -> "PoseData":
43
+ return PoseData(
44
+ qx=data.get("qx", 0.0),
45
+ qy=data.get("qy", 0.0),
46
+ qz=data.get("qz", 0.0),
47
+ qw=data.get("qw", 1.0),
48
+ tx=data.get("tx", 0.0),
49
+ ty=data.get("ty", 0.0),
50
+ tz=data.get("tz", 0.0),
51
+ )
52
+
53
+ def to_array(self) -> np.ndarray:
54
+ return np.array([self.qx, self.qy, self.qz, self.qw, self.tx, self.ty, self.tz])
55
+
56
+ @staticmethod
57
+ def from_array(arr: np.ndarray) -> "PoseData":
58
+ return PoseData(
59
+ qx=float(arr[0]),
60
+ qy=float(arr[1]),
61
+ qz=float(arr[2]),
62
+ qw=float(arr[3]),
63
+ tx=float(arr[4]),
64
+ ty=float(arr[5]),
65
+ tz=float(arr[6]),
66
+ )
67
+
68
+
69
+ @dataclass
70
+ class PoseMessageSchema:
71
+ """Standardized message schema for pose / positional data."""
72
+
73
+ timestamp: float = 0.0
74
+ device_id: str = "iphone"
75
+ pose: PoseData = field(default_factory=PoseData)
76
+
77
+ def serialize(self) -> bytes:
78
+ data = {
79
+ "timestamp": self.timestamp,
80
+ "device_id": self.device_id,
81
+ "pose": self.pose.to_dict(),
82
+ }
83
+ return msgpack.packb(data, use_bin_type=True)
84
+
85
+ @staticmethod
86
+ def deserialize(packed_data: bytes) -> "PoseMessageSchema":
87
+ data = msgpack.unpackb(packed_data, object_hook=m.decode)
88
+ return PoseMessageSchema(
89
+ timestamp=data.get("timestamp", 0.0),
90
+ device_id=data.get("device_id", "iphone"),
91
+ pose=PoseData.from_dict(data.get("pose", {})),
92
+ )
93
+
94
+ def asdict(self) -> dict[str, Any]:
95
+ return {
96
+ "timestamp": self.timestamp,
97
+ "device_id": self.device_id,
98
+ "pose": self.pose.to_dict(),
99
+ }
100
+
101
+
102
+ # =============================================================================
103
+ # Image Message Schema
104
+ # =============================================================================
105
+ @dataclass
106
+ class ImageMessageSchema:
107
+ """Standardized message schema for camera images.
108
+
109
+ Handles two encodings on the wire:
110
+
111
+ * **str** – legacy base64-encoded JPEG.
112
+ * **bytes** – raw JPEG from on-device MJPEG encoder (e.g. OAK).
113
+ """
114
+
115
+ timestamps: dict[str, float]
116
+ images: dict[str, np.ndarray]
117
+
118
+ def serialize(self) -> dict[str, Any]:
119
+ serialized_msg: dict[str, Any] = {"timestamps": self.timestamps, "images": {}}
120
+ for key, image in self.images.items():
121
+ if isinstance(image, bytes | bytearray):
122
+ serialized_msg["images"][key] = image
123
+ else:
124
+ serialized_msg["images"][key] = ImageUtils.encode_image(image)
125
+ return serialized_msg
126
+
127
+ @staticmethod
128
+ def deserialize(data: dict[str, Any]) -> "ImageMessageSchema":
129
+ timestamps = data.get("timestamps", {})
130
+ images = {}
131
+ for key, value in data.get("images", {}).items():
132
+ if isinstance(value, bytes | bytearray):
133
+ mat = cv2.imdecode(np.frombuffer(value, dtype=np.uint8), cv2.IMREAD_COLOR)
134
+ images[key] = mat[..., ::-1] # BGR -> RGB
135
+ elif isinstance(value, str):
136
+ images[key] = ImageUtils.decode_image(value)
137
+ elif isinstance(value, np.ndarray):
138
+ images[key] = value
139
+ elif isinstance(value, dict) and b"nd" in value:
140
+ images[key] = m.decode(value)
141
+ else:
142
+ images[key] = value
143
+ return ImageMessageSchema(timestamps=timestamps, images=images)
144
+
145
+ def asdict(self) -> dict[str, Any]:
146
+ return {"timestamps": self.timestamps, "images": self.images}
147
+
148
+
149
+ # =============================================================================
150
+ # ZMQ Server / Client
151
+ # =============================================================================
152
+ class SensorServer:
153
+ """ZMQ PUB server that streams msgpack-encoded sensor payloads."""
154
+
155
+ def start_server(self, port: int):
156
+ self.context = zmq.Context()
157
+ self.socket = self.context.socket(zmq.PUB)
158
+ self.socket.setsockopt(zmq.SNDHWM, 20)
159
+ self.socket.setsockopt(zmq.LINGER, 0)
160
+ self.socket.bind(f"tcp://*:{port}")
161
+ print(f"Sensor server running at tcp://*:{port}")
162
+
163
+ self.message_sent = 0
164
+ self.message_dropped = 0
165
+
166
+ def stop_server(self):
167
+ self.socket.close()
168
+ self.context.term()
169
+
170
+ def send_message(self, data: dict[str, Any]):
171
+ try:
172
+ packed = msgpack.packb(data, use_bin_type=True)
173
+ self.socket.send(packed, flags=zmq.NOBLOCK)
174
+ except zmq.Again:
175
+ self.message_dropped += 1
176
+ print(f"[Warning] message dropped: {self.message_dropped}")
177
+ self.message_sent += 1
178
+
179
+ if self.message_sent % 100 == 0:
180
+ print(
181
+ f"[Sensor server] Message sent: {self.message_sent}, "
182
+ f"message dropped: {self.message_dropped}"
183
+ )
184
+
185
+
186
+ class SensorClient:
187
+ """ZMQ SUB client that receives msgpack-encoded sensor payloads."""
188
+
189
+ def start_client(self, server_ip: str, port: int):
190
+ self.context = zmq.Context()
191
+ self.socket = self.context.socket(zmq.SUB)
192
+ self.socket.setsockopt_string(zmq.SUBSCRIBE, "")
193
+ self.socket.setsockopt(zmq.CONFLATE, True)
194
+ self.socket.setsockopt(zmq.RCVHWM, 3)
195
+ self.socket.connect(f"tcp://{server_ip}:{port}")
196
+
197
+ def stop_client(self):
198
+ self.socket.close()
199
+ self.context.term()
200
+
201
+ def receive_message(self):
202
+ packed = self.socket.recv()
203
+ return msgpack.unpackb(packed, object_hook=m.decode)
204
+
205
+ def receive_message_nonblocking(self, timeout_ms: int = 0):
206
+ if self.socket.poll(timeout_ms):
207
+ packed = self.socket.recv()
208
+ return msgpack.unpackb(packed, object_hook=m.decode)
209
+ return None
210
+
211
+
212
+ # =============================================================================
213
+ # Helpers
214
+ # =============================================================================
215
+ class CameraMountPosition(Enum):
216
+ EGO_VIEW = "ego_view"
217
+ HEAD = "head"
218
+ LEFT_WRIST = "left_wrist"
219
+ RIGHT_WRIST = "right_wrist"
220
+
221
+
222
+ class ImageUtils:
223
+ @staticmethod
224
+ def encode_image(image: np.ndarray) -> str:
225
+ _, color_buffer = cv2.imencode(".jpg", image, [int(cv2.IMWRITE_JPEG_QUALITY), 80])
226
+ return base64.b64encode(color_buffer).decode("utf-8")
227
+
228
+ @staticmethod
229
+ def encode_depth_image(image: np.ndarray) -> str:
230
+ depth_compressed = cv2.imencode(".png", image)[1].tobytes()
231
+ return base64.b64encode(depth_compressed).decode("utf-8")
232
+
233
+ @staticmethod
234
+ def decode_image(image: str) -> np.ndarray:
235
+ color_data = base64.b64decode(image)
236
+ color_array = np.frombuffer(color_data, dtype=np.uint8)
237
+ return cv2.imdecode(color_array, cv2.IMREAD_COLOR)
238
+
239
+ @staticmethod
240
+ def decode_depth_image(image: str) -> np.ndarray:
241
+ depth_data = base64.b64decode(image)
242
+ depth_array = np.frombuffer(depth_data, dtype=np.uint8)
243
+ return cv2.imdecode(depth_array, cv2.IMREAD_UNCHANGED)
GR00T-WholeBodyControl/gear_sonic/data/exporter.py ADDED
@@ -0,0 +1,472 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gr00t data exporter for LeRobot-format datasets.
3
+ """
4
+
5
+ import copy
6
+ from dataclasses import asdict, dataclass
7
+ from functools import partial
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+ import shutil
12
+ from typing import Any, Optional
13
+
14
+ import datasets
15
+ from datasets import load_dataset
16
+ from datasets.utils import disable_progress_bars
17
+ from huggingface_hub.errors import RepositoryNotFoundError
18
+ from lerobot.common.datasets.lerobot_dataset import (
19
+ LeRobotDataset,
20
+ LeRobotDatasetMetadata,
21
+ compute_episode_stats,
22
+ )
23
+ from lerobot.common.datasets.utils import (
24
+ check_timestamps_sync,
25
+ get_episode_data_index,
26
+ validate_episode_buffer,
27
+ validate_frame,
28
+ )
29
+ import numpy as np
30
+ from PIL import Image as PILImage
31
+ import torch
32
+ from torchvision import transforms
33
+
34
+ from gear_sonic.data.video_writer import VideoWriter
35
+
36
+ disable_progress_bars()
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # ArgsConfig (inlined from decoupled_wbc.control.main.config_template)
41
+ # ---------------------------------------------------------------------------
42
+
43
+
44
+ @dataclass
45
+ class ArgsConfig:
46
+ """Minimal config dataclass for script_config serialization."""
47
+
48
+ def update(
49
+ self,
50
+ config_dict: dict,
51
+ strict: bool = False,
52
+ skip_keys: list[str] = [],
53
+ allowed_keys: list[str] | None = None,
54
+ ):
55
+ for k, v in config_dict.items():
56
+ if k in skip_keys:
57
+ continue
58
+ if allowed_keys is not None and k not in allowed_keys:
59
+ continue
60
+ if strict and not hasattr(self, k):
61
+ raise ValueError(f"Config {k} not found in {self.__class__.__name__}")
62
+ if not strict and not hasattr(self, k):
63
+ continue
64
+ setattr(self, k, v)
65
+
66
+ @classmethod
67
+ def from_dict(
68
+ cls,
69
+ config_dict: dict,
70
+ strict: bool = False,
71
+ skip_keys: list[str] = [],
72
+ allowed_keys: list[str] | None = None,
73
+ ):
74
+ instance = cls()
75
+ instance.update(
76
+ config_dict=config_dict, strict=strict, skip_keys=skip_keys, allowed_keys=allowed_keys
77
+ )
78
+ return instance
79
+
80
+ def to_dict(self):
81
+ return asdict(self)
82
+
83
+ def get(self, key: str, default: Any = None):
84
+ return getattr(self, key) if hasattr(self, key) else default
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Gr00tDatasetMetadata
89
+ # ---------------------------------------------------------------------------
90
+
91
+
92
+ class Gr00tDatasetMetadata(LeRobotDatasetMetadata):
93
+ """Additional metadata on top of LeRobotDatasetMetadata:
94
+ - modality_config: Written to ``meta/modality.json``
95
+ - discarded_episode_indices: Written to ``meta/info.json``
96
+ """
97
+
98
+ MODALITY_CONFIG_REL_PATH = Path("meta/modality.json")
99
+
100
+ def __init__(self, *args, **kwargs):
101
+ super().__init__(*args, **kwargs)
102
+ with open(self.root / self.MODALITY_CONFIG_REL_PATH, "rb") as f:
103
+ self.modality_config = json.load(f)
104
+
105
+ @classmethod
106
+ def create(
107
+ cls,
108
+ modality_config: dict,
109
+ script_config: dict,
110
+ *args,
111
+ **kwargs,
112
+ ):
113
+ cls.validate_modality_config(modality_config)
114
+
115
+ obj = super().create(*args, **kwargs)
116
+
117
+ obj.info["script_config"] = script_config
118
+ obj.info["discarded_episode_indices"] = []
119
+ with open(obj.root / "meta" / "info.json", "w") as f:
120
+ json.dump(obj.info, f, indent=4)
121
+
122
+ obj.__class__ = cls
123
+ with open(obj.root / cls.MODALITY_CONFIG_REL_PATH, "w") as f:
124
+ json.dump(modality_config, f, indent=4)
125
+ obj.modality_config = modality_config
126
+ return obj
127
+
128
+ @staticmethod
129
+ def validate_modality_config(modality_config: dict) -> None:
130
+ valid_keys = ["state", "action", "video", "annotation"]
131
+ if not all(key in modality_config for key in valid_keys):
132
+ raise ValueError(
133
+ f"Modality config must contain all of the following keys: {valid_keys}"
134
+ )
135
+ for key in valid_keys:
136
+ if key not in modality_config:
137
+ raise ValueError(f"Modality config must contain a '{key}' key")
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # Gr00tDataExporter
142
+ # ---------------------------------------------------------------------------
143
+
144
+
145
+ class Gr00tDataExporter(LeRobotDataset):
146
+ """Exports data collected for a single session to LeRobot Dataset.
147
+
148
+ Lifecycle:
149
+ 1. Create a Gr00tDataExporter object
150
+ 2. Add frames using add_frame()
151
+ 3. Save the episode using save_episode()
152
+ - Flushes the episode buffer to disk
153
+ - Closes the video writers
154
+ - Creates new video writer and ep buffer for the next episode
155
+ """
156
+
157
+ def __init__(self, *args, **kwargs):
158
+ super().__init__(*args, **kwargs)
159
+ self.video_writers = self.create_video_writer()
160
+
161
+ @property
162
+ def repo_id(self):
163
+ return self.meta.repo_id
164
+
165
+ @property
166
+ def root(self):
167
+ return self.meta.root
168
+
169
+ @property
170
+ def local_files_only(self):
171
+ return self.meta.local_files_only
172
+
173
+ @property
174
+ def video_keys(self):
175
+ return self.meta.video_keys
176
+
177
+ @classmethod
178
+ def create(
179
+ cls,
180
+ save_root: str | Path,
181
+ fps: int,
182
+ features: dict,
183
+ modality_config: dict,
184
+ task: str,
185
+ script_config: ArgsConfig | dict = None,
186
+ robot_type: str | None = None,
187
+ tolerance_s: float = 1e-4,
188
+ vcodec: str = "h264",
189
+ overwrite_existing: bool = False,
190
+ ) -> "Gr00tDataExporter":
191
+ if script_config is None:
192
+ script_config = {}
193
+
194
+ obj = cls.__new__(cls)
195
+ repo_id = "tmp/tmp_dataset"
196
+
197
+ if overwrite_existing and (Path(save_root)).exists():
198
+ print(
199
+ f"Found existing dataset at {save_root}",
200
+ "Cleaning up this directory since overwrite_existing is True.",
201
+ )
202
+ shutil.rmtree(save_root)
203
+
204
+ if (Path(save_root)).exists():
205
+ try:
206
+ obj.meta = Gr00tDatasetMetadata(
207
+ repo_id=repo_id,
208
+ root=save_root,
209
+ )
210
+ except RepositoryNotFoundError as e:
211
+ raise ValueError(
212
+ f"Failed to resume from corrupted dataset. "
213
+ f"Please manually check the dataset at {save_root}"
214
+ ) from e
215
+ else:
216
+ if not isinstance(script_config, dict):
217
+ script_config = script_config.to_dict()
218
+ obj.meta = Gr00tDatasetMetadata.create(
219
+ repo_id=repo_id,
220
+ fps=fps,
221
+ root=save_root,
222
+ robot=None,
223
+ robot_type=robot_type,
224
+ features=features,
225
+ modality_config=modality_config,
226
+ script_config=script_config,
227
+ use_videos=True,
228
+ )
229
+
230
+ obj.tolerance_s = tolerance_s
231
+ obj.video_backend = "pyav"
232
+ obj.vcodec = vcodec
233
+ obj.task = task
234
+ obj.image_writer = None
235
+
236
+ obj.episode_buffer = obj.create_episode_buffer()
237
+
238
+ obj.episodes = None
239
+ obj.hf_dataset = obj.create_hf_dataset()
240
+ obj.image_transforms = None
241
+ obj.delta_timestamps = None
242
+ obj.delta_indices = None
243
+ obj.episode_data_index = None
244
+ obj.video_writers = obj.create_video_writer()
245
+ return obj
246
+
247
+ def create_video_writer(self) -> dict[str, VideoWriter]:
248
+ video_writers = {}
249
+ for key in self.meta.video_keys:
250
+ video_writers[key] = VideoWriter(
251
+ self.root
252
+ / self.meta.get_video_file_path(self.episode_buffer["episode_index"], key),
253
+ self.meta.shapes[key][1],
254
+ self.meta.shapes[key][0],
255
+ self.fps,
256
+ self.vcodec,
257
+ )
258
+ return video_writers
259
+
260
+ def add_frame(self, frame: dict) -> None:
261
+ """Add a frame to the episode buffer. Videos are handled by the video_writer."""
262
+ frame = copy.deepcopy(frame)
263
+ frame["task"] = frame.get("task", self.task)
264
+
265
+ for name in frame:
266
+ if isinstance(frame[name], torch.Tensor):
267
+ frame[name] = frame[name].numpy()
268
+
269
+ validate_frame(frame, self.features)
270
+
271
+ if self.episode_buffer is None:
272
+ self.episode_buffer = self.create_episode_buffer()
273
+
274
+ frame_index = self.episode_buffer["size"]
275
+ timestamp = frame.pop("timestamp") if "timestamp" in frame else frame_index / self.fps
276
+ self.episode_buffer["frame_index"].append(frame_index)
277
+ self.episode_buffer["timestamp"].append(timestamp)
278
+
279
+ for key in frame:
280
+ if key == "task":
281
+ self.episode_buffer["task"].append(frame["task"])
282
+ continue
283
+
284
+ if key not in self.features:
285
+ raise ValueError(
286
+ f"An element of the frame is not in the features. "
287
+ f"'{key}' not in '{self.features.keys()}'."
288
+ )
289
+
290
+ if self.features[key]["dtype"] in ["image", "video"]:
291
+ img_path = self._get_image_file_path(
292
+ episode_index=self.episode_buffer["episode_index"],
293
+ image_key=key,
294
+ frame_index=frame_index,
295
+ )
296
+ if frame_index == 0:
297
+ img_path.parent.mkdir(parents=True, exist_ok=True)
298
+
299
+ self.video_writers[key].add_frame(frame[key])
300
+ self.episode_buffer[key].append(str(img_path))
301
+ else:
302
+ self.episode_buffer[key].append(frame[key])
303
+
304
+ self.episode_buffer["size"] += 1
305
+
306
+ def stop_video_writers(self):
307
+ if not hasattr(self, "video_writers"):
308
+ raise RuntimeError(
309
+ "Can't stop video writers because they haven't been initialized. Call create() first."
310
+ )
311
+ for key in self.video_writers:
312
+ self.video_writers[key].stop()
313
+
314
+ def skip_and_start_new_episode(self) -> None:
315
+ """Skip the current episode and start a new one."""
316
+ self.stop_video_writers()
317
+ self.episode_buffer = self.create_episode_buffer()
318
+ self.video_writers = self.create_video_writer()
319
+
320
+ def save_episode(self, episode_data: dict | None = None) -> None:
321
+ if not episode_data:
322
+ episode_buffer = self.episode_buffer
323
+
324
+ validate_episode_buffer(episode_buffer, self.meta.total_episodes, self.features)
325
+
326
+ episode_length = episode_buffer.pop("size")
327
+ tasks = episode_buffer.pop("task")
328
+ episode_tasks = list(set(tasks))
329
+ episode_index = episode_buffer["episode_index"]
330
+
331
+ episode_buffer["index"] = np.arange(
332
+ self.meta.total_frames, self.meta.total_frames + episode_length
333
+ )
334
+ episode_buffer["episode_index"] = np.full((episode_length,), episode_index)
335
+
336
+ for task in episode_tasks:
337
+ task_index = self.meta.get_task_index(task)
338
+ if task_index is None:
339
+ self.meta.add_task(task)
340
+
341
+ episode_buffer["task_index"] = np.array([self.meta.get_task_index(task) for task in tasks])
342
+
343
+ for key, ft in self.features.items():
344
+ if key in ["index", "episode_index", "task_index"] or ft["dtype"] in ["image", "video"]:
345
+ continue
346
+ episode_buffer[key] = np.stack(episode_buffer[key])
347
+
348
+ self._wait_image_writer()
349
+ self._save_episode_table(episode_buffer, episode_index)
350
+
351
+ non_video_features = {k: v for k, v in self.features.items() if v["dtype"] not in ["video"]}
352
+ non_vid_ep_buffer = {
353
+ k: v for k, v in episode_buffer.items() if k in non_video_features.keys()
354
+ }
355
+ ep_stats = compute_episode_stats(non_vid_ep_buffer, non_video_features)
356
+
357
+ if len(self.meta.video_keys) > 0:
358
+ video_paths = self.encode_episode_videos(episode_index)
359
+ for key in self.meta.video_keys:
360
+ episode_buffer[key] = video_paths[key]
361
+
362
+ self.meta.save_episode(episode_index, episode_length, episode_tasks, ep_stats)
363
+
364
+ ep_data_index = get_episode_data_index(self.meta.episodes, [episode_index])
365
+ ep_data_index_np = {k: t.numpy() for k, t in ep_data_index.items()}
366
+ check_timestamps_sync(
367
+ episode_buffer["timestamp"],
368
+ episode_buffer["episode_index"],
369
+ ep_data_index_np,
370
+ self.fps,
371
+ self.tolerance_s,
372
+ )
373
+
374
+ video_files = list(self.root.rglob("*.mp4"))
375
+ assert len(video_files) == self.num_episodes * len(self.meta.video_keys)
376
+
377
+ parquet_files = list(self.root.rglob("*.parquet"))
378
+ assert len(parquet_files) == self.num_episodes
379
+
380
+ img_dir = self.root / "images"
381
+ if img_dir.is_dir():
382
+ shutil.rmtree(self.root / "images")
383
+
384
+ if not episode_data:
385
+ self.episode_buffer = self.create_episode_buffer()
386
+ self.video_writers = self.create_video_writer()
387
+
388
+ for key in self.meta.video_keys:
389
+ video_path = os.path.join(self.root, self.meta.get_video_file_path(episode_index, key))
390
+ if not os.path.exists(video_path):
391
+ raise FileNotFoundError(
392
+ f"Video path: {video_path} does not exist for episode {episode_index}"
393
+ )
394
+
395
+ parquet_path = os.path.join(self.root, self.meta.get_data_file_path(episode_index))
396
+ if not os.path.exists(parquet_path):
397
+ raise FileNotFoundError(
398
+ f"Parquet path: {parquet_path} does not exist for episode {episode_index}"
399
+ )
400
+
401
+ def encode_episode_videos(self, episode_index: int) -> dict:
402
+ video_paths = {}
403
+ for key in self.meta.video_keys:
404
+ video_paths[key] = self.video_writers[key].stop()
405
+ return video_paths
406
+
407
+ def save_episode_as_discarded(self) -> None:
408
+ """Flag ongoing episode as discarded and save it to disk."""
409
+ self.meta.info["discarded_episode_indices"] = self.meta.info.get(
410
+ "discarded_episode_indices", []
411
+ ) + [self.episode_buffer["episode_index"]]
412
+ self.save_episode()
413
+
414
+
415
+ # ---------------------------------------------------------------------------
416
+ # HF dataset helpers (for loading saved datasets)
417
+ # ---------------------------------------------------------------------------
418
+
419
+
420
+ def hf_transform_to_torch_by_features(
421
+ features: datasets.Sequence, items_dict: dict[torch.Tensor | None]
422
+ ):
423
+ for key in items_dict:
424
+ first_item = items_dict[key][0]
425
+ if isinstance(first_item, PILImage.Image):
426
+ to_tensor = transforms.ToTensor()
427
+ items_dict[key] = [to_tensor(img) for img in items_dict[key]]
428
+ elif first_item is None:
429
+ pass
430
+ else:
431
+ if isinstance(features[key], datasets.Value):
432
+ dtype_str = features[key].dtype
433
+ elif isinstance(features[key], datasets.Sequence):
434
+ assert isinstance(features[key].feature, datasets.Value)
435
+ dtype_str = features[key].feature.dtype
436
+ else:
437
+ raise ValueError(f"Unsupported feature type for key '{key}': {features[key]}")
438
+ dtype_mapping = {
439
+ "float32": torch.float32,
440
+ "float64": torch.float64,
441
+ "int32": torch.int32,
442
+ "int64": torch.int64,
443
+ }
444
+ items_dict[key] = [
445
+ torch.tensor(x, dtype=dtype_mapping[dtype_str]) for x in items_dict[key]
446
+ ]
447
+ return items_dict
448
+
449
+
450
+ class TypedLeRobotDataset(LeRobotDataset):
451
+ def __init__(self, load_video=True, *args, **kwargs):
452
+ super().__init__(*args, **kwargs)
453
+ if not load_video:
454
+ video_keys = []
455
+ for key in self.meta.features.keys():
456
+ if self.meta.features[key]["dtype"] == "video":
457
+ video_keys.append(key)
458
+ for key in video_keys:
459
+ self.meta.features.pop(key)
460
+
461
+ def load_hf_dataset(self) -> datasets.Dataset:
462
+ if self.episodes is None:
463
+ path = str(self.root / "data")
464
+ hf_dataset = load_dataset("parquet", data_dir=path, split="train")
465
+ else:
466
+ files = [
467
+ str(self.root / self.meta.get_data_file_path(ep_idx)) for ep_idx in self.episodes
468
+ ]
469
+ hf_dataset = load_dataset("parquet", data_files=files, split="train")
470
+
471
+ hf_dataset.set_transform(partial(hf_transform_to_torch_by_features, hf_dataset.features))
472
+ return hf_dataset
GR00T-WholeBodyControl/gear_sonic/data/features_sonic_vla.py ADDED
@@ -0,0 +1,410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Dataset configuration for the Sonic VLA pipeline.
3
+
4
+ Provides feature/modality config dicts and a convenience function to
5
+ instantiate the G1 RobotModel needed for FK and joint configuration
6
+ assembly during data collection.
7
+
8
+ Joint names, counts, and group indices are derived at runtime from the
9
+ ``RobotModel`` (via its ``supplemental_info``).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Literal
15
+
16
+ from gear_sonic.data.robot_model import RobotModel
17
+
18
+ EGO_VIEW_HEIGHT: int = 480
19
+ EGO_VIEW_WIDTH: int = 640
20
+ WRIST_VIEW_HEIGHT: int = 480
21
+ WRIST_VIEW_WIDTH: int = 640
22
+ FPS: int = 50
23
+
24
+
25
+ _JOINT_GROUPS_FOR_STATE: list[str] = [
26
+ "left_leg",
27
+ "right_leg",
28
+ "waist",
29
+ "left_arm",
30
+ "left_hand",
31
+ "right_arm",
32
+ "right_hand",
33
+ ]
34
+
35
+
36
+ def _get_joint_group_slices(robot_model: RobotModel) -> dict[str, dict[str, int]]:
37
+ """Derive ``{group_name: {"start": ..., "end": ...}}`` from the robot model."""
38
+ slices: dict[str, dict[str, int]] = {}
39
+ for group in _JOINT_GROUPS_FOR_STATE:
40
+ indices = sorted(robot_model.get_joint_group_indices(group))
41
+ slices[group] = {"start": indices[0], "end": indices[-1] + 1}
42
+ return slices
43
+
44
+
45
+ def get_modality_config_sonic_vla(robot_model: RobotModel) -> dict:
46
+ """Return the modality config for the Sonic VLA dataset.
47
+
48
+ Produces the exact content of meta/modality.json.
49
+ """
50
+ group_slices = _get_joint_group_slices(robot_model)
51
+
52
+ return {
53
+ "state": {
54
+ **group_slices,
55
+ "left_wrist_pos": {
56
+ "start": 0,
57
+ "end": 3,
58
+ "original_key": "observation.eef_state",
59
+ },
60
+ "left_wrist_abs_quat": {
61
+ "start": 3,
62
+ "end": 7,
63
+ "original_key": "observation.eef_state",
64
+ "rotation_type": "quaternion",
65
+ },
66
+ "right_wrist_pos": {
67
+ "start": 7,
68
+ "end": 10,
69
+ "original_key": "observation.eef_state",
70
+ },
71
+ "right_wrist_abs_quat": {
72
+ "start": 10,
73
+ "end": 14,
74
+ "original_key": "observation.eef_state",
75
+ "rotation_type": "quaternion",
76
+ },
77
+ "root_orientation": {
78
+ "start": 0,
79
+ "end": 4,
80
+ "original_key": "observation.root_orientation",
81
+ "rotation_type": "quaternion",
82
+ },
83
+ "projected_gravity": {
84
+ "start": 0,
85
+ "end": 3,
86
+ "original_key": "observation.projected_gravity",
87
+ },
88
+ "cpp_rotation_offset": {
89
+ "start": 0,
90
+ "end": 4,
91
+ "original_key": "observation.cpp_rotation_offset",
92
+ "rotation_type": "quaternion",
93
+ },
94
+ "init_base_quat": {
95
+ "start": 0,
96
+ "end": 4,
97
+ "original_key": "observation.init_base_quat",
98
+ "rotation_type": "quaternion",
99
+ },
100
+ },
101
+ "action": {
102
+ "delta_heading": {
103
+ "start": 0,
104
+ "end": 1,
105
+ "original_key": "teleop.delta_heading",
106
+ },
107
+ "motion_token": {
108
+ "start": 0,
109
+ "end": 64,
110
+ "original_key": "action.motion_token",
111
+ },
112
+ "smpl_joints": {
113
+ "start": 0,
114
+ "end": 72,
115
+ "original_key": "teleop.smpl_joints",
116
+ },
117
+ "smpl_pose": {
118
+ "start": 0,
119
+ "end": 63,
120
+ "original_key": "teleop.smpl_pose",
121
+ },
122
+ "body_quat_w": {
123
+ "start": 0,
124
+ "end": 4,
125
+ "original_key": "teleop.body_quat_w",
126
+ "rotation_type": "quaternion",
127
+ },
128
+ "target_body_orientation": {
129
+ "start": 0,
130
+ "end": 6,
131
+ "original_key": "teleop.target_body_orientation",
132
+ "rotation_type": "rotation_6d",
133
+ },
134
+ "left_hand_joints": {
135
+ "start": 0,
136
+ "end": 7,
137
+ "original_key": "teleop.left_hand_joints",
138
+ },
139
+ "right_hand_joints": {
140
+ "start": 0,
141
+ "end": 7,
142
+ "original_key": "teleop.right_hand_joints",
143
+ },
144
+ "left_wrist_joints": {
145
+ "start": 0,
146
+ "end": 3,
147
+ "original_key": "teleop.left_wrist_joints",
148
+ },
149
+ "right_wrist_joints": {
150
+ "start": 0,
151
+ "end": 3,
152
+ "original_key": "teleop.right_wrist_joints",
153
+ },
154
+ "stream_mode": {
155
+ "start": 0,
156
+ "end": 1,
157
+ "original_key": "teleop.stream_mode",
158
+ },
159
+ "planner_mode": {
160
+ "start": 0,
161
+ "end": 1,
162
+ "original_key": "teleop.planner_mode",
163
+ },
164
+ "planner_movement": {
165
+ "start": 0,
166
+ "end": 3,
167
+ "original_key": "teleop.planner_movement",
168
+ },
169
+ "planner_facing": {
170
+ "start": 0,
171
+ "end": 3,
172
+ "original_key": "teleop.planner_facing",
173
+ },
174
+ "planner_speed": {
175
+ "start": 0,
176
+ "end": 1,
177
+ "original_key": "teleop.planner_speed",
178
+ },
179
+ "planner_height": {
180
+ "start": 0,
181
+ "end": 1,
182
+ "original_key": "teleop.planner_height",
183
+ },
184
+ "vr_3pt_position": {
185
+ "start": 0,
186
+ "end": 9,
187
+ "original_key": "teleop.vr_3pt_position",
188
+ },
189
+ "vr_3pt_orientation": {
190
+ "start": 0,
191
+ "end": 18,
192
+ "original_key": "teleop.vr_3pt_orientation",
193
+ "rotation_type": "rotation_6d",
194
+ },
195
+ },
196
+ "video": {
197
+ "ego_view": {"original_key": "observation.images.ego_view"},
198
+ },
199
+ "annotation": {
200
+ "human.task_description": {"original_key": "task_index"},
201
+ },
202
+ }
203
+
204
+
205
+ def get_features_sonic_vla(robot_model: RobotModel) -> dict:
206
+ """Return the dataset features for the Sonic VLA dataset.
207
+
208
+ The returned dict populates the "features" key of meta/info.json.
209
+ """
210
+ joint_names = robot_model.joint_names
211
+ num_joints = robot_model.num_joints
212
+
213
+ return {
214
+ "observation.images.ego_view": {
215
+ "dtype": "video",
216
+ "shape": [EGO_VIEW_HEIGHT, EGO_VIEW_WIDTH, 3],
217
+ "names": ["height", "width", "channel"],
218
+ },
219
+ "observation.state": {
220
+ "dtype": "float64",
221
+ "shape": (num_joints,),
222
+ "names": joint_names,
223
+ },
224
+ "observation.eef_state": {
225
+ "dtype": "float64",
226
+ "shape": (14,),
227
+ "names": [
228
+ "left_wrist_pos",
229
+ "left_wrist_abs_quat",
230
+ "right_wrist_pos",
231
+ "right_wrist_abs_quat",
232
+ ],
233
+ },
234
+ "action.wbc": {
235
+ "dtype": "float64",
236
+ "shape": (num_joints,),
237
+ "names": joint_names,
238
+ },
239
+ "observation.root_orientation": {
240
+ "dtype": "float64",
241
+ "shape": (4,),
242
+ "names": ["base_qw", "base_qx", "base_qy", "base_qz"],
243
+ },
244
+ "observation.projected_gravity": {
245
+ "dtype": "float64",
246
+ "shape": (3,),
247
+ "names": ["gravity_x", "gravity_y", "gravity_z"],
248
+ },
249
+ "observation.cpp_rotation_offset": {
250
+ "dtype": "float64",
251
+ "shape": (4,),
252
+ "names": ["rot_offset_qw", "rot_offset_qx", "rot_offset_qy", "rot_offset_qz"],
253
+ },
254
+ "observation.init_base_quat": {
255
+ "dtype": "float64",
256
+ "shape": (4,),
257
+ "names": ["init_base_qw", "init_base_qx", "init_base_qy", "init_base_qz"],
258
+ },
259
+ "teleop.delta_heading": {
260
+ "dtype": "float64",
261
+ "shape": (1,),
262
+ "names": ["delta_heading"],
263
+ },
264
+ "action.motion_token": {
265
+ "dtype": "float64",
266
+ "shape": (64,),
267
+ "names": "motion_token",
268
+ },
269
+ "teleop.smpl_joints": {
270
+ "dtype": "float32",
271
+ "shape": (72,),
272
+ "names": "smpl_joints",
273
+ },
274
+ "teleop.smpl_pose": {
275
+ "dtype": "float32",
276
+ "shape": (63,),
277
+ "names": "smpl_pose",
278
+ },
279
+ "teleop.body_quat_w": {
280
+ "dtype": "float32",
281
+ "shape": (4,),
282
+ "names": "body_quat_w",
283
+ },
284
+ "teleop.target_body_orientation": {
285
+ "dtype": "float32",
286
+ "shape": (6,),
287
+ "names": [
288
+ "target_body_r00",
289
+ "target_body_r10",
290
+ "target_body_r01",
291
+ "target_body_r11",
292
+ "target_body_r02",
293
+ "target_body_r12",
294
+ ],
295
+ },
296
+ "teleop.left_hand_joints": {
297
+ "dtype": "float32",
298
+ "shape": (7,),
299
+ "names": "left_hand_joints",
300
+ },
301
+ "teleop.right_hand_joints": {
302
+ "dtype": "float32",
303
+ "shape": (7,),
304
+ "names": "right_hand_joints",
305
+ },
306
+ "teleop.smpl_frame_index": {
307
+ "dtype": "int64",
308
+ "shape": (1,),
309
+ "names": ["smpl_frame_index"],
310
+ },
311
+ "teleop.left_wrist_joints": {
312
+ "dtype": "float32",
313
+ "shape": (3,),
314
+ "names": ["left_wrist_roll", "left_wrist_pitch", "left_wrist_yaw"],
315
+ },
316
+ "teleop.right_wrist_joints": {
317
+ "dtype": "float32",
318
+ "shape": (3,),
319
+ "names": ["right_wrist_roll", "right_wrist_pitch", "right_wrist_yaw"],
320
+ },
321
+ "teleop.stream_mode": {
322
+ "dtype": "int32",
323
+ "shape": (1,),
324
+ "names": ["stream_mode"],
325
+ },
326
+ "teleop.planner_mode": {
327
+ "dtype": "int32",
328
+ "shape": (1,),
329
+ "names": ["locomotion_mode"],
330
+ },
331
+ "teleop.planner_movement": {
332
+ "dtype": "float32",
333
+ "shape": (3,),
334
+ "names": ["movement_x", "movement_y", "movement_z"],
335
+ },
336
+ "teleop.planner_facing": {
337
+ "dtype": "float32",
338
+ "shape": (3,),
339
+ "names": ["facing_x", "facing_y", "facing_z"],
340
+ },
341
+ "teleop.planner_speed": {
342
+ "dtype": "float32",
343
+ "shape": (1,),
344
+ "names": ["speed"],
345
+ },
346
+ "teleop.planner_height": {
347
+ "dtype": "float32",
348
+ "shape": (1,),
349
+ "names": ["height"],
350
+ },
351
+ "teleop.vr_3pt_position": {
352
+ "dtype": "float32",
353
+ "shape": (9,),
354
+ "names": [
355
+ "lwrist_x", "lwrist_y", "lwrist_z",
356
+ "rwrist_x", "rwrist_y", "rwrist_z",
357
+ "neck_x", "neck_y", "neck_z",
358
+ ],
359
+ },
360
+ "teleop.vr_3pt_orientation": {
361
+ "dtype": "float32",
362
+ "shape": (18,),
363
+ "names": [
364
+ "lwrist_r00", "lwrist_r10", "lwrist_r01", "lwrist_r11", "lwrist_r02", "lwrist_r12",
365
+ "rwrist_r00", "rwrist_r10", "rwrist_r01", "rwrist_r11", "rwrist_r02", "rwrist_r12",
366
+ "neck_r00", "neck_r10", "neck_r01", "neck_r11", "neck_r02", "neck_r12",
367
+ ],
368
+ },
369
+ }
370
+
371
+
372
+ def get_wrist_camera_features() -> dict:
373
+ """Features for optional wrist cameras (added when ``record_wrist_cameras`` is enabled)."""
374
+ return {
375
+ "observation.images.left_wrist": {
376
+ "dtype": "video",
377
+ "shape": [WRIST_VIEW_HEIGHT, WRIST_VIEW_WIDTH, 3],
378
+ "names": ["height", "width", "channel"],
379
+ },
380
+ "observation.images.right_wrist": {
381
+ "dtype": "video",
382
+ "shape": [WRIST_VIEW_HEIGHT, WRIST_VIEW_WIDTH, 3],
383
+ "names": ["height", "width", "channel"],
384
+ },
385
+ }
386
+
387
+
388
+ def get_wrist_camera_modality_config() -> dict:
389
+ """Modality config entries for optional wrist cameras."""
390
+ return {
391
+ "video": {
392
+ "left_wrist": {"original_key": "observation.images.left_wrist"},
393
+ "right_wrist": {"original_key": "observation.images.right_wrist"},
394
+ },
395
+ }
396
+
397
+
398
+ def get_g1_robot_model(
399
+ waist_location: Literal[
400
+ "lower_body", "upper_body", "lower_and_upper_body"
401
+ ] = "lower_and_upper_body",
402
+ high_elbow_pose: bool = False,
403
+ ):
404
+ """Instantiate the G1 + ThreeFinger RobotModel for Sonic VLA."""
405
+ from gear_sonic.data.robot_model.instantiation.g1 import instantiate_g1_robot_model
406
+
407
+ return instantiate_g1_robot_model(
408
+ waist_location=waist_location,
409
+ high_elbow_pose=high_elbow_pose,
410
+ )
GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/left_ankle_pitch_link.STL ADDED
Binary file (71.2 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/left_elbow_link.STL ADDED
Binary file (88.8 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/left_hand_thumb_0_link.STL ADDED
Binary file (8.88 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/right_ankle_pitch_link.STL ADDED
Binary file (71.2 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/right_elbow_link.STL ADDED
Binary file (88.8 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/right_hand_thumb_0_link.STL ADDED
Binary file (8.88 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/right_wrist_pitch_link.STL ADDED
Binary file (79.6 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/torso_constraint_L_rod_link.STL ADDED
Binary file (74.9 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/torso_constraint_R_rod_link.STL ADDED
Binary file (74.9 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/waist_roll_link.STL ADDED
Binary file (24.2 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/waist_roll_link_rev_1_0.STL ADDED
Binary file (85.9 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/data/video_writer.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import queue
3
+ import sys
4
+ import threading
5
+ import time
6
+
7
+ import av
8
+ import numpy as np
9
+
10
+
11
+ class VideoWriter:
12
+ def __init__(
13
+ self,
14
+ output_path: str,
15
+ width: int,
16
+ height: int,
17
+ fps: float,
18
+ codec: str = "h264",
19
+ buffer_size: int = 50,
20
+ ):
21
+ self.output_path = output_path
22
+ self._first_frame = True
23
+
24
+ output_dir = os.path.dirname(output_path)
25
+ if output_dir and not os.path.exists(output_dir):
26
+ os.makedirs(output_dir, exist_ok=True)
27
+
28
+ self.queue = queue.Queue(maxsize=buffer_size)
29
+ self.container = av.open(output_path, mode="w")
30
+ self.stream = self.container.add_stream(codec, rate=fps)
31
+ self.stream.width = width
32
+ self.stream.height = height
33
+ thread = threading.Thread(target=self._writer_worker, daemon=True)
34
+ thread.start()
35
+
36
+ def _assert_dimensions(self, frame: np.ndarray) -> None:
37
+ assert (
38
+ frame.shape[1] == self.stream.width and frame.shape[0] == self.stream.height
39
+ ), (
40
+ f"Incorrect frame dimensions. Input dimensions: {frame.shape[1]}x{frame.shape[0]}. "
41
+ f"Expected dimensions: {self.stream.width}x{self.stream.height}"
42
+ )
43
+
44
+ def add_frame(self, frame: np.ndarray) -> None:
45
+ self._assert_dimensions(frame)
46
+ self.queue.put(frame)
47
+
48
+ def _writer_worker(self) -> None:
49
+ while True:
50
+ frame = self.queue.get()
51
+ if frame is None:
52
+ continue
53
+ self._assert_dimensions(frame)
54
+ frame = av.VideoFrame.from_ndarray(frame, format="rgb24")
55
+
56
+ if self._first_frame:
57
+ stderr_fd = sys.stderr.fileno()
58
+ old_stderr = os.dup(stderr_fd)
59
+ devnull = os.open(os.devnull, os.O_WRONLY)
60
+ os.dup2(devnull, stderr_fd)
61
+ try:
62
+ packets = self.stream.encode(frame)
63
+ for packet in packets:
64
+ self.container.mux(packet)
65
+ finally:
66
+ os.dup2(old_stderr, stderr_fd)
67
+ os.close(old_stderr)
68
+ os.close(devnull)
69
+ self._first_frame = False
70
+ else:
71
+ packets = self.stream.encode(frame)
72
+ for packet in packets:
73
+ self.container.mux(packet)
74
+
75
+ def _flush_stream(self) -> None:
76
+ packets = self.stream.encode()
77
+ for packet in packets:
78
+ self.container.mux(packet)
79
+
80
+ def stop(self) -> str:
81
+ """Blocking call. Waits for queue to drain, flushes, and closes the container."""
82
+ if not self.queue.empty():
83
+ print("Waiting for video writer queue to empty...")
84
+ while not self.queue.empty():
85
+ time.sleep(0.1)
86
+
87
+ print("Video writer queue is empty, flushing stream...")
88
+ self._flush_stream()
89
+ self.container.close()
90
+ return self.output_path
91
+
92
+ def cancel(self) -> None:
93
+ """Immediately stops writing and deletes the output file."""
94
+ if os.path.exists(self.output_path):
95
+ os.remove(self.output_path)
96
+ self.container.close()
97
+
98
+ def __del__(self) -> None:
99
+ self.container.close()
GR00T-WholeBodyControl/gear_sonic/data_process/convert_soma_csv_to_motion_lib.py ADDED
@@ -0,0 +1,584 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3 # noqa: EXE001
2
+ # ruff: noqa: T201, DOC
3
+ """Convert SOMA retargeter CSV/PKL data to motion_lib format for SONIC training.
4
+
5
+ SOMA retargeter outputs G1 29-DOF motion data as CSV files (joint_pos.csv,
6
+ body_pos.csv, body_quat.csv) or as a joblib PKL with the same fields. This
7
+ script converts that data into the motion_lib PKL format expected by SONIC
8
+ training (root_trans_offset, pose_aa, dof, root_rot, fps).
9
+
10
+ Supports five input modes:
11
+ 1. Single motion directory with CSVs (joint_pos.csv, body_pos.csv, body_quat.csv)
12
+ 2. Parent directory containing multiple motion subdirectories
13
+ 3. Deploy PKL file (joblib dict with joint_pos, body_pos_w, body_quat_w per sequence)
14
+ 4. Directory of flat Bones-SEED CSVs (single CSV per motion, degrees+cm)
15
+ 5. Parent directory of session dirs containing Bones-SEED CSVs
16
+
17
+ Usage:
18
+ # Single CSV directory
19
+ python scripts/motion/convert_soma_csv_to_motion_lib.py \
20
+ --input data/soma_retarget/tired_squat_003__A360 \
21
+ --output data/soma_test.pkl --fps 50
22
+
23
+ # Batch: parent dir with multiple motion subdirs
24
+ python scripts/motion/convert_soma_csv_to_motion_lib.py \
25
+ --input data/soma_retarget/all_demo_4seqs \
26
+ --output data/soma_demo_4seqs.pkl --fps 50
27
+
28
+ # Deploy PKL file
29
+ python scripts/motion/convert_soma_csv_to_motion_lib.py \
30
+ --input data/soma_retarget/bones_test.pkl \
31
+ --output data/soma_bones_test.pkl --fps 50
32
+
33
+ # Bones-SEED: directory of flat CSVs (single session)
34
+ python scripts/motion/convert_soma_csv_to_motion_lib.py \
35
+ --input /path/to/bones_SEED/g1/csv/210531 \
36
+ --output data/bones_seed_210531.pkl --fps 50
37
+
38
+ # Bones-SEED: all sessions (parent dir)
39
+ python scripts/motion/convert_soma_csv_to_motion_lib.py \
40
+ --input /path/to/bones_SEED/g1/csv \
41
+ --output data/bones_seed_all.pkl --fps 50
42
+ """
43
+
44
+ import argparse
45
+ import os
46
+ import sys
47
+
48
+ import joblib
49
+ import numpy as np
50
+ from scipy.spatial import transform
51
+
52
+ # IsaacLab ↔ MuJoCo joint reordering (29 DOFs for G1).
53
+ # MJ_TO_IL[mj] = il: for MuJoCo DOF index mj, gives the IsaacLab index il.
54
+ # Source: external_dependencies/SONIC_Web/demo_python.py
55
+ MJ_TO_IL = np.array(
56
+ [
57
+ 0,
58
+ 3,
59
+ 6,
60
+ 9,
61
+ 13,
62
+ 17,
63
+ 1,
64
+ 4,
65
+ 7,
66
+ 10,
67
+ 14,
68
+ 18,
69
+ 2,
70
+ 5,
71
+ 8,
72
+ 11,
73
+ 15,
74
+ 19,
75
+ 21,
76
+ 23,
77
+ 25,
78
+ 27,
79
+ 12,
80
+ 16,
81
+ 20,
82
+ 22,
83
+ 24,
84
+ 26,
85
+ 28,
86
+ ],
87
+ dtype=np.int32,
88
+ )
89
+
90
+ # G1 29-DOF axis definitions (from Humanoid_Batch / g1_29dof_rev_1_0.xml).
91
+ # Each DOF rotates around a single axis. Hardcoded to avoid torch dependency.
92
+ NUM_DOF = 29
93
+ NUM_BODIES = 30 # pelvis + 29 actuated links
94
+ DOF_AXIS = np.array(
95
+ [
96
+ [0, 1, 0],
97
+ [1, 0, 0],
98
+ [0, 0, 1],
99
+ [0, 1, 0],
100
+ [0, 1, 0],
101
+ [1, 0, 0], # left leg
102
+ [0, 1, 0],
103
+ [1, 0, 0],
104
+ [0, 0, 1],
105
+ [0, 1, 0],
106
+ [0, 1, 0],
107
+ [1, 0, 0], # right leg
108
+ [0, 0, 1],
109
+ [1, 0, 0],
110
+ [0, 1, 0], # waist
111
+ [0, 1, 0],
112
+ [1, 0, 0],
113
+ [0, 0, 1],
114
+ [0, 1, 0],
115
+ [1, 0, 0],
116
+ [0, 1, 0],
117
+ [0, 0, 1], # left arm
118
+ [0, 1, 0],
119
+ [1, 0, 0],
120
+ [0, 0, 1],
121
+ [0, 1, 0],
122
+ [1, 0, 0],
123
+ [0, 1, 0],
124
+ [0, 0, 1], # right arm
125
+ ],
126
+ dtype=np.float32,
127
+ )
128
+
129
+
130
+ # Joint names in Bones-SEED CSV column order (after Frame + 6 root columns).
131
+ # These are in MuJoCo/MJCF actuator order (same as g1_29dof_rev_1_0.xml motors).
132
+ BONES_CSV_JOINT_NAMES = [
133
+ "left_hip_pitch_joint_dof",
134
+ "left_hip_roll_joint_dof",
135
+ "left_hip_yaw_joint_dof",
136
+ "left_knee_joint_dof",
137
+ "left_ankle_pitch_joint_dof",
138
+ "left_ankle_roll_joint_dof",
139
+ "right_hip_pitch_joint_dof",
140
+ "right_hip_roll_joint_dof",
141
+ "right_hip_yaw_joint_dof",
142
+ "right_knee_joint_dof",
143
+ "right_ankle_pitch_joint_dof",
144
+ "right_ankle_roll_joint_dof",
145
+ "waist_yaw_joint_dof",
146
+ "waist_roll_joint_dof",
147
+ "waist_pitch_joint_dof",
148
+ "left_shoulder_pitch_joint_dof",
149
+ "left_shoulder_roll_joint_dof",
150
+ "left_shoulder_yaw_joint_dof",
151
+ "left_elbow_joint_dof",
152
+ "left_wrist_roll_joint_dof",
153
+ "left_wrist_pitch_joint_dof",
154
+ "left_wrist_yaw_joint_dof",
155
+ "right_shoulder_pitch_joint_dof",
156
+ "right_shoulder_roll_joint_dof",
157
+ "right_shoulder_yaw_joint_dof",
158
+ "right_elbow_joint_dof",
159
+ "right_wrist_roll_joint_dof",
160
+ "right_wrist_pitch_joint_dof",
161
+ "right_wrist_yaw_joint_dof",
162
+ ]
163
+
164
+
165
+ def load_bones_csv(csv_path: str) -> dict:
166
+ """Load a single Bones-SEED flat CSV motion file.
167
+
168
+ Bones-SEED CSV format: Frame, root_translate{X,Y,Z}, root_rotate{X,Y,Z}, 29 joint DOFs.
169
+ All angles in degrees, positions in centimeters.
170
+ """
171
+ import pandas as pd
172
+
173
+ data = pd.read_csv(csv_path)
174
+ T = len(data)
175
+
176
+ # Root position: cm → meters
177
+ root_pos = (
178
+ np.stack(
179
+ [
180
+ data["root_translateX"].values, # noqa: PD011
181
+ data["root_translateY"].values, # noqa: PD011
182
+ data["root_translateZ"].values, # noqa: PD011
183
+ ],
184
+ axis=1,
185
+ ).astype(np.float32)
186
+ / 100.0
187
+ ) # cm → m
188
+
189
+ # Root rotation: Euler xyz (intrinsic) degrees → quaternion (xyzw scipy convention)
190
+ # Reference: gear_sonic/data_process/process_bones_to_motionlib.py uses "xyz" (intrinsic)
191
+ euler_deg = np.stack(
192
+ [
193
+ data["root_rotateX"].values, # noqa: PD011
194
+ data["root_rotateY"].values, # noqa: PD011
195
+ data["root_rotateZ"].values, # noqa: PD011
196
+ ],
197
+ axis=1,
198
+ ).astype(np.float64)
199
+ root_quat_xyzw = (
200
+ transform.Rotation.from_euler("xyz", euler_deg, degrees=True).as_quat().astype(np.float32)
201
+ )
202
+ # Convert xyzw → wxyz for body_quat_w format
203
+ root_quat_wxyz = root_quat_xyzw[:, [3, 0, 1, 2]]
204
+
205
+ # Joint DOFs: degrees → radians, already in MuJoCo/MJCF actuator order
206
+ joint_cols = [c for c in data.columns if c.endswith("_dof")]
207
+ joint_pos_mj = np.deg2rad(data[joint_cols].values).astype(np.float32) # (T, 29)
208
+
209
+ # Create dummy body_pos_w and body_quat_w (only root body populated, rest zeros)
210
+ # The converter only uses body_pos_w[:,0] for root_trans and body_quat_w[:,0] for root_rot
211
+ body_pos_w = np.zeros((T, 14, 3), dtype=np.float32)
212
+ body_pos_w[:, 0, :] = root_pos
213
+ body_quat_w = np.zeros((T, 14, 4), dtype=np.float32)
214
+ body_quat_w[:, :, 0] = 1.0 # identity quaternion wxyz
215
+ body_quat_w[:, 0, :] = root_quat_wxyz
216
+
217
+ return {
218
+ "joint_pos": joint_pos_mj, # (T, 29) MuJoCo order, radians
219
+ "body_pos_w": body_pos_w, # (T, 14, 3)
220
+ "body_quat_w": body_quat_w, # (T, 14, 4) wxyz
221
+ "joint_order": "mj", # already in MuJoCo order, skip IL→MJ reorder
222
+ }
223
+
224
+
225
+ def load_csv_motion(motion_dir: str) -> dict:
226
+ """Load a single motion from a directory of CSV files."""
227
+ joint_pos_f = os.path.join(motion_dir, "joint_pos.csv")
228
+ body_pos_f = os.path.join(motion_dir, "body_pos.csv")
229
+ body_quat_f = os.path.join(motion_dir, "body_quat.csv")
230
+
231
+ if not os.path.exists(joint_pos_f):
232
+ return None
233
+
234
+ joint_pos = np.loadtxt(joint_pos_f, delimiter=",", skiprows=1, dtype=np.float32)
235
+ body_pos = np.loadtxt(body_pos_f, delimiter=",", skiprows=1, dtype=np.float32)
236
+ body_quat = np.loadtxt(body_quat_f, delimiter=",", skiprows=1, dtype=np.float32)
237
+
238
+ # Reshape body data: (T, 14*3) → (T, 14, 3), (T, 14*4) → (T, 14, 4)
239
+ T = joint_pos.shape[0]
240
+ body_pos = body_pos.reshape(T, -1, 3)
241
+ body_quat = body_quat.reshape(T, -1, 4)
242
+
243
+ return {
244
+ "joint_pos": joint_pos, # (T, 29) IsaacLab order
245
+ "body_pos_w": body_pos, # (T, 14, 3) world frame
246
+ "body_quat_w": body_quat, # (T, 14, 4) wxyz format
247
+ }
248
+
249
+
250
+ def convert_sequence(seq_data: dict, fps: int, humanoid_fk=None) -> dict: # noqa: ARG001
251
+ """Convert a single deploy-format sequence to motion_lib format.
252
+
253
+ Args:
254
+ seq_data: dict with joint_pos (T, 29), body_pos_w (T, 14, 3),
255
+ body_quat_w (T, 14, 4 wxyz)
256
+ fps: frame rate of the input data
257
+ humanoid_fk: Optional Humanoid_Batch instance (unused, kept for compat)
258
+
259
+ Returns:
260
+ motion_lib entry dict with root_trans_offset, pose_aa, dof, root_rot, fps
261
+ """
262
+ joint_pos = seq_data["joint_pos"] # (T, 29)
263
+ body_pos_w = seq_data["body_pos_w"] # (T, 14, 3)
264
+ body_quat_w = seq_data["body_quat_w"] # (T, 14, 4) wxyz
265
+ joint_order = seq_data.get("joint_order", "il") # "il" or "mj"
266
+
267
+ T = joint_pos.shape[0]
268
+
269
+ # 1. Root position: body_0 (pelvis) position
270
+ root_trans_offset = body_pos_w[:, 0, :].copy() # (T, 3)
271
+
272
+ # 2. Root quaternion: body_0 quaternion, convert wxyz → xyzw (scipy convention)
273
+ root_quat_wxyz = body_quat_w[:, 0, :] # (T, 4) [w, x, y, z]
274
+ root_quat_xyzw = root_quat_wxyz[:, [1, 2, 3, 0]] # (T, 4) [x, y, z, w]
275
+
276
+ # 3. Reorder DOFs to MuJoCo order if needed
277
+ if joint_order == "il":
278
+ # Input is IsaacLab order → reorder to MuJoCo (MJCF actuator order)
279
+ dof_mj = joint_pos[:, MJ_TO_IL] # (T, 29)
280
+ else:
281
+ # Input is already in MuJoCo order (e.g., Bones-SEED CSVs)
282
+ dof_mj = joint_pos # (T, 29)
283
+
284
+ # 4. Convert DOF → pose_aa using hardcoded G1 axis definitions
285
+ dof = dof_mj[:, :NUM_DOF]
286
+
287
+ # pose_aa[body_idx] = dof_axis * dof_value (axis-angle representation)
288
+ # Body 0 = pelvis (root), bodies 1-29 = actuated joints
289
+ pose_aa = np.zeros((T, NUM_BODIES, 3), dtype=np.float32)
290
+ # Actuated joints: body idx = dof idx + 1
291
+ pose_aa[:, 1:NUM_BODIES, :] = DOF_AXIS[None, :, :] * dof[:, :, None]
292
+
293
+ # Set root rotation as axis-angle
294
+ pose_aa[:, 0, :] = transform.Rotation.from_quat(root_quat_xyzw).as_rotvec()
295
+
296
+ return {
297
+ "root_trans_offset": root_trans_offset.astype(np.float32),
298
+ "pose_aa": pose_aa.astype(np.float32),
299
+ "dof": dof.astype(np.float32),
300
+ "root_rot": root_quat_xyzw.astype(np.float32), # xyzw (scipy convention)
301
+ "smpl_joints": np.zeros((T, 24, 3), dtype=np.float32), # placeholder
302
+ "fps": fps,
303
+ }
304
+
305
+
306
+ def downsample_sequence(entry: dict, fps_source: int, fps_target: int) -> dict:
307
+ """Downsample a motion_lib entry using stride-based frame skipping.
308
+
309
+ Matches process_bones_to_motionlib.py: jump = int(fps_source / fps_target).
310
+ Best used when fps_source is an exact multiple of fps_target (e.g. 120→30).
311
+ The resulting PKL is stored at fps_target; fk_batch handles the final
312
+ resampling to target_fps at load time using the canonical interploate_pose formula.
313
+ """
314
+ if fps_source == fps_target:
315
+ return entry
316
+ jump = int(fps_source / fps_target)
317
+ if jump <= 1:
318
+ return entry
319
+ return {
320
+ "root_trans_offset": entry["root_trans_offset"][::jump],
321
+ "pose_aa": entry["pose_aa"][::jump],
322
+ "dof": entry["dof"][::jump],
323
+ "root_rot": entry["root_rot"][::jump],
324
+ "smpl_joints": entry["smpl_joints"][::jump],
325
+ "fps": fps_target,
326
+ }
327
+
328
+
329
+ def init_humanoid_fk():
330
+ """Initialize Humanoid_Batch from the G1 MJCF config.
331
+
332
+ Only needed for non-Bones-SEED inputs (deploy PKL, SOMA CSV dirs).
333
+ Bones-SEED path uses hardcoded DOF_AXIS constants instead.
334
+ """
335
+ import omegaconf
336
+
337
+ motion_cfg = omegaconf.OmegaConf.create(
338
+ {
339
+ "asset": {
340
+ "assetRoot": "gear_sonic/data/assets/robot_description/mjcf/",
341
+ "assetFileName": "g1_29dof_rev_1_0.xml",
342
+ "urdfFileName": "",
343
+ },
344
+ "extend_config": [],
345
+ }
346
+ )
347
+ from gear_sonic.utils.motion_lib import torch_humanoid_batch
348
+
349
+ return torch_humanoid_batch.Humanoid_Batch(motion_cfg)
350
+
351
+
352
+ def process_session_csvs(args_tuple):
353
+ """Process all CSVs in a single session directory. Used by multiprocessing."""
354
+ session_dir, session_name, out_dir, fps, fps_source = args_tuple
355
+ import warnings
356
+
357
+ warnings.filterwarnings("ignore")
358
+
359
+ csv_files = sorted([f for f in os.listdir(session_dir) if f.endswith(".csv")])
360
+
361
+ session_out = os.path.join(out_dir, session_name)
362
+ os.makedirs(session_out, exist_ok=True)
363
+
364
+ converted = 0
365
+ failed = 0
366
+ for csv_f in csv_files:
367
+ name = os.path.splitext(csv_f)[0]
368
+ out_path = os.path.join(session_out, name + ".pkl")
369
+ if os.path.exists(out_path):
370
+ converted += 1 # skip existing
371
+ continue
372
+ try:
373
+ seq = load_bones_csv(os.path.join(session_dir, csv_f))
374
+ fps_for_convert = fps_source if fps_source else fps
375
+ entry = convert_sequence(seq, fps_for_convert)
376
+ if fps_source and fps_source != fps:
377
+ entry = downsample_sequence(entry, fps_source, fps)
378
+ joblib.dump({name: entry}, out_path, compress=True)
379
+ converted += 1
380
+ except Exception: # noqa: BLE001
381
+ failed += 1
382
+ return session_name, converted, failed, len(csv_files)
383
+
384
+
385
+ def main():
386
+ parser = argparse.ArgumentParser(description="Convert SOMA CSV/PKL to motion_lib format")
387
+ parser.add_argument(
388
+ "--input", required=True, help="CSV dir, parent dir of CSV dirs, or deploy PKL"
389
+ )
390
+ parser.add_argument(
391
+ "--output", required=True, help="Output path (PKL file or directory for individual PKLs)"
392
+ )
393
+ parser.add_argument(
394
+ "--fps",
395
+ type=int,
396
+ default=30,
397
+ help="Target output FPS (default: 30, matches process_bones_to_motionlib)",
398
+ )
399
+ parser.add_argument(
400
+ "--fps_source",
401
+ type=int,
402
+ default=None,
403
+ help="Source data FPS. If set and != --fps, data is downsampled. "
404
+ "Bones-SEED CSVs are typically 120fps.",
405
+ )
406
+ parser.add_argument(
407
+ "--individual",
408
+ action="store_true",
409
+ help="Write individual PKLs per motion (preserves session dir structure)",
410
+ )
411
+ parser.add_argument(
412
+ "--num_workers",
413
+ type=int,
414
+ default=8,
415
+ help="Number of parallel workers for --individual mode",
416
+ )
417
+ args = parser.parse_args()
418
+
419
+ print(f"G1 {NUM_DOF} DOFs, {NUM_BODIES} bodies (hardcoded axes)")
420
+
421
+ # Individual PKL mode: skip scanning, go straight to parallel per-session processing
422
+ if args.individual:
423
+ if not os.path.isdir(args.input):
424
+ print("ERROR: --individual requires a directory input")
425
+ sys.exit(1)
426
+
427
+ # Detect: is input a single session dir (contains CSVs) or parent of sessions?
428
+ has_csvs = any(f.endswith(".csv") for f in os.listdir(args.input))
429
+ subdirs = sorted(
430
+ [d for d in os.listdir(args.input) if os.path.isdir(os.path.join(args.input, d))]
431
+ )
432
+ has_session_subdirs = (
433
+ any(
434
+ any(f.endswith(".csv") for f in os.listdir(os.path.join(args.input, d)))
435
+ for d in subdirs[:3]
436
+ )
437
+ if subdirs
438
+ else False
439
+ )
440
+
441
+ session_dirs = []
442
+ if has_session_subdirs:
443
+ for d in subdirs:
444
+ subdir = os.path.join(args.input, d)
445
+ if any(f.endswith(".csv") for f in os.listdir(subdir)):
446
+ session_dirs.append((subdir, d, args.output, args.fps, args.fps_source))
447
+ elif has_csvs:
448
+ session_name = os.path.basename(args.input.rstrip("/"))
449
+ session_dirs.append((args.input, session_name, args.output, args.fps, args.fps_source))
450
+
451
+ print(f"\nBatch converting {len(session_dirs)} sessions with {args.num_workers} workers")
452
+ print(f"Output: {args.output}")
453
+ os.makedirs(args.output, exist_ok=True)
454
+
455
+ import multiprocessing
456
+
457
+ total_converted = 0
458
+ total_failed = 0
459
+ total_csvs = 0
460
+ with multiprocessing.Pool(processes=args.num_workers) as pool:
461
+ for session_name, converted, failed, n_csvs in pool.imap_unordered(
462
+ process_session_csvs, session_dirs
463
+ ):
464
+ total_converted += converted
465
+ total_failed += failed
466
+ total_csvs += n_csvs
467
+ print(
468
+ f" {session_name}: {converted}/{n_csvs} converted"
469
+ + (f" ({failed} failed)" if failed else "")
470
+ )
471
+
472
+ print(
473
+ f"\nDone: {total_converted} motions converted, {total_failed} failed, {total_csvs} total CSVs"
474
+ )
475
+ return
476
+
477
+ # Detect input mode (combined PKL output path)
478
+ sequences = {}
479
+
480
+ if args.input.endswith(".pkl"):
481
+ # Mode 3: Deploy PKL file
482
+ print(f"Loading deploy PKL: {args.input}")
483
+ data = joblib.load(args.input)
484
+ for name, seq in data.items():
485
+ sequences[name] = seq
486
+ print(f" Found {len(sequences)} sequences")
487
+
488
+ elif os.path.isfile(os.path.join(args.input, "joint_pos.csv")):
489
+ # Mode 1: Single CSV directory
490
+ name = os.path.basename(args.input)
491
+ print(f"Loading single CSV motion: {name}")
492
+ seq = load_csv_motion(args.input)
493
+ if seq is None:
494
+ print("ERROR: joint_pos.csv not found")
495
+ sys.exit(1)
496
+ sequences[name] = seq
497
+ print(f" {seq['joint_pos'].shape[0]} frames")
498
+
499
+ elif os.path.isdir(args.input):
500
+ # Check if directory contains flat CSVs (Bones-SEED format)
501
+ csv_files = sorted([f for f in os.listdir(args.input) if f.endswith(".csv")])
502
+ subdirs = sorted(
503
+ [d for d in os.listdir(args.input) if os.path.isdir(os.path.join(args.input, d))]
504
+ )
505
+
506
+ if csv_files and not any(
507
+ os.path.exists(os.path.join(args.input, d, "joint_pos.csv"))
508
+ for d in subdirs[:5] # check first 5 subdirs
509
+ ):
510
+ # Mode 4: Directory of flat Bones-SEED CSVs
511
+ print(f"Scanning directory for Bones-SEED CSVs: {args.input}")
512
+ for csv_f in csv_files:
513
+ csv_path = os.path.join(args.input, csv_f)
514
+ name = os.path.splitext(csv_f)[0]
515
+ try:
516
+ seq = load_bones_csv(csv_path)
517
+ sequences[name] = seq
518
+ except Exception as e: # noqa: BLE001
519
+ print(f" WARNING: Failed to load {csv_f}: {e}")
520
+ print(f" Found {len(sequences)} Bones-SEED CSV motions")
521
+ elif subdirs:
522
+ # Check if subdirs contain flat CSVs (batch of session dirs)
523
+ has_session_csvs = False
524
+ for dname in subdirs[:3]:
525
+ subdir = os.path.join(args.input, dname)
526
+ sub_csvs = [f for f in os.listdir(subdir) if f.endswith(".csv")]
527
+ if sub_csvs and not os.path.exists(os.path.join(subdir, "joint_pos.csv")):
528
+ has_session_csvs = True
529
+ break
530
+
531
+ if has_session_csvs:
532
+ # Mode 5: Parent dir of session dirs containing Bones-SEED CSVs
533
+ print(f"Scanning session directories for Bones-SEED CSVs: {args.input}")
534
+ for dname in sorted(subdirs):
535
+ subdir = os.path.join(args.input, dname)
536
+ sub_csvs = sorted([f for f in os.listdir(subdir) if f.endswith(".csv")])
537
+ for csv_f in sub_csvs:
538
+ csv_path = os.path.join(subdir, csv_f)
539
+ name = os.path.splitext(csv_f)[0]
540
+ try:
541
+ seq = load_bones_csv(csv_path)
542
+ sequences[name] = seq
543
+ except Exception as e: # noqa: BLE001
544
+ print(f" WARNING: Failed to load {dname}/{csv_f}: {e}")
545
+ if sub_csvs:
546
+ print(f" Session {dname}: {len(sub_csvs)} CSVs")
547
+ print(f" Found {len(sequences)} total Bones-SEED CSV motions")
548
+ else:
549
+ # Mode 2: Parent directory with SOMA-style subdirectories
550
+ print(f"Scanning directory: {args.input}")
551
+ for dname in sorted(subdirs):
552
+ subdir = os.path.join(args.input, dname)
553
+ seq = load_csv_motion(subdir)
554
+ if seq is not None:
555
+ sequences[dname] = seq
556
+ print(f" Found {len(sequences)} motion directories with CSVs")
557
+ else:
558
+ print(f"ERROR: {args.input} is not a valid input")
559
+ sys.exit(1)
560
+
561
+ if not sequences:
562
+ print("ERROR: No sequences found")
563
+ sys.exit(1)
564
+
565
+ # Convert each sequence (combined PKL mode)
566
+ motion_lib_dict = {}
567
+ for name, seq_data in sequences.items():
568
+ T = seq_data["joint_pos"].shape[0]
569
+ print(f" Converting {name}: {T} frames @ {args.fps} fps")
570
+ fps_for_convert = args.fps_source if args.fps_source else args.fps
571
+ entry = convert_sequence(seq_data, fps_for_convert)
572
+ if args.fps_source and args.fps_source != args.fps:
573
+ entry = downsample_sequence(entry, args.fps_source, args.fps)
574
+ motion_lib_dict[name] = entry
575
+
576
+ # Save
577
+ os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True)
578
+ print(f"\nSaving motion_lib PKL: {args.output}")
579
+ joblib.dump(motion_lib_dict, args.output, compress=True)
580
+ print(f"Done: {len(motion_lib_dict)} sequences saved")
581
+
582
+
583
+ if __name__ == "__main__":
584
+ main()
GR00T-WholeBodyControl/gear_sonic/data_process/extract_soma_joints_from_bvh.py ADDED
@@ -0,0 +1,394 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # ruff: noqa: T201, BLE001, DOC
3
+ """Extract SOMA skeleton FK joint positions from BVH files.
4
+
5
+ Parses NOVA-skeleton BVH files and computes forward kinematics to extract
6
+ world-space 3D joint positions for a selected 26-joint subset. Outputs
7
+ per-motion PKL files in the same directory structure as the robot PKLs.
8
+
9
+ The 26 selected joints cover the major body landmarks with symmetric arms
10
+ (including Thumb1 + Middle1 per hand for orientation): hips, spine chain,
11
+ shoulders, arms, hands+fingers, legs, feet.
12
+
13
+ Input: BVH files from bones_update_240924/anims_uniform_novaskel_v1/BVH/
14
+ Output: Per-motion PKL files with soma_joints (T, 26, 3) Z-up meters body-local,
15
+ soma_root_quat (T, 4) wxyz Y-up BVH world rotation
16
+
17
+ Usage:
18
+ # Single session
19
+ python scripts/motion/extract_soma_joints_from_bvh.py \
20
+ --input /path/to/novaskel_v1/BVH/210531 \
21
+ --output /path/to/output/bones_soma_joints/210531 \
22
+ --fps 30
23
+
24
+ # All sessions (parent dir)
25
+ python scripts/motion/extract_soma_joints_from_bvh.py \
26
+ --input /path/to/novaskel_v1/BVH \
27
+ --output /path/to/output/bones_soma_joints \
28
+ --fps 30 --num_workers 8
29
+ """
30
+
31
+ import argparse
32
+ import glob
33
+ import multiprocessing
34
+ import os
35
+ import os.path as osp
36
+ import re
37
+ import sys
38
+ import time
39
+
40
+ import joblib
41
+ import numpy as np
42
+ from scipy.spatial import transform
43
+
44
+ # 26-joint subset of the 78-joint NOVA skeleton (Root excluded).
45
+ # Covers major body landmarks, excluding most fingers, face details, end sites.
46
+ # Arms are fully symmetric with two finger joints per hand (Thumb1 + Middle1)
47
+ # to determine hand orientation.
48
+ SOMA_JOINTS = [
49
+ "Hips", # 0 - pelvis
50
+ "Spine1", # 1 - lower spine
51
+ "Spine2", # 2 - mid spine
52
+ "Chest", # 3 - upper spine
53
+ "Neck1", # 4 - neck
54
+ "Head", # 5 - head
55
+ "LeftShoulder", # 6 - left clavicle
56
+ "LeftArm", # 7 - left upper arm
57
+ "LeftForeArm", # 8 - left elbow
58
+ "LeftHand", # 9 - left wrist
59
+ "LeftHandThumb1", # 10 - left thumb (hand orientation)
60
+ "LeftHandMiddle1", # 11 - left middle finger (hand orientation)
61
+ "RightShoulder", # 12 - right clavicle
62
+ "RightArm", # 13 - right upper arm
63
+ "RightForeArm", # 14 - right elbow
64
+ "RightHand", # 15 - right wrist
65
+ "RightHandThumb1", # 16 - right thumb (hand orientation)
66
+ "RightHandMiddle1", # 17 - right middle finger (hand orientation)
67
+ "LeftLeg", # 18 - left hip / upper leg
68
+ "LeftShin", # 19 - left knee
69
+ "LeftFoot", # 20 - left ankle
70
+ "LeftToeBase", # 21 - left toe
71
+ "RightLeg", # 22 - right hip / upper leg
72
+ "RightShin", # 23 - right knee
73
+ "RightFoot", # 24 - right ankle
74
+ "RightToeBase", # 25 - right toe
75
+ ]
76
+
77
+ NUM_SOMA_JOINTS = len(SOMA_JOINTS)
78
+
79
+
80
+ def parse_bvh(filepath):
81
+ """Parse BVH hierarchy and motion data.
82
+
83
+ Returns:
84
+ joints: list of dicts with name, offset, channels, parent_idx
85
+ channel_order: list of (joint_idx, channel_name) tuples
86
+ motion_data: (n_frames, n_channels) numpy array
87
+ n_frames: int
88
+ frame_time: float (seconds per frame)
89
+ """
90
+ with open(filepath) as f:
91
+ lines = f.readlines()
92
+
93
+ joints = []
94
+ joint_stack = []
95
+ channel_order = []
96
+ i = 0
97
+
98
+ while i < len(lines):
99
+ line = lines[i].strip()
100
+ if line == "MOTION":
101
+ i += 1
102
+ break
103
+
104
+ m = re.match(r"(ROOT|JOINT)\s+(\S+)", line)
105
+ if m:
106
+ name = m.group(2)
107
+ parent_idx = joint_stack[-1] if joint_stack else -1
108
+ joints.append({"name": name, "offset": None, "channels": [], "parent_idx": parent_idx})
109
+ joint_stack.append(len(joints) - 1)
110
+ elif line.startswith("OFFSET") and joint_stack:
111
+ vals = [float(x) for x in line.split()[1:]]
112
+ joints[joint_stack[-1]]["offset"] = np.array(vals)
113
+ elif line.startswith("CHANNELS") and joint_stack:
114
+ parts = line.split()
115
+ n_ch = int(parts[1])
116
+ ch_names = parts[2 : 2 + n_ch]
117
+ joints[joint_stack[-1]]["channels"] = ch_names
118
+ for ch in ch_names:
119
+ channel_order.append((joint_stack[-1], ch))
120
+ elif line == "}":
121
+ if joint_stack:
122
+ joint_stack.pop()
123
+ i += 1
124
+
125
+ # Parse MOTION section
126
+ frames_line = lines[i].strip()
127
+ n_frames = int(frames_line.split(":")[1])
128
+ i += 1
129
+ frame_time = float(lines[i].strip().split(":")[1])
130
+ i += 1
131
+
132
+ motion_data = np.empty((n_frames, len(channel_order)))
133
+ for f_idx in range(n_frames):
134
+ vals = lines[i].strip().split()
135
+ motion_data[f_idx] = [float(v) for v in vals]
136
+ i += 1
137
+
138
+ return joints, channel_order, motion_data, n_frames, frame_time
139
+
140
+
141
+ def compute_fk_selected(joints, channel_order, motion_data, selected_names):
142
+ """Compute FK world positions for selected joints only.
143
+
144
+ Uses vectorized rotation computation per joint across all frames.
145
+
146
+ Args:
147
+ joints: parsed joint hierarchy
148
+ channel_order: channel mapping
149
+ motion_data: (n_frames, n_channels)
150
+ selected_names: list of joint names to extract
151
+
152
+ Returns:
153
+ selected_positions: (n_frames, len(selected_names), 3) in BVH units (cm)
154
+ root_quats: (n_frames, 4) root orientation quaternions (xyzw)
155
+ """
156
+ n_frames = motion_data.shape[0]
157
+ n_joints = len(joints)
158
+ joint_names = [j["name"] for j in joints]
159
+
160
+ # Build selected indices
161
+ selected_indices = set()
162
+ for name in selected_names:
163
+ if name in joint_names:
164
+ selected_indices.add(joint_names.index(name))
165
+
166
+ # Also include all ancestors needed for FK
167
+ ancestors = set()
168
+ for idx in selected_indices:
169
+ j = idx
170
+ while j >= 0:
171
+ ancestors.add(j)
172
+ j = joints[j]["parent_idx"]
173
+ compute_joints = sorted(ancestors | selected_indices)
174
+
175
+ # Pre-compute per-joint channel indices
176
+ joint_channels = {j: [] for j in range(n_joints)}
177
+ for ch_idx, (j_idx, ch_name) in enumerate(channel_order):
178
+ joint_channels[j_idx].append((ch_idx, ch_name))
179
+
180
+ # Compute FK for all frames
181
+ world_rots = np.zeros((n_frames, n_joints, 3, 3))
182
+ world_pos = np.zeros((n_frames, n_joints, 3))
183
+
184
+ for j_idx in compute_joints:
185
+ joint = joints[j_idx]
186
+ offset = joint["offset"] if joint["offset"] is not None else np.zeros(3)
187
+
188
+ # Extract position and rotation channels
189
+ pos_channels = {}
190
+ rot_order = ""
191
+ rot_ch_indices = []
192
+ for ch_idx, ch_name in joint_channels[j_idx]:
193
+ if ch_name.endswith("position"):
194
+ pos_channels[ch_name] = ch_idx
195
+ elif ch_name.endswith("rotation"):
196
+ rot_order += ch_name[0].lower()
197
+ rot_ch_indices.append(ch_idx)
198
+
199
+ # Local position (all frames)
200
+ has_pos_channels = bool(pos_channels)
201
+ if has_pos_channels:
202
+ # Joints with position channels: use channels directly (not additive to offset)
203
+ local_pos = np.zeros((n_frames, 3))
204
+ if "Xposition" in pos_channels:
205
+ local_pos[:, 0] = motion_data[:, pos_channels["Xposition"]]
206
+ if "Yposition" in pos_channels:
207
+ local_pos[:, 1] = motion_data[:, pos_channels["Yposition"]]
208
+ if "Zposition" in pos_channels:
209
+ local_pos[:, 2] = motion_data[:, pos_channels["Zposition"]]
210
+ else:
211
+ # Joints with only rotation channels: use static offset
212
+ local_pos = np.tile(offset, (n_frames, 1))
213
+
214
+ # Local rotation (all frames)
215
+ # BVH uses extrinsic rotations: uppercase in scipy convention
216
+ if rot_order:
217
+ rot_vals = motion_data[:, rot_ch_indices] # (n_frames, n_rot_channels)
218
+ local_rot = transform.Rotation.from_euler(
219
+ rot_order.upper(), rot_vals, degrees=True
220
+ ).as_matrix()
221
+ else:
222
+ local_rot = np.tile(np.eye(3), (n_frames, 1, 1))
223
+
224
+ if joint["parent_idx"] < 0:
225
+ # Root joint: no parent transform
226
+ world_rots[:, j_idx] = local_rot
227
+ world_pos[:, j_idx] = local_pos
228
+ else:
229
+ p = joint["parent_idx"]
230
+ parent_rot = world_rots[:, p] # (n_frames, 3, 3)
231
+ parent_pos = world_pos[:, p] # (n_frames, 3)
232
+ # world_pos = parent_pos + parent_rot @ local_pos
233
+ world_pos[:, j_idx] = parent_pos + np.einsum("fij,fj->fi", parent_rot, local_pos)
234
+ # world_rot = parent_rot @ local_rot
235
+ world_rots[:, j_idx] = np.einsum("fij,fjk->fik", parent_rot, local_rot)
236
+
237
+ # Extract selected joints
238
+ sel_indices = [joint_names.index(name) for name in selected_names if name in joint_names]
239
+ selected_positions = world_pos[:, sel_indices, :] # (n_frames, len(selected_names), 3)
240
+
241
+ # Extract root quaternion (Hips joint, index 1)
242
+ hips_idx = joint_names.index("Hips") if "Hips" in joint_names else 0
243
+ root_quats_scipy = transform.Rotation.from_matrix(world_rots[:, hips_idx])
244
+ root_quats = root_quats_scipy.as_quat() # (n_frames, 4) as xyzw
245
+
246
+ return selected_positions, root_quats
247
+
248
+
249
+ def process_single_bvh(args):
250
+ """Process a single BVH file → PKL with soma_joints.
251
+
252
+ Returns (motion_name, success, error_msg)
253
+ """
254
+ bvh_path, output_dir, fps_target, skip_existing = args
255
+ motion_name = osp.splitext(osp.basename(bvh_path))[0]
256
+ output_path = osp.join(output_dir, f"{motion_name}.pkl")
257
+
258
+ if skip_existing and osp.exists(output_path):
259
+ return motion_name, True, "skipped"
260
+
261
+ try:
262
+ joints, channel_order, motion_data, n_frames, frame_time = parse_bvh(bvh_path)
263
+ fps_source = round(1.0 / frame_time)
264
+
265
+ positions, root_quats = compute_fk_selected(joints, channel_order, motion_data, SOMA_JOINTS)
266
+
267
+ # Convert cm → meters
268
+ positions_m = positions / 100.0
269
+
270
+ # Extract hips translation and subtract from all joints to get
271
+ # body-local positions (matching SMPL's compute_human_joints which
272
+ # produces joints without global translation).
273
+ hips_idx = 0 # Hips is joint index 0 in SOMA_JOINTS (Root removed)
274
+ transl = positions_m[:, hips_idx, :].copy() # (T, 3) Y-up
275
+ positions_m = positions_m - transl[:, None, :] # body-local
276
+
277
+ # Convert Y-up → Z-up: (x, y, z) → (x, -z, y)
278
+ # Same as applying rot90x, matching SMPL's convert_smpl_bones which
279
+ # applies rot90x to global_orient before FK to produce Z-up joints.
280
+ positions_zup = positions_m.copy()
281
+ positions_zup[..., 1] = -positions_m[..., 2]
282
+ positions_zup[..., 2] = positions_m[..., 1]
283
+
284
+ # Downsample to target fps using stride-based frame skipping.
285
+ # Matches convert_soma_csv_to_motion_lib.py: jump = int(fps_source / fps_target).
286
+ # For Bones-SEED (120fps BVH → 30fps), this is stride-4 (exact division).
287
+ # Both BVH and CSV have identical source frame counts at 120fps, so
288
+ # stride-based downsampling produces identical frame counts.
289
+ if fps_source != fps_target:
290
+ jump = max(1, int(fps_source / fps_target))
291
+ positions_zup = positions_zup[::jump]
292
+ transl = transl[::jump]
293
+ root_quats = root_quats[::jump]
294
+
295
+ # Convert xyzw → wxyz for compatibility with IsaacLab quat pipeline.
296
+ # Root quats stay Y-up — runtime converts via smpl_root_ytoz_up +
297
+ # remove_bvh_base_rot (same pattern as SMPL pose_aa).
298
+ root_quats = root_quats[:, [3, 0, 1, 2]]
299
+
300
+ # Store as PKL
301
+ entry = {
302
+ motion_name: {
303
+ "soma_joints": positions_zup.astype(
304
+ np.float32
305
+ ), # (T, 26, 3) Z-up meters, body-local
306
+ "soma_root_quat": root_quats.astype(
307
+ np.float32
308
+ ), # (T, 4) wxyz, Y-up BVH world rotation
309
+ "soma_transl": transl.astype(np.float32), # (T, 3) Hips world position, Y-up
310
+ "fps": fps_target,
311
+ "joint_names": SOMA_JOINTS,
312
+ }
313
+ }
314
+
315
+ os.makedirs(output_dir, exist_ok=True)
316
+ joblib.dump(entry, output_path)
317
+ return motion_name, True, None
318
+
319
+ except Exception as e:
320
+ return motion_name, False, str(e)
321
+
322
+
323
+ def main():
324
+ parser = argparse.ArgumentParser(description="Extract SOMA joints from BVH files")
325
+ parser.add_argument("--input", required=True, help="BVH dir (session or parent)")
326
+ parser.add_argument("--output", required=True, help="Output dir for PKL files")
327
+ parser.add_argument(
328
+ "--fps",
329
+ type=int,
330
+ default=30,
331
+ help="Target FPS (default: 30, matches process_bones_to_motionlib)",
332
+ )
333
+ parser.add_argument("--num_workers", type=int, default=4, help="Parallel workers")
334
+ parser.add_argument("--skip_existing", action="store_true", help="Skip existing PKL files")
335
+ args = parser.parse_args()
336
+
337
+ # Discover BVH files
338
+ bvh_files = sorted(glob.glob(osp.join(args.input, "*.bvh")))
339
+
340
+ if bvh_files:
341
+ # Single session directory
342
+ sessions = {osp.basename(args.input): bvh_files}
343
+ else:
344
+ # Parent directory with session subdirs
345
+ session_dirs = sorted([d for d in glob.glob(osp.join(args.input, "*")) if osp.isdir(d)])
346
+ sessions = {}
347
+ for sd in session_dirs:
348
+ files = sorted(glob.glob(osp.join(sd, "*.bvh")))
349
+ if files:
350
+ sessions[osp.basename(sd)] = files
351
+
352
+ if not sessions:
353
+ print(f"No BVH files found in {args.input}")
354
+ sys.exit(1)
355
+
356
+ total_bvh = sum(len(v) for v in sessions.values())
357
+ print(f"Found {total_bvh} BVH files across {len(sessions)} sessions")
358
+ print(f"Output: {args.output}, FPS: {args.fps}, Workers: {args.num_workers}")
359
+ total_converted = 0
360
+ total_failed = 0
361
+ t0 = time.time()
362
+
363
+ for session_name, bvh_list in sessions.items():
364
+ session_output = osp.join(args.output, session_name)
365
+ tasks = [(bvh_path, session_output, args.fps, args.skip_existing) for bvh_path in bvh_list]
366
+
367
+ with multiprocessing.Pool(args.num_workers) as pool:
368
+ results = pool.map(process_single_bvh, tasks)
369
+
370
+ converted = sum(1 for _, s, e in results if s and e != "skipped")
371
+ skipped = sum(1 for _, s, e in results if e == "skipped")
372
+ failed = sum(1 for _, s, _ in results if not s)
373
+
374
+ if failed > 0:
375
+ for name, success, err in results:
376
+ if not success:
377
+ print(f" FAILED: {name}: {err}")
378
+
379
+ total_converted += converted + skipped
380
+ total_failed += failed
381
+
382
+ elapsed = time.time() - t0
383
+ rate = total_converted / elapsed if elapsed > 0 else 0
384
+ print(
385
+ f" {session_name}: {converted} converted, {skipped} skipped, "
386
+ f"{failed} failed [{total_converted}/{total_bvh}, {rate:.0f}/s]"
387
+ )
388
+
389
+ elapsed = time.time() - t0
390
+ print(f"\nDone: {total_converted} converted, {total_failed} failed, " f"{elapsed:.1f}s elapsed")
391
+
392
+
393
+ if __name__ == "__main__":
394
+ main()
GR00T-WholeBodyControl/gear_sonic/data_process/filter_and_copy_bones_data.py ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script to filter and copy bones data from bones_gmr to single_pkls directory.
4
+
5
+ This script copies motion files from the bones_gmr directory structure while
6
+ filtering out unwanted sequences based on keywords in filenames.
7
+ It preserves the bones_xxx directory structure in the destination.
8
+ """
9
+
10
+ import argparse
11
+ from functools import partial
12
+ import glob
13
+ from multiprocessing import Pool, cpu_count
14
+ import os.path as osp
15
+ from pathlib import Path
16
+ import shutil
17
+
18
+ from tqdm import tqdm
19
+
20
+
21
+ def should_filter_out(filename, filter_keywords, include_keywords=None):
22
+ """
23
+ Check if a filename contains any of the filter keywords.
24
+
25
+ Args:
26
+ filename (str): The filename to check
27
+ filter_keywords (list): List of keywords to filter out
28
+
29
+ Returns:
30
+ bool: True if file should be filtered out, False otherwise
31
+ """
32
+ filename_lower = filename.lower()
33
+ if include_keywords is None:
34
+ return any(keyword.lower() in filename_lower for keyword in filter_keywords)
35
+ else:
36
+ return any(keyword.lower() in filename_lower for keyword in filter_keywords) or (
37
+ not any(keyword.lower() in filename_lower for keyword in include_keywords)
38
+ )
39
+
40
+
41
+ def process_bones_directory(
42
+ bones_dir, dest_path, filter_keywords, dry_run, verbose, include_keywords=None
43
+ ):
44
+ """
45
+ Process a single bones directory - worker function for multiprocessing.
46
+
47
+ Args:
48
+ bones_dir (Path): Source bones directory to process
49
+ dest_path (Path): Destination base directory
50
+ filter_keywords (list): Keywords to filter out
51
+ dry_run (bool): If True, don't actually copy files
52
+ verbose (bool): If True, show detailed output
53
+
54
+ Returns:
55
+ tuple: (total_files, copied_files, filtered_files)
56
+ """
57
+ dest_bones_dir = dest_path / bones_dir.name
58
+
59
+ if not dry_run:
60
+ dest_bones_dir.mkdir(parents=True, exist_ok=True)
61
+
62
+ # Find all pkl files in this bones directory
63
+
64
+ pkl_files = list(glob.glob(osp.join(bones_dir, "**", "*.pkl"), recursive=True))
65
+ total_files = len(pkl_files)
66
+ copied_files = 0
67
+ filtered_files = 0
68
+
69
+ if verbose:
70
+ print(f"Processing {bones_dir.name}: {total_files} files")
71
+
72
+ for pkl_file in pkl_files:
73
+ base = osp.basename(pkl_file)
74
+ parent = osp.basename(osp.dirname(pkl_file))
75
+ name_to_check = f"{parent}/{base}"
76
+
77
+ if (
78
+ should_filter_out(name_to_check, filter_keywords, include_keywords)
79
+ and not base == "metadata.pkl"
80
+ ):
81
+ filtered_files += 1
82
+ if verbose:
83
+ print(f" FILTERED: {osp.basename(pkl_file)}")
84
+ else:
85
+ copied_files += 1
86
+ dest_file = osp.join(dest_bones_dir, osp.basename(pkl_file))
87
+
88
+ if not dry_run:
89
+ shutil.copy2(pkl_file, dest_file)
90
+
91
+ return (total_files, copied_files, filtered_files)
92
+
93
+
94
+ def copy_filtered_bones_data(
95
+ source_dir,
96
+ dest_dir,
97
+ filter_keywords,
98
+ dry_run=False,
99
+ verbose=False,
100
+ workers=None,
101
+ filter_file=None,
102
+ ):
103
+ """
104
+ Copy bones data while filtering out unwanted sequences.
105
+
106
+ Args:
107
+ source_dir (Path): Source directory containing bones_xxx subdirs
108
+ dest_dir (Path): Destination directory
109
+ filter_keywords (list): Keywords to filter out
110
+ dry_run (bool): If True, only show what would be copied
111
+ verbose (bool): If True, show detailed output
112
+ workers (int): Number of worker processes. If None, uses all CPU cores.
113
+ """
114
+ source_path = Path(source_dir)
115
+ dest_path = Path(dest_dir)
116
+ include_keywords = None
117
+ if filter_file is not None:
118
+ with open(filter_file) as f:
119
+ include_keywords = f.read().splitlines()
120
+
121
+ if not source_path.exists():
122
+ print(f"Error: Source directory {source_path} does not exist!")
123
+ return False
124
+
125
+ # Find all bones_xxx directories
126
+ bones_dirs = [d for d in source_path.iterdir() if d.is_dir()]
127
+
128
+ if not bones_dirs:
129
+ print(f"No bones_xxx directories found in {source_path}")
130
+ return False
131
+
132
+ # Determine number of workers
133
+ if workers is None:
134
+ workers = cpu_count()
135
+ workers = max(1, min(workers, len(bones_dirs))) # Don't use more workers than directories
136
+
137
+ print(f"Found {len(bones_dirs)} bones directories to process")
138
+ print(f"Using {workers} worker processes")
139
+
140
+ if verbose:
141
+ for d in bones_dirs:
142
+ print(f" {d.name}")
143
+
144
+ # Process directories in parallel
145
+ worker_func = partial(
146
+ process_bones_directory,
147
+ dest_path=dest_path,
148
+ filter_keywords=filter_keywords,
149
+ dry_run=dry_run,
150
+ verbose=verbose,
151
+ include_keywords=include_keywords,
152
+ )
153
+
154
+ total_files = 0
155
+ copied_files = 0
156
+ filtered_files = 0
157
+ if workers == 1:
158
+ # Single-threaded execution for easier debugging
159
+ results = []
160
+ for bones_dir in tqdm(bones_dirs, desc="Processing bones directories"):
161
+ results.append(worker_func(bones_dir))
162
+ else:
163
+ # Multi-process execution
164
+ with Pool(processes=workers) as pool:
165
+ results = list(
166
+ tqdm(
167
+ pool.imap(worker_func, bones_dirs),
168
+ total=len(bones_dirs),
169
+ desc="Processing bones directories",
170
+ )
171
+ )
172
+
173
+ # Aggregate results
174
+ for result in results:
175
+ total_files += result[0]
176
+ copied_files += result[1]
177
+ filtered_files += result[2]
178
+
179
+ # Summary
180
+ print(f"\n{'='*60}")
181
+ print("FILTERING SUMMARY")
182
+ print(f"{'='*60}")
183
+ print(f"Source directory: {source_path}")
184
+ print(f"Destination directory: {dest_path}")
185
+ print(f"Total files found: {total_files}")
186
+ print(f"Files copied: {copied_files}")
187
+ print(f"Files filtered out: {filtered_files}")
188
+ print(f"Filter keywords: {', '.join(filter_keywords)}")
189
+
190
+ if dry_run:
191
+ print("\nDRY RUN - No files were actually copied")
192
+ else:
193
+ print(f"\nFiles successfully copied to: {dest_path}")
194
+
195
+ return True
196
+
197
+
198
+ def main():
199
+ parser = argparse.ArgumentParser(description="Filter and copy bones data")
200
+ parser.add_argument(
201
+ "--source",
202
+ default="data/bones_gmr/0903_all/",
203
+ help="Source directory containing bones_xxx subdirectories",
204
+ )
205
+ parser.add_argument(
206
+ "--dest", default="data/single_pkls/", help="Destination directory for filtered bones data"
207
+ )
208
+ parser.add_argument(
209
+ "--filter-keywords",
210
+ default=[
211
+ "bed",
212
+ "bike",
213
+ "chair",
214
+ "climb",
215
+ "com_up_50cm",
216
+ "sitting",
217
+ "step_on",
218
+ "seat",
219
+ "table",
220
+ "_sit_",
221
+ "sit_", "ladder",
222
+ "crutch",
223
+ "_bed_",
224
+ "_ride_",
225
+ "scooter",
226
+ "stepdown",
227
+ "acrobatics_",
228
+ "box_HSPU",
229
+ "cartwheel",
230
+ "50cm_box_",
231
+ "on_box", "fall_from",
232
+ "handstand_ff_",
233
+ "on_1m",
234
+ "form_box",
235
+ "off_1m",
236
+ "230m",
237
+ "jump_over_obstacle_",
238
+ "lift_crate_come_up_",
239
+ "jump_to_shoulder_roll",
240
+ "kozak_dance",
241
+ "stair",
242
+ "handstand",
243
+ "box_jump",
244
+ "monkey_jump",
245
+ "safety_roll",
246
+ "box_dips",
247
+ "walking_on_edge",
248
+ "push_obstacle",
249
+ ],
250
+ nargs="+",
251
+ help="Keywords to filter out from filenames",
252
+ )
253
+ parser.add_argument(
254
+ "--dry-run", action="store_true", help="Show what would be copied without actually copying"
255
+ )
256
+ parser.add_argument("--verbose", "-v", action="store_true", help="Show detailed output")
257
+ parser.add_argument(
258
+ "--add-keywords", nargs="+", help="Additional keywords to add to the default filter list"
259
+ )
260
+ parser.add_argument(
261
+ "--workers",
262
+ type=int,
263
+ default=None,
264
+ help="Number of worker processes (default: use all CPU cores)",
265
+ )
266
+
267
+ parser.add_argument("--filter_file", default=None, help="Filter file to use")
268
+
269
+ args = parser.parse_args()
270
+
271
+ # Combine default and additional keywords
272
+ filter_keywords = args.filter_keywords
273
+ if args.add_keywords:
274
+ filter_keywords.extend(args.add_keywords)
275
+
276
+ print(f"Filtering out files containing: {', '.join(filter_keywords)}")
277
+
278
+ success = copy_filtered_bones_data(
279
+ source_dir=args.source,
280
+ dest_dir=args.dest,
281
+ filter_keywords=filter_keywords,
282
+ dry_run=args.dry_run,
283
+ verbose=args.verbose,
284
+ workers=args.workers,
285
+ filter_file=args.filter_file,
286
+ )
287
+
288
+ return 0 if success else 1
289
+
290
+
291
+ if __name__ == "__main__":
292
+ import sys
293
+
294
+ sys.exit(main())
GR00T-WholeBodyControl/gear_sonic/data_process/split_pkl_files.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script to break large pickle files into individual motion sequence files.
4
+
5
+ This script reads large pickle files and breaks each motion sequence
6
+ within them into individual pickle files in subdirectories.
7
+ This enables motion_lib_base.py to use directory mode for efficient loading.
8
+ Supports any pkl file format, not just bone motion files.
9
+ """
10
+
11
+ import argparse
12
+ from pathlib import Path
13
+ import shutil
14
+ import sys
15
+ import time
16
+
17
+ import joblib
18
+ from tqdm import tqdm
19
+
20
+
21
+ def create_output_structure(output_dir, clean=False):
22
+ """Create the output directory structure."""
23
+ output_path = Path(output_dir)
24
+
25
+ if clean and output_path.exists():
26
+ print(f"Removing existing output directory: {output_path}")
27
+ shutil.rmtree(output_path)
28
+
29
+ output_path.mkdir(parents=True, exist_ok=True)
30
+ print(f"Created output directory: {output_path}")
31
+ return output_path
32
+
33
+
34
+ def extract_motion_metadata(motion_data):
35
+ """Extract metadata (length, fps) from motion data."""
36
+ metadata = {}
37
+
38
+ # Check common fields that might indicate fps
39
+ fps = None
40
+ if hasattr(motion_data, "get"):
41
+ fps = motion_data.get("fps", motion_data.get("frame_rate", motion_data.get("framerate")))
42
+
43
+ # If no fps found, try to infer from common values or set default
44
+ if fps is None:
45
+ fps = 30.0 # Default fps
46
+
47
+ # Get length - check for common motion data structures
48
+ length = 0
49
+ length = motion_data["root_trans_offset"].shape[0]
50
+
51
+ # If still no length found, try to get it from the data structure
52
+ if length == 0 and hasattr(motion_data, "__len__"):
53
+ length = len(motion_data)
54
+
55
+ return {"length": length, "fps": fps, "duration": length / fps if fps > 0 else 0.0}
56
+
57
+
58
+ def process_motion_file(input_file, output_dir, verbose=False):
59
+ """Process a single large motion file and break it into individual files."""
60
+ input_path = Path(input_file)
61
+
62
+ # Create subdirectory named after the input file
63
+ file_subdir = input_path.stem
64
+ output_path = Path(output_dir) / file_subdir
65
+ output_path.mkdir(parents=True, exist_ok=True)
66
+
67
+ try:
68
+ print(f"Loading {input_path.name}...")
69
+ motion_data = joblib.load(input_path)
70
+ print(f"Loaded {len(motion_data)} motion sequences -> {output_path}")
71
+
72
+ # Collect metadata for all motion sequences
73
+ metadata = {}
74
+
75
+ # Process each motion sequence
76
+ for motion_key, motion_sequence_data in tqdm(
77
+ motion_data.items(), desc=f"Processing {input_path.name}"
78
+ ):
79
+ individual_filepath = output_path / f"{motion_key}.pkl"
80
+ individual_dict = {motion_key: motion_sequence_data}
81
+ joblib.dump(individual_dict, individual_filepath)
82
+
83
+ # Extract metadata for this motion sequence
84
+ motion_metadata = extract_motion_metadata(motion_sequence_data)
85
+ metadata[motion_key] = motion_metadata
86
+
87
+ # Save metadata file
88
+ metadata_filepath = output_path / "metadata.pkl"
89
+ joblib.dump(metadata, metadata_filepath)
90
+
91
+ if verbose:
92
+ print(
93
+ f"Successfully processed {input_path.name} -> {len(motion_data)} individual files + metadata"
94
+ )
95
+ return True
96
+
97
+ except Exception as e:
98
+ print(f"Error processing {input_path.name}: {e}")
99
+ return False
100
+
101
+
102
+ def main():
103
+ parser = argparse.ArgumentParser(description="Break pickle files into individual sequences")
104
+ parser.add_argument("input", help="Input directory containing pkl files or single pkl file")
105
+ parser.add_argument(
106
+ "--output",
107
+ default="data/processed_pkl/",
108
+ help="Output directory for individual motion files",
109
+ )
110
+ parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose output")
111
+ parser.add_argument(
112
+ "--file-pattern",
113
+ default="*.pkl",
114
+ help="Pattern to match input files (only used for directories)",
115
+ )
116
+ parser.add_argument(
117
+ "--clean", action="store_true", help="Remove output directory if it already exists"
118
+ )
119
+
120
+ args = parser.parse_args()
121
+
122
+ input_path = Path(args.input)
123
+ if not input_path.exists():
124
+ print(f"Error: Input path {input_path} does not exist!")
125
+ return 1
126
+
127
+ # Create output directory
128
+ output_path = create_output_structure(args.output, args.clean)
129
+
130
+ # Determine input files
131
+ if input_path.is_file():
132
+ # Single file input
133
+ if not input_path.suffix == ".pkl":
134
+ print(f"Error: Input file must be a .pkl file, got {input_path.suffix}")
135
+ return 1
136
+ input_files = [input_path]
137
+ print(f"Processing single file: {input_path}")
138
+ else:
139
+ # Directory input
140
+ input_files = sorted(input_path.glob(args.file_pattern))
141
+ if not input_files:
142
+ print(f"No files found matching pattern {args.file_pattern} in {input_path}")
143
+ return 1
144
+ print(f"Processing directory: {input_path}")
145
+
146
+ print(f"Found {len(input_files)} files to process:")
147
+ for f in input_files[:5]:
148
+ print(f" {f.name}")
149
+ if len(input_files) > 5:
150
+ print(f" ... and {len(input_files) - 5} more files")
151
+
152
+ # Process all files
153
+ successful = failed = total_individual_files = 0
154
+ start_time = time.time()
155
+
156
+ for input_file in input_files:
157
+ print(f"\n{'='*60}")
158
+ if process_motion_file(input_file, output_path, args.verbose):
159
+ successful += 1
160
+ # Count files created (excluding metadata)
161
+ subdir_path = output_path / input_file.stem
162
+ individual_files = [
163
+ f for f in subdir_path.glob("*.pkl") if not f.name.endswith("metadata.pkl")
164
+ ]
165
+ total_individual_files += len(individual_files)
166
+ print(
167
+ f"Created {len(individual_files)} individual files + metadata from {input_file.name}"
168
+ )
169
+ else:
170
+ failed += 1
171
+
172
+ elapsed = time.time() - start_time
173
+
174
+ # Summary
175
+ print(f"\n{'='*60}")
176
+ print("PROCESSING SUMMARY")
177
+ print(f"{'='*60}")
178
+ print(f"Input: {input_path}")
179
+ print(f"Output directory: {output_path}")
180
+ print(f"Files processed successfully: {successful}")
181
+ print(f"Files failed: {failed}")
182
+ print(f"Total individual motion files created: {total_individual_files}")
183
+ print(f"Processing time: {elapsed:.2f} seconds")
184
+
185
+ if successful > 0:
186
+ print(f"\nSuccess! Individual motion files are available in: {output_path}")
187
+ print(f"To use with motion_lib_base.py: motion_file = '{output_path}'")
188
+
189
+ return 0 if failed == 0 else 1
190
+
191
+
192
+ if __name__ == "__main__":
193
+ sys.exit(main())
GR00T-WholeBodyControl/gear_sonic/envs/__init__.py ADDED
File without changes
GR00T-WholeBodyControl/gear_sonic/isaac_utils/__init__.py ADDED
File without changes
GR00T-WholeBodyControl/gear_sonic/isaac_utils/maths.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Basic math primitives for Isaac Lab: normalization, random floats, copysign, seeding."""
2
+
3
+ import torch
4
+ import numpy as np
5
+ import random
6
+ import os
7
+
8
+
9
+ @torch.jit.script
10
+ def normalize(x, eps: float = 1e-9):
11
+ return x / x.norm(p=2, dim=-1).clamp(min=eps, max=None).unsqueeze(-1)
12
+
13
+
14
+ @torch.jit.script
15
+ def torch_rand_float(lower, upper, shape, device):
16
+ # type: (float, float, Tuple[int, int], str) -> Tensor
17
+ return (upper - lower) * torch.rand(*shape, device=device) + lower
18
+
19
+
20
+ @torch.jit.script
21
+ def copysign(a, b):
22
+ # type: (float, Tensor) -> Tensor
23
+ a = torch.tensor(a, device=b.device, dtype=torch.float).repeat(b.shape[0])
24
+ return torch.abs(a) * torch.sign(b)
25
+
26
+
27
+ def set_seed(seed, torch_deterministic=False):
28
+ """set seed across modules"""
29
+ if seed == -1 and torch_deterministic:
30
+ seed = 42
31
+ elif seed == -1:
32
+ seed = np.random.randint(0, 10000)
33
+ print("Setting seed: {}".format(seed))
34
+
35
+ random.seed(seed)
36
+ np.random.seed(seed)
37
+ torch.manual_seed(seed)
38
+ os.environ["PYTHONHASHSEED"] = str(seed)
39
+ torch.cuda.manual_seed(seed)
40
+ torch.cuda.manual_seed_all(seed)
41
+
42
+ if torch_deterministic:
43
+ # refer to https://docs.nvidia.com/cuda/cublas/index.html#cublasApi_reproducibility
44
+ os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
45
+ torch.backends.cudnn.benchmark = False
46
+ torch.backends.cudnn.deterministic = True
47
+ torch.use_deterministic_algorithms(True)
48
+ else:
49
+ torch.backends.cudnn.benchmark = True
50
+ torch.backends.cudnn.deterministic = False
51
+
52
+ return seed
GR00T-WholeBodyControl/gear_sonic/isaac_utils/rotations.py ADDED
@@ -0,0 +1,787 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """JIT-compiled quaternion and rotation utilities for Isaac environments.
2
+
3
+ Provides quaternion arithmetic (multiply, inverse, conjugate, slerp), conversions
4
+ (axis-angle, rotation matrix, euler), and specialized helpers for SMPL root
5
+ orientation transforms (Y-up to Z-up, base rotation removal).
6
+ """
7
+
8
+ import torch
9
+ from torch import Tensor
10
+ import torch.nn.functional as F
11
+ from gear_sonic.isaac_utils.maths import (
12
+ normalize,
13
+ copysign,
14
+ )
15
+ from gear_sonic.trl.utils.torch_transform import angle_axis_to_quaternion, quaternion_to_angle_axis
16
+ from typing import Tuple
17
+ import numpy as np
18
+ from typing import List, Optional
19
+
20
+
21
+ @torch.jit.script
22
+ def quat_unit(a):
23
+ """Normalize quaternion to unit length."""
24
+ return normalize(a)
25
+
26
+
27
+ @torch.jit.script
28
+ def quat_apply(a: Tensor, b: Tensor, w_last: bool) -> Tensor:
29
+ shape = b.shape
30
+ a = a.reshape(-1, 4)
31
+ b = b.reshape(-1, 3)
32
+ if w_last:
33
+ xyz = a[:, :3]
34
+ w = a[:, 3:]
35
+ else:
36
+ xyz = a[:, 1:]
37
+ w = a[:, :1]
38
+ t = xyz.cross(b, dim=-1) * 2
39
+ return (b + w * t + xyz.cross(t, dim=-1)).view(shape)
40
+
41
+
42
+ def get_yaw_quat_from_quat(quat_angle):
43
+ rpy = get_euler_xyz_in_tensor(quat_angle)
44
+ roll, pitch, yaw = rpy[:, 0], rpy[:, 1], rpy[:, 2]
45
+ roll = torch.zeros_like(roll)
46
+ pitch = torch.zeros_like(pitch)
47
+ return quat_from_euler_xyz(roll, pitch, yaw)
48
+
49
+
50
+ @torch.jit.script
51
+ def yaw_quat(quat: torch.Tensor) -> torch.Tensor:
52
+ """Extract the yaw component of a quaternion.
53
+
54
+ Args:
55
+ quat: The orientation in (w, x, y, z). Shape is (..., 4)
56
+
57
+ Returns:
58
+ A quaternion with only yaw component.
59
+ """
60
+ shape = quat.shape
61
+ quat_yaw = quat.view(-1, 4)
62
+ qw = quat_yaw[:, 0]
63
+ qx = quat_yaw[:, 1]
64
+ qy = quat_yaw[:, 2]
65
+ qz = quat_yaw[:, 3]
66
+ yaw = torch.atan2(2 * (qw * qz + qx * qy), 1 - 2 * (qy * qy + qz * qz))
67
+ quat_yaw = torch.zeros_like(quat_yaw)
68
+ quat_yaw[:, 3] = torch.sin(yaw / 2)
69
+ quat_yaw[:, 0] = torch.cos(yaw / 2)
70
+ quat_yaw = normalize(quat_yaw)
71
+ return quat_yaw.view(shape)
72
+
73
+
74
+ @torch.jit.script
75
+ def wrap_to_pi(angles):
76
+ angles %= 2 * np.pi
77
+ angles -= 2 * np.pi * (angles > np.pi)
78
+ return angles
79
+
80
+
81
+ @torch.jit.script
82
+ def quat_conjugate(a: Tensor, w_last: bool) -> Tensor:
83
+ shape = a.shape
84
+ a = a.reshape(-1, 4)
85
+ if w_last:
86
+ return torch.cat((-a[:, :3], a[:, -1:]), dim=-1).view(shape)
87
+ else:
88
+ return torch.cat((a[:, 0:1], -a[:, 1:]), dim=-1).view(shape)
89
+
90
+
91
+
92
+
93
+ @torch.jit.script
94
+ def quat_rotate(q: Tensor, v: Tensor, w_last: bool) -> Tensor:
95
+ shape = q.shape
96
+ if w_last:
97
+ q_w = q[:, -1]
98
+ q_vec = q[:, :3]
99
+ else:
100
+ q_w = q[:, 0]
101
+ q_vec = q[:, 1:]
102
+ a = v * (2.0 * q_w**2 - 1.0).unsqueeze(-1)
103
+ b = torch.cross(q_vec, v, dim=-1) * q_w.unsqueeze(-1) * 2.0
104
+ c = q_vec * torch.bmm(q_vec.view(shape[0], 1, 3), v.view(shape[0], 3, 1)).squeeze(-1) * 2.0
105
+ return a + b + c
106
+
107
+
108
+ @torch.jit.script
109
+ def quat_rotate_inverse(q: Tensor, v: Tensor, w_last: bool) -> Tensor:
110
+ # Same as quat_rotate but with the cross-product term (b) negated,
111
+ # which is equivalent to rotating by the conjugate quaternion (inverse rotation).
112
+ shape = q.shape
113
+ if w_last:
114
+ q_w = q[:, -1]
115
+ q_vec = q[:, :3]
116
+ else:
117
+ q_w = q[:, 0]
118
+ q_vec = q[:, 1:]
119
+ a = v * (2.0 * q_w**2 - 1.0).unsqueeze(-1)
120
+ b = torch.cross(q_vec, v, dim=-1) * q_w.unsqueeze(-1) * 2.0
121
+ c = q_vec * torch.bmm(q_vec.view(shape[0], 1, 3), v.view(shape[0], 3, 1)).squeeze(-1) * 2.0
122
+ return a - b + c
123
+
124
+
125
+ @torch.jit.script
126
+ def quat_angle_axis(x: Tensor, w_last: bool) -> Tuple[Tensor, Tensor]:
127
+ """
128
+ The (angle, axis) representation of the rotation. The axis is normalized to unit length.
129
+ The angle is guaranteed to be between [0, pi].
130
+ """
131
+ if w_last:
132
+ w = x[..., -1]
133
+ axis = x[..., :3]
134
+ else:
135
+ w = x[..., 0]
136
+ axis = x[..., 1:]
137
+ # cos(theta) = 2*w^2 - 1, derived from w = cos(theta/2) and double-angle formula
138
+ s = 2 * (w**2) - 1
139
+ angle = s.clamp(-1, 1).arccos() # just to be safe
140
+ axis /= axis.norm(p=2, dim=-1, keepdim=True).clamp(min=1e-9)
141
+ return angle, axis
142
+
143
+
144
+ @torch.jit.script
145
+ def quat_from_angle_axis(angle: Tensor, axis: Tensor, w_last: bool) -> Tensor:
146
+ theta = (angle / 2).unsqueeze(-1)
147
+ xyz = normalize(axis) * theta.sin()
148
+ w = theta.cos()
149
+ if w_last:
150
+ return quat_unit(torch.cat([xyz, w], dim=-1))
151
+ else:
152
+ return quat_unit(torch.cat([w, xyz], dim=-1))
153
+
154
+
155
+ @torch.jit.script
156
+ def vec_to_heading(h_vec):
157
+ h_theta = torch.atan2(h_vec[..., 1], h_vec[..., 0])
158
+ return h_theta
159
+
160
+
161
+ @torch.jit.script
162
+ def heading_to_quat(h_theta, w_last: bool):
163
+ axis = torch.zeros(
164
+ h_theta.shape
165
+ + [
166
+ 3,
167
+ ],
168
+ device=h_theta.device,
169
+ )
170
+ axis[..., 2] = 1
171
+ heading_q = quat_from_angle_axis(h_theta, axis, w_last=w_last)
172
+ return heading_q
173
+
174
+
175
+ @torch.jit.script
176
+ def quat_axis(q: Tensor, axis: int, w_last: bool) -> Tensor:
177
+ basis_vec = torch.zeros(q.shape[0], 3, device=q.device)
178
+ basis_vec[:, axis] = 1
179
+ return quat_rotate(q, basis_vec, w_last)
180
+
181
+
182
+ @torch.jit.script
183
+ def normalize_angle(x):
184
+ return torch.atan2(torch.sin(x), torch.cos(x))
185
+
186
+
187
+ @torch.jit.script
188
+ def get_basis_vector(q: Tensor, v: Tensor, w_last: bool) -> Tensor:
189
+ return quat_rotate(q, v, w_last)
190
+
191
+
192
+ @torch.jit.script
193
+ def quat_to_angle_axis(q, w_last: bool):
194
+ # type: (Tensor, bool) -> Tuple[Tensor, Tensor]
195
+ # computes axis-angle representation from quaternion q
196
+ # q must be normalized
197
+ # ZL: could have issues.
198
+ min_theta = 1e-5
199
+ if w_last:
200
+ qx, qy, qz, qw = 0, 1, 2, 3
201
+ else:
202
+ qw, qx, qy, qz = 0, 1, 2, 3
203
+
204
+ sin_theta = torch.sqrt(1 - q[..., qw] * q[..., qw])
205
+ angle = 2 * torch.acos(q[..., qw])
206
+ angle = normalize_angle(angle)
207
+ sin_theta_expand = sin_theta.unsqueeze(-1)
208
+ axis = q[..., qx:qw] / sin_theta_expand
209
+
210
+ mask = torch.abs(sin_theta) > min_theta
211
+ default_axis = torch.zeros_like(axis)
212
+ default_axis[..., -1] = 1
213
+
214
+ angle = torch.where(mask, angle, torch.zeros_like(angle))
215
+ mask_expand = mask.unsqueeze(-1)
216
+ axis = torch.where(mask_expand, axis, default_axis)
217
+ return angle, axis
218
+
219
+
220
+ @torch.jit.script
221
+ def slerp(q0, q1, t):
222
+ # type: (Tensor, Tensor, Tensor) -> Tensor
223
+ cos_half_theta = torch.sum(q0 * q1, dim=-1)
224
+
225
+ neg_mask = cos_half_theta < 0
226
+ q1 = q1.clone()
227
+
228
+ # Replace: q1[neg_mask] = -q1[neg_mask]
229
+ # With: torch.where for safer broadcasting
230
+ neg_mask_expanded = neg_mask.unsqueeze(-1).expand_as(q1)
231
+ q1 = torch.where(neg_mask_expanded, -q1, q1)
232
+
233
+ cos_half_theta = torch.abs(cos_half_theta)
234
+ cos_half_theta = torch.unsqueeze(cos_half_theta, dim=-1)
235
+
236
+ half_theta = torch.acos(cos_half_theta)
237
+ sin_half_theta = torch.sqrt(1.0 - cos_half_theta * cos_half_theta)
238
+
239
+ ratioA = torch.sin((1 - t) * half_theta) / sin_half_theta
240
+ ratioB = torch.sin(t * half_theta) / sin_half_theta
241
+
242
+ new_q = ratioA * q0 + ratioB * q1
243
+
244
+ new_q = torch.where(torch.abs(sin_half_theta) < 0.001, 0.5 * q0 + 0.5 * q1, new_q)
245
+ new_q = torch.where(torch.abs(cos_half_theta) >= 1, q0, new_q)
246
+
247
+ return new_q
248
+
249
+
250
+ @torch.jit.script
251
+ def angle_axis_to_exp_map(angle, axis):
252
+ # type: (Tensor, Tensor) -> Tensor
253
+ # compute exponential map from axis-angle
254
+ angle_expand = angle.unsqueeze(-1)
255
+ exp_map = angle_expand * axis
256
+ return exp_map
257
+
258
+
259
+ @torch.jit.script
260
+ def my_quat_rotate(q, v, w_last=True):
261
+ # type: (Tensor, Tensor, bool) -> Tensor
262
+ shape = q.shape
263
+ if w_last:
264
+ q_w = q[:, -1]
265
+ q_vec = q[:, :3]
266
+ else:
267
+ q_w = q[:, 0]
268
+ q_vec = q[:, 1:]
269
+ a = v * (2.0 * q_w**2 - 1.0).unsqueeze(-1)
270
+ b = torch.cross(q_vec, v, dim=-1) * q_w.unsqueeze(-1) * 2.0
271
+ c = q_vec * torch.bmm(q_vec.view(shape[0], 1, 3), v.view(shape[0], 3, 1)).squeeze(-1) * 2.0
272
+ return a + b + c
273
+
274
+
275
+ @torch.jit.script
276
+ def quat_to_tan_norm(q, w_last):
277
+ # type: (Tensor, bool) -> Tensor
278
+ # represents a rotation using the tangent and normal vectors
279
+ ref_tan = torch.zeros_like(q[..., 0:3])
280
+ ref_tan[..., 0] = 1
281
+ if w_last:
282
+ tan = my_quat_rotate(q, ref_tan)
283
+ else:
284
+ raise NotImplementedError
285
+
286
+ ref_norm = torch.zeros_like(q[..., 0:3])
287
+ ref_norm[..., -1] = 1
288
+ if w_last:
289
+ norm = my_quat_rotate(q, ref_norm)
290
+ else:
291
+ raise NotImplementedError
292
+
293
+ norm_tan = torch.cat([tan, norm], dim=len(tan.shape) - 1)
294
+ return norm_tan
295
+
296
+
297
+ @torch.jit.script
298
+ def calc_heading(q, w_last=True):
299
+ # type: (Tensor, bool) -> Tensor
300
+ # calculate heading direction from quaternion
301
+ # the heading is the direction on the xy plane
302
+ # q must be normalized
303
+ # this is the x axis heading
304
+ ref_dir = torch.zeros_like(q[..., 0:3])
305
+ ref_dir[..., 0] = 1
306
+ rot_dir = my_quat_rotate(q, ref_dir, w_last)
307
+
308
+ heading = torch.atan2(rot_dir[..., 1], rot_dir[..., 0])
309
+ return heading
310
+
311
+
312
+ @torch.jit.script
313
+ def quat_to_exp_map(q, w_last):
314
+ # type: (Tensor, bool) -> Tensor
315
+ # compute exponential map from quaternion
316
+ # q must be normalized
317
+ angle, axis = quat_to_angle_axis(q, w_last)
318
+ exp_map = angle_axis_to_exp_map(angle, axis)
319
+ return exp_map
320
+
321
+
322
+ @torch.jit.script
323
+ def calc_heading_quat(q, w_last):
324
+ # type: (Tensor, bool) -> Tensor
325
+ # calculate heading rotation from quaternion
326
+ # the heading is the direction on the xy plane
327
+ # q must be normalized
328
+ heading = calc_heading(q, w_last)
329
+ axis = torch.zeros_like(q[..., 0:3])
330
+ axis[..., 2] = 1
331
+
332
+ heading_q = quat_from_angle_axis(heading, axis, w_last=w_last)
333
+ return heading_q
334
+
335
+
336
+ @torch.jit.script
337
+ def calc_heading_quat_inv(q, w_last):
338
+ # type: (Tensor, bool) -> Tensor
339
+ # calculate heading rotation from quaternion
340
+ # the heading is the direction on the xy plane
341
+ # q must be normalized
342
+ heading = calc_heading(q, w_last)
343
+ axis = torch.zeros_like(q[..., 0:3])
344
+ axis[..., 2] = 1
345
+
346
+ heading_q = quat_from_angle_axis(-heading, axis, w_last=w_last)
347
+ return heading_q
348
+
349
+
350
+ @torch.jit.script
351
+ def quat_inverse(x, w_last):
352
+ # type: (Tensor, bool) -> Tensor
353
+ """
354
+ The inverse of the rotation
355
+ """
356
+ return quat_conjugate(x, w_last=w_last)
357
+
358
+
359
+ @torch.jit.script
360
+ def get_euler_xyz(q: Tensor, w_last: bool) -> Tuple[Tensor, Tensor, Tensor]:
361
+ if w_last:
362
+ qx, qy, qz, qw = 0, 1, 2, 3
363
+ else:
364
+ qw, qx, qy, qz = 0, 1, 2, 3
365
+ # roll (x-axis rotation)
366
+ sinr_cosp = 2.0 * (q[:, qw] * q[:, qx] + q[:, qy] * q[:, qz])
367
+ cosr_cosp = (
368
+ q[:, qw] * q[:, qw] - q[:, qx] * q[:, qx] - q[:, qy] * q[:, qy] + q[:, qz] * q[:, qz]
369
+ )
370
+ roll = torch.atan2(sinr_cosp, cosr_cosp)
371
+
372
+ # pitch (y-axis rotation)
373
+ sinp = 2.0 * (q[:, qw] * q[:, qy] - q[:, qz] * q[:, qx])
374
+ pitch = torch.where(torch.abs(sinp) >= 1, copysign(np.pi / 2.0, sinp), torch.asin(sinp))
375
+
376
+ # yaw (z-axis rotation)
377
+ siny_cosp = 2.0 * (q[:, qw] * q[:, qz] + q[:, qx] * q[:, qy])
378
+ cosy_cosp = (
379
+ q[:, qw] * q[:, qw] + q[:, qx] * q[:, qx] - q[:, qy] * q[:, qy] - q[:, qz] * q[:, qz]
380
+ )
381
+ yaw = torch.atan2(siny_cosp, cosy_cosp)
382
+
383
+ return roll % (2 * np.pi), pitch % (2 * np.pi), yaw % (2 * np.pi)
384
+
385
+
386
+ # @torch.jit.script
387
+ def get_euler_xyz_in_tensor(q):
388
+ qx, qy, qz, qw = 0, 1, 2, 3
389
+ # roll (x-axis rotation)
390
+ sinr_cosp = 2.0 * (q[:, qw] * q[:, qx] + q[:, qy] * q[:, qz])
391
+ cosr_cosp = (
392
+ q[:, qw] * q[:, qw] - q[:, qx] * q[:, qx] - q[:, qy] * q[:, qy] + q[:, qz] * q[:, qz]
393
+ )
394
+ roll = torch.atan2(sinr_cosp, cosr_cosp)
395
+
396
+ # pitch (y-axis rotation)
397
+ sinp = 2.0 * (q[:, qw] * q[:, qy] - q[:, qz] * q[:, qx])
398
+ pitch = torch.where(torch.abs(sinp) >= 1, copysign(np.pi / 2.0, sinp), torch.asin(sinp))
399
+
400
+ # yaw (z-axis rotation)
401
+ siny_cosp = 2.0 * (q[:, qw] * q[:, qz] + q[:, qx] * q[:, qy])
402
+ cosy_cosp = (
403
+ q[:, qw] * q[:, qw] + q[:, qx] * q[:, qx] - q[:, qy] * q[:, qy] - q[:, qz] * q[:, qz]
404
+ )
405
+ yaw = torch.atan2(siny_cosp, cosy_cosp)
406
+
407
+ return torch.stack((roll, pitch, yaw), dim=-1)
408
+
409
+
410
+ @torch.jit.script
411
+ def quat_pos(x):
412
+ """
413
+ make all the real part of the quaternion positive
414
+ """
415
+ q = x
416
+ z = (q[..., 3:] < 0).float()
417
+ q = (1 - 2 * z) * q
418
+ return q
419
+
420
+
421
+ @torch.jit.script
422
+ def is_valid_quat(q):
423
+ x, y, z, w = q[..., 0], q[..., 1], q[..., 2], q[..., 3]
424
+ return (w * w + x * x + y * y + z * z).allclose(torch.ones_like(w))
425
+
426
+
427
+ @torch.jit.script
428
+ def quat_normalize(q):
429
+ """
430
+ Construct 3D rotation from quaternion (the quaternion needs not to be normalized).
431
+ """
432
+ q = quat_unit(quat_pos(q)) # normalized to positive and unit quaternion
433
+ return q
434
+
435
+
436
+ @torch.jit.script
437
+ def quat_mul(a, b, w_last: bool):
438
+ assert a.shape == b.shape
439
+ shape = a.shape
440
+ a = a.reshape(-1, 4)
441
+ b = b.reshape(-1, 4)
442
+
443
+ if w_last:
444
+ x1, y1, z1, w1 = a[..., 0], a[..., 1], a[..., 2], a[..., 3]
445
+ x2, y2, z2, w2 = b[..., 0], b[..., 1], b[..., 2], b[..., 3]
446
+ else:
447
+ w1, x1, y1, z1 = a[..., 0], a[..., 1], a[..., 2], a[..., 3]
448
+ w2, x2, y2, z2 = b[..., 0], b[..., 1], b[..., 2], b[..., 3]
449
+ ww = (z1 + x1) * (x2 + y2)
450
+ yy = (w1 - y1) * (w2 + z2)
451
+ zz = (w1 + y1) * (w2 - z2)
452
+ xx = ww + yy + zz
453
+ qq = 0.5 * (xx + (z1 - x1) * (x2 - y2))
454
+ w = qq - ww + (z1 - y1) * (y2 - z2)
455
+ x = qq - xx + (x1 + w1) * (x2 + w2)
456
+ y = qq - yy + (w1 - x1) * (y2 + z2)
457
+ z = qq - zz + (z1 + y1) * (w2 - x2)
458
+
459
+ if w_last:
460
+ quat = torch.stack([x, y, z, w], dim=-1).view(shape)
461
+ else:
462
+ quat = torch.stack([w, x, y, z], dim=-1).view(shape)
463
+
464
+ return quat
465
+
466
+
467
+
468
+ @torch.jit.script
469
+ def quat_mul_norm(x, y, w_last):
470
+ # type: (Tensor, Tensor, bool) -> Tensor
471
+ """
472
+ Combine two set of 3D rotations together using \**\* operator. The shape needs to be
473
+ broadcastable
474
+ """
475
+ return quat_unit(quat_mul(x, y, w_last))
476
+
477
+
478
+ @torch.jit.script
479
+ def quat_identity(shape: List[int]):
480
+ """
481
+ Construct 3D identity rotation given shape
482
+ """
483
+ w = torch.ones(shape + [1])
484
+ xyz = torch.zeros(shape + [3])
485
+ q = torch.cat([xyz, w], dim=-1)
486
+ return quat_normalize(q)
487
+
488
+
489
+ @torch.jit.script
490
+ def quat_identity_like(x):
491
+ """
492
+ Construct identity 3D rotation with the same shape
493
+ """
494
+ return quat_identity(list(x.shape[:-1]))
495
+
496
+
497
+ @torch.jit.script
498
+ def transform_from_rotation_translation(
499
+ r: Optional[torch.Tensor] = None, t: Optional[torch.Tensor] = None
500
+ ):
501
+ """
502
+ Construct a transform from a quaternion and 3D translation. Only one of them can be None.
503
+ """
504
+ assert r is not None or t is not None, "rotation and translation can't be all None"
505
+ if r is None:
506
+ assert t is not None
507
+ r = quat_identity(list(t.shape))
508
+ if t is None:
509
+ t = torch.zeros(list(r.shape) + [3])
510
+ return torch.cat([r, t], dim=-1)
511
+
512
+
513
+ @torch.jit.script
514
+ def transform_rotation(x):
515
+ """Get rotation from transform"""
516
+ return x[..., :4]
517
+
518
+
519
+ @torch.jit.script
520
+ def transform_translation(x):
521
+ """Get translation from transform"""
522
+ return x[..., 4:]
523
+
524
+
525
+ @torch.jit.script
526
+ def transform_mul(x, y):
527
+ """
528
+ Combine two transformation together
529
+ """
530
+ z = transform_from_rotation_translation(
531
+ r=quat_mul_norm(transform_rotation(x), transform_rotation(y), w_last=True),
532
+ t=quat_rotate(transform_rotation(x), transform_translation(y), w_last=True)
533
+ + transform_translation(x),
534
+ )
535
+ return z
536
+
537
+
538
+ ##################################### FROM PHC rotation_conversions.py #####################################
539
+ @torch.jit.script
540
+ def quaternion_to_matrix(quaternions: torch.Tensor) -> torch.Tensor:
541
+ """
542
+ Convert rotations given as quaternions to rotation matrices.
543
+
544
+ Args:
545
+ quaternions: quaternions with real part first,
546
+ as tensor of shape (..., 4).
547
+
548
+ Returns:
549
+ Rotation matrices as tensor of shape (..., 3, 3).
550
+ """
551
+ r, i, j, k = torch.unbind(quaternions, -1)
552
+ two_s = 2.0 / (quaternions * quaternions).sum(-1)
553
+
554
+ o = torch.stack(
555
+ (
556
+ 1 - two_s * (j * j + k * k),
557
+ two_s * (i * j - k * r),
558
+ two_s * (i * k + j * r),
559
+ two_s * (i * j + k * r),
560
+ 1 - two_s * (i * i + k * k),
561
+ two_s * (j * k - i * r),
562
+ two_s * (i * k - j * r),
563
+ two_s * (j * k + i * r),
564
+ 1 - two_s * (i * i + j * j),
565
+ ),
566
+ -1,
567
+ )
568
+ return o.reshape(quaternions.shape[:-1] + (3, 3))
569
+
570
+
571
+ @torch.jit.script
572
+ def axis_angle_to_quaternion(axis_angle: torch.Tensor) -> torch.Tensor:
573
+ """
574
+ Convert rotations given as axis/angle to quaternions.
575
+
576
+ Args:
577
+ axis_angle: Rotations given as a vector in axis angle form,
578
+ as a tensor of shape (..., 3), where the magnitude is
579
+ the angle turned anticlockwise in radians around the
580
+ vector's direction.
581
+
582
+ Returns:
583
+ quaternions with real part first, as tensor of shape (..., 4).
584
+ """
585
+ angles = torch.norm(axis_angle, p=2, dim=-1, keepdim=True)
586
+ half_angles = angles * 0.5
587
+ eps = 1e-6
588
+ small_angles = angles.abs() < eps
589
+ sin_half_angles_over_angles = torch.empty_like(angles)
590
+ sin_half_angles_over_angles[~small_angles] = (
591
+ torch.sin(half_angles[~small_angles]) / angles[~small_angles]
592
+ )
593
+ # for x small, sin(x/2) is about x/2 - (x/2)^3/6
594
+ # so sin(x/2)/x is about 1/2 - (x*x)/48
595
+ sin_half_angles_over_angles[small_angles] = (
596
+ 0.5 - (angles[small_angles] * angles[small_angles]) / 48
597
+ )
598
+ quaternions = torch.cat(
599
+ [torch.cos(half_angles), axis_angle * sin_half_angles_over_angles], dim=-1
600
+ )
601
+ return quaternions
602
+
603
+
604
+ # @torch.jit.script
605
+ def wxyz_to_xyzw(quat):
606
+ return quat[..., [1, 2, 3, 0]]
607
+
608
+
609
+ # @torch.jit.script
610
+ def xyzw_to_wxyz(quat):
611
+ return quat[..., [3, 0, 1, 2]]
612
+
613
+
614
+ def matrix_to_quaternion(matrix: torch.Tensor) -> torch.Tensor:
615
+ """
616
+ w x y z
617
+ Convert rotations given as rotation matrices to quaternions.
618
+
619
+ Args:
620
+ matrix: Rotation matrices as tensor of shape (..., 3, 3).
621
+
622
+ Returns:
623
+ quaternions with real part first, as tensor of shape (..., 4).
624
+ """
625
+ if matrix.size(-1) != 3 or matrix.size(-2) != 3:
626
+ raise ValueError(f"Invalid rotation matrix shape {matrix.shape}.")
627
+
628
+ batch_dim = matrix.shape[:-2]
629
+ m00, m01, m02, m10, m11, m12, m20, m21, m22 = torch.unbind(
630
+ matrix.reshape(batch_dim + (9,)), dim=-1
631
+ )
632
+
633
+ q_abs = _sqrt_positive_part(
634
+ torch.stack(
635
+ [
636
+ 1.0 + m00 + m11 + m22,
637
+ 1.0 + m00 - m11 - m22,
638
+ 1.0 - m00 + m11 - m22,
639
+ 1.0 - m00 - m11 + m22,
640
+ ],
641
+ dim=-1,
642
+ )
643
+ )
644
+
645
+ # we produce the desired quaternion multiplied by each of r, i, j, k
646
+ quat_by_rijk = torch.stack(
647
+ [
648
+ torch.stack([q_abs[..., 0] ** 2, m21 - m12, m02 - m20, m10 - m01], dim=-1),
649
+ torch.stack([m21 - m12, q_abs[..., 1] ** 2, m10 + m01, m02 + m20], dim=-1),
650
+ torch.stack([m02 - m20, m10 + m01, q_abs[..., 2] ** 2, m12 + m21], dim=-1),
651
+ torch.stack([m10 - m01, m20 + m02, m21 + m12, q_abs[..., 3] ** 2], dim=-1),
652
+ ],
653
+ dim=-2,
654
+ )
655
+
656
+ # We floor here at 0.1 but the exact level is not important; if q_abs is small,
657
+ # the candidate won't be picked.
658
+ flr = torch.tensor(0.1).to(dtype=q_abs.dtype, device=q_abs.device)
659
+ quat_candidates = quat_by_rijk / (2.0 * q_abs[..., None].max(flr))
660
+
661
+ # if not for numerical problems, quat_candidates[i] should be same (up to a sign),
662
+ # forall i; we pick the best-conditioned one (with the largest denominator)
663
+
664
+ return quat_candidates[
665
+ F.one_hot(q_abs.argmax(dim=-1), num_classes=4) > 0.5, : # pyre-ignore[16]
666
+ ].reshape(batch_dim + (4,))
667
+
668
+
669
+ def _sqrt_positive_part(x: torch.Tensor) -> torch.Tensor:
670
+ """
671
+ Returns torch.sqrt(torch.max(0, x))
672
+ but with a zero subgradient where x is 0.
673
+ """
674
+ ret = torch.zeros_like(x)
675
+ positive_mask = x > 0
676
+ ret[positive_mask] = torch.sqrt(x[positive_mask])
677
+ return ret
678
+
679
+
680
+ def quat_w_first(rot):
681
+ rot = torch.cat([rot[..., [-1]], rot[..., :-1]], -1)
682
+ return rot
683
+
684
+
685
+ @torch.jit.script
686
+ def quat_from_euler_xyz(roll, pitch, yaw):
687
+ cy = torch.cos(yaw * 0.5)
688
+ sy = torch.sin(yaw * 0.5)
689
+ cr = torch.cos(roll * 0.5)
690
+ sr = torch.sin(roll * 0.5)
691
+ cp = torch.cos(pitch * 0.5)
692
+ sp = torch.sin(pitch * 0.5)
693
+
694
+ qw = cy * cr * cp + sy * sr * sp
695
+ qx = cy * sr * cp - sy * cr * sp
696
+ qy = cy * cr * sp + sy * sr * cp
697
+ qz = sy * cr * cp - cy * sr * sp
698
+
699
+ return torch.stack([qx, qy, qz, qw], dim=-1)
700
+
701
+
702
+
703
+ @torch.jit.script
704
+ def remove_smpl_base_rot(quat, w_last: bool):
705
+ # [0.5,0.5,0.5,0.5] is a 120° rotation about the [1,1,1] axis — SMPL's default rest orientation.
706
+ # Conjugating it out aligns with a neutral standing pose.
707
+ base_rot = quat_conjugate(torch.tensor([[0.5, 0.5, 0.5, 0.5]]).to(quat), w_last=w_last) # SMPL
708
+ return quat_mul(quat, base_rot.repeat(quat.shape[0], 1), w_last=w_last)
709
+
710
+
711
+ @torch.jit.script
712
+ def smpl_root_ytoz_up(root_quat_y_up) -> torch.Tensor:
713
+ """Convert SMPL root quaternion from Y-up to Z-up coordinate system"""
714
+ # 90° rotation about X-axis maps Y-up (SMPL convention) to Z-up (robot convention)
715
+ base_rot = angle_axis_to_quaternion(torch.tensor([[np.pi / 2, 0.0, 0.0]]).to(root_quat_y_up))
716
+ root_quat_z_up = quat_mul(
717
+ base_rot.repeat(root_quat_y_up.shape[0], 1), root_quat_y_up, w_last=False
718
+ )
719
+ return root_quat_z_up
720
+
721
+
722
+ @torch.jit.script
723
+ def rotate_vectors_by_quaternion(quat: torch.Tensor, vec: torch.Tensor) -> torch.Tensor:
724
+ """
725
+ Rotate `vec` by `quat`, elementwise.
726
+
727
+ Args:
728
+ quat (torch.Tensor): Tensor of shape (..., 4), quaternions in [x, y, z, w] format.
729
+ vec (torch.Tensor): Tensor of shape (..., 3), vectors to rotate.
730
+
731
+ Returns:
732
+ torch.Tensor: Rotated vectors, same shape as `vec`.
733
+ """
734
+ q_xyz = quat[..., :3] # (..., 3)
735
+ q_w = quat[..., 3:].unsqueeze(-1) # (..., 1, 1) -> we'll squeeze to (...,1)
736
+
737
+ # Compute intermediate cross products
738
+ # t = 2 * q_xyz × v
739
+ t = 2.0 * torch.cross(q_xyz, vec, dim=-1) # (..., 3)
740
+
741
+ # v' = v + w * t + q_xyz × t
742
+ rotated = vec + q_w.squeeze(-1) * t + torch.cross(q_xyz, t, dim=-1)
743
+ return rotated
744
+
745
+
746
+ def rot6d_to_quat_first_two_cols(rot_6d: torch.Tensor) -> torch.Tensor:
747
+ """
748
+ Convert 6D rotation representation (first 2 columns of rotation matrix) to quaternion.
749
+
750
+ This function handles the 6D representation where the first 6 elements represent
751
+ the first 2 columns of a 3x3 rotation matrix (flattened). The third column is
752
+ reconstructed via cross product of the first two columns.
753
+
754
+ Args:
755
+ rot_6d (torch.Tensor): Tensor of shape (..., 6) representing the first 2 columns
756
+ of rotation matrix flattened.
757
+
758
+ Returns:
759
+ torch.Tensor: Quaternion in (w, x, y, z) format, shape (..., 4).
760
+ """
761
+ # Reshape to get first 2 columns: (..., 3, 2)
762
+ rot_2cols = rot_6d.reshape(*rot_6d.shape[:-1], 3, 2)
763
+
764
+ # Extract the two column vectors
765
+ col_0 = rot_2cols[..., :, 0]
766
+ col_1 = rot_2cols[..., :, 1]
767
+
768
+ # Normalize the columns to ensure they are unit vectors
769
+ col_0 = F.normalize(col_0, dim=-1)
770
+ col_1 = F.normalize(col_1, dim=-1)
771
+
772
+ # Reconstruct the third column via cross product
773
+ col_2 = torch.cross(col_0, col_1, dim=-1)
774
+
775
+ # Stack to form full rotation matrix (..., 3, 3)
776
+ rot_matrix = torch.stack([col_0, col_1, col_2], dim=-1)
777
+
778
+ # Convert rotation matrix to quaternion (w, x, y, z format)
779
+ quat = matrix_to_quaternion(rot_matrix)
780
+
781
+ return quat
782
+
783
+
784
+ def remove_bvh_base_rot(quat, w_last: bool):
785
+ """Remove BVH base rotation. BVH base = conj(SMPL base), so conj(BVH base) = SMPL base."""
786
+ base_rot = torch.tensor([[0.5, 0.5, 0.5, 0.5]]).to(quat)
787
+ return quat_mul(quat, base_rot.repeat(quat.shape[0], 1), w_last=w_last)
GR00T-WholeBodyControl/gear_sonic/utils/__init__.py ADDED
File without changes
GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (159 Bytes). View file
 
GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (175 Bytes). View file
 
GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (163 Bytes). View file
 
GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/average_meters.cpython-311.pyc ADDED
Binary file (9.29 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/average_meters.cpython-312.pyc ADDED
Binary file (8.54 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/batch_normalizer.cpython-311.pyc ADDED
Binary file (3.84 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/batch_normalizer.cpython-312.pyc ADDED
Binary file (3.56 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/common.cpython-311.pyc ADDED
Binary file (7.86 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/common.cpython-312.pyc ADDED
Binary file (7.45 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/config_utils.cpython-311.pyc ADDED
Binary file (2.81 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/config_utils.cpython-312.pyc ADDED
Binary file (2.59 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/logging.cpython-311.pyc ADDED
Binary file (3.51 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/logging.cpython-312.pyc ADDED
Binary file (3.3 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/obs_utils.cpython-311.pyc ADDED
Binary file (4.57 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/obs_utils.cpython-312.pyc ADDED
Binary file (4.09 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/running_mean_std.cpython-311.pyc ADDED
Binary file (18.3 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/utils/__pycache__/running_mean_std.cpython-312.pyc ADDED
Binary file (16.9 kB). View file