在 QGraphicsScene 上绘制点时出现填充问题

Padding issue while drawing points over QGraphicsScene

我有一个 PyQt 应用程序,我在其中使用 QPainterQGraphicsScene 上绘制点并制作了 drag n drop 之类的东西。

现在,我面临一个问题,那就是我无法拖动 QGraphicsScene 的极端角落和边缘的那些点。似乎总是留下一些填充或 space。

如何解决这个问题?

代码:

from collections import deque
from datetime import datetime
import sys
from threading import Thread
import time
import numpy as np

import cv2

from PyQt4 import QtCore, QtGui


class CameraWidget(QtGui.QGraphicsView):
    """Independent camera feed
    Uses threading to grab IP camera frames in the background

    @param width - Width of the video frame
    @param height - Height of the video frame
    @param stream_link - IP/RTSP/Webcam link
    @param aspect_ratio - Whether to maintain frame aspect ratio or force into fraame
    """

    def __init__(self, width, height, stream_link=0, aspect_ratio=False, parent=None, deque_size=1):
        super(CameraWidget, self).__init__(parent)

        # Initialize deque used to store frames read from the stream
        self.deque = deque(maxlen=deque_size)

        self.screen_width = width
        self.screen_height = height
        self.maintain_aspect_ratio = aspect_ratio

        self.camera_stream_link = stream_link

        # Flag to check if camera is valid/working
        self.online = False
        self.capture = None

        self.setScene(QtGui.QGraphicsScene(self))

        self._pixmap_item = self.scene().addPixmap(QtGui.QPixmap())
        
        canvas = Canvas()

        lay = QtGui.QVBoxLayout()
        lay.addWidget(canvas)
        self.setLayout(lay)

        self.load_network_stream()

        # Start background frame grabbing
        self.get_frame_thread = Thread(target=self.get_frame, args=())
        self.get_frame_thread.daemon = True
        self.get_frame_thread.start()

        # Periodically set video frame to display
        self.timer = QtCore.QTimer()
        self.timer.timeout.connect(self.set_frame)
        self.timer.start(0.5)

        print("Started camera: {}".format(self.camera_stream_link))

    def load_network_stream(self):
        """Verifies stream link and open new stream if valid"""

        def load_network_stream_thread():
            if self.verify_network_stream(self.camera_stream_link):
                self.capture = cv2.VideoCapture(self.camera_stream_link)
                self.online = True

        self.load_stream_thread = Thread(target=load_network_stream_thread, args=())
        self.load_stream_thread.daemon = True
        self.load_stream_thread.start()

    def verify_network_stream(self, link):
        """Attempts to receive a frame from given link"""

        cap = cv2.VideoCapture(link)
        if not cap.isOpened():
            return False
        cap.release()
        return True

    def get_frame(self):
        """Reads frame, resizes, and converts image to pixmap"""

        while True:
            try:
                if self.capture.isOpened() and self.online:
                    # Read next frame from stream and insert into deque
                    status, frame = self.capture.read()
                    if status:
                        self.deque.append(frame)
                    else:
                        self.capture.release()
                        self.online = False
                else:
                    # Attempt to reconnect
                    print("attempting to reconnect", self.camera_stream_link)
                    self.load_network_stream()
                    self.spin(2)
                self.spin(0.001)
            except AttributeError:
                pass

    def spin(self, seconds):
        """Pause for set amount of seconds, replaces time.sleep so program doesnt stall"""

        time_end = time.time() + seconds
        while time.time() < time_end:
            QtGui.QApplication.processEvents()

    def set_frame(self):
        """Sets pixmap image to video frame"""

        if not self.online:
            self.spin(1)
            return

        if self.deque and self.online:
            # Grab latest frame
            frame = self.deque[-1]

            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            h, w, ch = frame.shape
            bytesPerLine = ch * w

            # Convert to pixmap and set to video frame
            image = QtGui.QImage(frame, w, h, bytesPerLine, QtGui.QImage.Format_RGB888)
            pixmap = QtGui.QPixmap.fromImage(image.copy())
            self._pixmap_item.setPixmap(pixmap)
        self.fix_size()

    def resizeEvent(self, event):
        self.fix_size()
        super().resizeEvent(event)

    def fix_size(self):
        self.fitInView(
            self._pixmap_item,
            QtCore.Qt.KeepAspectRatio
            if self.maintain_aspect_ratio
            else QtCore.Qt.IgnoreAspectRatio,
        )


class Window(QtGui.QWidget):
    def __init__(self, cam=None, parent=None):
        super(Window, self).__init__(parent)

        self.showMaximized()

        self.screen_width = self.width()
        self.screen_height = self.height()

        # Create camera widget
        print("Creating Camera Widget...")
        self.camera = CameraWidget(self.screen_width, self.screen_height, cam)

        lay = QtGui.QVBoxLayout(self)
        lay.setContentsMargins(0, 0, 0, 0)
        lay.setSpacing(0)
        lay.addWidget(self.camera)


class Canvas(QtGui.QWidget):

    DELTA = 200 #for the minimum distance        

    def __init__(self, parent=None):
        super(Canvas, self).__init__(parent)
        self.draggin_idx = -1        
        self.points = np.array([[x[0],x[1]] for x in [[100,200], [200,200], [100,400], [200,400]]], dtype=np.float)  
        self.id = None 

        self.points_dict = {}

        for i, x in enumerate(self.points):
            point=(int(x[0]),int(x[1]))
            self.points_dict[i] = point 

    def paintEvent(self, e):
        qp = QtGui.QPainter()
        qp.begin(self)
        self.drawPoints(qp)
        self.drawLines(qp)
        qp.end()

    def drawPoints(self, qp):
        pen = QtGui.QPen()
        pen.setWidth(10)
        pen.setColor(QtGui.QColor('red'))
        qp.setPen(pen)
        for x,y in self.points:
            qp.drawPoint(x,y)        

    def drawLines(self, qp):
        qp.setPen(QtCore.Qt.red)
        qp.drawLine(self.points_dict[0][0], self.points_dict[0][1], self.points_dict[1][0], self.points_dict[1][1])
        qp.drawLine(self.points_dict[1][0], self.points_dict[1][1], self.points_dict[3][0], self.points_dict[3][1])
        qp.drawLine(self.points_dict[3][0], self.points_dict[3][1], self.points_dict[2][0], self.points_dict[2][1])
        qp.drawLine(self.points_dict[2][0], self.points_dict[2][1], self.points_dict[0][0], self.points_dict[0][1])

    def _get_point(self, evt):
        pos = evt.pos()
        if pos.x() < 0:
            pos.setX(0)
        elif pos.x() > self.width():
            pos.setX(self.width())
        if pos.y() < 0:
            pos.setY(0)
        elif pos.y() > self.height():
            pos.setY(self.height())
        return np.array([pos.x(), pos.y()])

    #get the click coordinates
    def mousePressEvent(self, evt):
        if evt.button() == QtCore.Qt.LeftButton and self.draggin_idx == -1:
            point = self._get_point(evt)
            int_point = (int(point[0]), int(point[1]))
            min_dist = ((int_point[0]-self.points_dict[0][0])**2 + (int_point[1]-self.points_dict[0][1])**2)**0.5
            
            for i, x in enumerate(list(self.points_dict.values())):
                distance = ((int_point[0]-x[0])**2 + (int_point[1]-x[1])**2)**0.5
                if min_dist >= distance:
                    min_dist = distance
                    self.id = i
                    
            #dist will hold the square distance from the click to the points
            dist = self.points - point
            dist = dist[:,0]**2 + dist[:,1]**2
            dist[dist>self.DELTA] = np.inf #obviate the distances above DELTA
            if dist.min() < np.inf:
                self.draggin_idx = dist.argmin()        

    def mouseMoveEvent(self, evt):
        if self.draggin_idx != -1:
            point = self._get_point(evt)
            self.points[self.draggin_idx] = point
            self.update()

    def mouseReleaseEvent(self, evt):
        if evt.button() == QtCore.Qt.LeftButton and self.draggin_idx != -1:
            point = self._get_point(evt)
            int_point = (int(point[0]), int(point[1]))
            self.points_dict[self.id] = int_point
            self.points[self.draggin_idx] = point
            self.draggin_idx = -1
            self.update()


camera = 0

if __name__ == "__main__":
    app = QtGui.QApplication([])
    win = Window(camera)
    sys.exit(app.exec_())

编辑:

我还有一个要求。

我的Canvasclass中的mousePressEventmouseReleaseEvent给我坐标w.r.t。我的显示器分辨率,而不是我想要它 w.r.t。 QGraphicsView。比如说我的 screen_resolution 是 1920x1080 我的 QGraphicsView 的大小是 640x480 那么我应该按照 640x480.

最简单的解决方案是为图形视图的布局添加 lay.setContentsMargins(0, 0, 0, 0)

class CameraWidget(QtGui.QGraphicsView):
    def __init__(self, width, height, stream_link=0, aspect_ratio=False, parent=None, deque_size=1):
        # ...
        canvas = Canvas()

        lay = QtGui.QVBoxLayout()
        lay.addWidget(canvas)
        self.setLayout(lay)
        <b>lay.setContentsMargins(0, 0, 0, 0)</b>
        # ...

但考虑到不建议这样做。

首先,您不需要单个小部件的布局,因为您可以创建以视图作为父级的小部件,然后在 resizeEvent:

        # ...
        self.canvas = Canvas(self)

    def resizeEvent(self, event):
        self.fix_size()
        super().resizeEvent(event)
        self.canvas.resize(self.size())

像 QGraphicsView 这样的小部件不应该设置布局,它不受支持并且在某些情况下可能会导致不需要的行为甚至错误。

在任何情况下,如果 QGraphicsView 用于绘画和鼠标交互,那么在 QGraphicsView 之上添加一个小部件没有多大意义:QGraphicsView 已经提供 更好 通过使用 QGraphicsRectItem 或 QGraphicsLineItem 来实现。

而且,即使不是这种情况,也应该在其 drawForeground() 实施中完成 图形视图上的自定义绘图。