Bc34618958c95919726c9f9d1b7e9d13
WWDC2020 - Core Image专题

概览

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

这几篇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 的建议

每个视图只创建一个 CIContext

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

通过 CIContextOption 创建 CIContext

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

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

结合使用 Metal 和 CIContext

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

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

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


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

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

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

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

尽量使用内置的 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

编写并应用自定义的 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结尾的二进制文件。


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

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

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

编写Metal内核

本次使用的内核是在SessionEdit and Playback HDR video with AVFoundation中使用的内核

在这个Demo中我们实现了一种斑马条纹的效果,可以突出显示HDR视频的明亮部分、扩展其中的中心区域。
下面我们通过代码来演示如何通过自定义内核实现这个效果:

  • 首先引入CoreImage的头文件
  • 定义这个效果的函数,这个函数必须标识extern "C"(表示这个函数会使用C语言编译)
// MyKernels.ci.metal
#include <CoreImage/CoreImage.h> // 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来确定处于哪个对角线,然后通过一些简单的计算判断是否处于斑马纹上,且这个像素的亮度超过了正常亮度标准,我们就返回一个亮红色的像素。其他情况就返回原像素。
最终实现的效果就是如下这样:

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

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

通过以下代码我们就能加载内核并用它来创建图片
```Swift
class HDRZebraFilter: CIFilter {
var inputImage: CIImage?
var inputTime: Float = 0.0

top Created with Sketch.