Smart target detection – PyQt5 builds a target detection interface

Smart target detection – PyQt5 builds target detection interface

Learn the preface

Based on the open source YoloV4-Pytorch source code of B guide, a face detection system for wearing a mask was developed (the undergraduate project completed in 21 years, it is relatively old, and can be replaced by the latest target detection algorithm by itself).

Source code download

https://github.com/Egrt/YOLO_PyQt5
You can order a star if you like it.

Support features

  1. Support reading local pictures
  2. Support reading local video
  3. Support open camera real-time detection
  4. Support multi-threading to prevent stuck
  5. Support recording when a face is detected without a mask, and a voice warning

Interface display

PyQt5

PyQt5 is a popular GUI (Graphical User Interface) development framework in the Python language. Based on the Qt GUI application development framework, it provides a powerful tool set for creating various desktop applications. PyQt5 can be used to develop desktop applications, web applications and mobile applications, with good cross-platform and rich functions.

Signals and slots

Signals and slots are an important concept in PyQt5 and are mechanisms for organizing and managing interactions between GUI elements. Signals are events or actions emitted by GUI elements, and slots are functions that handle signals. When a signal occurs, the slot associated with it will be called automatically.

Below is a simple example code demonstrating how to use signals and slots in PyQt5. This example creates a window that contains a button and a label. When the user clicks the button, the label’s text will change:

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_())

In this sample code, we create a window class named MyWindow, which inherits from QWidget. In the MyWindow constructor, we create a button and a label, and use the clicked signal to connect the button’s click event to changeText slot function. When the button is clicked, the changeText slot function will be called, which will change the text of the label.

After running the code, you can see that there is a button and a label on the window, and the text of the label will change to “Button Clicked” after the button is clicked. This example demonstrates how to use signals and slots in PyQt5 to implement interactive GUI applications.

Function realization

Interface design

According to the task requirements, the interface can be divided into four parts:

  1. The button is placed on the top to realize the selection of reading pictures, videos, and opening the camera for real-time detection.
  2. A directory control is placed on the left to browse local files.
  3. The image processed by YOLO is shown in the middle.
  4. When processing video or reading camera detection in real time, if a face without a mask is continuously recognized in multiple frames, it will be recorded and a voice warning will be issued.

So write the code as follows:

class MyApp(QMainWindow):
    def __init__(self):
        super(MyApp, self).__init__()

        self.cap = cv2.VideoCapture()
        self.CAM_NUM = 0
        self.thread_status = False # Determine whether the recognition thread is enabled
        self.tool_bar = self.addToolBar('Toolbar')
        self.action_right_rotate = QAction(
            QIcon("icons/rotate right.png"), "rotate right 90", self)
        self.action_left_rotate = QAction(
            QIcon("icons/left rotation.png"), "rotate left 90°", self)
        self.action_opencam = QAction(QIcon("icons/camera.png"), "open camera", self)
        self.action_video = QAction(QIcon("icons/video.png"), "load video", self)
        self.action_image = QAction(QIcon("icons/image.png"), "load image", 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('directory'))
        self.dock_file.setFeatures(QDockWidget.NoDockWidgetFeatures)

        self.dock_attr = QDockWidget(self)
        self.dock_attr.setWidget(self.stackedWidget)
        self.dock_attr.setTitleBarWidget(QLabel('report data'))
        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('mask wearing detection')
        self.setWindowIcon(QIcon('icons/mask.png'))
        self.src_img = None
        self.cur_img = None

Slot function

Configure the interface of the window in the initialization and use connect to connect the signal and the slot function. When the signal occurs, the slot associated with it will be called automatically. The slot functions that control opening pictures, videos and local cameras are:

def openvideo(self):
   print(self. thread_status)
   if self. thread_status == False:

       fileName, filetype = QFileDialog.getOpenFileName(
           self, "Select Video", "D:/", "*.mp4;;*.flv;;All Files(*)")

       flag = self.cap.open(fileName)
       if flag == False:
           msg = QtWidgets.QMessageBox.warning(self, u"Warning", u"Please select a video file",
                                               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('close video')
           self. thread_status = True
   elif self. thread_status == True:
       self. detectThread. terminate()
       if self.cap.isOpened():
           self.cap.release()
       self.action_video.setText('open video')
       self. thread_status = False

def openimage(self):
   if self. thread_status == False:
       fileName, filetype = QFileDialog.getOpenFileName(
           self, "Select Image", "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"Warning", u"Please check whether the camera is connected to the computer correctly",
                                               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('close video')
           self. thread_status = True
   else:
       self. detectThread. terminate()
       if self.cap.isOpened():
           self.cap.release()
       self.action_video.setText('open video')
       self. thread_status = False

Multithreading

When reading video files or cameras, in order to avoid interface freezes, multi-threading is used for processing, and the thread needs to be closed to prevent the system from being stuck when the video file is finished processing, and self needs to be used when closing the camera. cap.release() releases the camera.

When processing continuous frames with multiple threads, the multi-threaded library QThread that comes with Qt is used:

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 # Whether to send a warning signal

    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()
        # read a frame
        frame = self.frame
        # Format conversion, BGRtoRGB
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        # Convert to Image
        frame = Image.fromarray(np.uint8(frame))
        # check
        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 meets the opencv display format
        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
        # Send pyqt signal
        self.Send_signal.emit(frame, self.warn)

Information record

If a face without a mask is recognized for 30 consecutive frames, a signal will be sent and displayed in the list on the right, and the current frame will be recorded:

def add_item(self, image):
    # Total Widget
    weight = QWidget()
    # overall horizontal layout
    layout_main = QHBoxLayout()
    map_l = QLabel() # picture display
    map_l. setFixedSize(60, 40)
    map_l.setPixmap(image.scaled(60, 40))
    # vertical layout on the right
    layout_right = QVBoxLayout()
    # Horizontal layout for bottom right
    layout_right_down = QHBoxLayout() # The horizontal layout of the lower right
    layout_right_down.addWidget(
        QLabel(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())))

    # Add according to the layout from left to right, top to bottom
    layout_main.addWidget(map_l) # leftmost image
    layout_right.addWidget(QLabel('Warning! No mask was detected')) # vertical layout on the right
    layout_right.addLayout(layout_right_down) # Horizontal layout in the lower right corner
    layout_main.addLayout(layout_right) # layout on the right
    wight.setLayout(layout_main) # layout for weight
    item = QListWidgetItem() # create QListWidgetItem object
    item.setSizeHint(QSize(300, 80)) # Set the size of QListWidgetItem
    self.stackedWidget.addItem(item) # add item
    self.stackedWidget.setItemWidget(item, weight) # set widget for item

Shut down the system

When shutting down the system, you need to make sure that multi-threading is turned off, and the camera that has been opened is turned off, otherwise it will also cause a freeze when exiting:

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"Close", u"Are you sure to exit?")
    msg.addButton(ok, QtWidgets.QMessageBox.ActionRole)
    msg.addButton(cacel, QtWidgets.QMessageBox.RejectRole)
    ok.setText(u'OK')
    cacel.setText(u'Cancel')
    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()

The final complete code is as follows:

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")

# Multi-thread real-time detection
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 # Whether to send a warning signal

    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()
        # read a frame
        frame = self.frame
        # Format conversion, BGRtoRGB
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        # Convert to Image
        frame = Image.fromarray(np.uint8(frame))
        # check
        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 meets the opencv display format
        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
        # Send pyqt signal
        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 # Determine whether the recognition thread is enabled
        self.tool_bar = self.addToolBar('Toolbar')
        self.action_right_rotate = QAction(
            QIcon("icons/rotate right.png"), "rotate right 90", self)
        self.action_left_rotate = QAction(
            QIcon("icons/left rotation.png"), "rotate left 90°", self)
        self.action_opencam = QAction(QIcon("icons/camera.png"), "open camera", self)
        self.action_video = QAction(QIcon("icons/video.png"), "load video", self)
        self.action_image = QAction(QIcon("icons/image.png"), "load image", 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('directory'))
        self.dock_file.setFeatures(QDockWidget.NoDockWidgetFeatures)

        self.dock_attr = QDockWidget(self)
        self.dock_attr.setWidget(self.stackedWidget)
        self.dock_attr.setTitleBarWidget(QLabel('report data'))
        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('mask wearing detection')
        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):
        # Total Widget
        weight = QWidget()
        # overall horizontal layout
        layout_main = QHBoxLayout()
        map_l = QLabel() # picture display
        map_l. setFixedSize(60, 40)
        map_l.setPixmap(image.scaled(60, 40))
        # vertical layout on the right
        layout_right = QVBoxLayout()
        # Horizontal layout for bottom right
        layout_right_down = QHBoxLayout() # The horizontal layout of the lower right
        layout_right_down.addWidget(
            QLabel(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())))

        # Add according to the layout from left to right, top to bottom
        layout_main.addWidget(map_l) # leftmost image
        layout_right.addWidget(QLabel('Warning! No mask was detected')) # vertical layout on the right
        layout_right.addLayout(layout_right_down) # Horizontal layout in the lower right corner
        layout_main.addLayout(layout_right) # layout on the right
        wight.setLayout(layout_main) # layout for weight
        item = QListWidgetItem() # create QListWidgetItem object
        item.setSizeHint(QSize(300, 80)) # Set the size of QListWidgetItem
        self.stackedWidget.addItem(item) # add item
        self.stackedWidget.setItemWidget(item, weight) # set widget for item

    def openvideo(self):
        print(self. thread_status)
        if self. thread_status == False:

            fileName, filetype = QFileDialog.getOpenFileName(
                self, "Select Video", "D:/", "*.mp4;;*.flv;;All Files(*)")

            flag = self.cap.open(fileName)
            if flag == False:
                msg = QtWidgets.QMessageBox.warning(self, u"Warning", u"Please select a video file",
                                                    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('close video')
                self. thread_status = True
        elif self. thread_status == True:
            self. detectThread. terminate()
            if self.cap.isOpened():
                self.cap.release()
            self.action_video.setText('open video')
            self. thread_status = False

    def openimage(self):
        if self. thread_status == False:
            fileName, filetype = QFileDialog.getOpenFileName(
                self, "Select Image", "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"Warning", u"Please check whether the camera is connected to the computer correctly",
                                                    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('close video')
                self. thread_status = True
        else:
            self. detectThread. terminate()
            if self.cap.isOpened():
                self.cap.release()
            self.action_video.setText('open video')
            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"Close", u"Are you sure to exit?")
        msg.addButton(ok, QtWidgets.QMessageBox.ActionRole)
        msg.addButton(cacel, QtWidgets.QMessageBox.RejectRole)
        ok.setText(u'OK')
        cacel.setText(u'Cancel')
        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__":
    # Initialize the yolo model
    yolo = YOLO()
    app = QApplication(sys. argv)
    app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
    window = MyApp()
    window. show()
    sys. exit(app. exec_())