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 中,您可以使用 CIImage
的 init(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 是一个开放标准,用于存储超出“正常”图像颜色数据的任意位图数据,例如 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
版本。
本项目中使用的所有 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 可以使用 CITextImageGenerator
和 CIAttributedTextImageGenerator
生成包含文本的图像。我们添加了扩展,使它们使用起来更加方便
// 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)
使用旧版 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
[[ stitchable ]]
时才有效。有关详细信息,请参阅 此 WWDC 演讲。MTLDevice.supportsDynamicLibraries
以查看是否支持基于 Metal 的 CIKernel 的运行时编译。CIBlendKernel
无法通过这种方式编译,遗憾的是。CIKernel.kernels(withMetalString:)
API 只是将它们识别为 CIColorKernel
如果您的最低部署目标尚不支持 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
包含有关渲染过程的信息
image
:渲染后的图像本身为 CGImage
renderTask
:CIRenderTask
,它描述了优化的渲染图,然后由 Core Image 执行。renderInfo
:CIRenderInfo
,包含渲染的运行时信息以及串联的滤镜图还有其他访问器可以获取不同类型的滤镜图,类似于 CI_PRINT_TREE
可以生成的内容
initialGraph
:一个 PDFDocument
,显示渲染后的图像以及未优化的滤镜图(类似于 CI_PRINT_TREE 1 pdf
)。optimizedGraph
:一个 PDFDocument
,显示渲染前的优化滤镜图(与 CI_PRINT_TREE 2 pdf
相同)。programGraph
:一个 PDFDocument
,显示有关渲染的运行时信息以及串联的程序滤镜图(等效于 CI_PRINT_TREE 4 pdf
)。以下 API 可用于访问有关图像的一些基本统计信息(min
、max
和 avg
像素值)
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
。
CI_PRINT_TREE
是一个很棒的调试工具,但也有一些主要缺点
CIContext
渲染调用时才打印结果。无法获取任意 CIImage
的信息。以下 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 应用程序是沙盒化的,则需要确保将“用户选择的文件”访问权限设置为“读/写”。否则,应用程序将没有权限将调试文件保存到选定的文件夹。
CIImage
、CIRenderTask
和 CIRenderInfo
现在具有 pdfRepresentation
,它公开了用于为 QuickLook 和 CI_PRINT_TREE
生成滤镜图的内部 API,作为 PDFDocument
。当 QuickLook 无法及时为大型滤镜图生成预览时,这非常有用。只需将 pdfRepresentation
加载到变量中,然后使用 QuickLook 预览它即可。