一、项目结构

在这里插入图片描述

二、模块概述

PyQt 界面模块是人脸识别系统与用户交互的桥梁,负责将人脸检测模块的功能以可视化的方式呈现给用户,并接收用户的操作指令。

本模块主要完成以下任务:

  1. 设计并实现直观友好的图形用户界面
  2. 实现摄像头实时画面的采集与显示
  3. 提供开始 / 停止检测、加载本地图片
  4. 调用人脸检测模块对实时画面或本地图片进行处理
  5. 在界面上实时标注检测到的人脸区域
  6. 数据库人脸查看、搜索、删除

技术栈:Python 3.10+、PyQt5、OpenCV

三、人脸识别模块需修改的代码

人脸保存

import cv2
import numpy as np
import torch
from facenet_pytorch import InceptionResnetV1
from ultralytics import YOLO

from FaceNet.DB import DB


class FaceSave:
    def __init__(self):
        # 初始化设备
        self.device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
        print(f"Using device: {self.device}")

        # 加载前面训练出来的模型,检测人脸
        self.yolo_model = YOLO('../FaceDetection/runs/detect/train/weights/best.pt').to(self.device)
        # 加载 FaceNet 模型,提取人脸特征
        self.faceNet_model = InceptionResnetV1(pretrained='vggface2').eval()
        # 定义数据库,连接数据库
        self.db = DB()


    # 对人脸图像进行预处理
    def pretreatment_face(self, face):
        pred_face = cv2.cvtColor(face, cv2.COLOR_BGR2RGB)
        # 调整大小,FaceNet对图像大小有要求
        pred_face = cv2.resize(pred_face, (160, 160))
        # 归一化
        pred_face = (pred_face/255.0 - 0.5) / 0.5
        # 将图像转换成张量
        # 在 PyTorch 中,图像张量的标准格式通常要求通道维度在前
        # permute(2, 0, 1)将图像的维度从HWC (高度、宽度、通道) 转换为CHW (通道、高度、宽度)
        # 模型期望输入是4维的 [B, C, H, W]
        pred_face = torch.tensor(pred_face).permute(2, 0, 1).float().unsqueeze(0)
        return pred_face

    def detect_and_features(self, name, img, ext):

        results = self.yolo_model(img)
        for result in results:
            # 检测的人脸边框
            boxes = result.boxes
            # 处理人脸
            for box in boxes:
                # print(box)
                x1, y1, x2, y2 = np.int32(box.xyxy.cpu()[0])
                # 裁剪人脸
                face = img[y1:y2, x1:x2]
                # 预处理人脸
                pred_face = self.pretreatment_face(face)
                # 提取人脸特征
                with torch.no_grad():
                    face_feature = self.faceNet_model(pred_face).detach()
                # 保存
                self.db.insert(name, face_feature, img, ext)

数据库

import datetime
import sqlite3

import cv2
import numpy as np
import pandas as pd
import torch

class DB:
    def __init__(self):
        # 数据库地址
        self.db_path = "./face_database.db"

        # 初始化数据库表结构
        self.conn = None
        self.cursor = None
        self.open()

        # 如果表不存在,创建人脸数据表

        # 特征和图像用字节的方式保存
        self.cursor.execute('''
            CREATE TABLE IF NOT EXISTS faces (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
                feature BLOB NOT NULL,
                image BLOB NOT NULL
            )
        ''')
        self.conn.commit()
    # 连接数据库
    def open(self):
        self.conn = sqlite3.connect(self.db_path)
        self.cursor = self.conn.cursor()

    # 将Tensor转换为字节
    def tensor_to_bytes(self, tensor):
        if tensor.dim() > 1:
            tensor = tensor.squeeze()

        # 保存为numpy数组的字节形式
        np_array = tensor.cpu().numpy() if tensor.is_cuda else tensor.numpy()
        return np_array.tobytes()

    # 将字节转换成Tensor
    def bytes_to_tensor(self, bytes_data):
        # 从字节重建numpy数组
        # np.frombuffer得到的是不可写的 NumPy 数组
        np_array = np.frombuffer(bytes_data, dtype=np.float32)
        # PyTorch 期望张量是可写的
        # 创建可写副本
        np_array = np_array.copy()
        return torch.from_numpy(np_array).reshape(1, -1)

    # 添加操作
    def insert(self, name, feature, image, ext):
        # 将特征张量转换为字节
        feature_bytes = self.tensor_to_bytes(feature)

        # # 得到图像和图片的扩展名(转字节需要)
        # image = cv2.imread(image_path)
        # ext = os.path.splitext(image_path)[1]

        # 转换图像为字节
        success, image_bytes = cv2.imencode(ext, image)
        if not success:
            raise ValueError("图像编码失败")
        # 存入时间
        created_time = datetime.datetime.now()
        # 插入
        self.cursor.execute('''
                INSERT INTO faces (name, created_time, feature, image)
                VALUES (?, ?, ?, ?)
                ''', (name, created_time, feature_bytes, image_bytes))
        # 提交
        self.conn.commit()

    # 删除操作(根据Id)
    def delete(self, id):
        self.cursor.execute("DELETE FROM faces WHERE id = ?", (id,))
        self.conn.commit()

    # 查询操作(根据名字)
    def select_one(self, name):
        query = "SELECT * FROM faces WHERE name = ?"
        data = pd.read_sql_query(query, self.conn, params=(name, ))
        data.drop('feature', axis=1, inplace=True)
        return data

    # 获取数据库的特征内容
    def select_features(self):
        # 使用 pandas 读取数据
        query = "SELECT * FROM faces"
        datas = pd.read_sql_query(query, self.conn)
        # 用一个字典存储
        features = {}
        if not datas.empty:
            for name, feature in zip(datas.name, datas.feature):
                features[name] = self.bytes_to_tensor(feature)
        return features

    # 查看数据库内容
    def select_all(self):
        # 使用 pandas 读取数据
        query = "SELECT * FROM faces"
        datas = pd.read_sql_query(query, self.conn)
        datas.drop('feature', axis=1, inplace=True)
        return datas

    # 关闭数据库
    def close(self):
        self.cursor.close()
        self.conn.close()

四、PyQt代码实现

1.启动程序

import sys
from PyQt5.QtWidgets import QApplication
from PyQt.controller import Controller

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

2.前端代码

很简洁,感兴趣的小伙伴自行美观

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'test.ui'
#
# Created by: PyQt5 UI code generator 5.15.11
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(1000, 721)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.navWidget = QtWidgets.QWidget(self.centralwidget)
        self.navWidget.setMinimumSize(QtCore.QSize(150, 0))
        self.navWidget.setMaximumSize(QtCore.QSize(150, 16777215))
        self.navWidget.setObjectName("navWidget")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.navWidget)
        self.verticalLayout.setObjectName("verticalLayout")
        spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
        self.verticalLayout.addItem(spacerItem)
        self.btn_register = QtWidgets.QPushButton(self.navWidget)
        font = QtGui.QFont()
        font.setFamily("Arial")
        font.setPointSize(10)
        self.btn_register.setFont(font)
        self.btn_register.setObjectName("btn_register")
        self.verticalLayout.addWidget(self.btn_register)
        spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
        self.verticalLayout.addItem(spacerItem1)
        self.btn_recognize = QtWidgets.QPushButton(self.navWidget)
        font = QtGui.QFont()
        font.setFamily("Arial")
        font.setPointSize(10)
        self.btn_recognize.setFont(font)
        self.btn_recognize.setObjectName("btn_recognize")
        self.verticalLayout.addWidget(self.btn_recognize)
        spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
        self.verticalLayout.addItem(spacerItem2)
        self.btn_view = QtWidgets.QPushButton(self.navWidget)
        font = QtGui.QFont()
        font.setFamily("Arial")
        font.setPointSize(10)
        self.btn_view.setFont(font)
        self.btn_view.setObjectName("btn_view")
        self.verticalLayout.addWidget(self.btn_view)
        spacerItem3 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
        self.verticalLayout.addItem(spacerItem3)
        self.horizontalLayout.addWidget(self.navWidget)
        self.stacked_widget = QtWidgets.QStackedWidget(self.centralwidget)
        self.stacked_widget.setObjectName("stacked_widget")
        self.register_page = QtWidgets.QWidget()
        self.register_page.setObjectName("register_page")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.register_page)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.registerTitle = QtWidgets.QLabel(self.register_page)
        self.registerTitle.setAlignment(QtCore.Qt.AlignCenter)
        self.registerTitle.setObjectName("registerTitle")
        self.verticalLayout_2.addWidget(self.registerTitle)
        self.imageWidget = QtWidgets.QWidget(self.register_page)
        self.imageWidget.setObjectName("imageWidget")
        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.imageWidget)
        self.verticalLayout_3.setObjectName("verticalLayout_3")
        self.register_image_label = QtWidgets.QLabel(self.imageWidget)
        self.register_image_label.setMinimumSize(QtCore.QSize(400, 300))
        self.register_image_label.setAlignment(QtCore.Qt.AlignCenter)
        self.register_image_label.setObjectName("register_image_label")
        self.verticalLayout_3.addWidget(self.register_image_label)
        self.verticalLayout_2.addWidget(self.imageWidget)
        self.btnWidget1 = QtWidgets.QWidget(self.register_page)
        self.btnWidget1.setObjectName("btnWidget1")
        self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.btnWidget1)
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        self.btn_select_image = QtWidgets.QPushButton(self.btnWidget1)
        self.btn_select_image.setObjectName("btn_select_image")
        self.horizontalLayout_2.addWidget(self.btn_select_image)
        self.btn_open_camera = QtWidgets.QPushButton(self.btnWidget1)
        self.btn_open_camera.setObjectName("btn_open_camera")
        self.horizontalLayout_2.addWidget(self.btn_open_camera)
        self.verticalLayout_2.addWidget(self.btnWidget1)
        self.infoWidget = QtWidgets.QWidget(self.register_page)
        self.infoWidget.setObjectName("infoWidget")
        self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.infoWidget)
        self.verticalLayout_4.setObjectName("verticalLayout_4")
        self.nameWidget = QtWidgets.QWidget(self.infoWidget)
        self.nameWidget.setObjectName("nameWidget")
        self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.nameWidget)
        self.horizontalLayout_3.setObjectName("horizontalLayout_3")
        self.nameLabel = QtWidgets.QLabel(self.nameWidget)
        self.nameLabel.setObjectName("nameLabel")
        self.horizontalLayout_3.addWidget(self.nameLabel)
        self.name_input = QtWidgets.QLineEdit(self.nameWidget)
        self.name_input.setObjectName("name_input")
        self.horizontalLayout_3.addWidget(self.name_input)
        self.verticalLayout_4.addWidget(self.nameWidget)
        self.idWidget = QtWidgets.QWidget(self.infoWidget)
        self.idWidget.setObjectName("idWidget")
        self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.idWidget)
        self.horizontalLayout_4.setObjectName("horizontalLayout_4")
        self.btn_save_face = QtWidgets.QPushButton(self.idWidget)
        self.btn_save_face.setObjectName("btn_save_face")
        self.horizontalLayout_4.addWidget(self.btn_save_face)
        self.verticalLayout_4.addWidget(self.idWidget)
        self.verticalLayout_2.addWidget(self.infoWidget)
        self.stacked_widget.addWidget(self.register_page)
        self.recognize_page = QtWidgets.QWidget()
        self.recognize_page.setObjectName("recognize_page")
        self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.recognize_page)
        self.verticalLayout_5.setObjectName("verticalLayout_5")
        self.recognizeTitle = QtWidgets.QLabel(self.recognize_page)
        self.recognizeTitle.setAlignment(QtCore.Qt.AlignCenter)
        self.recognizeTitle.setObjectName("recognizeTitle")
        self.verticalLayout_5.addWidget(self.recognizeTitle)
        self.recImageWidget = QtWidgets.QWidget(self.recognize_page)
        self.recImageWidget.setObjectName("recImageWidget")
        self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.recImageWidget)
        self.verticalLayout_6.setObjectName("verticalLayout_6")
        self.recognize_image_label = QtWidgets.QLabel(self.recImageWidget)
        self.recognize_image_label.setMinimumSize(QtCore.QSize(400, 300))
        self.recognize_image_label.setAlignment(QtCore.Qt.AlignCenter)
        self.recognize_image_label.setObjectName("recognize_image_label")
        self.verticalLayout_6.addWidget(self.recognize_image_label)
        self.verticalLayout_5.addWidget(self.recImageWidget)
        self.resultWidget = QtWidgets.QWidget(self.recognize_page)
        self.resultWidget.setObjectName("resultWidget")
        self.verticalLayout_7 = QtWidgets.QVBoxLayout(self.resultWidget)
        self.verticalLayout_7.setObjectName("verticalLayout_7")
        self.recognize_result = QtWidgets.QLabel(self.resultWidget)
        self.recognize_result.setObjectName("recognize_result")
        self.verticalLayout_7.addWidget(self.recognize_result)
        self.detail_text = QtWidgets.QTextEdit(self.resultWidget)
        self.detail_text.setObjectName("detail_text")
        self.verticalLayout_7.addWidget(self.detail_text)
        self.verticalLayout_5.addWidget(self.resultWidget)
        self.btnWidget2 = QtWidgets.QWidget(self.recognize_page)
        self.btnWidget2.setObjectName("btnWidget2")
        self.horizontalLayout_6 = QtWidgets.QHBoxLayout(self.btnWidget2)
        self.horizontalLayout_6.setObjectName("horizontalLayout_6")
        self.btn_select_image_rec = QtWidgets.QPushButton(self.btnWidget2)
        self.btn_select_image_rec.setObjectName("btn_select_image_rec")
        self.horizontalLayout_6.addWidget(self.btn_select_image_rec)
        self.btn_open_camera_rec = QtWidgets.QPushButton(self.btnWidget2)
        self.btn_open_camera_rec.setObjectName("btn_open_camera_rec")
        self.horizontalLayout_6.addWidget(self.btn_open_camera_rec)
        self.btn_recognize_face = QtWidgets.QPushButton(self.btnWidget2)
        self.btn_recognize_face.setObjectName("btn_recognize_face")
        self.horizontalLayout_6.addWidget(self.btn_recognize_face)
        self.verticalLayout_5.addWidget(self.btnWidget2)
        self.stacked_widget.addWidget(self.recognize_page)
        self.view_page = QtWidgets.QWidget()
        self.view_page.setObjectName("view_page")
        self.verticalLayout_8 = QtWidgets.QVBoxLayout(self.view_page)
        self.verticalLayout_8.setObjectName("verticalLayout_8")
        self.viewTitle = QtWidgets.QLabel(self.view_page)
        self.viewTitle.setAlignment(QtCore.Qt.AlignCenter)
        self.viewTitle.setObjectName("viewTitle")
        self.verticalLayout_8.addWidget(self.viewTitle)
        self.searchWidget = QtWidgets.QWidget(self.view_page)
        self.searchWidget.setObjectName("searchWidget")
        self.horizontalLayout_7 = QtWidgets.QHBoxLayout(self.searchWidget)
        self.horizontalLayout_7.setObjectName("horizontalLayout_7")
        self.search_input = QtWidgets.QLineEdit(self.searchWidget)
        self.search_input.setObjectName("search_input")
        self.horizontalLayout_7.addWidget(self.search_input)
        self.btn_search = QtWidgets.QPushButton(self.searchWidget)
        self.btn_search.setObjectName("btn_search")
        self.horizontalLayout_7.addWidget(self.btn_search)
        self.btn_show_all = QtWidgets.QPushButton(self.searchWidget)
        self.btn_show_all.setObjectName("btn_show_all")
        self.horizontalLayout_7.addWidget(self.btn_show_all)
        self.verticalLayout_8.addWidget(self.searchWidget)
        self.faces_table = QtWidgets.QTableWidget(self.view_page)
        self.faces_table.setColumnCount(5)
        self.faces_table.setObjectName("faces_table")
        self.faces_table.setRowCount(0)
        item = QtWidgets.QTableWidgetItem()
        self.faces_table.setHorizontalHeaderItem(0, item)
        item = QtWidgets.QTableWidgetItem()
        self.faces_table.setHorizontalHeaderItem(1, item)
        item = QtWidgets.QTableWidgetItem()
        self.faces_table.setHorizontalHeaderItem(2, item)
        item = QtWidgets.QTableWidgetItem()
        self.faces_table.setHorizontalHeaderItem(3, item)
        item = QtWidgets.QTableWidgetItem()
        self.faces_table.setHorizontalHeaderItem(4, item)
        self.verticalLayout_8.addWidget(self.faces_table)
        self.stacked_widget.addWidget(self.view_page)
        self.horizontalLayout.addWidget(self.stacked_widget)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 1000, 26))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        self.stacked_widget.setCurrentIndex(0)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "人脸识别系统"))
        self.btn_register.setText(_translate("MainWindow", "录入人脸"))
        self.btn_recognize.setText(_translate("MainWindow", "识别人脸"))
        self.btn_view.setText(_translate("MainWindow", "人脸查看"))
        self.registerTitle.setStyleSheet(_translate("MainWindow", "font-size: 18pt; font-weight: bold; margin: 10px;"))
        self.registerTitle.setText(_translate("MainWindow", "人脸录入"))
        self.register_image_label.setStyleSheet(_translate("MainWindow", "border: 1px solid gray; background-color: #f0f0f0;"))
        self.register_image_label.setText(_translate("MainWindow", "图像预览区域"))
        self.btn_select_image.setText(_translate("MainWindow", "选择图片"))
        self.btn_open_camera.setText(_translate("MainWindow", "打开摄像头"))
        self.nameLabel.setText(_translate("MainWindow", "姓名:"))
        self.name_input.setPlaceholderText(_translate("MainWindow", "请输入姓名"))
        self.btn_save_face.setText(_translate("MainWindow", "保存人脸"))
        self.recognizeTitle.setStyleSheet(_translate("MainWindow", "font-size: 18pt; font-weight: bold; margin: 10px;"))
        self.recognizeTitle.setText(_translate("MainWindow", "人脸识别"))
        self.recognize_image_label.setStyleSheet(_translate("MainWindow", "border: 1px solid gray; background-color: #f0f0f0;"))
        self.recognize_image_label.setText(_translate("MainWindow", "图像预览区域"))
        self.recognize_result.setStyleSheet(_translate("MainWindow", "font-size: 14pt; margin: 10px;"))
        self.recognize_result.setText(_translate("MainWindow", "等待识别..."))
        self.detail_text.setPlaceholderText(_translate("MainWindow", "识别详细信息将显示在这里..."))
        self.btn_select_image_rec.setText(_translate("MainWindow", "选择图片"))
        self.btn_open_camera_rec.setText(_translate("MainWindow", "打开摄像头"))
        self.btn_recognize_face.setText(_translate("MainWindow", "识别人脸"))
        self.viewTitle.setStyleSheet(_translate("MainWindow", "font-size: 18pt; font-weight: bold; margin: 10px;"))
        self.viewTitle.setText(_translate("MainWindow", "人脸数据库"))
        self.search_input.setPlaceholderText(_translate("MainWindow", "输入姓名搜索..."))
        self.btn_search.setText(_translate("MainWindow", "搜索"))
        self.btn_show_all.setText(_translate("MainWindow", "显示全部"))
        item = self.faces_table.horizontalHeaderItem(0)
        item.setText(_translate("MainWindow", "ID"))
        item = self.faces_table.horizontalHeaderItem(1)
        item.setText(_translate("MainWindow", "姓名"))
        item = self.faces_table.horizontalHeaderItem(2)
        item.setText(_translate("MainWindow", "录入时间"))
        item = self.faces_table.horizontalHeaderItem(3)
        item.setText(_translate("MainWindow", "图像"))
        item = self.faces_table.horizontalHeaderItem(4)
        item.setText(_translate("MainWindow", "操作"))

3.后端代码

初始化

def __init__(self):
    super(Controller, self).__init__()
    self.ui = Ui_MainWindow()
    self.ui.setupUi(self)

    # 摄像头
    self.camera = None
    # 定时器
    self.timer = QTimer(self)

    # 摄像头暂存的最后一个画面
    self.last_frame = None
    # 打开的图片的文件地址
    self.last_path = None

    # 连接数据库
    self.db = DB()
    self.save = FaceSave()
    self.recognition = FaceRecognition()
    # 数据库中数据
    self.datas = np.array(self.db.select_all())

    # 当前显示的界面的图像显示区域
    self.interface = self.ui.register_image_label

    # 识别模式
    self.pattern = False

    # 显示所有数据
    self.show_all()

    # 连接信号
    self.connect_signals()

切换界面

# 切换界面
def change_interface(self, index):
    # 切换界面自动关闭摄像头
    if self.camera is not None:
        self.timer.stop()
        self.camera.release()
        self.camera = None
        self.interface.clear()
    self.ui.stacked_widget.setCurrentIndex(index)
    if index == 0:
        self.interface = self.ui.register_image_label
    elif index == 1:
        self.interface = self.ui.recognize_image_label
    else:
        self.interface = None

选择图片

# 选择图片
def select_image(self):
    if self.camera is None:
        # 打开文件对话框
        file_path =QFileDialog.getOpenFileName(self, "选择图片文件", "", "图片文件 (*.png *.jpg *.jpeg *.bmp *.gif);;所有文件 (*)")
        if len(file_path[0]) == 0:
                return
        # 如果用户选择了文件
        if file_path:
            # 获取文件路径
            self.last_path = file_path[0]
            img = cv2.imread(self.last_path)
            self.show_frame(img)
        else:
            self.interface.setText("无法加载图片")
    elif self.camera is not None and self.timer.isActive():
        # 停止定时器
        self.timer.stop()
        if self.interface == self.ui.register_image_label:
            self.ui.btn_select_image.setText("重新选择")
        else:
            self.ui.btn_select_image_rec.setText("重新选择")
    else:
        if self.interface == self.ui.register_image_label:
            self.ui.btn_select_image.setText("选择图像")
        else:
            self.ui.btn_select_image_rec.setText("选择图像")
        self.timer.start(30)

打开摄像头

# 打开摄像头
def open_camera(self):
    if self.camera is None:
        self.camera = cv2.VideoCapture(0)
        if not self.camera.isOpened():
            self.interface.setText("无法打开摄像头,请检查设备是否正常")
            return
        if self.interface == self.ui.register_image_label:
            self.ui.btn_open_camera.setText("关闭摄像头")
        else:
            self.ui.btn_open_camera_rec.setText("关闭摄像头")
        # 启用定时器,每隔30毫秒刷新一次画面
        self.timer.start(30)
    else:
        # 停止定时器
        self.timer.stop()
        # 释放摄像头资源
        self.camera.release()
        self.camera = None
        if self.interface == self.ui.register_image_label:
            self.ui.btn_open_camera.setText("打开摄像头")
            self.ui.btn_select_image.setText("选择图像")
        else:
            self.ui.btn_open_camera_rec.setText("打开摄像头")
            self.ui.btn_select_image_rec.setText("选择图像")

        self.interface.clear()
        self.interface.setText("图像预览区域")
    self.timer.timeout.connect(lambda: self.update_frame())  # 定时刷新画面
    # self.update_frame(interface)

更新摄像头画面

# 更新摄像头画面
def update_frame(self):
    # 读取摄像头帧
    ret, frame = self.camera.read()

    if not ret:
        self.interface.setText("无法获取画面")
        return
    # 识别模式
    if self.pattern:
        # 识别
        face_num, frame = self.recognition.detect_and_recognize(frame)
        self.ui.detail_text.setText(f'识别了{face_num}张人脸')

    # 翻转
    # frame = cv2.flip(frame, 1)
    self.last_frame = frame
    self.show_frame(frame)

显示图像

# 显示图像
def show_frame(self, frame):
    # 转换图像格式(OpenCV默认是BGR格式,需要转换为RGB)
    frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    # 调整图像大小以适应显示区域
    height, width, channel = frame.shape
    bytes_per_line = channel * width

    # 创建QImage对象并转换为QPixmap
    q_image = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888)
    pixmap = QPixmap.fromImage(q_image)
    # 按比例缩放图片以适应标签
    scaled_pixmap = pixmap.scaled(
        self.interface.width(),
        self.interface.height(),
        Qt.KeepAspectRatio,
        Qt.SmoothTransformation
    )
    # 设置图片
    self.interface.setPixmap(scaled_pixmap)
    self.interface.setText("")  # 清空标签文本

人脸录入

# 录入人脸
def save_face(self):
    if self.interface.text() == "图像预览区域" or self.timer.isActive():
        QMessageBox.warning(
            None,
            "提示",
            "请先选择图像",
            QMessageBox.Ok
        )
        return

    name = self.ui.name_input.text()
    if name is None or name == '':
        QMessageBox.warning(
            None,
            "提示",
            "姓名不能为空",
            QMessageBox.Ok
        )
        return
    if self.camera is not None:
        # 保存人脸
        res = QMessageBox.warning(
            None,
            "提示",
            "确认保存",
            QMessageBox.Yes | QMessageBox.No
        )
        if res == QMessageBox.No:
            return
        else:
            self.save.detect_and_features(name, self.last_frame, '.jpg')
            QMessageBox.warning(
                None,
                "提示",
                "保存成功!",
                QMessageBox.Ok
            )
    else:
        img = cv2.imread(self.last_path)
        _, ext = os.path.splitext(self.last_path)
        # 保存人脸
        res = QMessageBox.warning(
            None,
            "提示",
            "确认保存",
            QMessageBox.Yes | QMessageBox.No
        )
        if res == QMessageBox.No:
            return
        else:
            self.save.detect_and_features(name, img, ext)
            QMessageBox.warning(
                None,
                "提示",
                "保存成功!",
                QMessageBox.Ok
            )
    self.show_all()

人脸识别

# 识别人脸
def recognize_face(self):
    if self.interface.text() == "图像预览区域":
        QMessageBox.warning(
            None,
            "提示",
            "无图像可识别",
            QMessageBox.Ok
        )
        return

    if not self.pattern:
        self.pattern = True
        self.ui.btn_recognize_face.setText("取消识别")
        self.ui.recognize_result.setText("识别中")
        # self.ui.detail_text.setText()
    else:
        self.pattern = False
        self.ui.btn_recognize_face.setText("识别人脸")
        self.ui.recognize_result.setText("等待识别...")
        self.ui.detail_text.setText("")
    # 视频
    if self.camera is not None:
        self.update_frame()
    # 图片
    else:
        img = cv2.imread(self.last_path)
        if self.pattern:
            face_num, img_recognition = self.recognition.detect_and_recognize(img)
            self.ui.detail_text.setText(f'识别了{face_num}张人脸')
            # 显示识别图片
            self.show_frame(img_recognition)
        else:
            self.show_frame(img)

人脸查看

# 显示图像
def on_view_click(self, img_bytes):
    # 将字节数据转换为 NumPy 数组
    img_array = np.frombuffer(img_bytes, dtype=np.uint8)
    # 使用 OpenCV 解码字节数组为图像
    img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
    # cv2.imshow('view', img)
    # cv2.waitKey(0)
    # 创建弹窗显示详情
    dialog = QDialog(self)
    dialog.resize(600, 600)
    layout = QVBoxLayout(dialog)
    info_label = QLabel()
    info_label.resize(600, 600)
    info_label.setText("dsdad")
    layout.addWidget(info_label)
    self.interface = info_label
    self.show_frame(img)
    dialog.exec_()

# 删除数据
def on_delete_click(self, face_id):
    # 删除
    self.db.delete(face_id)
    # 刷新
    self.show_all()

# 为指定行添加操作按钮(查看 / 删除)
def add_action_buttons(self, row_idx, img_bytes, face_id):

    # 创建按钮容器(避免按钮在单元格中拉伸变形)
    btn_container1 = QWidget()
    btn_layout1 = QVBoxLayout(btn_container1)
    btn_layout1.setContentsMargins(2, 2, 2, 2)  # 减小边距
    btn_layout1.setSpacing(3)  # 按钮间距

    # 查看按钮
    view_btn = QPushButton("查看")
    view_btn.setStyleSheet("""
        QPushButton {
            background-color: #4CAF50;
            color: white;
            border-radius: 3px;
            padding: 3px;
            font-size: 12px;
        }
        QPushButton:hover {
            background-color: #45a049;
        }
    """)
    # 绑定点击事件(传递当前人脸ID)
    view_btn.clicked.connect(lambda checked: self.on_view_click(img_bytes))
    # 添加按钮到容器
    btn_layout1.addWidget(view_btn)
    # 将容器添加到表格单元格(第3列)
    self.ui.faces_table.setCellWidget(row_idx, 3, btn_container1)

    #删除按钮
    # 创建按钮容器(避免按钮在单元格中拉伸变形)
    btn_container2 = QWidget()
    btn_layout2 = QVBoxLayout(btn_container2)
    btn_layout2.setContentsMargins(2, 2, 2, 2)  # 减小边距
    btn_layout2.setSpacing(3)  # 按钮间距

    delete_btn = QPushButton("删除")
    delete_btn.setStyleSheet("""
        QPushButton {
            background-color: #f44336;
            color: white;
            border-radius: 3px;
            padding: 3px;
            font-size: 12px;
        }
        QPushButton:hover {
            background-color: #d32f2f;
        }
    """)
    # 绑定点击事件
    delete_btn.clicked.connect(lambda checked: self.on_delete_click(face_id))

    # 添加按钮到容器
    btn_layout2.addWidget(delete_btn)
    # 将容器添加到表格单元格(第4列)
    self.ui.faces_table.setCellWidget(row_idx, 4, btn_container2)

# 显示所有数据
def show_all(self):
    # 当前的数据
    self.datas = np.array(self.db.select_all())
    # 设置行
    self.ui.faces_table.setRowCount(len(self.datas))
    # self.add_action_buttons(3, row)
    for row, data in enumerate(self.datas):
        self.ui.faces_table.setItem(row, 0, QTableWidgetItem(str(data[0])))
        self.ui.faces_table.setItem(row, 1, QTableWidgetItem(str(data[1])))
        self.ui.faces_table.setItem(row, 2, QTableWidgetItem(str(data[2])))
        self.add_action_buttons(row, data[3], data[0])
        # self.ui.faces_table.setItem(row, 3, QTableWidgetItem(self.add_action_buttons(3, row)))

# 查询
def search(self):
    name = self.ui.search_input.text()
    if name is None or len(name) == 0:
        self.show_all()
        return
    # print(name)
    # 当前的数据
    self.datas = np.array(self.db.select_one(name))
    # 设置行
    self.ui.faces_table.setRowCount(len(self.datas))
    # self.add_action_buttons(3, row)
    for row, data in enumerate(self.datas):
        self.ui.faces_table.setItem(row, 0, QTableWidgetItem(str(data[0])))
        self.ui.faces_table.setItem(row, 1, QTableWidgetItem(str(data[1])))
        self.ui.faces_table.setItem(row, 2, QTableWidgetItem(str(data[2])))
        self.add_action_buttons(row, data[3], data[0])

连接信号

# 连接信号
def connect_signals(self):
    self.ui.btn_select_image.clicked.connect(self.select_image)
    self.ui.btn_open_camera.clicked.connect(self.open_camera)
    self.ui.btn_save_face.clicked.connect(self.save_face)
    self.ui.btn_register.clicked.connect(lambda: self.change_interface(0))
    self.ui.btn_recognize.clicked.connect(lambda: self.change_interface(1))
    self.ui.btn_view.clicked.connect(lambda: self.change_interface(2))
    self.ui.btn_select_image_rec.clicked.connect(self.select_image)
    self.ui.btn_open_camera_rec.clicked.connect(self.open_camera)
    self.ui.btn_recognize_face.clicked.connect(self.recognize_face)
    self.ui.btn_show_all.clicked.connect(self.show_all)
    self.ui.btn_search.clicked.connect(self.search)

程序退出,关闭数据库

def closeEvent(self, event):
    """重写窗口关闭事件"""
    self.db.close()
    event.accept()  # 接受关闭事件

完整代码(后端)

import os

import cv2
import numpy as np
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QPixmap, QImage
from PyQt5.QtWidgets import QMainWindow, QFileDialog, QMessageBox, QTableWidgetItem, QPushButton, QWidget, QVBoxLayout, \
    QDialog, QLabel

from FaceNet.DB import DB
from FaceNet.FaceRecognition import FaceRecognition
from FaceNet.FaceSave import FaceSave
from PyQt.index import Ui_MainWindow


class Controller(QMainWindow):
    def __init__(self):
        super(Controller, self).__init__()
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)

        # 摄像头
        self.camera = None
        # 定时器
        self.timer = QTimer(self)

        # 摄像头暂存的最后一个画面
        self.last_frame = None
        # 打开的图片的文件地址
        self.last_path = None

        # 连接数据库
        self.db = DB()
        self.save = FaceSave()
        self.recognition = FaceRecognition()
        # 数据库中数据
        self.datas = np.array(self.db.select_all())

        # 当前显示的界面的图像显示区域
        self.interface = self.ui.register_image_label

        # 识别模式
        self.pattern = False

        # 显示所有数据
        self.show_all()

        # 连接信号
        self.connect_signals()

    # 选择图片
    def select_image(self):
        if self.camera is None:
            # 打开文件对话框
            file_path =QFileDialog.getOpenFileName(self, "选择图片文件", "", "图片文件 (*.png *.jpg *.jpeg *.bmp *.gif);;所有文件 (*)")
            if len(file_path[0]) == 0:
                return
            # 如果用户选择了文件
            if file_path:
                # 获取文件路径
                self.last_path = file_path[0]
                img = cv2.imread(self.last_path)
                self.show_frame(img)
            else:
                self.interface.setText("无法加载图片")
        elif self.camera is not None and self.timer.isActive():
            # 停止定时器
            self.timer.stop()
            if self.interface == self.ui.register_image_label:
                self.ui.btn_select_image.setText("重新选择")
            else:
                self.ui.btn_select_image_rec.setText("重新选择")
        else:
            if self.interface == self.ui.register_image_label:
                self.ui.btn_select_image.setText("选择图像")
            else:
                self.ui.btn_select_image_rec.setText("选择图像")
            self.timer.start(30)

    # 打开摄像头
    def open_camera(self):
        if self.camera is None:
            self.camera = cv2.VideoCapture(0)
            if not self.camera.isOpened():
                self.interface.setText("无法打开摄像头,请检查设备是否正常")
                return
            if self.interface == self.ui.register_image_label:
                self.ui.btn_open_camera.setText("关闭摄像头")
            else:
                self.ui.btn_open_camera_rec.setText("关闭摄像头")
            # 启用定时器,每隔30毫秒刷新一次画面
            self.timer.start(30)
        else:
            # 停止定时器
            self.timer.stop()
            # 释放摄像头资源
            self.camera.release()
            self.camera = None
            if self.interface == self.ui.register_image_label:
                self.ui.btn_open_camera.setText("打开摄像头")
                self.ui.btn_select_image.setText("选择图像")
            else:
                self.ui.btn_open_camera_rec.setText("打开摄像头")
                self.ui.btn_select_image_rec.setText("选择图像")

            self.interface.clear()
            self.interface.setText("图像预览区域")
        self.timer.timeout.connect(lambda: self.update_frame())  # 定时刷新画面
        # self.update_frame(interface)


    # 更新摄像头画面
    def update_frame(self):
        # 读取摄像头帧
        ret, frame = self.camera.read()

        if not ret:
            self.interface.setText("无法获取画面")
            return
        # 识别模式
        if self.pattern:
            # 识别
            face_num, frame = self.recognition.detect_and_recognize(frame)
            self.ui.detail_text.setText(f'识别了{face_num}张人脸')

        # 翻转
        # frame = cv2.flip(frame, 1)
        self.last_frame = frame
        self.show_frame(frame)

    # 显示图像
    def show_frame(self, frame):
        # 转换图像格式(OpenCV默认是BGR格式,需要转换为RGB)
        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        # 调整图像大小以适应显示区域
        height, width, channel = frame.shape
        bytes_per_line = channel * width

        # 创建QImage对象并转换为QPixmap
        q_image = QImage(frame.data, width, height, bytes_per_line, QImage.Format_RGB888)
        pixmap = QPixmap.fromImage(q_image)
        # 按比例缩放图片以适应标签
        scaled_pixmap = pixmap.scaled(
            self.interface.width(),
            self.interface.height(),
            Qt.KeepAspectRatio,
            Qt.SmoothTransformation
        )
        self.interface.setPixmap(scaled_pixmap)
        self.interface.setText("")  # 清空标签文本

    # 录入人脸
    def save_face(self):
        if self.interface.text() == "图像预览区域" or self.timer.isActive():
            QMessageBox.warning(
                None,
                "提示",
                "请先选择图像",
                QMessageBox.Ok
            )
            return

        name = self.ui.name_input.text()
        if name is None or name == '':
            QMessageBox.warning(
                None,
                "提示",
                "姓名不能为空",
                QMessageBox.Ok
            )
            return
        if self.camera is not None:
            # 保存人脸
            res = QMessageBox.warning(
                None,
                "提示",
                "确认保存",
                QMessageBox.Yes | QMessageBox.No
            )
            if res == QMessageBox.No:
                return
            else:
                self.save.detect_and_features(name, self.last_frame, '.jpg')
                QMessageBox.warning(
                    None,
                    "提示",
                    "保存成功!",
                    QMessageBox.Ok
                )
        else:
            img = cv2.imread(self.last_path)
            _, ext = os.path.splitext(self.last_path)
            # 保存人脸
            res = QMessageBox.warning(
                None,
                "提示",
                "确认保存",
                QMessageBox.Yes | QMessageBox.No
            )
            if res == QMessageBox.No:
                return
            else:
                self.save.detect_and_features(name, img, ext)
                QMessageBox.warning(
                    None,
                    "提示",
                    "保存成功!",
                    QMessageBox.Ok
                )
        self.show_all()

    # 识别人脸
    def recognize_face(self):
        if self.interface.text() == "图像预览区域":
            QMessageBox.warning(
                None,
                "提示",
                "无图像可识别",
                QMessageBox.Ok
            )
            return

        if not self.pattern:
            self.pattern = True
            self.ui.btn_recognize_face.setText("取消识别")
            self.ui.recognize_result.setText("识别中")
            # self.ui.detail_text.setText()
        else:
            self.pattern = False
            self.ui.btn_recognize_face.setText("识别人脸")
            self.ui.recognize_result.setText("等待识别...")
            self.ui.detail_text.setText("")
        # 视频
        if self.camera is not None:
            self.update_frame()
        # 图片
        else:
            img = cv2.imread(self.last_path)
            if self.pattern:
                face_num, img_recognition = self.recognition.detect_and_recognize(img)
                self.ui.detail_text.setText(f'识别了{face_num}张人脸')
                # 显示识别图片
                self.show_frame(img_recognition)
            else:
                self.show_frame(img)

    # 显示图像
    def on_view_click(self, img_bytes):
        # 将字节数据转换为 NumPy 数组
        img_array = np.frombuffer(img_bytes, dtype=np.uint8)
        # 使用 OpenCV 解码字节数组为图像
        img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
        # cv2.imshow('view', img)
        # cv2.waitKey(0)
        # 创建弹窗显示详情
        dialog = QDialog(self)
        dialog.resize(600, 600)
        layout = QVBoxLayout(dialog)
        info_label = QLabel()
        info_label.resize(600, 600)
        info_label.setText("dsdad")
        layout.addWidget(info_label)
        self.interface = info_label
        self.show_frame(img)
        dialog.exec_()

    # 删除数据
    def on_delete_click(self, face_id):
        # 删除
        self.db.delete(face_id)
        # 刷新
        self.show_all()

    # 为指定行添加操作按钮(查看 / 删除)
    def add_action_buttons(self, row_idx, img_bytes, face_id):

        # 创建按钮容器(避免按钮在单元格中拉伸变形)
        btn_container1 = QWidget()
        btn_layout1 = QVBoxLayout(btn_container1)
        btn_layout1.setContentsMargins(2, 2, 2, 2)  # 减小边距
        btn_layout1.setSpacing(3)  # 按钮间距

        # 查看按钮
        view_btn = QPushButton("查看")
        view_btn.setStyleSheet("""
            QPushButton {
                background-color: #4CAF50;
                color: white;
                border-radius: 3px;
                padding: 3px;
                font-size: 12px;
            }
            QPushButton:hover {
                background-color: #45a049;
            }
        """)
        # 绑定点击事件(传递当前人脸ID)
        view_btn.clicked.connect(lambda checked: self.on_view_click(img_bytes))
        # 添加按钮到容器
        btn_layout1.addWidget(view_btn)
        # 将容器添加到表格单元格(第3列)
        self.ui.faces_table.setCellWidget(row_idx, 3, btn_container1)

        #删除按钮
        # 创建按钮容器(避免按钮在单元格中拉伸变形)
        btn_container2 = QWidget()
        btn_layout2 = QVBoxLayout(btn_container2)
        btn_layout2.setContentsMargins(2, 2, 2, 2)  # 减小边距
        btn_layout2.setSpacing(3)  # 按钮间距

        delete_btn = QPushButton("删除")
        delete_btn.setStyleSheet("""
            QPushButton {
                background-color: #f44336;
                color: white;
                border-radius: 3px;
                padding: 3px;
                font-size: 12px;
            }
            QPushButton:hover {
                background-color: #d32f2f;
            }
        """)
        # 绑定点击事件
        delete_btn.clicked.connect(lambda checked: self.on_delete_click(face_id))

        # 添加按钮到容器
        btn_layout2.addWidget(delete_btn)
        # 将容器添加到表格单元格(第4列)
        self.ui.faces_table.setCellWidget(row_idx, 4, btn_container2)

    # 显示所有数据
    def show_all(self):
        # 当前的数据
        self.datas = np.array(self.db.select_all())
        # 设置行
        self.ui.faces_table.setRowCount(len(self.datas))
        # self.add_action_buttons(3, row)
        for row, data in enumerate(self.datas):
            self.ui.faces_table.setItem(row, 0, QTableWidgetItem(str(data[0])))
            self.ui.faces_table.setItem(row, 1, QTableWidgetItem(str(data[1])))
            self.ui.faces_table.setItem(row, 2, QTableWidgetItem(str(data[2])))
            self.add_action_buttons(row, data[3], data[0])
            # self.ui.faces_table.setItem(row, 3, QTableWidgetItem(self.add_action_buttons(3, row)))

    # 查询
    def search(self):
        name = self.ui.search_input.text()
        if name is None or len(name) == 0:
            self.show_all()
            return
        # print(name)
        # 当前的数据
        self.datas = np.array(self.db.select_one(name))
        # 设置行
        self.ui.faces_table.setRowCount(len(self.datas))
        # self.add_action_buttons(3, row)
        for row, data in enumerate(self.datas):
            self.ui.faces_table.setItem(row, 0, QTableWidgetItem(str(data[0])))
            self.ui.faces_table.setItem(row, 1, QTableWidgetItem(str(data[1])))
            self.ui.faces_table.setItem(row, 2, QTableWidgetItem(str(data[2])))
            self.add_action_buttons(row, data[3], data[0])

    # 切换界面
    def change_interface(self, index):
        # 切换界面自动关闭摄像头
        if self.camera is not None:
            self.timer.stop()
            self.camera.release()
            self.camera = None
            self.interface.clear()
        self.ui.stacked_widget.setCurrentIndex(index)
        if index == 0:
            self.interface = self.ui.register_image_label
        elif index == 1:
            self.interface = self.ui.recognize_image_label
        else:
            self.interface = None

    # 连接信号
    def connect_signals(self):
        self.ui.btn_select_image.clicked.connect(self.select_image)
        self.ui.btn_open_camera.clicked.connect(self.open_camera)
        self.ui.btn_save_face.clicked.connect(self.save_face)
        self.ui.btn_register.clicked.connect(lambda: self.change_interface(0))
        self.ui.btn_recognize.clicked.connect(lambda: self.change_interface(1))
        self.ui.btn_view.clicked.connect(lambda: self.change_interface(2))
        self.ui.btn_select_image_rec.clicked.connect(self.select_image)
        self.ui.btn_open_camera_rec.clicked.connect(self.open_camera)
        self.ui.btn_recognize_face.clicked.connect(self.recognize_face)
        self.ui.btn_show_all.clicked.connect(self.show_all)
        self.ui.btn_search.clicked.connect(self.search)
       # 程序退出,关闭数据库
    def closeEvent(self, event):
        """重写窗口关闭事件"""
        self.db.close()
        event.accept()  # 接受关闭事件

五、演示

在这里插入图片描述

本项目及相关代码仅用于学习和研究目的,不保证其在任何场景下的准确性、完整性和可靠性。

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐