

在 iOS 中给视频添加滤镜
source link: http://www.lymanli.com/2020/03/15/ios-video-filter/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

「众所周知,视频可以 P」,今天我们来学习怎么给视频添加滤镜。
在 iOS 中,对视频进行图像处理一般有两种方式: GPUImage 和 AVFoundation 。
一、GPUImage
在之前的文章中,我们对 GPUImage 已经有了一定的了解。之前一般使用它对摄像头采集的图像数据进行处理,然而,它对本地视频的处理也一样方便。
直接看代码:
// movie NSString *path = [[NSBundle mainBundle] pathForResource:@"sample" ofType:@"mp4"]; NSURL *url = [NSURL fileURLWithPath:path]; GPUImageMovie *movie = [[GPUImageMovie alloc] initWithURL:url]; // filter GPUImageSmoothToonFilter *filter = [[GPUImageSmoothToonFilter alloc] init]; // view GPUImageView *imageView = [[GPUImageView alloc] initWithFrame:CGRectMake(0, 80, self.view.frame.size.width, self.view.frame.size.width)]; [self.view addSubview:imageView]; // chain [movie addTarget:filter]; [filter addTarget:imageView]; // processing [movie startProcessing];
核心代码一共就几行。 GPUImageMovie
负责视频文件的读取, GPUImageSmoothToonFilter
负责滤镜效果处理, GPUImageView
负责最终图像的展示。
通过滤镜链将三者串起来,然后调用 GPUImageMovie
的 startProcessing
方法开始处理。
虽然 GPUImage
在使用上简单,但是存在着 没有声音 、 在非主线程调用 UI 、 导出文件麻烦 、 无法进行播放控制 等诸多缺点。
小结:GPUImage 虽然使用很方便,但是存在诸多缺点,不满足生产环境需要。
二、AVFoundation
1、 AVPlayer 的使用
首先来复习一下 AVPlayer
最简单的使用方式:
NSURL *url = [[NSBundle mainBundle] URLForResource:@"sample" withExtension:@"mp4"]; AVURLAsset *asset = [AVURLAsset assetWithURL:url]; AVPlayerItem *playerItem = [[AVPlayerItem alloc] initWithAsset:asset]; AVPlayer *player = [[AVPlayer alloc] initWithPlayerItem:playerItem]; AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
第一步先构建 AVPlayerItem
,然后通过 AVPlayerItem
创建 AVPlayer
,最后通过 AVPlayer
创建 AVPlayerLayer
。
AVPlayerLayer
是 CALayer
的子类,可以把它添加到任意的 Layer
上。当 AVPlayer
调用 play
方法时, AVPlayerLayer
上就能将图像渲染出来。
AVPlayer
的使用方式十分简单。但是,按照上面的方式,最终只能在 AVPlayerLayer
上渲染出最原始的图像。如果我们希望在播放的同时,对原始图像进行处理,则需要修改 AVPlayer
的渲染过程。
2、修改 AVPlayer 的渲染过程
修改 AVPlayer
的渲染过程,要从 AVPlayerItem
下手,主要分为 四步 :
第一步:自定义 AVVideoCompositing 类
AVVideoCompositing
是一个协议,我们的自定义类要实现这个协议。在这个自定义类中,可以获取到每一帧的原始图像,进行处理并输出。
在这个协议中,最关键是 startVideoCompositionRequest
方法的实现:
// CustomVideoCompositing.m - (void)startVideoCompositionRequest:(AVAsynchronousVideoCompositionRequest *)asyncVideoCompositionRequest { dispatch_async(self.renderingQueue, ^{ @autoreleasepool { if (self.shouldCancelAllRequests) { [asyncVideoCompositionRequest finishCancelledRequest]; } else { CVPixelBufferRef resultPixels = [self newRenderdPixelBufferForRequest:asyncVideoCompositionRequest]; if (resultPixels) { [asyncVideoCompositionRequest finishWithComposedVideoFrame:resultPixels]; CVPixelBufferRelease(resultPixels); } else { // print error } } } }); }
通过 newRenderdPixelBufferForRequest
方法从 AVAsynchronousVideoCompositionRequest
中获取到处理后的 CVPixelBufferRef
后输出,看下这个方法的实现:
// CustomVideoCompositing.m - (CVPixelBufferRef)newRenderdPixelBufferForRequest:(AVAsynchronousVideoCompositionRequest *)request { CustomVideoCompositionInstruction *videoCompositionInstruction = (CustomVideoCompositionInstruction *)request.videoCompositionInstruction; NSArray<AVVideoCompositionLayerInstruction *> *layerInstructions = videoCompositionInstruction.layerInstructions; CMPersistentTrackID trackID = layerInstructions.firstObject.trackID; CVPixelBufferRef sourcePixelBuffer = [request sourceFrameByTrackID:trackID]; CVPixelBufferRef resultPixelBuffer = [videoCompositionInstruction applyPixelBuffer:sourcePixelBuffer]; if (!resultPixelBuffer) { CVPixelBufferRef emptyPixelBuffer = [self createEmptyPixelBuffer]; return emptyPixelBuffer; } else { return resultPixelBuffer; } }
在这个方法中,我们通过 trackID
从 AVAsynchronousVideoCompositionRequest
中获取到 sourcePixelBuffer
,也就是当前帧的原始图像。
然后调用 videoCompositionInstruction
的 applyPixelBuffer
方法,将 sourcePixelBuffer
作为输入,得到处理后的结果 resultPixelBuffer
。也就是说,我们对图像的处理操作,都发生在 applyPixelBuffer
方法中。
在 newRenderdPixelBufferForRequest
这个方法中,我们已经拿到了当前帧的原始图像 sourcePixelBuffer
,其实也可以直接在这个方法中对图像进行处理。
那为什么还需要把处理操作放在 CustomVideoCompositionInstruction
中呢?
因为在实际渲染的时候,自定义 AVVideoCompositing
类的实例创建是系统内部完成的。也就是说,我们访问不到最终的 AVVideoCompositing
对象。所以无法进行一些渲染参数的动态修改。而从 AVAsynchronousVideoCompositionRequest
中,可以获取到 AVVideoCompositionInstruction
对象,所以我们需要自定义 AVVideoCompositionInstruction
,这样就可以间接地通过修改 AVVideoCompositionInstruction
的属性,来动态修改渲染参数。
第二步:自定义 AVVideoCompositionInstruction
这个类的关键点是 applyPixelBuffer
方法的实现:
// CustomVideoCompositionInstruction.m - (CVPixelBufferRef)applyPixelBuffer:(CVPixelBufferRef)pixelBuffer { self.filter.pixelBuffer = pixelBuffer; CVPixelBufferRef outputPixelBuffer = self.filter.outputPixelBuffer; CVPixelBufferRetain(outputPixelBuffer); return outputPixelBuffer; }
这里把 OpenGL ES 的处理细节都封装到了 filter
中。这个类的实现细节可以先忽略,只需要知道它接受 原始的 CVPixelBufferRef
,返回 处理后的 CVPixelBufferRef
。
第三步:构建 AVMutableVideoComposition
构建的代码如下:
self.videoComposition = [self createVideoCompositionWithAsset:self.asset]; self.videoComposition.customVideoCompositorClass = [CustomVideoCompositing class];
- (AVMutableVideoComposition *)createVideoCompositionWithAsset:(AVAsset *)asset { AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoCompositionWithPropertiesOfAsset:asset]; NSArray *instructions = videoComposition.instructions; NSMutableArray *newInstructions = [NSMutableArray array]; for (AVVideoCompositionInstruction *instruction in instructions) { NSArray *layerInstructions = instruction.layerInstructions; // TrackIDs NSMutableArray *trackIDs = [NSMutableArray array]; for (AVVideoCompositionLayerInstruction *layerInstruction in layerInstructions) { [trackIDs addObject:@(layerInstruction.trackID)]; } CustomVideoCompositionInstruction *newInstruction = [[CustomVideoCompositionInstruction alloc] initWithSourceTrackIDs:trackIDs timeRange:instruction.timeRange]; newInstruction.layerInstructions = instruction.layerInstructions; [newInstructions addObject:newInstruction]; } videoComposition.instructions = newInstructions; return videoComposition; }
构建 AVMutableVideoComposition
的过程 主要做两件事情 。
第一件事情,把 videoComposition
的 customVideoCompositorClass
属性,设置为我们自定义的 CustomVideoCompositing
。
第二件事情,首先通过系统提供的方法 videoCompositionWithPropertiesOfAsset
构建出 AVMutableVideoComposition
对象,然后将它的 instructions
属性修改为自定义的 CustomVideoCompositionInstruction
类型。(就像「第一步」提到的,后续可以在 CustomVideoCompositing
中,拿到 CustomVideoCompositionInstruction
对象。)
注意:这里可以把 CustomVideoCompositionInstruction
保存下来,然后通过修改它的属性,去修改渲染参数。
第四步:构建 AVPlayerItem
有了 AVMutableVideoComposition
之后,后面的事情就简单多了。
只需要在创建 AVPlayerItem
的时候,多赋值一个 videoComposition
属性。
self.playerItem = [[AVPlayerItem alloc] initWithAsset:self.asset]; self.playerItem.videoComposition = self.videoComposition;
这样,整条链路就串起来了, AVPlayer
在播放时,就能在 CustomVideoCompositionInstruction
的 applyPixelBuffer
方法中接收到 原始图像的 CVPixelBufferRef
。
3、应用滤镜效果
这一步要做的事情是: 在 CVPixelBufferRef
上添加滤镜效果,并输出处理后的 CVPixelBufferRef
。
要做到这件事情,有很多种方式。包括但不限定于: OpenGL ES 、 CIImage 、 Metal 、 GPUImage 等。
为了同样使用前面用到的 GPUImageSmoothToonFilter
,这里介绍一下 GPUImage 的方式。
关键代码如下:
- (CVPixelBufferRef)renderByGPUImage:(CVPixelBufferRef)pixelBuffer { CVPixelBufferRetain(pixelBuffer); __block CVPixelBufferRef output = nil; runSynchronouslyOnVideoProcessingQueue(^{ [GPUImageContext useImageProcessingContext]; // (1) GLuint textureID = [self.pixelBufferHelper convertYUVPixelBufferToTexture:pixelBuffer]; CGSize size = CGSizeMake(CVPixelBufferGetWidth(pixelBuffer), CVPixelBufferGetHeight(pixelBuffer)); [GPUImageContext setActiveShaderProgram:nil]; // (2) GPUImageTextureInput *textureInput = [[GPUImageTextureInput alloc] initWithTexture:textureID size:size]; GPUImageSmoothToonFilter *filter = [[GPUImageSmoothToonFilter alloc] init]; [textureInput addTarget:filter]; GPUImageTextureOutput *textureOutput = [[GPUImageTextureOutput alloc] init]; [filter addTarget:textureOutput]; [textureInput processTextureWithFrameTime:kCMTimeZero]; // (3) output = [self.pixelBufferHelper convertTextureToPixelBuffer:textureOutput.texture textureSize:size]; [textureOutput doneWithTexture]; glDeleteTextures(1, &textureID); }); CVPixelBufferRelease(pixelBuffer); return output; }
(1)一开始读入的视频帧是 YUV 格式的,首先把 YUV 格式的 CVPixelBufferRef
转成 OpenGL 纹理。
(2)通过 GPUImageTextureInput
来构造滤镜链起点, GPUImageSmoothToonFilter
来添加滤镜效果, GPUImageTextureOutput
来构造滤镜链终点,最终也是输出 OpenGL 纹理。
(3)将处理后的 OpenGL 纹理转化为 CVPixelBufferRef
。
另外,由于 CIImage 使用简单,也顺便提一下用法。
关键代码如下:
- (CVPixelBufferRef)renderByCIImage:(CVPixelBufferRef)pixelBuffer { CVPixelBufferRetain(pixelBuffer); CGSize size = CGSizeMake(CVPixelBufferGetWidth(pixelBuffer), CVPixelBufferGetHeight(pixelBuffer)); // (1) CIImage *image = [[CIImage alloc] initWithCVPixelBuffer:pixelBuffer]; // (2) CIImage *filterImage = [CIImage imageWithColor:[CIColor colorWithRed:255.0 / 255 green:245.0 / 255 blue:215.0 / 255 alpha:0.1]]; // (3) image = [filterImage imageByCompositingOverImage:image]; // (4) CVPixelBufferRef output = [self.pixelBufferHelper createPixelBufferWithSize:size]; [self.context render:image toCVPixelBuffer:output]; CVPixelBufferRelease(pixelBuffer); return output; }
(1)将 CVPixelBufferRef
转化为 CIImage
。
(2)创建一个带透明度的 CIImage
。
(3)用系统方法将 CIImage
进行叠加。
(4)将叠加后的 CIImage
转化为 CVPixelBufferRef
。
4、导出处理后的视频
视频处理完成后,最终都希望能导出并保存。
导出的代码也很简单:
self.exportSession = [[AVAssetExportSession alloc] initWithAsset:self.asset presetName:AVAssetExportPresetHighestQuality]; self.exportSession.videoComposition = self.videoComposition; self.exportSession.outputFileType = AVFileTypeMPEG4; self.exportSession.outputURL = [NSURL fileURLWithPath:self.exportPath]; [self.exportSession exportAsynchronouslyWithCompletionHandler:^{ // 保存到相册 // ... }];
这里关键的地方在于将 videoComposition
设置为前面构造的 AVMutableVideoComposition
对象,然后设置好输出路径和文件格式后就可以开始导出。导出成功后,可以将视频文件转存到相册中。
小结: AVFoundation
虽然使用比较繁琐,但是功能强大,可以很方便地导出视频处理的结果,是用来做视频处理的不二之选。
Recommend
-
69
Nihon Cam for iOS - 包含 5810 种滤镜的另类 app - NEXT
-
50
-
58
作为手机行业中知名的“小众厂商”,美图在前不久推出了年度旗舰——美图T9系列手机。在过去的半年里,美图可以说是“小动作”频频,不断和颐和园、兰博基尼等机构进行深度的IP合作。然而,据赛诺公布的201...
-
64
作为手机行业中知名的“小众厂商”,美图在前不久推出了年度旗舰 ——美图T9系列手机。在过去的半年里,美图可以说是“小动作”频频,不断和颐和园、兰博基尼等...
-
54
Nihon Live for iOS - 有创意地以实时滤镜,呈现霓虹国 451 种传统颜色 - NEXT
-
12
← 地热供暖技术的寿命还剩不到一个世纪粪便移植缩小了之前无药可医的癌症患者的肿瘤 →majer...
-
2
滤镜最早的出现应该是应用在相机镜头前实现自然光过滤和调色的镜片,然而在软件开发中更多的指的是软件滤镜,是对镜头滤镜的模拟实现。当然这种方式更加方便快捷,...
-
5
滤镜也能复制粘贴?视频编辑服务专属滤镜一键搞定剪辑制作视频时,如果用户想要实现视频与某张特定图片一样的滤镜风格,怎么提取呢?
-
3
-
7
使用CSS和DataURI来添加杂色滤镜效果发布时间: 2013-08-18
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK