之后都以之前的demo为中心。进行opencv的绘制。
接下来就是识别这一块的output信息了,并且把它打印出来:

image.png

打印点成功:

image.png

这个landmark的意思是有多少个关键点。

image.png

这些数据,是否闭眼、闭嘴、微笑。
2.8 Opencv进行标记
安装opencv库:

        sudo apt-get install libopencv-dev

image.png

image.png

引入成功。
之后从output这儿获取数据:
标记算法:遍历面部关键点,再根据这个坐标进行一个图片的绘制:

image.png

Circle()函数会绘制框,颜色就是绿色了。

但是!!这个程序的图像是用OpenGL绘制的。而OpenCV是静态处理图像的。
尝试用这个函数:
image.png

失败:并没有任何变化:
image.png

经查验,发现这个函数的用法是OpenGL老版本的用法了,叫做 immediate Mode
在现代OpenGL中,用VBOs和VAOs可以在GPU中存储顶点数据,并通过VAOs快速配置这些数据。
步骤:
1,设置全局变量:
image.png
2,初始化VAO、VBO变量:
image.png
3,检测人脸关键点,并准备好绘制它之前,需要更新VBO的数据。
image.png
4,然后开始绘制:
image.png
经实验,还是不行。理想状况下只能用opencv了可能,但Opencv的信息和OpenGL怎么进行一个结合呢?

什么是opencv?
Opencv是一个开源的视觉和机器学习软件库,其中包含了许多图像处理和计算机视觉方面的通用算法。并提供了C++等多种语言的接口。主要功能有:图像处理、图像分析、特征检测和描述、机器视觉、计算机视觉、视频分析。

OpenCV和OpenGL如何结合呢?
OpenCV主要关注图像和视频的分析、处理和识别,而那个VNN的人脸识别函数已经做到这个事情了,甚至说已经把这个Output数据已经传出来了,此时再让Opencv出现,就没有什么必要了。GPT提了一嘴,说可以用CV::circle在关键点位置绘制圆圈,然后再把这个图像处理为适合OpenGL使用的格式,并将其上传为纹理,最后,在OpenGL中渲染这个带有人脸关键点标记的图像。

问题来了,什么是纹理?
纹理是一种用于增加三维模型表面细节的图像或数据集。在计算机图形中,纹理允许我们给简单的三维形状添加复杂的外观,比如模拟现实世界的表面特性,如木纹、砌墙、皮肤等。通过将纹理映射到3D模型的表面,可以使模型看起来更加真实和详细。
但对图像的每一种操作,比如美颜、口红、瘦脸、都是OpenGL在操作。如果把opencv加进来,相当于多了一步操作。

以前是:人脸检测关键点 ---> OpenGL渲染 ---> 滤镜 --->OpenGL渲染
现在是:人脸检测关键点 ---> OpenCV画图 --->OpenGL渲染 ---> 滤镜 --->OpenGL渲染。
这样做从理论上是可行的。
但经查阅资料,我发现OpenGL本身也是有绘图的功能的,也就是上面我曾尝试的工作。所以接下来还是想办法从容易到困难,先尝试第二种方法吧。
发现有这个回调函数:
image.png

2.11 什么是回调函数呢?
回调函数是一种允许某个事件发生时被自动执行的函数。它不是由程序直接调用的,而是在满足某些条件或某个特定事件发生时,由另一个函数或框架调用的。这种机制允许程序在需要时,“回调”提供的函数,执行某些操作。
具体而言:
第一步:
image.png

在C++中,using关键字可以用来定义类型的别名,比typedef更加高级一些的用法。它可以为现有类型创建一个名字。在这行代码中,CallbackFunction是作为一个别名存在的,指代一类特定签名的函数。

2.11.1什么是特定签名?
这里指的是所有具有相同参数列表和返回类型的函数。在本语句情况下,这个特定签名指的是所有接受一个int类型参数并返回void的函数或可调用对象。

2.11.2什么是泛型?
在编程中,泛型是一种在编译时能够处理不同数据类型的特性。
Std::function是C++标准库中的一个模板类,是一个通用的、能够处理不同类型的可调用实体的模板类。它能够包装几乎任何形式的可调用对象,使得使用者可以以统一的方式处理不同类型的函数调用。
能够包装几乎所有类型的可调用实体:这表明它能够存储和调用多种类型的可调用对象,包括:
普通函数、lambda表达式:[](int x) { return x * x; }等。

2.11.3 什么是lamdba表达式?
Lamda表达式是C++11引入的一种定义匿名函数对象的方式,它允许在需要函数对象的地方快速定义一个函数。Lambda表达式的一般形式如下:
image.png
Capture是指捕获列表,定义了lambda表达式可以访问的外部变量,以及如何访问这些变量。
Parameters是指传递给表达式的参数。
Return_type是指返回类型,是个可选项,如果省略,编译器会根据函数体中的返回语句自动推断返回类型。
Funtion_body是指具体的实现代码了。

2.11.4 回调函数怎么写
image.png

在这个代码中,numbers是普通的参数列表。而CallbackFunction则是之前定义的万金油函数类型。Callback是函数名。在这里我们是第一次见函数参数列表中出现了函数,出现在这儿的函数,我们就把他叫做回调函数了。进入那个循环之后,函数不停得被调用,最终打印出结果就结束了。
问题来了,printNumber这个函数如果写在main函数外面,那processNumbers参数列表把那个函数去了,那就和普通的函数一样了吧?回调函数和普通函数是如何区分的呢?
终于答案揭晓了,回调函数是一个异步进行的函数,当设置好回调函数后,该函数就会挂载到内存中等待特定事件的到来,在此期间是可以运行其它函数的。不至于让程序变闲置状态。

image.png

那这个代码的意思就是说,landmarks这个参数准备好的时候,就会调用这个cb函数了,这个cb函数是_face_detector_callbacks函数库的一个集合。然后一一去调用和渲染它才对。
暂时找不到这一部分内容,所以我先探测一下吧:

image.png

然后发现它就处理一个函数:

image.png

那就看看那最终的一个回调函数的具体实现代码是怎么样的吧。经过梳理,流程是这样的:
1,在C++的人脸检测函数完成后,会生成一系列人脸关键点数据,这些数据会被存储在landmarks这个std::vector<float>变量中。
然后进入回调函数cd中,激活这个回调函数,在回调函数这儿说实话,没太弄清之后发生了什么,代码跳转功能是不太强的。反正就是在这一步是不太清楚这些关键点数据是怎么输出出去的,但可以确定的是,获取键盘的参数信息后,landmarks就会发生变化,这些变化的语句是暂时没找到的,之后看看GDB调试的过程吧。

image.png
在这个渲染函数这儿,最终进入proceed函数:
image.png

3,这个proceed函数的过程:
1,先进行一个检查updateTargets的bool变量,如果为真,则说明需要更新当前帧。
2,对图像设置新的帧缓存。
3,将原始的帧缓存先拷贝上去。
4,IsPrepared方法中,通过检查所有输入帧缓冲的状态来确定是否所有的数据都已经准备就绪,以便进行下一步的渲染。如果所有必要的输入帧缓冲都已就绪,那么渲染目标被认为是准备好了的。
5,Update执行更新过程。
6,unPrepear过程进行一个帧缓存的清理工作。
之后就是对update过程进行一个详细的解析试试了,解析失败,发现这是比较不好用的。

回到原点:
image.png

这个函数是回调函数,这个代码的意思是调用所有注册到_face_detector_callbacks这个容器中的所有函数,那么现在的问题就是,无法查看注册的函数是什么,不然的话,我也再注册进去一个函数,然后画一下,每一帧都出来,我觉得这个思路应该是对的。
2.11.5 查看所有注册的函数

  经过逐层查找,最终发现了是在主函数中的这儿:

image.png

为什么调用cd函数,就会到这儿的这个机理。之后需要补一下,这儿先进行一个标记。
现在先关注主要矛盾:

每一次获得landmarks之后,就会调用这三个函数,分别是口红、腮红、瘦脸三个滤镜。

我们所能标记的,就是嘴唇的位置、腮红的位置、脸的位置,那还缺失着脸呢主要是。先一个一个进去瞅一瞅。
上面这段代码注册了一个匿名的lambda表达式作为回调函数。这个lambda表达式捕获了当前作用域内的lip、blush、face_reshape对象,并将landmarks分别传入这三个对象的关键函数中。
lip这个类:

image.png
是一个派生类对象。
image.png
这个函数刚开始会寻找是否获取到了人脸的关键点。
1,获取到了之后,会对每个坐标值 * 2 - 1(逆向恢复)。
2,处理后的关键点数据会被保存到face_land_marks_变量中。
3,将是否检测到人脸标记为true。
这个函数属于是把这三个关键点都传给三个相应的实例中,进行一个保存,还谈不上对其继续宁渲染,充其量算是渲染的准备了。
image.png

这个才是渲染的流水线工作。先应用唇彩效果、然后进行腮红处理,然后进行美颜处理,一层一层的更改掉自己原本的纹理数据,最后将图像渲染到target_view中。

image.png
这个函数的作用是:
将一个target对象,添加到当前的source对象中,首先先调用了一个方法来获取下一个可用的纹理索引。这是因为一个target可能会从多个source中接收图像,每个图像可能需要独立的纹理来存储,因此需要确定使用哪个纹理槽。得到纹理索引后,函数再调用addTarget(target, targetTexIdx),这个重载版本的addTarget方法接受目标target和指定的纹理索引targetTexIdx作为参数,建立Source到Target的连接。


柬之不是剪枝
1 声望0 粉丝