最终效果

笔者识别

test.h

#ifndef TEST_H
#define TEST_H

#include <QWidget>
#include <QTimer>
#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>

QT_BEGIN_NAMESPACE
namespace Ui {
class test;
}
QT_END_NAMESPACE

class test : public QWidget
{
    Q_OBJECT

public:
    explicit test(QWidget *parent = nullptr);
    ~test() override;

private slots:
    void on_ptn_start_clicked();
    void readFrame();
    void on_ptn_record_clicked();

private:
    Ui::test *ui;
    cv::VideoCapture cap;
    QTimer *timer;
    cv::CascadeClassifier faceCascade;  // 增加分类器对象,用于加载模型

    cv::dnn::Net faceDetector;
    cv::dnn::Net faceRecognizer;

    std::map<QString, cv::Mat> faceDatabase;
    cv::Mat lastFeature;

    void loadDatabase();
    void saveDatabase();

    QString recognizeFace(cv::Mat feature);

    // 增加录入人脸功能
    bool recording = false;
    QString recordName;
    int recordCount = 0;
    const int maxSamples = 30;

    void testopencv();
};
#endif // TEST_H

test.cpp

#include "test.h"
#include "ui_test.h"

#include <QImage>
#include <QPixmap>

#include <opencv2/opencv.hpp>

test::test(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::test)
{
    ui->setupUi(this);
    timer = new QTimer(this);

    // readFrame通过定时器时间循环来触发
    // 如果通过while(1)循环,UI会卡死
    connect(timer, &QTimer::timeout, this, &test::readFrame);

    // 使用分类器加载opencv自带的模型
    if(!faceCascade.load("haarcascade_frontalface_default.xml")) {
        qDebug()<<"face cascade load failed";
    }

    // 加载人脸检测模型,SSD CNN检测器
    faceDetector = cv::dnn::readNetFromCaffe("models/deploy.prototxt",
        "models/res10_300x300_ssd_iter_140000.caffemodel");

    // 人脸特征模型:128维人脸特征
    faceRecognizer = cv::dnn::readNetFromTorch("models/nn4.small2.v1.t7");

    loadDatabase();
}

test::~test()
{
    delete ui;
}

void test::on_ptn_start_clicked()
{
    //testopencv();
    cap.open(0);   // 0 = 默认摄像头
    if(!cap.isOpened()) {
        qDebug()<<"camera open failed";
        return;
    }

    timer->start(30);   // 30ms ≈ 33fps
}

void test::readFrame()
{
    cv::Mat frame;
    cv::Mat gray;

    cap >> frame;
    if(frame.empty())
        return;
#if 0
    std::vector<cv::Rect> faces;
    cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);

    // OpenCV 默认BGR, 而QT是RGB
    // cv::cvtColor(frame, frame, cv::COLOR_BGR2RGB);

    // scaleFactor含义: 图像缩放比例。原理: 模型训练时的窗口大小是固定的(比如 24*24),
    // 为了检测图像中不同大小的人脸,算法会不断缩放原图。
    // 影响: * 1.1 表示每次缩放时图像缩小 10%。
    // 值越小(如 1.05): 缩放层级越密,检测更精细,不容易漏掉小脸,但计算量剧增,速度变慢。
    // 值越大(如 1.4): 缩放速度快,检测速度快,但可能会跳过某些尺寸的人脸,导致漏检。
    // minNeighbors含义: 最小相邻矩形个数。
    // 原理: 滑动窗口在检测时,同一个目标附近可能会产生多个重叠的检测框。
    // 该参数规定:只有当一个目标被至少 3 个重叠框同时检测到时,才会被最终判定为“人脸”。
    // 影响:值越高: 准确度越高,能有效过滤噪声和误报(False Positives),但可能导致难以识别光线不好的人脸。
    // 值越低(如 0 或 1): 极其灵敏,但会有大量错误识别(把背景里的阴影看成人脸)。
    // minSize含义: 目标的最小尺寸。说明: 低于这个尺寸(比如 30*30)的对象会被忽略。
    // 如果你是在近距离摄像头前,可以调大这个值以提高速度;如果你需要检测远处的人脸,则需要调小它。
    faceCascade.detectMultiScale(gray, faces,
                    1.1, 3, 0, cv::Size(30,30));
    // 画面中识别到多张脸,都画出来
    for(auto &face : faces)
    {
        cv::rectangle(frame, face, cv::Scalar(0,255,0), 2);
    }
    cv::cvtColor(frame, frame, cv::COLOR_BGR2RGB);

    QImage img(frame.data, frame.cols, frame.rows, frame.step, QImage::Format_RGB888);

    ui->label_camera->setPixmap(QPixmap::fromImage(img));
#else
    // 人脸检测
    cv::Mat blob = cv::dnn::blobFromImage(frame, 1.0, cv::Size(300,300), cv::Scalar(104,177,123));

    faceDetector.setInput(blob);

    cv::Mat detections = faceDetector.forward();

    // 解析检测结果
    cv::Mat detMat(
        detections.size[2],
        detections.size[3],
        CV_32F,
        detections.ptr<float>());

    for(int i=0;i<detMat.rows;i++)
    {
        float confidence = detMat.at<float>(i,2);
        if(confidence < 0.6) continue;
        int x1 = detMat.at<float>(i,3)*frame.cols;
        int y1 = detMat.at<float>(i,4)*frame.rows;
        int x2 = detMat.at<float>(i,5)*frame.cols;
        int y2 = detMat.at<float>(i,6)*frame.rows;

        cv::Rect faceRect(x1,y1,x2-x1,y2-y1);
        cv::rectangle(frame,faceRect,
                      cv::Scalar(0,255,0),2);

        // 裁剪人脸
        cv::Mat face = frame(faceRect);
        // 生成输入
        cv::Mat faceBlob = cv::dnn::blobFromImage(
                face,
                1.0/255,
                cv::Size(96,96),
                cv::Scalar(),
                true,
                false );
        // 送入网络
        faceRecognizer.setInput(faceBlob);

        cv::Mat feature = faceRecognizer.forward();

        lastFeature = feature.clone();
        QString name = recognizeFace(feature);

        cv::putText(frame,
                    name.toStdString(),
                    cv::Point(x1,y1-10),
                    cv::FONT_HERSHEY_SIMPLEX,
                    0.8,
                    cv::Scalar(0,255,0),
                    2);
    }
    cv::cvtColor(frame,frame,
                 cv::COLOR_BGR2RGB);

    QImage img(frame.data,
               frame.cols,
               frame.rows,
               frame.step,
               QImage::Format_RGB888);

    ui->label_camera->setPixmap(
        QPixmap::fromImage(img)
            .scaled(ui->label_camera->size(),
                    Qt::KeepAspectRatio)
        );

#endif
}

void test::on_ptn_record_clicked()
{
    QString name = ui->lineEdit_name->text();
    if(lastFeature.empty()) {
        qDebug()<<"no face feature";
        return;
    }

    if(name.isEmpty()) {
        qDebug()<<"name empty";
        return;
    }
    faceDatabase[name] = lastFeature;

    saveDatabase();
}

void test::loadDatabase()
{
    cv::FileStorage fs("database/faces.yml", cv::FileStorage::READ);
    if(!fs.isOpened()) {
        qDebug() << "database not exist";
        return;
    }

    for(auto it = fs.root().begin(); it != fs.root().end(); ++it) {
        cv::Mat feature;
        (*it) >> feature;
        faceDatabase[ QString::fromStdString((*it).name()) ] = feature;
    }
    fs.release();
}

void test::saveDatabase()
{
    cv::FileStorage fs("database/faces.yml", cv::FileStorage::WRITE);
    if(!fs.isOpened()) {
        qDebug() << "database open error";
        return;
    }
    for(auto &item : faceDatabase) {
        fs << item.first.toStdString() << item.second;
    }
    fs.release();
}

QString test::recognizeFace(cv::Mat feature)
{
    double minDist = 999;
    QString name = "Unknown";

    for(auto &item : faceDatabase) {
        double dist = cv::norm(feature,item.second);
        if(dist < minDist) {
            minDist = dist;
            name = item.first;
        }
    }

    if(minDist < 0.6)
        return name;

    return "Unknown";
}

void test::testopencv()
{
    cv::Mat img = cv::imread("test.jpg");

    if(img.empty())
        qDebug()<<"opencv load fail";
    else
        cv::imshow("test", img);
}