C946c75325ea65f82f703c990c29a115
Core Image【4】—— 2018 新特性

Core Image 系列,目前的文章如下:

  • Core Image【1】—— 概述
  • Core Image【2】—— 自定义 Filter
  • Core Image【3】—— 2017 新特性
  • Core Image【4】—— 2018 新特性

如果想了解 Core Image 相关,建议按序阅读,前后有依赖。


概述

2018,Core Image 主要更新了三个点:性能优化原型开发以及与 CoreML 的结合

整体更新的不多,但都比较有意思。下面逐一详细阐述。

Performance

性能方面,2018 主要更新了两点:

  • intermediate buffers,中间缓存
  • new CIKernel Language features,CIKernel 新特性,按组读写

intermediate buffers

首先,回顾下 Core Image 滤镜链现有的工作方式:

我们可以通过这样首尾相连的方式,组合不同的 Filter,达到我们想要的效果,各个 Filter 又对应着各自的 Kernel,我们也可以自定义 Kernel。

PS:

关于如何使用滤镜链,以及如何自定义 Filter,如果有不了解的,可以翻看前两篇文章。

这里不再阐述。

Core Image 为了性能,包括处理速度,以及降低内存,会自动把整个滤镜链,优化成一个 Filter 处理,如下:

这可以说是 Core Image 很厉害的地方,也正因为这个特性,Core Image 在处理滤镜链上,性能比 GPUImage 要好得多。

PS:

GPUImage 的做法,和我们平时的处理方式一致,是按序处理的,没有优化,即 原图— Filter1 —> 结果图1 — Filter2 —> 结果图2 —> Filter3 —> 结果图 这样的一个过程。这期间产生了两个中间缓存,即 结果图1结果图2

当然,Core Image 减少中间缓存,提高性能,绝大多数情况下,都是非常棒的,但也有一些情况下,需要额外去修改,扩展。

比如这样一个场景:

滤镜链里面的三个滤镜,Sharpen,Hue,Contrast。

其中,Sharpen 操作是比较耗时的,并且,用户能动态修改的,只有 Contrast。

所以如果按照常规的做法,每次修改 Contrast 程度值的时候,都重新跑一遍滤镜链,毫无疑问,会造成不必要的性能损耗。因为 Sharpen + Hue 这两个效果,任何情况下,出来的结果图都是一样的(因为没改变这两个 Filter 的程度值),并且它们本身也是比较耗时的。

当然,在介绍 Core Image 新功能之前,我们之前遇到类似的问题,是怎么解决的呢?

很明显,要引入一个临时的 CIImage,接收 Sharpen + Hue 处理出来的 outputImage。然后作为 inputImage,输入给 Contrast Filter。接下去的操作,都只处理 Contrast,它的 inputImage 暂存,固定不变。也就是将原有的一个滤镜链,拆分成两个。

当然,这种做法是可行的,也是目前的通用方式。只是在代码逻辑维护上,需要额外的成本。

那么,Core Image 会如何优化这个问题呢?

它的做法,其实和我们之前提到的一样,iOS 12 之后,CIImage 新增了一个方法,insertingIntermediate

func insertingIntermediate() -> CIImage
// Returns a new image created by inserting an intermediate.

Hue 得到的 outputImage,调用 insertingIntermediate() 方法,再作为 inputImage 传入 Contrast,优化后的流程如下:

自动组合的逻辑,会根据 insertingIntermediate 做调整。这里,前两个 Filter 自动组合了。

对比我们自己维护多个滤镜链和系统提供的 insertingIntermediate,效率上应该是没什么差异,只是系统的使用起来更加方便罢了。

另外,使用上还要注意什么时候需要缓存,什么时候不需要缓存。Core Image 给我们提供了相关的属性,方便我们控制。

static let cacheIntermediates: CIContextOption
// The value for this key is an NSNumber object containing a Boolean value. If this value is false, the context empties such buffers during and after renders. The default value is true.

func insertingIntermediate(cache: Bool) -> CIImage
// Intermediate buffers created through setting cache to true have a higher priority than others. This setting is independent of of CIContext's cacheIntermediates option.

默认是缓存所有的 Intermediates,如果不需要,可以强制关闭。

let context = CIContext(options: [.cacheIntermediates: false] );

当然,CIContext 整体设置不缓存后,也可以针对个别 Intermediates 单独开启,下面的优先级更高

image.insertingIntermediate(cache: true);

总之,决定权在你自己。

new CIKernel Language features

在 2017 新特性中,我们提到过,CIKernel 支持 Metal 直接编写。所以目前自定义 Filter 有这么两种方式:

PS:
这两种之前的文章中都已经详细阐述了,这里不再说明,有疑惑的可以翻看之前的。

但是 iOS 12 之后,主推 Metal,不仅 OpenGL ES 被弃用,这里的 CIKernel Language 编写方式,也被弃用。当然,Metal 的性能优势还是很明显的,所以尽可能的使用 Metal,也是合理的。

另外,CIKernel 还有两点比较重要的性能优化:

  • Half float support
  • Group reads

Half float support,支持半精度浮点数。

很多时候,half float 精度处理出来的效果,是足够好的,比如处理 RGB 的时候。

通过降低精度,使得运行速度变得更快,尤其是在 A11 芯片上。
另外,half float 的另一个优点是它可以使用更小的寄存器,从而能更充分的利用 GPU,进而提升效率。

接下去,重点分析下按组读写。

给 shader 提供了新的接口,实现单通道每次读取 4 个像素点,已经每次写入 4 个目标像素值。

这里举了一个卷积操作为例说明。

PS:

在图像处理中,卷积操作指的是使用一个卷积核对图像中的每个像素进行一系列操作。

卷积核(算子)是用来做图像处理时的矩阵,图像处理时也称为掩膜,是与原图像做运算的参数。

卷积核通常是一个四方形的网格结构(例如3x3的矩阵或像素区域),该区域上每个方格都有一个权重值。

使用卷积进行计算时,需要将卷积核的中心放置在要计算的像素上,一次计算核中每个元素和其覆盖的图像像素值的乘积并求和,得到的结构就是该位置的新像素值。

这里不细说,感兴趣的可以了解下 Convolutional Neural Networks。

如下图所示,展示了一个 3x3 的卷积核在 5x5 的图像上做卷积的过程。每个卷积都是一种特征提取方式,就像一个筛子,将图像中符合条件(激活值越大越符合条件)的部分筛选出来。卷积神经网络

卷积神经网络

卷积在图像处理中最常见的应用为锐化和边缘提取。 感兴趣可以查阅下 kGPUImageSharpenFragmentShaderString,锐化算法就是根据周围区域像素值,计算得到中心区域最终像素值。

想象一下,我们有一个 3x3 的单通道卷积运算。按照上图的描述,我们每计算一次目标位置的像素值,就需要 9 次读操作,和 1 次写操作,如下:

再复杂点,如果需要获取 4 个位置的像素值,按照之前逐个读写的方式,那么就需要 36 次的读操作,和 4 次的写操作。但是仔细观察最终读的区域,其实是有很大一部分重复的。如果能一次写入 4 个像素值,那么实际上只有 16 个位置的像素值需要读取。

而 CIkernel 的一个新特性就是,支持按组写,优化这部分性能,所以上述操作,可以同时进行 16 次读操作,4 次写操作。

再进一步,CIKernel 还支持按组读,所以 16 次读操作,可以细分成 4 组来完成。最终,一次操作会完成:4 组 读,4 次写。

具体实践起来,kernel 代码如下:(这里是 r 通道的处理)

top Created with Sketch.