实时美颜

在iOS里面进行实时美颜,我使用的是GPUImageBeautifyFilter, 具体原理可以看作者的文章

具体使用方法如下:

1.在项目的Podfile里面引入GPUImage

target 'TestBeauty' do
    pod 'GPUImage'
end

2.将GPUImageBeautyFilter.hGPUImageBeautyFilter.m添加到项目中

将GPUImageBeautyFilter添加到项目中

3.编写相关代码

ViewController.m里面实现如下代码:

#import "ViewController.h"
#import <GPUImage/GPUImage.h>
#import "GPUImageBeautifyFilter.h"

@interface ViewController ()

@property (nonatomic, strong) GPUImageVideoCamera *videoCamera;
@property (nonatomic, strong) GPUImageView *filterView;
@end


@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];    
    // 设置使用前置摄像头进行美颜
    self.videoCamera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPreset640x480 cameraPosition:AVCaptureDevicePositionFront];
    self.videoCamera.outputImageOrientation = UIInterfaceOrientationPortrait;
    self.videoCamera.horizontallyMirrorFrontFacingCamera = YES; // 镜像
    
    self.filterView = [[GPUImageView alloc] initWithFrame:self.view.frame];
    self.filterView.backgroundColor = [UIColor redColor];
    self.filterView.center = self.view.center;
    
    [self.view addSubview:self.filterView];
    [self.videoCamera addTarget:self.filterView];
    [self.videoCamera startCameraCapture];
}

运行项目之后,就可以成功实时美颜。

4.获取美颜之前的原图

如果想要获取美颜之前的原图,可以实现GPUImageVideoCameraDelegate协议里面的方法

需要进行如下修改:

  • @interface ViewController () <GPUImageVideoCameraDelegate>

  • ViewController.m的viewDidLoad方法里面,让self.videoCamera.delegate = self;

  • 实现协议里面的- (void)willOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer方法。将CMSampleBufferRef -> CVPixelBufferRef -> CGImage -> UIImage经过如下转换,获取到原图

通过下方这个方法,可以把CMSampleBufferRef转成UIImagestackoverflow上有人已经给出了答案,代码如下

#define clamp(a) (a>255?255:(a<0?0:a))

- (UIImage *)imageFromSampleBuffer:(CMSampleBufferRef)sampleBuffer {
    CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    CVPixelBufferLockBaseAddress(imageBuffer,0);
    
    size_t width = CVPixelBufferGetWidth(imageBuffer);
    size_t height = CVPixelBufferGetHeight(imageBuffer);
    uint8_t *yBuffer = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0);
    size_t yPitch = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0);
    uint8_t *cbCrBuffer = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 1);
    size_t cbCrPitch = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 1);
    
    int bytesPerPixel = 4;
    uint8_t *rgbBuffer = malloc(width * height * bytesPerPixel);
    
    for(int y = 0; y < height; y++) {
        uint8_t *rgbBufferLine = &rgbBuffer[y * width * bytesPerPixel];
        uint8_t *yBufferLine = &yBuffer[y * yPitch];
        uint8_t *cbCrBufferLine = &cbCrBuffer[(y >> 1) * cbCrPitch];
        
        for(int x = 0; x < width; x++) {
            int16_t y = yBufferLine[x];
            int16_t cb = cbCrBufferLine[x & ~1] - 128;
            int16_t cr = cbCrBufferLine[x | 1] - 128;
            
            uint8_t *rgbOutput = &rgbBufferLine[x*bytesPerPixel];
            
            int16_t r = (int16_t)roundf( y + cr *  1.4 );
            int16_t g = (int16_t)roundf( y + cb * -0.343 + cr * -0.711 );
            int16_t b = (int16_t)roundf( y + cb *  1.765);
            
            rgbOutput[0] = 0xff;
            rgbOutput[1] = clamp(b);
            rgbOutput[2] = clamp(g);
            rgbOutput[3] = clamp(r);
        }
    }
    
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(rgbBuffer, width, height, 8, width * bytesPerPixel, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipLast);
    CGImageRef quartzImage = CGBitmapContextCreateImage(context);
    UIImage *image = [UIImage imageWithCGImage:quartzImage];
    
    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);
    CGImageRelease(quartzImage);
    free(rgbBuffer);
    
    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
    
    return image;
}

经过以上几步,可以成功拿到未美颜的原图。但是需要注意图片的方向问题:图片的imageOrientation的值是0(即UIImageOrientationUp),但是把图片发送到服务器打开一看,发现逆时针旋转了90度,而且镜像了。而且图片的宽和高的值对换了,比如说正常图片是宽480x高640,但是拿到的未美颜的图片是宽640X高480

在镜头里看到的美颜后的人脸图片如下图所示:

美颜后

在协议方法willOutputSampleBuffer里面用上面这个imageFromSampleBuffer方法取出的未美颜的原图所如下所示:

未美颜

因为未美颜的原图的imageOrientation的值是0,不能直接使用我写的另一篇笔记:调用相机拍照图片旋转了90度里的方法一调整图片方向,但是调整的原理是一样的。

所以,在把未美颜的原图发送到服务器之前,需要将图片进行如下变换:

- (UIImage *)fixNotBeautyImageOrientation:(UIImage *)originImage {
    CGAffineTransform transform = CGAffineTransformIdentity;
    transform = CGAffineTransformTranslate(transform, originImage.size.height, 0);
    transform = CGAffineTransformRotate(transform, M_PI_2);
    transform = CGAffineTransformTranslate(transform, originImage.size.width, 0);
    transform = CGAffineTransformScale(transform, -1, 1);
    
    CGContextRef ctx = CGBitmapContextCreate(NULL, originImage.size.height, originImage.size.width,
                                             CGImageGetBitsPerComponent(originImage.CGImage), 0,
                                             CGImageGetColorSpace(originImage.CGImage),
                                             CGImageGetBitmapInfo(originImage.CGImage));
    CGContextConcatCTM(ctx, transform);
    CGContextDrawImage(ctx, CGRectMake( 0, 0,originImage.size.width,originImage.size.height), originImage.CGImage);
    
    CGImageRef cgimg = CGBitmapContextCreateImage(ctx);
    UIImage *img = [UIImage imageWithCGImage:cgimg];
    CGContextRelease(ctx);
    CGImageRelease(cgimg);
    return img;
}

下面用图片来详细说一下是如何进行变换,最后得到正确的图片的:

  1. 步骤一、获取初始的CTM

黑色区域表示用于生成图片的图形上下文。初始的时候,坐标原点在左下角,X轴向右,Y轴向上。

有一个需要注意的地方:UIGraphicsGetCurrentContext()获得的图形上下文坐标原点是在左上角的。而使用Quartz方法如CGBitmapContextCreate获得的图形上下文的坐标原点是在左下角的。

获取初始的CTM

  1. 步骤二、沿着X正方向移动

因为未美颜的图片的widht是640,height是480。此处沿着X正方向移动了480。

沿着X正方向移动480

  1. 步骤三、绕原点,从X轴向Y轴旋转90度

由于在步骤一里面提到,有两种不同的坐标系,旋转的时候,如果弧度是正的,表示从X轴正方向向Y轴正方向旋转,如果弧度是负的,表示从X轴正方向向Y轴正方向旋转。 这样就不用考虑到底是逆时针还是顺时针旋转的问题了。

旋转方向

绕原点,从X轴向Y轴旋转90度

  1. 步骤四、沿着X正方向移动

因为未美颜的图片的widht是640,height是480。此处沿着X正方向移动了640。

沿着X正方向移动640

  1. 步骤五、X轴沿着Y轴翻转180度

X轴沿着Y轴翻转180度

  1. 步骤六、变换完成之后,以此为基础画图。

绘制图片

上方代码里面CGBitmapContextCreate的宽是480长高640,与图片大小一样。画出来大小就是图片大小。假如说CGBitmapContextCreate设置的宽是500高是700,比图片本身大一点。那么在第五步的基础上,画出来的图片如下图所示,上方和右方会有黑边:

变换完成之后,以此为基础画图

到此为止,在把未美颜的原图经过变换之后,生成新的方向正确的图片,就可以发送给服务器了。