实时美颜
在iOS里面进行实时美颜,我使用的是GPUImageBeautifyFilter
, 具体原理可以看作者的文章。
具体使用方法如下:
1.在项目的Podfile
里面引入GPUImage
target 'TestBeauty' do
pod 'GPUImage'
end
2.将GPUImageBeautyFilter.h
和GPUImageBeautyFilter.m
添加到项目中
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
转成UIImage
,stackoverflow上有人已经给出了答案,代码如下
#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;
}
下面用图片来详细说一下是如何进行变换,最后得到正确的图片的:
- 步骤一、获取初始的CTM
黑色区域表示用于生成图片的图形上下文
。初始的时候,坐标原点在左下角,X轴向右,Y轴向上。
有一个需要注意的地方:UIGraphicsGetCurrentContext()获得的图形上下文坐标原点是在左上角的。而使用Quartz方法如CGBitmapContextCreate
获得的图形上下文的坐标原点是在左下角的。
- 步骤二、沿着X正方向移动
因为未美颜的图片的widht是640,height是480。此处沿着X正方向移动了480。
- 步骤三、绕原点,从X轴向Y轴旋转90度
由于在步骤一
里面提到,有两种不同的坐标系,旋转的时候,如果弧度是正的,表示从X轴正方向向Y轴正方向旋转,如果弧度是负的,表示从X轴正方向向Y轴正方向旋转。 这样就不用考虑到底是逆时针还是顺时针旋转的问题
了。
- 步骤四、沿着X正方向移动
因为未美颜的图片的widht是640,height是480。此处沿着X正方向移动了640。
- 步骤五、X轴沿着Y轴翻转180度
- 步骤六、变换完成之后,以此为基础画图。
上方代码里面CGBitmapContextCreate
的宽是480长高640,与图片大小一样。画出来大小就是图片大小。假如说CGBitmapContextCreate
设置的宽是500高是700,比图片本身大一点。那么在第五步的基础上,画出来的图片如下图所示,上方和右方会有黑边:
到此为止,在把未美颜的原图经过变换之后,生成新的方向正确的图片,就可以发送给服务器了。