0154215daeb3ad454f1927ef6306ccde
Core Image【3】—— 2017 新特性

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

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

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

对应源码,见最末链接。


概述

先回顾一下 Core Image 目前强大的功能。

  • A simple, high-performance API to apply filters to images,提供简单使用,性能优秀的 API,以及内置各种 CIFiter,方便处理图片
  • Automatically tiles if images are large or graph is complex,大图处理优化
  • Automatically tiles if only a region of the output is rendered,只处理部分区域
  • Each CIFilter has one or more CIKernel functions,自定义 CIFliter
  • Multiple CIKernels are concatenated to improve performance,滤镜链延迟处理,合并成一个

这几点之前的文章都详细描述过了,这里不再说明。

2017 年,额外引入了一些新的东西,具体如下:

从三个方面讨论,性能,调试信息,新功能。

性能:

  • 支持使用 Metal 直接自定义 CIKernel,提高效率
  • 引入 CIRenderDestination,更方便,性能更好的渲染到指定目的地

信息:

  • CIRenderInfo,包含更多的信息
  • Quick Looks,支持 Core Image 多个对象直观调试

新功能:

  • 更多内置滤镜
  • 条码扫描支持
  • 与不同框架的协同处理

下面逐一展开说明。

性能

Metal

先回顾旧的 CIKernel 编写方式,之前的文章也提到过,Core Image 支持自定义 CIFilter,它们的脚本是通过 CIKernel Language 编写的, CIKernel Language 又基于 GLSL。

所以,当我们运行 App 时候,要用到这个 Filter,那么系统会自动帮我们把对应的 kernel,翻译成 GLSL 或者 Metal 规范的 kernel。然后再编译得到的 kernel。

所以之前的方式,存在两个问题:

  • 编写 kernel 的时候,没有报错提示,哪怕是参数名错误都无法检查处理。效率极低。
  • 翻译转换,编译,都是发生到运行时,导致第一次使用滤镜的时候,耗时较久。

关于耗时这点,具体如下:

这里的各个阶段分别指:

  • Translate CIKernels,转换 kernel,转成其他格式的。
  • Concatenate CIKernels,按序连接 kernel,滤镜链里头提到过
  • Compile CIKernels to Intermediate Representation,编译 CIKernel,这里的 IR(中间代码)我们无需关心,也干预不到
  • Compile to GPU Code,将 IR 转成 GPU 识别的代码
  • Render,在 GPU 上渲染

在旧的模式里面,这五步都是发生在运行时,且无法避免。

CIKernel 编译后会有缓存机制,所以耗时第一次较为明显。

这就导致了一个问题,你可能只需要渲染一次,显示带效果的图片。但是哪怕你的图片很小,也需要相当久的等待,因为需要对 CIKernel 进行转换编译。

进一步拆分,必须发生在运行时的,包含 Concatenate CIKernels,Compile to GPU Code 以及 Render,因为拼接滤镜可能是动态的,没法一开始就确定下来。

而占大头的前两部,并不是一定需要在运行时才能处理的。Metal 恰恰能解决。

将 Kernel 的编译时间,提前到 App 编译阶段,并且有语法错误检查,大大提高效率。

那么,具体怎么用 Metal 编写 CIKernel 呢,对比旧的流程,有什么差异呢?下面举个实际例子。将上一篇文章里面实现的 Vignette, 改用 Metal 处理,便于参照。

Write CIKernel in Metal shader file

CIKL(CIKernel Language) 和 Metal 本质上是很相似的,基础语法都是一样的。

关于语法类的东西,这里不细说,具体可以参照官方说明来。MetalCIKLReference

这里提一点。CIKL 之前为了特性,扩展的那些支持, Metal 也同样支持。具体的转换规则如下:

所以不同类型的 CIKernel,它们的简单转换应该是这样:

CIWarpKernel

CIColorKernel

CIKernel:

基本上,差异都体现在额外扩展的这些内容。实际的算法编写,基本不变。

我们以之前实现的 vignetteKernel 为例,Vignette.cikernel 白板代码如下:

kernel vec4 vignetteKernel(__sample image, vec2 center, float radius, float alpha)
{
    // 计算出当前点与中心的距离
    float distance = distance(destCoord(), center) ;
    // 根据距离计算出暗淡程度
    float darken = 1.0 - (distance / radius * alpha);
    // 返回该像素点最终的色值
    image.rgb *= darken;
    return image.rgba;
}

转换成 Metal 应该是:Vignette.metal

#include <metal_stdlib>
using namespace metal;

#include <CoreImage/CoreImage.h> // includes CIKernelMetalLib.h

extern "C" { namespace coreimage {
    float4 vignetteMetal(sample_t image, float2 center, float radius, float alpha, destination dest) {
        // 计算出当前点与中心的距离
        float distance2 = distance(dest.coord(), center);

        // 根据距离计算出暗淡程度
        float darken = 1.0 - (distance2 / radius * alpha);
        // 返回该像素点最终的色值
        image.rgb *= darken;

        return image.rgba;
    }
}}

这里有几个改变点逐一说下:

#include <metal_stdlib>
using namespace metal;

#include <CoreImage/CoreImage.h> // includes CIKernelMetalLib.h

extern "C" { namespace coreimage {
}}

这里需要引入对应的库,以及命名空间。因为系统内部的实现大致是这样的:

这基本是固定的格式,保持就好。

然后就是特定的修改:

  • __sample —> sample_t
  • vec2 — > float2
  • destCoord() —> dest.coord()
  • vec4 —> float4

这里注意,Metal 不支持 vec 类型,参数类型都需要转成浮点值类型。

另外,入参这里,多了一个 destination dest,这个对应 CIColorKernel 是可选的,因为并不一定要获取当前的坐标,正常像素值就够了。

如果要带的话,它是隐式的,必须放在参数列表最后一个,无须我们传参,系统自动赋值。这点需要额外注意!

至此,shader 的编写就结束了,也是很好理解。

Compile and link Metal shader file

至于编译,Xcode 默认是不会帮我们编译 CIKernel 对应的 Metal 文件,需要我们显示的去设置。

具体步骤如下:

Build Settings 里头找到 Other Metal Compiler Flags,添加值:-fcikernel

然后新增一个自定义配置

对应的 Key 为: MTLLINKER_FLAGS,value 为:-cikernel

PS:

如果没添加对应的编译选项,下一步初始化 CIKernel 的时候,会失败。

Initialize CIKernel

这里同样对比旧的创建方式,

NSBundle *bundle = [NSBundle bundleForClass: [self class]];
NSURL *kernelURL = [bundle URLForResource:@"Vignette" withExtension:@"cikernel"];

NSError *error;
NSString *kernelCode = [NSString stringWithContentsOfURL:kernelURL
                                                encoding:NSUTF8StringEncoding
                                                   error:&error];
NSArray *kernels = [CIColorKernel kernelsWithString:kernelCode];
customKernel = [kernels objectAtIndex:0];

只需要改为:

NSURL *kernelURL = [[NSBundle mainBundle] URLForResource:@"default" withExtension:@"metallib"];
NSError *error;
NSData *data = [NSData dataWithContentsOfURL:kernelURL];
customKernel = [CIColorKernel kernelWithFunctionName:@"vignetteMetal"
                                fromMetalLibraryData:data
                                               error:&error];

初始化方法不一样,在使用上是一致的。

至此,通过 Metal 自定义 CIFilter 的流程,已经全部走通了。对旧有的修改很小。
这里额外提一点,UIImageView 针对 CIImage 有做优化,如果一个 UIImage 是通过 UIImage.init(ciImage:) 这种方式创建的,

设置到 UIImageView 上的时候,UIImageView 会在 GPU 上执行 Core Image 相关操作。GPU 处理很高效,并且能释放 CPU 压力。

所以,实时调整 Filter 的时候,也可以借助 UIImageView 来直接显示,效率很高:

@interface MetalKernelViewController ()

@property (strong, nonatomic) MetalKernelFilter *vignetteFilter;
@property (strong, nonatomic) CIImage *inputImage;
@property (strong, nonatomic) IBOutlet UIImageView *imageView;

@end

@implementation MetalKernelViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    // 初始化 Filter
    self.vignetteFilter = [[MetalKernelFilter alloc] init];
    NSURL *imageURL = [[NSBundle mainBundle] URLForResource:@"vignetteImage" withExtension:@"jpg"];
    self.inputImage = [CIImage imageWithContentsOfURL:imageURL];
    [self.vignetteFilter setValue:_inputImage forKey:@"inputImage"];

    self.imageView.image = [UIImage imageWithCIImage:self.inputImage];

}

#pragma mark - Action
- (IBAction)alphaChanged:(UISlider *)sender {
    [self.vignetteFilter setValue:@(sender.value) forKey:@"inputAlpha"];
    CIImage *result = _vignetteFilter.outputImage;
    self.imageView.image = [UIImage imageWithCIImage:result];
}

@end

CIRenderDestination

top Created with Sketch.