睿智的目标检测——PyQt5搭建目标检测界面

学习前言

基于B导开源的YoloV4-Pytorch源码开发了戴口罩人脸检测系统(21年完成的本科毕设,较为老旧,可自行替换为最新的目标检测算法)。

源码下载

https://github.com/Egrt/YOLO_PyQt5
喜欢的可以点个star噢。

支持功能

  1. 支持读取本地图片
  2. 支持读取本地视频
  3. 支持打开摄像头实时检测
  4. 支持多线程,防止卡顿
  5. 支持检测到人脸未佩戴口罩时记录,并语音警告

界面展示

睿智的目标检测——PyQt5搭建目标检测界面

PyQt5

PyQt5是Python语言中一款流行的GUI(图形用户界面)开发框架,基于Qt GUI应用程序开发框架,提供了一个强大的工具集,用于创建各种桌面应用程序。PyQt5可以用于开发桌面应用程序、Web应用程序和移动应用程序,具有良好的跨平台性和丰富的功能。

信号与槽

信号和槽是PyQt5中一个重要的概念,是用于组织和管理GUI元素之间交互的机制。信号是GUI元素发出的事件或动作,槽是处理信号的函数。当信号发生时,与之相关联的槽将被自动调用。

下面是一个简单的示例代码,演示如何在PyQt5中使用信号和槽。这个示例创建了一个窗口,其中包含一个按钮和一个标签。当用户单击按钮时,标签的文本将会改变:

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLabel
class MyWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
    def initUI(self):
        self.setGeometry(300, 300, 300, 200)
        self.setWindowTitle('Signal and Slot')
        self.button = QPushButton('Click', self)
        self.button.move(100, 100)
        self.button.clicked.connect(self.changeText)
        self.label = QLabel('Hello World', self)
        self.label.move(110, 60)
    def changeText(self):
        self.label.setText('Button Clicked')
if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MyWindow()
    window.show()
    sys.exit(app.exec_())

在这个示例代码中,我们创建了一个名为MyWindow的窗口类,该类继承自QWidget。在MyWindow的构造函数中,我们创建了一个按钮和一个标签,并使用clicked信号将按钮的单击事件连接到changeText槽函数。当按钮被单击时,changeText槽函数将会被调用,该函数会改变标签的文本。

运行代码后,可以看到窗口上有一个按钮和一个标签,单击按钮后标签的文本会改变为“Button Clicked”。这个示例演示了如何使用PyQt5中的信号和槽来实现交互式GUI应用程序。

功能实现

界面设计

根据任务需求,可以将界面分为四部分:

  1. 最上方放置按钮来实现选择读取图片、视频、开启摄像头实时检测。
  2. 左侧放置目录控件,浏览本地文件。
  3. 中间显示YOLO处理后的图片。
  4. 在处理视频或实时读取摄像头检测时,如果多帧连续识别到不戴口罩人脸将其记录并发出语音警告。

因此编写代码如下:

class MyApp(QMainWindow):
    def __init__(self):
        super(MyApp, self).__init__()
        self.cap                 = cv2.VideoCapture()
        self.CAM_NUM             = 0
        self.thread_status       = False  # 判断识别线程是否开启
        self.tool_bar            = self.addToolBar('工具栏')
        self.action_right_rotate = QAction(
            QIcon("icons/右旋转.png"), "向右旋转90", self)
        self.action_left_rotate = QAction(
            QIcon("icons/左旋转.png"), "向左旋转90°", self)
        self.action_opencam = QAction(QIcon("icons/摄像头.png"), "开启摄像头", self)
        self.action_video   = QAction(QIcon("icons/video.png"), "加载视频", self)
        self.action_image   = QAction(QIcon("icons/图片.png"), "加载图片", self)
        self.action_right_rotate.triggered.connect(self.right_rotate)
        self.action_left_rotate.triggered.connect(self.left_rotate)
        self.action_opencam.triggered.connect(self.opencam)
        self.action_video.triggered.connect(self.openvideo)
        self.action_image.triggered.connect(self.openimage)
        self.tool_bar.addActions((self.action_left_rotate, self.action_right_rotate,
                                  self.action_opencam, self.action_video, self.action_image))
        self.stackedWidget      = StackedWidget(self)
        self.fileSystemTreeView = FileSystemTreeView(self)
        self.graphicsView       = GraphicsView(self)
        self.dock_file          = QDockWidget(self)
        self.dock_file.setWidget(self.fileSystemTreeView)
        self.dock_file.setTitleBarWidget(QLabel('目录'))
        self.dock_file.setFeatures(QDockWidget.NoDockWidgetFeatures)
        self.dock_attr = QDockWidget(self)
        self.dock_attr.setWidget(self.stackedWidget)
        self.dock_attr.setTitleBarWidget(QLabel('上报数据'))
        self.dock_attr.setFeatures(QDockWidget.NoDockWidgetFeatures)
        self.setCentralWidget(self.graphicsView)
        self.addDockWidget(Qt.LeftDockWidgetArea, self.dock_file)
        self.addDockWidget(Qt.RightDockWidgetArea, self.dock_attr)
        self.setWindowTitle('口罩佩戴检测')
        self.setWindowIcon(QIcon('icons/mask.png'))
        self.src_img = None
        self.cur_img = None

槽函数

在初始化中配置窗口的界面并使用connect连接信号与槽函数,当信号发生时,与之相关联的槽将被自动调用。控制打开图片、视频与本地摄像头的槽函数分别为:

def openvideo(self):
   print(self.thread_status)
   if self.thread_status == False:
       fileName, filetype = QFileDialog.getOpenFileName(
           self, "选择视频", "D:/", "*.mp4;;*.flv;;All Files(*)")
       flag = self.cap.open(fileName)
       if flag == False:
           msg = QtWidgets.QMessageBox.warning(self, u"警告", u"请选择视频文件",
                                               buttons=QtWidgets.QMessageBox.Ok,
                                               defaultButton=QtWidgets.QMessageBox.Ok)
       else:
           self.detectThread = DetectThread(fileName)
           self.detectThread.Send_signal.connect(self.Display)
           self.detectThread.start()
           self.action_video.setText('关闭视频')
           self.thread_status = True
   elif self.thread_status == True:
       self.detectThread.terminate()
       if self.cap.isOpened():
           self.cap.release()
       self.action_video.setText('打开视频')
       self.thread_status = False
def openimage(self):
   if self.thread_status == False:
       fileName, filetype = QFileDialog.getOpenFileName(
           self, "选择图片", "D:/", "*.jpg;;*.png;;All Files(*)")
       if fileName != '':
           src_img = Image.open(fileName)
           r_image, predicted_class = yolo.detect_image(src_img)
           r_image = np.array(r_image)
           showImage = QtGui.QImage(
               r_image.data, r_image.shape[1], r_image.shape[0], QtGui.QImage.Format_RGB888)
           self.graphicsView.set_image(QtGui.QPixmap.fromImage(showImage))
def opencam(self):
   if self.thread_status == False:
       flag = self.cap.open(self.CAM_NUM)
       if flag == False:
           msg = QtWidgets.QMessageBox.warning(self, u"警告", u"请检测相机与电脑是否连接正确",
                                               buttons=QtWidgets.QMessageBox.Ok,
                                               defaultButton=QtWidgets.QMessageBox.Ok)
       else:
           self.detectThread = DetectThread(self.CAM_NUM)
           self.detectThread.Send_signal.connect(self.Display)
           self.detectThread.start()
           self.action_video.setText('关闭视频')
           self.thread_status = True
   else:
       self.detectThread.terminate()
       if self.cap.isOpened():
           self.cap.release()
       self.action_video.setText('打开视频')
       self.thread_status = False

多线程

在读取视频文件或摄像头时,为避免界面卡顿,使用了多线程进行处理,并在结束处理视频文件时需要关闭线程防止系统卡死,且在关闭摄像头时还需要使用self.cap.release()对摄像头进行释放。

在多线程处理连续帧时,采用了Qt自带的多线程库QThread:

class DetectThread(QThread):
    Send_signal = pyqtSignal(np.ndarray, int)
    def __init__(self, fileName):
        super(DetectThread, self).__init__()
        self.capture = cv2.VideoCapture(fileName)
        self.count = 0
        self.warn = False  # 是否发送警告信号
    def run(self):
        ret, self.frame = self.capture.read()
        while ret:
            ret, self.frame = self.capture.read()
            self.detectCall()
    def detectCall(self):
        fps = 0.0
        t1 = time.time()
        # 读取某一帧
        frame = self.frame
        # 格式转变,BGRtoRGB
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        # 转变成Image
        frame = Image.fromarray(np.uint8(frame))
        # 进行检测
        frame_new, predicted_class = yolo.detect_image(frame)
        frame = np.array(frame_new)
        if predicted_class == "face":
            self.count = self.count+1
        else:
            self.count = 0
        # RGBtoBGR满足opencv显示格式
        frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
        fps = (fps + (1./(time.time()-t1))) / 2
        print("fps= %.2f" % (fps))
        frame = cv2.putText(frame, "fps= %.2f" % (
            fps), (0, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
        if self.count > 30:
            self.count = 0
            self.warn = True
        else:
            self.warn = False
        # 发送pyqt信号
        self.Send_signal.emit(frame, self.warn)

信息记录

如果连续30帧识别到未佩戴口罩的人脸时,将发送信号在右侧列表中显示,并记录当前帧画面:

def add_item(self, image):
    # 总Widget
    wight = QWidget()
    # 总体横向布局
    layout_main = QHBoxLayout()
    map_l = QLabel()  # 图片显示
    map_l.setFixedSize(60, 40)
    map_l.setPixmap(image.scaled(60, 40))
    # 右边的纵向布局
    layout_right = QVBoxLayout()
    # 右下的的横向布局
    layout_right_down = QHBoxLayout()  # 右下的横向布局
    layout_right_down.addWidget(
        QLabel(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())))
    # 按照从左到右, 从上到下布局添加
    layout_main.addWidget(map_l)  # 最左边的图片
    layout_right.addWidget(QLabel('警告!检测到未佩戴口罩'))  # 右边的纵向布局
    layout_right.addLayout(layout_right_down)  # 右下角横向布局
    layout_main.addLayout(layout_right)  # 右边的布局
    wight.setLayout(layout_main)  # 布局给wight
    item = QListWidgetItem()  # 创建QListWidgetItem对象
    item.setSizeHint(QSize(300, 80))  # 设置QListWidgetItem大小
    self.stackedWidget.addItem(item)  # 添加item
    self.stackedWidget.setItemWidget(item, wight)  # 为item设置widget

关闭系统

在关闭系统时,需要确保关闭了多线程,且关闭了已经打开的摄像头,否则在退出时也将造成卡顿:

def Display(self, frame, warn):
        im = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        showImage = QtGui.QImage(
            im.data, im.shape[1], im.shape[0], QtGui.QImage.Format_RGB888)
        self.graphicsView.set_image(QtGui.QPixmap.fromImage(showImage))
def closeEvent(self, event):
    ok = QtWidgets.QPushButton()
    cacel = QtWidgets.QPushButton()
    msg = QtWidgets.QMessageBox(
        QtWidgets.QMessageBox.Warning, u"关闭", u"确定退出?")
    msg.addButton(ok, QtWidgets.QMessageBox.ActionRole)
    msg.addButton(cacel, QtWidgets.QMessageBox.RejectRole)
    ok.setText(u'确定')
    cacel.setText(u'取消')
    if msg.exec_() == QtWidgets.QMessageBox.RejectRole:
        event.ignore()
    else:
        if self.thread_status == True:
            self.detectThread.terminate()
        if self.cap.isOpened():
            self.cap.release()
        event.accept()

最终完整的代码如下:

import ctypes
import sys
import time
import cv2
import numpy as np
import qdarkstyle
from PIL import Image
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.Qt import QThread
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from custom.graphicsView import GraphicsView
from custom.listWidgets import *
from custom.stackedWidget import *
from custom.treeView import FileSystemTreeView
from yolo import YOLO
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("myappid")
# 多线程实时检测
class DetectThread(QThread):
    Send_signal = pyqtSignal(np.ndarray, int)
    def __init__(self, fileName):
        super(DetectThread, self).__init__()
        self.capture = cv2.VideoCapture(fileName)
        self.count = 0
        self.warn = False  # 是否发送警告信号
    def run(self):
        ret, self.frame = self.capture.read()
        while ret:
            ret, self.frame = self.capture.read()
            self.detectCall()
    def detectCall(self):
        fps = 0.0
        t1 = time.time()
        # 读取某一帧
        frame = self.frame
        # 格式转变,BGRtoRGB
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        # 转变成Image
        frame = Image.fromarray(np.uint8(frame))
        # 进行检测
        frame_new, predicted_class = yolo.detect_image(frame)
        frame = np.array(frame_new)
        if predicted_class == "face":
            self.count = self.count+1
        else:
            self.count = 0
        # RGBtoBGR满足opencv显示格式
        frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
        fps = (fps + (1./(time.time()-t1))) / 2
        print("fps= %.2f" % (fps))
        frame = cv2.putText(frame, "fps= %.2f" % (
            fps), (0, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
        if self.count > 30:
            self.count = 0
            self.warn = True
        else:
            self.warn = False
        # 发送pyqt信号
        self.Send_signal.emit(frame, self.warn)
class MyApp(QMainWindow):
    def __init__(self):
        super(MyApp, self).__init__()
        self.cap                 = cv2.VideoCapture()
        self.CAM_NUM             = 0
        self.thread_status       = False  # 判断识别线程是否开启
        self.tool_bar            = self.addToolBar('工具栏')
        self.action_right_rotate = QAction(
            QIcon("icons/右旋转.png"), "向右旋转90", self)
        self.action_left_rotate = QAction(
            QIcon("icons/左旋转.png"), "向左旋转90°", self)
        self.action_opencam = QAction(QIcon("icons/摄像头.png"), "开启摄像头", self)
        self.action_video   = QAction(QIcon("icons/video.png"), "加载视频", self)
        self.action_image   = QAction(QIcon("icons/图片.png"), "加载图片", self)
        self.action_right_rotate.triggered.connect(self.right_rotate)
        self.action_left_rotate.triggered.connect(self.left_rotate)
        self.action_opencam.triggered.connect(self.opencam)
        self.action_video.triggered.connect(self.openvideo)
        self.action_image.triggered.connect(self.openimage)
        self.tool_bar.addActions((self.action_left_rotate, self.action_right_rotate,
                                  self.action_opencam, self.action_video, self.action_image))
        self.stackedWidget      = StackedWidget(self)
        self.fileSystemTreeView = FileSystemTreeView(self)
        self.graphicsView       = GraphicsView(self)
        self.dock_file          = QDockWidget(self)
        self.dock_file.setWidget(self.fileSystemTreeView)
        self.dock_file.setTitleBarWidget(QLabel('目录'))
        self.dock_file.setFeatures(QDockWidget.NoDockWidgetFeatures)
        self.dock_attr = QDockWidget(self)
        self.dock_attr.setWidget(self.stackedWidget)
        self.dock_attr.setTitleBarWidget(QLabel('上报数据'))
        self.dock_attr.setFeatures(QDockWidget.NoDockWidgetFeatures)
        self.setCentralWidget(self.graphicsView)
        self.addDockWidget(Qt.LeftDockWidgetArea, self.dock_file)
        self.addDockWidget(Qt.RightDockWidgetArea, self.dock_attr)
        self.setWindowTitle('口罩佩戴检测')
        self.setWindowIcon(QIcon('icons/mask.png'))
        self.src_img = None
        self.cur_img = None
    def update_image(self):
        if self.src_img is None:
            return
        img = self.process_image()
        self.cur_img = img
        self.graphicsView.update_image(img)
    def change_image(self, img):
        self.src_img = img
        img = self.process_image()
        self.cur_img = img
        self.graphicsView.change_image(img)
    def process_image(self):
        img = self.src_img.copy()
        for i in range(self.useListWidget.count()):
            img = self.useListWidget.item(i)(img)
        return img
    def right_rotate(self):
        self.graphicsView.rotate(90)
    def left_rotate(self):
        self.graphicsView.rotate(-90)
    def add_item(self, image):
        # 总Widget
        wight = QWidget()
        # 总体横向布局
        layout_main = QHBoxLayout()
        map_l = QLabel()  # 图片显示
        map_l.setFixedSize(60, 40)
        map_l.setPixmap(image.scaled(60, 40))
        # 右边的纵向布局
        layout_right = QVBoxLayout()
        # 右下的的横向布局
        layout_right_down = QHBoxLayout()  # 右下的横向布局
        layout_right_down.addWidget(
            QLabel(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())))
        # 按照从左到右, 从上到下布局添加
        layout_main.addWidget(map_l)  # 最左边的图片
        layout_right.addWidget(QLabel('警告!检测到未佩戴口罩'))  # 右边的纵向布局
        layout_right.addLayout(layout_right_down)  # 右下角横向布局
        layout_main.addLayout(layout_right)  # 右边的布局
        wight.setLayout(layout_main)  # 布局给wight
        item = QListWidgetItem()  # 创建QListWidgetItem对象
        item.setSizeHint(QSize(300, 80))  # 设置QListWidgetItem大小
        self.stackedWidget.addItem(item)  # 添加item
        self.stackedWidget.setItemWidget(item, wight)  # 为item设置widget
    def openvideo(self):
        print(self.thread_status)
        if self.thread_status == False:
            fileName, filetype = QFileDialog.getOpenFileName(
                self, "选择视频", "D:/", "*.mp4;;*.flv;;All Files(*)")
            flag = self.cap.open(fileName)
            if flag == False:
                msg = QtWidgets.QMessageBox.warning(self, u"警告", u"请选择视频文件",
                                                    buttons=QtWidgets.QMessageBox.Ok,
                                                    defaultButton=QtWidgets.QMessageBox.Ok)
            else:
                self.detectThread = DetectThread(fileName)
                self.detectThread.Send_signal.connect(self.Display)
                self.detectThread.start()
                self.action_video.setText('关闭视频')
                self.thread_status = True
        elif self.thread_status == True:
            self.detectThread.terminate()
            if self.cap.isOpened():
                self.cap.release()
            self.action_video.setText('打开视频')
            self.thread_status = False
    def openimage(self):
        if self.thread_status == False:
            fileName, filetype = QFileDialog.getOpenFileName(
                self, "选择图片", "D:/", "*.jpg;;*.png;;All Files(*)")
            if fileName != '':
                src_img = Image.open(fileName)
                r_image, predicted_class = yolo.detect_image(src_img)
                r_image = np.array(r_image)
                showImage = QtGui.QImage(
                    r_image.data, r_image.shape[1], r_image.shape[0], QtGui.QImage.Format_RGB888)
                self.graphicsView.set_image(QtGui.QPixmap.fromImage(showImage))
    def opencam(self):
        if self.thread_status == False:
            flag = self.cap.open(self.CAM_NUM)
            if flag == False:
                msg = QtWidgets.QMessageBox.warning(self, u"警告", u"请检测相机与电脑是否连接正确",
                                                    buttons=QtWidgets.QMessageBox.Ok,
                                                    defaultButton=QtWidgets.QMessageBox.Ok)
            else:
                self.detectThread = DetectThread(self.CAM_NUM)
                self.detectThread.Send_signal.connect(self.Display)
                self.detectThread.start()
                self.action_video.setText('关闭视频')
                self.thread_status = True
        else:
            self.detectThread.terminate()
            if self.cap.isOpened():
                self.cap.release()
            self.action_video.setText('打开视频')
            self.thread_status = False
    def Display(self, frame, warn):
        im = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        showImage = QtGui.QImage(
            im.data, im.shape[1], im.shape[0], QtGui.QImage.Format_RGB888)
        self.graphicsView.set_image(QtGui.QPixmap.fromImage(showImage))
    def closeEvent(self, event):
        ok = QtWidgets.QPushButton()
        cacel = QtWidgets.QPushButton()
        msg = QtWidgets.QMessageBox(
            QtWidgets.QMessageBox.Warning, u"关闭", u"确定退出?")
        msg.addButton(ok, QtWidgets.QMessageBox.ActionRole)
        msg.addButton(cacel, QtWidgets.QMessageBox.RejectRole)
        ok.setText(u'确定')
        cacel.setText(u'取消')
        if msg.exec_() == QtWidgets.QMessageBox.RejectRole:
            event.ignore()
        else:
            if self.thread_status == True:
                self.detectThread.terminate()
            if self.cap.isOpened():
                self.cap.release()
            event.accept()
if __name__ == "__main__":
    # 初始化yolo模型
    yolo = YOLO()
    app  = QApplication(sys.argv)
    app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
    window = MyApp()
    window.show()
    sys.exit(app.exec_())

发表回复