CoreImage和GPUImage的結(jié)合使用

大綱

簡介

本文主要介紹如何結(jié)合使用CoreImage的人臉識別和GPUImage濾鏡功能,實現(xiàn)在人臉的矩形區(qū)域?qū)崟r添加濾鏡的功能。

git地址:DEMO

效果預(yù)覽

demo實現(xiàn)的效果如下動圖所示:

未命名.gif

技術(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)的方法,例如以GPUImageGPUImageContrastFiltercontrast參數(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)系,如下所示:

coreImageCoor.jpg

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

UIkitCoor.png

所以當(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上,比如如下所示的人臉紅框:

redrectangle.png

我們需要注意展示圖片的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)圖片的局部濾鏡添加處理,我們可以很簡單的理解為下面的簡單步驟:

localProcess.png

但是對于視頻的處理,我們需要考慮如何獲取到每一個視頻幀的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。如下所示:

videoCIImage.png

所以后續(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)系,如下所示:

hen.png

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

videFrameRotate.png

我們對比發(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)化。

參考資料

著色器

CoreImage和UIKit坐標(biāo)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 許多UIView的子類,如一個UIButton或一個UILabel,它們知道怎么繪制自己。遲早,你也將想要做一些自...
    shenzhenboy閱讀 1,754評論 2 8
  • { ??、引導(dǎo)界面 sleep(1.5); self.window = [[UIWindow alloc] init...
    CYC666閱讀 398評論 0 1
  • 【前言】: 轉(zhuǎn)眼之間婆婆過世已經(jīng)一個年頭了,其間我努力地不讓自己沉浸在失去親人的痛苦之中,并不是疏遠(yuǎn)了這份情結(jié),只...
    恨水a(chǎn)閱讀 630評論 4 4
  • 正月初四下午,一家人往橋墩碗窯而去,沿著蜿蜒的山道,在清風(fēng)綠水中,徐徐駛進(jìn)了目的地——碗窯。其實之前已經(jīng)來...
    冰以璇閱讀 319評論 0 0
  • 上篇文章寫了關(guān)于Ubuntu下安裝opencv3.2.0的具體步驟,以及最后的代碼測試。不過,在視覺實際的開發(fā)過...
    徐大徐閱讀 2,393評論 1 0

友情鏈接更多精彩內(nèi)容