编程学习网 > 编程语言 > Python > python qt教程(使用QT搭建人脸识别客户端)
2023
05-29

python qt教程(使用QT搭建人脸识别客户端)

1. 功能


使用python搭建一个人脸识别的客户端,用来连接窗口和实时捕获人脸,还可以批量加载图像,建立串口输入输出通讯。

2. 环境
windows和linux通用,建议带摄像头和串口的设备,USB连接的摄像头更好。

安装一些必备的包,QT和串口驱动(python3.8.10):

pip install PyQt=5.12
pip install pyserial


3. 导入一些必须的包



import cv2
import argparse
import numpy as np

from PyQt5 import QtWidgets
from PyQt5.QtGui import QImage, QPixmap, QIcon
from PyQt5.QtCore import pyqtSlot, Qt



4. 搭建QT的基本框架

QWidget类是所有用户界面对象的基类。

QWidget继承自QObject类和QPaintDevice类。QObject是所有支持Qt对象模型的对象的基类,QPaintDevice类是所有可以绘制的对象的基类。

几乎所有的部件都继承自QWidget。从理论上来讲,任何继承自QWidget的类的派生类的实例,都可以作为中心窗口部件使用。

def run_face_id_app():
    """
    Main function to initiate demo GUI.
    """
    parser = argparse.ArgumentParser()
    parser.add_argument('--uart_com_port', '-c', type=str, help='com port for UART communication')
    args = parser.parse_args()

    try:
        app = QtWidgets.QApplication([]) # 实例化一个应用对象 , app 就是整个工程
        app_window = FaceIdWindow(args.uart_com_port)# 窗口各组件的构建,人脸识别窗口的主函数。
        app_window.show()# 让控件在桌面上显示出来。控件在内存里创建,之后才能在显示器上显示出来。
        app.exit(app.exec_())# 确保主循环安全退出
    except Exception as ex: #pylint: disable=broad-except
        template = "An exception of type {0} occurred. Arguments:\n{1!r}"
        message = template.format(type(ex).__name__, ex.args)
        print(message)


if __name__ == "__main__":
    run_face_id_app()
    新建一个QtWidgets.QMainWindow class,在__init__内定义完页面布局,

构建主窗口:

#create GUI screen
 self.central_widget = QtWidgets.QWidget() #窗口界面的基本控件,它提供了基本的应用构造器。默认情况下,构造器是没有父级的,没有父级的构造器被称为窗口(window)。


5. 串口

构造一个下拉列表项:

#create dropdown menu to select environment for model execution
self.ai85_source_label = QtWidgets.QLabel('AI-85 Source: ', self.central_widget)#主窗口的标签栏
self.ai85_source_combo = QtWidgets.QComboBox(self.central_widget)#QComboBox提供了一种向用户呈现选项列表的方式
self.ai85_source_combo.addItem('<Select AI85 Device>')#下面是列表的可选项
self.ai85_source_combo.addItem('Simulator')
self.ai85_source_combo.addItem('Emulator')
self.ai85_source_combo.addItem('EV-Kit')
self.ai85_source_combo.activated.connect(self.__source_selected)#点击触发函数
self.ai85_source_combo.setCurrentIndex(3)
self.__source_selected()
self.ai85_source_combo.setDisabled(True)#不可选择,只能默认
其中是self.__source_selected()适配器函数,用来配置串口:包括串口连接、串口输入、串口读出、串口交互等。

class AI85UartAdapter(AI85Adapter):
    """
    Adapter for UART interface.
    """
    run_test_code = bytes([58])

    def __init__(self, port, baud_rate, embedding_len):
        super().__init__()
        self.ser = serial.Serial(port=port, baudrate=baud_rate, bytesize=8, parity='N', stopbits=1,
                                 timeout=0, xonxoff=0, rtscts=0)
        self.embedding_len = embedding_len

    def get_network_out(self, data):
        """Returns output of the neural network on device."""
        self.ser.flushInput()
        self.__connect_device()
        print('Connected')
        self.__transmit_data(data)
        time.sleep(1)
        print('Transmitted')
        model_out = self.__receive_data()
        self.__release_device()
        return model_out

    def __del__(self):
        pass

    def __connect_device(self):
        initial_device_message = b'Start_Sequence'
        print('Listening to connect device!')

        while True:
            sync_message = self.ser.read(len(initial_device_message))
            print(sync_message)
            if self.__check_synch_message(sync_message, initial_device_message):
                break
            time.sleep(0.05)
        print('From Device: %s' % sync_message)

        self.ser.write(self.run_test_code)
        time.sleep(1)

    def __check_synch_message(self, sync_message, initial_device_message): #pylint: disable=no-self-use
        if Counter(sync_message) == Counter(initial_device_message):
            return True
        return False

    def __release_device(self):
        final_device_message = b'End_Sequence'
        print('Listening to release device!')

        while True:
            sync_message = self.ser.read(len(final_device_message))
            print(sync_message)
            if sync_message == final_device_message:
                self.ser.write(bytes([100]))
                break
            time.sleep(1)
        print('From Device: %s' % sync_message)

    def __transmit_data(self, data):
        temp = np.int8(data - 128)
        byte_arr = temp.copy()
        byte_arr[:, :, 0] = temp[:, :, 2]
        byte_arr[:, :, 2] = temp[:, :, 0]
        byte_arr = byte_arr.tobytes()
        self.ser.write(byte_arr)

    def __receive_data(self):
        size = self.embedding_len
        array = b""
        while size > 0:
            len_to_read = self.ser.inWaiting()
            if size < len_to_read:
                len_to_read = size
            size = size - len_to_read
            array += self.ser.read(len_to_read)
        return np.frombuffer(array, np.int8)


6. 加载图像

接下来构建一个加载图像按钮:

#create button to load image
self.load_img_button = QtWidgets.QPushButton('Load Image', self.central_widget)
self.load_img_button.resize(100, 32) # 重置窗口大小
self.load_img_button.clicked.connect(self.__load_image)#按钮触发事件
按钮连接的self.__load_image是触发函数,我们可以利用该函数读取一副图像:

def __load_image(self):
        if not self.face_identifier.has_ai85_adapter():
            self.__show_adapter_error()
            return
        
        # 第一个参数parent,用于指定父组件。
        # 第二个参数caption,是对话框的标题;
        # 第三个参数dir ,打开路径,可以为空;
        # 第四个参数filter ,过滤器,过滤格式为
        # "Image Files (*.png *.jpg *.bmp)",当需要过滤多种格式是使用中间加两个下引号“;;”:
        # 比如:"Images (*.png *.xpm *.jpg);;Text files (*.txt);;XML files (*.xml);;all file(*)"
        img_path, _ = QtWidgets.QFileDialog.getOpenFileName(self, 'Open file', directory='',
                                                            filter="Image files (*.jpg *.jpeg *.bmp *.png) ") #pylint: disable=line-too-long
        if img_path == '':
            return

        print(img_path)


7. 摄像头

构建一个打开摄像头的按钮:

#create button to start camera
self.start_camera_button = QtWidgets.QPushButton('Start Cam', self.central_widget)
self.start_camera_button.resize(100, 32)
self.start_camera_button.clicked.connect(self.__init_camera)
摄像头初始化:

    def __init_camera(self):
        self.start_camera_button.setText('Starting...')

        if self.camera is None:
            self.camera = Camera(cam_num=self.cam_num, frame_size=self.frame_size)

            # run thread to get frames from camera
            self.thread = Thread(parent=None, camera=self.camera, frame_rate=self.frame_rate)
            #self.thread.change_pixmap.connect(lambda p: self.__set_preview_image(p))
            self.thread.change_pixmap.connect(self.__set_preview_image)
            self.thread.start()

            self.start_camera_button.setVisible(False)
            self.load_img_button.setVisible(False)
            self.capture_button.setVisible(True)
            self.stop_camera_button.setVisible(True)

        self.start_camera_button.setText('Start Cam')
我们利用cv2来打开设备的默认摄像头:

class Camera:
    """Captures image from selected camera and returns image inside drawed rectangle"""
    def __init__(self, cam_num=0, frame_size=(240, 320)):
        self.cap = cv2.VideoCapture(cam_num)
        self.cam_num = cam_num

        width = int(self.cap.get(3))  # int
        height = int(self.cap.get(4))  # int
        print(f'Webcam image size: ({width}, {height})')  # webcam_size

        frame_width = frame_size[0]
        frame_height = frame_size[1]

        self.thickness = 3
        self.start_point = ((width - frame_width) // 2, (height - frame_height) // 2)
        self.end_point = (self.start_point[0] + frame_width, self.start_point[1] + frame_height)
        self.color = (255, 0, 0)

    def get_frame(self):
        """Returns the next captured image"""
        rval, frame = self.cap.read()
        if rval:
            frame = cv2.rectangle(frame, tuple(q-self.thickness for q in self.start_point),
                                  tuple(q+self.thickness for q in self.end_point),
                                  self.color, self.thickness)
            frame = cv2.flip(frame, 1)
            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

        return rval, frame

    def close_camera(self):
        """Releases camera"""
        self.cap.release()

    def __str__(self):
        return f'OpenCV Camera {self.cam_num}'
这里使用双线程进行图像捕获和处理。QT 多线程的使用主要是通过 QThread来实现。QThread是Qt线程类中最核心的底层类。要使用QThread开始一个线程,必须创建一个QThread的子类,然后重写QThread.run方法。在使用线程时,可以直接得到Thread实例,调用其start()方法即可启动线程。。

class Thread(QThread):
    """
    Thread to capture image from camera
    """
    # 自定义信号
    change_pixmap = pyqtSignal(QImage)

    # 构造函数,接受参数
    def __init__(self, parent=None, camera=None, frame_rate=25):
        QThread.__init__(self, parent=parent)
        self.camera = camera
        self.emit_period = 1.0 / frame_rate

    # 重写run()方法
    def run(self):
        """Runs camera capture"""
        prev = time.time()
        while True:
            now = time.time()
            # 图像提取框
            rval, frame = self.camera.get_frame()
            # 转换图片格式
            if rval:
                convert_qt_format = cvt_img_to_qimage(frame)
                qt_img = convert_qt_format.scaled(640, 480, Qt.KeepAspectRatio)
                if (now - prev) >= self.emit_period:
                    # 发射信号,执行它所连接的函数
                    self.change_pixmap.emit(qt_img)
                    prev = now


8. 从摄像中捕获图像

新建一个capture_button按钮,因为需要用到前面的子线程,为防止陷入死锁或来不及处理捕获的图像,特别设置了一个忙标志,只允许一个capture的存在。

#create button to capture image
self.capture_button = QtWidgets.QPushButton('Capture', self.central_widget)
self.capture_button.resize(100, 32)
self.capture_button.clicked.connect(self.__capture_button_pressed)
self.capture_button.setVisible(False)
self.capture_busy = False


按下按钮直至捕获结束,self.capture_busy都处于true状态。

    def __capture_button_pressed(self):
        if not self.capture_busy:
            self.capture_busy = True

            if not self.face_identifier.has_ai85_adapter():
                self.__show_adapter_error()
                return

            capture = self.preview_frame.pixmap()
            capture = capture.toImage()

            captured_img = cvt_qimage_to_img(capture)

            cropped_img = captured_img[self.camera.start_point[1]:self.camera.end_point[1],
                                       self.camera.start_point[0]:self.camera.end_point[0]]
            cropped_img = cv2.resize(cropped_img, self.capture_size)
            cropped_img = cv2.cvtColor(cropped_img, cv2.COLOR_BGR2RGB).copy()

            self.__set_captured_image(cvt_img_to_qimage(cropped_img))
            self.__identify_face(cropped_img)

            self.capture_busy = False
到这里,既然要从摄像视频流中截取图像帧,那就需要先将其显示出来,截取的图像也需要可视化,如上capture_button_pressed函数的self.preview_frame.pixmap() 和 self.__set_captured_image() ,两者都是QLabel类:



#create view to show camera stream
self.preview_frame = QtWidgets.QLabel('Preview', self.central_widget)
self.preview_frame.resize(self.img_size[0], self.img_size[1])
self.preview_black_img = QPixmap(self.img_size[1], self.img_size[0])
self.preview_black_img.fill(Qt.black)
self.preview_black_img = self.preview_black_img.toImage()
self.__set_preview_image(self.preview_black_img)

#create view to show captures frame
self.captured_frame = QtWidgets.QLabel('Capture', self.central_widget)
self.captured_frame.resize(self.capture_size[0], self.capture_size[1])
black_img = QPixmap(self.capture_size[0], self.capture_size[1])
black_img.fill(Qt.black)
black_img = black_img.toImage()
self.__set_captured_image(black_img)
初始化是两组黑底图,视频流截取时会分别调用self.__set_preview_image 和 self.__set_captured_image 两个函数进行实时可视化。



9. 图像处理

如果只是截取图像并显示,那么我们现在任务就基本结束了,但我们还可以添加一些复杂的任务,比如在截取图像之后对图像进行人脸识别,前面的self.__identify_face(cropped_img)就是实现这样的任务。

    def __identify_face(self, cropped_img):
        # identify face
        subject_id, dist, ai85_time, db_match_time = self.face_identifier.run(cropped_img)

        prob = self.__cvt_dist_to_prob(dist)

        self.subject_text.setText(self.subj_name_map[subject_id[0]])
        self.ai85_time_text.setText('%.3f' % (1000 * ai85_time))
        self.db_time_label_text.setText('%.3f' % (1000 * db_match_time))
        self.ai85_energy_text.setText('N/A')

        self.top1_subj_text.setText(self.subj_name_map[subject_id[0]])
        self.top1_subj_dist.setText('%.2f' % prob[0])
        self.top2_subj_text.setText(self.subj_name_map[subject_id[1]])
        self.top2_subj_dist.setText('%.2f' % prob[1])
        self.top3_subj_text.setText(self.subj_name_map[subject_id[2]])
        self.top3_subj_dist.setText('%.2f' % prob[2])


10. 页面整体布局

    基本原理是把窗口划分为若干个单元格,每个子部件被放置于一个或多个单元格之中,各单元格的大小可由拉伸因子和一行或列中单元格的数量来确定,若子部件的大小(由sizeHint()确定)小于单元格,则可以设置该子部件位于单元格的什么位置(顶部、左侧、底部等),还可设置该子部件是否可以拉伸以填满该单元格等等。

        #create layout that includes the ui components
        self.layout = QtWidgets.QGridLayout(self.central_widget)

        self.layout.addWidget(self.ai85_source_label, 0, 0)
        self.layout.addWidget(self.ai85_source_combo, 0, 1)

        self.layout.addWidget(self.load_img_button, 0, 4)
        self.layout.addWidget(self.start_camera_button, 0, 5)
        self.layout.addWidget(self.capture_button, 0, 5)
        self.layout.addWidget(self.stop_camera_button, 1, 5)

        self.layout.addWidget(self.preview_frame, 2, 0, 21, 6)
        self.layout.addWidget(self.captured_frame, 2, 7, 8, 4)

        self.layout.addLayout(self.result_box, 10, 6, 4, 3)
        self.layout.addWidget(self.result_table, 14, 6, 6, 4)

        self.setCentralWidget(self.central_widget)


11. 任务结束

最后,任务结束和进程退出的设计是所有软件设计的应有之义。

首先要关闭摄像头:

# create button to stop camera
self.stop_camera_button = QtWidgets.QPushButton('Stop Cam', self.central_widget)
self.stop_camera_button.resize(100, 32)
self.stop_camera_button.clicked.connect(self.__stop_camera)
self.stop_camera_button.setVisible(False)
同时要把相关的处理线程关闭:

    def __stop_camera(self):
        if self.camera is not None:
            self.thread.terminate()
            self.camera.close_camera()

            self.__set_preview_image(self.preview_black_img)

            self.start_camera_button.setVisible(True)
            self.capture_button.setVisible(False)
            self.stop_camera_button.setVisible(False)
            self.load_img_button.setVisible(True)

            del self.camera
            self.camera = None


12. 结果概览

以上就是python qt教程(使用QT搭建人脸识别客户端)的详细内容,想要了解更多Python教程欢迎持续关注编程学习网。

扫码二维码 获取免费视频学习资料

Python编程学习

查 看2022高级编程视频教程免费获取