问题描述
我有一个 PyQt 应用程序,我在其中使用 QPainter
在 QGraphicsScene
上绘制点并制作了一个 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 类中的 mousepressEvent
和 mouseReleaseEvent
为我提供了坐标 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()
实现中完成在图形视图上的自定义绘制。