Skip to content

Commit a47d80e

Browse files
committed
feat: full calibration support
- calibration interface - calibration data added to observation - intrinsic calibration for sim and realsense - implemented april tag calibration
1 parent 2c03fb0 commit a47d80e

File tree

8 files changed

+331
-189
lines changed

8 files changed

+331
-189
lines changed

extensions/rcs_realsense/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ description="RCS realsense module"
99
dependencies = [
1010
"rcs==0.4.0",
1111
"pyrealsense2~=2.55.1",
12+
"apriltag==0.0.16",
1213
]
1314
readme = "README.md"
1415
maintainers = [
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import logging
2+
import typing
3+
from queue import Queue
4+
from time import sleep
5+
6+
import apriltag
7+
import cv2
8+
import numpy as np
9+
from rcs.camera.hw import CalibrationStrategy
10+
from rcs.camera.interface import Frame
11+
from tqdm import tqdm
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
class FR3BaseArucoCalibration(CalibrationStrategy):
17+
"""Calibration with a 3D printed aruco marker that fits around the vention's FR3 base mounting plate."""
18+
19+
def __init__(self, camera_name: str):
20+
# base frame to camera, world to base frame
21+
self._extrinsics: np.ndarray[tuple[typing.Literal[4], typing.Literal[4]], np.dtype[np.float64]] | None = None
22+
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+
)
31+
32+
def calibrate(
33+
self,
34+
samples: Queue[Frame],
35+
intrinsics: np.ndarray[tuple[typing.Literal[3], typing.Literal[4]], np.dtype[np.float64]],
36+
) -> bool:
37+
logger.info("Calibrating camera %s. Position it as you wish and press enter.", self.camera_name)
38+
input()
39+
tries = 3
40+
while samples.qsize() < samples.maxsize - 1 and tries > 0:
41+
logger.info("not enought frames in recorded, waiting 2 seconds...")
42+
tries = -1
43+
sleep(2)
44+
if tries == 0:
45+
logger.warning("Calibration failed, not enough frames arrived.")
46+
return False
47+
frames = []
48+
for _ in samples.qsize():
49+
frames.append(samples.get())
50+
51+
_, cam_to_tag, _ = get_average_marker_pose(
52+
frames, intrinsics=intrinsics, calib_tag_id=9, show_live_window=False
53+
)
54+
55+
world_to_cam = self.world_to_tag @ np.linalg.inv(cam_to_tag)
56+
self._extrinsics = world_to_cam
57+
return True
58+
59+
def get_extrinsics(self) -> np.ndarray[tuple[typing.Literal[4], typing.Literal[4]], np.dtype[np.float64]] | None:
60+
return self._extrinsics
61+
62+
63+
def get_average_marker_pose(
64+
samples,
65+
intrinsics,
66+
calib_tag_id,
67+
show_live_window,
68+
):
69+
# create detector
70+
options = apriltag.DetectorOptions(families="tag25h9")
71+
detector = apriltag.Detector(options=options)
72+
73+
# make while loop with tqdm
74+
poses = []
75+
76+
for frame in tqdm(samples):
77+
78+
# detect tags
79+
marker_det, pose = get_marker_pose(calib_tag_id, detector, intrinsics, frame)
80+
81+
if marker_det is None:
82+
continue
83+
84+
for corner in marker_det.corners:
85+
corner = corner.astype(int)
86+
cv2.circle(frame, tuple(corner), 5, (0, 0, 255), -1)
87+
88+
poses.append(pose)
89+
90+
last_frame = frame.copy()
91+
92+
camera_matrix = intrinsics
93+
94+
if show_live_window:
95+
cv2.drawFrameAxes(frame, camera_matrix, None, pose[:3, :3], pose[:3, 3], 0.1)
96+
# show frame
97+
cv2.imshow("frame", frame)
98+
99+
# wait for key press
100+
if cv2.waitKey(1) & 0xFF == ord("q"):
101+
break
102+
103+
if show_live_window:
104+
cv2.destroyAllWindows()
105+
106+
# calculate the average marker pose
107+
poses = np.array(poses)
108+
avg_pose = np.mean(poses, axis=0)
109+
logger.info(f"Average pose: {avg_pose}")
110+
111+
# 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
115+
116+
117+
def get_marker_pose(calib_tag_id, detector, intrinsics, frame):
118+
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
119+
detections = detector.detect(gray)
120+
121+
# count detections
122+
n_det = 0
123+
marker_det = None
124+
for det in detections:
125+
if det.tag_id != calib_tag_id:
126+
continue
127+
n_det += 1
128+
marker_det = det
129+
130+
if n_det > 1:
131+
raise ValueError("Expected 1 detection of tag id " f"{calib_tag_id}, got {n_det}.")
132+
133+
if marker_det is None:
134+
return None, None
135+
136+
fx = intrinsics[0, 0]
137+
fy = intrinsics[1, 1]
138+
cx = intrinsics[0, 2]
139+
cy = intrinsics[1, 2]
140+
141+
pose, _, _ = detector.detection_pose(
142+
marker_det,
143+
camera_params=(
144+
fx,
145+
fy,
146+
cx,
147+
cy,
148+
),
149+
tag_size=0.1,
150+
)
151+
152+
return marker_det, pose

0 commit comments

Comments
 (0)