CoreImageExtensions

Apple Core Image 框架的实用扩展。

异步渲染

CIContext 的几乎所有渲染 API 都是同步的,即它们会阻塞当前线程直到渲染完成。在许多情况下,特别是从主线程调用时,这是不可取的。

我们为 CIContext 添加了一个扩展,通过包装的 actor 实例添加了所有渲染 API 的 async 版本。可以通过 async 属性访问该 actor

let cgImage = await context.async.createCGImage(ciImage, from: ciImage.extent)

注意: 尽管它们已经是异步的,但即使是用于处理 CIRenderDestination 的 API,例如 startTask(toRender:to:),也将受益于使用 async 版本。这是因为 Core Image 会在将渲染工作交给 GPU 之前,分析应该应用于给定图像的滤镜图。特别是对于更复杂的滤镜管道,这种分析可能非常耗时,最好在后台队列中执行,以避免阻塞主线程。

我们还为与 CIRenderDestination 相关的 API 添加了异步替代方案,这些 API 等待任务执行并返回 CIRenderInfo 对象

let info = try await context.async.render(image, from: rect, to: destination, at: point)
let info = try await context.async.render(image, to: destination)
let info = try await context.async.clear(destination)

图像查找

我们为 CIImage 添加了一个便捷的初始化器,您可以使用它通过名称从资产目录或直接从 bundle 加载图像

let image = CIImage(named: "myImage")

这提供了与相应的 UIImage 方法相同的签名

// on iOS, Catalyst, tvOS
init?(named name: String, in bundle: Bundle? = nil, compatibleWith traitCollection: UITraitCollection? = nil)

// on macOS
init?(named name: String, in bundle: Bundle? = nil)

具有固定值的图像

在 Core Image 中,您可以使用 CIImageinit(color: CIColor) 初始化器创建一个具有无限范围的图像,该图像仅包含具有给定颜色的像素。但是,这仅允许创建填充 [0…1] 范围内值的图像,因为 CIColor 会将值钳制到此范围。

我们在 CIImage 上添加了两个新的工厂方法,允许创建填充任意值的图像

/// Returns a `CIImage` with infinite extent only containing the given pixel value.
static func containing(values: CIVector) -> CIImage?

/// Returns a `CIImage` with infinite extent only containing the given value in RGB and alpha 1.
/// So `CIImage.containing(42.3)` would result in an image containing the value (42.3, 42.3, 42.3, 1.0) in each pixel.
static func containing(value: Double) -> CIImage?

这很有用,例如,用于将标量值传递到混合滤镜中。例如,这将创建 RGB 中的颜色反转效果

var inverted = CIBlendKernel.multiply.apply(foreground: image, background: CIImage.containing(value: -1)!)!
inverted = CIBlendKernel.componentAdd.apply(foreground: inverted, background: CIImage.containing(value: 1)!)!

图像值访问

访问 CIImage 的实际像素值可能相当复杂。图像需要先渲染,然后才能正确访问生成的位图内存。

我们为 CIContext 添加了一些便捷方法,只需一行代码即可完成此操作

// get all pixel values of `image` as an array of `SIMD4<UInt8>` values:
let values = context.readUInt8PixelValues(from: image, in: image.extent)
let red: UInt8 = values[42].r // for instance

// get the value of a specific pixel as a `SIMD4<Float32>`:
let value = context.readFloat32PixelValue(from: image, at: CGPoint.zero)
let green: Float32 = value.g // for instance

这些方法有多种变体,用于访问像素区域(在给定的 CGRect 中)或单个像素(在给定的 CGPoint 处)。它们也适用于三种不同的数据类型:UInt8(正常的每通道 8 位格式,范围为 [0…255]),Float32(也称为 float,包含任意值,但颜色通常映射到 [0...1]),以及 Float16(仅在 iOS 上)。

注意: 也提供 async 版本。

OpenEXR 支持

OpenEXR 是一个开放标准,用于存储超出“正常”图像颜色数据的任意位图数据,例如 32 位高动态范围数据或负浮点值(例如用于高度场)。

尽管 Image I/O 本身支持 EXR 格式,但 Core Image 没有提供将 CIImage 渲染为 EXR 的便捷方法。我们为 CIContext 添加了相应的 EXR 导出方法,这些方法与为其他文件格式提供的 API 对齐

// to create a `Data` object containing a 16-bit float EXR representation:
let exrData = try context.exrRepresentation(of: image, format: .RGBAh)

// to write a 32-bit float representation to an EXR file at `url`:
try context.writeEXRRepresentation(of: image, to: url, format: .RGBAf)

要将 EXR 文件读取到 CIImage 中,可以使用常用的初始化器,例如 CIImage(contentsOf: url)CIImage(named: “myImage.exr”(见上文)。

注意: 也提供 async 版本。

OpenEXR 测试图像

本项目中使用的所有 EXR 测试图像均取自 此处

图像变换

我们为 CIImage 添加了一些便捷方法,只需一行代码即可在图像上执行常见的仿射变换(而不是使用 CGAffineTransform

// Scaling the image by the given factors in x- and y-direction.
let scaledImage = image.scaledBy(x: 0.5, y: 2.0)
// Translating the image within the working space by the given amount in x- and y-direction.
let translatedImage = image.translatedBy(dx: 42, dy: -321)
// Moving the image's origin within the working space to the given point.
let movedImage = image.moved(to: CGPoint(x: 50, y: 100))
// Moving the center of the image's extent to the given point.
let centeredImage = image.centered(in: .zero)
// Adding a padding of clear pixels around the image, effectively increasing its virtual extent.
let paddedImage = image.paddedBy(dx: 20, dy: 60)

您还可以像这样为图像添加圆角(透明)

let imageWithRoundedCorners = image.withRoundedCorners(radius: 5)

图像合成

我们添加了便捷的 API,用于使用不同的混合内核合成两个图像(不仅仅是内置 CIImage.composited(over:) API 中的 sourceOver

// Compositing the image over the specified background image using the given blend kernel.
let composition = image.composited(over: otherImage, using: .multiply)
// Compositing the image over the specified background image using the given blend kernel in the given color space.
let composition = image.composited(over: otherImage, using: .softLight, colorSpace: .displayP3ColorSpace)

您还可以像这样轻松地为图像着色(即,将所有可见像素变成给定的颜色)

// Colorizes visible pixels of the image in the given CIColor.
let colorized = image.colorized(with: .red)

颜色扩展

CIColor 通常将其组件值钳制到 [0...1],这在处理广色域和/或扩展动态范围 (EDR) 颜色时是不切实际的。可以通过使用允许组件值超出这些范围的扩展颜色空间初始化颜色来解决此问题。我们添加了一些便捷的扩展,用于使用(扩展的)白色和颜色值初始化颜色。扩展颜色将在线性 sRGB 中定义,这意味着 1.0 的值将匹配 sRGB 中的最大组件值。超出此范围的一切都被认为是广色域。

// Convenience initializer for standard linear sRGB 50% gray.
let gray = CIColor(white: 0.5)
// A bright EDR white, way outside of the standard sRGB range.
let brightWhite = CIColor(extendedWhite: 2.0)
// A bright red color, way outside of the standard sRGB range.
// It will likely be clipped to the maximum value of the target color space when rendering.
let brightRed = CIColor(extendedRed: 2.0, green: 0.0, blue: 0.0)

我们还添加了一个便捷属性,用于获取与当前颜色叠加时清晰可见的对比色(黑色或白色)。例如,这可以用于为文本标签叠加层着色。

// A color that provide a high contrast to `backgroundColor`.
let labelColor = backgroundColor.contrastColor

颜色空间便捷性

CGColorSpace 通常通过其名称初始化,例如 CGColorSpace(name: CGColorSpace.extendedLinearSRGB)。由于这相当长,我们为方便起见,为使用 Core Image 时最常用的颜色空间添加了一些静态访问器

CGColorSpace.sRGBColorSpace
CGColorSpace.extendedLinearSRGBColorSpace
CGColorSpace.displayP3ColorSpace
CGColorSpace.extendedLinearDisplayP3ColorSpace
CGColorSpace.itur2020ColorSpace
CGColorSpace.extendedLinearITUR2020ColorSpace
CGColorSpace.itur2100HLGColorSpace
CGColorSpace.itur2100PQColorSpace

这些可以很好地内联使用,如下所示

let color = CIColor(red: 1.0, green: 0.5, blue: 0.0, colorSpace: .displayP3ColorSpace)

文本生成

Core Image 可以使用 CITextImageGeneratorCIAttributedTextImageGenerator 生成包含文本的图像。我们添加了扩展,使它们使用起来更加方便

// Generating a text image with default settings.
let textImage = CIImage.text("This is text")
// Generating a text image with adjust text settings.
let textImage = CIImage.text("This is text", fontName: "Arial", fontSize: 24, color: .white, padding: 10)
// Generating a text image with a `UIFont` or `NSFont`.
let textImage = CIImage.text("This is text", font: someFont, color: .red, padding: 42)
// Generating a text image with an attributed string.
let attributedTextImage = CIImage.attributedText(someAttributedString, padding: 10)

从 Metal 源代码进行运行时内核编译

使用旧版 Core Image Kernel Language,可以(甚至必须)在运行时从 CIKL 源代码字符串编译自定义内核例程。对于用 Metal 编写的自定义内核,需要在构建时与其余源代码一起编译源代码(使用特定标志)。虽然这具有编译时源代码检查和运行时性能大幅提升的巨大优势,但它也失去了一些灵活性。最值得注意的是在原型设计方面,因为设置 Core Image Metal 构建工具链相当复杂,并且加载预编译内核需要一些样板代码。

然而,在 iOS 15 和 macOS 12 中新增了使用 CIKernel.kernels(withMetalString:) API 在运行时编译基于 Metal 的内核的能力。但是,此 API 需要一些类型检查和样板代码才能检索适当类型的实际 CIKernel 实例。因此,我们添加了以下便捷 API 以简化此过程

let metalKernelCode = """
    #include <CoreImage/CoreImage.h>
    using namespace metal;

    [[ stitchable ]] half4 general(coreimage::sampler_h src) {
        return src.sample(src.coord());
    }
    [[ stitchable ]] half4 otherGeneral(coreimage::sampler_h src) {
        return src.sample(src.coord());
    }
    [[ stitchable ]] half4 color(coreimage::sample_h src) {
        return src;
    }
    [[ stitchable ]] float2 warp(coreimage::destination dest) {
        return dest.coord();
    }
"""
// Load the first kernel that matches the type (CIKernel) from the metal sources.
let generalKernel = try CIKernel.kernel(withMetalString: metalKernelCode) // loads "general" kernel function
// Load the kernel with a specific function name.
let otherGeneralKernel = try CIKernel.kernel(withMetalString: metalKernelCode, kernelName: "otherGeneral")
// Load the first color kernel from the metal sources.
let colorKernel = try CIColorKernel.kernel(withMetalString: metalKernelCode) // loads "color" kernel function
// Load the first warp kernel from the metal sources.
let colorKernel = try CIWarp.kernel(withMetalString: metalKernelCode) // loads "warp" kernel function

⚠️ 重要提示: 此 API 有一些限制

如果您的最低部署目标尚不支持 Metal 内核的运行时编译,则可以使用以下 API 代替。它允许提供 CIKL 中的备份内核实现,该实现用于较旧的系统,在这些系统中不支持 Metal 运行时编译

let metalKernelCode = """
    #include <CoreImage/CoreImage.h>
    using namespace metal;

    [[ stitchable ]] half4 general(coreimage::sampler_h src) {
        return src.sample(src.coord());
    }
"""
let ciklKernelCode = """
    kernel vec4 general(sampler src) {
        return sample(src, samplerTransform(src, destCoord()));
    }
"""
let kernel = try CIKernel.kernel(withMetalString: metalKernelCode, fallbackCIKLString: ciklKernelCode)

注意: 通常,更好的做法是将 Metal CIKernel 与其余代码一起编译,并且仅将运行时编译用作例外情况。这样,编译器可以在构建时检查您的源代码,并且从预编译的源代码在运行时初始化 CIKernel 会更快得多。一个值得注意的例外情况可能是当您需要在 Swift 包中使用自定义内核时,因为 CI Metal 内核尚无法与 Swift 包一起构建。但这应该仅用作最后的手段。

调试扩展

⚠️ 警告: 以下扩展和 API 仅供在 DEBUG 模式下使用!其中一些访问内部 Core Image API,并且通常不是为了优雅地失败而编写的。这就是为什么它们仅在启用 DEBUG 编译时可用。

所有调试帮助都可以通过 DebugProxy 对象访问,该对象可以通过调用 ciImage.debug 来访问,它使用 Core Image 中的内部调试 CIContext。或者,调试上下文也可以通过 ciImage.debug(with: myContext) 指定。

渲染图像并获取渲染信息

在调试期间使用 QuickLook 检查 CIImage 时,Core Image 将首先渲染图像及其滤镜图,并在预览中一起呈现。对于复杂的滤镜图,这可能会产生不希望看到的杂乱预览,甚至由于处理时间过长而无法显示 QuickLook 预览。

如果您只想查看渲染后的图像,可以使用以下访问器,它仅渲染图像并将其作为 CGImage 返回,这应该可以很好地预览

let cgImage = ciImage.debug.cgImage

如果您想了解有关 Core Image 在渲染图像时内部发生情况的更多信息,可以使用以下方法获取调试 RenderInfo

let renderInfo = ciImage.debug.render() // with optional outputColorSpace parameter

返回的 RenderInfo 包含有关渲染过程的信息

还有其他访问器可以获取不同类型的滤镜图,类似于 CI_PRINT_TREE 可以生成的内容

获取图像统计信息

以下 API 可用于访问有关图像的一些基本统计信息(minmaxavg 像素值)

let stats = image.debug.statistics(in: region, colorSpace: .sRGBColorSpace) // both parameters optional
print(stats)
// min: (r:  0.076, g:  0.076, b:  0.076, a:  1.000)
// max: (r:  1.004, g:  1.004, b:  1.004, a:  1.000)
// avg: (r:  0.676, g:  0.671, b:  0.671, a:  1.000)

请记住,colorSpace 会影响像素值及其值范围。如果未指定颜色空间,则使用调试上下文的 workingColorSpace

导出图像及其 RenderInfo

CI_PRINT_TREE 是一个很棒的调试工具,但也有一些主要缺点

以下 API 可用于随时轻松导出/保存图像

// simple, exporting as 8-bit TIFF in context working space:
ciImage.debug.export()`
// exports the image as file "<AppName>_09-41-00_image.tiff"

// ... or with parameters (see method docs for more details):
ciImage.debug.export(filePrefix: "Debug", codec: .jpeg, format: .RGBA8, quality: 0.7, colorSpace: .sRGBColorSpace)
// exports image as file "Debug_09-41-00_image.jpeg"

图像的渲染信息也可以导出。导出渲染信息会将图像导出为 TIFF 文件,并将所有三个渲染图导出为 PDF 文件

ciImage.debug.render().export() // with optional filePrefix parameter
// Exports the following files:
//   <AppName>_09-41-00_image.tiff
//   <AppName>_09-41-00_initial_graph.pdf
//   <AppName>_09-41-00_optimized_graph.pdf
//   <AppName>_09-41-00_program_graph.pdf    

在 macOS 上,调用这些方法将触发系统对话框,用于选择保存文件的目录。在 iOS 上,包含导出的项目的系统共享表单将在主窗口上打开。它可以用于例如通过 AirDrop 将文件发送到计算机,或保存到“文件”。

export 方法可以随时为任何图像调用,即使从断点操作调用也是如此。

注意: 如果您的 macOS 应用程序是沙盒化的,则需要确保将“用户选择的文件”访问权限设置为“读/写”。否则,应用程序将没有权限将调试文件保存到选定的文件夹。

其他实用扩展

CIImageCIRenderTaskCIRenderInfo 现在具有 pdfRepresentation,它公开了用于为 QuickLook 和 CI_PRINT_TREE 生成滤镜图的内部 API,作为 PDFDocument。当 QuickLook 无法及时为大型滤镜图生成预览时,这非常有用。只需将 pdfRepresentation 加载到变量中,然后使用 QuickLook 预览它即可。