WWDC20 CoreImage 专题

作者:Lucca,iOS 初学者,目前就职于字节跳动抖音iOS业务安全组。

Sessions: 

https://developer.apple.com/videos/play/wwdc2020/10008/ 

https://developer.apple.com/videos/play/wwdc2020/10021/ https://developer.apple.com/videos/play/wwdc2020/10089/

同时感谢 @Puttin 和 @jojotov 的帮助

概览

本文含括了本次WWDC2020里Core Image专题三个文章的脱水翻译。分别是:

  • 优化视频处理终端Core Image工作流(10008-Optimize the Core Image pipeline for your video app[1]

  • 如何构建基于Metal的Core Image内核(10021-Build Metal-based Core Image kernels with Xcode[2]

  • 探索Core Image的debug技巧(10089-Discover Core Image debugging techniques[3]

这几篇Session主要讲的是对于CoreImage处理视频滤镜时的一些最佳实践,同时也简单介绍了怎么更好的去debug整个Core Image的渲染流程。基于文章内容以及笔者自己的一些理解,将文章的整体大纲定义如下:

  • 优化视频处理终端Core Image工作流

    • 创建CIContext的一些建议

    • 尽量使用内置的CIFilter

    • 编写并应用自定义的Core Image内核

    • 合理选择视图类呈现视频渲染的效果

  • 探索Core Image的debug技巧

    • CI_PRINT_TREE是什么

    • 怎么在应用中开启和控制CI_PRINT_TREE

    • 怎样获取和理解CI_PRINT_TREE的产物

优化视频处理终端Core Image工作流

创建 CIContext 的建议

WWDC20 CoreImage 专题_第1张图片

每个视图只创建一个 CIContext

Context 的创建比较容易,但初始化需要花费较多的时间和内存,因此我们需要尽量减少重复创建 CIContext 所带来的性能损耗。

通过 CIContextOption 创建 CIContext

创建 CIContext 时可以传入需要的 CIContextOption(更多信息请参考 苹果官方文档[4]),其中有一些选项可以帮助我们优化所创建的 CIContext:

  • .cacheIntermediates:在渲染过程中,是否需要缓存中间像素缓冲区的内容。由于我们面对的是视频处理,而在视频中,绝大部分情况下每一帧的内容都与上一帧不同,因此关闭缓存可以非常有效地减少内存占用。把这个选项设置为 false 对我们的优化非常重要!

  • .name:为 context 设置一个名字可以帮助我们调试 Core Image,更多详细内容可以参考 [CoreImage Debugging Techniques]( "CoreImage Debugging Techniques")。

结合使用 Metal 和 CIContext

在有些时候,我们可能需要混合使用 Core Image 和其他的 Metal 特性,例如我们 Core Image 的输入或者输出是 MTLTexture时。在这种情况下,session 中提到一种推荐的处理方法:使用 MTLCommandQueue 来构造 CIContext 实例。

为了解释为何需要通过这种方式构造 CIContext,我们首先考虑一下此情况下的流程时间线,假设我们的应用使用一条 MTLCommandQueue 队列来渲染一个 Metal 纹理,此时 CPU 和 GPU 都会进行相应的工作:

WWDC20 CoreImage 专题_第2张图片

接下来,我们把渲染好的 Metal 纹理传入 Core Image,假如此时 Core Image 没有显示地设置队列,它会使用内置的独立  Metal 队列进行工作:

WWDC20 CoreImage 专题_第3张图片

最后,Core Image 处理完成后的纹理对象会再次在一开始的 Metal 队列中进行其他处理:

WWDC20 CoreImage 专题_第4张图片

可以看到,由于 CoreImage 和 Metal 的工作都在不同队列中进行,在不同的工作切换时,应用必须发出等待的指令来保证接收到正确的结果,这造成了不必要的性能和时间损耗:

WWDC20 CoreImage 专题_第5张图片

为了解决这个问题,消除等待造成的浪费,我们可以让 Core Image 和 Metal 公用同一条 MTLCommandQueue 队列,这样的话 Metal 的渲染工作和 Core Image 处理工作之间就不会有等待的间隙,整个流程会变得更加高效:

WWDC20 CoreImage 专题_第6张图片

苹果文档中关于其他 Core Image 性能相关的最佳实践请参考 Getting the Best Performance[5]

尽量使用内置的 CIFilter

为了获得更好的性能,使用内置的 CIFilter 是最简单有效的方法:

  • Built-in 的 CIFilters 为 Metal 单独作了优化,目前所有的内置 CIFilter 都是使用 Metal 实现的

  • 文档更新(参数说明、示例图片效果、示例代码)

步骤:

  • import CIFilterBuitins

  • 设置输入图片和参数属性

  • 获取输出图片

使用 Built-in 的 CIFilter 添加模糊滤镜:

import CoreImage.CIFilterBuiltins

func motionBlur(inputImage: CIImage) -> CIImage? {
    let motionBlurFilter = CIFilter.motionBlur()
    motionBlurFilter.inputImage = inputImage
    motionBlurFilter.angle = 0
    motionBlurFilter.radius = 20
    return motionBlurFilter.outputImage
}

更多关于使用 Core Image 内置滤镜的信息,请参考苹果官方文档 Processing an Image Using Built-in Filters[6]

编写并应用自定义的 CI Kernels

为什么要使用自定义的CI Kernels

  • 拥有CIKernels的所有特性

  • 减少运行时编译时间

  • 提升语言的运行性能(例如聚集读取、组写入、half-float的数学计算)

  • 更棒的高亮提醒和语法检查。

在应用程序中加入基于Metal的自定义Core Image内核

这里只需要简单的五步操作就可以把自定义Core Image内核加到你的应用中

  1. 添加自定义的构建规则到你的工程中

  2. 添加.ci.metal结尾的源文件到你的工程中

  3. 编写你的内核

  4. 初始化你的内核对象

  5. 使用你的内核创建一个新的CIImages

添加自定义的构建规则到你的工程中

首先我们针对.ci.metal结尾的文件添加构建规则,对于所有以此结尾的文件我们都将调用如下脚本,该脚本的-fcikernel标志表示此类文件会使用metal编译器构建出一个.ci.air结尾的二进制文件。

WWDC20 CoreImage 专题_第7张图片

其次我们针对.ci.air的文件也添加一条构建规则,该规则运行的脚本的-cikernel标志会调用metal的链接程序,最后在应用程序的目录中产出一个.ci.mentallib结尾的文件。

WWDC20 CoreImage 专题_第8张图片
添加.ci.metal结尾的源文件到你的工程中

通过文件-新建,我们可以新建一个Meta File类型的文件,然后命名需要以.ci结尾,以便最后产出的文件是以.ci.metal结尾

WWDC20 CoreImage 专题_第9张图片

WWDC20 CoreImage 专题_第10张图片

<<< 左右滑动见更多 >>>

编写Metal内核

本次使用的内核是在Session*[Edit and Playback HDR video with AVFoundation]( "Edit and Playback HDR video with AVFoundation")*中使用的内核

WWDC20 CoreImage 专题_第11张图片

在这个 Demo 中我们实现了一种斑马条纹的效果,可以突出显示HDR视频的明亮部分、扩展其中的中心区域。

下面我们通过代码来演示如何通过自定义内核实现这个效果:

  • 首先引入CoreImage的头文件

  • 定义这个效果的函数,这个函数必须标识extern "C"(表示这个函数会使用C语言编译)

// MyKernels.ci.metal
#include  // includes CIKernelMetalLib.h
using namespace metal;

extern "C" float4 HDRZebra (coreimage::sample_t s, float time, coreimage::destination dest) 
{
 float diagLine = dest.coord().x + dest.coord().y;
 float zebra = fract(diagLine/20.0 + time*2.0);
 if ((zebra > 0.5) && (s.r > 1 || s.g > 1 || s.b > 1))
  return float4(2.0, 0.0, 0.0, 1.0);
 return s;
}

因为这是一个CIColorKernel,所以返回值必须是float4,这里接受的第一个参数是sample_t_s,表示输入图片的像素。最后一个参数是一个提供返回像素的坐标(destination)的一个结构体。在代码实现中,我们根据dest来确定处于哪个对角线,然后通过一些简单的计算判断是否处于斑马纹上,且这个像素的亮度超过了正常亮度标准,我们就返回一个亮红色的像素。其他情况就返回原像素。

最终实现的效果就是如下这样:

WWDC20 CoreImage 专题_第12张图片

关于更多Core Image内核中Metal Shader Language的知识,你可以访问我们的官方网站来下载更多相关内容。Metal Shader Language For Core Image Kernels[7]

使用 Swift 代码加载内核并生成一张图片

通过以下代码我们就能加载内核并用它来创建图片

class HDRZebraFilter: CIFilter {
    var inputImage: CIImage?
 var inputTime: Float = 0.0

    static var kernel: CIColorKernel = { () -> CIColorKernel in 
     let url = Bundle.main.url(forResource: "MyKernels", 
                                withExtension: "ci.metallib")!
  let data = try! Data(contentsOf: url)
  return try! CIColorKernel(functionName: "HDRzebra", 
                          fromMetalLibraryData: data)
 }()

   override var outputImage : CIImage? {
  get {
   guard let input = inputImage else {return nil}
   return HDRZebraFilter.kernel.apply(extent: input.extent, 
            arguments: [input, inputTime])
  }
 }
}

内核经常被用于子类化CIFilter,他一般会接受一个inputImage的CIImage对象和一些其他参数。我们建议将kernel定义为静态变量,这样我们就只需要在第一次使用到这个内核对象的时候去加载metallib资源。

接下来我们需要重写outputImage这个变量,在这个变量的getter方法里,我们可以拿到静态变量加载好的内核,然后通过编写好的斑马纹函数来生成一张新的图片!

如何更好地渲染到视图

WWDC20 CoreImage 专题_第13张图片
  • 务必避免使用 UIImageView 和 NSImageView,他们是为静态图片而设计的

  • 最简单的选择:AVPlayerView:自动播放视频、

  • 更优异的选择:MTKView

使用AVPlayerView

使用AVPlayerView是非常方便的,代码如下:

let videoComposition = AVMutableVideoComposition(
    asset: asset, 
    applyingCIFiltersWithHandler:
    { (request: AVAsynchronousCIImageFilteringRequest) -> Void in
        let filter = HDRZebraFilter()
        filter.inputImage = request.sourceImage
        let output = filter.outputImage

        if (output != nil) {
            request.finish(with: output, context: myCtx)
        }
        else { request.finish(with: err) }
    }
)

关键的对象是AVMutableVideoComposition,他通过一个video asset和一个block初始化。这个block传递过来一个AVAsynchronousCIImageFilteringRequest的request对象。block会在视频的每一帧被调用,你所需要做的就是创建一个CIFilter,设置好输入图像(也就是当前视频帧的图片request.sourceImage),拿到滤镜渲染好的图像传递给request对象就大功告成了。

同时你也可以通过debug过程的快速预览功能了解到更多渲染过程中的信息

WWDC20 CoreImage 专题_第14张图片

比如你可以看到输入的视频帧是一个10位的HDR图形。关于更多的debug技巧我们会在下文深入了解

WWDC20 CoreImage 专题_第15张图片

使用MTKView

第一步你需要做的是重写MTKView的init方法

class MyView : MTKView {
    var context: CIContext
    var commandQueue : MTLCommandQueue
    
    override init(frame frameRect: CGRect, device: MTLDevice?) {
        let dev = device ?? MTLCreateSystemDefaultDevice()!
        context = CIContext(mtlDevice: dev, options: [.cacheIntermediates : false] )
        commandQueue = dev.makeCommandQueue()!
        
        super.init(frame: frameRect, device: dev)

        framebufferOnly = false  // allow Core Image to use Metal Compute
        colorPixelFormat = MTLPixelFormat.rgba16Float
        if let caml = layer as? CAMetalLayer {
            caml.wantsExtendedDynamicRangeContent = true
        }
    }

在init过程中是我们创建CIContext的最佳过程,确保设置.cacheIntermediates为false,这样Core Image才能使用到Metal compute。在macOS中如果你的视图需要支持HDR,你需要设置colorPixelFormat为rgba16Float,同时设置caml.wantsExtendedDynamicRangeContent为true。接下来我们需要重写draw方法

func draw(in view: MTKView) {

        let size = self.convertToBacking(self.bounds.size)
        let rd = CIRenderDestination(width: Int(size.width),
                                     height: Int(size.height),
                                     pixelFormat: colorPixelFormat,
                                     commandBuffer: nil)
                  { () -> MTLTexture in return view.currentDrawable!.texture }

        context.startTask(toRender:image, from:rect, to:rd, at:point)

        // Present the current drawable
        let cmdBuf = commandQueue.makeCommandBuffer()!
        cmdBuf.present(view.currentDrawable!)
        cmdBuf.commit()
   }

在draw方法中我们需要通过特殊的方法创建CIRenderDestination对象,我们通过正确的宽高和像素格式来创建destination,但是过程中我们不是直接返回Metal纹理(texture),而是通过一个block的形式返回纹理。这个可以让CIContext在上一帧完成前就可以把Metal的任务入队,接下来我们告诉CIContext可以开始任务。最后一步就是创建一个命令缓冲区去将当前绘制的图像呈现到视图上。

探索更强大的 Core Image Debug 技巧

CI_PRINT_TREE 是什么

CI_PRINT_TREE基于的原理和为XCode提供Core Image预览功能的原理是一样的。例如对于下面代码,如果我们把鼠标聚焦在output上,会有一个浮窗展示这个变量的对象地址,如果再点击上面的眼睛图标,就能看到一个可视化的界面展示产生这个图片过程中的一些方法调用以及一些过程信息。

WWDC20 CoreImage 专题_第16张图片

而图片的快速预览功能只是CI_PRINT_TREE的一个基础应用,CI_PRINT_TREE是一个非常灵活的环境变量,他可以设置多种的模式和操作,让你可以了解到Core Image是如何优化和渲染图片的。

如何开启和控制CI_TREE_PRINT

开启CI_PRINT_TREE

  1. 通过编辑target-schema增加环境变量

WWDC20 CoreImage 专题_第17张图片
  1. 在启动APP之前通过终端开启CI_PRINT_TREE的环境变量

WWDC20 CoreImage 专题_第18张图片

控制CI_PRINT_TREE

CI_PRINT_TREE主要接收三个参数:graph typeoutput type以及options

WWDC20 CoreImage 专题_第19张图片
  • graph type

graph type表示了Core Image渲染过程的三个阶段:

1表示初始化阶段,这个阶段对于了解本次渲染使用了什么颜色空间是很有帮助的。

2表示图像优化阶段,可以看到core image的优化过程。

4表示Core Image把图像链接到GPU中的过程,这对于了解渲染过程中使用了多少个中间缓冲区很有帮助。

我们可以通过组合的方式同时打印上述几个阶段。例如:7表示3个阶段都打印,3表示初始化和优化阶段。

WWDC20 CoreImage 专题_第20张图片

WWDC20 CoreImage 专题_第21张图片

<<< 左右滑动见更多 >>>

  • output type

output type用来指定生成文档的输出类型。可以指定为pdf和png,最终输出的结果将会保存在macOS的缓存路径以及iOS的文档目录(Documents)。如果未指定输出类型,那文本将会以标准格式的压缩文本形式输出。同时你可以通过指定CI_LOG_FILE =“ oslog”将文本信息输出到Console.app里,在OS开发调试中这样会更加的方便。

WWDC20 CoreImage 专题_第22张图片
  • options

options可以指定很多选项来更精确的输出信息。例如:

指定context==name,可以只输出特定名字上下文的相关信息。

frame-n,可以只记录每个上下文的第n次的渲染过程。

剩下几个选项可以将输入图像中间图像以及输出图像都输出到文档中,这会提供很多有用的信息,但同时也会生成文档的时间和内存,确保在你需要这些信息的时候选择这些参数。

如何获取和理解CI_PRINT_TREE文件

如何获取CI_PRINT_TREE文件

在macOS中这将会非常容易,只要定位到缓存文件目录就可以看到生成的CI_PRINT_TREE文档:

WWDC20 CoreImage 专题_第23张图片

WWDC20 CoreImage 专题_第24张图片

<<< 左右滑动见更多 >>>

在iOS中首先要确保info.plist中的Application supports iTunes file sharing的值是YES

WWDC20 CoreImage 专题_第25张图片

然后连接手机到电脑,在Finder侧边栏找到你的设备并切换到files的窗口,这里就可以看到所有的CI_PRINT_TREE文件,然后复制到macOS中就可以了

WWDC20 CoreImage 专题_第26张图片

如何理解CI_PRINT_TREE文件

首先输出在底部,输入在顶部。绿色的节点是装饰内核(wrap kernels),红色的节点是色彩内核。

WWDC20 CoreImage 专题_第27张图片

在初始化的树种寻找颜色空间是很方便的,可以看到这里是HLG的颜色空间

WWDC20 CoreImage 专题_第28张图片

同时每个节点会显示他的ROI,表示“region of interest”,意思是这次渲染过程中每个节点需要的区域大小

WWDC20 CoreImage 专题_第29张图片

如果指定了graph type为4,且optionsdunp-intermediates。文档会输出除了输出通道以外的所有中间缓冲区,这对于定位渲染错误是如何发生是很有帮助的。

WWDC20 CoreImage 专题_第30张图片

同时可以看到每个过程中各个中间缓冲区的执行时间、像素数量和像素格式,这对于理解哪个过程消耗了更多的内存和时间是很有帮助的。

WWDC20 CoreImage 专题_第31张图片

WWDC20 CoreImage 专题_第32张图片

WWDC20 CoreImage 专题_第33张图片

<<< 左右滑动见更多 >>>

结语

本次Core Image的专题讲的东西其实不是很高深,但也需要一定的Core Image基础才能够更好的了解。而且其中对于很多知识点的描述都是比较简略的,如果大家对于这块感兴趣还是要去更详细的阅读CoreImage的官方专题。Core Image[8]

推荐阅读

#Metal

关注我们

我们是「老司机技术周报」,每周会发布一份关于 iOS 的周报,也会定期分享一些和 iOS 相关的技术。欢迎关注。

WWDC20 CoreImage 专题_第34张图片

关注有礼,关注【老司机技术周报】,回复「2020」,领取学习大礼包。

支持作者

这篇文章的内容来自于 《WWDC20 内参》。在这里给大家推荐一下这个专栏,专栏目前已经创作了 108 篇文章,只需要 29.9 元。点击【阅读原文】,就可以购买继续阅读 ~

WWDC 内参 系列是由老司机周报、知识小集合以及 SwiftGG 几个技术组织发起的。已经做了几年了,口碑一直不错。 主要是针对每年的 WWDC 的内容,做一次精选,并号召一群一线互联网的 iOS 开发者,结合自己的实际开发经验、苹果文档和视频内容做二次创作。

参考资料

[1]

Optimize the Core Image pipeline for your video app: https://developer.apple.com/videos/play/wwdc2020/10008/

[2]

Build Metal-based Core Image kernels with Xcode: https://developer.apple.com/videos/play/wwdc2020/10021/

[3]

Discover Core Image debugging techniques: https://developer.apple.com/videos/play/wwdc2020/10089/

[4]

苹果官方文档: https://developer.apple.com/documentation/coreimage/cicontextoption

[5]

Getting the Best Performance: https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/CoreImaging/ci_performance/ci_performance.html#//apple_ref/doc/uid/TP30001185-CH10-SW1

[6]

Processing an Image Using Built-in Filters: https://developer.apple.com/documentation/coreimage/processing_an_image_using_built-in_filters

[7]

Metal Shader Language For Core Image Kernels: https://developer.apple.com/metal/MetalCIKLReference6.pdf

[8]

Core Image: https://developer.apple.com/documentation/coreimage

你可能感兴趣的:(android,编程语言,python,linux,java)