feat(pybot×actpred): R2 多 MPU6050 支持——8×N 姿态向量拼接与归一化平铺
- robot:_find_imus/_read_imus 按脚本块行序收集拼接 ≤4 个 MPU6050,_pad_imu 对齐 predictor.imu_dim - recorder/imu_norm/dataset/config:imu.npy 宽度 8×N,归一化除数按传感器数平铺 - 测试:双传感器闭环拼接/pad/truncate、宽布局录制、16 维归一化加载
This commit is contained in:
1 parent
b289e7d17c
commit
cd4d512ecd
10 files changed
+234
-56
No files matched your search
+10
-8
@@ -113,7 +113,7 @@ data/
|
||||
0001.jpg
|
||||
...
|
||||
actions.npy # shape: (T, action_dim) float32
|
||||
imu.npy # 可选,shape: (T, imu_dim) float32(MPU6050 姿态,R2)
|
||||
imu.npy # 可选,shape: (T, 8×N) float32(N 个 MPU6050 姿态拼接,R2)
|
||||
episode_002/
|
||||
...
|
||||
```
|
||||
@@ -121,12 +121,14 @@ data/
|
||||
每个 timestep 的样本:帧 `[t-frame_len+1, t]`(旧→新)+ 前 `history_len` 步动作 → 当前步动作。
|
||||
采样起点为 `max(history_len, frame_len-1)`。
|
||||
|
||||
`imu.npy` 的键序为 `[roll, pitch, ax, ay, az, gx, gy, gz]`(roll/pitch 角度制,
|
||||
其余为 MPU6050 原始 int16),由 pybot `--record` 在脚本含 `@MPU6050_BIND` 块时
|
||||
逐帧同步落盘。**旧 episode 没有姿态数据,训练 IMU 模型需用新版 pybot 重新录制**;
|
||||
`--imu-dim`>0 时缺失 `imu.npy` 的 episode 按零向量兜底(向后兼容,但无姿态先验收益)。
|
||||
姿态归一化按物理量程固定缩放(roll/pitch ÷180,原始 int16 ÷32768),训练与推理
|
||||
共用 `imu_norm.py`,无需统计文件。
|
||||
`imu.npy` 每传感器占 8 槽 `[roll, pitch, ax, ay, az, gx, gy, gz]`(roll/pitch
|
||||
角度制,其余为 MPU6050 原始 int16),多传感器(最多 4 个)按脚本
|
||||
`@MPU6050_BIND` 行顺序拼接,由 pybot `--record` 逐帧同步落盘。**旧 episode
|
||||
没有姿态数据,训练 IMU 模型需用新版 pybot 重新录制**;`--imu-dim`>0 时缺失
|
||||
`imu.npy` 的 episode 按零向量兜底(向后兼容,但无姿态先验收益)。
|
||||
姿态归一化按物理量程固定缩放(roll/pitch ÷180,原始 int16 ÷32768,8 槽模式
|
||||
按传感器数平铺),训练与推理共用 `imu_norm.py`,无需统计文件。
|
||||
录制/训练/闭环脚本的传感器行顺序必须一致,否则槽位错位。
|
||||
|
||||
### 数据采集
|
||||
|
||||
@@ -150,7 +152,7 @@ recorder.flush()
|
||||
| `--action-dim` | 18 | 动作向量维度(对齐 bot4cj 18 列:10 舵机 + 2 云台 + 扬声器频率 + 延时 + 4 预留)|
|
||||
| `--history-len` | 5 | 历史动作窗口长度 |
|
||||
| `--frame-len` | 3 | 输入图像帧数(含当前帧,旧帧在前)|
|
||||
| `--imu-dim` | 0 | R2 姿态输入维度(>0 启用独立 IMU token;8=roll,pitch,ax,ay,az,gx,gy,gz)|
|
||||
| `--imu-dim` | 0 | R2 姿态输入维度(>0 启用独立 IMU token;8×N,N=传感器数≤4)|
|
||||
| `--backbone` | simple | CNN 类型 (simple/mobilenet) |
|
||||
| `--attn-dim` | 128 | 注意力 token 维度 d_model(须能被 attn-heads 整除)|
|
||||
| `--attn-heads` | 4 | 注意力头数 |
|
||||
|
||||
+3
-2
@@ -15,7 +15,8 @@ class DataConfig:
|
||||
action_stats_path: Optional[str] = None
|
||||
# R2 方案一:MPU6050 姿态作为独立 IMU token 注入(序列 F+H+2)。
|
||||
# 0 = 关闭(默认,序列保持 F+H+1,旧 checkpoint 完全兼容);
|
||||
# 8 = roll,pitch,ax,ay,az,gx,gy,gz(pybot recorder 的 imu.npy 格式)。
|
||||
# 8×N = N 个传感器的 roll,pitch,ax,ay,az,gx,gy,gz 按 pybot 脚本
|
||||
# @MPU6050_BIND 行顺序拼接(最多 4 个 → 32,pybot recorder 的 imu.npy 格式)。
|
||||
imu_dim: int = 0
|
||||
|
||||
|
||||
@@ -85,7 +86,7 @@ def build_argparser(description: str = "") -> argparse.ArgumentParser:
|
||||
parser.add_argument("--action-stats-path", type=str, default=None)
|
||||
parser.add_argument("--imu-dim", type=int, default=None,
|
||||
help="R2 方案一:MPU6050 姿态维度(0=关闭,默认;"
|
||||
"8=roll,pitch,ax,ay,az,gx,gy,gz,序列变 F+H+2)")
|
||||
"8×N=N 个传感器拼接(≤4 个),序列变 F+H+2)")
|
||||
parser.add_argument("--backbone", type=str, default=None, choices=["simple", "mobilenet"])
|
||||
parser.add_argument("--cnn-feature-dim", type=int, default=None)
|
||||
parser.add_argument("--fused-hidden-dim", type=int, default=None)
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ from torch.utils.data import Dataset
|
||||
from torchvision import transforms
|
||||
from torchvision.transforms import functional as TF
|
||||
|
||||
from imu_norm import IMU_SCALES
|
||||
from imu_norm import imu_scales
|
||||
|
||||
|
||||
def _natural_key(p):
|
||||
@@ -164,7 +164,7 @@ class ActionDataset(Dataset):
|
||||
if len(imus) < n: # 与帧数对齐:不足补零
|
||||
imus = np.concatenate([
|
||||
imus, np.zeros((n - len(imus), self.imu_dim), dtype=np.float32)])
|
||||
return imus[:n] / IMU_SCALES[:self.imu_dim]
|
||||
return imus[:n] / imu_scales(self.imu_dim)
|
||||
|
||||
def _imu_at(self, imus, t):
|
||||
"""时刻 t 的归一化姿态向量(与最新帧对齐);无数据 → 零向量"""
|
||||
|
||||
+14
-3
@@ -9,20 +9,31 @@
|
||||
索引 0,1 /180 → ≈[-1, 1]
|
||||
索引 2..7 /32768 → ≈[-1, 1]
|
||||
|
||||
键序与 pybot robot.IMU_KEYS / recorder 一致:
|
||||
单传感器键序与 pybot robot.IMU_KEYS / recorder 一致:
|
||||
[roll, pitch, ax, ay, az, gx, gy, gz]
|
||||
多传感器(最多 4 个)按 pybot 脚本 @MPU6050_BIND 行顺序拼接,
|
||||
每传感器占 8 槽,imu_dim = 8×N,归一化除数按 8 槽模式平铺。
|
||||
|
||||
仅依赖 numpy,保证 hailo_infer.py(HAT 部署环境无 torch)可直接复用。
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
# 各索引归一化除数(imu_dim=8 全量;更短的向量取前缀)
|
||||
# 各索引归一化除数(单传感器 8 槽;多传感器用 imu_scales() 平铺)
|
||||
IMU_SCALES = np.array([180.0, 180.0,
|
||||
32768.0, 32768.0, 32768.0,
|
||||
32768.0, 32768.0, 32768.0], dtype=np.float32)
|
||||
|
||||
|
||||
def imu_scales(imu_dim: int) -> np.ndarray:
|
||||
"""归一化除数向量:8 槽模式按传感器数平铺(imu_dim = 8×N,N≤4)
|
||||
|
||||
imu_dim ≤ 8 时等价 IMU_SCALES 前缀(roll/pitch 子集仍可用)。
|
||||
"""
|
||||
reps = (imu_dim + len(IMU_SCALES) - 1) // len(IMU_SCALES)
|
||||
return np.tile(IMU_SCALES, reps)[:imu_dim]
|
||||
|
||||
|
||||
def normalize_imu(imu, imu_dim: int = 8) -> np.ndarray:
|
||||
"""原始姿态向量 → 归一化向量(pad/truncate 到 imu_dim)
|
||||
|
||||
@@ -34,4 +45,4 @@ def normalize_imu(imu, imu_dim: int = 8) -> np.ndarray:
|
||||
src = np.asarray(imu, dtype=np.float32).ravel()
|
||||
n = min(len(src), imu_dim)
|
||||
vec[:n] = src[:n]
|
||||
return vec / IMU_SCALES[:imu_dim]
|
||||
return vec / imu_scales(imu_dim)
|
||||
@@ -1836,6 +1836,20 @@ class TestR2ImuNorm(unittest.TestCase):
|
||||
self.assertEqual(out.shape, (2,))
|
||||
np.testing.assert_allclose(out, [1.0, -1.0], atol=1e-6)
|
||||
|
||||
def test_multi_sensor_tiled_scales(self):
|
||||
"""多传感器(8×N,N≤4):归一化除数按 8 槽模式平铺"""
|
||||
from imu_norm import normalize_imu, imu_scales, IMU_SCALES
|
||||
scales = imu_scales(32)
|
||||
self.assertEqual(scales.shape, (32,))
|
||||
for k in range(4):
|
||||
np.testing.assert_array_equal(scales[k * 8:(k + 1) * 8],
|
||||
IMU_SCALES)
|
||||
# 单传感器 8 槽模式 [±180, ∓180, 32768×6] 拼接两组 → 归一化重复
|
||||
raw = ([180.0, -180.0] + [32768.0] * 6) * 2
|
||||
out = normalize_imu(raw, 16)
|
||||
np.testing.assert_allclose(
|
||||
out, ([1.0, -1.0] + [1.0] * 6) * 2, atol=1e-6)
|
||||
|
||||
|
||||
class TestR2ImuModel(unittest.TestCase):
|
||||
"""R2 方案一模型:独立 IMU token 注入(序列 F+H+2)"""
|
||||
@@ -1965,6 +1979,22 @@ class TestR2ImuDataset(unittest.TestCase):
|
||||
np.testing.assert_allclose(imu[:2], [1.0, -1.0], atol=1e-6)
|
||||
np.testing.assert_array_equal(imu[2:], np.zeros(6, np.float32))
|
||||
|
||||
def test_multi_sensor_16wide_normalized(self):
|
||||
"""双传感器 imu.npy(16 列):两组 8 槽分别按物理量程归一化"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
imus = [[180.0, 0, 0, 0, 0, 0, 0, 0, # sensor0 roll
|
||||
0, 0, 0, 0, 0, 0, 0, 32768.0]] * 10 # sensor1 gz
|
||||
self._create_episode(Path(tmpdir), "episode_0001", 10, 2,
|
||||
imus=imus)
|
||||
ds = ActionDataset(tmpdir, history_len=2, action_dim=2,
|
||||
image_width=16, image_height=16, frame_len=2,
|
||||
imu_dim=16)
|
||||
imu = ds[0]["imu"].numpy()
|
||||
self.assertEqual(imu.shape, (16,))
|
||||
np.testing.assert_allclose(imu[0], 1.0, atol=1e-6)
|
||||
np.testing.assert_allclose(imu[15], 1.0, atol=1e-6)
|
||||
np.testing.assert_array_equal(imu[1:15], np.zeros(14, np.float32))
|
||||
|
||||
def test_short_imu_aligned_with_zero_pad(self):
|
||||
"""imu.npy 行数少于帧数 → 尾部补零对齐"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
|
||||
Reference in new issue
Block a user