import pyrealsense2 as rs import numpy as np import cv2 import os import time import math from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.parametertree import Parameter, ParameterTree, parameterTypes import pyqtgraph as pg from shapely.geometry import LineString from shapely.affinity import scale from queue import Queue import aruco class FPSCounter: def __init__(self): self.fps_start_time = time.time() self.fps_counter = 0 self.fps_display_rate = 1.0 def get_fps(self): self.fps_counter += 1 delta_t = time.time() - self.fps_start_time if delta_t > self.fps_display_rate: self.fps_counter = 0 self.fps_start_time = time.time() return self.fps_counter / delta_t ## test add/remove ## this group includes a menu allowing the user to add new parameters into its child list class RobotMarkerGroup(parameterTypes.GroupParameter): def __init__(self, **opts): opts['type'] = 'group' opts['addText'] = "Add Marker" opts['addList'] = ['Robot'] parameterTypes.GroupParameter.__init__(self, **opts) def addNew(self, typ): current_robots = self.children() current_indices = [int(r.name().split(' ')[1]) for r in current_robots] new_index = len(current_indices) + 1 val = 0 self.addChild(dict(name=f"Robot {new_index}", type='int', value=val, removable=False, renamable=True)) class CornerMarkerGroup(parameterTypes.GroupParameter): def __init__(self, **opts): opts['type'] = 'group' opts['addText'] = "Add Marker" opts['addList'] = ['Corner'] parameterTypes.GroupParameter.__init__(self, **opts) def addNew(self, typ): current_corners = self.children() current_chars = [str(r.name().split(' ')[1]) for r in current_corners] new_char = chr(ord(current_chars[-1]) + 1) val = 0 self.addChild(dict(name=f"Corner {new_char}", type='int', value=val, removable=False)) class ArucoEstimator: def __init__(self, robot_marker_ids=None, use_realsense=True, grid_columns=7, grid_rows=4): self.app = QtGui.QApplication([]) ## Create window with GraphicsView widget self.win = QtGui.QWidget() self.layout = QtGui.QGridLayout() self.win.setLayout(self.layout) self.win.keyPressEvent = self.handleKeyPressEvent self.win.setWindowTitle('ArucoEstimator') self.plotwidget = pg.PlotWidget() self.layout.addWidget(self.plotwidget) ## lock the aspect ratio so pixels are always square self.plotwidget.setAspectLocked(True) self.plotwidget.getPlotItem().getAxis('left').hide() self.plotwidget.getPlotItem().getAxis('bottom').hide() ## Create image item self.img = pg.ImageItem(border='w') self.img.setLevels([[0, 255], [0, 255], [0, 255]]) self.img.mouseClickEvent = self.handleMouseEvent self.plotwidget.addItem(self.img) # fps display self.fps_counter = FPSCounter() self.fps_overlay = pg.TextItem('fps = 0', color=(255, 255, 0), anchor=(0,1)) self.plotwidget.addItem(self.fps_overlay) self.invert_grayscale = False self.draw_grid = True self.draw_markers = True self.draw_marker_coordinate_system = False self.corner_marker_size = 0.075 self.corner_marker_ids = { 'a': 0, 'b': 1, 'c': 2, 'd': 3 } self.corner_estimates = { 'a': {'pixel_coordinate': None, 'x': None, 'y': None, 'n_estimates': 0}, 'b': {'pixel_coordinate': None, 'x': None, 'y': None, 'n_estimates': 0}, 'c': {'pixel_coordinate': None, 'x': None, 'y': None, 'n_estimates': 0}, 'd': {'pixel_coordinate': None, 'x': None, 'y': None, 'n_estimates': 0}, } self.grid_columns = grid_columns self.grid_rows = grid_rows self.robot_marker_size = 0.07 if robot_marker_ids is None: robot_marker_ids = [] self.robot_marker_ids = robot_marker_ids self.robot_marker_estimates = dict([(marker_id, {'t': None, 'x': None, 'y': None, 'angle': None}) for marker_id in self.robot_marker_ids]) robot_marker_group = [{'name': f'Robot {ind}', 'type': 'int', 'value': marker_id} for ind, marker_id in enumerate(self.robot_marker_ids, 1)] corner_marker_group = [{'name': f'Corner {letter}', 'type': 'int', 'value': marker_id} for letter, marker_id in self.corner_marker_ids.items()] self.threshold = 10.0 # parameter params_spec = [ {'name': 'Corner marker size', 'type': 'float', 'value': self.corner_marker_size, 'siPrefix': True, 'suffix': 'm', 'limits': (0.01, 1), 'step': 0.001}, {'name': 'Robot marker size', 'type': 'float', 'value': self.robot_marker_size, 'siPrefix': True, 'suffix': 'm', 'limits': (0.01, 1), 'step': 0.001}, {'name': 'Draw markers', 'type': 'bool', 'value': self.draw_markers}, {'name': 'Draw marker coordinate system', 'type': 'bool', 'value': self.draw_marker_coordinate_system}, {'name': 'Threshold', 'type': 'float', 'value': self.threshold}, {'name': 'Invert grayscale', 'type': 'bool', 'value': self.invert_grayscale, 'tip': "Invert grayscale image before marker detection"}, {'name': 'Show FPS', 'type': 'bool', 'value': True, 'tip': "Display frames per second counter"}, {'name': 'Draw grid', 'type': 'bool', 'value': self.draw_grid, 'tip': "Draw grid spanned by the markers 0 - 3"}, {'name': 'Grid columns', 'type': 'int', 'value': self.grid_columns, 'tip': "Number of columns for the grid"}, {'name': 'Grid rows', 'type': 'int', 'value': self.grid_rows, 'tip': "Number of rows for the grid"}, {'name': 'Display mode', 'type': 'list', 'values': ['color', 'grayscale'], 'value': 'color', 'tip': "Display mode for the video"}, {'name': 'Autoexposure', 'type': 'bool', 'value': True}, {'name': 'Controlled robot', 'type': 'list', 'values': self.robot_marker_ids, 'tip': 'Robot to control'}, RobotMarkerGroup(name="Robot markers", children=robot_marker_group), CornerMarkerGroup(name="Corner markers", children=corner_marker_group), ] self.params = Parameter.create(name='params', type='group', children=params_spec) self.params.param('Invert grayscale').sigValueChanged.connect(lambda _, v: self.__setattr__('invert_grayscale', v)) self.params.param('Threshold').sigValueChanged.connect(lambda _, v: self.__setattr__('threshold', v)) self.params.param('Draw markers').sigValueChanged.connect(lambda _, v: self.__setattr__('draw_markers', v)) self.params.param('Draw marker coordinate system').sigValueChanged.connect(lambda _, v: self.__setattr__('draw_marker_coordinate_system', v)) self.params.param('Draw grid').sigValueChanged.connect(lambda _, v: self.__setattr__('draw_grid', v)) self.params.param('Grid columns').sigValueChanged.connect(lambda _, v: self.__setattr__('grid_columns', v)) self.params.param('Grid rows').sigValueChanged.connect(lambda _, v: self.__setattr__('grid_rows', v)) self.params.param('Corner marker size').sigValueChanged.connect(lambda _, v: self.__setattr__('corner_marker_size', v)) self.params.param('Robot marker size').sigValueChanged.connect(lambda _, v: self.__setattr__('robot_marker_size', v)) self.params.param('Show FPS').sigValueChanged.connect(lambda _, v: self.fps_overlay.show() if v else self.fps_overlay.hide()) self.params.param('Autoexposure').sigValueChanged.connect(lambda _, v: self.set_autoexposure(v)) self.params.param('Controlled robot').sigValueChanged.connect(lambda _, v: self.event_queue.put(('controlled_robot', {'robot_id': v}))) self.paramtree = ParameterTree() self.paramtree.setParameters(self.params, showTop=False) self.layout.addWidget(self.paramtree) self.win.show() ## show widget alone in its own window self.event_queue = Queue() if use_realsense: # check if realsense camera is connected # Configure depth and color streams self.pipeline = rs.pipeline() config = rs.config() # config.enable_stream(rs.stream.color, 1920, 1080, rs.format.bgr8, 30) config.enable_stream(rs.stream.color, 1280, 720, rs.format.bgr8, 30) # Start streaming self.pipeline.start(config) # enable auto exposure self.set_autoexposure(self.params['Autoexposure']) camera_intrinsics = self.pipeline.get_active_profile().get_stream( rs.stream.color).as_video_stream_profile().get_intrinsics() self.camera_matrix = np.zeros((3, 3)) self.camera_matrix[0][0] = camera_intrinsics.fx self.camera_matrix[1][1] = camera_intrinsics.fy self.camera_matrix[0][2] = camera_intrinsics.ppx self.camera_matrix[1][2] = camera_intrinsics.ppy self.dist_coeffs = np.array(camera_intrinsics.coeffs) # more info: https://dev.intelrealsense.com/docs/projection-in-intel-realsense-sdk-20 else: # use other camera self.cv_camera = cv2.VideoCapture(0) self.pipeline = None # create detector and get parameters # print detector parameters # print("detector params:") # print(self.detector_params) # for val in dir(self.detector_params): # if not val.startswith("__"): # print("\t{} : {}".format(val, self.detector_params.__getattribute__(val))) self.camparam = aruco.CameraParameters() if use_realsense: self.camparam.readFromXMLFile(os.path.join(os.path.dirname(__file__), "realsense.yml")) else: self.camparam.readFromXMLFile(os.path.join(os.path.dirname(__file__), "dfk72_6mm_param2.yml")) self.drag_line_end = None self.drag_line_start = None self.previous_click = None def set_autoexposure(self, v): if self.pipeline is not None: color_sensor = self.pipeline.get_active_profile().get_device().query_sensors()[1] color_sensor.set_option(rs.option.enable_auto_exposure, v) print(color_sensor.get_option(rs.option.enable_auto_exposure)) def compute_clicked_position(self, px, py): if self.all_corners_detected(): # inter/extrapolate from clicked point to marker position px1 = self.corner_estimates['a']['pixel_coordinate'][0] px3 = self.corner_estimates['c']['pixel_coordinate'][0] py1 = self.corner_estimates['a']['pixel_coordinate'][1] py3 = self.corner_estimates['c']['pixel_coordinate'][1] x1 = self.corner_estimates['a']['x'] x3 = self.corner_estimates['c']['x'] y1 = self.corner_estimates['a']['y'] y3 = self.corner_estimates['c']['y'] alpha = (px - px1) / (px3 - px1) beta = (py - py1) / (py3 - py1) print(f"alpha = {alpha}, beta = {beta}") target_x = x1 + alpha * (x3 - x1) target_y = y1 + beta * (y3 - y1) target = np.array([target_x, target_y]) else: print("not all markers have been detected!") target = np.array([px, -py]) return target def handleMouseEvent(self, event): # get click position as distance to top-left corner of the image px = event.pos().x() py = self.img.height() - event.pos().y() print(f"px = {px}, py = {py}") if event.button() == QtCore.Qt.MouseButton.LeftButton: # self.drag_line_start = (px, py) # elif event == cv2.EVENT_LBUTTONUP: self.drag_line_end = (px, py) self.drag_line_start = (px, py) target_pos = self.compute_clicked_position(self.drag_line_start[0], self.drag_line_start[1]) if self.drag_line_start != self.drag_line_end: # compute target angle for clicked position facing_pos = self.compute_clicked_position(px, py) v = facing_pos - target_pos target_angle = math.atan2(v[1], v[0]) else: # determine angle from previously clicked pos (= self.drag_line_end) if self.previous_click is not None: previous_pos = self.compute_clicked_position(self.previous_click[0], self.previous_click[1]) v = target_pos - previous_pos target_angle = math.atan2(v[1], v[0]) else: target_angle = 0.0 target = np.array([target_pos[0], target_pos[1], target_angle]) print(target) self.previous_click = (px, py) self.event_queue.put(('click', {'x': target[0], 'y': target[1], 'angle': target[2]})) self.drag_line_start = None elif event == cv2.EVENT_MOUSEMOVE: if self.drag_line_start is not None: self.drag_line_end = (px, py) def process_frame(self): if self.pipeline: frames = self.pipeline.wait_for_frames() color_frame = frames.get_color_frame() # Convert images to numpy arrays color_image = np.asanyarray(color_frame.get_data()) else: # Capture frame-by-frame ret, color_image = self.cv_camera.read() t_image = time.time() gray = cv2.cvtColor(color_image, cv2.COLOR_BGR2GRAY) if self.invert_grayscale: cv2.bitwise_not(gray, gray) detector = aruco.MarkerDetector() detector.setDetectionMode(aruco.DM_VIDEO_FAST, 0.01) #detector_params = detector.getParameters() # run aruco marker detection detected_markers = detector.detect(gray) # detected_markers2 = detector.detect(gray) #gray = detector.getThresholdedImage() display_mode = self.params.param('Display mode').value() #print(f"detected_markers = {[marker.id for marker in detected_markers]}") #print("threshold = ", self.threshold) # extract data for detected markers detected_marker_data = {} for marker in detected_markers: if marker.id >= 0: detected_marker_data[marker.id] = {'marker_center': marker.getCenter()} if marker.id in self.corner_marker_ids.values(): marker.calculateExtrinsics(self.corner_marker_size, self.camparam) else: marker.calculateExtrinsics(self.robot_marker_size, self.camparam) detected_marker_data[marker.id]['Rvec'] = marker.Rvec detected_marker_data[marker.id]['Tvec'] = marker.Tvec if self.draw_markers: marker.draw(color_image, np.array([255, 255, 255]), 2, True) if self.draw_marker_coordinate_system: aruco.CvDrawingUtils.draw3dAxis(color_image, self.camparam, marker.Rvec, marker.Tvec, .1) # store data for marker_id, data in detected_marker_data.items(): self.update_estimate(marker_id, data['marker_center'], data['Rvec'], data['Tvec'], t_image) # draw grid if self.draw_grid: color_image = self.draw_grid_lines(color_image, detected_marker_data) color_image = self.draw_robot_pos(color_image, detected_marker_data) # draw drag if self.drag_line_start is not None and self.drag_line_end is not None: color_image = cv2.line(color_image, self.drag_line_start, self.drag_line_end, color=(0, 0, 255), thickness=2) # compute frame rate self.fps_overlay.setText(f"fps = {self.fps_counter.get_fps():.1f}") # Show images color_image_rgb = cv2.cvtColor(color_image, cv2.COLOR_BGR2RGB) # convert to RGB if display_mode == 'color': self.img.setImage(np.transpose(np.flipud(color_image_rgb), axes=(1, 0, 2))) elif display_mode == 'grayscale': self.img.setImage(np.transpose(np.flipud(gray))) QtCore.QTimer.singleShot(1, self.process_frame) def handleKeyPressEvent(self, ev): key = ev.key() self.event_queue.put(('key', key)) if key == QtCore.Qt.Key_Q: if self.pipeline is not None: # Stop streaming self.pipeline.stop() self.app.quit() elif key == QtCore.Qt.Key_I: self.invert_grayscale = not self.invert_grayscale def update_estimate(self, marker_id, pixel_coord_center, rvec, tvec, t_image): # update the marker estimate with new data if marker_id in self.corner_marker_ids.values(): # for corner markers we compute the mean of all measurements s.t. the position stabilizes over time # (we assume the markers do not move) # get corresponding corner to the detected marker corner = next(filter(lambda key: self.corner_marker_ids[key] == marker_id, self.corner_marker_ids.keys())) old_estimate_x = self.corner_estimates[corner]['x'] old_estimate_y = self.corner_estimates[corner]['y'] n_estimates = self.corner_estimates[corner]['n_estimates'] x = tvec[0][0] y = -tvec[1][0] # flip y coordinate if not any([old_estimate_x is None, old_estimate_y is None]): new_estimate_x = (n_estimates * old_estimate_x + x) / (n_estimates + 1) # weighted update new_estimate_y = (n_estimates * old_estimate_y + y) / (n_estimates + 1) else: new_estimate_x = x # first estimate new_estimate_y = y self.corner_estimates[corner]['t'] = t_image self.corner_estimates[corner]['x'] = new_estimate_x self.corner_estimates[corner]['y'] = new_estimate_y self.corner_estimates[corner]['n_estimates'] = n_estimates + 1 self.corner_estimates[corner]['pixel_coordinate'] = pixel_coord_center elif marker_id in self.robot_marker_ids: # for robot markers we extract x and y position as well as the angle # here we could also implement a filter x = tvec[0][0] y = -tvec[1][0] # flip y coordinate # compute angle rot_mat, _ = cv2.Rodrigues(rvec) pose_mat = cv2.hconcat((rot_mat, tvec)) _, _, _, _, _, _, euler_angles = cv2.decomposeProjectionMatrix(pose_mat) angle = -euler_angles[2][0] * np.pi / 180.0 self.robot_marker_estimates[marker_id] = {'t': float(t_image), 'x': float(x), 'y': float(y), 'angle': float(angle)} def all_corners_detected(self): # checks if all corner markers have been detected at least once return not any([estimate['n_estimates'] == 0 for estimate in self.corner_estimates.values()]) def all_robots_detected(self): # checks if all robot markers have been detected at least once return not any([estimate['t'] is None for estimate in self.robot_marker_estimates.values()]) def get_pos_from_grid_point(self, x, y, dimx, dimy, orientation=None): """ returns the position for the given grid point based on the current corner estimates :param x: x position on the grid ( 0 &le x < number of grid columns) :param y: y position on the grid ( 0 &le x < number of grid rows) :param orientation: (optional) orientation in the given grid cell (one of ^, >, v, < ) :return: numpy array with corresponding real world x- and y-position if orientation was specified the array also contains the matching angle for the orientation """ if not self.all_corners_detected(): #raise RuntimeError("not all corner markers have been detected yet") a = np.array([0,1]) b = np.array([1,1]) c = np.array([1,0]) d = np.array([0,0]) else: # compute column line a = np.array([self.corner_estimates['a']['x'], self.corner_estimates['a']['y']]) b = np.array([self.corner_estimates['b']['x'], self.corner_estimates['b']['y']]) c = np.array([self.corner_estimates['c']['x'], self.corner_estimates['c']['y']]) d = np.array([self.corner_estimates['d']['x'], self.corner_estimates['d']['y']]) x_frac = (x + 0.5) / dimx y_frac = (y + 0.5) / dimy vab = b - a vdc = c - d column_line_top = a + x_frac * vab column_line_bottom = d + x_frac * vdc vad = d - a vbc = c - b row_line_top = a + y_frac * vad row_line_bottom = b + y_frac * vbc column_line_orig = LineString([column_line_top, column_line_bottom]) row_line_orig = LineString([row_line_top, row_line_bottom]) column_line = scale(column_line_orig, 10, 10) row_line = scale(row_line_orig, 10, 10) int_pt = column_line.intersection(row_line) point_of_intersection = np.array([int_pt.x, int_pt.y]) if orientation is not None: # compute angle corresponding to the orientation w.r.t. the grid # TODO: test this code angle_ab = np.arctan2(vab[1], vab[0]) angle_dc = np.arctan2(vdc[1], vdc[0]) angle_ad = np.arctan2(vad[1], vad[0]) angle_bc = np.arctan2(vbc[1], vbc[0]) angle = 0.0 if orientation == '>': angle = y_frac * angle_ab + (1 - y_frac) * angle_dc elif orientation == '<': angle = y_frac * angle_ab + (1 - y_frac) * angle_dc + np.pi elif orientation == 'v': angle = x_frac * angle_ad + (1 - x_frac) * angle_bc elif orientation == '^': angle = x_frac * angle_ad + (1 - x_frac) * angle_bc + np.pi return np.array((point_of_intersection[0], point_of_intersection[1], angle)) else: return point_of_intersection def get_grid_point_from_pos(self): # TODO return the nearest grid point for the given position estimate pass def print_corner_estimates(self): for key, value in self.corner_estimates.items(): if value['n_estimates'] != 0: print(f"corner marker {key} at pos ({value['x']},{value['y']})") def draw_corner_line(self, frame, corner_1, corner_2): # draws a line between the given markers onto the given frame corner_1_center = self.corner_estimates[corner_1]['pixel_coordinate'] corner_2_center = self.corner_estimates[corner_2]['pixel_coordinate'] if corner_1_center is not None and corner_2_center is not None: frame = cv2.line(frame, tuple(corner_1_center.astype(int)), tuple(corner_2_center.astype(int)), color=(0, 0, 255), thickness=2) return frame def draw_grid_lines(self, frame, detected_marker_data): # draws a grid onto the given frame frame = self.draw_corner_line(frame, 'a', 'b') frame = self.draw_corner_line(frame, 'b', 'c') frame = self.draw_corner_line(frame, 'c', 'd') frame = self.draw_corner_line(frame, 'd', 'a') if not any([estimate['pixel_coordinate'] is None for estimate in self.corner_estimates.values()]): # compute outlines of board a = self.corner_estimates['a']['pixel_coordinate'] b = self.corner_estimates['b']['pixel_coordinate'] c = self.corner_estimates['c']['pixel_coordinate'] d = self.corner_estimates['d']['pixel_coordinate'] # draw vertical lines vab = b - a vdc = c - d for x in range(1, self.grid_columns): column_line_top = a + x / self.grid_columns * vab column_line_bottom = d + x / self.grid_columns * vdc frame = cv2.line(frame, tuple(column_line_top.astype(int)), tuple(column_line_bottom.astype(int)), color=(0, 255, 0), thickness=1) # draw horizontal lines vad = d - a vbc = c - b for y in range(1, self.grid_rows): row_line_top = a + y / self.grid_rows * vad row_line_bottom = b + y / self.grid_rows * vbc frame = cv2.line(frame, tuple(row_line_top.astype(int)), tuple(row_line_bottom.astype(int)), color=(0, 255, 0), thickness=1) return frame def get_robot_state_estimate(self, marker_id): if marker_id in self.robot_marker_estimates: if self.robot_marker_estimates[marker_id]['t'] is not None: return np.array([self.robot_marker_estimates[marker_id]['x'], self.robot_marker_estimates[marker_id]['y'], self.robot_marker_estimates[marker_id]['angle']]) else: print(f"error: no estimate available for robot {marker_id}") return None else: print(f"error: invalid robot id {marker_id}") return None def draw_robot_pos(self, frame, detected_marker_data): # draws information about the robot positions onto the given frame robot_corners_pixel_coords = {} for marker_id, estimate in self.robot_marker_estimates.items(): if marker_id in detected_marker_data.keys(): robot_corners_pixel_coords[marker_id] = tuple(detected_marker_data[marker_id]['marker_center']) for marker_id, coord in robot_corners_pixel_coords.items(): x = self.robot_marker_estimates[marker_id]['x'] y = self.robot_marker_estimates[marker_id]['y'] angle = self.robot_marker_estimates[marker_id]['angle'] #frame = cv2.putText(frame, "pos = ({:5.3f}, {:5.3f}), ang = {:5.3f}".format(x, y, angle), coord, # cv2.FONT_HERSHEY_SIMPLEX, 0.50, (0, 255, 0)) frame = cv2.putText(frame, f"{marker_id}", coord, cv2.FONT_HERSHEY_PLAIN, 2, (0, 255, 255), thickness=4) return frame if __name__ == "__main__": estimator = ArucoEstimator(use_realsense=True, robot_marker_ids=[11, 12, 13, 14]) estimator.process_frame() import sys if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): QtGui.QApplication.instance().exec_()