大綱
簡介
本文主要介紹如何結(jié)合使用CoreImage的人臉識別和GPUImage濾鏡功能,實現(xiàn)在人臉的矩形區(qū)域?qū)崟r添加濾鏡的功能。
git地址:DEMO
效果預(yù)覽
demo實現(xiàn)的效果如下動圖所示:

技術(shù)點
- CoreImage
- GPUImage
- OpenGL
設(shè)計思路
1. 概述
設(shè)計大體思路在于利用GPUImage自定義一個濾鏡,這個濾鏡的實際效果可以根據(jù)自己所需來實現(xiàn),比如demo中實現(xiàn)的反色效果。重點在于自定義濾鏡需要提供一個設(shè)置參數(shù)mask,參數(shù)類型為矩形區(qū)域坐標(biāo)(x,y,width,height),我們可以通過這個參數(shù)設(shè)置濾鏡的作用范圍。在自定義的濾鏡中,片元著色器獲取到傳入的這個mask值,然后對每個紋素(可以理解為像素),進(jìn)行區(qū)域判斷,如果在mask所設(shè)定的區(qū)域內(nèi),則進(jìn)行濾鏡效果變換,如果不在的話,則不進(jìn)行處理。
大體流程可以簡單分為下面幾步:
- 第一步,獲取圖像或者視頻幀的
CIImage對象。 - 第二步,通過
CoreImage識別圖像中的人臉,獲取人臉的矩形區(qū)域坐標(biāo) (x , y, width, height)。 - 第三步,獲取矩形區(qū)域坐標(biāo)后,把值賦給自定義濾鏡的mask參數(shù)。
- 第四步,自定義濾鏡把mask值傳遞給自己的片元著色器。片元著色器根據(jù)傳入的區(qū)域坐標(biāo),決定濾鏡的作用區(qū)域。
2. 自定義濾鏡設(shè)計說明
我自定義了一個濾鏡類GPUImageCustomColorInvertFilter,這個濾鏡提供的主要功能是給視頻或者靜態(tài)圖片實現(xiàn)局部反色濾鏡的功能。代碼如下所示:
//GPUImageCustomColorInvertFilter.h文件
#import "GPUImageFilter.h"
@interface GPUImageCustomColorInvertFilter : GPUImageFilter
@property (nonatomic, assign) CGRect mask; //濾鏡作用范圍
@end
//GPUImageCustomColorInvertFilter.m文件
#import "GPUImageCustomColorInvertFilter.h"
NSString *const kGPUImageCustomInvertFragmentShaderString = SHADER_STRING
(
varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
uniform lowp vec4 mask;
void main()
{
lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
//根據(jù)mask判斷,當(dāng)前像素是否在指定舉行區(qū)域內(nèi)
if(gl_FragCoord.x < (mask.x + mask.z) && gl_FragCoord.y < (mask.y + mask.w) && gl_FragCoord.x > mask.x && gl_FragCoord.y > mask.y) {
gl_FragColor = vec4((1.0 - textureColor.rgb), textureColor.w);//實現(xiàn)反色效果的代碼,可以根據(jù)自己所需實現(xiàn)不同效果
}else {
gl_FragColor = textureColor;
}
}
);
@interface GPUImageCustomColorInvertFilter() {
GLint maskUniform;
}
@end
@implementation GPUImageCustomColorInvertFilter
- (id)init;
{
if ((self = [super initWithFragmentShaderFromString:kGPUImageCustomInvertFragmentShaderString])) {
maskUniform = [filterProgram uniformIndex:@"mask"];
}
return self;
}
- (void)setMask:(CGRect)mask {
_mask = mask;
NSLog(@"dkTest: %s mask %@", __func__, NSStringFromCGRect(mask));
GPUVector4 maskVector4 = {mask.origin.x, mask.origin.y, mask.size.width, mask.size.height};
[self setVec4:maskVector4 forUniform:maskUniform program:filterProgram];
}
@end
代碼比較簡單,我們主要關(guān)注兩個點:
- 我們傳遞參數(shù)給自定義濾鏡后,自定義濾鏡如何傳遞值給片元著色器。
- 片元著色器是如何工作的。
2.1 濾鏡如何傳遞值給片元著色器
對于第一點,GPUImage提供了接口方法setVec4:forUniform:program,我們可以通過這個方法,把需要的mask值傳入到片元著色器中,當(dāng)然其他類型的值也有相應(yīng)的方法,例如以GPUImage中GPUImageContrastFilter的contrast參數(shù)為例,可以使用setFoloat:forUniform:program方法,把值傳到了片元著色器。
- (void)setMask:(CGRect)mask {
_mask = mask;
GPUVector4 maskVector4 = {mask.origin.x, mask.origin.y, mask.size.width, mask.size.height};
[self setVec4:maskVector4 forUniform:maskUniform program:filterProgram];
}
然后我們可以通過調(diào)用方法maskUniform = [filterProgram uniformIndex:@"mask"];,設(shè)置在片元著色器中用以獲取我們傳入值的變量名。這樣我們就可以在片元著色器中聲明uniform lowp vec4 mask;,直接使用mask的值。其中uniform代表修飾的變量為全局變量,lowp表示精度。
2.2 片元著色器是如何工作的
對于片元著色器是如何工作的,我覺得可以簡單的理解為對于每一個像素rgba值的處理。著色器會固定接受兩個參數(shù),即2D紋理圖像inputImageTexture和紋理坐標(biāo)textureCoordinate,然后通過texture2D方法去獲取紋素,這是一個紋理圖片的像素。接著對該像素進(jìn)行相應(yīng)的處理。處理完成后,賦值到gl_FragColor中,進(jìn)行輸出。
NSString *const kGPUImageCustomInvertFragmentShaderString = SHADER_STRING
(
varying highp vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
uniform lowp vec4 mask;
void main()
{
lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
//根據(jù)mask判斷,當(dāng)前像素是否在指定舉行區(qū)域內(nèi)
if(gl_FragCoord.x < (mask.x + mask.z) && gl_FragCoord.y < (mask.y + mask.w) && gl_FragCoord.x > mask.x && gl_FragCoord.y > mask.y) {
gl_FragColor = vec4((1.0 - textureColor.rgb), textureColor.w);
}else {
gl_FragColor = textureColor;
}
}
);
難點分析
個人覺得功能實現(xiàn)的主要的難點,第一,在于coreImage和我們熟悉的UIkit以及OpenGL三者之間的坐標(biāo)轉(zhuǎn)換。第二,在與視頻處理的時候,如何獲取每一幀的CIImage圖像,從而獲取當(dāng)前幀人臉的位置。
1. 坐標(biāo)轉(zhuǎn)換
1.1 coreImage和UIKit的坐標(biāo)轉(zhuǎn)換
coreImage中人臉識別完成后,返回的坐標(biāo)是基于以左下角為原點的坐標(biāo)系,如下所示:

而我們熟悉的UIKit的坐標(biāo),是基于左上角為原點的坐標(biāo)系,如下所示:

所以當(dāng)獲取到CIFaceFeature返回的人臉坐標(biāo)的時候,我們需要轉(zhuǎn)換成我們以左上角為原點的坐標(biāo)系的坐標(biāo),示例代碼如下所示
//
CIImage* image = [CIImage imageWithCGImage:imageView.image.CGImage];
CIDetector* detector = [CIDetector detectorOfType:CIDetectorTypeFace
context:... options:...];
//設(shè)置坐標(biāo)轉(zhuǎn)換需要的transform
CGAffineTransform transform = CGAffineTransformMakeScale(1, -1);
transform = CGAffineTransformTranslate(transform,
0, -imageView.bounds.size.height);
//人臉識別
NSArray* features = [detector featuresInImage:image];
for(CIFaceFeature* faceFeature in features) {
//進(jìn)行坐標(biāo)轉(zhuǎn)換
const CGRect faceRect = CGRectApplyAffineTransform(faceFeature.bounds, transform);
UIView* faceView = [[UIView alloc] initWithFrame:faceRect];
...
}
特別注意的一點,因為識別返回的坐標(biāo)是圖片真實的坐標(biāo),如果需要把人臉識別的矩形區(qū)域標(biāo)注到我們自己的view上,比如如下所示的人臉紅框:

我們需要注意展示圖片的imageView可能會把image給拉伸了,所以人臉識別的坐標(biāo)需要乘以拉伸系數(shù),示例代碼如下所示:
self.widthScale = imageSize.width / imageViewSize.width;
self.heigthScale = imageSize.height / imageViewSize.height;
CGRect rect = CGRectMake(feature.bounds.origin.x / self.widthScale, feature.bounds.origin.y / self.heigthScale, feature.bounds.size.width / self.widthScale, feature.bounds.size.height / self.heigthScale);
1.2 OpenGL坐標(biāo)
片元著色器存在著很多類型的坐標(biāo),例如世界坐標(biāo),觀察坐標(biāo),裁剪坐標(biāo),屏幕坐標(biāo)等等,而我們需要用到的就是屏幕坐標(biāo)。只有知道了像素點的屏幕坐標(biāo),我們才能對比人臉識別出來的區(qū)域,判斷是否對該像素點進(jìn)行處理。經(jīng)過查找資料,發(fā)現(xiàn)OpenGL提供了gl_FragCoord值,它描述了當(dāng)前像素點在屏幕上的xy軸坐標(biāo),這正是我們所需要的。
所以我們坐標(biāo)處理可以分為以下幾步:
* 第一步,從CIFeature中獲取人臉的矩形區(qū)域。
* 第二步,把矩形區(qū)域的坐標(biāo)作為以左上角為原點的坐標(biāo)。
* 第三部,對比片元著色器中的點是否在在該矩形區(qū)域中,重要一點,這里矩形區(qū)域無需的坐標(biāo)無需做上文所說的伸縮系數(shù)處理。
著色器坐標(biāo)對比代碼如下所示:
void main()
{
lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
//坐標(biāo)對比
if(gl_FragCoord.x < (mask.x + mask.z) && gl_FragCoord.y < (mask.y + mask.w) && gl_FragCoord.x > mask.x && gl_FragCoord.y > mask.y) {
gl_FragColor = vec4((1.0 - textureColor.rgb), textureColor.w);
}else {
gl_FragColor = textureColor;
}
}
);
2. 視頻幀處理
對于靜態(tài)圖片的局部濾鏡添加處理,我們可以很簡單的理解為下面的簡單步驟:

但是對于視頻的處理,我們需要考慮如何獲取到每一個視頻幀的CIImage,并實時監(jiān)測到人臉區(qū)域坐標(biāo),然后把參數(shù)傳送給濾鏡。下面介紹如何實現(xiàn)這個功能。
2.1 獲取視頻的每一幀數(shù)據(jù)
看了下GPUImage的代碼,發(fā)現(xiàn)GPUImageVideoCamera類中定義了一個代理方法:
@protocol GPUImageVideoCameraDelegate <NSObject>
@optional
- (void)willOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer;
@end
繼續(xù)看這個方法在哪個地方使用:
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
.....
runAsynchronouslyOnVideoProcessingQueue(^{
....
if (self.delegate)
{
[self.delegate willOutputSampleBuffer:sampleBuffer];
}
...
});
.....
}
上述方法是AVCaptureVideoDataOutPut中定義的代理方法,攝像頭開啟后,獲取每一個視頻幀,都會調(diào)用這個代理方法。所以我們可以利用willOutputSampleBuffer:獲取到視頻的每一幀圖像,即sampleBuffer。這樣就解決了我們的獲取視頻幀的問題。
2.2 視頻幀轉(zhuǎn)為CIImage
我們獲取到sampleBuffer,可以通過它獲取到對應(yīng)的CIImage,代碼如下所示:
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CFDictionaryRef attachments = CMCopyDictionaryOfAttachments(kCFAllocatorDefault, sampleBuffer, kCMAttachmentMode_ShouldPropagate);
//從幀中獲取到的圖片相對鏡頭下看到的會向左旋轉(zhuǎn)90度,所以后續(xù)坐標(biāo)的轉(zhuǎn)換要注意。
CIImage *convertedImage = [[CIImage alloc] initWithCVPixelBuffer:pixelBuffer options:(__bridge NSDictionary *)attachments];
特別要注意一點,獲取的CIImage相對于我們手機(jī)上看到的圖,圖的方向會向左旋轉(zhuǎn)90。如下所示:

所以后續(xù)我們的人臉識別以及識別后的坐標(biāo)轉(zhuǎn)換都需要特別的注意。
2.3 人臉識別
如上文所示,圖像是向左旋轉(zhuǎn)了90度,所以識別人臉的時候,我們需要正確設(shè)置CIDetectorImageOrientation,否則識別會失敗。代碼如下所示:
NSDictionary *imageOptions = nil;
UIDeviceOrientation curDeviceOrientation = [[UIDevice currentDevice] orientation];
int exifOrientation;
enum {
PHOTOS_EXIF_0ROW_TOP_0COL_LEFT = 1, // 1 = 0th row is at the top, and 0th column is on the left (THE DEFAULT).
PHOTOS_EXIF_0ROW_TOP_0COL_RIGHT = 2, // 2 = 0th row is at the top, and 0th column is on the right.
PHOTOS_EXIF_0ROW_BOTTOM_0COL_RIGHT = 3, // 3 = 0th row is at the bottom, and 0th column is on the right.
PHOTOS_EXIF_0ROW_BOTTOM_0COL_LEFT = 4, // 4 = 0th row is at the bottom, and 0th column is on the left.
PHOTOS_EXIF_0ROW_LEFT_0COL_TOP = 5, // 5 = 0th row is on the left, and 0th column is the top.
PHOTOS_EXIF_0ROW_RIGHT_0COL_TOP = 6, // 6 = 0th row is on the right, and 0th column is the top.
PHOTOS_EXIF_0ROW_RIGHT_0COL_BOTTOM = 7, // 7 = 0th row is on the right, and 0th column is the bottom.
PHOTOS_EXIF_0ROW_LEFT_0COL_BOTTOM = 8 // 8 = 0th row is on the left, and 0th column is the bottom.
};
BOOL isUsingFrontFacingCamera = NO;
AVCaptureDevicePosition currentCameraPosition = [self.camera cameraPosition];
if (currentCameraPosition != AVCaptureDevicePositionBack) {
isUsingFrontFacingCamera = YES;
}
switch (curDeviceOrientation) {
case UIDeviceOrientationPortraitUpsideDown:
exifOrientation = PHOTOS_EXIF_0ROW_LEFT_0COL_BOTTOM;
break;
case UIDeviceOrientationLandscapeLeft:
if (isUsingFrontFacingCamera) {
exifOrientation = PHOTOS_EXIF_0ROW_BOTTOM_0COL_RIGHT;
}else {
exifOrientation = PHOTOS_EXIF_0ROW_TOP_0COL_LEFT;
}
break;
case UIDeviceOrientationLandscapeRight:
if (isUsingFrontFacingCamera) {
exifOrientation = PHOTOS_EXIF_0ROW_TOP_0COL_LEFT;
}else {
exifOrientation = PHOTOS_EXIF_0ROW_BOTTOM_0COL_RIGHT;
}
break;
default:
exifOrientation = PHOTOS_EXIF_0ROW_RIGHT_0COL_TOP; //值為6。確定初始化原點坐標(biāo)的位置,坐標(biāo)原點為右上。其中橫的為y,豎的為x
break;
}
//exifOrientation的值用于確定圖片的方向
imageOptions = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:exifOrientation] forKey:CIDetectorImageOrientation];
NSArray *features = [self.faceDetector featuresInImage:convertedImage options:imageOptions];
2.4 人臉坐標(biāo)轉(zhuǎn)換
人臉識別的得到的坐標(biāo)仍然是基于左下角為原點的坐標(biāo)系,如下所示:

但是在視頻幀獲取到的CIImage上,人臉識別坐標(biāo)是建立在向左旋轉(zhuǎn)了90度的圖片上,我們實際顯示的圖像應(yīng)該讓它向右旋轉(zhuǎn)90度,視圖回到垂直的位置。如下所示:

我們對比發(fā)現(xiàn),這時候coreImage坐標(biāo)的原點和我們熟悉的UIKit坐標(biāo)的原點是重合的,只是x軸和y軸的坐標(biāo)變換了位置,所以我們只需轉(zhuǎn)換下x,y坐標(biāo)以及交換長寬就能完成坐標(biāo)的轉(zhuǎn)換。示例代碼如下所示:
for (CIFeature *feature in featureArray) {
CGRect faceRect = feature.bounds;
CGFloat temp = faceRect.size.width;
faceRect.size.width = faceRect.size.height; //長寬互換
faceRect.size.height = temp;
temp = faceRect.origin.x;
faceRect.origin.x = faceRect.origin.y;
faceRect.origin.y = temp;
....
}
獲取到坐標(biāo)后,傳值給自定義濾鏡即可,濾鏡中的對比代碼再上文已經(jīng)提到了。
待優(yōu)化點
- 未實現(xiàn)多人臉識別。
- 視頻臉部添加濾鏡過程中,cpu使用率過高,待優(yōu)化。