### [iOS GPUImage实现美颜相机功能(优秀)](https://pmself.com/article/1196) **Published:** 2022-02-24T07:11:24 **Author:** crawlpw **Excerpt:** 本文介绍了如何使用 GPUImage 来实现一个简单的相机。具体功能包括拍照、录制视频、多段视频合成、实时美颜… 本文介绍了如何使用 GPUImage 来实现一个简单的相机。具体功能包括拍照、录制视频、多段视频合成、实时美颜、自定义滤镜实现等。 ![](https://pmself.com/wp-content/uploads/2022/02/20220224150140.png) # 前言 AVFoundation 是苹果提供的用于处理基于时间的媒体数据的一个框架。我们想要实现一个相机,需要从手机摄像头采集数据,离不开这个框架的支持。GPUImage 对 AVFoundation 做了一些封装,使我们的采集工作变得十分简单。 另外,GPUImage 的核心魅力还在于,它封装了一个链路结构的图像数据处理流程,简称**滤镜链**。滤镜链的结构使得多层滤镜的叠加功能变得很容易实现。 在下面介绍的功能中,有一些和 GPUImage 本身的关系并不大,我们是直接调用 AVFoundation 的 API 来实现的。但是,这些功能也是一个相机应用必不可少的一部分。所以,我们也会简单讲一下每个功能的实现方式和注意事项。 # 滤镜链简介 在 GPUImage 中,对图像数据的处理都是通过建立滤镜链来实现的。 这里就涉及到了一个类 `GPUImageOutput` 和一个协议 `GPUImageInput` 。对于继承了 `GPUImageOutput` 的类,可以理解为**具备输出图像数据**的能力;对于实现了 `GPUImageInput` 协议的类,可以理解为**具备接收图像数据输入**的能力。 顾名思义,滤镜链作为一个链路,具有起点和终点。根据前面的描述,滤镜链的起点应该只继承了 `GPUImageOutput` 类,滤镜链的终点应该只实现了 `GPUImageInput` 协议,而对于中间的结点应该同时继承了 `GPUImageOutput` 类并实现了 `GPUImageInput` 协议,这样才具备承上启下的作用。 ## 一、滤镜链起点 在 GPUImage 中,只继承了 `GPUImageOutput`,而没有实现 `GPUImageInput` 协议的类有六个,也就是说有六种类型的输入源: **1、GPUImagePicture** `GPUImagePicture` 通过图片来初始化,本质上是先将图片转化为 `CGImageRef`,然后将 `CGImageRef` 转化为纹理。 **2、GPUImageRawDataInput** `GPUImageRawDataInput` 通过二进制数据初始化,然后将二进制数据转化为纹理,在初始化的时候需要指明数据的格式(`GPUPixelFormat`)。 **3、GPUImageTextureInput** `GPUImageTextureInput` 通过已经存在的纹理来初始化。既然纹理已经存在,在初始化的时候就不会重新去生成,只是将纹理的索引保存下来。 **4、GPUImageUIElement** `GPUImageUIElement` 可以通过 `UIView` 或者 `CALayer` 来初始化,最后都是调用 `CALayer` 的 `renderInContext:` 方法,将当前显示的内容绘制到 CoreGraphics 的上下文中,从而获取图像数据。然后将数据转化为纹理。简单来说就是截屏,截取当前控件的内容。 这个类可以用来实现在视频上添加文字水印的功能。因为在 OpenGL 中不能直接进行文本的绘制,所以如果我们想把一个 `UILabel` 的内容添加到滤镜链里面去,使用 `GPUImageUIElement` 来实现是很合适的。 **5、GPUImageMovie** `GPUImageMovie` 通过本地的视频来初始化。首先通过 `AVAssetReader` 来逐帧读取视频,然后将帧数据转化为纹理,具体的流程大概是:`AVAssetReaderOutput` -> `CMSampleBufferRef` -> `CVImageBufferRef` -> `CVOpenGLESTextureRef` -> `Texture` 。 **6、GPUImageVideoCamera** `GPUImageVideoCamera` 通过相机参数来初始化,通过**屏幕比例**和**相机位置(前后置)** 来初始化相机。这里主要使用 `AVCaptureVideoDataOutput` 来获取持续的视频流数据输出,在代理方法 `captureOutput:didOutputSampleBuffer:fromConnection:` 中可以拿到 `CMSampleBufferRef` ,将其转化为纹理的过程与 `GPUImageMovie` 类似。 然而,我们在项目中使用的是它的子类 `GPUImageStillCamera`。 `GPUImageStillCamera` 在原来的基础上多了一个 `AVCaptureStillImageOutput`,它是我们实现拍照功能的关键,在 `captureStillImageAsynchronouslyFromConnection:completionHandler:` 方法的回调中,同样能拿到我们熟悉 `CMSampleBufferRef`。 简单来说,**`GPUImageVideoCamera` 只能录制视频,`GPUImageStillCamera` 还可以拍照,** 因此我们使用 `GPUImageStillCamera` 。 ## 二、滤镜 滤镜链的关键角色是 `GPUImageFilter`,它同时继承了 `GPUImageOutput` 类并实现了 `GPUImageInput` 协议。`GPUImageFilter` 实现**承上启下功能**的基础是「渲染到纹理」,这个操作我们在 [《使用 iOS OpenGL ES 实现长腿功能》](https://links.jianshu.com/go?to=https%3A%2F%2Flink.juejin.im%2F%3Ftarget%3Dhttp%253A%252F%252Fwww.lymanli.com%252F2019%252F03%252F04%252Fios-opengles-spring%252F) 一文中已经介绍过了,简单来说就是**将结果渲染到纹理而不是屏幕上**。 这样,每一个滤镜都能把输出的纹理作为下一个滤镜的输入,实现多层滤镜效果的叠加。 ## 三、滤镜链终点 在 GPUImage 中,实现了 `GPUImageInput` 协议,而没有继承 `GPUImageOutput` 的类有四个: **1、GPUImageMovieWriter** `GPUImageMovieWriter` 封装了 `AVAssetWriter`,可以逐帧从帧缓存的渲染结果中读取数据,最后通过 `AVAssetWriter` 将视频文件保存到指定的路径。 **2、GPUImageRawDataOutput** `GPUImageRawDataOutput` 通过 `rawBytesForImage` 属性,可以获取到当前输入纹理的二进制数据。 假设我们的滤镜链在输入源和终点之间,连接了三个滤镜,而我们需要拿到第二个滤镜渲染后的数据,用来做人脸识别。那我们可以在第二个滤镜后面再添加一个 `GPUImageRawDataOutput` 作为输出,则可以拿到对应的二进制数据,且不会影响原来的渲染流程。 **3、GPUImageTextureOutput** 这个类的实现十分简单,提供协议方法 `newFrameReadyFromTextureOutput:`,在每一帧渲染结束后,将自身返回,通过 `texture` 属性就可以拿到输入纹理的索引。 **4、GPUImageView** `GPUImageView` 继承自 `UIView`,通过输入的纹理,执行一遍渲染流程。这次的渲染目标不是新的纹理,而是自身的 `layer` 。 这个类是我们实现相机功能的重要组成部分,我们所有的滤镜效果,都要依靠它来呈现。 # 功能实现 ## 一、拍照 拍照功能只需调用一个接口就能搞定,在回调方法中可以直接拿到 `UIImage`。代码如下: ``` - (void)takePhotoWtihCompletion:(TakePhotoResult)completion { GPUImageFilter *lastFilter = self.currentFilterHandler.lastFilter; [self.camera capturePhotoAsImageProcessedUpToFilter:lastFilter withCompletionHandler:^(UIImage *processedImage, NSError *error) { if (error && completion) { completion(nil, error); return; } if (completion) { completion(processedImage, nil); } }]; } ``` 值得注意的是,相机的预览页面由 `GPUImageView` 承载,显示的是整个滤镜链作用的结果。而我们的拍照接口,可以传入这个链路上的任意一个滤镜,甚至可以在后面多加一个滤镜,然后拍照接口会返回对应滤镜的渲染结果。即**我们的拍照结果不一定要和我们的预览一致**。 示意图如下: \[图片上传失败…(image-68bb82-1610618344597)\] ## 二、录制视频 ### 1、单段录制 录制视频首先要创建一个 `GPUImageMovieWriter` 作为链路的输出,与上面的拍照接口类似,这里录制的视频不一定和我们的预览一样。 整个过程比较简单,当我们调用停止录制的接口并回调之后,视频就被保存到我们指定的路径了。 ``` - (void)setupMovieWriter { NSString *videoPath = [SCFileHelper randomFilePathInTmpWithSuffix:@".m4v"]; NSURL *videoURL = [NSURL fileURLWithPath:videoPath]; CGSize videoSize = self.videoSize; self.movieWriter = [[GPUImageMovieWriter alloc] initWithMovieURL:videoURL size:videoSize]; GPUImageFilter *lastFilter = self.currentFilterHandler.lastFilter; [lastFilter addTarget:self.movieWriter]; self.camera.audioEncodingTarget = self.movieWriter; self.movieWriter.shouldPassthroughAudio = YES; self.currentTmpVideoPath = videoPath; } ``` ``` - (void)recordVideo { [self setupMovieWriter]; [self.movieWriter startRecording]; } ``` ``` - (void)stopRecordVideoWithCompletion:(RecordVideoResult)completion { @weakify(self); [self.movieWriter finishRecordingWithCompletionHandler:^{ @strongify(self); [self removeMovieWriter]; if (completion) { completion(self.currentTmpVideoPath); } }]; } ``` ### 2、多段录制 在 `GPUImage` 中并没有提供多段录制的功能,需要我们自己去实现。 首先,我们要重复单段视频的录制过程,这样我们就有了多段视频的文件路径。然后主要实现两个功能,一个是 `AVPlayer` 的多段视频循环播放;另一个是通过 `AVComposition` 来合并多段视频,并用 `AVAssetExportSession` 来导出新的视频。 整个过程逻辑并不复杂,出于篇幅的考虑,代码就不贴了,请到项目中查看。 ## 三、保存 在拍照或者录视频结束后,通过 `PhotoKit` 保存到相册里。 ### 1、保存图片 ``` - (void)writeImageToSavedPhotosAlbum:(UIImage *)image completion:(void (^)(BOOL success))completion { [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ [PHAssetChangeRequest creationRequestForAssetFromImage:image]; } completionHandler:^(BOOL success, NSError * _Nullable error) { if (completion) { completion(success); } }]; } ``` ### 2、保存视频 ``` - (void)saveVideo:(NSString *)path completion:(void (^)(BOOL success))completion { NSURL *url = [NSURL fileURLWithPath:path]; [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:url]; } completionHandler:^(BOOL success, NSError * _Nullable error) { dispatch_async(dispatch_get_main_queue(), ^{ if (completion) { completion(success); } }); }]; } ``` ## 四、闪光灯 系统的闪光灯类型通过 `AVCaptureDevice` 的 `flashMode` 属性来控制,其实只有三种,分别是: - **AVCaptureFlashModeOff** 关闭 - **AVCaptureFlashModeOn** 开启(在拍照的时候会闪一下) - **AVCaptureFlashModeAuto** 自动(系统会自动判断当前的环境是否需要闪光灯) 但是市面上的相机应用,一般还有一种**常亮**类型,这种类型在夜间的时候会比较适用。这个功能需要通过 `torchMode` 属性来实现,它其实是指手电筒。 我们对这个两个属性做一下封装,允许这四种类型来回切换,下面是根据封装的类型来同步系统类型的代码: ``` - (void)syncFlashState { AVCaptureDevice *device = self.camera.inputCamera; if (![device hasFlash] || self.camera.cameraPosition == AVCaptureDevicePositionFront) { [self closeFlashIfNeed]; return; } [device lockForConfiguration:nil]; switch (self.flashMode) { case SCCameraFlashModeOff: device.torchMode = AVCaptureTorchModeOff; device.flashMode = AVCaptureFlashModeOff; break; case SCCameraFlashModeOn: device.torchMode = AVCaptureTorchModeOff; device.flashMode = AVCaptureFlashModeOn; break; case SCCameraFlashModeAuto: device.torchMode = AVCaptureTorchModeOff; device.flashMode = AVCaptureFlashModeAuto; break; case SCCameraFlashModeTorch: device.torchMode = AVCaptureTorchModeOn; device.flashMode = AVCaptureFlashModeOff; break; default: break; } [device unlockForConfiguration]; } ``` ## 五、画幅比例 相机的比例通过设置 `AVCaptureSession` 的 `sessionPreset` 属性来实现。这个属性并不只意味着比例,也意味着分辨率。 由于不是所有的设备都支持高分辨率,所以这里只使用 `AVCaptureSessionPreset640x480` 和 `AVCaptureSessionPreset1280x720` 这两个分辨率,分别用来作为 `3:4` 和 `9:16` 的输出。 市面上的相机除了上面的两个比例外,一般还支持 `1:1` 和 `Full` (iPhoneX 系列的全屏)比例,但是系统并没有提供对应比例的 `AVCaptureSessionPreset` 。 这里可以通过 `GPUImageCropFilter` 来实现,这是 GPUImage 的一个内置滤镜,可以对输入的纹理进行裁剪。使用时通过 `cropRegion` 属性来传入一个归一化的裁剪区域。 切换比例的关键代码如下: ``` - (void)setRatio:(SCCameraRatio)ratio { _ratio = ratio; CGRect rect = CGRectMake(0, 0, 1, 1); if (ratio == SCCameraRatio1v1) { self.camera.captureSessionPreset = AVCaptureSessionPreset640x480; CGFloat space = (4 - 3) / 4.0; // 竖直方向应该裁剪掉的空间 rect = CGRectMake(0, space / 2, 1, 1 - space); } else if (ratio == SCCameraRatio4v3) { self.camera.captureSessionPreset = AVCaptureSessionPreset640x480; } else if (ratio == SCCameraRatio16v9) { self.camera.captureSessionPreset = AVCaptureSessionPreset1280x720; } else if (ratio == SCCameraRatioFull) { self.camera.captureSessionPreset = AVCaptureSessionPreset1280x720; CGFloat currentRatio = SCREEN_HEIGHT / SCREEN_WIDTH; if (currentRatio > 16.0 / 9.0) { // 需要在水平方向裁剪 CGFloat resultWidth = 16.0 / currentRatio; CGFloat space = (9.0 - resultWidth) / 9.0; rect = CGRectMake(space / 2, 0, 1 - space, 1); } else { // 需要在竖直方向裁剪 CGFloat resultHeight = 9.0 * currentRatio; CGFloat space = (16.0 - resultHeight) / 16.0; rect = CGRectMake(0, space / 2, 1, 1 - space); } } [self.currentFilterHandler setCropRect:rect]; self.videoSize = [self videoSizeWithRatio:ratio]; } ``` ## 六、前后置切换 通过调用 `GPUImageVideoCamera` 的 `rotateCamera` 方法来实现。 另外,由于前置摄像头不支持闪光灯,如果在前置的时候去切换闪光灯,只能修改我们封装的类型。所以在切换到后置的时候,需要去同步一下系统的闪光灯类型: ``` - (void)rotateCamera { [self.camera rotateCamera]; // 切换摄像头,同步一下闪光灯 [self syncFlashState]; } ``` ## 七、对焦 `AVCaptureDevice` 的 `focusMode` 用来设置聚焦模式,`focusPointOfInterest` 用来设置聚焦点;`exposureMode` 用来设置曝光模式,`exposurePointOfInterest` 用来设置曝光点。 **前置摄像头只支持设置曝光,后置摄像头支持设置曝光和聚焦**,所以在设置之前要先判断是否支持。 需要注意的是,相机默认输出的图像是横向的,图像向右偏转。而前置摄像头又是镜像,所以图像是向左偏转。我们从 `UIView` 获得的触摸点,要经过相应的转化,才是正确的坐标。关键代码如下: ``` - (void)setFocusPoint:(CGPoint)focusPoint { _focusPoint = focusPoint; AVCaptureDevice *device = self.camera.inputCamera; // 坐标转换 CGPoint currentPoint = CGPointMake(focusPoint.y / self.outputView.bounds.size.height, 1 - focusPoint.x / self.outputView.bounds.size.width); if (self.camera.cameraPosition == AVCaptureDevicePositionFront) { currentPoint = CGPointMake(currentPoint.x, 1 - currentPoint.y); } [device lockForConfiguration:nil]; if ([device isFocusPointOfInterestSupported] && [device isFocusModeSupported:AVCaptureFocusModeAutoFocus]) { [device setFocusPointOfInterest:currentPoint]; [device setFocusMode:AVCaptureFocusModeAutoFocus]; } if ([device isExposurePointOfInterestSupported] && [device isExposureModeSupported:AVCaptureExposureModeAutoExpose]) { [device setExposurePointOfInterest:currentPoint]; [device setExposureMode:AVCaptureExposureModeAutoExpose]; } [device unlockForConfiguration]; } ``` ## 八、改变焦距 改变焦距简单来说就是画面的放大缩小,通过设置 `AVCaptureDevice` 的 `videoZoomFactor` 属性实现。 值得注意的是,这个属性有最大值和最小值,设置之前需要做好判断,否则会直接崩溃。代码如下: ``` - (void)setVideoScale:(CGFloat)videoScale { _videoScale = videoScale; videoScale = [self availableVideoScaleWithScale:videoScale]; AVCaptureDevice *device = self.camera.inputCamera; [device lockForConfiguration:nil]; device.videoZoomFactor = videoScale; [device unlockForConfiguration]; } ``` ``` - (CGFloat)availableVideoScaleWithScale:(CGFloat)scale { AVCaptureDevice *device = self.camera.inputCamera; CGFloat maxScale = kMaxVideoScale; CGFloat minScale = kMinVideoScale; if (@available(iOS 11.0, *)) { maxScale = device.maxAvailableVideoZoomFactor; } scale = MAX(scale, minScale); scale = MIN(scale, maxScale); return scale; } ``` ## 九、滤镜 ### 1、滤镜的使用 当我们想使用一个滤镜的时候,只需要把它加到滤镜链里去,通过 `addTarget:` 方法实现。来看一下这个方法的定义: ``` - (void)addTarget:(id)newTarget; ``` 可以看到,只要实现了 `GPUImageInput` 协议,就可以成为滤镜链的下一个结点。 ### 2、美颜滤镜 目前美颜效果已经成为相机应用的标配,我们也来给自己的相机加上美颜的效果。 美颜效果本质上是对图片做模糊,想要达到比较好的效果,需要结合人脸识别,只对人脸的部分进行模糊处理。这里并不去探究美颜算法的实现,直接找开源的美颜滤镜来用。 目前找到的实现效果比较好的是 [LFGPUImageBeautyFilter](https://links.jianshu.com/go?to=https%3A%2F%2Flink.juejin.im%2F%3Ftarget%3Dhttps%253A%252F%252Fgithub.com%252FLaiFengiOS%252FLFLiveKit%252Ftree%252Fmaster%252FLFLiveKit%252Ffilter) ,虽然它的效果肯定比不上现在市面上的美颜类 APP,但是作为学习级别的 Demo 已经足够了。 效果展示: \[图片上传失败…(image-173148-1610618344596)\] ### 3、自定义滤镜 打开 `GPUImageFilter` 的头文件,可以看到有下面这个方法: ``` - (id)initWithVertexShaderFromString:(NSString *)vertexShaderString fragmentShaderFromString:(NSString *)fragmentShaderString; ``` 很容易理解,通过一个顶点着色器和一个片段着色器来初始化,并且可以看到是字符串类型。 另外,`GPUImageFilter` 中还内置了简单的顶点着色器和片段着色器,顶点着色器代码如下: ``` NSString *const kGPUImageVertexShaderString = SHADER_STRING ( attribute vec4 position; attribute vec4 inputTextureCoordinate; varying vec2 textureCoordinate; void main() { gl_Position = position; textureCoordinate = inputTextureCoordinate.xy; } ); ``` 这里用到了 `SHADER_STRING` 宏,看一下它的定义: ``` #define STRINGIZE(x) #x #define STRINGIZE2(x) STRINGIZE(x) #define SHADER_STRING(text) @ STRINGIZE2(text) ``` 在 `#define` 中的 `#` 是「字符串化」的意思,返回 C 语言风格字符串,而 `SHADER_STRING` 在字符串前面加了一个 `@` 符号,则 `SHADER_STRING` 的定义就是将括号中的内容转化为 OC 风格的字符串。 我们之前都是为着色器代码单独创建两个文件,而在 `GPUImageFilter` 中直接以字符串的形式,写死在代码中,两种方式本质上没什么区别。 当我们想自定义一个滤镜的时候,只需要继承 `GPUImageFilter` 来定义一个子类,然后用相同的方式来定义两个保存着色器代码的字符串,并且用这两个字符串来初始化子类就可以了。 作为示例,我把之前实现的 [抖音滤镜](https://links.jianshu.com/go?to=https%3A%2F%2Flink.juejin.im%2F%3Ftarget%3Dhttp%253A%252F%252Fwww.lymanli.com%252F2019%252F04%252F05%252Fios-opengles-filter%252F) 也添加到这个工程里,来看一下效果: \[图片上传失败…(image-28bd40-1610618344595)\] # 总结 通过上面的步骤,我们实现了一个具备基础功能的相机。之后会在这个相机的基础上,继续做一些有趣的尝试,欢迎持续关注~ # 源码 请到 [GitHub](https://links.jianshu.com/go?to=https%3A%2F%2Flink.juejin.im%2F%3Ftarget%3Dhttps%253A%252F%252Fgithub.com%252Flmf12%252FSimpleCam) 上查看完整代码。 # 参考 - [GPUImage(五):五种类型输入源](https://links.jianshu.com/go?to=https%3A%2F%2Flink.juejin.im%2F%3Ftarget%3Dhttps%253A%252F%252Fwww.jianshu.com%252Fp%252F690d83f9c052) - [GPUImage源码阅读(六)](https://links.jianshu.com/go?to=https%3A%2F%2Flink.juejin.im%2F%3Ftarget%3Dhttps%253A%252F%252Fwww.jianshu.com%252Fp%252F8b4263d13cf0) - [GPUImage详细解析(九)图像的输入输出和滤镜通道](https://links.jianshu.com/go?to=https%3A%2F%2Flink.juejin.im%2F%3Ftarget%3Dhttps%253A%252F%252Fwww.jianshu.com%252Fp%252F91044a81ec85) - [iOS 上的相机捕捉](https://links.jianshu.com/go?to=https%3A%2F%2Flink.juejin.im%2F%3Ftarget%3Dhttps%253A%252F%252Fobjccn.io%252Fissue-21-3%252F) - [C语言宏高级用法 \[总结\]](https://links.jianshu.com/go?to=https%3A%2F%2Flink.juejin.im%2F%3Ftarget%3Dhttps%253A%252F%252Fwww.cnblogs.com%252FAnker%252Fp%252F3418792.html) - [iOS美颜实现](https://links.jianshu.com/go?to=https%3A%2F%2Flink.juejin.im%2F%3Ftarget%3Dhttp%253A%252F%252Fwww.saberismywife.com%252F2016%252F10%252F26%252FiOS%2525E7%2525BE%25258E%2525E9%2525A2%25259C%2525E5%2525AE%25259E%2525E7%25258E%2525B0%252F) 获取更佳的阅读体验,请访问原文地址[【Lyman’s Blog】使用 GPUImage 实现一个简单相机](https://links.jianshu.com/go?to=https%3A%2F%2Flink.juejin.im%2F%3Ftarget%3Dhttp%253A%252F%252Fwww.lymanli.com%252F2019%252F06%252F15%252Fios-gpuimage-camera%252F) **Tags:** iOS GPUImage, 美颜相机 **Categories:** 技术 ---