Skip to content

Commit ea3810e

Browse files
committed
Extrinsic calibration inital scripts
1 parent 5b6e052 commit ea3810e

File tree

3 files changed

+353
-1
lines changed

3 files changed

+353
-1
lines changed
+331
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
import os
2+
import argparse
3+
import sys
4+
# file_path_root = os.path.split(os.path.split(__file__)[0])[0]
5+
# sys.path.append('file_path_root')
6+
7+
from robot_cell.detection.realsense_depth import DepthCamera
8+
from robot_cell.detection.packet_detector import PacketDetector
9+
ROB_CONFIG_FILE = os.path.join("config", "robot_config.json")
10+
import json
11+
from robot_cell.detection.threshold_detector import ThresholdDetector
12+
from robot_cell.detection.apriltag_detection import ProcessingApriltag
13+
import numpy as np
14+
import cv2
15+
16+
def rotation_angles(matrix, order):
17+
"""
18+
input
19+
matrix = 3x3 rotation matrix (numpy array)
20+
oreder(str) = rotation order of x, y, z : e.g, rotation XZY -- 'xzy'
21+
output
22+
theta1, theta2, theta3 = rotation angles in rotation order
23+
"""
24+
r11, r12, r13 = matrix[0]
25+
r21, r22, r23 = matrix[1]
26+
r31, r32, r33 = matrix[2]
27+
28+
if order == 'xzx':
29+
theta1 = np.arctan(r31 / r21)
30+
theta2 = np.arctan(r21 / (r11 * np.cos(theta1)))
31+
theta3 = np.arctan(-r13 / r12)
32+
33+
elif order == 'xyx':
34+
theta1 = np.arctan(-r21 / r31)
35+
theta2 = np.arctan(-r31 / (r11 *np.cos(theta1)))
36+
theta3 = np.arctan(r12 / r13)
37+
38+
elif order == 'yxy':
39+
theta1 = np.arctan(r12 / r32)
40+
theta2 = np.arctan(r32 / (r22 *np.cos(theta1)))
41+
theta3 = np.arctan(-r21 / r23)
42+
43+
elif order == 'yzy':
44+
theta1 = np.arctan(-r32 / r12)
45+
theta2 = np.arctan(-r12 / (r22 *np.cos(theta1)))
46+
theta3 = np.arctan(r23 / r21)
47+
48+
elif order == 'zyz':
49+
theta1 = np.arctan(r23 / r13)
50+
theta2 = np.arctan(r13 / (r33 *np.cos(theta1)))
51+
theta3 = np.arctan(-r32 / r31)
52+
53+
elif order == 'zxz':
54+
theta1 = np.arctan(-r13 / r23)
55+
theta2 = np.arctan(-r23 / (r33 *np.cos(theta1)))
56+
theta3 = np.arctan(r31 / r32)
57+
58+
elif order == 'xzy':
59+
theta1 = np.arctan(r32 / r22)
60+
theta2 = np.arctan(-r12 * np.cos(theta1) / r22)
61+
theta3 = np.arctan(r13 / r11)
62+
63+
elif order == 'xyz':
64+
theta1 = np.arctan(-r23 / r33)
65+
theta2 = np.arctan(r13 * np.cos(theta1) / r33)
66+
theta3 = np.arctan(-r12 / r11)
67+
68+
elif order == 'yxz':
69+
theta1 = np.arctan(r13 / r33)
70+
theta2 = np.arctan(-r23 * np.cos(theta1) / r33)
71+
theta3 = np.arctan(r21 / r22)
72+
73+
elif order == 'yzx':
74+
theta1 = np.arctan(-r31 / r11)
75+
theta2 = np.arctan(r21 * np.cos(theta1) / r11)
76+
theta3 = np.arctan(-r23 / r22)
77+
78+
elif order == 'zyx':
79+
theta1 = np.arctan(r21 / r11)
80+
theta2 = np.arctan(-r31 * np.cos(theta1) / r11)
81+
theta3 = np.arctan(r32 / r33)
82+
83+
elif order == 'zxy':
84+
theta1 = np.arctan(-r12 / r22)
85+
theta2 = np.arctan(r32 * np.cos(theta1) / r22)
86+
theta3 = np.arctan(-r31 / r33)
87+
88+
theta1 = theta1 * 180 / np.pi
89+
theta2 = theta2 * 180 / np.pi
90+
theta3 = theta3 * 180 / np.pi
91+
92+
return (theta1, theta2, theta3)
93+
94+
def drawText(
95+
frame: np.ndarray, text: str, position: tuple[int, int], size: float = 1
96+
) -> None:
97+
"""
98+
Draws white text with black border to the frame.
99+
100+
Args:
101+
frame (np.ndarray): Frame into which the text will be draw.
102+
text (str): Text to draw.
103+
position (tuple[int, int]): Position on the frame in pixels.
104+
size (float): Size modifier of the text.
105+
"""
106+
107+
cv2.putText(frame, text, position, cv2.FONT_HERSHEY_SIMPLEX, size, (0, 0, 0), 4)
108+
cv2.putText(
109+
frame, text, position, cv2.FONT_HERSHEY_SIMPLEX, size, (255, 255, 255), 2
110+
)
111+
112+
def bool_str(string: str) -> bool:
113+
"""
114+
Used in argument parser to detect boolean flags written as a string.
115+
116+
Args:
117+
string (str): String to be evaluated.
118+
119+
Returns:
120+
bool: True, False, depending on contents of the string.
121+
122+
Raises:
123+
argparse.ArgumentTypeError: Error in case the string does not contain any of the expected values.
124+
"""
125+
126+
if string in ["True", "true"]:
127+
return True
128+
elif string in ["False", "false"]:
129+
return False
130+
else:
131+
raise argparse.ArgumentTypeError
132+
133+
if __name__ == '__main__':
134+
135+
parser = argparse.ArgumentParser(description="Robot cell input arguments.")
136+
parser.add_argument(
137+
"--config-file",
138+
default=ROB_CONFIG_FILE,
139+
type=str,
140+
dest="CONFIG_FILE",
141+
help="Path to configuration file",
142+
)
143+
config, _ = parser.parse_known_args()
144+
145+
# Read config file using provided path
146+
with open(config.CONFIG_FILE, "r") as file:
147+
rob_config = json.load(file)
148+
149+
# Read all other input arguments as specified inside the config file
150+
for param in rob_config.items():
151+
if isinstance(param[1]["default"], bool):
152+
parser.add_argument(
153+
param[1]["arg"],
154+
default=param[1]["default"],
155+
dest=param[0],
156+
help=param[1]["help"],
157+
type=bool_str,
158+
)
159+
elif isinstance(param[1]["default"], list):
160+
parser.add_argument(
161+
param[1]["arg"],
162+
default=param[1]["default"],
163+
dest=param[0],
164+
help=param[1]["help"],
165+
nargs=len(param[1]["default"]),
166+
type=type(param[1]["default"][0]),
167+
)
168+
elif isinstance(param[1]["default"], int):
169+
parser.add_argument(
170+
param[1]["arg"],
171+
default=param[1]["default"],
172+
dest=param[0],
173+
help=param[1]["help"],
174+
type=int,
175+
)
176+
elif isinstance(param[1]["default"], float):
177+
parser.add_argument(
178+
param[1]["arg"],
179+
default=param[1]["default"],
180+
dest=param[0],
181+
help=param[1]["help"],
182+
type=float,
183+
)
184+
elif isinstance(param[1]["default"], str):
185+
parser.add_argument(
186+
param[1]["arg"],
187+
default=param[1]["default"],
188+
dest=param[0],
189+
help=param[1]["help"],
190+
type=str,
191+
)
192+
else:
193+
print(f"[WARNING] Default value of {param[0]} config parameter not handled")
194+
rob_config = parser.parse_args()
195+
196+
# Read robot positions dictionaries from json file
197+
with open(rob_config.path_robot_positions) as file:
198+
robot_poses = json.load(file)
199+
200+
201+
# Program variables
202+
frame_count = 1 # Counter of frames for homography update
203+
text_size = 1
204+
homography = None # Homography matrix
205+
206+
# Inititalize Apriltag Detector
207+
apriltag = ProcessingApriltag()
208+
apriltag.load_world_points(rob_config.path_homography_points)
209+
210+
detector = ThresholdDetector(
211+
rob_config.hsv_ignore_vertical,
212+
rob_config.hsv_ignore_horizontal,
213+
rob_config.hsv_max_ratio_error,
214+
rob_config.hsv_white_lower,
215+
rob_config.hsv_white_upper,
216+
rob_config.hsv_brown_lower,
217+
rob_config.hsv_brown_upper,
218+
)
219+
220+
camera = DepthCamera(config_path=rob_config.path_camera_config)
221+
intrinsic = camera.intr
222+
camera_parameter = [intrinsic.fx, intrinsic.fy, intrinsic.ppx, intrinsic.ppy]
223+
fx, fy, cx, cy = camera_parameter
224+
K = np.array([fx, 0, cx, 0, fy, cy, 0, 0, 1]).reshape(3, 3)
225+
dist = np.array(camera.intr.coeffs)
226+
print(K)
227+
228+
while True:
229+
230+
# Get frames from camera
231+
success, depth_frame, rgb_frame, colorized_depth = camera.get_frames()
232+
if not success:
233+
continue
234+
235+
236+
frame_height, frame_width, frame_channel_count = rgb_frame.shape
237+
text_size = frame_height / 1000
238+
239+
# rgb_frame is used for detection, image_frame is used for graphics and displayed
240+
image_frame = rgb_frame.copy()
241+
242+
# HOMOGRAPHY UPDATE
243+
##################
244+
245+
apriltag.detect_tags(rgb_frame)
246+
homography = apriltag.compute_homog()
247+
248+
gray_frame = cv2.cvtColor(image_frame, cv2.COLOR_BGR2GRAY)
249+
(corners, ids, rejected) = cv2.aruco.detectMarkers(
250+
gray_frame,
251+
cv2.aruco.Dictionary_get(cv2.aruco.DICT_APRILTAG_36h11),
252+
parameters=cv2.aruco.DetectorParameters_create(),
253+
)
254+
255+
256+
transformation_marker = np.eye(4)
257+
258+
for (tag_corners, tag_id) in zip(corners, ids):
259+
260+
261+
if tag_id == 1:
262+
# Get (x, y) corners of the tag
263+
corners = tag_corners.reshape((4, 2))
264+
(top_left, top_right, bottom_right, bottom_left) = corners
265+
266+
top_left = (int(top_left[0]), int(top_left[1]))
267+
top_right = (int(top_right[0]), int(top_right[1]))
268+
bottom_right = (int(bottom_right[0]), int(bottom_right[1]))
269+
bottom_left = (int(bottom_left[0]), int(bottom_left[1]))
270+
271+
# Compute centroid
272+
cX = int((top_left[0] + bottom_right[0]) / 2.0)
273+
cY = int((top_left[1] + bottom_right[1]) / 2.0)
274+
275+
cv2.circle(image_frame, (cX,cY), 10, (255, 0, 0), -1)
276+
277+
# TODO : check why 3d points from depth camera and tvec doesnt corresponds
278+
marker_centroid = [cX, cY]
279+
rvec, tvec, markerPoints =cv2.aruco.estimatePoseSingleMarkers(tag_corners, 0.05, K, dist)
280+
tdp_marker_center = camera.pixel_to_3d_point(marker_centroid, camera.get_raw_depth_frame())
281+
282+
rotation_matrix, idk = cv2.Rodrigues(rvec)
283+
angle_marker_w2c = rotation_angles(rotation_matrix, 'zyx') # zxy
284+
# print(angle_marker_w2c)
285+
286+
transformation_marker[:3, :3] = rotation_matrix
287+
transformation_marker[:3,3:] = tvec.reshape(3,1)
288+
transformation_marker = np.linalg.inv(transformation_marker)
289+
290+
# marker_point = camera.pixel_to_3d_point(marker_centroid, camera.get_raw_depth_frame())
291+
# print(marker_point, tvec)
292+
293+
# TODO: save this transformation matrix
294+
295+
image_frame = apriltag.draw_tags(image_frame)
296+
detector.set_homography(homography)
297+
image_frame, detected_packets, mask = detector.detect_packet_hsv(rgb_frame,0,True)
298+
299+
300+
301+
for packet in detected_packets:
302+
# Draw packet centroid value in milimeters
303+
text_centroid = "X: {:.2f}, Y: {:.2f} (mm)".format(
304+
packet.centroid_mm.x, packet.centroid_mm.y
305+
)
306+
drawText(
307+
image_frame,
308+
text_centroid,
309+
(packet.centroid_px.x + 10, packet.centroid_px.y + int(80 * text_size)),
310+
text_size,
311+
)
312+
313+
pixel = [packet.centroid_px.x, packet.centroid_px.y]
314+
threed_point = np.array(camera.pixel_to_3d_point(pixel, camera.get_raw_depth_frame())).reshape(3, 1)
315+
threed_point = np.append(threed_point, 1)
316+
317+
318+
transformed_3d_point = np.matmul(transformation_marker, threed_point)
319+
print(transformed_3d_point * 1000)
320+
321+
image_frame = cv2.resize(image_frame, (960, 540))
322+
cv2.imshow('window_name', image_frame)
323+
324+
# Press Q on keyboard to exit
325+
if cv2.waitKey(25) & 0xFF == ord('q'):
326+
break
327+
328+
# closing all open windows
329+
cv2.destroyAllWindows()
330+
331+

cv_pick_place/robot_cell/detection/realsense_depth.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def __init__(
120120
# Maximal supported Depth stream resolution of D435 camera is 1280 x 720
121121
# Maximal supported RGB stream resolution of D435 camera is 1920 x 1080
122122
self.config.enable_stream(rs.stream.depth, 1280, 720, rs.format.z16, 30)
123-
self.config.enable_stream(rs.stream.color, 1920, 1080, rs.format.bgr8, 30)
123+
self.config.enable_stream(rs.stream.color, 1280, 720, rs.format.bgr8, 30)
124124

125125
# Create object for aligning depth frame to RGB frame, so that they have equal resolution
126126
self.align = rs.align(rs.stream.color)
@@ -135,6 +135,12 @@ def __init__(
135135
# Start video stream
136136
self.profile = self.pipeline.start(self.config)
137137

138+
# Get intrinsic parameter
139+
profile = self.profile.get_stream(rs.stream.depth) # Fetch stream profile for depth stream
140+
self.intr = profile.as_video_stream_profile().get_intrinsics() # Downcast to video_stream_profile and fetch intrinsics
141+
# self.depth_scale = profile.get_device().first_depth_sensor().get_depth_scale()
142+
143+
138144
def get_frames(self) -> tuple[bool, np.ndarray, np.ndarray, np.ndarray]:
139145
"""
140146
Reads and processes frames from connected camera.
@@ -152,6 +158,7 @@ def get_frames(self) -> tuple[bool, np.ndarray, np.ndarray, np.ndarray]:
152158

153159
# Extract RGB and depth frames from frameset
154160
depth_frame = frameset.get_depth_frame()
161+
depth_frame_raw = depth_frame
155162
color_frame = frameset.get_color_frame()
156163

157164
if not depth_frame or not color_frame:
@@ -160,6 +167,7 @@ def get_frames(self) -> tuple[bool, np.ndarray, np.ndarray, np.ndarray]:
160167
# Apply hole filling filter
161168
depth_frame = self.hole_filling.process(depth_frame)
162169

170+
163171
depth_frame = np.asanyarray(depth_frame.get_data())
164172
color_frame = np.asanyarray(color_frame.get_data())
165173

@@ -171,6 +179,19 @@ def get_frames(self) -> tuple[bool, np.ndarray, np.ndarray, np.ndarray]:
171179

172180
return True, depth_frame, color_frame, colorized_depth_frame
173181

182+
def get_raw_depth_frame(self):
183+
frameset = self.pipeline.wait_for_frames()
184+
frameset = self.align.process(frameset)
185+
return frameset.get_depth_frame()
186+
187+
def pixel_to_3d_point(self, pixel, depth_frame):
188+
189+
dist = depth_frame.get_distance(pixel[0], pixel[1])
190+
coordinates_3d = rs.rs2_deproject_pixel_to_point(self.intr, [pixel[0], pixel[1]], dist)
191+
coordinates_3d = [coordinates_3d[0],coordinates_3d[1],coordinates_3d[2]]
192+
193+
return coordinates_3d
194+
174195
def release(self):
175196
"""
176197
Disconnects the camera.

requirements.txt

-6 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)