class BaseCanvas(QWidget, metaclass=QABCMeta): """The subclass can draw a blank canvas more easier.""" @abstractmethod def __init__(self, parent: QWidget): """Set the parameters for drawing.""" super(BaseCanvas, self).__init__(parent) self.setSizePolicy( QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) self.setFocusPolicy(Qt.StrongFocus) self.setMouseTracking(True) self.painter = QPainter() # Origin coordinate self.ox = self.width() / 2 self.oy = self.height() / 2 # Canvas zoom rate self.zoom = 1. # Joint size self.joint_size = 5 # Canvas line width self.link_width = 3 self.path_width = 3 # Font size self.font_size = 15 # Show point mark or dimension self.show_ticks = _TickMark.SHOW self.show_point_mark = True self.show_dimension = True # Path track self.path = _PathOption() # Path solving self.ranges: Dict[str, QRectF] = {} self.target_path: Dict[int, Sequence[_Coord]] = {} self.show_target_path = False # Background self.background = QImage() self.background_opacity = 1. self.background_scale = 1. self.background_offset = QPointF(0, 0) # Monochrome mode self.monochrome = False # Grab mode self.__grab_mode = False def switch_grab(self) -> None: """Start grab mode.""" self.__grab_mode = not self.__grab_mode @staticmethod def zoom_factor(width: int, height: int, x_right: float, x_left: float, y_top: float, y_bottom: float) -> float: """Calculate the zoom factor.""" x_diff = x_left - x_right y_diff = y_top - y_bottom x_diff = x_diff if x_diff else 1. y_diff = y_diff if y_diff else 1. if width / x_diff < height / y_diff: return width / x_diff else: return height / y_diff @abstractmethod def paintEvent(self, event: QPaintEvent) -> None: """Using a QPainter under 'self', so just change QPen or QBrush before painting. """ if not self.__grab_mode: self.painter.begin(self) self.painter.fillRect(event.rect(), QBrush(Qt.white)) # Translation self.painter.translate(self.ox, self.oy) # Background if not self.background.isNull(): rect = self.background.rect() self.painter.setOpacity(self.background_opacity) self.painter.drawImage( QRectF( self.background_offset * self.zoom, QSizeF(rect.width(), rect.height()) * self.background_scale * self.zoom), self.background, QRectF(rect)) self.painter.setOpacity(1) # Show frame pen = QPen(Qt.blue) pen.setWidth(1) self.painter.setPen(pen) self.painter.setFont(QFont("Arial", self.font_size)) # Draw origin lines if self.show_ticks not in {_TickMark.SHOW, _TickMark.SHOW_NUM}: return pen.setColor(Qt.gray) self.painter.setPen(pen) x_l = -self.ox x_r = self.width() - self.ox self.painter.drawLine(QPointF(x_l, 0), QPointF(x_r, 0)) y_t = self.height() - self.oy y_b = -self.oy self.painter.drawLine(QPointF(0, y_b), QPointF(0, y_t)) def indexing(v: float) -> int: """Draw tick.""" return int(v / self.zoom - v / self.zoom % 5) # Draw tick for x in range(indexing(x_l), indexing(x_r) + 1, 5): if x == 0: continue is_ten = x % 10 == 0 end = QPointF(x * self.zoom, -10 if is_ten else -5) self.painter.drawLine(QPointF(x, 0) * self.zoom, end) if self.show_ticks == _TickMark.SHOW_NUM and is_ten: self.painter.drawText(end + QPointF(0, 3), f"{x}") for y in range(indexing(y_b), indexing(y_t) + 1, 5): if y == 0: continue is_ten = y % 10 == 0 end = QPointF(10 if is_ten else 5, y * self.zoom) self.painter.drawLine(QPointF(0, y) * self.zoom, end) if self.show_ticks == _TickMark.SHOW_NUM and is_ten: self.painter.drawText(end + QPointF(3, 0), f"{-y}") # Please to call the "end" method when ending paint event. def draw_circle(self, p: QPointF, r: float) -> None: """Draw circle.""" self.painter.drawEllipse(p, r, r) def draw_point(self, i: int, cx: float, cy: float, fixed: bool, color: Optional[Tuple[int, int, int]], mul: int = 1) -> None: """Draw a joint.""" if self.monochrome or color is None: color = Qt.black else: color = QColor(*color) pen = QPen(color) pen.setWidth(2) self.painter.setPen(pen) x = cx * self.zoom y = cy * -self.zoom if fixed: # Draw a triangle below self.painter.drawPolygon( QPointF(x, y), QPointF(x - self.joint_size, y + 2 * self.joint_size), QPointF(x + self.joint_size, y + 2 * self.joint_size)) r = self.joint_size for _ in range(1 if mul < 1 else mul): self.draw_circle(QPointF(x, y), r) r += 5 if not self.show_point_mark: return pen.setColor(Qt.darkGray) pen.setWidth(2) self.painter.setPen(pen) text = f"[Point{i}]" if self.show_dimension: text += f":({cx:.02f}, {cy:.02f})" self.painter.drawText(QPointF(x, y) + QPointF(6, -6), text) def draw_ranges(self) -> None: """Draw rectangle ranges.""" pen = QPen() pen.setWidth(5) for i, (tag, rect) in enumerate(self.ranges.items()): range_color = QColor(color_num(i + 1)) range_color.setAlpha(30) self.painter.setBrush(range_color) range_color.setAlpha(255) pen.setColor(range_color) self.painter.setPen(pen) cx = rect.x() * self.zoom cy = rect.y() * -self.zoom if rect.width(): self.painter.drawRect( QRectF(QPointF(cx, cy), QSizeF(rect.width(), rect.height()) * self.zoom)) else: self.draw_circle(QPointF(cx, cy), 3) range_color.setAlpha(255) pen.setColor(range_color) self.painter.setPen(pen) self.painter.drawText(QPointF(cx, cy) + QPointF(6, -6), tag) self.painter.setBrush(Qt.NoBrush) def draw_target_path(self) -> None: """Draw solving path.""" pen = QPen() pen.setWidth(self.path_width) for i, n in enumerate(sorted(self.target_path)): path = self.target_path[n] if self.monochrome: line, dot = target_path_style(0) else: line, dot = target_path_style(i + 1) pen.setColor(line) self.painter.setPen(pen) if len(path) == 1: x, y = path[0] p = QPointF(x, -y) * self.zoom self.painter.drawText(p + QPointF(6, -6), f"P{n}") pen.setColor(dot) self.painter.setPen(pen) self.draw_circle(p, self.joint_size) else: painter_path = QPainterPath() for j, (x, y) in enumerate(path): p = QPointF(x, -y) * self.zoom self.draw_circle(p, self.joint_size) if j == 0: self.painter.drawText(p + QPointF(6, -6), f"P{n}") painter_path.moveTo(p) else: x2, y2 = path[j - 1] self.__draw_arrow(x, -y, x2, -y2, zoom=True) painter_path.lineTo(p) pen.setColor(line) self.painter.setPen(pen) self.painter.drawPath(painter_path) for x, y in path: pen.setColor(dot) self.painter.setPen(pen) self.draw_circle( QPointF(x, -y) * self.zoom, self.joint_size) self.painter.setBrush(Qt.NoBrush) def __draw_arrow(self, x1: float, y1: float, x2: float, y2: float, *, zoom: bool = False, text: str = '') -> None: """Front point -> Back point""" if zoom: x1 *= self.zoom y1 *= self.zoom x2 *= self.zoom y2 *= self.zoom a = atan2(y2 - y1, x2 - x1) x1 = (x1 + x2) / 2 - 7.5 * cos(a) y1 = (y1 + y2) / 2 - 7.5 * sin(a) first_point = QPointF(x1, y1) self.painter.drawLine( first_point, QPointF(x1 + 15 * cos(a + radians(20)), y1 + 15 * sin(a + radians(20)))) self.painter.drawLine( first_point, QPointF(x1 + 15 * cos(a - radians(20)), y1 + 15 * sin(a - radians(20)))) if not text: return # Font font = self.painter.font() font_copy = QFont(font) font.setBold(True) font.setPointSize(font.pointSize() + 8) self.painter.setFont(font) # Color pen = self.painter.pen() color = pen.color() pen.setColor(color.darker()) self.painter.setPen(pen) self.painter.drawText(first_point, text) pen.setColor(color) self.painter.setPen(pen) self.painter.setFont(font_copy) def draw_curve(self, path: Sequence[_Coord]) -> None: """Draw path as curve.""" if len(set(path)) < 2: return painter_path = QPainterPath() error = False for i, (x, y) in enumerate(path): if isnan(x): error = True self.painter.drawPath(painter_path) painter_path = QPainterPath() else: p = QPointF(x, -y) * self.zoom if i == 0: painter_path.moveTo(p) self.draw_circle(p, 2) continue if error: painter_path.moveTo(p) error = False else: painter_path.lineTo(p) self.painter.drawPath(painter_path) def draw_dot(self, path: Sequence[_Coord]) -> None: """Draw path as dots.""" if len(set(path)) < 2: return for i, (x, y) in enumerate(path): if isnan(x): continue p = QPointF(x, -y) * self.zoom if i == 0: self.draw_circle(p, 2) else: self.painter.drawPoint(p) def solution_polygon( self, func: str, args: Sequence[str], target: str, pos: Sequence[VPoint]) -> Tuple[List[QPointF], QColor]: """Get solution polygon.""" if func == 'PLLP': color = QColor(121, 171, 252) params = [args[0], args[-1]] elif func == 'PLAP': color = QColor(249, 84, 216) params = [args[0]] else: if func == 'PLPP': color = QColor(94, 255, 185) else: # PXY color = QColor(249, 175, 27) params = [args[0]] params.append(target) tmp_list = [] for name in params: try: index = int(name.replace('P', '')) except ValueError: continue else: vpoint = pos[index] tmp_list.append(QPointF(vpoint.cx, -vpoint.cy) * self.zoom) return tmp_list, color def draw_solution(self, func: str, args: Sequence[str], target: str, pos: Sequence[VPoint]) -> None: """Draw the solution triangle.""" points, color = self.solution_polygon(func, args, target, pos) color.setAlpha(150) pen = QPen(color) pen.setWidth(self.joint_size) self.painter.setPen(pen) def draw_arrow(index: int, text: str) -> None: """Draw arrow.""" self.__draw_arrow(points[-1].x(), points[-1].y(), points[index].x(), points[index].y(), text=text) draw_arrow(0, args[1]) if func == 'PLLP': draw_arrow(1, args[2]) color.setAlpha(30) self.painter.setBrush(QBrush(color)) self.painter.drawPolygon(QPolygonF(points)) self.painter.setBrush(Qt.NoBrush) @Slot(int) def set_show_ticks(self, show: int): """Set the appearance of tick mark.""" self.show_ticks = _TickMark(show + 1) self.update() @Slot(bool) def set_monochrome_mode(self, monochrome: bool) -> None: """Set monochrome mode.""" self.monochrome = monochrome self.update()
class UEyeView(QQuickPaintedItem): runningChanged = Signal(bool) camIdChanged = Signal(int) imageWidthChanged = Signal(int) imageHeightChanged = Signal(int) devicesChanged = Signal() def __init__(self, parent=None): super(UEyeView, self).__init__(parent) self._frame_image = QImage() self._running = False self._update_thread = None self._cam_id = 0 self._image_width = 2048 self._image_height = 1088 self._devices = [] self.destroyed.connect(lambda: self._stop_thread) def _on_camera_stopped(self): self._stop_thread() def _on_camera_errored(self, msg): logger.error(f"Error loading camera {msg}") self._running = False self.runningChanged.emit(self._running) @Property(list, notify=devicesChanged) def devices(self): return self._devices @Property(bool, notify=runningChanged) def running(self): return self._running @running.setter def running(self, value): if value == self._running: return self._running = value if self._running: self._start_thread() else: self._stop_thread() self.runningChanged.emit(value) @Property(int, notify=camIdChanged) def camId(self): return self._cam_id @camId.setter def camId(self, value): if value == self._cam_id: return self._cam_id = value self.camIdChanged.emit(value) @Property(int, notify=imageWidthChanged) def imageWidth(self): return self._image_width @imageWidth.setter def imageWidth(self, value): if value == self._image_width: return self._image_width = value self.imageWidthChanged.emit(value) @Property(int, notify=imageHeightChanged) def imageHeight(self): return self._image_height @imageHeight.setter def imageHeight(self, value): if value == self._image_height: return self._image_height = value self.imageHeightChanged.emit(value) @Property(bool, constant=True) def driverLoaded(self): return pue is not None def _start_thread(self): if self._update_thread: return self._update_thread = UpdateThread( self._cam_id, self._image_width, self._image_height ) self._update_thread.pixmapReady.connect(self._update_pixmap) self._update_thread.stopped.connect(self._on_camera_stopped) self._update_thread.errored.connect(self._on_camera_errored) self._update_size() self._update_thread.start() def _stop_thread(self): if not self._update_thread: return self._update_thread.stop() self._update_thread.pixmapReady.disconnect(self._update_pixmap) self._update_thread.stopped.disconnect(self._on_camera_stopped) self._update_thread.errored.disconnect(self._on_camera_errored) self._update_thread = None def geometryChanged(self, new_geometry, old_geometry): super(UEyeView, self).geometryChanged(new_geometry, old_geometry) if self._update_thread: self._update_size() def paint(self, painter: QPainter): bounding_rect = QRect(0, 0, self.width(), self.height()) if not self._frame_image.isNull(): draw_rect = self._frame_image.rect() draw_rect.moveCenter(bounding_rect.center()) painter.drawImage(draw_rect, self._frame_image, self._frame_image.rect()) def _update_pixmap(self, image): self._frame_image = image self.update() def _update_size(self): self._update_thread.width = self.width() self._update_thread.height = self.height()