一、项目效果展示
本项目基于 C++、Qt Widgets、Qt Multimedia、OpenCV DNN 实现了一个本地人脸识别小系统。系统可以在 Qt 界面中实时显示摄像头画面,点击"开始人脸识别"按钮后,程序会在 5 秒内对摄像头画面中的人脸进行检测和识别。如果当前人脸与本地人脸库中的某个样本相似度超过设定阈值,则显示对应的人名并提示"识别成功";如果 5 秒内没有识别成功,则显示"未识别"和"识别失败"。
本项目的核心功能包括:
-
Qt 界面显示摄像头实时画面
-
使用 OpenCV 加载 YuNet 人脸检测模型
-
使用 OpenCV 加载 SFace 人脸识别模型
-
扫描本地 faces 文件夹,构建本地人脸库
-
点击按钮后进行一次 5 秒识别
-
识别成功后自动停止识别
-
识别失败超时后自动停止识别
-
关闭窗口时安全释放摄像头资源

二、开发环境
操作系统:Windows 10 / Windows 11
开发工具:Qt Creator
Qt版本:Qt 6.x
编译器:MSVC 2022 64-bit
OpenCV版本:OpenCV 4.12.0
构建工具:CMake
编程语言:C++
使用模型:
-
face_detection_yunet_2023mar.onnx
-
face_recognition_sface_2021dec.onnxV
本项目使用 Qt 负责界面和摄像头采集,OpenCV 负责人脸检测、特征提取和相似度匹配。
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
opencv:存放 OpenCV 官方解压后的库文件faces:存放本地人脸库,每个人一个文件夹models:存放 OpenCV DNN 需要的两个 ONNX 模型(https://github.com/opencv/opencv_zoo/raw/main/models/face_detection_yunet/face_detection_yunet_2023mar.onnx- https://github.com/opencv/opencv_zoo/blob/main/models/face_detection_yunet/face_detection_yunet_2023mar.onnx)
widget.ui:Qt Designer 设计的界面widget.cpp:核心功能实现文件widget.h:窗口类声明文件CMakeLists.txt:工程构建配置文件
四、界面设计
| 控件类型 | 对象名 | 作用 |
|---|---|---|
| QLabel | labelCamera | 显示摄像头画面 |
| QLabel | labelName | 显示识别到的人名 |
| QLabel | labelResult | 显示识别结果 |
| QPushButton | btnStartRecognition | 开始人脸识别 |
| QPushButton | btnStopRecognition | 停止人脸识别 |
在 Qt Designer 中拖入一个较大的 QLabel 用来显示摄像头画面,将其对象名设置为 labelCamera。另外再添加两个 QLabel,分别用于显示姓名和识别状态,对象名分别为 labelName 和 labelResult。最后添加两个按钮,分别用于开始识别和停止识别,对象名设置为 btnStartRecognition 和 btnStopRecognition。

五、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 所在目录。所以需要把 faces 和 models 自动复制到 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 中:
- 配置
OpenCV_DIR find_package(OpenCV REQUIRED ...)- 链接
${OpenCV_LIBS} - 加入
${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(...)
把 faces 和 models 自动复制到 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 或对象。
解决方式是:
-
设置 isClosing = true
-
停止识别
-
断开 videoSink 信号
-
解除 captureSession 绑定
-
停止 camera
-
最后再释放 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库,并部署到开发板上。