
在相機(jī)應(yīng)用中,實時貼紙、實時瘦臉是比較常見的功能,它們的實現(xiàn)基礎(chǔ)是人臉關(guān)鍵點檢測。本文主要介紹,如何在 GPUImage 中檢測人臉關(guān)鍵點。
前言
我們要通過某一種方式,獲取視頻中每一幀的人臉關(guān)鍵點,然后通過 OpenGL ES 將關(guān)鍵點繪制到屏幕上。最終呈現(xiàn)效果如下:
這里分為兩個步驟:關(guān)鍵點獲取、關(guān)鍵點繪制。
一、關(guān)鍵點獲取
在蘋果自帶的 SDK 中,已經(jīng)包含了一部分的人臉識別功能。比如在 CoreImage、AVFoundation 中,就提供了相關(guān)的接口。但是,它們提供的接口功能有限,并不具備人臉關(guān)鍵點檢測功能。
我們要在視頻中進(jìn)行實時的人臉關(guān)鍵點檢測,還需要借助第三方的庫。這里主要介紹兩種方式:
- Face++
- OpenCV + Stasm
1、Face++
1、簡介
Face++ 的人臉關(guān)鍵點 SDK 是收費的,但是它也提供免費試用的版本。
在免費試用的版本中,試用的 API Key 每天可以發(fā)起 5 次聯(lián)網(wǎng)授權(quán),每次授權(quán)的時長為 24 小時。也就是說,在不刪除 APP 的情況下,只要測試設(shè)備不超過 5 臺,就可以一直使用下去。
這對于開發(fā)者來說還是非常友好的,而且 Face++ 的注冊集成也比較簡單,建議大家都嘗試一下。
2、如何集成
人臉關(guān)鍵點 SDK 的集成可以參照 官方文檔 ,先注冊再下載 SDK 壓縮包,壓縮包里有詳細(xì)的集成步驟。
3、如何使用
人臉關(guān)鍵點 SDK 的使用主要分為三步:
第一步:發(fā)起聯(lián)網(wǎng)授權(quán)
授權(quán)的操作不一定發(fā)起網(wǎng)絡(luò)請求,而是會先檢查本地的授權(quán)信息是否過期,過期了才會發(fā)起網(wǎng)絡(luò)請求。
@weakify(self);
[MGFaceLicenseHandle licenseForNetwokrFinish:^(bool License, NSDate *sdkDate) {
@strongify(self);
dispatch_async(dispatch_get_main_queue(), ^{
if (License) {
[[UIApplication sharedApplication].keyWindow makeToast:@"Face++ 授權(quán)成功!"];
[self setupFacepp];
} else {
[[UIApplication sharedApplication].keyWindow makeToast:@"Face++ 授權(quán)失??!"];
}
});
}];
第二步:初始化人臉檢測器
授權(quán)成功后,開始人臉檢測器的初始化。初始化過程會進(jìn)行模型數(shù)據(jù)加載,然后對識別模式、視頻流格式、視頻旋轉(zhuǎn)角度等進(jìn)行設(shè)置。
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;
}];
第三步:檢測視頻幀
人臉檢測器初始化成功后,可以對視頻流每一幀進(jìn)行檢測,這里傳入的是 CMSampleBufferRef 類型的數(shù)據(jù)。由于頂點坐標(biāo)的范圍是 -1 ~ 1,所以還需要根據(jù)當(dāng)前的視頻尺寸比例,對識別的結(jié)果進(jìn)行坐標(biāo)轉(zhuǎn)換。
- (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];
// 人臉個數(shù)
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 是一個開源的跨平臺計算機(jī)視覺庫,實現(xiàn)了圖像處理方面的很多通用算法。Stasm 是用于檢測人臉特征的開源算法庫,依賴于 OpenCV 。
我們知道,iPhone 屏幕的刷新頻率可以達(dá)到 60 幀每秒。在相機(jī)預(yù)覽時,出于功耗方面的考慮,一般會將幀率限制到 30 幀每秒左右,且不會引起明顯的卡頓。
所以,我們要對每一幀數(shù)據(jù)進(jìn)行識別,則要求每一幀的識別時間要小于 1 / 30 秒,否則圖像數(shù)據(jù)的渲染操作就要等待識別結(jié)果,從而導(dǎo)致幀率下降,引起卡頓。
遺憾的是,采用 OpenCV + Stasm 的方式,每一幀的識別時間是超過 1 / 30 秒的。它或許更適合用來做靜態(tài)圖片的識別。
所以也更推薦使用 Face++ 的方式。
2、如何集成
OpenCV 通過 CocoPods 的方式來引入:
pod 'OpenCV2-contrib'
OpenCV2-contrib 相比于 OpenCV2 多包含了一些拓展包,比如 face 模塊,而 Stasm 算法庫需要依賴 face 模塊。
Stasm 算法庫可以從 這個地址 下載,需要將 stasm 和 haarcascades 文件夾都加入工程中。
3、如何使用
人臉關(guān)鍵點的識別主要通過調(diào)用 stasm_search_single 函數(shù)來實現(xiàn)。
由于這個方法的檢測時間較長,因此我們在將視頻幀數(shù)據(jù)傳入之前,會先做單通道化、尺寸壓縮等處理。這樣的話, Stasm 拿到的每一幀的數(shù)據(jù)量會減少,可以有效地縮短檢測的時長,但相應(yīng)地也會損失檢測的精度。
關(guān)鍵的代碼:
- (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]; // 此時還沒旋轉(zhuǎn),所以傳入高度
cvImage = [self correctMat:cvImage isMirror:isMirror];
const char *imgData = (const char *)cvImage.data;
// 是否找到人臉
int foundface;
// stasm_NLANDMARKS 表示人臉關(guān)鍵點數(shù),乘 2 表示要分別存儲 x, y
int len = 2 * stasm_NLANDMARKS;
float *landmarks = (float *)malloc(len * sizeof(float));
// 獲取寬高
int imgCols = cvImage.cols;
int imgRows = cvImage.rows;
// 訓(xùn)練庫的目錄,直接傳 [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) {
// 轉(zhuǎn)換坐標(biāo)
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 進(jìn)行橫向縮放
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 進(jìn)行縱向縮放
landmarks[index] = (landmarks[index] / imgRows * 2 - 1) * scale;
}
}
*facePointCount = stasm_NLANDMARKS;
return landmarks;
} else {
free(landmarks);
return nil;
}
}
二、關(guān)鍵點繪制
通過上面的步驟,我們已經(jīng)有了頂點數(shù)據(jù),區(qū)別只是兩種方式的頂點數(shù)量不同。
頂點數(shù)據(jù)的繪制,要在 GPUImageFilter 中進(jìn)行。我們要自定義一個濾鏡,然后在這個濾鏡中實現(xiàn)人臉關(guān)鍵點的繪制邏輯。
在 GPUImageFilter 中,渲染的流程是在 -renderToTextureWithVertices:textureCoordinates: 這個方法里執(zhí)行的。因此在自定義的濾鏡中,我們需要重寫這個方法。
在這個方法里,我們需要做兩件事情,一是將輸入的紋理原封不動地繪制,二是對人臉關(guān)鍵點的繪制。
紋理的繪制使用的是三角形圖元,人臉關(guān)鍵點的繪制使用的是點圖元,因此我們需要分成兩次繪制。在原來的繪制方法中,已經(jīng)有了紋理的繪制邏輯。所以,我們只需要在紋理繪制結(jié)束后,加上人臉關(guān)鍵點的繪制。
完整的重寫后的方法:
- (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); // 設(shè)置點的大小
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 進(jìn)行賦值,來指定點的大小。然后在外部通過 uniform 變量傳值的方式進(jìn)行控制。
頂點著色器代碼:
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;
}
由于兩次渲染的邏輯是獨立的,所以一般來說,應(yīng)該使用不同的 Shader 來實現(xiàn)。但由于這里的渲染邏輯比較簡單,所以直接將兩次渲染的邏輯都放到同一個 Shader 中。這也可以避免 Program 的來回切換,然后用一個 uniform 變量來判斷當(dāng)前的繪制類型。
片段著色器代碼:
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);
}
}
最后,只需要將這個濾鏡加入到濾鏡鏈里,就可以看到人臉關(guān)鍵點的繪制效果了。
源碼
請到 GitHub 上查看完整代碼。
參考
獲取更佳的閱讀體驗,請訪問原文地址 【Lyman's Blog】在 GPUImage 中檢測人臉關(guān)鍵點