lsnu commited on
Commit
f81aa8a
·
verified ·
1 Parent(s): d93804e

Upload folder using huggingface_hub

Browse files
.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

  • SHA256: 1ad637d15b5f20ae5f3c587ec61c36321996d9a6237f494e7de54b7de3394aba
  • Pointer size: 133 Bytes
  • Size of remote file: 26.4 MB
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

  • SHA256: 0f08d2e63b268178032f7d4cedded9c2d431f01114074a6755689e8126d08ebd
  • Pointer size: 132 Bytes
  • Size of remote file: 2.73 MB
bundle/sim_model_based_20260329/artifacts/pi_rollout_rendered_4000_r96.gif ADDED

Git LFS Details

  • SHA256: 38ce2a549338e25b5635a3c832fb357b340b41a1dc0b4980a032ec02d4d0f18b
  • Pointer size: 133 Bytes
  • Size of remote file: 18.7 MB
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

  • SHA256: 59214e1b5f017b73fad8838c86ab59fe05fc56f9da2e295e11a84e4ac8509fe1
  • Pointer size: 132 Bytes
  • Size of remote file: 3.92 MB
bundle/sim_model_based_20260329/artifacts/robot_motion_fit_overlay.png ADDED

Git LFS Details

  • SHA256: 3cbde1d7a5ff3bb1e3ef9d31d862a19e740be794fb8bf3e3b5ff51363b4dfd42
  • Pointer size: 132 Bytes
  • Size of remote file: 1.59 MB
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

  • SHA256: 981bcc7aae156f0f469e454292f3730f34287d511e08819d0889ed0210c3c148
  • Pointer size: 132 Bytes
  • Size of remote file: 2.15 MB
bundle/sim_model_based_20260329/artifacts/scene_start_render_fitted.png ADDED

Git LFS Details

  • SHA256: 10aaa3a7fb147b63786caeea55c4af59aa4618636e02f007e4a64b1c0003fcc3
  • Pointer size: 132 Bytes
  • Size of remote file: 2.1 MB
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))