查看原文
其他

iOS移动端人脸贴纸实践

王易扬老师 好未来技术 2023-03-15

出于业务的需求,我们实现了一套直播可用的轻量型人脸贴纸。下面简单介绍一下实现的过程和难点。

01

技术分析

对于这个需求,实现上需要考虑的地方有:
  • 首先保证可用和效果良好;
  • 由于使用场景是在直播,本身功能的各方面性能消耗就高,所以要尽可能的优化性能;
  • 控制包增量;
  • 侵入性低;
  • 使用方便。
经过调研,我们的技术方案确定如下:
  1. 视屏处理:视频方面采用GPUImage来进行处理。开源,完成度高,而且流式结构以及对滤镜,视频的抽象都很好,使用方便。唯一美中不足的是采用单例来保存BufferCache以及Context,对于只是实现额外贴纸功能的我们(APP的主功能并非视频处理)带来的内存增长利大于弊【注释1】。
  2. 人脸识别模块:人脸识别模块我们做了很多调研。其中包括付费的商汤,Face++,开源的智云(HyperLandmark),OpenCV,原生的Vision,ARKit等。多方对比过:商汤,Face++这些在人脸准确性,稳定性,使用便捷上都非常出色,但是本身带有不小的包增量,对于我们这个小功能来说负担很大。开源方面,智云只有安卓版本,而iOS版本(HyperLandmark-iOS)内部使用的引擎是opencv,稳定性和准确性不高,另外包增量很大(OpenCV依赖,内部很难分离其中的人脸识别模块,cv::mat的数据流通也需要转换)。原生方面ARKit的性能由于实时分析,结构光等原因,很难在低端机使用(本身版本限制,硬件限制很多)。只有Vision是相对较好的选择,无包增量,识别效果较好,稳定性虽然较差,但是这个可以通过算法优化。版本限制也不高(iOS12即可使用),考虑到性能问题本身就有限制,这个的影响其实可以接受。当然,Vision作为一个原生库,很多地方都不够完善,例如欧拉角的识别,识别点防抖的处理等。
  3. 人脸贴图:人脸贴图与视频的合成,视频处理的内容,使用OpenGL。贴纸格式参考市面上SDK的部分进行设计。

02

流程介绍

实现上大致分成三条线处理,这三部分是异步的互不干扰:
  • 接收到视频数据后,对视频方向和分辨率进行处理(视频方向对人脸识别精度影响很大),生成人脸特征点,留待备用。
  • 接收到人脸贴纸后,对放大比例,坐标,特征点,触发进行处理,留待备用
  • 拿到视频数据,人脸特征点,贴纸图片和信息,进行绘制

03

实现细节及难点

1.贴纸json解析width和height,以及贴纸图片地址,每帧速率都很容易解析。
定位相关的数据在下面
"position": { "flag": 49, "x": 185.59, "y": 299.39 },
position描述了特征点距离贴纸左上的XY距离,如果贴纸本身具有倾斜角,可以使用rollOffset。
"rotateCenter": [ { "flag": 49, "x": 239.92, "y": 357.87 } ],
rotateCenter描述了贴纸的旋转中心,一般是鼻尖。人的头部偏转时,贴纸围绕这个点进行旋转
"scale": { "pointA": { "flag": 6, "x": 185.59, "y": 299.39 }, "pointB": { "flag": 13, "x": 293.67, "y": 298.72 }},
scale描述了贴纸的缩放比例,这里的具体点逻辑与position相同,但这里会有两个点,这两个点的间距就可以确定实际的距离比例。(scale一般取双眼的点)
"begin":“sticker1.complete”
begin描述了贴纸的触发时机,某个动画可能会由另一动画的结束为开始时机,这里的参数决定了贴纸的顺序触发功能。2.三维贴纸变换即使有了人脸识别的关键点,人脸贴图的实现也并非易事。因为它并不是简单的二维粘贴,而是包含欧拉角【注释2】的三维变换(人脸的远近旋转都会影响贴纸形态)。
先从坐标系说起。这个项目涉及的坐标系共有五个:
  • Vision坐标系
  • 模型坐标系
  • 世界坐标系
  • 摄像机坐标系
  • 齐次坐标系
我们通过Vision得到的人脸特征点【注释3】属于Vision坐标系,这个坐标系的原点在左下,x,y的范围为[0, imageSize],imageSize为输入图像的像素大小。而下面的四个坐标系则代表了OpenGL三维变换的四个空间(Model,World,Camera,Homogeneou s),这些坐标系都是三维的。
  • 模型坐标系,代表模型本身的坐标系,原点在模型的中心点
  • 世界坐标系,代表整个固定空间的坐标系,这个坐标系下,模型的平移才有了意义(模型坐标系下平移并不会引起坐标变化)
  • 摄像机坐标系,代表有了观测点的坐标系,此时我们同时拥有了摄像机和模型的世界坐标,模型此时的坐标全部转化为了摄像机为原点的坐标。
  • ndc坐标系,此时视椎体【注释4】形成,位于视椎体之外的部分将被剪裁,模型进行了投影变换,产生了近大远小的投影效果。
    三维变换在引入了齐次坐标【注释5】后,都可以在数学上划归为4*4的矩阵相乘。
Vision坐标系与模型坐标系的转化非常简单,只是简单的加减这里暂且不提。剩下的四个空间转换则对应了OpenGL经典的MVP转换矩阵【注释6】,来看一下项目内使用的三矩阵。(项目的模型指的是人脸图像模型,贴纸是在人脸图像模型上进行相对偏移的)
_modelViewMatrix = GLKMatrix4Identity;_viewMatrix = GLKMatrix4MakeLookAt(0, 0, 6, 0, 0, 0, 0, 1, 0);float ratio = outputFramebuffer.size.width/outputFramebuffer.size.height;_projectionMatrix = GLKMatrix4MakeFrustum(-ratio, ratio, -1, 1, 3, 9);
Model矩阵采用了单元矩阵【注释7】,代表世界坐标系与模型坐标系原点相同。View矩阵的参数代表我们的摄像机处在世界坐标系的(0, 0 , 6),望向(0, 0 , 0),且摄影机的正向朝向y轴正方向(这里可能难以理解,但实际想象一下摄像机是个球,有了位置和望向,自己还是能旋转,看到的图形也可能上下倒置,所以需要确定头方向)Projection矩阵【注释8】的参数代表我们的视椎体近平面左右ratio,上下1,近平面距离3,远平面距离9(注意此时的坐标已经不是世界坐标了,转换成世界坐标近平面z是3,而远平面z是-3,模型的z在0)将模型坐标乘上mvp矩阵,我们就可以得到一个ndc坐标系下的人脸坐标锚点,下面可以开始绘制贴纸了。由于贴纸的大小受到远近影响,所以第一步,求出归一化模型坐标系下贴纸的长宽,贴纸的中心点。
x = x * imageWidth / imageHeight * 2 * ProjectionScale ;y = y * imageHeight / imageHeight * 2 * ProjectionScale;//x,y范围[-1, 1],ProjectionScale代表这贴纸跟近平面的坐标比例为2
除以imageHeight * 2 * ProjectionScale的原因是投影矩阵将近平面上下设为[-1, 1],将左右设置为了宽高比,而我们的图像平面处在摄像机坐标系的-6位置,近平面在-3(摄像机坐标系视角朝向z轴负方向),需要换算。
投影矩阵的具体计算公式如下:
具体的推导过程有兴趣的可以上网查询一下,这里面l代表左,r代表右,t代表上,b代表下,n代表近,f代表远。w最后的结果是-z,经过投影除法(x/w,y/w)以后近平面的距离会比我们模型位置小2倍,所以实际的单位1是imageHeight/2/2,x和y原来的单位不统一,代表着宽高/2,所以需要转换,如上式。拿到坐标以后,求出贴纸的8个顶点(2维),自己补上z坐标和w(z为0,因为贴纸与图像模型同一远近,这里的z代表的是模型坐标系下,它还没乘mvp矩阵,w为1这个恒定的),在顶点着色器里乘上平移旋转矩阵(为了做欧拉角)和mvp矩阵,绘制即可。
3.人脸识别点平滑另一个令人困扰的问题在于Vision识别点会在视频中会有小幅度抖动。
但那些付费的SDK都十分稳定。如果这样使用的话,贴纸也会跟着识别点抖来抖去,怎么解决这个问题呢。
由于Vision封装好的人脸识别模块,我们想要通过AI解决抖动的问题代价太大,基本不可能。但是观察目前的市面上的付费SDK,发现有些是在识别特征点生成后处理平滑问题的。那么证明我们对这个问题也有入手之处。先寻找一下有没有成熟的解决方案。目前开源的人脸算法和解决方案都不多,找过一遍也只找到智云的算法,这个项目里并没有解决防抖的问题。但人脸识别点抖动实际可以抽象成识别的误差抖动,在人脸领域之外,也有很多的相关资料,我去查询了传感器的平滑算法,传感器的预估算法等。总结一下找到的算法如下:卡尔曼滤波,一阶低通滤波,Savitzky-Golay算法等。卡尔曼滤波假设对于一个实际值,观测值会产生无规律的噪音偏差,而这些偏差,在分布上符合正态分布。由此基础推出了一个迭代公式。我找到了一个开源算法接入尝试了一下,实际效果并不算好,而且由于知识储备不足,这个算法的调优异常困难,所以放弃了这个方案。Savitzky-Golay算法更倾向于数据组已有后的平滑,需要前置窗口和后置窗口,对于实时预测的观测点平滑效果也较差,尝试后选择了放弃。
最后采用的是一种动态修改滤波系数的一阶低通滤波形式。一阶滤波公式为:Y(n)=αX(n)+(1−α)Y(n−1)
式中:α 为滤波系数,X ( n ) 为本次采样值,Y ( n − 1 )为上次滤波输出值,Y ( n )为本次滤波输出值。
改进方式:
  • 当数据快速变化时,滤波结果能及时跟进,并且数据的变化越快,灵敏度应该越高(灵敏度优先原则)
  • 当数据趋于稳定,并在一个范围内振荡时,滤波结果能趋于平稳(平稳度优先原则)
  • 当数据稳定后,滤波结果能逼近并最终等于采样数据(消除因计算中小数带来的误差)
调整前判断:
  • 数据变化方向是否为同一个方向(如当连续两次的采样值都比其上次滤波结果大时,视为变化方向一致,否则视为不一致)
  • 数据变化是否较快(主要是判断采样值和上一次滤波结果之间的差值)
调整原则:
  • 当两次数据变化不一致时,说明有抖动,将滤波系数清零,忽略本次新采样值
  • 当数据持续向一个方向变化时,逐渐提高滤波系数,提供本次采样值得权;
  • 当数据变化较快(差值>消抖计数加速反应阈值)时,要加速提高滤波系数
流程图
经过了仔细的参数调整使其符合我们的项目,最后效果如下:
稳定性和跟随性上都有了较好的体验,实际在贴纸使用上完全足够,基本不会出现抖动。
4.欧拉角的探索对于人脸欧拉角的问题,本身vision是提供了相关的参数的。但是目前只包含了roll和yaw,pitch直到iOS15才会给出。并且Vision在训练上可能有些问题,这些欧拉角本身都不够平滑。
为了追求更好的体验,也去寻找了相关的方案。
欧拉角的估计实际上不属于人脸识别,而属于一个单独的hand pose预估,这个问题也有解决方案。PNP算法是通用的处理头部估计的算法关于PNP问题就是指通过世界中的N个特征点与图像成像中的N个像点,计算出其投影关系,从而获得相机或物体的位姿的问题。
opencv提供的solvepnp函数就是用来解决pnp问题。利用该函数可以实现测算相机/物体的空间姿态,也可以用来空间定位。也是照旧的学习,参照开源,接入流程。这里直接给出测试结果,想要详细讨论的伙伴可以联系我一起研究。PNP算法对于头部估计的效果不错(手动去除噪声),但是有个很大的缺点,在头偏转超过45度后,预估错误的几率会很大。可能这块还是前置处理(AI部分)的效果最好。
目前我们处理的方式,对yaw进行了平滑处理,而对于平常使用最多的roll,采用了玉强老师提出的两眼连线与水平面算夹角的方式,roll角度现在的跟随性十分可靠。看一下最后的效果。

04

项目优势

  • 体积小巧,iOS基本无增加。由于采用了iOS原生的Vision库,在人脸模块上无需另外增加引擎,也不用新引入model训练集来支持AI识别。
  • 性能优良,CPU增量单核5%-20%,内存增量10-20M,高端机使用无压力,低端设备在本身场景性能消耗不高的情况下使用也完全可以。
  • 使用方便,支持流式输入输出,支持直播场景。且我们在内部做了贴纸json的转换,使得此项目支持了现有的贴纸生成工具,无缝对接原有业务。
  • 支持拆分,在实现上特意将有可能替换的Json转换,人脸引擎分离实现。替换人脸识别库,替换贴纸格式的改动都很小。只需要实现一个方法就可以使用内部的视频处理,流式输入输出,人脸稳定算法等模块。
  • 可扩展性,由于采用GPUImage架构,所以完美支持滤镜链的添加删除,扩展工作和自定义功能都十分方便。

05

注释

1.解决这个问题可以通过自定义context,buffer,cache,然后在每次初始化滤镜的时候传进去自定义的来解决,但是工作量很大,相当麻烦,项目里将人脸滤镜的这部分抽离了,不过如果有额外滤镜,或者开启自采集的话,这部分只能等释放以后purgeAllUnassignedFramebuff ers解决(这个在SDK释放后已经处理了),而且这样也不会完全释放,实测还有两个占用很小的buffercache。2.欧拉角
用一句话说,欧拉角就是物体绕坐标系三个坐标轴(x,y,z轴)的旋转角度,放两张图。

3.人脸特征点
人脸识别其实给出的统一格式是特征点,Vision可以参考这个可以观察到在脸廓,眼等部分,这些特征点是均分的,所以不同点数的人脸识别库,很多点指的都不是统一部位,无法进行互相转换。4.视椎体(Frustum)

指的就是观测者看的视界形成的平截头体,上面左侧的面叫近平面,右侧是远平面。5.齐次坐标
指在坐标加一维w,转回笛卡尔坐标系只需要前几个维度除以w即可,这个w大多数情况为1,所以前几维度都保持不变。
引入齐次坐标可以使得原来笛卡尔坐标系的矩阵加法也可以用齐次坐标的矩阵乘法表示。使得任何坐标的转换可以表示为乘以一个矩阵。6.MVP转换矩阵
经典图示

一个模型坐标系的坐标乘以这三个矩阵,即可得到一个齐次坐标系的坐标。7.单元矩阵
即矩阵中左上至右下对角线为1,其余都为0的矩阵,这个矩阵乘以任何矩阵都不变,相当于自然数的1。8.Projection矩阵
透视矩阵,即mvp矩阵中的p,经过这个矩阵后,物体的形状会发生变化,上面视椎体的物体会变形如下

正交情况下仍然会显示出近大远小的投影效果。


扫描下方二维码添加「好未来技术」微信官方账号
进入好未来技术官方交流群与作者实时互动~
(若扫码无效,可通过微信号TAL-111111直接添加)
- 也许你还想看 -
核心技术|Apache Flink CDC 批流融合技术原理分析
全域数据用户行为拉通理论与实践
消息队列利器—MQProxy架构设计

我知道你“在看”哟~


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存