在相機應用中,實時貼紙、實時瘦臉是比較常見的功能,它們的實現基礎是人臉關鍵點檢測。本文主要介紹,如何在 GPUImage 中檢測人臉關鍵點。ios
咱們要經過某一種方式,獲取視頻中每一幀的人臉關鍵點,而後經過 OpenGL ES 將關鍵點繪製到屏幕上。最終呈現效果以下:git
這裏分爲兩個步驟:關鍵點獲取、關鍵點繪製。github
在蘋果自帶的 SDK 中,已經包含了一部分的人臉識別功能。好比在 CoreImage、AVFoundation 中,就提供了相關的接口。可是,它們提供的接口功能有限,並不具有人臉關鍵點檢測功能。算法
咱們要在視頻中進行實時的人臉關鍵點檢測,還須要藉助第三方的庫。這裏主要介紹兩種方式:bash
Face++ 的人臉關鍵點 SDK 是收費的,可是它也提供免費試用的版本。markdown
在免費試用的版本中,試用的 API Key 天天能夠發起 5 次聯網受權,每次受權的時長爲 24 小時。也就是說,在不刪除 APP 的狀況下,只要測試設備不超過 5 臺,就能夠一直使用下去。網絡
這對於開發者來講仍是很是友好的,並且 Face++ 的註冊集成也比較簡單,建議你們都嘗試一下。async
人臉關鍵點 SDK 的集成能夠參照 官方文檔 ,先註冊再下載 SDK 壓縮包,壓縮包裏有詳細的集成步驟。ide
人臉關鍵點 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; } } 複製代碼
OpenCV 是一個開源的跨平臺計算機視覺庫,實現了圖像處理方面的不少通用算法。Stasm 是用於檢測人臉特徵的開源算法庫,依賴於 OpenCV 。
咱們知道,iPhone 屏幕的刷新頻率能夠達到 60 幀每秒。在相機預覽時,出於功耗方面的考慮,通常會將幀率限制到 30 幀每秒左右,且不會引發明顯的卡頓。
因此,咱們要對每一幀數據進行識別,則要求每一幀的識別時間要小於 1 / 30 秒,不然圖像數據的渲染操做就要等待識別結果,從而致使幀率降低,引發卡頓。
遺憾的是,採用 OpenCV + Stasm 的方式,每一幀的識別時間是超過 1 / 30 秒的。它或許更適合用來作靜態圖片的識別。
因此也更推薦使用 Face++ 的方式。
OpenCV 經過 CocoPods 的方式來引入:
pod 'OpenCV2-contrib' 複製代碼
OpenCV2-contrib 相比於 OpenCV2 多包含了一些拓展包,好比 face 模塊,而 Stasm 算法庫須要依賴 face 模塊。
Stasm 算法庫能夠從 這個地址 下載,須要將 stasm 和 haarcascades 文件夾都加入工程中。
人臉關鍵點的識別主要經過調用 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); } } 複製代碼
最後,只須要將這個濾鏡加入到濾鏡鏈裏,就能夠看到人臉關鍵點的繪製效果了。
請到 GitHub 上查看完整代碼。
獲取更佳的閱讀體驗,請訪問原文地址 【Lyman's Blog】在 GPUImage 中檢測人臉關鍵點