之前公司項目需要,研究了一下人臉識別和活體識別,並運用免費的訊飛人臉識別,在其基礎上做了二次開發,添加了活體識別。項目需要就開發了張嘴和搖頭兩個活體動作的識別。 這里簡單介紹一下當時的開發思路和一些個人拙見,歡迎大神指點。
首先說一下訊飛第三方的人臉識別的幾個缺點:1.識別不穩定,各點坐標跳動偏差比較大,不容易捕捉;2.CPU使用率比較高,連續識別一會兒手機會明顯發燙,手機配置低的,就會反應很慢,本人使用的iPhone 6s,配置還可以,還算比較流暢,但也會發燙。3.屏幕小的手機識別率相對會低一點,當然這也和手機的配置脫不了干系。
下面開始我們的活體識別開發之路:
確定位置
訊飛的人臉識別坐標跳動比較大,如果全屏識別發現很容易出現錯誤的識別,導致識別錯誤的被通過,所以為了降低這個可能性,特意加了臉部位置的限制,把識別位置和范圍大大縮小,大大提高了識別精度和成功率。
原版的Demo里給出了人臉框的坐標,也顯示出了人臉的框,代碼如下:
-(void)drawPointWithPoints:(NSArray *)arrPersons
{
if (context) {
CGContextClearRect(context, self.bounds) ;
}
context = UIGraphicsGetCurrentContext();
for (NSDictionary *dicPerson in arrPersons) {
if ([dicPerson objectForKey:POINTS_KEY]) {
for (NSString *strPoints in [dicPerson objectForKey:POINTS_KEY]) {
CGPoint p = CGPointFromString(strPoints);
CGContextAddEllipseInRect(context, CGRectMake(p.x - 1 , p.y - 1 , 2 , 2));
}
}
BOOL isOriRect=NO;
if ([dicPerson objectForKey:RECT_ORI]) {
isOriRect=[[dicPerson objectForKey:RECT_ORI] boolValue];
}
if ([dicPerson objectForKey:RECT_KEY]) {
CGRect rect=CGRectFromString([dicPerson objectForKey:RECT_KEY]);
if(isOriRect){//完整矩形
CGContextAddRect(context,rect) ;
}
else{ //只畫四角
// 左上
CGContextMoveToPoint(context, rect.origin.x, rect.origin.y+rect.size.height/8);
CGContextAddLineToPoint(context, rect.origin.x, rect.origin.y);
CGContextAddLineToPoint(context, rect.origin.x+rect.size.width/8, rect.origin.y);
//右上
CGContextMoveToPoint(context, rect.origin.x+rect.size.width*7/8, rect.origin.y);
CGContextAddLineToPoint(context, rect.origin.x+rect.size.width, rect.origin.y);
CGContextAddLineToPoint(context, rect.origin.x+rect.size.width, rect.origin.y+rect.size.height/8);
//左下
CGContextMoveToPoint(context, rect.origin.x, rect.origin.y+rect.size.height*7/8);
CGContextAddLineToPoint(context, rect.origin.x, rect.origin.y+rect.size.height);
CGContextAddLineToPoint(context, rect.origin.x+rect.size.width/8, rect.origin.y+rect.size.height);
//右下
CGContextMoveToPoint(context, rect.origin.x+rect.size.width*7/8, rect.origin.y+rect.size.height);
CGContextAddLineToPoint(context, rect.origin.x+rect.size.width, rect.origin.y+rect.size.height);
CGContextAddLineToPoint(context, rect.origin.x+rect.size.width, rect.origin.y+rect.size.height*7/8);
}
}
}
[[UIColor greenColor] set];
CGContextSetLineWidth(context, 2);
CGContextStrokePath(context);
}
在這段代碼的啟發下,我對此作了改裝,把動態的人臉框,改成了靜態的框,這個靜態框,就是指示和限定人臉位置的框,根據屏幕大小畫出的,代碼如下:
-(void)drawFixedPointWithPoints:(NSArray *)arrFixed
{
for (NSDictionary *dicPerson in arrFixed) {
if ([dicPerson objectForKey:POINTS_KEY]) {
for (NSString *strPoints in [dicPerson objectForKey:POINTS_KEY]) {
CGPoint p = CGPointFromString(strPoints);
CGContextAddEllipseInRect(context, CGRectMake(p.x - 1 , p.y - 1 , 2 , 2));
}
}
if ([dicPerson objectForKey:RECT_KEY]) {
CGRect rect=CGRectFromString([dicPerson objectForKey:RECT_KEY]);
// 左上
CGContextMoveToPoint(context, rect.origin.x, rect.origin.y+rect.size.height/8);
CGContextAddLineToPoint(context, rect.origin.x, rect.origin.y);
CGContextAddLineToPoint(context, rect.origin.x+rect.size.width/8, rect.origin.y);
//右上
CGContextMoveToPoint(context, rect.origin.x+rect.size.width*7/8, rect.origin.y);
CGContextAddLineToPoint(context, rect.origin.x+rect.size.width, rect.origin.y);
CGContextAddLineToPoint(context, rect.origin.x+rect.size.width, rect.origin.y+rect.size.height/8);
//左下
CGContextMoveToPoint(context, rect.origin.x, rect.origin.y+rect.size.height*7/8);
CGContextAddLineToPoint(context, rect.origin.x, rect.origin.y+rect.size.height);
CGContextAddLineToPoint(context, rect.origin.x+rect.size.width/8, rect.origin.y+rect.size.height);
//右下
CGContextMoveToPoint(context, rect.origin.x+rect.size.width*7/8, rect.origin.y+rect.size.height);
CGContextAddLineToPoint(context, rect.origin.x+rect.size.width, rect.origin.y+rect.size.height);
CGContextAddLineToPoint(context, rect.origin.x+rect.size.width, rect.origin.y+rect.size.height*7/8);
}
}
[[UIColor blueColor] set];
CGContextSetLineWidth(context, 2);
CGContextStrokePath(context);
}
這里的框是限定臉部位置的,所以臉部位置超出設置的范圍的時候,就需要停止人臉識別,停止動作識別,並給出用戶提示,提示用戶調整位置,或者明確告訴用戶,臉部距離屏幕太近了,或者太遠了。判定臉部位置的代碼如下:
#pragma mark --- 判斷位置
-(BOOL)identifyYourFaceLeft:(CGFloat)left right:(CGFloat)right top:(CGFloat)top bottom:(CGFloat)bottom
{
//判斷位置
if (right - left < 230 || bottom - top < 250) {
self.textLabel.text = @"太遠了...";
[self delateNumber];//清數據
isCrossBorder = YES;
return YES;
}else if (right - left > 320 || bottom - top > 320) {
self.textLabel.text = @"太近了...";
[self delateNumber];//清數據
isCrossBorder = YES;
return YES;
}else{
if (isJudgeMouth != YES) {
self.textLabel.text = @"請重復張嘴動作...";
[self tomAnimationWithName:@"openMouth" count:2];
#pragma mark --- 限定臉部位置為中間位置
if (left < 100 || top < 100 || right > 460 || bottom > 400) {
isCrossBorder = YES;
isJudgeMouth = NO;
self.textLabel.text = @"調整下位置先...";
[self delateNumber];//清數據
return YES;
}
}else if (isJudgeMouth == YES && isShakeHead != YES) {
self.textLabel.text = @"請重復搖頭動作...";
[self tomAnimationWithName:@"shakeHead" count:4];
number = 0;
}else{
takePhotoNumber += 1;
if (takePhotoNumber == 2) {
[self timeBegin];
}
}
isCrossBorder = NO;
}
return NO;
}
這個方法基於Demo中第三方封裝庫中給的代理方法-(NSString)praseDetect:(NSDictionary )positionDic OrignImage:(IFlyFaceImage*)faceImg; 判斷臉部並返回人臉的臉框的坐標,所以利用給的臉部框坐標做判斷,超出設置的范圍時停止識別。
其中,臉部框兩邊的坐標左邊大於一定值且右邊小於一定值的時候,判定為臉部位置“太遠了”;同理,臉部框兩邊的坐標左邊小於設定邊框點且右邊大於設定邊框右邊點的時候,判定為臉部位置“太近了”;如果位置正確,則臉部位置到達正確位置,這個時候顯示臉部各點,並開始活體動作識別:張嘴和搖頭。我這里先做張嘴,再做搖頭。
張嘴識別
張嘴識別,這里的嘴部定點有五個:上、下、左、右、中。這里我取的是上下左右四個點,並判斷上下點的距離變化和左右點的距離變化,一開始只判斷了上下點距離變化超過設定值得時候就判斷為張嘴,后來測試過程中,上下晃動屏幕,會判斷失敗,直接通過。所以為了解決這個bug,並判斷更嚴謹,加上了左右點的判斷,即上下點變化大於設定值並且左右點變化小於設定值的時候判定為張嘴動作識別通過。代碼如下:
#pragma mark --- 判斷是否張嘴
-(void)identifyYourFaceOpenMouth:(NSString *)key p:(CGPoint )p
{
if ([key isEqualToString:@"mouth_upper_lip_top"]) {
upperY = p.y;
}
if ([key isEqualToString:@"mouth_lower_lip_bottom"]) {
lowerY = p.y;
}
if ([key isEqualToString:@"mouth_left_corner"]) {
leftX = p.x;
}
if ([key isEqualToString:@"mouth_right_corner"]) {
rightX = p.x;
}
if (rightX && leftX && upperY && lowerY && isJudgeMouth != YES) {
number ++;
if (number == 1 || number == 300 || number == 600 || number ==900) {
mouthWidthF = rightX - leftX < 0 ? abs(rightX - leftX) : rightX - leftX;
mouthHeightF = lowerY - upperY < 0 ? abs(lowerY - upperY) : lowerY - upperY;
NSLog(@"%d,%d",mouthWidthF,mouthHeightF);
}else if (number > 1200) {
[self delateNumber];//時間過長時重新清除數據
[self tomAnimationWithName:@"openMouth" count:2];
}
mouthWidth = rightX - leftX < 0 ? abs(rightX - leftX) : rightX - leftX;
mouthHeight = lowerY - upperY < 0 ? abs(lowerY - upperY) : lowerY - upperY;
NSLog(@"%d,%d",mouthWidth,mouthHeight);
NSLog(@"張嘴前:width=%d,height=%d",mouthWidthF - mouthWidth,mouthHeight - mouthHeightF);
if (mouthWidth && mouthWidthF) {
//張嘴驗證完畢
if (mouthHeight - mouthHeightF >= 20 && mouthWidthF - mouthWidth >= 15) {
isJudgeMouth = YES;
imgView.animationImages = nil;
}
}
}
}
張嘴動作識別通過后,開始判斷搖頭動作。
搖頭識別
搖頭識別,這里的搖頭動作相比於張嘴動作,搖頭動作我沒有限制位置,張嘴識別必須在設置的框內完成動作,搖頭動作不需要,因為搖頭動作幅度大,需要的位置大,如果再限定位置的話,識別要求比較高,不容易識別通過,用戶體驗差。
搖頭識別的思路比較簡單,沒有做細致的計算分析,僅僅是判斷了鼻尖的點的坐標改變大於設定值,即判定為搖頭動作通過。代碼如下:
#pragma mark --- 判斷是否搖頭
-(void)identifyYourFaceShakeHead:(NSString *)key p:(CGPoint )p
{
if ([key isEqualToString:@"mouth_middle"] && isJudgeMouth == YES) {
if (bigNumber == 0 ) {
firstNumber = p.x;
bigNumber = p.x;
smallNumber = p.x;
}else if (p.x > bigNumber) {
bigNumber = p.x;
}else if (p.x < smallNumber) {
smallNumber = p.x;
}
//搖頭驗證完畢
if (bigNumber - smallNumber > 60) {
isShakeHead = YES;
[self delateNumber];//清數據
}
}
}
其實這樣判斷搖頭是有bug的,左右晃動手機超過一定的距離,也會判定搖頭通過,當時時間緊張,沒做過多處理,所以就暫時這樣判定了。
其他細節
判斷比較數據,我用了計數法,取得是不同時間點的幀圖片上的點的位置並記錄下來,然后和初始值做比較,所以如果判斷不符合要求,需要清除數據,並重新開始記錄並判定。
另外Demo里給出了兩種記錄動作的方式,一種是有聲音的拍照,一種是無聲音的截圖,可以為人臉的對比做鋪墊。
文件目錄截圖

尾聲
Demo的gitHub歡迎大家下載、參考、指正、交流,如果對您有幫助,感謝star,另外建立了一個活體識別交流群:498197808,歡迎同道中人加入,大家一起交流學習。
iOS活體人臉識別的Demo和一些思路
注:本文著作權歸作者,由demo大師代發,拒絕轉載,轉載需要作者授權
