在 QGraphicsScene

问题描述

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

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

如何解决这个问题?

代码

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,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)
        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,[100,400],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_())

编辑:

我还有一个要求。

我的 Canvas 类中的 mousepressEventmouseReleaseEvent 为我提供了坐标 w.r.t.我的显示器分辨率,而不是我想要它 w.r.t. QGraphicsView。说例如我的 screen_resolution 是 1920x1080 而我的 QGraphicsView 的大小是 640x480 那么我应该按照 640x480 获得积分。

解决方法

最简单的解决方案是为图形视图的布局添加 lay.setContentsMargins(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)
        lay.setContentsMargins(0,0)
        # ...

但考虑到不建议这样做。

首先,您不需要为单个小部件设置布局,因为您只需创建以视图为父级的小部件,然后在 resizeEvent 中调整其大小:

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

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

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

无论如何,如果该小部件用于绘画和鼠标交互,则在 QGraphicsView 顶部添加小部件没有多大意义:QGraphicsView 已经为此提供了更好实现通过使用 QGraphicsRectItem 或 QGraphicsLineItem。

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