基于qt的opencv实时图像处理框架FastCvLearn实战

简介: 本文介绍了一个基于Qt的OpenCV实时图像处理框架FastCvLearn,通过手撕代码的方式详细讲解了如何实现实时人脸马赛克等功能,并提供了结果展示和基础知识回顾。

结果展示

基于qt的opencv实时处理框架FastCvLearn

基础知识回顾

首先回顾一下基础知识。

uint8_t
uint8_tunsigned char
unsigned char

BIN:0001 0000
BIN:0001 0000
NOT BIN:0001 0000
NOT BIN:0001 0000
static_cast
static_cast

手撕代码

capture_thread

对于enum 类型,感觉很陌生,如何剖析?

    enum MASK_TYPE{
   
                   RECTANGLE = 0,
                   LANDMARKS,
                   GLASSES,
                   MUSTACHE,
                   MOUSE_NOSE,
                   MASK_COUNT,
    };

使用qDebug()函数即可。

    qDebug()<<"CaptureThread::MASK_COUNT"<<CaptureThread::MASK_COUNT;
    qDebug()<<"CaptureThread::MOUSE_NOSE"<<CaptureThread::MOUSE_NOSE;
    qDebug()<<"CaptureThread::MUSTACHE"<<CaptureThread::MUSTACHE;
    qDebug()<<"CaptureThread::GLASSES"<<CaptureThread::GLASSES;
    qDebug()<<"CaptureThread::LANDMARKS"<<CaptureThread::LANDMARKS;
    qDebug()<<"CaptureThread::RECTANGLE"<<CaptureThread::RECTANGLE;

可以发现,原来其枚举值是整数。

CaptureThread::MASK_COUNT 5
CaptureThread::MOUSE_NOSE 4
CaptureThread::MUSTACHE 3
CaptureThread::GLASSES 2
CaptureThread::LANDMARKS 1
CaptureThread::RECTANGLE 0

那么,再结合先前我们百科得到的static_cast类型转换函数的定义,就不难理解下面这句代码了。对int 类型i,进行类型转换为枚举类型CaptureThread::MASK_TYPE。

static_cast<CaptureThread::MASK_TYPE>(i)

综合上述知识,下面这个函数可以理解为将uint8_t masks_flag与uint8_t bit进行位运算。其可作为多个算法的开关函数。

uint8_t masks_flag;
masks_flag = 0;
    void updateMasksFlag(MASK_TYPE type, bool on_or_off) {
   
        uint8_t bit = 1 << type;
        if(on_or_off) {
   
            masks_flag |= bit;
        } else {
   
            masks_flag &= ~bit;
        }
    };

isMaskOn的位运算同理。

    bool isMaskOn(MASK_TYPE type) {
   return (masks_flag & (1 << type)) != 0; };

updateMasksFlag使用和isMaskOn使用,实现的效果就算给实时的视频帧添加多个特效独立的开关。

实时的视频帧添加多个特效独立的开关

//updateMasksFlag使用
    QCheckBox *box = qobject_cast<QCheckBox*>(sender());
    for (int i = 0; i < CaptureThread::MASK_COUNT; i++){
   
        if (mask_checkboxes[i] == box) {
   
            capturer->updateMasksFlag(static_cast<CaptureThread::MASK_TYPE>(i), status != 0);
        }
    }

//isMaskOn使用
vector< vector<cv::Point2f> > shapes;
    if (mark_detector->fit(frame, faces, shapes)) {
   
        // draw facial land marks
        for (unsigned long i=0; i<faces.size(); i++) {
   
            if (isMaskOn(LANDMARKS)) {
   
                for(unsigned long k=0; k<shapes[i].size(); k++) {
   
                    cv::circle(frame, shapes[i][k], 2, color, cv::FILLED);
                     QString index = QString("%1").arg(k);
                     cv::putText(frame, index.toStdString(), shapes[i][k], cv::FONT_HERSHEY_SIMPLEX, 0.4, color, 2);
                }
            }
            if (isMaskOn(GLASSES))
                drawGlasses(frame, shapes[i]);
            if (isMaskOn(MUSTACHE))
                drawMustache(frame, shapes[i]);
            if (isMaskOn(MOUSE_NOSE))
                drawMouseNose(frame, shapes[i]);
        }
    }

下面来看一下,特效装饰素材。
MASK_TYPE
运用素材的方法:

在CaptureThread::run()线程里使用while函数,不停的调用分类器。注意帧数据的上锁和解锁,保证数据同步。


void CaptureThread::run() {
   
    running = true;
    cv::VideoCapture cap(cameraID);
    // cv::VideoCapture cap("/home/kdr2/Videos/WIN_20190123_20_14_56_Pro.mp4");
    cv::Mat tmp_frame;

    frame_width = cap.get(cv::CAP_PROP_FRAME_WIDTH);
    frame_height = cap.get(cv::CAP_PROP_FRAME_HEIGHT);

    const cv::String classifier_data = "haarcascades/haarcascade_frontalface_default.xml";
    classifier = new cv::CascadeClassifier(classifier_data);

    mark_detector = cv::face::createFacemarkLBF();
    QString model_data = QApplication::instance()->applicationDirPath() + "/data/lbfmodel.yaml";
    qDebug()<<"model_data"<<model_data;
    mark_detector->loadModel(model_data.toStdString());
    while(running) {
   
        cap >> tmp_frame;
        if (tmp_frame.empty()) {
   
            break;
        }

        if(masks_flag > 0)
            detectFaces(tmp_frame);

        if(taking_photo) {
   
            takePhoto(tmp_frame);
        }

        cvtColor(tmp_frame, tmp_frame, cv::COLOR_BGR2RGB);
        data_lock->lock();
        frame = tmp_frame;
        data_lock->unlock();
        emit frameCaptured(&frame);
    }
    cap.release();
    delete classifier;
    classifier = nullptr;
    running = false;
}

这里有个坑,cv::CascadeClassifier函数的输入需要是const cv::String格式的(即std::string),QString不行。

    const cv::String classifier_data = "haarcascades/haarcascade_frontalface_default.xml";
    classifier = new cv::CascadeClassifier(classifier_data);

保存图像bug,
本想借鉴如下toStdString()方式,发现运行不成功,toStdString(时间带冒号格式)方式的字符串,cv::imwrite调用后,写入图像无结果。

    QString photo_path = "";
    cv::imwrite(photo_path.toStdString(), frame);

后来发现是字符串格式问题,测试发现这句bool writeResult = cv::imwrite(“a:a.jpg”, frame);不行。
走过一些弯路后,发现时间日期格式修改后即可,将"yyyy-MM-dd+HH:mm:ss"修改为"yyyy-MM-dd+HH-mm-ss"。其中不能有冒号:。

在这里插入图片描述

接下来了解下,人脸识别的奥秘。

void CaptureThread::detectFaces(cv::Mat &frame)
{
   
    vector<cv::Rect> faces;
    cv::Mat gray_frame;
    cv::cvtColor(frame, gray_frame, cv::COLOR_BGR2GRAY);
    classifier->detectMultiScale(gray_frame, faces, 1.3, 5);

    cv::Scalar color = cv::Scalar(0, 0, 255); // red

    // draw the circumscribe rectangles
    if (isMaskOn(RECTANGLE)) {
   
        for(size_t i = 0; i < faces.size(); i++) {
   
            cv::rectangle(frame, faces[i], color, 1);
        }
    }

    vector< vector<cv::Point2f> > shapes;
    if (mark_detector->fit(frame, faces, shapes)) {
   
        // draw facial land marks
        for (unsigned long i=0; i<faces.size(); i++) {
   
            if (isMaskOn(LANDMARKS)) {
   
                for(unsigned long k=0; k<shapes[i].size(); k++) {
   
                    cv::circle(frame, shapes[i][k], 2, color, cv::FILLED);
                     QString index = QString("%1").arg(k);
                     cv::putText(frame, index.toStdString(), shapes[i][k], cv::FONT_HERSHEY_SIMPLEX, 0.4, color, 2);
                }
            }
            if (isMaskOn(GLASSES))
                drawGlasses(frame, shapes[i]);
            if (isMaskOn(MUSTACHE))
                drawMustache(frame, shapes[i]);
            if (isMaskOn(MOUSE_NOSE))
                drawMouseNose(frame, shapes[i]);
        }
    }
}

classifier->detectMultiScale(gray_frame, faces, 1.3, 5);用来创建多尺度的分类器,检测到的对象作为矩形列表返回。

detectMultiScale参数的理解可以参考:https://www.it610.com/article/1295047563740782592.htm

detectMultiScale()

cv::face::createFacemarkLBF()用来创建局部二值特征(LBF)。

局部二值特征(LBF)具体可参考科普文:https://blog.csdn.net/qq_14845119/article/details/53575091

opencv官方文档介绍不多。
cv::face::createFacemarkLBF()
mark_detector->fit(frame, faces, shapes)用来画特征点。
mark_detector->fit(frame, faces, shapes)素材加载的方法:
这里QImage 与cv::Mat的相互转化值得借鉴。


void CaptureThread::loadOrnaments()
{
   
    QImage image;
    image.load(":/images/glasses.jpg");
    image = image.convertToFormat(QImage::Format_RGB888);
    glasses = cv::Mat(
        image.height(), image.width(), CV_8UC3,
        image.bits(), image.bytesPerLine()).clone();

    image.load(":/images/mustache.jpg");
    image = image.convertToFormat(QImage::Format_RGB888);
    mustache = cv::Mat(
        image.height(), image.width(), CV_8UC3,
        image.bits(), image.bytesPerLine()).clone();

    image.load(":/images/mouse-nose.jpg");
    image = image.convertToFormat(QImage::Format_RGB888);
    mouse_nose = cv::Mat(
        image.height(), image.width(), CV_8UC3,
        image.bits(), image.bytesPerLine()).clone();
}

drawGlasses(frame, shapes[i]);
该函数对角度敏感,角度变化大时,失效了。选取的特征点是根据landmarks排序后位置来手工定义的。比如:resize和rotate中的marks[]中的数字。

void CaptureThread::drawGlasses(cv::Mat &frame, vector<cv::Point2f> &marks)
{
   
    // resize
    cv::Mat ornament;
    double distance = cv::norm(marks[45] - marks[36]) * 1.5;
    cv::resize(glasses, ornament, cv::Size(0, 0), distance / glasses.cols, distance / glasses.cols, cv::INTER_NEAREST);

    // rotate
    double angle = -atan((marks[45].y - marks[36].y) / (marks[45].x - marks[36].x));
    cv::Point2f center = cv::Point(ornament.cols/2, ornament.rows/2);
    cv::Mat rotateMatrix = cv::getRotationMatrix2D(center, angle * 180 / 3.14, 1.0);

    cv::Mat rotated;
    cv::warpAffine(
        ornament, rotated, rotateMatrix, ornament.size(),
        cv::INTER_LINEAR, cv::BORDER_CONSTANT, cv::Scalar(255, 255, 255));

    // paint
    center = cv::Point((marks[45].x + marks[36].x) / 2, (marks[45].y + marks[36].y) / 2);
    cv::Rect rec(center.x - rotated.cols / 2, center.y - rotated.rows / 2, rotated.cols, rotated.rows);
    frame(rec) &= rotated;
}

cv::norm求解范数
一种非严密的解释就是,对应向量范数,向量空间中的向量都是有大小的,这个大小如何度量,就是用范数来度量的,不同的范数都可以来度量这个大小,就好比米和尺都可以来度量远近一样;对于矩阵范数,学过线性代数,我们知道,通过运算AX=B,可以将向量X变化为B,矩阵范数就是来度量这个变化大小的。

OpenCV:norm-范数求解函数:https://jingyan.baidu.com/article/454316ab3d46d4f7a7c03a89.html

cv::resize
要缩小图像,通常使用INTER_AREA插值效果最佳,而要放大图像,通常使用c :: INTER_CUBIC(速度慢)或INTER_LINEAR(速度更快,但看起来仍然可以)最好。
在这里插入图片描述
double angle = -atan((marks[45].y - marks[36].y) / (marks[45].x - marks[36].x));
反正切求角度

frame(rec) &= rotated;
局部ROI区域添加蒙版图。

凑近看landmarks的序号的时候,发现程序闪退,发现是这面这个问题:

OpenCV: terminate handler is called! The last OpenCV error is:
OpenCV(4.5.1) Error: Assertion failed (0 <= roi.x && 0 <= roi.width && roi.x + roi.width <= m.cols && 0 <= roi.y && 0 <= roi.height && roi.y + roi.height <= m.rows) in cv::Mat::Mat, file D:\work\OpenCV451Sln\opencv-4.5.1\modules\core\src\matrix.cpp, line 811

查看了下面的代码参考后,认定是rec的问题:

https://github.com/Huang9495/roi-

解决方法:

//    cv::Rect rec(center.x - rotated.cols / 2, center.y - rotated.rows / 2, rotated.cols, rotated.rows);
    cv::Rect rec((center.x - rotated.cols / 2)>0?(center.x - rotated.cols / 2):0,
                 (center.y - rotated.rows / 2)>0?(center.y - rotated.rows / 2):0,
                 rotated.cols>0?rotated.cols:0,
                 rotated.rows>0?rotated.rows:0);
    frame(rec) &= rotated;

该方式解决后,胡子和花猫的素材,凑近后仍有程序自动退出问题。解决方向是对的,后续有需要再优化吧。

基于qt的opencv实时处理框架FastCvLearn参考链接:
1 : Qt-5-and-OpenCV-4-Computer-Vision-Projects.
2 : FastCvLearn代码仓库,欢迎stars

相关文章
|
11天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
8天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2520 17
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
|
7天前
|
机器学习/深度学习 算法 数据可视化
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
2024年中国研究生数学建模竞赛C题聚焦磁性元件磁芯损耗建模。题目背景介绍了电能变换技术的发展与应用,强调磁性元件在功率变换器中的重要性。磁芯损耗受多种因素影响,现有模型难以精确预测。题目要求通过数据分析建立高精度磁芯损耗模型。具体任务包括励磁波形分类、修正斯坦麦茨方程、分析影响因素、构建预测模型及优化设计条件。涉及数据预处理、特征提取、机器学习及优化算法等技术。适合电气、材料、计算机等多个专业学生参与。
1522 15
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
|
3天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
9天前
|
编解码 JSON 自然语言处理
通义千问重磅开源Qwen2.5,性能超越Llama
击败Meta,阿里Qwen2.5再登全球开源大模型王座
574 14
|
1月前
|
运维 Cloud Native Devops
一线实战:运维人少,我们从 0 到 1 实践 DevOps 和云原生
上海经证科技有限公司为有效推进软件项目管理和开发工作,选择了阿里云云效作为 DevOps 解决方案。通过云效,实现了从 0 开始,到现在近百个微服务、数百条流水线与应用交付的全面覆盖,有效支撑了敏捷开发流程。
19282 30
|
10天前
|
人工智能 自动驾驶 机器人
吴泳铭:AI最大的想象力不在手机屏幕,而是改变物理世界
过去22个月,AI发展速度超过任何历史时期,但我们依然还处于AGI变革的早期。生成式AI最大的想象力,绝不是在手机屏幕上做一两个新的超级app,而是接管数字世界,改变物理世界。
481 49
吴泳铭:AI最大的想象力不在手机屏幕,而是改变物理世界
|
1月前
|
人工智能 自然语言处理 搜索推荐
阿里云Elasticsearch AI搜索实践
本文介绍了阿里云 Elasticsearch 在AI 搜索方面的技术实践与探索。
18839 20
|
1月前
|
Rust Apache 对象存储
Apache Paimon V0.9最新进展
Apache Paimon V0.9 版本即将发布,此版本带来了多项新特性并解决了关键挑战。Paimon自2022年从Flink社区诞生以来迅速成长,已成为Apache顶级项目,并广泛应用于阿里集团内外的多家企业。
17528 13
Apache Paimon V0.9最新进展
|
2天前
|
云安全 存储 运维
叮咚!您有一份六大必做安全操作清单,请查收
云安全态势管理(CSPM)开启免费试用
364 4
叮咚!您有一份六大必做安全操作清单,请查收