Skip to content

Commit 479d5c5

Browse files
committed
refactor: calibration with list instead of queue
1 parent a47d80e commit 479d5c5

3 files changed

Lines changed: 76 additions & 44 deletions

File tree

extensions/rcs_realsense/src/rcs_realsense/calibration.py

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import logging
2+
import threading
23
import typing
3-
from queue import Queue
44
from time import sleep
55

66
import apriltag
77
import cv2
88
import numpy as np
9+
from PIL import Image, ImageDraw
10+
from rcs._core import common
911
from rcs.camera.hw import CalibrationStrategy
1012
from rcs.camera.interface import Frame
1113
from tqdm import tqdm
@@ -20,39 +22,37 @@ def __init__(self, camera_name: str):
2022
# base frame to camera, world to base frame
2123
self._extrinsics: np.ndarray[tuple[typing.Literal[4], typing.Literal[4]], np.dtype[np.float64]] | None = None
2224
self.camera_name = camera_name
23-
self.world_to_tag = np.array(
24-
[
25-
[0, -1, 0, 0.2],
26-
[-1, 0, 0, 0],
27-
[0, 0, -1, 0],
28-
[0, 0, 0, 1],
29-
]
30-
)
25+
self.tag_to_world = common.Pose(rpy_vector=[np.pi, 0, -np.pi / 2], translation=[0.2, 0, 0]).pose_matrix()
3126

3227
def calibrate(
3328
self,
34-
samples: Queue[Frame],
29+
samples: list[Frame],
3530
intrinsics: np.ndarray[tuple[typing.Literal[3], typing.Literal[4]], np.dtype[np.float64]],
31+
lock: threading.Lock,
3632
) -> bool:
3733
logger.info("Calibrating camera %s. Position it as you wish and press enter.", self.camera_name)
3834
input()
3935
tries = 3
40-
while samples.qsize() < samples.maxsize - 1 and tries > 0:
36+
while len(samples) < 10 and tries > 0:
4137
logger.info("not enought frames in recorded, waiting 2 seconds...")
4238
tries = -1
4339
sleep(2)
4440
if tries == 0:
4541
logger.warning("Calibration failed, not enough frames arrived.")
4642
return False
43+
4744
frames = []
48-
for _ in samples.qsize():
49-
frames.append(samples.get())
45+
with lock:
46+
for sample in samples:
47+
frames.append(sample.camera.color.data.copy())
48+
print(frames)
5049

51-
_, cam_to_tag, _ = get_average_marker_pose(
52-
frames, intrinsics=intrinsics, calib_tag_id=9, show_live_window=False
50+
last_frame, tag_to_cam = get_average_marker_pose(
51+
frames, intrinsics=intrinsics, calib_tag_id=9, show_live_window=True
5352
)
5453

55-
world_to_cam = self.world_to_tag @ np.linalg.inv(cam_to_tag)
54+
cam_to_world = self.tag_to_world @ np.linalg.inv(tag_to_cam)
55+
world_to_cam = np.linalg.inv(cam_to_world)
5656
self._extrinsics = world_to_cam
5757
return True
5858

@@ -89,7 +89,7 @@ def get_average_marker_pose(
8989

9090
last_frame = frame.copy()
9191

92-
camera_matrix = intrinsics
92+
camera_matrix = intrinsics[:3, :3]
9393

9494
if show_live_window:
9595
cv2.drawFrameAxes(frame, camera_matrix, None, pose[:3, :3], pose[:3, 3], 0.1)
@@ -109,9 +109,8 @@ def get_average_marker_pose(
109109
logger.info(f"Average pose: {avg_pose}")
110110

111111
# paint avg pose on last frame
112-
if show_live_window:
113-
cv2.drawFrameAxes(last_frame, camera_matrix, None, avg_pose[:3, :3], avg_pose[:3, 3], 0.1) # type: ignore
114-
return last_frame, avg_pose, camera_matrix
112+
# cv2.drawFrameAxes(last_frame, camera_matrix, None, avg_pose[:3, :3], avg_pose[:3, 3], 0.1) # type: ignore
113+
return last_frame, avg_pose
115114

116115

117116
def get_marker_pose(calib_tag_id, detector, intrinsics, frame):

extensions/rcs_realsense/src/rcs_realsense/camera.py

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import copy
12
import logging
2-
import queue
3+
import threading
34
import typing
45
from dataclasses import dataclass
56

@@ -59,7 +60,8 @@ def __init__(
5960
self.resolution_width = sample_camera_config.resolution_width
6061
self.resolution_height = sample_camera_config.resolution_height
6162
self.frame_rate = sample_camera_config.frame_rate
62-
self._frame_buffer: dict[str, queue.Queue] = {}
63+
self._frame_buffer_lock: dict[str, threading.Lock] = {}
64+
self._frame_buffer: dict[str, list] = {}
6365

6466
self.D400_config = rs.config()
6567
self.D400_config.enable_stream(
@@ -181,17 +183,21 @@ def enable_device(self, camera_name: str, device_info: RealSenseDeviceInfo, enab
181183
color_vp = pipeline_profile.get_stream(rs.stream.color).as_video_stream_profile()
182184

183185
rs_color_intrinsics = color_vp.get_intrinsics()
184-
color_intrinsics = np.array([
185-
[rs_color_intrinsics.fx, 0, (rs_color_intrinsics.width-1)/2, 0],
186-
[0, rs_color_intrinsics.fy, (rs_color_intrinsics.height-1)/2, 0],
187-
[0, 0, 1, 0],
188-
])
186+
color_intrinsics = np.array(
187+
[
188+
[rs_color_intrinsics.fx, 0, (rs_color_intrinsics.width - 1) / 2, 0],
189+
[0, rs_color_intrinsics.fy, (rs_color_intrinsics.height - 1) / 2, 0],
190+
[0, 0, 1, 0],
191+
]
192+
)
189193
rs_depth_intrinsics = depth_vp.get_intrinsics()
190-
depth_intrinsics = np.array([
191-
[rs_depth_intrinsics.fx, 0, (rs_depth_intrinsics.width-1)/2, 0],
192-
[0, rs_depth_intrinsics.fy, (rs_depth_intrinsics.height-1)/2, 0],
193-
[0, 0, 1, 0],
194-
])
194+
depth_intrinsics = np.array(
195+
[
196+
[rs_depth_intrinsics.fx, 0, (rs_depth_intrinsics.width - 1) / 2, 0],
197+
[0, rs_depth_intrinsics.fy, (rs_depth_intrinsics.height - 1) / 2, 0],
198+
[0, 0, 1, 0],
199+
]
200+
)
195201

196202
depth_to_color = depth_vp.get_extrinsics_to(color_vp)
197203

@@ -202,10 +208,13 @@ def enable_device(self, camera_name: str, device_info: RealSenseDeviceInfo, enab
202208
depth_scale=sensor.get_depth_scale(),
203209
color_intrinsics=color_intrinsics,
204210
depth_intrinsics=depth_intrinsics,
205-
depth_to_color=common.Pose(translation=depth_to_color.translation, rotation=np.array(depth_to_color.rotation).reshape(3, 3)),
211+
depth_to_color=common.Pose(
212+
translation=depth_to_color.translation, rotation=np.array(depth_to_color.rotation).reshape(3, 3)
213+
),
206214
)
207215

208-
self._frame_buffer[camera_name] = queue.Queue(maxsize=self.CALIBRATION_FRAME_SIZE)
216+
self._frame_buffer[camera_name] = []
217+
self._frame_buffer_lock[camera_name] = threading.Lock()
209218
self._logger.debug("Enabled device %s (%s)", device_info.serial, device_info.product_line)
210219

211220
@staticmethod
@@ -257,7 +266,9 @@ def to_ts(frame: rs.frame) -> float:
257266

258267
color_extrinsics = self.calibration_strategy[camera_name].get_extrinsics()
259268
depth_to_color = device.depth_to_color
260-
depth_extrinsics = color_extrinsics @ depth_to_color.inverse().pose_matrix() if color_extrinsics is not None else None
269+
depth_extrinsics = (
270+
color_extrinsics @ depth_to_color.inverse().pose_matrix() if color_extrinsics is not None else None
271+
)
261272

262273
timestamps = []
263274
for stream in streams:
@@ -275,7 +286,9 @@ def to_ts(frame: rs.frame) -> float:
275286
elif rs.stream.depth == stream.stream_type():
276287
frame = frameset.get_depth_frame()
277288
depth = DataFrame(
278-
data=(to_numpy(frame).astype(np.float64) * device.depth_scale / BaseCameraSet.DEPTH_SCALE).astype(np.int16),
289+
data=(to_numpy(frame).astype(np.float64) * device.depth_scale / BaseCameraSet.DEPTH_SCALE).astype(
290+
np.int16
291+
),
279292
timestamp=to_ts(frame),
280293
intrinsics=device.depth_intrinsics,
281294
extrinsics=depth_extrinsics,
@@ -302,7 +315,10 @@ def to_ts(frame: rs.frame) -> float:
302315
)
303316
imu = IMUFrame(accel=accel, gyro=gyro)
304317
f = Frame(camera=cf, imu=imu, avg_timestamp=float(np.mean(timestamps)) if len(timestamps) > 0 else None)
305-
self._frame_buffer[camera_name].put(f)
318+
with self._frame_buffer_lock[camera_name]:
319+
if len(self._frame_buffer[camera_name]) >= self.CALIBRATION_FRAME_SIZE:
320+
self._frame_buffer[camera_name].pop(0)
321+
self._frame_buffer[camera_name].append(copy.deepcopy(f))
306322
return f
307323

308324
def disable_streams(self):
@@ -329,6 +345,12 @@ def enable_emitter(self, enable_ir_emitter=True):
329345

330346
def calibrate(self) -> bool:
331347
for camera_name in self.cameras:
332-
self.calibration_strategy[camera_name].calibrate(
333-
intrinsics=self._enabled_devices[camera_name].color_intrinsics, samples=self._frame_buffer
334-
)
348+
if not self.calibration_strategy[camera_name].calibrate(
349+
intrinsics=self._enabled_devices[camera_name].color_intrinsics,
350+
samples=self._frame_buffer[camera_name],
351+
lock=self._frame_buffer_lock[camera_name],
352+
):
353+
self._logger.warning(f"Calibration of camera {camera_name} failed.")
354+
return False
355+
self._logger.info("Calibration successful.")
356+
return True

python/rcs/camera/hw.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,20 @@ class CalibrationStrategy(typing.Protocol):
5050

5151
def calibrate(
5252
self,
53-
samples: Queue[Frame],
53+
samples: list[Frame],
5454
intrinsics: np.ndarray[tuple[typing.Literal[3], typing.Literal[4]], np.dtype[np.float64]],
55+
lock: threading.Lock,
5556
) -> bool:
56-
"""Implements algorithm to calibrate the camera."""
57+
"""Implements algorithm to calibrate the camera.
58+
59+
Args:
60+
samples: List of frames to use for calibration.
61+
intrinsics: Intrinsic camera parameters, e.g. from a previous calibration.
62+
lock: A lock to ensure thread safety during calibration as the samples might refresh in parallel.
63+
64+
Returns:
65+
bool: True if calibration was successful, False otherwise.
66+
"""
5767

5868
def get_extrinsics(self) -> np.ndarray[tuple[typing.Literal[4], typing.Literal[4]], np.dtype[np.float64]] | None:
5969
"""
@@ -67,8 +77,9 @@ class DummyCalibrationStrategy(CalibrationStrategy):
6777

6878
def calibrate(
6979
self,
70-
samples: Queue[Frame],
80+
samples: list[Frame],
7181
intrinsics: np.ndarray[tuple[typing.Literal[3], typing.Literal[4]], np.dtype[np.float64]],
82+
lock: threading.Lock,
7283
) -> bool:
7384
return True
7485

@@ -272,5 +283,5 @@ def poll_frame(self, camera_name: str) -> Frame:
272283
def __enter__(self):
273284
pass
274285

275-
def __exit__(self, **kwargs):
286+
def __exit__(self, *args, **kwargs):
276287
self.close()

0 commit comments

Comments
 (0)