Upload lerobot_ee6d_piper_real.py with huggingface_hub
Browse files- lerobot_ee6d_piper_real.py +139 -0
lerobot_ee6d_piper_real.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import numpy as np, torch, random
|
| 3 |
+
from mmengine import fileio
|
| 4 |
+
from scipy.interpolate import interp1d
|
| 5 |
+
from ..utils import read_video_to_frames, read_parquet, quat_to_rotate6d, euler_to_rotate6d
|
| 6 |
+
from PIL import Image
|
| 7 |
+
from .base import DomainHandler
|
| 8 |
+
import pandas as pd
|
| 9 |
+
import json
|
| 10 |
+
from typing import Iterable, Any
|
| 11 |
+
from tqdm import tqdm
|
| 12 |
+
import pickle
|
| 13 |
+
|
| 14 |
+
DEBUG_TRIGGER = True
|
| 15 |
+
|
| 16 |
+
class PiperLeRobotEE6DHandler(DomainHandler):
|
| 17 |
+
|
| 18 |
+
def __init__(self, meta: dict, num_views: int):
|
| 19 |
+
super().__init__(meta, num_views)
|
| 20 |
+
stats_path = self.meta.get("action_stats_path", None)
|
| 21 |
+
self.stats_path = stats_path
|
| 22 |
+
if stats_path is not None and fileio.exists(stats_path):
|
| 23 |
+
with open(stats_path, 'rb') as f:
|
| 24 |
+
self.action_stats = pickle.load(f)
|
| 25 |
+
self.mean = self.action_stats['mean']
|
| 26 |
+
self.std = self.action_stats['std']
|
| 27 |
+
self.min = self.action_stats['min']
|
| 28 |
+
self.max = self.action_stats['max']
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def index_candidates(self, T_left: int, training: bool) -> Iterable[int]:
|
| 32 |
+
return range(0, max(0, T_left - 10))
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def iter_episode(self, traj_idx: int, *, num_actions: int, training: bool,
|
| 36 |
+
image_aug, lang_aug_map: dict | None, **kwargs):
|
| 37 |
+
datapath = self.meta["datalist"][traj_idx]
|
| 38 |
+
video_base_path = self.meta["video_base_path"]
|
| 39 |
+
tasks_path = self.meta.get("tasks_path", None)
|
| 40 |
+
with open(tasks_path, 'r') as f:
|
| 41 |
+
tasks = f.readlines()
|
| 42 |
+
|
| 43 |
+
df = pd.read_parquet(datapath)
|
| 44 |
+
task_index = df['task_index'].iloc[0]
|
| 45 |
+
cur_task = json.loads(tasks[task_index])["task"]
|
| 46 |
+
raw_action = np.stack(df['action_eepose'])
|
| 47 |
+
|
| 48 |
+
# 2. 提取左臂 (Arm 1) 的组件并转换
|
| 49 |
+
# 假设格式: [pos(3), euler(3), grip(1), pos(3), euler(3), grip(1)]
|
| 50 |
+
pos1 = raw_action[:, :3]
|
| 51 |
+
rot1_6d = euler_to_rotate6d(raw_action[:, 3:6], 'xyz') # 只传欧拉角部分 (N, 3)
|
| 52 |
+
grip1 = raw_action[:, 6:7]
|
| 53 |
+
|
| 54 |
+
# 3. 提取右臂 (Arm 2) 的组件并转换
|
| 55 |
+
pos2 = raw_action[:, 7:10]
|
| 56 |
+
rot2_6d = euler_to_rotate6d(raw_action[:, 10:13], 'xyz') # 只传欧拉角部分 (N, 3)
|
| 57 |
+
grip2 = raw_action[:, 13:14]
|
| 58 |
+
|
| 59 |
+
# 4. 重新拼接成 20 维的向量 (3+6+1 + 3+6+1)
|
| 60 |
+
action_ee6d = np.concatenate([pos1, rot1_6d, grip1, pos2, rot2_6d, grip2], axis=-1)
|
| 61 |
+
image_observations = ['observation.images.cam_high', 'observation.images.cam_left_wrist', 'observation.images.cam_right_wrist']
|
| 62 |
+
vid_paths = []
|
| 63 |
+
for image_type in image_observations:
|
| 64 |
+
image_type_base_path = video_base_path + image_type + "/"
|
| 65 |
+
vid_path = image_type_base_path + f"episode_{traj_idx:06d}.mp4"
|
| 66 |
+
vid_paths.append(vid_path)
|
| 67 |
+
|
| 68 |
+
images = [read_video_to_frames(p) for p in vid_paths]
|
| 69 |
+
image_mask = torch.ones(self.num_views, dtype=torch.bool)
|
| 70 |
+
image_mask[:len(images)] = True
|
| 71 |
+
|
| 72 |
+
# stack 之后 shape 应该是 [T, 20]
|
| 73 |
+
all_actions = np.stack(action_ee6d)
|
| 74 |
+
|
| 75 |
+
# --- 切分左右臂 ---
|
| 76 |
+
dim_per_arm = 10
|
| 77 |
+
left = all_actions[:, :dim_per_arm]
|
| 78 |
+
right = all_actions[:, dim_per_arm:]
|
| 79 |
+
|
| 80 |
+
# 4. 时间插值 (处理帧率对齐)
|
| 81 |
+
freq = 30.0 # 请确认你的录制频率
|
| 82 |
+
qdur = 1.0 # 预测未来 1 秒
|
| 83 |
+
t = np.arange(left.shape[0], dtype=np.float64) / freq
|
| 84 |
+
|
| 85 |
+
lt = np.arange(left.shape[0], dtype=np.float64) / float(freq)
|
| 86 |
+
rt = np.arange(right.shape[0], dtype=np.float64) / float(freq)
|
| 87 |
+
|
| 88 |
+
# Candidate indices (optionally shuffled)
|
| 89 |
+
idxs = list(self.index_candidates(left.shape[0], training))
|
| 90 |
+
if training: random.shuffle(idxs)
|
| 91 |
+
|
| 92 |
+
# Interpolators; clamp to endpoints
|
| 93 |
+
L = interp1d(lt, left, axis=0, bounds_error=False, fill_value=(left[0], left[-1]))
|
| 94 |
+
R = interp1d(rt, right, axis=0, bounds_error=False, fill_value=(right[0], right[-1]))
|
| 95 |
+
ref = (lt + rt) / 2.0
|
| 96 |
+
|
| 97 |
+
V = min(self.num_views, len(images))
|
| 98 |
+
for idx in idxs:
|
| 99 |
+
imgs = []
|
| 100 |
+
for v in range(min(self.num_views, len(images))):
|
| 101 |
+
imgs.append(image_aug(Image.fromarray(images[v][idx])))
|
| 102 |
+
while len(imgs) < self.num_views: imgs.append(torch.zeros_like(imgs[0]))
|
| 103 |
+
image_input = torch.stack(imgs, 0)
|
| 104 |
+
cur = t[idx]
|
| 105 |
+
q = np.linspace(cur, min(cur + qdur, float(t.max())), num_actions + 1, dtype=np.float32)
|
| 106 |
+
lseq, rseq = torch.tensor(L(q)), torch.tensor(R(q))
|
| 107 |
+
if (lseq[1]-lseq[0]).abs().max() < 1e-5 and (rseq[1]-rseq[0]).abs().max() < 1e-5:continue
|
| 108 |
+
if lang_aug_map is not None and ins in lang_aug_map: ins = random.choice(lang_aug_map[ins])
|
| 109 |
+
|
| 110 |
+
trajectory = torch.cat([lseq, rseq], -1).float()
|
| 111 |
+
# if self.stats_path is not None and hasattr(self, 'min') and hasattr(self, 'max'):
|
| 112 |
+
# min_val = torch.tensor(self.min)
|
| 113 |
+
# max_val = torch.tensor(self.max)
|
| 114 |
+
# # 归一化到 [-1, 1]
|
| 115 |
+
# norm_action = 2 * (trajectory - min_val) / (max_val - min_val + 1e-8) - 1
|
| 116 |
+
# trajectory = norm_action
|
| 117 |
+
|
| 118 |
+
# if training:
|
| 119 |
+
# # 注入微量高斯噪声 (例如 0.001 级别)
|
| 120 |
+
# # 注意不要给最后的 gripper 维度加噪声(如果是 0/1)
|
| 121 |
+
# noise = torch.randn_like(trajectory) * 0.001
|
| 122 |
+
# # 第10维是左臂 gripper,第20维是右臂 gripper
|
| 123 |
+
# noise[:, 9] = 0.0
|
| 124 |
+
# noise[:, 19] = 0.0
|
| 125 |
+
# trajectory += noise
|
| 126 |
+
|
| 127 |
+
yield {
|
| 128 |
+
"language_instruction": cur_task,
|
| 129 |
+
"image_input": image_input,
|
| 130 |
+
"image_mask": image_mask,
|
| 131 |
+
"abs_trajectory": trajectory
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
# yield {
|
| 135 |
+
# "language_instruction": cur_task,
|
| 136 |
+
# "image_input": image_input,
|
| 137 |
+
# "image_mask": image_mask,
|
| 138 |
+
# "abs_trajectory": torch.cat([lseq, rseq], -1).float()
|
| 139 |
+
# }
|