WWDC 2018:Core Image - 更强的性能并支持基于 Python 快速开发

5,304 阅读8分钟

Session 719: Core Image: Performance, Prototyping, and Python

相信绝大多数 iOS 开发者对 Core Image 都不陌生,作为系统标配的、异常强大的图像处理库,在绝大多数场景下都能满足 App 的图像处理需求。而且,目前 Core Image 已经支持在 iOS 上做自定义 filter,颇有赶超 GPUImage 的态势(GPUImage 是目前 iOS 做图像处理事实上的标杆)。加上 iOS12 苹果打算 deprecate OpenGL 和 OpenGL ES, 推广 Metal。那和 Metal 联系紧密的 Core Image 无疑更有胜算。

这个 Session 讲的内容主要包括三个部分:

  1. Core Image 新的性能 API;
  2. 在 Core Image 体系上快速搭建滤镜原型;
  3. 在 Core Image 体系上应用机器学习;

1. Core Image 新的性能 API

中间缓存

在讲中间缓存之前,需要先复习一下 Core Image。在 Core Image 中,我们能够对图像链式的执行 Filter,如下图所示:

通过 Filter 的组合,我们可以实现一些复杂的图像处理效果。建立 Filter 链的方法可以参考如下代码( 节选自 Core Image Programming Guide):

func applyFilterChain(to image: CIImage) -> CIImage {
    // The CIPhotoEffectInstant filter takes only an input image
    let colorFilter = CIFilter(name: "CIPhotoEffectProcess", withInputParameters:
        [kCIInputImageKey: image])!
    
    // Pass the result of the color filter into the Bloom filter
    // and set its parameters for a glowy effect.
    let bloomImage = colorFilter.outputImage!.applyingFilter("CIBloom",
                                                             withInputParameters: [
                                                                kCIInputRadiusKey: 10.0,
                                                                kCIInputIntensityKey: 1.0
        ])
    
    // imageByCroppingToRect is a convenience method for
    // creating the CICrop filter and accessing its outputImage.
    let cropRect = CGRect(x: 350, y: 350, width: 150, height: 150)
    let croppedImage = bloomImage.cropping(to: cropRect)
    
    return croppedImage
}

整个过程很直观,我们将图片喂到第一个 Filter,然后得到第一个 Filter 的 outputImage,然后再把该对象喂到第二个 Filter……以此类推建立 Filter 链。

Core Image 的 Lazy

值得注意的一点是,当上述代码执行时,图像处理并没有发生,只是 Core Image 内部进行了一些关系的建立,只有当图像需要被渲染的时候,才会实际去执行各个 Filter 的图像处理过程。

因为有 Lazy 的特性,所以 Core Image 上最重要的一个优化就是 "自动连接(Filter Concatenation)", 因为最终图像处理的过程都发生在所有 Filter 成链之后。所以 Core Image 可以将链式的多个 Filter 合并 成一个来执行,省去不必要的开销。如下图所示:

中间缓存

现在回过头来看这样一个场景:

三个 Filter,第一个计算很耗时,而第三个的参数可以让用户手动调节。这意味着每次用户调节后都需要重新计算这三个 Filter。但其实前两份 Filter 的参数是不变的,也就是说前两个 Filter 的运算过程和结果都是不随着用户调整第三个 Filter 的参数改变而改变的。这里重复的计算是否有可能进行优化呢?

我们很容易就想到,我们只需要把前两次运算的结果 cache 下来就可以了,如下图所示:

但是上文提到,Core Image 会把 Filter 链自动合并为一个 Filter,我们如何访问中间结果呢?

苹果在 iOS12的 Core Image 中,给 CIImage 新增了一个中间缓存的属性(insertingIntermediate), 来解决这个问题,如下图所示:

我们希望保存第二个 Filter 的结果,只需要在第二个 Filter 的 outputImage 调用 insertingIntermediate() 来生成一个新的 CIImage 传到后面的流程即可。这样第三个 Filter 的参数调整就不会导致前两个 Filter 的重复计算。

怎么做的呢? 其实就是自动合并的逻辑会根据 insertingIntermediate 进行调整。如下图所示:

Core Image 的 CIContext 可以设置是否要打开 cacheIntermediate, 但这次新增的 insertingIntermediate 有更高的优先级。具体一些使用上的建议可以参考下图:

Kernal 语言的新特性

两种模式

iOS 上支持自定义 Filter,自定义 Filter 使用 Kernal 语言进行开发(一种类似 GLSL 的脚本语言)。目前一共有两种开发 CIKernel 程序的模式:

第一种是传统的基于 CIKernal 开发语言进行编写,然后编译成 Metal 或者 GLSL 的方式,第二种是直接使用 Metal Shading 语言进行开发,然后在 build 期间就生成二进制的库,执行阶段 load 之后直接转换为 GPU 的指令。

目前因为苹果主推 MPS(Metal Performance Shader), 所以方式一已经被标记为 deprecated

按组读写

使用 Metal 来开发 CIKernel 的优势:

  1. 支持半精度浮点数;
  2. 支持按组读写(Group read & Group write);

半精度浮点是纯运算性质方面的优化,在 A11 芯片上运算更快,而且因为用到的寄存器小所以也有较大的优化空间。

接下来重点介绍一下按组读写。

假设我们对左图红框像素做一个3x3的卷积运算,结果为存入右边的绿色框。显而易见,对于每个新的像素,都需要读取输入图像9次像素值。

但如果是按组读写,如下图所示。我们一次性读取16个像素来计算并写入右边的四个像素,那我们整个过程中写了4次,读取了16次。每个新像素平均需读取的数量为4,比上述的单像素需要9次显著降低。

按组读写的原理是很简单的,接下来介绍一下如果我们有一个之前使用 CIKernal Language 开发的 kernal,如果修改使其能够使用按组读写这样高速的优化。

假设我们的 kernal 如下图所示:

第一步,转换为 metal:

第二步, 改造为按组读写的模式。核心就是使用了 s.gatherX 来实现。

在使用了按组读写和半浮点经典的优化后,基本都可以得到2倍的性能提升。

2. 在 Core Image 体系上快速搭建滤镜原型

一般来讲,一个滤镜典型的研发流程是首先在电脑上进行快速原型的测试,之后再移植到生产环境,电脑上有大量的工具(OpenCV、SciPy、Numpy、Tensorflow 等等)来进行快速原型开发,而生产环境的技术栈却是 Core Image, vImage,Metal等完全不同的技术架构栈,这往往会导致一个问题:在电脑上原型测得好好的,结果到手机上效果却扑街了。

苹果为了解决这个问题,发布了一个神器 —— PyCoreImage。

初次看到这个名词是不是感到非常穿越? 但其实很明显,就是可以在 Python 中调用 Core Image 的接口

我们在 prototype 的时候使用 Python + PyCoreImage 这样的方式,那就最大程度的模拟了真实的运行环境,基本上移植到手机上效果也不会打折。而且最关键的是,只要学一个框架就好了啊,多的学不完啊!!!!

在使用 PyCoreImage 时,最关键是要了解 PyObjc 的用法,PyObjc 在 OS X 10.5 发布,实现了在 Python 可以调用 Objective-C 的代码,其中最主要的转换规则就是冒号变下划线,具体可以参考图中的例子。

说回 PyCoreImage,其中的原理其实大概也可以想到,如下图所示,PyCoreImage 通过 PyObjC 和 macOS 的 Core Image 进行交互,并将结果输出回 NumPy。

下图中的代码首先导入了一个图片,然后对其应用高斯模糊的 Filter,然后将结果输出到变量 bar 中。

剩下的更多关于 PyCoreImage 的用法可以参考 Session 的 ppt,这里不再赘述。

3. 在 Core Image 体系上应用机器学习

图像处理和计算机发展至今,已经大量通过使用机器学习和深度学习来提升算法的效果。Core Image 也对机器学习提供了非常有好的支持。

CoreML Filter

Core Image 现在可以直接将图片 apply 到一个 CoreML 的模型里,相对于给 Core Image 的 Filter 链接上了深度学习的能力。

iOS12 中的 Core Image 提供了 CICoreMLModelFilter 类来将 CoreML 的 model 封装成 Core Image 能够识别的 Filter 格式。

下图是一个 ML 领域的经典应用的例子(风格迁移)

不过现在在网上还完全搜不到 CICoreMLModelFilter ,(大雾

数据填补

对于机器学习而言,训练集的完整性、覆盖度能够很大程度上决定最后模型的精确程度。但是现实情况是,我们往往没有那么多训练集,在这样的情况下,学术界一般都采用对现有训练集的图片进行相应的变化来起到扩充数据集的作用。这类技术统称数据填补(Data Augmentation

Core Image 对于这类任务天生支持的很好,支持包括以下几种类型的变化:

  1. 图像外观;
  2. 图像噪声;
  3. 几何变换;

以下是几种使用 Core Image 的不同 Filter 来将一张图变多多张训练图片的例子:

小结

这个 Session 带来的内容总体来说还是激动人心的,虽然有的同学可能觉得比较小,没有那种颠覆式的创新,但对于从事图像领域工作的同学而言,毫无疑问这几个工作都给人一种 mind opener 的感觉,切实的反应了苹果对于多媒体、用户体验这两个领域非常超前的思考。