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

学习前言

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

源码下载

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

支持功能

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

界面展示

PyQt5

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

信号与槽

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

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

import sysfrom PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLabelclass 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 = 0self.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 = Noneself.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 = Falsedef 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 = 0self.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.0t1 = time.time()# 读取某一帧frame = self.frame# 格式转变,BGRtoRGBframe = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)# 转变成Imageframe = 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+1else:self.count = 0# RGBtoBGR满足opencv显示格式frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)fps = (fps + (1./(time.time()-t1))) / 2print("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 = 0self.warn = Trueelse:self.warn = False# 发送pyqt信号self.Send_signal.emit(frame, self.warn)

信息记录

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

def add_item(self, image):# 总Widgetwight = 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)# 布局给wightitem = QListWidgetItem()# 创建QListWidgetItem对象item.setSizeHint(QSize(300, 80))# 设置QListWidgetItem大小self.stackedWidget.addItem(item)# 添加itemself.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 ctypesimport sysimport timeimport cv2import numpy as npimport qdarkstylefrom PIL import Imagefrom PyQt5 import QtCore, QtGui, QtWidgetsfrom PyQt5.Qt import QThreadfrom PyQt5.QtCore import *from PyQt5.QtGui import *from PyQt5.QtWidgets import *from custom.graphicsView import GraphicsViewfrom custom.listWidgets import *from custom.stackedWidget import *from custom.treeView import FileSystemTreeViewfrom yolo import YOLOctypes.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 = 0self.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.0t1 = time.time()# 读取某一帧frame = self.frame# 格式转变,BGRtoRGBframe = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)# 转变成Imageframe = 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+1else:self.count = 0# RGBtoBGR满足opencv显示格式frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)fps = (fps + (1./(time.time()-t1))) / 2print("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 = 0self.warn = Trueelse: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 = 0self.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 = Noneself.cur_img = Nonedef update_image(self):if self.src_img is None:returnimg = self.process_image()self.cur_img = imgself.graphicsView.update_image(img)def change_image(self, img):self.src_img = imgimg = self.process_image()self.cur_img = imgself.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 imgdef right_rotate(self):self.graphicsView.rotate(90)def left_rotate(self):self.graphicsView.rotate(-90)def add_item(self, image):# 总Widgetwight = 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)# 布局给wightitem = QListWidgetItem()# 创建QListWidgetItem对象item.setSizeHint(QSize(300, 80))# 设置QListWidgetItem大小self.stackedWidget.addItem(item)# 添加itemself.stackedWidget.setItemWidget(item, wight)# 为item设置widgetdef 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 = Trueelif self.thread_status == True:self.detectThread.terminate()if self.cap.isOpened():self.cap.release()self.action_video.setText('打开视频')self.thread_status = Falsedef 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 = Trueelse:self.detectThread.terminate()if self.cap.isOpened():self.cap.release()self.action_video.setText('打开视频')self.thread_status = Falsedef 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_())