打开轻颜相机,调整摄像头,使用前置摄像头拍摄,点击“美颜”。
底部弹窗中可以看到磨皮、美白和瘦脸等多个设置项,点击其中的“瘦脸”,“瘦脸”分类下可以看到自然、女神、长脸专属和圆脸专属,每一种分类的效果都可以通过上面对应的图例展示,其中虚线为原图,实线为效果图。据此,再结合自身情况选择即可。
本文是讲解特效相机中的大眼瘦脸的实现,完整源码可查看 AwemeLike 。
要实现瘦脸大眼,首先需要获取到人脸特征点,在本项目中使用的是Face++的人脸识别库,它可以获取到106个人脸特征点,接着再通过变形算法就可以实现了。
项目使用的瘦脸算法是参照这篇文章 在OpenGL中利用shader进行实时瘦脸大眼等脸型微调
textureCoord 表示当前要修改的坐标, originPosition 表示圆心坐标, targetPosition 表示目标坐标, delta 用来控制变形强度。
上述shader方法可以这样理解,首先确定一个以 originPosition 为圆心、 targetPosition 和 originPosition 之间的距离为半径的圆,然后将圆内的像素朝着同一个方向移动一个偏移值,且偏移值在距离圆心越近时越大,最终将变换后的坐标返回。
如果将方法简化为这样的表达式 变换后的坐标 = 原坐标 - (目标坐标 - 圆心坐标) 变形强度 ,也就是说,方法的作用就是要在原坐标的基础上减去一个偏移值,而 (targetPosition - originPosition) 决定了移动的方向和最大值。
式子中的 变形强度 可以这样表示:
除了项目中使用的算法,还有另外两种脸部变形算法可以使用,一个是基于 Interactive Image Warping 的局部调整算法(其原理可查看 文章 ),我们在项目中使用的算法其实可以看做是它的一个变种,都可以用表达式 变换后的坐标 = 原坐标 - (目标坐标 - 圆心坐标) 变形强度 来表示,不同之处在于 变形强度 的取值不同。;另一个是基于 Image deformation using moving least squares 的全局点位变形算法(其原理可查看 文章 )。
当我们要使用上述瘦脸算法时,只需要选取多对特征点作为 originPosition 和 targetPosition ,使得它们作用范围覆盖的两个脸颊和下巴,然后通过改变 delta 来控制瘦脸的强度。
在Face++中,获取的106个特征点分布如下
将这106个特征点上传到片元着色器
设置统一的变形强度
指定圆心坐标和目标坐标,共有9对
大眼算法也是参照这篇文章 在OpenGL中利用shader进行实时瘦脸大眼等脸型微调
textureCoord 表示当前要修改的坐标, originPosition 表示圆心坐标, radius 表示圆的半径, delta 用来控制变形强度。
和瘦脸的算法类似,根据 originPosition 和 targetPosition 确定一个圆,圆内的坐标会参与计算,圆外的不变。
圆内的坐标围绕圆心 originPosition 在变化,最终的坐标完全是由 weight 的值决定, weight 越大,最终的坐标变化越小,当 weight 为1,即坐标处于圆边界或圆外时,最终的坐标不变;当 weight 小于1时,最终的坐标会落在原坐标和圆点之间,也就是说最终返回的像素点比原像素点距离圆点更近,这样就产生了以圆点为中心的放大效果。
项目中的 FaceDetector 是一个专门用来处理Face++相关的操作的类,其头文件如下
使用前需要替换Face++的key和secret,在项目中,它的路径是 Face++/MGNetAccounth ,然后调用授权方法,授权成功之后才能使用face++的人脸检测。
face++接受的是 CMSampleBufferRef 类型的视频帧,但他不支持YUV格式,所以在解码时需要选择BGRA格式。
项目使用 GPUImage 来做解码,但 GPUImage 库在将视频帧解码为BGRA格式时有一些实现问题,所以我们在使用相机和读取视频文件时,一定要使用项目自己创建的 GPUImageFaceCamera 和 GPUImageFaceMovie ,它们分别继承自 GPUImage 的 GPUImageVideoCamera 和 GPUImageMovie ,在内部重写了一些配置方法,使得返回的视频帧格式都是BGRA。
除此以外,这两个类在获取到视频帧之后还会自行调用 FaceDetector 的 - (void)getLandmarksFromSampleBuffer:(CMSampleBufferRef)detectSampleBufferRef; 方法获取人脸信息,使得我们可以在之后的滤镜类中直接使用它解析出来的人脸数据了。
人脸方向是指人脸在视频帧中的逆时针偏移角度,偏移角度为0表示人脸是正的,处于竖直方向。
如果给我们一张人脸的,我们的肉眼很容易判断出人脸在中的偏移角度,但是传递给face++的是一个来自相机或视频文件视频帧,那么我们应该如何获取人脸的偏移角度呢。
1 相机拍摄( GPUImageFaceCamera )
当使用相机拍摄时,相机产生的视频帧默认情况下和我们看到的并不一样,可以通过 AVCaptureConnection 类的属性 videoOrientation 来指定视频帧的方向,其取值有如下几种
上面提到的原图就是我们肉眼看到的场景,它和相机或视频文件产生的视频帧可能是不一样的。
因为项目是通过 GPUImage 库来调用相机的,而 GPUImage 并没有设置这个属性——它使用的是默认值,所以项目使用的是这个属性的默认值。(使用默认值可能是性能原因,因为设置这个属性会导致系统应用一个相应matrix来旋转视频帧,GPUImage选择将旋转操作放到了GPU中执行)
后置摄像头
在使用后置摄像头时, videoOrientation 属性的默认值是 AVCaptureVideoOrientationLandscapeRight ,也就是说当手机水平方向放置,且home键在右边时,相机产生的和原图一致。
根据这个,我们可以得出下面两个变换图,左边的代表原图(实际场景),右边代表相机产生的视频帧,也是传递给face++的视频帧,每一行代表一个放置手机的方向。
我们需要根据上面两个图来得到人脸的偏移角度
首先我们需要设置了一个前提,人脸在原图中总是处于竖直方向的,即偏移角度为0,这其实是符合逻辑的,我们的脑袋不可能歪到大于90度。
然后我们可以将上图中的3和4看做是人脸,计算出右图中的3和4的旋转角度,就可以得出在当前手机方向下人脸的偏移角度。
最终我们得出了下面这个对照表
前置摄像头
使用前置摄像头时, videoOrientation 的默认值为 AVCaptureVideoOrientationLandscapeLeft ,同样的,当手机水平方向放置进行拍摄,home键在左边时,相机产生的和原图是水平镜像的关系,所以还需要额外做一个水平方向的翻转,这时的才和原图一样。
同样给出手机处于竖直方向或水平方向拍摄时,原图和视频帧的变换
手机方向和人脸偏移角度的对照表
相比后置摄像头,使用前置摄像头时需要多做一个额外的水平翻转,由于face++并没有提供设置水平翻转的接口,所以在识别前置摄像头产生的时,face++返回的人脸数据有一些小问题——人脸特征点的排列顺序左右颠倒了,不过这个问题是可以忽略的,因为人脸两边是对齐的;还有一个问题就是欧拉角的方向会取反,这个我们后面会讲到怎么解决。
检测手机的方向
由于App只支持 Portrait 方向,所以无法使用类似 - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration 方法来获取手机方向。
一个更好的方法是通过 CoreMotion 检测xyz方向的加速度,以此来判断当前手机的朝向
2 从视频文件获取( GPUImageFaceMovie )
通过 AVAssetTrack 的 preferredTransform 属性获得变换矩阵,再通过矩阵来判断视频的旋转角度,也即是人脸的旋转角度。
face++返回的人脸数据会在哪里被使用?
GPUImage 会将视频帧上传到纹理中,然后将纹理传递给后续的 targets , targets 是指那些遵守了 GPUImageInput 协议的类,在这里我们简称它们为滤镜类。
face++返回的人脸数据只会被使用在这些滤镜类中,这些滤镜类中的纹理和传递给face++做人脸检测的视频帧是不一样的,也就是说生成人脸数据时的参考坐标系和使用人脸数据时的参考坐标系是不同的。所以在使用人脸数据之前,我们还需要对人脸数据做一些转换操作。
问题是如何转换
之前在设置人脸方向时,我们已经了解了视频帧,那么滤镜类中的纹理又是什么样的,可以参考下面这张图
第一行使用后置摄像头,第二行使用前置摄像头;
第一列和第二列分别表示原图和相机产生的视频帧;
第三列表示视频帧上传到纹理时的情形,因为OpenGL的原点是在左下角,所以需要上下颠倒;
第四列表示变换后的纹理,也就是滤镜类中的纹理。
( GPUImage 是如何从上图的第三列变换到第四列的,查看 GPUImageVideoCamera 的方法 updateOrientationSendToTargets 可知,按照本项目对 GPUImageVideoCamera 的配置( outputImageOrientation=UIInterfaceOrientationPortrait, _horizontallyMirrorFrontFacingCamera=true ,前置),当使用后置摄像头时,用来指定旋转的枚举是 kGPUImageRotateRight ,当使用前置摄像头时,用来指定旋转的枚举是 kGPUImageRotateRightFlipVertical ,这两个枚举的名字刚好是反向变换——第四列变换到第三列所需要的步骤)
特征点坐标
假设 point 表示使用后置摄像头时Face++的特征点坐标,对应上图的第一行的视频帧,它是以4号位作为原点,也就是说 point 的值是相对于4号位的,然后我们再看第一行的最终纹理,原点是3号位,4号位变换到右下角了,我们需要做的是将 point 变换到以3号位为原点。
所以变换后的 point 应该等于 (height - pointy, pointx) ,其中width是第二列视频帧的4号位和2号位所在的那条边的长度,height是视频帧的4号位和3号位所在的那条边的长度。
特征点变换规则如下
欧拉角
faceinfo 的三个属性 pitch 、 yaw 、 roll 分别表示人脸在未变换的视频帧中围绕x、y、z轴的旋转角度。
欢迎分享,转载请注明来源:浪漫分享网
评论列表(0条)