PyQt5中2x2视频矩阵鼠标回调失效,如何检测位置并打开新窗口?
问题原因
你用cv2.setMouseCallback的方式完全错误:
cv2.setMouseCallback仅对OpenCV自身创建的窗口(即cv2.imshow生成的窗口)生效,你现在把视频帧转成QPixmap显示在PyQt的QLabel中,这个控件属于Qt的窗口体系,OpenCV的回调无法捕获它的鼠标事件。- 代码里
cv2.setMouseCallback(s3, mouse_callback)的第一个参数是numpy数组(拼接后的视频帧),而该函数要求第一个参数是OpenCV窗口的名称字符串,这属于参数类型错误,根本无法绑定回调。
解决方案
改用PyQt自身的鼠标事件机制,给显示视频的QLabel重写mousePressEvent方法,同时处理坐标缩放问题(因为视频帧会被QLabel缩放显示,需要把控件上的点击坐标映射回原始帧的坐标)。
具体步骤
- 创建自定义
VideoLabel类继承自QLabel,重写mousePressEvent方法处理点击事件。 - 在
App类中用自定义的VideoLabel替代原有的QLabel。 - 计算原始视频帧的尺寸,以及每个分块视频的区域,将QLabel上的点击坐标转换为原始帧坐标,判断点击的是哪个视频块。
修改后的完整代码
from PyQt5 import QtGui from PyQt5.QtWidgets import QWidget, QApplication, QLabel, QVBoxLayout from PyQt5.QtGui import QPixmap import sys import cv2 from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QThread, QPoint import numpy as np class VideoLabel(QLabel): # 自定义信号,传递点击的视频块索引(0-3对应左上、右上、左下、右下) video_clicked = pyqtSignal(int) def __init__(self, parent=None): super().__init__(parent) self.original_frame_size = None # 存储原始拼接帧的尺寸 def setOriginalFrameSize(self, size): self.original_frame_size = size def mousePressEvent(self, event): if event.button() == Qt.LeftButton and self.original_frame_size: # 获取QLabel上的点击坐标 label_pos = event.pos() # 计算缩放比例:QLabel显示尺寸 / 原始帧尺寸 scale_x = self.width() / self.original_frame_size.width() scale_y = self.height() / self.original_frame_size.height() # 转换为原始帧上的坐标 original_x = label_pos.x() / scale_x original_y = label_pos.y() / scale_y # 原始拼接帧是2x2布局,每个分块的宽高是原始帧的一半 frame_width = self.original_frame_size.width() // 2 frame_height = self.original_frame_size.height() // 2 # 判断点击的是哪个视频块 if original_x < frame_width and original_y < frame_height: self.video_clicked.emit(0) # 左上a.avi elif original_x >= frame_width and original_y < frame_height: self.video_clicked.emit(1) # 右上b.avi elif original_x < frame_width and original_y >= frame_height: self.video_clicked.emit(2) # 左下c.avi elif original_x >= frame_width and original_y >= frame_height: self.video_clicked.emit(3) # 右下d.avi super().mousePressEvent(event) class VideoThread(QThread): change_pixmap_signal = pyqtSignal(np.ndarray) def __init__(self): super().__init__() self._run_flag = True def run(self): count1 = 0 count2 = 0 count3 = 0 count4 = 0 v1 = cv2.VideoCapture('a.avi') v2 = cv2.VideoCapture('b.avi') v3 = cv2.VideoCapture('c.avi') v4 = cv2.VideoCapture('d.avi') # 获取单个视频帧的尺寸,假设四个视频尺寸一致 frame_width = int(v1.get(cv2.CAP_PROP_FRAME_WIDTH)) frame_height = int(v1.get(cv2.CAP_PROP_FRAME_HEIGHT)) v1_length = int(v1.get(cv2.CAP_PROP_FRAME_COUNT)) v2_length = int(v2.get(cv2.CAP_PROP_FRAME_COUNT)) v3_length = int(v3.get(cv2.CAP_PROP_FRAME_COUNT)) v4_length = int(v4.get(cv2.CAP_PROP_FRAME_COUNT)) while self._run_flag: ret1, frame1 = v1.read() ret2, frame2 = v2.read() ret3, frame3 = v3.read() ret4, frame4 = v4.read() # 循环播放视频 if count1 == v1_length: v1.set(cv2.CAP_PROP_POS_FRAMES, 0) count1 = 0 if count2 == v2_length: v2.set(cv2.CAP_PROP_POS_FRAMES, 0) count2 = 0 if count3 == v3_length: v3.set(cv2.CAP_PROP_POS_FRAMES, 0) count3 = 0 if count4 == v4_length: v4.set(cv2.CAP_PROP_POS_FRAMES, 0) count4 = 0 count1 +=1 count2 +=1 count3 +=1 count4 +=1 if ret1 and ret2 and ret3 and ret4: s1 = np.hstack((frame1, frame2)) s2 = np.hstack((frame3, frame4)) s3 = np.vstack((s1, s2)) self.change_pixmap_signal.emit(s3) else: # 重新打开视频 v1 = cv2.VideoCapture('a.avi') v2 = cv2.VideoCapture('b.avi') v3 = cv2.VideoCapture('c.avi') v4 = cv2.VideoCapture('d.avi') if cv2.waitKey(20) & 0xFF == ord('q'): break v1.release() v2.release() v3.release() v4.release() cv2.destroyAllWindows() def stop(self): self._run_flag = False self.wait() class App(QWidget): def __init__(self): super().__init__() self.setWindowTitle("2x2视频矩阵") self.display_width = 640 self.display_height = 480 # 使用自定义的VideoLabel self.image_label = VideoLabel(self) self.image_label.resize(self.display_width, self.display_height) self.image_label.setAlignment(Qt.AlignCenter) self.image_label.video_clicked.connect(self.on_video_clicked) self.textLabel = QLabel('点击任意视频块打开窗口') vbox = QVBoxLayout() vbox.addWidget(self.image_label) vbox.addWidget(self.textLabel) self.setLayout(vbox) self.thread = VideoThread() self.thread.change_pixmap_signal.connect(self.update_image) self.thread.start() def closeEvent(self, event): self.thread.stop() event.accept() @pyqtSlot(np.ndarray) def update_image(self, cv_img): qt_img = self.convert_cv_qt(cv_img) self.image_label.setPixmap(qt_img) # 设置原始帧尺寸,用于坐标转换 h, w = cv_img.shape[:2] self.image_label.setOriginalFrameSize(Qt.QSize(w, h)) def convert_cv_qt(self, cv_img): rgb_image = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape bytes_per_line = ch * w convert_to_Qt_format = QtGui.QImage(rgb_image.data, w, h, bytes_per_line, QtGui.QImage.Format_RGB888) p = convert_to_Qt_format.scaled(self.display_width, self.display_height, Qt.KeepAspectRatio) return QPixmap.fromImage(p) def on_video_clicked(self, index): # 这里处理点击事件,比如打开第二个窗口 video_names = ['a.avi', 'b.avi', 'c.avi', 'd.avi'] print(f"点击了视频: {video_names[index]}") # 这里可以添加打开新窗口的代码,比如创建一个新的QWidget显示该视频 # 示例: # from PyQt5.QtWidgets import QDialog # dialog = QDialog(self) # dialog.setWindowTitle(video_names[index]) # dialog.resize(640,480) # dialog.show() if __name__ == "__main__": app = QApplication(sys.argv) a = App() a.show() sys.exit(app.exec_())
代码说明
- 自定义
VideoLabel类:重写mousePressEvent,实现坐标转换和点击区域判断,通过自定义信号video_clicked传递点击的视频块索引。 - 坐标转换:将QLabel上的点击坐标按缩放比例映射回原始拼接帧的坐标,确保点击区域判断准确。
- 事件响应:在
App类的on_video_clicked方法中处理点击事件,你可以在这里添加打开第二个窗口的逻辑。
内容的提问来源于stack exchange,提问作者hobik
相关产品推荐
相关产品推荐

