探索 Core Image Kernel 改进
Explore Core Image kernel improvements
2021年6月9日
一句话判断
Core Image 的自定义 Kernel 终于不再是一个需要用 CIKL(Core Image Kernel Language)写的外星 API 了——现在你可以直接用 Metal Shading Language 编写自定义滤镜,调试体验和普通 Metal shader 完全一致。
这场 Session 讲了什么
Session 介绍了 Core Image 框架在自定义图像处理 Kernel 方面的重大改进。过去写 Core Image 自定义滤镜需要使用 CIKL(Core Image Kernel Language),一种基于 GLSL 的专有语言。现在 Core Image 直接支持 Metal Shading Language 编写自定义 Kernel,这意味着你可以在同一个项目中使用统一的 Metal 工具链。
三种 Kernel 类型都得到了更新:CIColorKernel(颜色变换,逐像素处理)、CIWarpKernel(几何变换,改变像素位置)、CIBlendKernel(混合两个图像)。新的 API 使用 CIImageProcessorKernel 的 Metal 版本来替代旧的 CIKL 方式。Session 还介绍了 Core Image 对 Apple Silicon 的优化——自定义 Kernel 会自动被编译为针对 GPU 的最优代码,不需要手动配置 compute pipeline。
值得深挖的点
从 CIKL 迁移到 Metal 的实际收益
CIKL 是一种功能受限的语言——不支持 buffer 参数、不支持数组、不支持 threadgroup 内存。迁移到 Metal Shading Language 后,自定义滤镜可以使用 Metal 的全部能力:共享内存、SIMD 向量操作、甚至 ray tracing。实际性能提升取决于 Kernel 的复杂度:简单的颜色变换差异不大(CIKL 已经足够),但涉及多输入图像或需要共享内存的复杂操作,Metal 版本可以快 2-5 倍。
ROI(Region of Interest)计算的简化
自定义 Kernel 的一个痛点是 ROI 计算——告诉 Core Image 你的滤镜需要读取输入图像的哪个区域。旧的 CIKL 方式需要在 Objective-C 代码中手动实现 regionOfInterestForBound: 方法。新的 Metal Kernel 通过 DOD(Domain of Definition)和 ROI 属性声明,让 Core Image 自动计算需要的采样区域,大多数情况下不需要手写 ROI 逻辑。
代码片段
// 使用 Metal Shading Language 编写自定义 Core Image Kernel
// 这是一个颜色变换滤镜,将图像转换为棕褐色调
#include <CoreImage/CoreImage.h> // Core Image 的 Metal 头文件
extern "C" {
// 命名空间 ci 是 Core Image Metal Kernel 的约定
namespace coreimage {
// CIColorKernel:逐像素颜色变换
float4 sepiaTone(sample_h s, float intensity) {
float3 color = s.rgb();
// 棕褐色调的颜色变换矩阵
float3 sepia = float3(
color.r * 0.393 + color.g * 0.769 + color.b * 0.189,
color.r * 0.349 + color.g * 0.686 + color.b * 0.168,
color.r * 0.272 + color.g * 0.534 + color.b * 0.131
);
// 在原图和棕褐色之间按 intensity 插值
float3 result = mix(color, sepia, intensity);
return float4(result, s.a());
}
}
}
// 在 Swift 中注册和使用 Metal Kernel
class SepiaFilter: CIFilter {
private static var kernel: CIKernel?
var inputImage: CIImage?
var inputIntensity: CGFloat = 0.8
override class func register() {
// 从 Metal 文件加载 Kernel
let url = Bundle.main.url(forResource: "SepiaKernel",
withExtension: "ci.metallib")!
let data = try! Data(contentsOf: url)
kernel = try! CIKernel(functionName: "sepiaTone",
fromMetalLibraryData: data)
}
override var outputImage: CIImage? {
guard let input = inputImage,
let kernel = SepiaFilter.kernel else { return nil }
return kernel.apply(
extent: input.extent,
roiCallback: { _, rect in return rect }, // ROI 等于输出区域
arguments: [input, inputIntensity]
)
}
}
// CIWarpKernel 示例:漩涡扭曲效果
// Metal Shading Language
extern "C" {
namespace coreimage {
// Warp Kernel 返回新的采样坐标
float2 swirlWarp(destination dest, float radius, float angle) {
float2 center = dest.extent().xy / 2.0;
float2 delta = dest.coord() - center;
float dist = length(delta);
if (dist < radius) {
// 距离中心越近旋转角度越大
float percent = (radius - dist) / radius;
float theta = percent * percent * angle;
float s = sin(theta);
float c = cos(theta);
delta = float2(
delta.x * c - delta.y * s,
delta.x * s + delta.y * c
);
}
return center + delta;
}
}
}
最佳实践
- 新项目直接用 Metal Shading Language 编写 Core Image Kernel,不要再使用 CIKL。CIKL 已经没有被弃用,但 Metal 版本是明确的未来方向。
- 将
.ci.metallib的编译产物加入 build phase,确保 Metal Kernel 在编译时就被验证。运行时才发现 Kernel 语法错误太晚了。 - 自定义 Kernel 的
roiCallback不要图省事返回整张图像——只返回真正需要的区域,可以减少不必要的 GPU 采样操作。
还有什么值得关注
- Core Image 的内置滤镜(CIFilter)总数超过了 230 个,覆盖了绝大多数常见的图像处理需求——写自定义 Kernel 之前先检查是否有内置替代。
CIImageProcessorKernel支持在 CPU 上执行,适合某些 GPU 不擅长的操作(比如条件分支密集的算法)。- Xcode 13 的 Metal Shader Profiler 可以直接 profile Core Image 的 Metal Kernel,之前这是做不到的。