C++ Qt + OpenCV 实现本地人脸识别系统:摄像头采集、ONNX模型加载、人脸库比对完整流程

一、项目效果展示

本项目基于 C++、Qt Widgets、Qt Multimedia、OpenCV DNN 实现了一个本地人脸识别小系统。系统可以在 Qt 界面中实时显示摄像头画面,点击"开始人脸识别"按钮后,程序会在 5 秒内对摄像头画面中的人脸进行检测和识别。如果当前人脸与本地人脸库中的某个样本相似度超过设定阈值,则显示对应的人名并提示"识别成功";如果 5 秒内没有识别成功,则显示"未识别"和"识别失败"。

本项目的核心功能包括:

  1. Qt 界面显示摄像头实时画面

  2. 使用 OpenCV 加载 YuNet 人脸检测模型

  3. 使用 OpenCV 加载 SFace 人脸识别模型

  4. 扫描本地 faces 文件夹,构建本地人脸库

  5. 点击按钮后进行一次 5 秒识别

  6. 识别成功后自动停止识别

  7. 识别失败超时后自动停止识别

  8. 关闭窗口时安全释放摄像头资源

二、开发环境

操作系统:Windows 10 / Windows 11

开发工具:Qt Creator

Qt版本:Qt 6.x

编译器:MSVC 2022 64-bit

OpenCV版本:OpenCV 4.12.0

构建工具:CMake

编程语言:C++

使用模型:

  1. face_detection_yunet_2023mar.onnx

  2. face_recognition_sface_2021dec.onnxV

本项目使用 Qt 负责界面和摄像头采集,OpenCV 负责人脸检测、特征提取和相似度匹配。

opencv官网

Releases - OpenCV

下载windows版的,下载下来是个.exe文件,运行后得到下面的文件夹(相当于解压),让后放到FaceRecognition目录下,方便后期调用

三、工程目录结构

D:\FaceRecognition

├─ opencv

│ ├─ build

│ └─ sources

└─ FaceRecognition

├─ CMakeLists.txt

├─ main.cpp

├─ widget.h

├─ widget.cpp

├─ widget.ui

├─ faces

│ ├─ LiuYiFei

│ │ ├─ 1.jpg

│ │ └─ 2.jpg

│ └─ YangMi

│ ├─ 1.jpg

│ └─ 2.jpg

└─ models

├─ face_detection_yunet_2023mar.onnx

└─ face_recognition_sface_2021dec.onnx

四、界面设计

控件类型 对象名 作用
QLabel labelCamera 显示摄像头画面
QLabel labelName 显示识别到的人名
QLabel labelResult 显示识别结果
QPushButton btnStartRecognition 开始人脸识别
QPushButton btnStopRecognition 停止人脸识别

在 Qt Designer 中拖入一个较大的 QLabel 用来显示摄像头画面,将其对象名设置为 labelCamera。另外再添加两个 QLabel,分别用于显示姓名和识别状态,对象名分别为 labelNamelabelResult。最后添加两个按钮,分别用于开始识别和停止识别,对象名设置为 btnStartRecognitionbtnStopRecognition

五、CMake配置OpenCV和Qt模块

1. 添加 Qt Widgets 和 Multimedia 模块

因为项目需要界面和摄像头,所以必须添加:

find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Multimedia)

find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Multimedia)

其中:

  • Widgets 用于 Qt 界面
  • Multimedia 用于摄像头采集

2. 配置 OpenCV 路径

set(OpenCV_DIR "D:/FaceRecognition/opencv/build")

find_package(OpenCV REQUIRED COMPONENTS core imgproc imgcodecs objdetect dnn)

这里 OpenCV_DIR 必须指向包含 OpenCVConfig.cmake 的目录。如果 CMake 报错,就在 OpenCV 文件夹中搜索:

OpenCVConfig.cmake

然后把 OpenCV_DIR 改成它所在的目录。

3. 自动复制 faces 和 models 到 exe 目录

程序运行时用的是:

QCoreApplication::applicationDirPath()

也就是 exe 所在目录。所以需要把 facesmodels 自动复制到 exe 同级目录:

add_custom_command(TARGET FaceRecognition POST_BUILD

COMMAND ${CMAKE_COMMAND} -E copy_directory

"${CMAKE_CURRENT_SOURCE_DIR}/faces"

"$<TARGET_FILE_DIR:FaceRecognition>/faces"

)

add_custom_command(TARGET FaceRecognition POST_BUILD

COMMAND ${CMAKE_COMMAND} -E copy_directory

"${CMAKE_CURRENT_SOURCE_DIR}/models"

"$<TARGET_FILE_DIR:FaceRecognition>/models"

)

CMakeLists.txt

复制代码
# 指定 CMake 的最低版本要求
cmake_minimum_required(VERSION 3.16)

# 定义工程名称、版本号、语言
project(FaceRecognition VERSION 0.1 LANGUAGES CXX)

# 开启 Qt 的自动处理功能:
# AUTOUIC  自动处理 .ui 文件
# AUTOMOC  自动处理带 Q_OBJECT 的类
# AUTORCC  自动处理 Qt 资源文件(本工程暂时没用 qrc,但开着也没问题)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

# 指定使用 C++17 标准
set(CMAKE_CXX_STANDARD 17)

# 强制要求编译器必须支持 C++17
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 查找 Qt,兼容 Qt6 和 Qt5
# 本工程需要 Widgets(界面)和 Multimedia(摄像头)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Multimedia)

# 根据实际 Qt 主版本继续查找模块
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Multimedia)

# 手动指定 OpenCVConfig.cmake 所在目录
# 如果这里报错,就改成你电脑里 OpenCVConfig.cmake 的实际目录
set(OpenCV_DIR "D:/FaceRecognition/opencv/build")

# 查找 OpenCV,并要求这几个模块:
# core       核心模块
# imgproc    图像处理
# imgcodecs  图像读写
# objdetect  目标检测 / FaceDetectorYN / FaceRecognizerSF
# dnn        深度学习网络推理
find_package(OpenCV REQUIRED COMPONENTS core imgproc imgcodecs objdetect dnn)

# 定义本工程的源文件列表
set(PROJECT_SOURCES
    main.cpp       # 程序入口
    widget.cpp     # 主窗口实现文件
    widget.h       # 主窗口头文件
    widget.ui      # 界面设计文件
)

# 如果 Qt 主版本 >= 6
if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)

    # 用 qt_add_executable 创建可执行程序
    qt_add_executable(FaceRecognition
        MANUAL_FINALIZATION   # 手动 finalize
        ${PROJECT_SOURCES}    # 加入前面定义的所有源文件
    )

else()

    # 如果是 Android 平台
    if(ANDROID)

        # Android 下一般构建共享库
        add_library(FaceRecognition SHARED
            ${PROJECT_SOURCES}
        )

    else()

        # 普通桌面平台构建可执行程序
        add_executable(FaceRecognition
            ${PROJECT_SOURCES}
        )

    endif()
endif()

# 把 OpenCV 的头文件目录加入编译器搜索路径
target_include_directories(FaceRecognition PRIVATE ${OpenCV_INCLUDE_DIRS})

# 链接 Qt 和 OpenCV 库
target_link_libraries(FaceRecognition PRIVATE
    Qt${QT_VERSION_MAJOR}::Widgets      # Qt Widgets 模块
    Qt${QT_VERSION_MAJOR}::Multimedia   # Qt Multimedia 模块
    ${OpenCV_LIBS}                      # OpenCV 相关库
)

# 编译完成后,把源工程目录下的 faces 文件夹复制到 exe 所在目录
add_custom_command(TARGET FaceRecognition POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_directory
            "${CMAKE_CURRENT_SOURCE_DIR}/faces"
            "$<TARGET_FILE_DIR:FaceRecognition>/faces"
)

# 编译完成后,把源工程目录下的 models 文件夹复制到 exe 所在目录
add_custom_command(TARGET FaceRecognition POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_directory
            "${CMAKE_CURRENT_SOURCE_DIR}/models"
            "$<TARGET_FILE_DIR:FaceRecognition>/models"
)

# 如果 Qt 版本低于 6.1.0,则给 macOS/iOS 设一个 bundle id
if(${QT_VERSION} VERSION_LESS 6.1.0)
    set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.FaceRecognition)
endif()

# 设置目标程序的一些属性
set_target_properties(FaceRecognition PROPERTIES
    ${BUNDLE_ID_OPTION}                                      # bundle id(如果有)
    MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}          # macOS bundle 版本
    MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}  # 短版本号
    MACOSX_BUNDLE TRUE                                       # 允许作为 macOS bundle
    WIN32_EXECUTABLE TRUE                                    # Windows 下构建 GUI 程序,不弹控制台
)

# Qt6 工程结束时调用 finalize
if(QT_VERSION_MAJOR EQUAL 6)
    qt_finalize_executable(FaceRecognition)
endif()

六、摄像头画面显示流程

这一部分讲 Qt 摄像头画面是怎么显示到 QLabel 上的。

整体流程如下:

QCamera

QMediaCaptureSession

QVideoSink

QVideoFrame

QImage

QPixmap

QLabel显示

程序初始化时会先获取系统摄像头列表:

const QList<QCameraDevice> cameras = QMediaDevices::videoInputs();

如果检测到摄像头,就创建 QCamera 对象:

camera = new QCamera(cameras.first(), this);

然后建立采集链路:

captureSession.setCamera(camera);

captureSession.setVideoOutput(videoSink);

最后连接视频帧信号:

connect(videoSink, &QVideoSink::videoFrameChanged,

this, &Widget::onVideoFrameChanged);

摄像头启动后:

camera->start();

每当摄像头产生一帧画面,都会进入 onVideoFrameChanged() 函数。在这个函数中,程序先将 QVideoFrame 转成 QImage,再通过 QPixmap 显示到 labelCamera 上。你的当前实现中,构造函数里完成了摄像头枚举、采集链路建立、视频帧回调连接和摄像头启动等步骤。

七、人脸识别整体流程

这一部分是博客重点。

整体流程如下:

程序启动

加载 YuNet 人脸检测模型

加载 SFace 人脸识别模型

扫描 faces 文件夹

读取每个人的人脸图片

检测人脸并提取特征

将特征保存到 faceDatabase

点击开始识别

对当前摄像头帧提取人脸特征

与 faceDatabase 中的特征逐个比对

找到最高相似度

超过阈值则识别成功

显示姓名并停止本轮识别

1. 初始化模型

项目使用两个模型:

face_detection_yunet_2023mar.onnx

face_recognition_sface_2021dec.onnx

其中:

  • YuNet 用于人脸检测
  • SFace 用于人脸特征提取与匹配

代码中通过:

cv::FaceDetectorYN::create(...)

cv::FaceRecognizerSF::create(...)

来创建检测器和识别器。

2. 加载本地人脸库

本地人脸库采用文件夹结构:

faces

├─ LiuYiFei

│ ├─ 1.jpg

│ └─ 2.jpg

└─ YangMi

├─ 1.jpg

└─ 2.jpg

程序遍历 faces 下的每个人名文件夹,读取图片,检测人脸并提取特征,然后保存到 faceDatabase 中。你的实现中,loadFaceDatabase() 会遍历 faces 下的每个人名文件夹,读取 jpg/jpeg/png/bmp 图片,并为每张有效图片提取特征后保存到本地人脸库。

3. 当前帧识别

点击"开始人脸识别"后,程序会对摄像头帧进行识别:

当前帧

检测人脸

选择最大人脸

对齐裁剪

提取特征

与本地人脸库逐个比对

取最高相似度

程序采用余弦相似度进行匹配:

cv::FaceRecognizerSF::FR_COSINE

如果最高相似度超过设定阈值,比如:

double recognitionThreshold = 0.40;

则认为识别成功。

八、按钮逻辑设计

点击开始识别

开始计时

5秒内持续尝试识别

如果成功:

显示姓名

显示识别成功

停止识别

如果5秒内失败:

显示未识别

显示识别失败

停止识别

widget.h

cpp 复制代码
#ifndef WIDGET_H
#define WIDGET_H

// QWidget:所有窗口控件的基类
#include <QWidget>

// QCamera:Qt 摄像头对象,用于打开和控制摄像头
#include <QCamera>

// QMediaCaptureSession:媒体采集会话,用来把摄像头和视频输出连接起来
#include <QMediaCaptureSession>

// QVideoSink:视频帧接收器,可以逐帧拿到摄像头画面
#include <QVideoSink>

// QVideoFrame:视频帧对象,每来一帧就会以这个类型传进来
#include <QVideoFrame>

// QImage:Qt 图像类,适合图像处理和格式转换
#include <QImage>

// QPixmap:Qt 用于界面显示图片的类,适合 QLabel 显示
#include <QPixmap>

// QMediaDevices:用于查询系统中的多媒体设备,比如摄像头
#include <QMediaDevices>

// QCameraDevice:表示一个摄像头设备
#include <QCameraDevice>

// QCoreApplication:可以获取应用程序运行目录
#include <QCoreApplication>

// QDir:目录操作类,用来遍历 faces 文件夹
#include <QDir>

// QFileInfo:文件信息类,用来判断模型文件、图片文件是否存在
#include <QFileInfo>

// QElapsedTimer:高精度计时器,用来统计单轮识别是否超时 5 秒
#include <QElapsedTimer>

// QCloseEvent:窗口关闭事件类,用于重写 closeEvent
#include <QCloseEvent>

// vector:C++ STL 容器,用来保存本地人脸库
#include <vector>

// OpenCV 核心模块
#include <opencv2/core.hpp>

// OpenCV 图像处理模块
#include <opencv2/imgproc.hpp>

// OpenCV 图像编解码模块,用于 imread
#include <opencv2/imgcodecs.hpp>

// OpenCV 人脸检测/识别相关接口
#include <opencv2/objdetect/face.hpp>

// Qt 的命名空间宏开始
QT_BEGIN_NAMESPACE

// 前向声明 Ui 命名空间中的 Widget 类
namespace Ui {
class Widget;
}

// Qt 的命名空间宏结束
QT_END_NAMESPACE

// 定义一个结构体,表示本地人脸库中的一条记录
struct FaceEntry
{
    QString name;      // 人名,对应 faces 下的文件夹名
    QString imagePath; // 该样本图片的完整路径
    cv::Mat feature;   // 这张图片提取出来的人脸特征向量
};

// 主窗口类,继承 QWidget
class Widget : public QWidget
{
    Q_OBJECT   // 启用 Qt 的元对象系统,支持信号槽

public:
    Widget(QWidget *parent = nullptr); // 构造函数
    ~Widget();                         // 析构函数

protected:
    void closeEvent(QCloseEvent *event) override; // 重写关闭事件,用于安全停止摄像头

private slots:
    void onVideoFrameChanged(const QVideoFrame &frame); // 摄像头每来一帧时触发
    void on_btnStartRecognition_clicked();             // 点击"开始人脸识别"按钮
    void on_btnStopRecognition_clicked();              // 点击"停止人脸识别"按钮

private:
    bool initFaceEngine();                               // 初始化人脸检测模型和识别模型
    bool loadFaceDatabase(const QString &facesRootDir);  // 加载本地人脸库
    bool extractFeature(const cv::Mat &image, cv::Mat &feature); // 从一张图中提取人脸特征

    // 对当前一帧做人脸识别
    // 成功返回 true,并通过引用参数输出识别到的姓名和得分
    bool recognizeFrame(const cv::Mat &frame, QString &bestName, double &bestScore);

    cv::Mat qImageToCvMat(const QImage &image) const;   // 把 Qt 的 QImage 转成 OpenCV 的 Mat
    int selectLargestFace(const cv::Mat &faces) const;  // 多张脸时选最大的一张
    void updatePreview(const QImage &image);            // 刷新界面上的摄像头画面

    void resetResult(const QString &nameText, const QString &resultText); // 更新结果文字
    void setRecognitionRunning(bool running);           // 设置当前是否正在识别
    void finishRecognitionSuccess(const QString &name, double score); // 识别成功后结束流程
    void finishRecognitionFail();                       // 识别失败后结束流程
    void stopCameraSafely();                            // 安全停止摄像头和回调

private:
    Ui::Widget *ui = nullptr;  // 指向 ui 界面对象

    // =========================
    // Qt 摄像头相关成员
    // =========================
    QCamera *camera = nullptr;            // 摄像头对象
    QMediaCaptureSession captureSession;  // 采集会话对象
    QVideoSink *videoSink = nullptr;      // 视频帧接收器

    // =========================
    // OpenCV 人脸识别相关成员
    // =========================
    cv::Ptr<cv::FaceDetectorYN> faceDetector;     // YuNet 人脸检测器
    cv::Ptr<cv::FaceRecognizerSF> faceRecognizer; // SFace 人脸识别器

    // =========================
    // 本地人脸库
    // =========================
    std::vector<FaceEntry> faceDatabase; // 保存所有本地人脸特征

    // =========================
    // 状态变量
    // =========================
    bool faceEngineReady = false;     // 人脸识别模型是否初始化成功
    bool recognitionEnabled = false;  // 当前是否允许识别
    bool isClosing = false;           // 当前窗口是否正在关闭

    // 节流控制:不是每一帧都识别,避免程序太卡
    int frameCounter = 0;     // 帧计数器
    int frameInterval = 5;    // 每隔 5 帧识别一次

    // 识别阈值:得分大于等于这个值就认为识别成功
    double recognitionThreshold = 0.40;

    // 单轮识别最大持续时间,单位毫秒,5000ms = 5秒
    int recognitionTimeoutMs = 5000;

    // 单轮识别计时器
    QElapsedTimer recognitionTimer;
};

#endif // WIDGET_H

widget.cpp

cpp 复制代码
// 引入当前类的头文件
#include "widget.h"

// 引入 ui 文件生成的头文件
#include "ui_widget.h"

// QMessageBox:消息框,用来弹警告和错误
#include <QMessageBox>

// QDebug:调试输出,用来打印得分和调试信息
#include <QDebug>

// QFileInfoList:文件信息列表,用于遍历目录中的文件
#include <QFileInfoList>

// limits:用于获取 double 的最小值
#include <limits>

// =========================
// 构造函数
// =========================
Widget::Widget(QWidget *parent)
    : QWidget(parent)                  // 调用父类 QWidget 构造函数
    , ui(new Ui::Widget)               // 创建 ui 对象
    , videoSink(new QVideoSink(this))  // 创建视频帧接收器,父对象设为当前窗口
{
    ui->setupUi(this); // 加载并初始化界面

    // 设置摄像头画面显示区域居中
    ui->labelCamera->setAlignment(Qt::AlignCenter);

    // 初始化姓名显示标签
    ui->labelName->setText("未识别");

    // 初始化结果显示标签
    ui->labelResult->setText("已停止");

    // 初始状态下,允许点击开始识别
    ui->btnStartRecognition->setEnabled(true);

    // 初始状态下,不允许点击停止识别
    ui->btnStopRecognition->setEnabled(false);

    // =========================
    // 1. 初始化摄像头显示
    // =========================

    // 获取系统中可用摄像头设备列表
    const QList<QCameraDevice> cameras = QMediaDevices::videoInputs();

    // 如果没有检测到摄像头
    if (cameras.isEmpty()) {
        // 弹出提示框
        QMessageBox::warning(this, "提示", "没有检测到摄像头!");
    } else {
        // 默认打开第一个摄像头
        camera = new QCamera(cameras.first(), this);

        // 把摄像头绑定到采集会话
        captureSession.setCamera(camera);

        // 把视频输出绑定到 videoSink
        captureSession.setVideoOutput(videoSink);

        // 每当 videoSink 收到一帧图像,就调用 onVideoFrameChanged
        connect(videoSink, &QVideoSink::videoFrameChanged,
                this, &Widget::onVideoFrameChanged);

        // 启动摄像头
        camera->start();
    }

    // =========================
    // 2. 初始化人脸识别模型
    // =========================

    // 初始化模型,返回值表示是否成功
    faceEngineReady = initFaceEngine();

    // 如果模型初始化失败
    if (!faceEngineReady) {
        // 界面提示模型加载失败
        ui->labelResult->setText("模型加载失败");

        // 禁止开始识别按钮
        ui->btnStartRecognition->setEnabled(false);

        // 禁止停止识别按钮
        ui->btnStopRecognition->setEnabled(false);

        // 直接返回,后面不再加载人脸库
        return;
    }

    // =========================
    // 3. 加载本地人脸库
    // =========================

    // 获取程序运行目录下的 faces 文件夹路径
    const QString facesDir = QCoreApplication::applicationDirPath() + "/faces";

    // 加载本地人脸库
    if (!loadFaceDatabase(facesDir)) {
        // 如果加载失败,说明人脸库为空或无效
        ui->labelResult->setText("人脸库为空");

        // 禁用开始识别
        ui->btnStartRecognition->setEnabled(false);

        // 禁用停止识别
        ui->btnStopRecognition->setEnabled(false);
    } else {
        // 如果加载成功,界面显示已停止,等待用户点击开始识别
        ui->labelResult->setText("已停止");
    }
}

// =========================
// 析构函数
// =========================
Widget::~Widget()
{
    // 标记窗口正在关闭
    isClosing = true;

    // 安全停止摄像头和回调
    stopCameraSafely();

    // 释放 ui 对象
    delete ui;

    // 置空指针,防止误用
    ui = nullptr;
}

// =========================
// 重写关闭事件
// =========================
void Widget::closeEvent(QCloseEvent *event)
{
    // 标记正在关闭
    isClosing = true;

    // 安全停止摄像头
    stopCameraSafely();

    // 调用父类关闭事件处理
    QWidget::closeEvent(event);
}

// =========================
// 安全停止摄像头和回调
// =========================
void Widget::stopCameraSafely()
{
    // 关闭识别功能
    recognitionEnabled = false;

    // 让识别计时器失效
    recognitionTimer.invalidate();

    // 如果 videoSink 存在
    if (videoSink) {
        // 断开 videoSink 到当前对象的所有信号连接
        disconnect(videoSink, nullptr, this, nullptr);
    }

    // 解除视频输出绑定
    captureSession.setVideoOutput(nullptr);

    // 解除摄像头绑定
    captureSession.setCamera(nullptr);

    // 如果摄像头对象存在
    if (camera) {
        // 停止摄像头
        camera->stop();
    }
}

// =========================
// 更新界面上的姓名和结果文字
// =========================
void Widget::resetResult(const QString &nameText, const QString &resultText)
{
    // 如果 ui 无效,直接返回
    if (!ui) return;

    // 更新姓名标签
    ui->labelName->setText(nameText);

    // 更新结果标签
    ui->labelResult->setText(resultText);
}

// =========================
// 设置当前是否正在识别
// =========================
void Widget::setRecognitionRunning(bool running)
{
    // 更新内部识别状态
    recognitionEnabled = running;

    // 如果 ui 无效,直接返回
    if (!ui) return;

    // 如果正在识别,则开始按钮禁用;否则启用
    ui->btnStartRecognition->setEnabled(!running);

    // 如果正在识别,则停止按钮启用;否则禁用
    ui->btnStopRecognition->setEnabled(running);
}

// =========================
// 初始化人脸检测和识别模型
// =========================
bool Widget::initFaceEngine()
{
    // 获取程序运行目录
    const QString appDir = QCoreApplication::applicationDirPath();

    // 拼接检测模型路径
    const QString detectorModel = appDir + "/models/face_detection_yunet_2023mar.onnx";

    // 拼接识别模型路径
    const QString recognizerModel = appDir + "/models/face_recognition_sface_2021dec.onnx";

    // 如果检测模型不存在
    if (!QFileInfo::exists(detectorModel)) {
        QMessageBox::warning(this, "模型缺失",
                             "未找到模型:\n" + detectorModel);
        return false;
    }

    // 如果识别模型不存在
    if (!QFileInfo::exists(recognizerModel)) {
        QMessageBox::warning(this, "模型缺失",
                             "未找到模型:\n" + recognizerModel);
        return false;
    }

    // 尝试创建 OpenCV 模型对象
    try {
        // 创建 YuNet 人脸检测器
        faceDetector = cv::FaceDetectorYN::create(
            detectorModel.toStdString(), // 模型文件路径
            "",                          // 配置文件路径,这里为空
            cv::Size(320, 320),          // 初始输入尺寸
            0.9f,                        // 置信度阈值
            0.3f,                        // NMS 阈值
            5000                         // topK
            );

        // 创建 SFace 人脸识别器
        faceRecognizer = cv::FaceRecognizerSF::create(
            recognizerModel.toStdString(), // 模型文件路径
            ""                             // 配置文件路径,这里为空
            );
    }
    // 如果 OpenCV 创建模型时报异常
    catch (const cv::Exception &e) {
        QMessageBox::critical(this, "OpenCV异常", e.what());
        return false;
    }

    // 返回两个模型对象是否都有效
    return !faceDetector.empty() && !faceRecognizer.empty();
}

// =========================
// 加载本地人脸库
// =========================
bool Widget::loadFaceDatabase(const QString &facesRootDir)
{
    // 先清空原有人脸库
    faceDatabase.clear();

    // 用给定路径创建根目录对象
    QDir rootDir(facesRootDir);

    // 如果根目录不存在
    if (!rootDir.exists()) {
        qDebug() << "faces 目录不存在:" << facesRootDir;
        return false;
    }

    // 获取 faces 下所有子目录(每个子目录对应一个人)
    QFileInfoList personDirs = rootDir.entryInfoList(
        QDir::Dirs | QDir::NoDotAndDotDot
        );

    // 遍历每个人的文件夹
    for (const QFileInfo &personInfo : personDirs) {
        // 文件夹名作为人名
        QString personName = personInfo.fileName();

        // 创建该人的目录对象
        QDir personDir(personInfo.absoluteFilePath());

        // 获取该目录下所有 jpg/jpeg/png/bmp 文件
        QFileInfoList imageFiles = personDir.entryInfoList(
            QStringList() << "*.jpg" << "*.jpeg" << "*.png" << "*.bmp",
            QDir::Files
            );

        // 遍历该人的每张图片
        for (const QFileInfo &imgInfo : imageFiles) {
            // 用 OpenCV 读取图片
            cv::Mat image = cv::imread(imgInfo.absoluteFilePath().toStdString());

            // 如果图片读取失败
            if (image.empty()) {
                qDebug() << "无法读取图片:" << imgInfo.absoluteFilePath();
                continue;
            }

            // 定义用于保存特征的变量
            cv::Mat feature;

            // 从图片中提取特征
            if (!extractFeature(image, feature)) {
                qDebug() << "图片中未检测到有效人脸:" << imgInfo.absoluteFilePath();
                continue;
            }

            // 创建一条人脸记录
            FaceEntry entry;

            // 保存姓名
            entry.name = personName;

            // 保存图片路径
            entry.imagePath = imgInfo.absoluteFilePath();

            // 保存特征,clone 避免共享内存问题
            entry.feature = feature.clone();

            // 加入人脸库
            faceDatabase.push_back(entry);
        }
    }

    // 打印人脸库加载数量
    qDebug() << "已加载人脸库特征数量:" << faceDatabase.size();

    // 返回是否成功加载到至少一条特征
    return !faceDatabase.empty();
}

// =========================
// 多张脸时,选择面积最大的一张
// =========================
int Widget::selectLargestFace(const cv::Mat &faces) const
{
    // 如果 faces 为空或行数小于等于 0
    if (faces.empty() || faces.rows <= 0) {
        return -1;
    }

    // 默认先选第 0 张
    int bestIndex = 0;

    // 当前最大面积
    float bestArea = 0.0f;

    // 遍历所有检测到的人脸
    for (int i = 0; i < faces.rows; ++i) {
        // 第 2 列是宽
        float w = faces.at<float>(i, 2);

        // 第 3 列是高
        float h = faces.at<float>(i, 3);

        // 计算面积
        float area = w * h;

        // 如果当前面积更大
        if (area > bestArea) {
            // 更新最大面积
            bestArea = area;

            // 记录当前下标
            bestIndex = i;
        }
    }

    // 返回最大人脸的下标
    return bestIndex;
}

// =========================
// 从一张图像中提取最大人脸的特征
// =========================
bool Widget::extractFeature(const cv::Mat &image, cv::Mat &feature)
{
    // 如果输入图像为空,直接失败
    if (image.empty()) {
        return false;
    }

    // clone 一份输入图像,避免修改原图
    cv::Mat input = image.clone();

    // 如果输入是 4 通道图像
    if (input.channels() == 4) {
        // 转成 BGR 3通道
        cv::cvtColor(input, input, cv::COLOR_BGRA2BGR);
    }
    // 如果输入是灰度图
    else if (input.channels() == 1) {
        // 转成 BGR 3通道
        cv::cvtColor(input, input, cv::COLOR_GRAY2BGR);
    }

    // 设置人脸检测器输入尺寸为当前图片大小
    faceDetector->setInputSize(input.size());

    // 用于保存检测结果
    cv::Mat faces;

    // 执行人脸检测
    int numFaces = faceDetector->detect(input, faces);

    // 如果没检测到人脸
    if (numFaces <= 0 || faces.rows <= 0) {
        return false;
    }

    // 选择面积最大的一张脸
    int bestIndex = selectLargestFace(faces);

    // 如果没有有效下标
    if (bestIndex < 0) {
        return false;
    }

    // 定义对齐裁剪后的人脸图像
    cv::Mat alignedFace;

    // 根据检测到的人脸框和关键点做对齐 + 裁剪
    faceRecognizer->alignCrop(input, faces.row(bestIndex), alignedFace);

    // 从对齐后的人脸图像中提取特征
    faceRecognizer->feature(alignedFace, feature);

    // 如果特征非空则成功
    return !feature.empty();
}

// =========================
// QImage 转 OpenCV Mat
// =========================
cv::Mat Widget::qImageToCvMat(const QImage &image) const
{
    // 把 QImage 转成 BGR888 格式
    QImage converted = image.convertToFormat(QImage::Format_BGR888);

    // 用 QImage 的底层数据构造 OpenCV Mat
    cv::Mat mat(
        converted.height(),         // 高
        converted.width(),          // 宽
        CV_8UC3,                    // 8位无符号 3通道
        const_cast<uchar*>(converted.bits()), // 图像数据指针
        converted.bytesPerLine()    // 每行字节数
        );

    // clone 一份,避免 QImage 生命周期结束后 Mat 指针失效
    return mat.clone();
}

// =========================
// 刷新界面中的摄像头预览
// =========================
void Widget::updatePreview(const QImage &image)
{
    // 如果 ui 无效,直接返回
    if (!ui) return;

    // 把 QImage 转成 QPixmap,并缩放到 label 大小
    QPixmap pixmap = QPixmap::fromImage(image).scaled(
        ui->labelCamera->size(),    // 目标大小
        Qt::KeepAspectRatio,        // 保持比例
        Qt::SmoothTransformation    // 平滑缩放
        );

    // 显示到 labelCamera 上
    ui->labelCamera->setPixmap(pixmap);
}

// =========================
// 对当前帧做人脸识别
// =========================
bool Widget::recognizeFrame(const cv::Mat &frame, QString &bestName, double &bestScore)
{
    // 默认姓名设为未知
    bestName = "未知";

    // 默认得分设为 -1
    bestScore = -1.0;

    // 当前帧特征
    cv::Mat currentFeature;

    // 先提取当前帧的人脸特征
    if (!extractFeature(frame, currentFeature)) {
        return false;
    }

    // 初始化最高分
    double maxScore = std::numeric_limits<double>::lowest();

    // 初始化最高分对应姓名
    QString maxName = "未知";

    // 遍历本地人脸库
    for (const FaceEntry &entry : faceDatabase) {
        // 用余弦相似度计算当前帧与库中样本的相似度
        double score = faceRecognizer->match(
            currentFeature,
            entry.feature,
            cv::FaceRecognizerSF::FR_COSINE
            );

        // 如果当前分数更高
        if (score > maxScore) {
            // 更新最高分
            maxScore = score;

            // 更新对应姓名
            maxName = entry.name;
        }
    }

    // 输出最终识别到的姓名
    bestName = maxName;

    // 输出最终最高得分
    bestScore = maxScore;

    // 打印调试信息
    qDebug() << "threshold =" << recognitionThreshold
             << ", bestName =" << bestName
             << ", bestScore =" << bestScore
             << ", pass =" << (bestScore >= recognitionThreshold);

    // 得分大于等于阈值就认为成功
    return bestScore >= recognitionThreshold;
}

// =========================
// 识别成功后的处理
// =========================
void Widget::finishRecognitionSuccess(const QString &name, double score)
{
    // score 当前没有直接用于界面,防止编译器警告
    Q_UNUSED(score);

    // 如果 ui 无效,直接返回
    if (!ui) return;

    // 显示识别到的姓名
    ui->labelName->setText(name);

    // 显示识别成功
    ui->labelResult->setText("识别成功");

    // 让计时器失效
    recognitionTimer.invalidate();

    // 停止当前识别状态
    setRecognitionRunning(false);
}

// =========================
// 识别失败后的处理
// =========================
void Widget::finishRecognitionFail()
{
    // 如果 ui 无效,直接返回
    if (!ui) return;

    // 显示未识别
    ui->labelName->setText("未识别");

    // 显示识别失败
    ui->labelResult->setText("识别失败");

    // 让计时器失效
    recognitionTimer.invalidate();

    // 停止当前识别状态
    setRecognitionRunning(false);
}

// =========================
// 摄像头每来一帧都会调用这个槽函数
// =========================
void Widget::onVideoFrameChanged(const QVideoFrame &frame)
{
    // 如果窗口正在关闭,或者 ui 已经失效,直接返回
    if (isClosing || ui == nullptr) {
        return;
    }

    // 把视频帧转成 QImage
    QImage image = frame.toImage();

    // 如果转换失败,直接返回
    if (image.isNull()) {
        return;
    }

    // 先把画面实时显示到界面
    updatePreview(image);

    // 如果当前没有开启识别,或者模型没准备好,或者人脸库为空
    if (!recognitionEnabled || !faceEngineReady || faceDatabase.empty()) {
        // 那就只显示画面,不做识别
        return;
    }

    // 如果当前轮识别已经超过 5 秒
    if (recognitionTimer.isValid() && recognitionTimer.elapsed() >= recognitionTimeoutMs) {
        // 直接判失败
        finishRecognitionFail();
        return;
    }

    // 帧计数器加 1
    ++frameCounter;

    // 不是每一帧都做识别,而是每隔 frameInterval 帧识别一次
    if (frameCounter % frameInterval != 0) {
        return;
    }

    // 把当前 QImage 转成 OpenCV Mat
    cv::Mat frameMat = qImageToCvMat(image);

    // 定义识别结果变量
    QString bestName;
    double bestScore = -1.0;

    // 对当前帧做人脸识别
    bool success = recognizeFrame(frameMat, bestName, bestScore);

    // 如果识别成功
    if (success) {
        // 结束本轮识别并显示成功
        finishRecognitionSuccess(bestName, bestScore);
        return;
    }

    // 如果这帧没成功,再次判断是否已经超时
    if (recognitionTimer.isValid() && recognitionTimer.elapsed() >= recognitionTimeoutMs) {
        // 超时则失败
        finishRecognitionFail();
        return;
    }
}

// =========================
// 点击"开始人脸识别"按钮
// =========================
void Widget::on_btnStartRecognition_clicked()
{
    // 如果窗口正在关闭,不允许再开始识别
    if (isClosing) {
        return;
    }

    // 如果模型没有准备好
    if (!faceEngineReady) {
        resetResult("未知", "模型未就绪");
        return;
    }

    // 如果人脸库为空
    if (faceDatabase.empty()) {
        resetResult("未知", "人脸库为空");
        return;
    }

    // 新一轮识别开始前,把帧计数器清零
    frameCounter = 0;

    // 启动识别计时器
    recognitionTimer.start();

    // 设置当前正在识别
    setRecognitionRunning(true);

    // 界面先显示"识别中"
    ui->labelName->setText("未识别");
    ui->labelResult->setText("识别中...");
}

// =========================
// 点击"停止人脸识别"按钮
// =========================
void Widget::on_btnStopRecognition_clicked()
{
    // 让当前识别计时器失效
    recognitionTimer.invalidate();

    // 设置当前不再识别
    setRecognitionRunning(false);

    // 更新界面文字
    resetResult("未识别", "已停止");
}

九、几个关键函数说明

1. initFaceEngine()

作用:

加载人脸检测模型 YuNet

加载人脸识别模型 SFace

判断模型文件是否存在

创建 OpenCV 检测器和识别器

2. loadFaceDatabase()

作用:

扫描 faces 文件夹

按文件夹名作为人名

读取每张人脸图片

提取人脸特征

保存到 faceDatabase

3. extractFeature()

作用:

检测图像中的人脸

选择最大人脸

做人脸对齐和裁剪

提取人脸特征向量

4. recognizeFrame()

作用:

提取当前摄像头帧的人脸特征

遍历本地人脸库

计算余弦相似度

找到最高分

判断是否超过阈值

5. onVideoFrameChanged()

作用:

接收摄像头每一帧

刷新画面

判断是否开启识别

控制识别频率

判断是否超时

成功或失败后结束本轮识别

6. stopCameraSafely()

作用:

关闭识别

停止计时器

断开视频帧信号

解除采集链路

停止摄像头

避免关闭窗口时报内存错误

十、实现过程中遇到的坑

坑 1:只写 include 不等于配置好了 OpenCV

很多初学者以为写了:

#include <opencv2/core.hpp>

就可以使用 OpenCV。实际上还必须在 CMake 中:

  1. 配置 OpenCV_DIR
  2. find_package(OpenCV REQUIRED ...)
  3. 链接 ${OpenCV_LIBS}
  4. 加入 ${OpenCV_INCLUDE_DIRS}

否则会出现:

fatal error: opencv2/core.hpp: No such file or directory

或者链接错误。

坑 2:OpenCV_DIR 路径容易写错

OpenCV_DIR 不是随便写 OpenCV 根目录,而是要写到包含 OpenCVConfig.cmake 的目录。

如果不确定,直接在 OpenCV 文件夹里搜索:

OpenCVConfig.cmake

找到它后,把 OpenCV_DIR 改成它所在的文件夹。

坑 3:运行时报找不到 DLL

编译通过不代表运行一定成功。Windows 下如果运行时报找不到 OpenCV DLL,需要把:

D:\FaceRecognition\opencv\build\x64\vc17\bin

加入系统环境变量 Path,或者把相关 DLL 复制到 exe 所在目录。

坑 4:models 和 faces 放在源码目录下,程序却找不到

因为代码里用的是:

QCoreApplication::applicationDirPath()

它指的是 exe 所在目录,不是源码目录。

所以需要在 CMake 里使用:

add_custom_command(...)

facesmodels 自动复制到 exe 所在目录。

坑 5:不要把人脸图片放到 qrc 资源文件里

OpenCV 的 cv::imread() 更适合读取普通磁盘路径,不适合直接读取 Qt 的 :/xxx 资源路径。

所以人脸库建议使用普通文件夹:

faces/人名/图片.jpg

坑 6:人脸库文件夹里必须真的有图片

只创建:

faces/LiuYiFei

faces/YangMi

还不够。里面必须放 jpg、png、bmp 等图片,否则程序会提示人脸库为空。

坑 7:相似度阈值不要一开始设太高

一开始你想用:

0.80

但实际测试中可能过高,容易导致明明是同一个人也识别失败。

调试阶段建议先用:

0.35 ~ 0.50

等功能稳定后再慢慢调高。

坑 8:不能每一帧都识别

摄像头可能一秒几十帧,如果每帧都做人脸识别,会导致:

CPU占用高

界面卡顿

结果频繁刷新

所以代码里使用:

frameInterval = 5;

表示每隔 5 帧识别一次。

坑 9:识别成功后不能继续一直识别

一开始如果一直连续识别,会出现:

某一帧识别成功

下一帧识别失败

界面又被刷新成失败

所以最终逻辑改成:

识别成功后立即停止本轮识别

重新识别必须再次点击开始按钮

坑 10:关闭窗口时报 Runtime Check Failure

你之前遇到过:

Run-Time Check Failure #2 - Stack around the variable 'w' was corrupted

这类问题通常是关闭窗口时摄像头回调还在执行,导致访问已经销毁的 ui 或对象。

解决方式是:

  1. 设置 isClosing = true

  2. 停止识别

  3. 断开 videoSink 信号

  4. 解除 captureSession 绑定

  5. 停止 camera

  6. 最后再释放 ui

你的实现中已经通过 closeEvent()、析构函数和 stopCameraSafely() 做了安全关闭处理。

坑 11:按钮槽函数不要重复连接

如果函数名已经是:

void on_btnStartRecognition_clicked();

Qt 的自动槽连接机制可能会自动连接。如果你又手动 connect() 一次,就可能导致按钮点击后逻辑执行两遍。

建议二选一:

方式1:使用 Qt 自动槽命名,不手动 connect

方式2:函数改普通名字,然后手动 connect

坑 12:MSVC 和 OpenCV 编译器版本要匹配

你下载的是 Windows 预编译 OpenCV,通常对应的是 MSVC 版本。如果你的 Qt Kit 用的是 MinGW,就可能出现链接问题。

建议使用:

Qt MSVC 2022 64-bit

OpenCV vc17

这样匹配更稳定。

展望

目前是在windows上实现的,自己没有编译opencv,后期将自己编译opencv库,并部署到开发板上。

相关推荐
深蓝海拓2 小时前
Qt的HSL色彩系统
笔记·python·qt·学习
永远睡不够的入2 小时前
C++11新特性(3):lambda不是玄学:从编译器生成的仿函数类彻底搞懂 C++ 匿名函数
开发语言·c++
HAPPY酷2 小时前
UE5 C++ 避坑指南:暴力移除 Electronic Nodes 插件,回归纯净开发
开发语言·c++·ue5
小此方2 小时前
Re:思考·重建·记录 现代C++ C++11篇 (四)C++ Lambda 全解析:编译器是如何为你生成仿函数的?
开发语言·c++·c++11·现代c++
Brilliantwxx2 小时前
【C++】初认识模版
开发语言·c++
c++之路2 小时前
C++ 命名空间(Namespace)
开发语言·c++·算法
艾莉丝努力练剑3 小时前
【Linux网络】计算机网络入门:Socket编程预备,从字节序共识到 Socket 地址结构的“伪多态”设计
linux·服务器·网络·c++·学习·计算机网络
借雨醉东风10 小时前
程序分享--常见算法/编程面试题:旋转矩阵
c++·线性代数·算法·面试·职场和发展·矩阵
云泽80811 小时前
笔试算法 - 双指针篇(二):四大经典求和题型 + 有效三角形计数问题
c++·算法