53

在 GPUImage 中检测人脸关键点

 4 years ago
source link: https://www.tuicool.com/articles/QR7nI3J
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

3MZRNvZ.jpg!web

在相机应用中,实时贴纸、实时瘦脸是比较常见的功能,它们的实现基础是人脸关键点检测。本文主要介绍,如何在 GPUImage 中检测人脸关键点。

前言

我们要通过某一种方式,获取视频中每一帧的人脸关键点,然后通过 OpenGL ES 将关键点绘制到屏幕上。最终呈现效果如下:

JRFV7bI.gif

这里分为两个步骤: 关键点获取关键点绘制

一、关键点获取

在苹果自带的 SDK 中,已经包含了一部分的人脸识别功能。比如在 CoreImage、AVFoundation 中,就提供了相关的接口。但是,它们提供的接口功能有限,并不具备人脸关键点检测功能。

我们要在视频中进行实时的人脸关键点检测,还需要借助第三方的库。这里主要介绍两种方式:

  1. Face++
  2. OpenCV + Stasm

1、Face++

1、简介

Face++ 的人脸关键点 SDK 是收费的,但是它也提供免费试用的版本。

在免费试用的版本中,试用的 API Key 每天可以发起 5 次联网授权,每次授权的时长为 24 小时。也就是说,在不删除 APP 的情况下,只要测试设备不超过 5 台,就可以一直使用下去。

这对于开发者来说还是非常友好的,而且 Face++ 的注册集成也比较简单,建议大家都尝试一下。

2、如何集成

人脸关键点 SDK 的集成可以参照 官方文档 ,先注册再下载 SDK 压缩包,压缩包里有详细的集成步骤。

3、如何使用

人脸关键点 SDK 的使用主要分为三步:

第一步:发起联网授权

授权的操作不一定发起网络请求,而是会先检查本地的授权信息是否过期,过期了才会发起网络请求。

@weakify(self);
[MGFaceLicenseHandle licenseForNetwokrFinish:^(bool License, NSDate *sdkDate) {
@strongify(self);
dispatch_async(dispatch_get_main_queue(), ^{
if (License) {
[[UIApplication sharedApplication].keyWindow makeToast:@"Face++ 授权成功!"];
[self setupFacepp];
} else {
[[UIApplication sharedApplication].keyWindow makeToast:@"Face++ 授权失败!"];
}
});
}];

第二步:初始化人脸检测器

授权成功后,开始人脸检测器的初始化。初始化过程会进行模型数据加载,然后对识别模式、视频流格式、视频旋转角度等进行设置。

NSString *modelPath = [[NSBundle mainBundle] pathForResource:KMGFACEMODELNAME
ofType:@""];
NSData *modelData = [NSData dataWithContentsOfFile:modelPath];
self.markManager = [[MGFacepp alloc] initWithModel:modelData
faceppSetting:^(MGFaceppConfig *config) {
config.detectionMode = MGFppDetectionModeTrackingRobust;
config.pixelFormatType = PixelFormatTypeNV21;
config.orientation = 90;
}];

第三步:检测视频帧

人脸检测器初始化成功后,可以对视频流每一帧进行检测,这里传入的是 CMSampleBufferRef 类型的数据。由于顶点坐标的范围是 -1 ~ 1 ,所以还需要根据当前的视频尺寸比例,对识别的结果进行坐标转换。

- (float *)detectInFaceppWithSampleBuffer:(CMSampleBufferRef)sampleBuffer
facePointCount:(int *)facePointCount
isMirror:(BOOL)isMirror {
if (!self.markManager) {
return nil;
}

MGImageData *imageData = [[MGImageData alloc] initWithSampleBuffer:sampleBuffer];
[self.markManager beginDetectionFrame];
NSArray *faceArray = [self.markManager detectWithImageData:imageData];

// 人脸个数
NSInteger faceCount = [faceArray count];

int singleFaceLen = 2 * kFaceppPointCount;
int len = singleFaceLen * (int)faceCount;
float *landmarks = (float *)malloc(len * sizeof(float));

for (MGFaceInfo *faceInfo in faceArray) {
NSInteger faceIndex = [faceArray indexOfObject:faceInfo];
[self.markManager GetGetLandmark:faceInfo isSmooth:YES pointsNumber:kFaceppPointCount];
[faceInfo.points enumerateObjectsUsingBlock:^(NSValue *value, NSUInteger idx, BOOL *stop) {
float x = (value.CGPointValue.y - self.sampleBufferLeftOffset) / self.videoSize.width;
x = (isMirror ? x : (1 - x)) * 2 - 1;
float y = (value.CGPointValue.x - self.sampleBufferTopOffset) / self.videoSize.height * 2 - 1;
landmarks[singleFaceLen * faceIndex + idx * 2] = x;
landmarks[singleFaceLen * faceIndex + idx * 2 + 1] = y;
}];
}
[self.markManager endDetectionFrame];

if (faceArray.count) {
*facePointCount = kFaceppPointCount * (int)faceCount;
return landmarks;
} else {
free(landmarks);
return nil;
}
}

2、OpenCV + Stasm

1、简介

OpenCV 是一个开源的跨平台计算机视觉库,实现了图像处理方面的很多通用算法。Stasm 是用于检测人脸特征的开源算法库,依赖于 OpenCV 。

我们知道,iPhone 屏幕的刷新频率可以达到 60 帧每秒。在相机预览时,出于功耗方面的考虑,一般会将帧率限制到 30 帧每秒左右,且不会引起明显的卡顿。

所以,我们要对每一帧数据进行识别,则要求每一帧的识别时间要小于 1 / 30 秒,否则图像数据的渲染操作就要等待识别结果,从而导致帧率下降,引起卡顿。

遗憾的是,采用 OpenCV + Stasm 的方式,每一帧的识别时间是超过 1 / 30 秒的。它或许更适合用来做静态图片的识别。

所以也更推荐使用 Face++ 的方式。

2、如何集成

OpenCV 通过 CocoPods 的方式来引入:

pod 'OpenCV2-contrib'

OpenCV2-contrib 相比于 OpenCV2 多包含了一些拓展包,比如 face 模块,而 Stasm 算法库需要依赖 face 模块。

Stasm 算法库可以从 这个地址 下载,需要将 stasm 和 haarcascades 文件夹都加入工程中。

3、如何使用

人脸关键点的识别主要通过调用 stasm_search_single 函数来实现。

由于这个方法的检测时间较长,因此我们在将视频帧数据传入之前,会先做单通道化、尺寸压缩等处理。这样的话, Stasm 拿到的每一帧的数据量会减少,可以有效地缩短检测的时长,但相应地也会损失检测的精度。

关键的代码:

- (float *)detectInOpenCVWithSampleBuffer:(CMSampleBufferRef)sampleBuffer
facePointCount:(int *)facePointCount
isMirror:(BOOL)isMirror {
cv::Mat cvImage = [self grayMatWithSampleBuffer:sampleBuffer];
int resultWidth = 250;
int resultHeight = resultWidth * 1.0 / cvImage.rows * cvImage.cols;
cvImage = [self resizeMat:cvImage toWidth:resultHeight]; // 此时还没旋转,所以传入高度
cvImage = [self correctMat:cvImage isMirror:isMirror];
const char *imgData = (const char *)cvImage.data;

// 是否找到人脸
int foundface;
// stasm_NLANDMARKS 表示人脸关键点数,乘 2 表示要分别存储 x, y
int len = 2 * stasm_NLANDMARKS;
float *landmarks = (float *)malloc(len * sizeof(float));

// 获取宽高
int imgCols = cvImage.cols;
int imgRows = cvImage.rows;

// 训练库的目录,直接传 [NSBundle mainBundle].bundlePath 就可以,会自动找到所有文件
const char *xmlPath = [[NSBundle mainBundle].bundlePath UTF8String];

// 返回 0 表示出错
int stasmActionError = stasm_search_single(&foundface,
landmarks,
imgData,
imgCols,
imgRows,
"",
xmlPath);
// 打印错误信息
if (!stasmActionError) {
printf("Error in stasm_search_single: %s\n", stasm_lasterr());
}

// 释放cv::Mat
cvImage.release();

// 识别到人脸
if (foundface) {
// 转换坐标
for (int index = 0; index < len; ++index) {
if (index % 2 == 0) {
float scale = (self.videoSize.height / self.videoSize.width) / (16.0 / 9.0);
scale = MAX(1, scale); // 比例超过 16 : 9 进行横向缩放
landmarks[index] = (landmarks[index] / imgCols * 2 - 1) * scale;
} else {
float scale = (16.0 / 9.0) / (self.videoSize.height / self.videoSize.width);
scale = MAX(1, scale); // 比例小于 16 : 9 进行纵向缩放
landmarks[index] = (landmarks[index] / imgRows * 2 - 1) * scale;
}
}
*facePointCount = stasm_NLANDMARKS;
return landmarks;
} else {
free(landmarks);
return nil;
}
}

二、关键点绘制

通过上面的步骤,我们已经有了顶点数据,区别只是两种方式的顶点数量不同。

顶点数据的绘制,要在 GPUImageFilter 中进行。我们要自定义一个滤镜,然后在这个滤镜中实现人脸关键点的绘制逻辑。

GPUImageFilter 中,渲染的流程是在 -renderToTextureWithVertices:textureCoordinates: 这个方法里执行的。因此在自定义的滤镜中,我们需要重写这个方法。

在这个方法里,我们需要做两件事情,一是将输入的纹理原封不动地绘制,二是对人脸关键点的绘制。

纹理的绘制使用的是 三角形图元 ,人脸关键点的绘制使用的是 点图元 ,因此我们需要分成两次绘制。在原来的绘制方法中,已经有了纹理的绘制逻辑。所以,我们只需要在纹理绘制结束后,加上人脸关键点的绘制。

完整的重写后的方法:

- (void)renderToTextureWithVertices:(const GLfloat *)vertices
textureCoordinates:(const GLfloat *)textureCoordinates {
if (self.preventRendering)
{
[firstInputFramebuffer unlock];
return;
}

[GPUImageContext setActiveShaderProgram:filterProgram];

outputFramebuffer = [[GPUImageContext sharedFramebufferCache] fetchFramebufferForSize:[self sizeOfFBO] textureOptions:self.outputTextureOptions onlyTexture:NO];
[outputFramebuffer activateFramebuffer];
if (usingNextFrameForImageCapture)
{
[outputFramebuffer lock];
}

[self setUniformsForProgramAtIndex:0];

glClearColor(backgroundColorRed, backgroundColorGreen, backgroundColorBlue, backgroundColorAlpha);
glClear(GL_COLOR_BUFFER_BIT);

glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, [firstInputFramebuffer texture]);

glUniform1i(filterInputTextureUniform, 2);
glUniform1i(self.isPointUniform, 0); // 表示是绘制纹理

glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, vertices);
glVertexAttribPointer(filterTextureCoordinateAttribute, 2, GL_FLOAT, 0, 0, textureCoordinates);

glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

// 绘制点
if (self.facesPoints) {
glUniform1i(self.isPointUniform, 1); // 表示是绘制点
glUniform1f(self.pointSizeUniform, self.sizeOfFBO.width * 0.006); // 设置点的大小
glVertexAttribPointer(filterPositionAttribute, 2, GL_FLOAT, 0, 0, self.facesPoints);
glDrawArrays(GL_POINTS, 0, self.facesPointCount);
}

[firstInputFramebuffer unlock];

if (usingNextFrameForImageCapture)
{
dispatch_semaphore_signal(imageCaptureSemaphore);
}
}

在绘制点图元的时候,可以通过对 gl_PointSize 进行赋值,来指定点的大小。然后在外部通过 uniform 变量传值的方式进行控制。

顶点着色器代码:

precision highp float;

attribute vec4 position;
attribute vec4 inputTextureCoordinate;

varying vec2 textureCoordinate;

uniform float pointSize;

void main()
{
gl_Position = position;
gl_PointSize = pointSize;
textureCoordinate = inputTextureCoordinate.xy;
}

由于两次渲染的逻辑是独立的,所以一般来说,应该使用不同的 Shader 来实现。但由于这里的渲染逻辑比较简单,所以直接将两次渲染的逻辑都放到同一个 Shader 中。这也可以避免 Program 的来回切换,然后用一个 uniform 变量来判断当前的绘制类型。

片段着色器代码:

precision highp float;

varying vec2 textureCoordinate;

uniform sampler2D inputImageTexture;

uniform int isPoint;

void main()
{
if (isPoint != 0) {
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
} else {
gl_FragColor = texture2D(inputImageTexture, textureCoordinate);
}
}

最后,只需要将这个滤镜加入到滤镜链里,就可以看到人脸关键点的绘制效果了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK