Upload folder using huggingface_hub
Browse files- .gitattributes +7 -0
- bundle/sim_model_based_20260329/artifacts/pi_rollout_rendered_4000_r56.gif +3 -0
- bundle/sim_model_based_20260329/artifacts/pi_rollout_rendered_4000_r56.json +0 -0
- bundle/sim_model_based_20260329/artifacts/pi_rollout_rendered_4000_r56_strip.png +3 -0
- bundle/sim_model_based_20260329/artifacts/pi_rollout_rendered_4000_r96.gif +3 -0
- bundle/sim_model_based_20260329/artifacts/pi_rollout_rendered_4000_r96.json +0 -0
- bundle/sim_model_based_20260329/artifacts/real_vs_scene_start_fitted.png +3 -0
- bundle/sim_model_based_20260329/artifacts/robot_motion_fit_overlay.png +3 -0
- bundle/sim_model_based_20260329/artifacts/robot_scene_model.json +44 -0
- bundle/sim_model_based_20260329/artifacts/scene_background_model.png +3 -0
- bundle/sim_model_based_20260329/artifacts/scene_start_render_fitted.png +3 -0
- bundle/sim_model_based_20260329/code/fit_robot_scene_model.py +249 -0
- bundle/sim_model_based_20260329/code/render_pose_sequence.py +108 -0
- bundle/sim_model_based_20260329/code/rollout_pi_policy_rendered.py +154 -0
- bundle/sim_model_based_20260329/code/scene_tools.py +487 -0
.gitattributes
CHANGED
|
@@ -155,3 +155,10 @@ bundle/artifacts/sim/recorded_motion.gif filter=lfs diff=lfs merge=lfs -text
|
|
| 155 |
bundle/artifacts/sim/scene_motion_fit.png filter=lfs diff=lfs merge=lfs -text
|
| 156 |
bundle/artifacts/sim/scene_start_render.png filter=lfs diff=lfs merge=lfs -text
|
| 157 |
bundle/artifacts/sim/teddy_mask_overlay.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
bundle/artifacts/sim/scene_motion_fit.png filter=lfs diff=lfs merge=lfs -text
|
| 156 |
bundle/artifacts/sim/scene_start_render.png filter=lfs diff=lfs merge=lfs -text
|
| 157 |
bundle/artifacts/sim/teddy_mask_overlay.png filter=lfs diff=lfs merge=lfs -text
|
| 158 |
+
bundle/sim_model_based_20260329/artifacts/pi_rollout_rendered_4000_r56.gif filter=lfs diff=lfs merge=lfs -text
|
| 159 |
+
bundle/sim_model_based_20260329/artifacts/pi_rollout_rendered_4000_r56_strip.png filter=lfs diff=lfs merge=lfs -text
|
| 160 |
+
bundle/sim_model_based_20260329/artifacts/pi_rollout_rendered_4000_r96.gif filter=lfs diff=lfs merge=lfs -text
|
| 161 |
+
bundle/sim_model_based_20260329/artifacts/real_vs_scene_start_fitted.png filter=lfs diff=lfs merge=lfs -text
|
| 162 |
+
bundle/sim_model_based_20260329/artifacts/robot_motion_fit_overlay.png filter=lfs diff=lfs merge=lfs -text
|
| 163 |
+
bundle/sim_model_based_20260329/artifacts/scene_background_model.png filter=lfs diff=lfs merge=lfs -text
|
| 164 |
+
bundle/sim_model_based_20260329/artifacts/scene_start_render_fitted.png filter=lfs diff=lfs merge=lfs -text
|
bundle/sim_model_based_20260329/artifacts/pi_rollout_rendered_4000_r56.gif
ADDED
|
Git LFS Details
|
bundle/sim_model_based_20260329/artifacts/pi_rollout_rendered_4000_r56.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
bundle/sim_model_based_20260329/artifacts/pi_rollout_rendered_4000_r56_strip.png
ADDED
|
Git LFS Details
|
bundle/sim_model_based_20260329/artifacts/pi_rollout_rendered_4000_r96.gif
ADDED
|
Git LFS Details
|
bundle/sim_model_based_20260329/artifacts/pi_rollout_rendered_4000_r96.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
bundle/sim_model_based_20260329/artifacts/real_vs_scene_start_fitted.png
ADDED
|
Git LFS Details
|
bundle/sim_model_based_20260329/artifacts/robot_motion_fit_overlay.png
ADDED
|
Git LFS Details
|
bundle/sim_model_based_20260329/artifacts/robot_scene_model.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"session_root": "/workspace/data/teddybear_raw/session_20260327_165944_bear",
|
| 3 |
+
"fit_rows": [
|
| 4 |
+
0,
|
| 5 |
+
52,
|
| 6 |
+
60,
|
| 7 |
+
68,
|
| 8 |
+
76,
|
| 9 |
+
84,
|
| 10 |
+
92,
|
| 11 |
+
100,
|
| 12 |
+
108,
|
| 13 |
+
116,
|
| 14 |
+
124
|
| 15 |
+
],
|
| 16 |
+
"robot_world_rvec": [
|
| 17 |
+
0.15999206444427902,
|
| 18 |
+
-0.24953838143443263,
|
| 19 |
+
-1.8906016307408011
|
| 20 |
+
],
|
| 21 |
+
"robot_world_tvec": [
|
| 22 |
+
0.4878224859284271,
|
| 23 |
+
0.48919255742572404,
|
| 24 |
+
-0.5070228135125122
|
| 25 |
+
],
|
| 26 |
+
"fit_score": 6.463505947380474,
|
| 27 |
+
"motion_iou_by_row": {
|
| 28 |
+
"52": 0.06655859635739488,
|
| 29 |
+
"60": 0.3588341642469731,
|
| 30 |
+
"68": 0.4452867623651695,
|
| 31 |
+
"76": 0.48717815284451305,
|
| 32 |
+
"84": 0.5015739820521984,
|
| 33 |
+
"92": 0.4417927926329424,
|
| 34 |
+
"100": 0.39052739334048026,
|
| 35 |
+
"108": 0.3360236265714658,
|
| 36 |
+
"116": 0.28526207302709067,
|
| 37 |
+
"124": 0.2234565091812978
|
| 38 |
+
},
|
| 39 |
+
"motion_iou_mean": 0.3536494052619526,
|
| 40 |
+
"background_path": "/workspace/kinova_scene_sim/outputs/scene_background_model.png",
|
| 41 |
+
"scene_start_render_path": "/workspace/kinova_scene_sim/outputs/scene_start_render_fitted.png",
|
| 42 |
+
"real_vs_scene_path": "/workspace/kinova_scene_sim/outputs/real_vs_scene_start_fitted.png",
|
| 43 |
+
"motion_overlay_path": "/workspace/kinova_scene_sim/outputs/robot_motion_fit_overlay.png"
|
| 44 |
+
}
|
bundle/sim_model_based_20260329/artifacts/scene_background_model.png
ADDED
|
Git LFS Details
|
bundle/sim_model_based_20260329/artifacts/scene_start_render_fitted.png
ADDED
|
Git LFS Details
|
bundle/sim_model_based_20260329/code/fit_robot_scene_model.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import warnings
|
| 6 |
+
from dataclasses import replace
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import cv2
|
| 10 |
+
import numpy as np
|
| 11 |
+
import pandas as pd
|
| 12 |
+
from PIL import Image
|
| 13 |
+
from scipy.optimize import minimize
|
| 14 |
+
|
| 15 |
+
from scene_tools import (
|
| 16 |
+
DEFAULT_ROBOT_WORLD_RVEC,
|
| 17 |
+
DEFAULT_ROBOT_WORLD_TVEC,
|
| 18 |
+
SceneCalibration,
|
| 19 |
+
load_scene_calibration,
|
| 20 |
+
render_robot_mask,
|
| 21 |
+
render_scene,
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def parse_rows(spec: str) -> list[int]:
|
| 26 |
+
return [int(chunk.strip()) for chunk in spec.split(",") if chunk.strip()]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def load_rgb_frames(session_root: Path, sync: pd.DataFrame, rows: list[int]) -> dict[int, np.ndarray]:
|
| 30 |
+
return {row_idx: np.array(Image.open(session_root / sync.iloc[row_idx]["azure_rgb_file"]).convert("RGB")) for row_idx in rows}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def load_q_rows(sync: pd.DataFrame, rows: list[int]) -> dict[int, np.ndarray]:
|
| 34 |
+
return {row_idx: sync.iloc[row_idx][[f"q{i}_deg" for i in range(1, 8)]].to_numpy(np.float64) for row_idx in rows}
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def compute_motion_mask(base_rgb: np.ndarray, rgb: np.ndarray, roi_mask: np.ndarray, threshold: int = 18) -> np.ndarray:
|
| 38 |
+
diff = np.abs(rgb.astype(np.int16) - base_rgb.astype(np.int16)).mean(axis=2).astype(np.uint8)
|
| 39 |
+
mask = ((diff > threshold) & roi_mask).astype(np.uint8)
|
| 40 |
+
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, np.ones((5, 5), dtype=np.uint8))
|
| 41 |
+
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((11, 11), dtype=np.uint8))
|
| 42 |
+
return mask > 0
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def mask_iou(lhs: np.ndarray, rhs: np.ndarray) -> float:
|
| 46 |
+
intersection = np.logical_and(lhs, rhs).sum()
|
| 47 |
+
union = np.logical_or(lhs, rhs).sum()
|
| 48 |
+
return float(intersection / union) if union else 0.0
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def fit_robot_transform(
|
| 52 |
+
calibration: SceneCalibration, rgbs: dict[int, np.ndarray], q_by_row: dict[int, np.ndarray], rows: list[int]
|
| 53 |
+
) -> tuple[np.ndarray, dict[int, np.ndarray], dict[int, float], float]:
|
| 54 |
+
start_row = rows[0]
|
| 55 |
+
base_rgb = rgbs[start_row]
|
| 56 |
+
roi_mask = np.zeros(base_rgb.shape[:2], dtype=bool)
|
| 57 |
+
roi_mask[250:1079, 850:1919] = True
|
| 58 |
+
motion_masks = {row_idx: compute_motion_mask(base_rgb, rgbs[row_idx], roi_mask) for row_idx in rows[1:]}
|
| 59 |
+
|
| 60 |
+
def score(params: np.ndarray) -> float:
|
| 61 |
+
robot_world_rvec = params[:3]
|
| 62 |
+
robot_world_tvec = params[3:]
|
| 63 |
+
start_mask = render_robot_mask(
|
| 64 |
+
base_rgb.shape,
|
| 65 |
+
q_by_row[start_row],
|
| 66 |
+
calibration.camera_matrix,
|
| 67 |
+
calibration.rvec,
|
| 68 |
+
calibration.tvec,
|
| 69 |
+
robot_world_rvec,
|
| 70 |
+
robot_world_tvec,
|
| 71 |
+
extra_dilate=35,
|
| 72 |
+
) > 0
|
| 73 |
+
total = 0.0
|
| 74 |
+
for row_idx in rows[1:]:
|
| 75 |
+
current_mask = render_robot_mask(
|
| 76 |
+
base_rgb.shape,
|
| 77 |
+
q_by_row[row_idx],
|
| 78 |
+
calibration.camera_matrix,
|
| 79 |
+
calibration.rvec,
|
| 80 |
+
calibration.tvec,
|
| 81 |
+
robot_world_rvec,
|
| 82 |
+
robot_world_tvec,
|
| 83 |
+
extra_dilate=35,
|
| 84 |
+
) > 0
|
| 85 |
+
predicted_change = np.logical_xor(start_mask, current_mask)
|
| 86 |
+
total += 1.0 - mask_iou(predicted_change, motion_masks[row_idx])
|
| 87 |
+
return total
|
| 88 |
+
|
| 89 |
+
start_params = np.concatenate([DEFAULT_ROBOT_WORLD_RVEC.copy(), DEFAULT_ROBOT_WORLD_TVEC.copy()])
|
| 90 |
+
result = minimize(score, start_params, method="Powell", options={"maxiter": 40, "disp": False})
|
| 91 |
+
best_params = np.asarray(result.x, dtype=np.float64)
|
| 92 |
+
|
| 93 |
+
start_mask = render_robot_mask(
|
| 94 |
+
base_rgb.shape,
|
| 95 |
+
q_by_row[start_row],
|
| 96 |
+
calibration.camera_matrix,
|
| 97 |
+
calibration.rvec,
|
| 98 |
+
calibration.tvec,
|
| 99 |
+
best_params[:3],
|
| 100 |
+
best_params[3:],
|
| 101 |
+
extra_dilate=35,
|
| 102 |
+
) > 0
|
| 103 |
+
ious: dict[int, float] = {}
|
| 104 |
+
for row_idx in rows[1:]:
|
| 105 |
+
current_mask = render_robot_mask(
|
| 106 |
+
base_rgb.shape,
|
| 107 |
+
q_by_row[row_idx],
|
| 108 |
+
calibration.camera_matrix,
|
| 109 |
+
calibration.rvec,
|
| 110 |
+
calibration.tvec,
|
| 111 |
+
best_params[:3],
|
| 112 |
+
best_params[3:],
|
| 113 |
+
extra_dilate=35,
|
| 114 |
+
) > 0
|
| 115 |
+
ious[row_idx] = mask_iou(np.logical_xor(start_mask, current_mask), motion_masks[row_idx])
|
| 116 |
+
return best_params, motion_masks, ious, float(result.fun)
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def build_background(
|
| 120 |
+
calibration: SceneCalibration,
|
| 121 |
+
rgbs: dict[int, np.ndarray],
|
| 122 |
+
q_by_row: dict[int, np.ndarray],
|
| 123 |
+
rows: list[int],
|
| 124 |
+
robot_world_rvec: np.ndarray,
|
| 125 |
+
robot_world_tvec: np.ndarray,
|
| 126 |
+
) -> np.ndarray:
|
| 127 |
+
base_rgb = rgbs[rows[0]]
|
| 128 |
+
masked_frames = []
|
| 129 |
+
valid_masks = []
|
| 130 |
+
for row_idx in rows:
|
| 131 |
+
robot_mask = render_robot_mask(
|
| 132 |
+
base_rgb.shape,
|
| 133 |
+
q_by_row[row_idx],
|
| 134 |
+
calibration.camera_matrix,
|
| 135 |
+
calibration.rvec,
|
| 136 |
+
calibration.tvec,
|
| 137 |
+
robot_world_rvec,
|
| 138 |
+
robot_world_tvec,
|
| 139 |
+
extra_dilate=65,
|
| 140 |
+
) > 0
|
| 141 |
+
masked_frames.append(rgbs[row_idx].astype(np.float32))
|
| 142 |
+
valid_masks.append(~robot_mask)
|
| 143 |
+
|
| 144 |
+
frame_stack = np.stack(masked_frames, axis=0)
|
| 145 |
+
valid_stack = np.stack(valid_masks, axis=0)
|
| 146 |
+
with warnings.catch_warnings():
|
| 147 |
+
warnings.simplefilter("ignore", category=RuntimeWarning)
|
| 148 |
+
background = np.zeros_like(base_rgb, dtype=np.float32)
|
| 149 |
+
for channel in range(3):
|
| 150 |
+
channel_values = np.where(valid_stack, frame_stack[..., channel], np.nan)
|
| 151 |
+
background[..., channel] = np.nanmedian(channel_values, axis=0)
|
| 152 |
+
hole_mask = np.isnan(background[..., 0])
|
| 153 |
+
background = np.nan_to_num(background, nan=0.0).astype(np.uint8)
|
| 154 |
+
if np.any(hole_mask):
|
| 155 |
+
background = cv2.cvtColor(
|
| 156 |
+
cv2.inpaint(cv2.cvtColor(background, cv2.COLOR_RGB2BGR), hole_mask.astype(np.uint8) * 255, 9, cv2.INPAINT_TELEA),
|
| 157 |
+
cv2.COLOR_BGR2RGB,
|
| 158 |
+
)
|
| 159 |
+
return background
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def main() -> None:
|
| 163 |
+
parser = argparse.ArgumentParser()
|
| 164 |
+
parser.add_argument("--session-root", default="/workspace/data/teddybear_raw/session_20260327_165944_bear")
|
| 165 |
+
parser.add_argument("--fit-rows", default="0,52,60,68,76,84,92,100,108,116,124")
|
| 166 |
+
parser.add_argument("--output-dir", default="/workspace/kinova_scene_sim/outputs")
|
| 167 |
+
args = parser.parse_args()
|
| 168 |
+
|
| 169 |
+
session_root = Path(args.session_root)
|
| 170 |
+
output_dir = Path(args.output_dir)
|
| 171 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 172 |
+
|
| 173 |
+
sync = pd.read_csv(session_root / "sync_index.csv")
|
| 174 |
+
fit_rows = parse_rows(args.fit_rows)
|
| 175 |
+
calibration = load_scene_calibration(session_root, sync_row_index=0)
|
| 176 |
+
rgbs = load_rgb_frames(session_root, sync, fit_rows)
|
| 177 |
+
q_by_row = load_q_rows(sync, fit_rows)
|
| 178 |
+
|
| 179 |
+
best_params, motion_masks, ious, fit_score = fit_robot_transform(calibration, rgbs, q_by_row, fit_rows)
|
| 180 |
+
calibration = replace(
|
| 181 |
+
calibration,
|
| 182 |
+
robot_world_rvec=best_params[:3].copy(),
|
| 183 |
+
robot_world_tvec=best_params[3:].copy(),
|
| 184 |
+
)
|
| 185 |
+
background = build_background(calibration, rgbs, q_by_row, fit_rows, best_params[:3], best_params[3:])
|
| 186 |
+
|
| 187 |
+
start_row = fit_rows[0]
|
| 188 |
+
representative_row = fit_rows[min(len(fit_rows) - 1, 4)]
|
| 189 |
+
start_q = q_by_row[start_row]
|
| 190 |
+
start_rgb = rgbs[start_row]
|
| 191 |
+
start_render = render_scene(calibration, start_q, background=background.copy(), alpha=1.0)
|
| 192 |
+
|
| 193 |
+
start_mask = render_robot_mask(
|
| 194 |
+
start_rgb.shape,
|
| 195 |
+
start_q,
|
| 196 |
+
calibration.camera_matrix,
|
| 197 |
+
calibration.rvec,
|
| 198 |
+
calibration.tvec,
|
| 199 |
+
calibration.robot_world_rvec,
|
| 200 |
+
calibration.robot_world_tvec,
|
| 201 |
+
) > 0
|
| 202 |
+
representative_mask = render_robot_mask(
|
| 203 |
+
start_rgb.shape,
|
| 204 |
+
q_by_row[representative_row],
|
| 205 |
+
calibration.camera_matrix,
|
| 206 |
+
calibration.rvec,
|
| 207 |
+
calibration.tvec,
|
| 208 |
+
calibration.robot_world_rvec,
|
| 209 |
+
calibration.robot_world_tvec,
|
| 210 |
+
) > 0
|
| 211 |
+
predicted_change = np.logical_xor(start_mask, representative_mask)
|
| 212 |
+
motion_overlay = rgbs[representative_row].copy()
|
| 213 |
+
motion_overlay[motion_masks[representative_row]] = np.array([0, 255, 0], dtype=np.uint8)
|
| 214 |
+
motion_overlay[predicted_change] = np.array([255, 0, 0], dtype=np.uint8)
|
| 215 |
+
|
| 216 |
+
background_path = output_dir / "scene_background_model.png"
|
| 217 |
+
start_render_path = output_dir / "scene_start_render_fitted.png"
|
| 218 |
+
real_vs_scene_path = output_dir / "real_vs_scene_start_fitted.png"
|
| 219 |
+
motion_overlay_path = output_dir / "robot_motion_fit_overlay.png"
|
| 220 |
+
scene_model_json = output_dir / "robot_scene_model.json"
|
| 221 |
+
|
| 222 |
+
Image.fromarray(background).save(background_path)
|
| 223 |
+
Image.fromarray(start_render).save(start_render_path)
|
| 224 |
+
Image.fromarray(np.concatenate([start_rgb, start_render], axis=1)).save(real_vs_scene_path)
|
| 225 |
+
Image.fromarray(motion_overlay).save(motion_overlay_path)
|
| 226 |
+
|
| 227 |
+
payload = {
|
| 228 |
+
"session_root": str(session_root),
|
| 229 |
+
"fit_rows": fit_rows,
|
| 230 |
+
"robot_world_rvec": calibration.robot_world_rvec.tolist(),
|
| 231 |
+
"robot_world_tvec": calibration.robot_world_tvec.tolist(),
|
| 232 |
+
"fit_score": fit_score,
|
| 233 |
+
"motion_iou_by_row": {str(key): value for key, value in ious.items()},
|
| 234 |
+
"motion_iou_mean": float(np.mean(list(ious.values()))),
|
| 235 |
+
"background_path": str(background_path),
|
| 236 |
+
"scene_start_render_path": str(start_render_path),
|
| 237 |
+
"real_vs_scene_path": str(real_vs_scene_path),
|
| 238 |
+
"motion_overlay_path": str(motion_overlay_path),
|
| 239 |
+
}
|
| 240 |
+
scene_model_json.write_text(json.dumps(payload, indent=2))
|
| 241 |
+
|
| 242 |
+
print(f"saved scene model to {scene_model_json}")
|
| 243 |
+
print(f"robot_world_rvec={calibration.robot_world_rvec.tolist()}")
|
| 244 |
+
print(f"robot_world_tvec={calibration.robot_world_tvec.tolist()}")
|
| 245 |
+
print(f"motion_iou_mean={payload['motion_iou_mean']:.4f}")
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
if __name__ == "__main__":
|
| 249 |
+
main()
|
bundle/sim_model_based_20260329/code/render_pose_sequence.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import cv2
|
| 8 |
+
import numpy as np
|
| 9 |
+
import pandas as pd
|
| 10 |
+
from PIL import Image
|
| 11 |
+
from scipy.optimize import least_squares
|
| 12 |
+
|
| 13 |
+
from scene_tools import create_background_inpaint, kinova_fk_points_and_tool_pose, load_scene_calibration, render_scene
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def euler_xyz_deg_to_matrix(theta_x_deg: float, theta_y_deg: float, theta_z_deg: float) -> np.ndarray:
|
| 17 |
+
rx, ry, rz = np.deg2rad([theta_x_deg, theta_y_deg, theta_z_deg])
|
| 18 |
+
cx, sx = np.cos(rx), np.sin(rx)
|
| 19 |
+
cy, sy = np.cos(ry), np.sin(ry)
|
| 20 |
+
cz, sz = np.cos(rz), np.sin(rz)
|
| 21 |
+
rot_x = np.array([[1, 0, 0], [0, cx, -sx], [0, sx, cx]], dtype=np.float64)
|
| 22 |
+
rot_y = np.array([[cy, 0, sy], [0, 1, 0], [-sy, 0, cy]], dtype=np.float64)
|
| 23 |
+
rot_z = np.array([[cz, -sz, 0], [sz, cz, 0], [0, 0, 1]], dtype=np.float64)
|
| 24 |
+
return rot_z @ rot_y @ rot_x
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def solve_ik(target_pose: np.ndarray, q_init_deg: np.ndarray) -> np.ndarray:
|
| 28 |
+
target_pos = target_pose[:3].astype(np.float64)
|
| 29 |
+
target_rot = euler_xyz_deg_to_matrix(*target_pose[3:6])
|
| 30 |
+
q_init = np.asarray(q_init_deg, dtype=np.float64)
|
| 31 |
+
|
| 32 |
+
def residual(q_deg: np.ndarray) -> np.ndarray:
|
| 33 |
+
_, tool = kinova_fk_points_and_tool_pose(q_deg)
|
| 34 |
+
pos_err = tool[:3, 3] - target_pos
|
| 35 |
+
rot_err_mat = tool[:3, :3].T @ target_rot
|
| 36 |
+
rot_err, _ = cv2.Rodrigues(rot_err_mat)
|
| 37 |
+
regularize = 0.01 * (q_deg - q_init)
|
| 38 |
+
return np.concatenate([12.0 * pos_err, 0.75 * rot_err.reshape(-1), regularize])
|
| 39 |
+
|
| 40 |
+
result = least_squares(residual, q_init, method="lm", max_nfev=120)
|
| 41 |
+
return result.x.astype(np.float64)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def load_target_poses(args: argparse.Namespace) -> tuple[np.ndarray, np.ndarray]:
|
| 45 |
+
if args.poses_json:
|
| 46 |
+
payload = json.loads(Path(args.poses_json).read_text())
|
| 47 |
+
if isinstance(payload, dict) and "actions" in payload:
|
| 48 |
+
poses = np.asarray(payload["actions"], dtype=np.float64)
|
| 49 |
+
else:
|
| 50 |
+
poses = np.asarray(payload, dtype=np.float64)
|
| 51 |
+
start_q = np.asarray(args.q_init_deg, dtype=np.float64)
|
| 52 |
+
return poses, start_q
|
| 53 |
+
|
| 54 |
+
sync = pd.read_csv(Path(args.session_root) / "sync_index.csv")
|
| 55 |
+
rows = sync.iloc[args.sync_start : args.sync_stop : args.sync_step]
|
| 56 |
+
poses = rows[
|
| 57 |
+
["tool_x_m", "tool_y_m", "tool_z_m", "tool_theta_x_deg", "tool_theta_y_deg", "tool_theta_z_deg", "gripper_pos"]
|
| 58 |
+
].to_numpy(np.float64)
|
| 59 |
+
start_q = rows.iloc[0][[f"q{i}_deg" for i in range(1, 8)]].to_numpy(np.float64)
|
| 60 |
+
return poses, start_q
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def main() -> None:
|
| 64 |
+
parser = argparse.ArgumentParser()
|
| 65 |
+
parser.add_argument("--session-root", default="/workspace/data/teddybear_raw/session_20260327_165944_bear")
|
| 66 |
+
parser.add_argument("--output", default="/workspace/kinova_scene_sim/outputs/rollout.gif")
|
| 67 |
+
parser.add_argument("--poses-json", default=None, help="JSON file containing a list of 7D tool poses.")
|
| 68 |
+
parser.add_argument(
|
| 69 |
+
"--q-init-deg",
|
| 70 |
+
nargs=7,
|
| 71 |
+
type=float,
|
| 72 |
+
default=[0.0008167303, 340.0004272461, 180.0014038086, 213.9954223633, 359.9988708496, 309.9989318848, 90.0014266968],
|
| 73 |
+
help="Initial joint seed for IK when using --poses-json.",
|
| 74 |
+
)
|
| 75 |
+
parser.add_argument("--sync-start", type=int, default=0)
|
| 76 |
+
parser.add_argument("--sync-stop", type=int, default=130)
|
| 77 |
+
parser.add_argument("--sync-step", type=int, default=8)
|
| 78 |
+
parser.add_argument("--fps", type=int, default=5)
|
| 79 |
+
args = parser.parse_args()
|
| 80 |
+
|
| 81 |
+
calibration = load_scene_calibration(args.session_root, sync_row_index=0)
|
| 82 |
+
background = create_background_inpaint(calibration.rgb)
|
| 83 |
+
poses, q_seed = load_target_poses(args)
|
| 84 |
+
|
| 85 |
+
frames: list[Image.Image] = []
|
| 86 |
+
q_current = q_seed.copy()
|
| 87 |
+
for idx, pose in enumerate(poses):
|
| 88 |
+
q_current = solve_ik(pose, q_current)
|
| 89 |
+
frame = render_scene(calibration, q_current, background=background)
|
| 90 |
+
# Mark the target tool center on the rendered frame.
|
| 91 |
+
points_world, _ = kinova_fk_points_and_tool_pose(q_current)
|
| 92 |
+
_ = points_world # quiet lint-like readers; pose solving already uses the FK.
|
| 93 |
+
frames.append(Image.fromarray(frame))
|
| 94 |
+
|
| 95 |
+
output = Path(args.output)
|
| 96 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 97 |
+
frames[0].save(
|
| 98 |
+
output,
|
| 99 |
+
save_all=True,
|
| 100 |
+
append_images=frames[1:],
|
| 101 |
+
loop=0,
|
| 102 |
+
duration=int(1000 / max(args.fps, 1)),
|
| 103 |
+
)
|
| 104 |
+
print(f"saved rollout gif to {output}")
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
if __name__ == "__main__":
|
| 108 |
+
main()
|
bundle/sim_model_based_20260329/code/rollout_pi_policy_rendered.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import time
|
| 6 |
+
from dataclasses import replace
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
import pandas as pd
|
| 11 |
+
from PIL import Image
|
| 12 |
+
|
| 13 |
+
from render_pose_sequence import solve_ik
|
| 14 |
+
from scene_tools import load_scene_calibration, render_scene, robot_tool_position_world
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def main() -> None:
|
| 18 |
+
t0 = time.time()
|
| 19 |
+
parser = argparse.ArgumentParser()
|
| 20 |
+
parser.add_argument("--checkpoint-dir", required=True)
|
| 21 |
+
parser.add_argument("--session-root", default="/workspace/data/teddybear_raw/session_20260327_165944_bear")
|
| 22 |
+
parser.add_argument("--sync-row-index", type=int, default=52)
|
| 23 |
+
parser.add_argument("--config-name", default="pi05_kinova_teddybear")
|
| 24 |
+
parser.add_argument("--prompt", default="pick up the teddy bear and place it in the red box")
|
| 25 |
+
parser.add_argument("--rollout-steps", type=int, default=32)
|
| 26 |
+
parser.add_argument("--scene-model-json", default="/workspace/kinova_scene_sim/outputs/robot_scene_model.json")
|
| 27 |
+
parser.add_argument("--output-gif", default=None)
|
| 28 |
+
parser.add_argument("--output-json", default="/workspace/kinova_scene_sim/outputs/pi_rollout_rendered.json")
|
| 29 |
+
args = parser.parse_args()
|
| 30 |
+
|
| 31 |
+
session_root = Path(args.session_root)
|
| 32 |
+
sync = pd.read_csv(session_root / "sync_index.csv")
|
| 33 |
+
row = sync.iloc[args.sync_row_index]
|
| 34 |
+
calibration = load_scene_calibration(session_root, sync_row_index=0)
|
| 35 |
+
if args.scene_model_json and Path(args.scene_model_json).exists():
|
| 36 |
+
scene_model = json.loads(Path(args.scene_model_json).read_text())
|
| 37 |
+
calibration = replace(
|
| 38 |
+
calibration,
|
| 39 |
+
robot_world_rvec=np.asarray(scene_model["robot_world_rvec"], dtype=np.float64),
|
| 40 |
+
robot_world_tvec=np.asarray(scene_model["robot_world_tvec"], dtype=np.float64),
|
| 41 |
+
)
|
| 42 |
+
background_path = Path(scene_model["background_path"])
|
| 43 |
+
else:
|
| 44 |
+
background_path = session_root / row["azure_rgb_file"]
|
| 45 |
+
base_background = np.array(Image.open(background_path).convert("RGB"))
|
| 46 |
+
wrist_rgb = np.array(Image.open(session_root / row["rgb_file"]).convert("RGB"))
|
| 47 |
+
|
| 48 |
+
current_state = np.array(
|
| 49 |
+
[
|
| 50 |
+
row["tool_x_m"],
|
| 51 |
+
row["tool_y_m"],
|
| 52 |
+
row["tool_z_m"],
|
| 53 |
+
row["tool_theta_x_deg"],
|
| 54 |
+
row["tool_theta_y_deg"],
|
| 55 |
+
row["tool_theta_z_deg"],
|
| 56 |
+
row["gripper_pos"],
|
| 57 |
+
],
|
| 58 |
+
dtype=np.float32,
|
| 59 |
+
)
|
| 60 |
+
q_current = row[[f"q{i}_deg" for i in range(1, 8)]].to_numpy(np.float64)
|
| 61 |
+
print(f"[rendered] start row={args.sync_row_index}", flush=True)
|
| 62 |
+
print("[rendered] importing openpi modules", flush=True)
|
| 63 |
+
from openpi.policies import policy_config as _policy_config
|
| 64 |
+
from openpi.training import config as _config
|
| 65 |
+
|
| 66 |
+
print(f"[rendered] imported openpi in {time.time() - t0:.2f}s", flush=True)
|
| 67 |
+
train_config = _config.get_config(args.config_name)
|
| 68 |
+
policy = _policy_config.create_trained_policy(train_config, Path(args.checkpoint_dir))
|
| 69 |
+
print(f"[rendered] policy ready in {time.time() - t0:.2f}s", flush=True)
|
| 70 |
+
|
| 71 |
+
states = [current_state.copy()]
|
| 72 |
+
qs = [q_current.copy()]
|
| 73 |
+
chunks = []
|
| 74 |
+
rendered_files: list[str] = []
|
| 75 |
+
teddy_distances_m = [
|
| 76 |
+
float(
|
| 77 |
+
np.linalg.norm(
|
| 78 |
+
robot_tool_position_world(
|
| 79 |
+
current_state[:3], calibration.robot_world_rvec, calibration.robot_world_tvec
|
| 80 |
+
)[:2]
|
| 81 |
+
- calibration.teddy_world_center[:2]
|
| 82 |
+
)
|
| 83 |
+
)
|
| 84 |
+
]
|
| 85 |
+
|
| 86 |
+
output_dir = Path(args.output_json).parent / "rendered_frames"
|
| 87 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 88 |
+
|
| 89 |
+
for step_idx in range(args.rollout_steps):
|
| 90 |
+
rendered = render_scene(calibration, q_current, background=base_background.copy(), alpha=1.0)
|
| 91 |
+
rendered_path = output_dir / f"frame_{step_idx:03d}.png"
|
| 92 |
+
Image.fromarray(rendered).save(rendered_path)
|
| 93 |
+
rendered_files.append(str(rendered_path))
|
| 94 |
+
|
| 95 |
+
results = policy.infer(
|
| 96 |
+
{
|
| 97 |
+
"observation/state": current_state,
|
| 98 |
+
"observation/image": rendered,
|
| 99 |
+
"observation/wrist_image": wrist_rgb,
|
| 100 |
+
"prompt": args.prompt,
|
| 101 |
+
}
|
| 102 |
+
)
|
| 103 |
+
chunk = np.asarray(results["actions"], dtype=np.float32)
|
| 104 |
+
chunks.append(chunk)
|
| 105 |
+
current_state = chunk[0].copy()
|
| 106 |
+
q_current = solve_ik(current_state, q_current)
|
| 107 |
+
states.append(current_state.copy())
|
| 108 |
+
qs.append(q_current.copy())
|
| 109 |
+
teddy_distances_m.append(
|
| 110 |
+
float(
|
| 111 |
+
np.linalg.norm(
|
| 112 |
+
robot_tool_position_world(
|
| 113 |
+
current_state[:3], calibration.robot_world_rvec, calibration.robot_world_tvec
|
| 114 |
+
)[:2]
|
| 115 |
+
- calibration.teddy_world_center[:2]
|
| 116 |
+
)
|
| 117 |
+
)
|
| 118 |
+
)
|
| 119 |
+
print(
|
| 120 |
+
f"[rendered] step={step_idx + 1}/{args.rollout_steps} "
|
| 121 |
+
f"xyz={current_state[:3].tolist()} grip={float(current_state[6]):.3f} "
|
| 122 |
+
f"teddy_xy_m={teddy_distances_m[-1]:.4f}",
|
| 123 |
+
flush=True,
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
output = {
|
| 127 |
+
"checkpoint_dir": str(Path(args.checkpoint_dir)),
|
| 128 |
+
"session_root": str(session_root),
|
| 129 |
+
"sync_row_index": int(args.sync_row_index),
|
| 130 |
+
"states": np.stack(states, axis=0).tolist(),
|
| 131 |
+
"actions": np.stack(states[1:], axis=0).tolist(),
|
| 132 |
+
"chunks": np.stack(chunks, axis=0).tolist(),
|
| 133 |
+
"joint_sequences_deg": np.stack(qs, axis=0).tolist(),
|
| 134 |
+
"rendered_files": rendered_files,
|
| 135 |
+
"teddy_xy_distance_m": teddy_distances_m,
|
| 136 |
+
}
|
| 137 |
+
output_path = Path(args.output_json)
|
| 138 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 139 |
+
output_path.write_text(json.dumps(output, indent=2))
|
| 140 |
+
output_gif = Path(args.output_gif) if args.output_gif else output_path.with_suffix(".gif")
|
| 141 |
+
frames = [Image.open(frame_path) for frame_path in rendered_files]
|
| 142 |
+
frames[0].save(
|
| 143 |
+
output_gif,
|
| 144 |
+
save_all=True,
|
| 145 |
+
append_images=frames[1:],
|
| 146 |
+
loop=0,
|
| 147 |
+
duration=130,
|
| 148 |
+
)
|
| 149 |
+
print(f"[rendered] saved rollout to {output_path}", flush=True)
|
| 150 |
+
print(f"[rendered] saved gif to {output_gif}", flush=True)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
if __name__ == "__main__":
|
| 154 |
+
main()
|
bundle/sim_model_based_20260329/code/scene_tools.py
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
import cv2
|
| 9 |
+
import numpy as np
|
| 10 |
+
import pandas as pd
|
| 11 |
+
from PIL import Image
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# Manually fitted table corners for the synced overhead frame used for episode 0.
|
| 15 |
+
# Order: top-left, top-right, bottom-right, bottom-left.
|
| 16 |
+
TABLE_CORNERS_PX = np.array(
|
| 17 |
+
[
|
| 18 |
+
[192.0, 138.0],
|
| 19 |
+
[1635.0, 46.0],
|
| 20 |
+
[1712.0, 787.0],
|
| 21 |
+
[185.0, 858.0],
|
| 22 |
+
],
|
| 23 |
+
dtype=np.float64,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
# Assumed tabletop size for the large brown work surface in the overhead image.
|
| 27 |
+
TABLE_SIZE_M = (1.52, 0.76)
|
| 28 |
+
|
| 29 |
+
# Tuned against the pre-grasp motion silhouette.
|
| 30 |
+
CAMERA_FOCAL_PX = 650.0
|
| 31 |
+
ROBOT_BASE_WORLD = np.array([0.50, 0.45, -0.45], dtype=np.float64)
|
| 32 |
+
ROBOT_BASE_YAW_RAD = -2.0
|
| 33 |
+
DEFAULT_ROBOT_WORLD_RVEC = np.array([0.06924932, -0.1712883, -1.95079235], dtype=np.float64)
|
| 34 |
+
DEFAULT_ROBOT_WORLD_TVEC = np.array([0.50851769, 0.49565435, -0.50654742], dtype=np.float64)
|
| 35 |
+
|
| 36 |
+
# Kinova tool-center offset relative to the interface frame from FK.
|
| 37 |
+
TOOL_OFFSET_M = 0.12
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@dataclass(frozen=True)
|
| 41 |
+
class SceneCalibration:
|
| 42 |
+
session_root: Path
|
| 43 |
+
sync_row_index: int
|
| 44 |
+
azure_rgb_seq: int
|
| 45 |
+
azure_depth_seq: int
|
| 46 |
+
robot_seq: int
|
| 47 |
+
rgb_path: Path
|
| 48 |
+
depth_path: Path
|
| 49 |
+
rgb: np.ndarray
|
| 50 |
+
depth: np.ndarray
|
| 51 |
+
sync_row: dict[str, Any]
|
| 52 |
+
camera_matrix: np.ndarray
|
| 53 |
+
rvec: np.ndarray
|
| 54 |
+
tvec: np.ndarray
|
| 55 |
+
table_corners_px: np.ndarray
|
| 56 |
+
table_size_m: tuple[float, float]
|
| 57 |
+
robot_base_world: np.ndarray
|
| 58 |
+
robot_base_yaw_rad: float
|
| 59 |
+
robot_world_rvec: np.ndarray
|
| 60 |
+
robot_world_tvec: np.ndarray
|
| 61 |
+
box_mask: np.ndarray
|
| 62 |
+
teddy_mask: np.ndarray
|
| 63 |
+
box_world_polygon: np.ndarray
|
| 64 |
+
teddy_world_center: np.ndarray
|
| 65 |
+
teddy_world_extent: np.ndarray
|
| 66 |
+
table_depth_mm: float
|
| 67 |
+
box_height_m: float
|
| 68 |
+
teddy_height_m: float
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _to_rgb(path: Path) -> np.ndarray:
|
| 72 |
+
return np.array(Image.open(path).convert("RGB"))
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _to_depth(path: Path) -> np.ndarray:
|
| 76 |
+
return np.array(Image.open(path))
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def load_sync_dataframe(session_root: Path) -> pd.DataFrame:
|
| 80 |
+
return pd.read_csv(session_root / "sync_index.csv")
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def load_scene_calibration(session_root: str | Path, sync_row_index: int = 0) -> SceneCalibration:
|
| 84 |
+
session_root = Path(session_root)
|
| 85 |
+
sync = load_sync_dataframe(session_root)
|
| 86 |
+
row = sync.iloc[sync_row_index]
|
| 87 |
+
|
| 88 |
+
azure_rgb_seq = int(row["azure_rgb_seq"])
|
| 89 |
+
azure_depth_seq = int(row["azure_depth_seq"])
|
| 90 |
+
robot_seq = int(row["robot_seq"])
|
| 91 |
+
|
| 92 |
+
rgb_path = session_root / row["azure_rgb_file"]
|
| 93 |
+
depth_path = session_root / row["azure_depth_file"]
|
| 94 |
+
rgb = _to_rgb(rgb_path)
|
| 95 |
+
depth = _to_depth(depth_path)
|
| 96 |
+
|
| 97 |
+
camera_matrix, rvec, tvec = solve_table_camera(TABLE_CORNERS_PX, TABLE_SIZE_M, CAMERA_FOCAL_PX)
|
| 98 |
+
box_mask = detect_red_box(rgb)
|
| 99 |
+
teddy_mask = detect_teddy(rgb)
|
| 100 |
+
box_world_polygon = image_mask_to_world_polygon(box_mask, camera_matrix, rvec, tvec)
|
| 101 |
+
teddy_world_center, teddy_world_extent = mask_centroid_and_extent_world(teddy_mask, camera_matrix, rvec, tvec)
|
| 102 |
+
|
| 103 |
+
table_depth_mm = estimate_table_depth_mm(depth)
|
| 104 |
+
box_height_m = estimate_height_from_depth(rgb, depth, box_mask, table_depth_mm)
|
| 105 |
+
teddy_height_m = estimate_height_from_depth(rgb, depth, teddy_mask, table_depth_mm)
|
| 106 |
+
|
| 107 |
+
return SceneCalibration(
|
| 108 |
+
session_root=session_root,
|
| 109 |
+
sync_row_index=sync_row_index,
|
| 110 |
+
azure_rgb_seq=azure_rgb_seq,
|
| 111 |
+
azure_depth_seq=azure_depth_seq,
|
| 112 |
+
robot_seq=robot_seq,
|
| 113 |
+
rgb_path=rgb_path,
|
| 114 |
+
depth_path=depth_path,
|
| 115 |
+
rgb=rgb,
|
| 116 |
+
depth=depth,
|
| 117 |
+
sync_row=row.to_dict(),
|
| 118 |
+
camera_matrix=camera_matrix,
|
| 119 |
+
rvec=rvec,
|
| 120 |
+
tvec=tvec,
|
| 121 |
+
table_corners_px=TABLE_CORNERS_PX.copy(),
|
| 122 |
+
table_size_m=TABLE_SIZE_M,
|
| 123 |
+
robot_base_world=ROBOT_BASE_WORLD.copy(),
|
| 124 |
+
robot_base_yaw_rad=float(ROBOT_BASE_YAW_RAD),
|
| 125 |
+
robot_world_rvec=DEFAULT_ROBOT_WORLD_RVEC.copy(),
|
| 126 |
+
robot_world_tvec=DEFAULT_ROBOT_WORLD_TVEC.copy(),
|
| 127 |
+
box_mask=box_mask,
|
| 128 |
+
teddy_mask=teddy_mask,
|
| 129 |
+
box_world_polygon=box_world_polygon,
|
| 130 |
+
teddy_world_center=teddy_world_center,
|
| 131 |
+
teddy_world_extent=teddy_world_extent,
|
| 132 |
+
table_depth_mm=table_depth_mm,
|
| 133 |
+
box_height_m=box_height_m,
|
| 134 |
+
teddy_height_m=teddy_height_m,
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def solve_table_camera(
|
| 139 |
+
table_corners_px: np.ndarray, table_size_m: tuple[float, float], focal_px: float
|
| 140 |
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 141 |
+
width_m, height_m = table_size_m
|
| 142 |
+
object_points = np.array(
|
| 143 |
+
[
|
| 144 |
+
[-width_m / 2, -height_m / 2, 0.0],
|
| 145 |
+
[width_m / 2, -height_m / 2, 0.0],
|
| 146 |
+
[width_m / 2, height_m / 2, 0.0],
|
| 147 |
+
[-width_m / 2, height_m / 2, 0.0],
|
| 148 |
+
],
|
| 149 |
+
dtype=np.float64,
|
| 150 |
+
)
|
| 151 |
+
camera_matrix = np.array(
|
| 152 |
+
[[focal_px, 0.0, 960.0], [0.0, focal_px, 540.0], [0.0, 0.0, 1.0]],
|
| 153 |
+
dtype=np.float64,
|
| 154 |
+
)
|
| 155 |
+
ok, rvec, tvec = cv2.solvePnP(object_points, table_corners_px, camera_matrix, None, flags=cv2.SOLVEPNP_IPPE)
|
| 156 |
+
if not ok:
|
| 157 |
+
raise RuntimeError("solvePnP failed for table camera fit")
|
| 158 |
+
return camera_matrix, rvec, tvec
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def detect_red_box(rgb: np.ndarray) -> np.ndarray:
|
| 162 |
+
hsv = cv2.cvtColor(rgb, cv2.COLOR_RGB2HSV)
|
| 163 |
+
h, s, v = hsv[..., 0], hsv[..., 1], hsv[..., 2]
|
| 164 |
+
mask = (((h < 12) | (h > 170)) & (s > 60) & (v > 80)).astype(np.uint8)
|
| 165 |
+
num, labels, stats, centers = cv2.connectedComponentsWithStats(mask, connectivity=8)
|
| 166 |
+
best = None
|
| 167 |
+
best_area = -1
|
| 168 |
+
for i in range(1, num):
|
| 169 |
+
x, y, w, h_box, area = stats[i]
|
| 170 |
+
cx, cy = centers[i]
|
| 171 |
+
if area > 50_000 and x > 900 and y < 700 and area > best_area:
|
| 172 |
+
best_area = int(area)
|
| 173 |
+
best = labels == i
|
| 174 |
+
if best is None:
|
| 175 |
+
raise RuntimeError("failed to detect red box component")
|
| 176 |
+
return best
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def detect_teddy(rgb: np.ndarray) -> np.ndarray:
|
| 180 |
+
hsv = cv2.cvtColor(rgb, cv2.COLOR_RGB2HSV)
|
| 181 |
+
h, s, v = hsv[..., 0], hsv[..., 1], hsv[..., 2]
|
| 182 |
+
mask = ((h > 5) & (h < 22) & (s > 15) & (s < 120) & (v > 110) & (v < 245)).astype(np.uint8)
|
| 183 |
+
num, labels, stats, centers = cv2.connectedComponentsWithStats(mask, connectivity=8)
|
| 184 |
+
best = None
|
| 185 |
+
best_area = -1
|
| 186 |
+
for i in range(1, num):
|
| 187 |
+
x, y, w, h_box, area = stats[i]
|
| 188 |
+
cx, cy = centers[i]
|
| 189 |
+
if 5_000 < area < 50_000 and 850 < cx < 1250 and 350 < cy < 700 and area > best_area:
|
| 190 |
+
best_area = int(area)
|
| 191 |
+
best = labels == i
|
| 192 |
+
if best is None:
|
| 193 |
+
raise RuntimeError("failed to detect teddy component")
|
| 194 |
+
return best
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def estimate_table_depth_mm(depth: np.ndarray) -> float:
|
| 198 |
+
center_region = depth[150:430, 120:520]
|
| 199 |
+
valid = center_region[center_region > 0]
|
| 200 |
+
if len(valid) == 0:
|
| 201 |
+
raise RuntimeError("no valid table depth values")
|
| 202 |
+
return float(np.percentile(valid, 50))
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def estimate_height_from_depth(rgb: np.ndarray, depth: np.ndarray, rgb_mask: np.ndarray, table_depth_mm: float) -> float:
|
| 206 |
+
ys, xs = np.where(rgb_mask)
|
| 207 |
+
if len(xs) == 0:
|
| 208 |
+
return 0.0
|
| 209 |
+
xd = np.clip((xs * depth.shape[1] / rgb.shape[1]).astype(int), 0, depth.shape[1] - 1)
|
| 210 |
+
yd = np.clip((ys * depth.shape[0] / rgb.shape[0]).astype(int), 0, depth.shape[0] - 1)
|
| 211 |
+
values = depth[yd, xd]
|
| 212 |
+
values = values[values > 0]
|
| 213 |
+
if len(values) == 0:
|
| 214 |
+
return 0.0
|
| 215 |
+
# Use a low percentile to reduce the effect of table pixels around the object.
|
| 216 |
+
object_depth_mm = float(np.percentile(values, 10))
|
| 217 |
+
return max(0.0, (table_depth_mm - object_depth_mm) / 1000.0)
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def image_mask_to_world_polygon(mask: np.ndarray, camera_matrix: np.ndarray, rvec: np.ndarray, tvec: np.ndarray) -> np.ndarray:
|
| 221 |
+
points = np.column_stack(np.where(mask > 0))[:, ::-1].astype(np.float64)
|
| 222 |
+
rect = cv2.minAreaRect(points.astype(np.float32))
|
| 223 |
+
corners = cv2.boxPoints(rect).astype(np.float64)
|
| 224 |
+
return pixels_to_world_on_table(corners, camera_matrix, rvec, tvec)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def mask_centroid_and_extent_world(
|
| 228 |
+
mask: np.ndarray, camera_matrix: np.ndarray, rvec: np.ndarray, tvec: np.ndarray
|
| 229 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 230 |
+
points = np.column_stack(np.where(mask > 0))[:, ::-1].astype(np.float64)
|
| 231 |
+
world_points = pixels_to_world_on_table(points, camera_matrix, rvec, tvec)
|
| 232 |
+
center = world_points.mean(axis=0)
|
| 233 |
+
extent = world_points.max(axis=0) - world_points.min(axis=0)
|
| 234 |
+
return center, extent
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def pixels_to_world_on_table(
|
| 238 |
+
pixels_uv: np.ndarray, camera_matrix: np.ndarray, rvec: np.ndarray, tvec: np.ndarray
|
| 239 |
+
) -> np.ndarray:
|
| 240 |
+
rotation, _ = cv2.Rodrigues(rvec)
|
| 241 |
+
camera_position = (-rotation.T @ tvec).reshape(3)
|
| 242 |
+
inv_camera = np.linalg.inv(camera_matrix)
|
| 243 |
+
world_points = []
|
| 244 |
+
for u, v in pixels_uv:
|
| 245 |
+
ray_cam = inv_camera @ np.array([u, v, 1.0], dtype=np.float64)
|
| 246 |
+
ray_cam /= np.linalg.norm(ray_cam)
|
| 247 |
+
ray_world = rotation.T @ ray_cam
|
| 248 |
+
if abs(ray_world[2]) < 1e-8:
|
| 249 |
+
continue
|
| 250 |
+
scale = -camera_position[2] / ray_world[2]
|
| 251 |
+
world_points.append(camera_position + scale * ray_world)
|
| 252 |
+
if not world_points:
|
| 253 |
+
raise RuntimeError("failed to back-project pixels to the table plane")
|
| 254 |
+
return np.stack(world_points, axis=0)
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def create_background_inpaint(rgb: np.ndarray) -> np.ndarray:
|
| 258 |
+
mask = np.zeros(rgb.shape[:2], dtype=np.uint8)
|
| 259 |
+
polygon = np.array(
|
| 260 |
+
[[1080, 520], [1919, 520], [1919, 1079], [980, 1079], [980, 850], [1080, 760]],
|
| 261 |
+
dtype=np.int32,
|
| 262 |
+
)
|
| 263 |
+
cv2.fillPoly(mask, [polygon], 255)
|
| 264 |
+
cv2.rectangle(mask, (1120, 300), (1450, 680), 0, -1)
|
| 265 |
+
cv2.rectangle(mask, (860, 350), (1180, 700), 0, -1)
|
| 266 |
+
return cv2.cvtColor(cv2.inpaint(cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR), mask, 7, cv2.INPAINT_TELEA), cv2.COLOR_BGR2RGB)
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def kinova_fk_points_world(q_deg: np.ndarray, base_world: np.ndarray, base_yaw_rad: float) -> np.ndarray:
|
| 270 |
+
points, _ = kinova_fk_points_and_tool_pose(q_deg)
|
| 271 |
+
cy, sy = np.cos(base_yaw_rad), np.sin(base_yaw_rad)
|
| 272 |
+
base_rotation = np.array([[cy, -sy, 0.0], [sy, cy, 0.0], [0.0, 0.0, 1.0]], dtype=np.float64)
|
| 273 |
+
return points @ base_rotation.T + base_world.reshape(1, 3)
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def transform_robot_points(points_robot: np.ndarray, robot_world_rvec: np.ndarray, robot_world_tvec: np.ndarray) -> np.ndarray:
|
| 277 |
+
rotation, _ = cv2.Rodrigues(np.asarray(robot_world_rvec, dtype=np.float64).reshape(3, 1))
|
| 278 |
+
translation = np.asarray(robot_world_tvec, dtype=np.float64).reshape(1, 3)
|
| 279 |
+
return np.asarray(points_robot, dtype=np.float64) @ rotation.T + translation
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def robot_tool_position_world(
|
| 283 |
+
tool_position_robot: np.ndarray, robot_world_rvec: np.ndarray, robot_world_tvec: np.ndarray
|
| 284 |
+
) -> np.ndarray:
|
| 285 |
+
return transform_robot_points(np.asarray(tool_position_robot, dtype=np.float64).reshape(1, 3), robot_world_rvec, robot_world_tvec)[0]
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
def kinova_fk_points_and_tool_pose(q_deg: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
| 289 |
+
q = np.deg2rad(np.asarray(q_deg, dtype=np.float64))
|
| 290 |
+
params = [
|
| 291 |
+
(np.pi, 0.0, 0.0, 0.0),
|
| 292 |
+
(np.pi / 2, 0.0, -(0.1564 + 0.1284), q[0]),
|
| 293 |
+
(np.pi / 2, 0.0, -(0.0054 + 0.0064), q[1] + np.pi),
|
| 294 |
+
(np.pi / 2, 0.0, -(0.2104 + 0.2104), q[2] + np.pi),
|
| 295 |
+
(np.pi / 2, 0.0, -(0.0064 + 0.0064), q[3] + np.pi),
|
| 296 |
+
(np.pi / 2, 0.0, -(0.2084 + 0.1059), q[4] + np.pi),
|
| 297 |
+
(np.pi / 2, 0.0, 0.0, q[5] + np.pi),
|
| 298 |
+
(np.pi, 0.0, -(0.1059 + 0.0615), q[6] + np.pi),
|
| 299 |
+
]
|
| 300 |
+
|
| 301 |
+
def dh(alpha: float, a: float, d: float, theta: float) -> np.ndarray:
|
| 302 |
+
ca, sa = np.cos(alpha), np.sin(alpha)
|
| 303 |
+
ct, st = np.cos(theta), np.sin(theta)
|
| 304 |
+
return np.array(
|
| 305 |
+
[
|
| 306 |
+
[ct, -st * ca, st * sa, a * ct],
|
| 307 |
+
[st, ct * ca, -ct * sa, a * st],
|
| 308 |
+
[0.0, sa, ca, d],
|
| 309 |
+
[0.0, 0.0, 0.0, 1.0],
|
| 310 |
+
],
|
| 311 |
+
dtype=np.float64,
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
transform = np.eye(4, dtype=np.float64)
|
| 315 |
+
points = []
|
| 316 |
+
for alpha, a, d, theta in params:
|
| 317 |
+
transform = transform @ dh(alpha, a, d, theta)
|
| 318 |
+
points.append(transform[:3, 3].copy())
|
| 319 |
+
|
| 320 |
+
tool_transform = transform.copy()
|
| 321 |
+
tool_transform[:3, 3] = tool_transform[:3, 3] + tool_transform[:3, 2] * TOOL_OFFSET_M
|
| 322 |
+
points.append(tool_transform[:3, 3].copy())
|
| 323 |
+
return np.stack(points, axis=0), tool_transform
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def project_world_points(points_world: np.ndarray, camera_matrix: np.ndarray, rvec: np.ndarray, tvec: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
| 327 |
+
image_points, _ = cv2.projectPoints(points_world.astype(np.float64), rvec, tvec, camera_matrix, None)
|
| 328 |
+
rotation, _ = cv2.Rodrigues(rvec)
|
| 329 |
+
camera_points = (rotation @ points_world.T + tvec).T
|
| 330 |
+
return image_points[:, 0, :], camera_points[:, 2]
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
def draw_robot(
|
| 334 |
+
image: np.ndarray,
|
| 335 |
+
q_deg: np.ndarray,
|
| 336 |
+
camera_matrix: np.ndarray,
|
| 337 |
+
rvec: np.ndarray,
|
| 338 |
+
tvec: np.ndarray,
|
| 339 |
+
base_world: np.ndarray | None = None,
|
| 340 |
+
base_yaw_rad: float | None = None,
|
| 341 |
+
robot_world_rvec: np.ndarray | None = None,
|
| 342 |
+
robot_world_tvec: np.ndarray | None = None,
|
| 343 |
+
color: tuple[int, int, int] = (232, 242, 250),
|
| 344 |
+
alpha: float = 1.0,
|
| 345 |
+
) -> np.ndarray:
|
| 346 |
+
canvas = image.copy()
|
| 347 |
+
if robot_world_rvec is not None and robot_world_tvec is not None:
|
| 348 |
+
points_robot, _ = kinova_fk_points_and_tool_pose(q_deg)
|
| 349 |
+
points_world = transform_robot_points(points_robot, robot_world_rvec, robot_world_tvec)
|
| 350 |
+
else:
|
| 351 |
+
if base_world is None or base_yaw_rad is None:
|
| 352 |
+
raise ValueError("base_world/base_yaw_rad or robot_world_rvec/robot_world_tvec must be provided")
|
| 353 |
+
points_world = kinova_fk_points_world(q_deg, base_world, base_yaw_rad)
|
| 354 |
+
uv, z = project_world_points(points_world, camera_matrix, rvec, tvec)
|
| 355 |
+
|
| 356 |
+
overlay = canvas.copy()
|
| 357 |
+
link_palette = [
|
| 358 |
+
(214, 224, 232),
|
| 359 |
+
(222, 232, 239),
|
| 360 |
+
(214, 224, 232),
|
| 361 |
+
(206, 220, 229),
|
| 362 |
+
(198, 214, 224),
|
| 363 |
+
(190, 208, 220),
|
| 364 |
+
(182, 202, 215),
|
| 365 |
+
(172, 196, 212),
|
| 366 |
+
]
|
| 367 |
+
outline = (70, 86, 98)
|
| 368 |
+
joint_fill = (230, 236, 240)
|
| 369 |
+
|
| 370 |
+
for idx, (p0, p1, depth_value) in enumerate(zip(uv[:-1], uv[1:], z[:-1], strict=False)):
|
| 371 |
+
base_thickness = max(14, int(52 / max(depth_value, 0.35)))
|
| 372 |
+
fill_color = link_palette[min(idx, len(link_palette) - 1)]
|
| 373 |
+
cv2.line(
|
| 374 |
+
overlay,
|
| 375 |
+
tuple(np.round(p0).astype(int)),
|
| 376 |
+
tuple(np.round(p1).astype(int)),
|
| 377 |
+
outline,
|
| 378 |
+
base_thickness + 8,
|
| 379 |
+
lineType=cv2.LINE_AA,
|
| 380 |
+
)
|
| 381 |
+
cv2.line(
|
| 382 |
+
overlay,
|
| 383 |
+
tuple(np.round(p0).astype(int)),
|
| 384 |
+
tuple(np.round(p1).astype(int)),
|
| 385 |
+
fill_color,
|
| 386 |
+
base_thickness,
|
| 387 |
+
lineType=cv2.LINE_AA,
|
| 388 |
+
)
|
| 389 |
+
for point, depth_value in zip(uv, z, strict=False):
|
| 390 |
+
radius = max(10, int(24 / max(depth_value, 0.35)))
|
| 391 |
+
center = tuple(np.round(point).astype(int))
|
| 392 |
+
cv2.circle(overlay, center, radius + 4, outline, -1, lineType=cv2.LINE_AA)
|
| 393 |
+
cv2.circle(overlay, center, radius, joint_fill, -1, lineType=cv2.LINE_AA)
|
| 394 |
+
if alpha >= 1.0:
|
| 395 |
+
return overlay
|
| 396 |
+
return cv2.addWeighted(overlay, alpha, canvas, 1.0 - alpha, 0.0)
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
def render_robot_mask(
|
| 400 |
+
image_shape: tuple[int, int] | tuple[int, int, int],
|
| 401 |
+
q_deg: np.ndarray,
|
| 402 |
+
camera_matrix: np.ndarray,
|
| 403 |
+
rvec: np.ndarray,
|
| 404 |
+
tvec: np.ndarray,
|
| 405 |
+
robot_world_rvec: np.ndarray,
|
| 406 |
+
robot_world_tvec: np.ndarray,
|
| 407 |
+
extra_dilate: int = 0,
|
| 408 |
+
) -> np.ndarray:
|
| 409 |
+
height, width = image_shape[:2]
|
| 410 |
+
points_robot, _ = kinova_fk_points_and_tool_pose(q_deg)
|
| 411 |
+
points_world = transform_robot_points(points_robot, robot_world_rvec, robot_world_tvec)
|
| 412 |
+
uv, z = project_world_points(points_world, camera_matrix, rvec, tvec)
|
| 413 |
+
mask = np.zeros((height, width), dtype=np.uint8)
|
| 414 |
+
for p0, p1, depth_value in zip(uv[:-1], uv[1:], z[:-1], strict=False):
|
| 415 |
+
thickness = max(18, int(60 / max(depth_value, 0.35)))
|
| 416 |
+
cv2.line(
|
| 417 |
+
mask,
|
| 418 |
+
tuple(np.round(p0).astype(int)),
|
| 419 |
+
tuple(np.round(p1).astype(int)),
|
| 420 |
+
255,
|
| 421 |
+
thickness,
|
| 422 |
+
lineType=cv2.LINE_AA,
|
| 423 |
+
)
|
| 424 |
+
for point, depth_value in zip(uv, z, strict=False):
|
| 425 |
+
radius = max(12, int(26 / max(depth_value, 0.35)))
|
| 426 |
+
cv2.circle(mask, tuple(np.round(point).astype(int)), radius, 255, -1, lineType=cv2.LINE_AA)
|
| 427 |
+
if extra_dilate > 0:
|
| 428 |
+
kernel = np.ones((extra_dilate, extra_dilate), dtype=np.uint8)
|
| 429 |
+
mask = cv2.dilate(mask, kernel, iterations=1)
|
| 430 |
+
return mask
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
def render_scene(
|
| 434 |
+
calibration: SceneCalibration,
|
| 435 |
+
q_deg: np.ndarray,
|
| 436 |
+
background: np.ndarray | None = None,
|
| 437 |
+
color: tuple[int, int, int] = (232, 242, 250),
|
| 438 |
+
alpha: float = 1.0,
|
| 439 |
+
) -> np.ndarray:
|
| 440 |
+
if background is None:
|
| 441 |
+
background = create_background_inpaint(calibration.rgb)
|
| 442 |
+
return draw_robot(
|
| 443 |
+
background,
|
| 444 |
+
q_deg,
|
| 445 |
+
calibration.camera_matrix,
|
| 446 |
+
calibration.rvec,
|
| 447 |
+
calibration.tvec,
|
| 448 |
+
base_world=calibration.robot_base_world,
|
| 449 |
+
base_yaw_rad=calibration.robot_base_yaw_rad,
|
| 450 |
+
robot_world_rvec=calibration.robot_world_rvec,
|
| 451 |
+
robot_world_tvec=calibration.robot_world_tvec,
|
| 452 |
+
color=color,
|
| 453 |
+
alpha=alpha,
|
| 454 |
+
)
|
| 455 |
+
|
| 456 |
+
|
| 457 |
+
def scene_to_jsonable(calibration: SceneCalibration) -> dict[str, Any]:
|
| 458 |
+
return {
|
| 459 |
+
"session_root": str(calibration.session_root),
|
| 460 |
+
"sync_row_index": calibration.sync_row_index,
|
| 461 |
+
"azure_rgb_seq": calibration.azure_rgb_seq,
|
| 462 |
+
"azure_depth_seq": calibration.azure_depth_seq,
|
| 463 |
+
"robot_seq": calibration.robot_seq,
|
| 464 |
+
"rgb_path": str(calibration.rgb_path),
|
| 465 |
+
"depth_path": str(calibration.depth_path),
|
| 466 |
+
"table_corners_px": calibration.table_corners_px.tolist(),
|
| 467 |
+
"table_size_m": list(calibration.table_size_m),
|
| 468 |
+
"camera_focal_px": float(calibration.camera_matrix[0, 0]),
|
| 469 |
+
"rvec": calibration.rvec.reshape(-1).tolist(),
|
| 470 |
+
"tvec": calibration.tvec.reshape(-1).tolist(),
|
| 471 |
+
"robot_base_world": calibration.robot_base_world.tolist(),
|
| 472 |
+
"robot_base_yaw_rad": float(calibration.robot_base_yaw_rad),
|
| 473 |
+
"robot_world_rvec": calibration.robot_world_rvec.tolist(),
|
| 474 |
+
"robot_world_tvec": calibration.robot_world_tvec.tolist(),
|
| 475 |
+
"sync_row": calibration.sync_row,
|
| 476 |
+
"table_depth_mm": float(calibration.table_depth_mm),
|
| 477 |
+
"box_height_m": float(calibration.box_height_m),
|
| 478 |
+
"teddy_height_m": float(calibration.teddy_height_m),
|
| 479 |
+
"box_world_polygon": calibration.box_world_polygon.tolist(),
|
| 480 |
+
"teddy_world_center": calibration.teddy_world_center.tolist(),
|
| 481 |
+
"teddy_world_extent": calibration.teddy_world_extent.tolist(),
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
def save_scene_json(calibration: SceneCalibration, path: str | Path) -> None:
|
| 486 |
+
path = Path(path)
|
| 487 |
+
path.write_text(json.dumps(scene_to_jsonable(calibration), indent=2))
|