Files
blackbird/pybot/pybot/recorder.py
T
qing 286e632288 fix(pybot): 录制 spk.F 列恒 0——与模型 FIXED_OUTPUT 对齐
F2 方案下推理端 spk.F 固定输出 0(bins.FIXED_OUTPUT),而脚本真值
可达 1000Hz,保留真值产生"模型 0 vs 数据 1000"的评估伪影(14.5K)。
录制时写常量 0 消除伪影;spk.delay 微步录制下恒 10.0 已与模型一致。
data_v3(296MB 可复现录制产物)加入 .gitignore。
2026-08-15 19:48:09 +08:00

128 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Episode 录制器 —— 运行 .mt 脚本时同步产出 actpred 格式的自监督数据集
产出格式(与 actpred/dataset.py 的 DataRecorder 兼容):
output_dir/
episode_XXXX/
images/
0000.jpg, 0001.jpg, ...
actions.npy # shape (T, action_dim) float32
imu.npy # 可选,shape (T, imu_dim) float32(MPU6050 姿态回流,R2)
numpy / Pillow 为可选依赖(lazy import),仅 --record 时需要,
不影响 pybot 的零依赖运行时承诺。
"""
import logging
from pathlib import Path
log = logging.getLogger("pybot.recorder")
# spk.F 列(动作契约列 10)与模型固定输出对齐:F2 方案下推理端该列恒为
# 常量 0(actpred/bins.py FIXED_OUTPUT),而脚本真实值可达 1000Hz——
# 保留真值会产生"模型 0 vs 数据 1000"的评估伪影。v3 起录制时写常量 0。
# spk.delay(列 11)无需处理:微步录制下恒为 10.0,与 FIXED_OUTPUT 一致。
SPK_F_COL = 10
class EpisodeRecorder:
"""录制 actpred 格式的 episode(images/*.jpg + actions.npy [+ imu.npy])
用法:
rec = EpisodeRecorder("./actpred/data", action_dim=18)
for frame, action in stream:
rec.record(frame, action, imu_vec=imu_or_None)
rec.flush()
imu_vec:可选 MPU6050 姿态向量(单传感器 roll,pitch,ax,ay,az,gx,gy,gz;
多传感器按脚本 @MPU6050_BIND 行顺序拼接为 8×N,最多 4 个)。
RobotStruct 会在录制前把 imu_dim 同步为 8×N(N=传感器数)。
只要同一 record 调用内同时传入帧/动作/姿态,三者即按帧时刻天然对齐。
全程 None(无 IMU 硬件)则不产出 imu.npy,保持旧格式兼容。
"""
def __init__(self, output_dir, action_dim=18, imu_dim=0):
self.action_dim = action_dim
self.imu_dim = imu_dim
self._frame_idx = 0
self._actions = []
self._imus = []
self._has_imu = False
output = Path(output_dir)
output.mkdir(parents=True, exist_ok=True)
ep_idx = 0
for d in output.glob("episode_*"):
try:
ep_idx = max(ep_idx, int(d.name.split("_")[1]) + 1)
except (ValueError, IndexError):
pass
while True:
self.episode_dir = output / f"episode_{ep_idx:04d}"
self.images_dir = self.episode_dir / "images"
try:
self.images_dir.mkdir(parents=True, exist_ok=False)
break
except FileExistsError:
ep_idx += 1
log.info("recording to %s (action_dim=%d imu_dim=%d)",
self.episode_dir, action_dim, imu_dim)
def record(self, frame, action_vec, imu_vec=None):
"""记录一帧 + 动作向量 [+ 姿态向量]
frame: np.ndarray (H,W,3) uint8 或 None(跳过图像保存)
action_vec: list/np.array,长度任意,内部 pad/truncate 到 action_dim
imu_vec: 可选 list/np.array/dict,长度任意,pad/truncate 到 imu_dim;
dict 时按 (roll,pitch,ax,ay,az,gx,gy,gz) 顺序取值(单传感器;
多传感器请传 RobotStruct._read_imus 拼好的 8×N 向量)。
为 None 时该帧记零向量(混合数据集中标记"无姿态")。
"""
import numpy as np
vec = np.zeros(self.action_dim, dtype=np.float32)
src = np.asarray(action_vec, dtype=np.float32).ravel()
n = min(len(src), self.action_dim)
vec[:n] = src[:n]
if self.action_dim > SPK_F_COL:
vec[SPK_F_COL] = 0.0
self._actions.append(vec)
if imu_vec is None:
ivec = np.zeros(self.imu_dim, dtype=np.float32)
else:
self._has_imu = True
if isinstance(imu_vec, dict):
imu_vec = [imu_vec.get(k, 0.0) for k in
("roll", "pitch", "ax", "ay", "az", "gx", "gy", "gz")]
ivec = np.zeros(self.imu_dim, dtype=np.float32)
isrc = np.asarray(imu_vec, dtype=np.float32).ravel()
m = min(len(isrc), self.imu_dim)
ivec[:m] = isrc[:m]
self._imus.append(ivec)
if frame is not None:
from PIL import Image
img = Image.fromarray(frame)
img.save(self.images_dir / f"{self._frame_idx:04d}.jpg",
quality=90)
self._frame_idx += 1
def flush(self):
"""保存 actions.npy(及有姿态时的 imu.npy),返回 episode 路径"""
import numpy as np
if self._actions:
actions = np.array(self._actions, dtype=np.float32)
np.save(self.episode_dir / "actions.npy", actions)
# 仅当确实采到非零姿态才落盘 imu.npy,避免全程无 IMU 时产生误导性文件
if self._has_imu:
imus = np.array(self._imus, dtype=np.float32)
np.save(self.episode_dir / "imu.npy", imus)
log.info("recorded imu.npy shape=%s", imus.shape)
log.info("recorded %d frames to %s",
self._frame_idx, self.episode_dir)
return str(self.episode_dir)