OpenCV-PyQT项目实战(10)项目案例06:键盘事件与视频抓拍

Posted YouCans

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OpenCV-PyQT项目实战(10)项目案例06:键盘事件与视频抓拍相关的知识,希望对你有一定的参考价值。

欢迎关注『OpenCV-PyQT项目实战 @ Youcans』系列,持续更新中
OpenCV-PyQT项目实战(1)安装与环境配置
OpenCV-PyQT项目实战(2)QtDesigner 和 PyUIC 快速入门
OpenCV-PyQT项目实战(3)信号与槽机制
OpenCV-PyQT项目实战(4)OpenCV 与PyQt的图像转换
OpenCV-PyQT项目实战(5)项目案例01:图像模糊
OpenCV-PyQT项目实战(6)项目案例02:滚动条应用
OpenCV-PyQT项目实战(7)项目案例03:鼠标框选
OpenCV-PyQT项目实战(8)项目案例04:鼠标定位
OpenCV-PyQT项目实战(9)项目案例04:视频播放
OpenCV-PyQT项目实战(10)项目案例06:键盘事件与视频抓拍
OpenCV-PyQT项目实战(11)项目案例07:摄像头操作与拍摄视频

文章目录

(10)项目案例06:键盘事件与视频抓拍

在上一个案例中我们介绍了OpenCV和PyQt 实现视频播放,本节继续介绍使用键盘事件从播放的视频中抓拍图像。

本例使用 OpenCV对视频文件进行解码获得图像帧,然后用 QTime 定时器和 QThread 的方式来控制 QLabel 中的图像更新。使用PyQt键盘事件,抓拍视频中的图像。


1. PyQt 中的事件处理机制

1.1 PyQt 中的事件类型

Qt 事件的类型很多,常见的事件类型如下:

  • 键盘事件:按键按下和松开。
  • 鼠标事件:鼠标指针移动,鼠标按下和松开。
  • 拖放事件:用鼠标进行拖放。
  • 滚轮事件:鼠标滚轮滚动。
  • 绘屏事件:重绘屏幕的某些部分。
  • 定时事件:定时器到时。
  • 焦点事件:键盘焦点移动。
  • 进入/离开事件:鼠标指针移入Widget内,或者移出。
  • 移动事件:Widget的位置改变。
  • 大小改变事件:Widget的大小改变。
  • 显示/隐藏事件:Widget显示和隐藏。
  • 窗口事件:窗口是否为当前窗口。

此外,还有一些 Qt事件,如:Socket事件、剪贴板事件、字体改变事件、布局改变事件等。


1.2 PyQt 中的事件处理过程

控件→事件→信号→槽

  1. 事件产生,使用应用程序对象调用notify函数,将事件派发到指定窗口。

def notify (arg_1, arg_2),属于QCoreApplication

  1. 事件过滤,使用eventFilter函数,对派发的事件进行过滤,默认不过滤。

def eventFilter (watched, event),属于QObject,用于对指定控件接收到的事件进行监控,可以实现对特定事件的拦截或者在进行事件处理之前进行一些操作

  1. 事件分发,使用窗口的事件分发器函数event,对事件进行分类。可以重载 event 函数,实现对特定事件的过滤或者在进行事件处理之前进行一些操作,可以拦截事件或继续分发事件。

def event (event),属于QObject,返回 true 则该事件不继续传播,返回 false 则该事件会继续传播到其父控件进行响应。

  1. 事件处理,将分类后的事件(鼠标事件、键盘事件、绘图事件等)分发给对应的事件处理函数进行处理,实际用户定义的功能。

def mouseMoveEvent (event),属于QWidget,只能用来处理特定部件的特定事件


1.3 PyQt 中的事件处理方法

PyQt 提供了 5 种事件处理方法:

  1. 重新实现事件函数,例如paintEvent(),mousePressEvent(),keyPressEvent()等事件处理函数。这是最常用的方法,不过它只能用来处理特定部件的特定事件。

  2. 重新实现QObject.event()函数,QObject类的event()函数可以在事件到达默认的事件处理函数之前获得该事件。

  3. 在QApplication对象上安装事件过滤器。使用事件过滤器可以在一个界面类中同时处理不同子部件的不同事件。

  4. 向QApplication对象安装事件过滤器。一个程序只有一个QApplication对象,这样实现的功能与使用notify()函数是相同的,优点是可以同时处理多个事件。

  5. 重新实现QAppliction的notifiy()方法。这个函数功能强大,提供了完全的控制,可以在事件过滤器得到事件之前就获得它们,但它一次只能处理一个事件。


1.4 例程:PyQt 中的事件处理

import sys
from PyQt5.QtWidgets import (QApplication, QMenu, QWidget)
from PyQt5.QtCore import (QEvent, QTimer, Qt)
from PyQt5.QtGui import QPainter

class MyEventDemoWindow(QWidget):
    def __init__(self, parent=None):
        super(MyEventDemoWindow, self).__init__(parent)
        self.justDoubleClikcked = False
        self.key = ""
        self.text = ""
        self.message = ""
        self.resize(400, 300)
        self.move(100, 100)
        self.setWindowTitle("Events Demo 1")
        QTimer.singleShot(1000, self.giveHelp)


    def giveHelp(self):
        self.text = "请点击这里触发追踪鼠标功能"
        self.update() # 重绘事件,也就是除非paintEvent函数
    
    # 重新实现关闭事件
    def closeEvent(self, event):
        print("Closed")
    
    # 重新实现上下文菜单事件
    def contextMenuEvent(self, event):
        menu = QMenu(self)
        oneAction = menu.addAction("&One")
        twoAction = menu.addAction("&Two")
        oneAction.triggered.connect(self.one)
        twoAction.triggered.connect(self.two)
        if not self.message:
            menu.addSeparator()
            threeAction = menu.addAction("&Three")
            threeAction.triggered.connect(self.three)
        menu.exec_(event.globalPos())
        
    def one(self):
        self.message = "Menu Option One"
        self.update()
    
    def two(self):
        self.message = "Menu Option Two"
        self.update()

    def three(self):
        self.message = "Menu Option Three"
        self.update()

    '''重新实现绘制事件'''
    def paintEvent(self, event):
        text = self.text
        i = text.find("\\n\\n")
        if i >= 0:
            text = text[0:i]
        if self.key:
            text += "\\n\\n你按下了:0".format(self.key)
        painter = QPainter(self)
        painter.setRenderHint(QPainter.TextAntialiasing)
        painter.drawText(self.rect(), Qt.AlignCenter, text) # 绘制文本
        if self.message:
            painter.drawText(self.rect(), Qt.AlignBottom | Qt.AlignHCenter, self.message)
            QTimer.singleShot(5000, self.clearMessage)
            QTimer.singleShot(5000, self.update)

    # 清空文本信息槽函数
    def clearMessage(self):
        self.message = ""
        
    # 重新实现调整窗口大小事件
    def resizeEvent(self, event):
        self.text = "调整窗口大小为: QSize(0, 1)".format(event.size().width(), event.size().height())
        self.update()
    
    # 重新实现鼠标释放事件
    def mouseReleaseEvent(self, event) -> None:
        if self.justDoubleClikcked:
            self.justDoubleClikcked = False
        else :
            self.setMouseTracking(not self.hasMouseTracking()) # 单击鼠标
            if self.hasMouseTracking():
                self.text = "释放鼠标 开启鼠标跟踪功能.\\n\\n" + \\
                        "请移动鼠标\\n\\n" + \\
                        "单击鼠标可以关闭这个功能"
            else:
                self.text = "释放鼠标  关闭鼠标跟踪功能" + \\
                        "单击鼠标可以开启这个功能"
        self.update()
 
    '''重新实现鼠标移动事件'''
    def mouseMoveEvent(self, event):
        if not self.justDoubleClikcked:
            globalPos = self.mapToGlobal(event.pos())# 将窗口坐标转换为屏幕坐标
            self.text = """鼠标位置:
            窗口坐标为:QPoint(0, 1)
            屏幕坐标为:QPoint(2, 3)""".format(event.pos().x(), event.pos().y(), globalPos.x(), globalPos.y())
            self.update()

    '''重新实现鼠标双击事件'''
    def mouseDoubleClickEvent(self, event):
        self.justDoubleClikcked = True
        self.text = "双击鼠标"
        self.update()

    def mousePressEvent(self, event):
        self.text = "按下鼠标"
        self.update()

    def keyPressEvent(self, event):
        self.text = "按下按键"
        self.key = ""
        if event.key() == Qt.Key_Home:
            self.key = "Home"
        elif event.key() == Qt.Key_End:
            self.key = "End"
        elif event.key() == Qt.Key_PageUp:
            if event.modifiers() & Qt.ControlModifier:
                self.key = "Ctrl + Page Up"
            else:
                self.key = "Page Up"
        elif event.key() == Qt.Key_PageDown:
            if event.modifiers() & Qt.ControlModifier:
                self.key = "Ctrl + Key_PageDown"
            else:
                self.key = "Key_PageDown"
        elif Qt.Key_A <= event.key() <= Qt.Key_Z:
            if event.modifiers() & Qt.ShiftModifier:
                self.key = "Shift+"
            self.key += event.text()
            
        if self.key:
            self.key = self.key
            self.update()
        else:
            QWidget.keyPressEvent(self, event)
    
    def keyReleaseEvent(self, event):
        self.text = "释放了按键"
        self.update()

    '''重新实现event,捕获Tab键'''
    def event(self, event):
        if (event.type() == QEvent.KeyPress and event.key() == Qt.Key_Tab):
            self.key = "在event() 中捕获Tab键"
            self.update()
            return True # 返回True表示本次事件已经执行处理
        else:
            return QWidget.event(self, event) # 继续处理事件
        
if __name__ == "__main__":
    app = QApplication(sys.argv)
    win = MyEventDemoWindow()
    win.show()
    sys.exit(app.exec_())



2. 键盘事件的处理

2.1 键盘事件

按下并释放键盘按键时,以下方法将被调用:

  • keyPressEvent(self,event): 按下某一键时,该方法被调用直到键被释放为止;
  • keyReleaseEvent(self,event) - 释放之前按下的键时被调用。

上述方法中的event参数为QKeyEvent对象,其常用方法有:

  • key(): 返回按下键的值;
  • text(): 返回按下键的Unicode字符编码信息,当按键为Shift, Control, Alt等时,则该函数返回的字符为空值。
  • modifiers(): 判断按下了哪些按键(Shift,Ctrl,Alt,等等)。返回值为Qt. KeyboardModifier 中下列枚举变量的组合:
    • NoModifier - 没有按键;
    • ShiftModifier - Shift按键;
    • ControlModifier - Ctrl按键;
    • AltModifier - Alt按键;
    • MetaModifier - 按键;
    • KeypadModifier - 附加键盘上的任何按键;
    • GroupSwitchModifier - 按下键(仅限X11系统)。
    • isAutoRepeat(): 如果一直按着某键,返回True,否则返回False;
    • match(QKeySequence.StandardKey key): 如果当前的键组合与key相同,返回True;否则,返回False。

2.2 keyPressEvent方法获取按键值

通过重写控件对象的 keyPressEvent方法或 event方法,可以捕获键盘事件的具体按键。

由于event方法是通用事件,需要判断是否键盘事件,因此推荐使用 keyPressEvent方法。

函数原型:

keyPressEvent(self,event)

event类型为QKeyEvent,从QInputEvent继承,而QInputEvent从QEvent继承。
该方法没有返回值。

获取按键值的方法:
通过QKeyEvent的方法text()可以获取可打印字符的按键。

方法key()可以获取按键对应的Qt键常量值,功能键、光标控制键、shift、Alt、Ctrl等都能捕获。按键与按键值的对应关系,请参考官方文档


2.3 例程:按键事件与捕获按键值

import sys
from PyQt5.QtWidgets import QMainWindow, QWidget, QApplication
from PyQt5.QtCore import Qt


class MainWindow(QMainWindow):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.initUI()

    def initUI(self):
        self.setWindowTitle("鼠标键盘事件示例")
        self.setCentralWidget(QWidget())  # 指定主窗口中心部件
        self.statusBar().showMessage("ready")  # 状态栏显示信息
        self.resize(300, 185)

    # 重新实现各事件处理程序
    def keyPressEvent(self, event):
        key = event.key()
        if Qt.Key_A <= key <= Qt.Key_Z:
            if event.modifiers() & Qt.ShiftModifier:  # Shift 键被按下
                self.statusBar().showMessage('"Shift+%s" pressed' % chr(key), 500)
            elif event.modifiers() & Qt.ControlModifier:  # Ctrl 键被按下
                self.statusBar().showMessage('"Control+%s" pressed' % chr(key), 500)
            elif event.modifiers() & Qt.AltModifier:  # Alt 键被按下
                self.statusBar().showMessage('"Alt+%s" pressed' % chr(key), 500)
            else:
                self.statusBar().showMessage('"%s" pressed' % chr(key), 500)

        elif key == Qt.Key_Home:
            self.statusBar().showMessage('"Home" pressed', 500)
        elif key == Qt.Key_End:
            self.statusBar().showMessage('"End" pressed', 500)
        elif key == Qt.Key_PageUp:
            self.statusBar().showMessage('"PageUp" pressed', 500)
        elif key == Qt.Key_PageDown:
            self.statusBar().showMessage('"PageDown" pressed', 500)
        else:  # 其它未设定的情况
            QWidget.keyPressEvent(self, event)  # 留给基类处理

    def mousePressEvent(self, event):  # 鼠标按下事件
        pos = event.pos()  # 返回鼠标所在点QPoint
        self.statusBar().showMessage('Mouse is pressed at (%d,%d) of widget ' % (pos.x(), pos.y()), 500)
        globalPos = self.mapToGlobal(pos)
        print('Mouse is pressed at (%d,%d) of screen ' % (globalPos.x(), globalPos.y()))

    def mouseReleaseEvent(self, event):  # 鼠标释放事件
        pos = event.pos()  # 返回鼠标所在点QPoint
        self.statusBar().showMessage('Mouse is released at (%d,%d) of widget ' % (pos.x(), pos.y()), 500)
        if event.button() == Qt.LeftButton:
            print("左键")
        elif event.button() == Qt.MidButton:
            print("中键")
        elif event.button() == Qt.RightButton:
            print("右键")

    def mouseDoubleClickEvent(self, event):  # 鼠标双击事件
        pos = event.pos()  # 返回鼠标所在点QPoint
        self.statusBar().showMessage('Mouse is double-clicked at (%d,%d) of widget ' % (pos.x(), pos.y()), 500)

    def mouseMoveEvent(self, event):  # 鼠标移动事件
        pos = event.pos()  # 返回鼠标所在点QPoint
        self.statusBar().showMessage('Mouse is moving at (%d,%d) of widget ' % (pos.x(), pos.y()), 500)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    mw = MainWindow()
    mw.show()
    sys.exit(app.exec_())



3. 项目实战:PyQt 视频播放与抓拍图像

在上一个案例中我们介绍了OpenCV和PyQt 实现视频播放,本节继续介绍使用键盘事件从播放的视频中抓拍图像。

播放的视频,可以是读取的视频文件,也可以是摄像头实时获取的视频文件。

抓拍视频中的图像,可以使用键盘事件触发,当然也可以使用鼠标事件触发,还可以通过对于图像帧自动识别判定来触发。由于此前介绍过鼠标事件,本节以此为例介绍键盘事件。至于自动识别触发抓拍,将在今后的文章中进行介绍。

本例使用 OpenCV对视频文件进行解码获得图像帧,然后用 QTime 定时器和 QThread 的方式来控制 QLabel 中的图像更新。使用PyQt键盘事件,抓拍视频中的图像。


3.1 使用 QtDesigner 开发 PyQt5 图形界面

本例的 UI 继承自 uiDemo10.ui ,并进行修改如下:


完成了本项目的图形界面设计,将其保存为 uiDemo11.ui文件。

在 PyCharm中,使用 PyUIC 将选中的 uiDemo11.ui 文件转换为 .py 文件,就得到了 uiDemo11.py 文件。



3.2. 完整例程: 视频播放与抓拍图像

# OpenCVPyqt11.py
# Demo05 of GUI by PyQt5
# Copyright 2023 Youcans, XUPT
# Crated:2023-02-24

import sys
import cv2 as cv
import numpy as np
from PyQt5 import QtCore
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from uiDemo10 import Ui_MainWindow  # 导入 uiDemo10.py 中的 Ui_MainWindow 界面类

class MyMainWindow(QMainWindow, Ui_MainWindow):  # 继承 QMainWindow 类和 Ui_MainWindow 界面类
    def __init__(self, parent=None):
        super(MyMainWindow, self).__init__(parent)  # 初始化父类
        self.setupUi(self)  # 继承 Ui_MainWindow 界面类
        self.timerCam = QtCore.QTimer()  # 定时器,毫秒
        self.cap = None  #
        self.frameNum = 1  # 视频帧数初值

        # 菜单栏
        self.actionOpen.triggered.connect(self.openVideo)  # 连接并执行 openSlot 子程序
        self.actionHelp.triggered.connect(self.trigger_actHelp)  # 连接并执行 trigger_actHelp 子程序
        self.actionQuit.triggered.connect(self.close)  # 连接并执行 trigger_actHelp 子程序

        # 通过 connect 建立信号/槽连接,点击按钮事件发射 triggered 信号,执行相应的子程序 click_pushButton
        self.pushButton_1.clicked.connect(self.openVideo)  # 打开视频文件
        self.pushButton_2.clicked.connect(self.playVideo)  # 播放视频文件
        self.pushButton_3.clicked.connect(self.pauseVideo)  # 停止视频播放
        self.pushButton_4.clicked.connect(self.trigger_actHelp)  # 按钮触发
        self.pushButton_5.clicked.connect(self.close)  # 点击 # 按钮触发:关闭
        self.timerCam.timeout.connect(self.refreshFrame)  # 计时器结束时调用槽函数刷新当前帧

        # 初始化
        return

    def openVideo(self):  # 读取视频文件,点击 pushButton_1 触发
        self.videoPath, _ = QFileDialog.getOpenFileName(self, "Open Video", "../images/", "*.mp4 *.avi *.mov")
        print("Open Video: ", self.videoPath)
        return

    def playVideo(self):  # 播放视频文件,点击 pushButton_2 触发
        if self.timerCam.isActive()==False:
            self.cap = cv.VideoCapture(self.videoPath)  # 实例化 VideoCapture 类
            if self.cap.isOpened():  # 检查视频捕获是否成功
                self.timerCam.start(20)  # 设置计时间隔并启动,定时结束将触发刷新当前帧
        else:  # 
            self.timerCam.stop()  # 停止定时器
            self.cap.release()  # 关闭读取视频文件
            self.label_1.clear()  # 清除显示内容

    def pauseVideo(self):
        self.timerCam.blockSignals(False)  # 取消信号阻塞,恢复定时器
        if self.timerCam.isActive() and self.frameNum%2==1:
            self.timerCam.blockSignals(True)  # 信号阻塞,暂停定时器
            self.pushButton_3.setText("3 继续")  # 点击"继续",恢复播放
            print("信号阻塞,暂停播放。", self.frameNum)
        else:
            self.pushButton_3.setText("3 暂停")  # 点击"暂停",暂停播放
            print("取消阻塞,恢复播放。", self.frameNum)
        self.frameNum = self.frameNum + 1

    def closeEvent(self, event):
        if self.timerCam.isActive():
            self.timerCam.stop()

    def keyPressEvent(self, event):  # 键盘事件
        keyEvent = QKeyEvent(event)
        if keyEvent.key()==QtCore.Qt.Key_Enter or keyEvent.key()==QtCore.Qt.Key_Return:
            print("keyPressEvent", self.frame.shape)
            capture = self.frame
            self.refreshShow(capture, self.label_2)  # 刷新显示

    def refreshFrame(self):  # 刷新视频图像
        ret, self.frame = self.cap.read()  # 读取下一帧视频图像
        qImg = self.cvToQImage(self.frame)  # OpenCV 转为 PyQt 图像格式
        self.label_1.setPixmap((QPixmap.fromImage(qImg)))  # 加载 PyQt 图像
        return

    def refreshShow(self, img, label):  # 刷新显示图像
        qImg = self.cvToQImage(img)  # OpenCV 转为 PyQt 图像格式
        label.setPixmap((QPixmap.fromImage(qImg)))  # 加载 PyQt 图像
        return

    def cvToQImage(self, image):
        # 8-bits unsigned, NO. OF CHANNELS=1
        if image.dtype == np.uint8:
            channels = 1 if len(image.shape) == 2 else image.shape[2]
        if channels == 3:  # CV_8UC3
            # Create QImage with same dimensions as input Mat
            qImg = QImage(image, image.shape[1], image.shape[0], image.strides[0], QImage.Format_RGB888)
            return qImg.rgbSwapped()
        elif channels == 1:
            # Create QImage with same dimensions as input Mat
            qImg = QImage(image, image.shape[1], image.shape[0], image.strides[0], QImage.Format_Indexed8)
            return qImg
        else:
            QtCore.qDebug("ERROR: numpy.ndarray could not be converted to QImage. Channels = %d" % image.shape[2])
            return QImage()

    def trigger_actHelp(self):  # 动作 actHelp 触发
        QMessageBox.about(self, "About",
                          """数字图像处理工具箱 v1.0\\nCopyright YouCans, XUPT 2023""")
        return

if __name__ == '__main__':
    app = QApplication(sys.argv)  # 在 QApplication 方法中使用,创建应用程序对象
    myWin = MyMainWindow()  # 实例化 MyMainWindow 类,创建主窗口
    myWin.show()  # 在桌面显示控件 myWin
    sys.exit(app.exec_())  # 结束进程,退出程序


2.3 程序说明

(1)“打开”按钮用于从文件夹选择播放的视频文件。

(2)“播放”按钮用于播放打开的视频文件,播放结束后自动关闭。

(3)“暂停/继续”按钮用于暂停/继续播放视频文件。按钮初始显示为“暂停”,按下“暂停”按钮后暂停播放,按钮显示切换为“继续”;再次按下“继续”按钮后继续播放,按钮显示切换为“暂停”。

(4)在播放过程中,按下键盘回车键,则抓拍视频当前帧并显示在GUI 右侧窗口控件中。

运行结果:


【本节完】


版权声明:

Copyright 2023 youcans, XUPT

Crated:2023-2-24


以上是关于OpenCV-PyQT项目实战(10)项目案例06:键盘事件与视频抓拍的主要内容,如果未能解决你的问题,请参考以下文章

OpenCV-PyQT项目实战项目案例04:视频播放

OpenCV-PyQT项目实战项目案例03:鼠标定位

OpenCV-PyQT项目实战项目案例01:图像模糊

OpenCV-PyQT项目实战项目案例04:视频播放

OpenCV-PyQT项目实战项目案例01图像模糊

OpenCV-PyQT项目实战项目案例02滚动条应用