DitheringEngine

抖动引擎

适用于 iOS 和 Mac Catalyst 的框架,用于对图像和视频进行抖动处理。

抖动是一种向图像添加噪声的过程,目的是使我们感知到图像具有更丰富的色彩。

A dithered image with four colors. A rose bush on a field in foreground with a pergola in the background.

此图像仅包含四种颜色:黑色、白色、青色和品红色。

查看适用于 iOS 和 macOS 的演示应用程序

目录

安装

要在 SwiftPM 项目中使用此包,您需要将其设置为包依赖项

// swift-tools-version:5.9
import PackageDescription

let package = Package(
  name: "MyPackage",
  dependencies: [
    .package(
      url: "https://github.com/Eskils/DitheringEngine", 
      .upToNextMinor(from: "1.8.2") // or `.upToNextMajor
    )
  ],
  targets: [
    .target(
      name: "MyTarget",
      dependencies: [
        .product(name: "DitheringEngine", package: "DitheringEngine")
      ]
    )
  ]
)

使用

该引擎适用于 CGImages 和视频 URLs/AVAsset。

支持的抖动方法有

注意: 默认情况下,有序抖动方法是在 GPU 上使用 Metal 计算的。您可以根据需要指定在 CPU 上运行它们。

开箱即用支持的调色板有

抖动图像

使用示例

// Create an instance of DitheringEngine
let ditheringEngine = DitheringEngine()
// Set input image
try ditheringEngine.set(image: inputCGImage)
// Dither to quantized color with 5 bits using Floyd-Steinberg.
let cgImage = try ditheringEngine.dither(
    usingMethod: .floydSteinberg,
    andPalette: .quantizedColor,
    withDitherMethodSettings: FloydSteinbergSettingsConfiguration(direction: .leftToRight),
    withPaletteSettings: QuantizedColorSettingsConfiguration(bits: 5)
)

抖动视频

使用示例

// Create an instance of VideoDitheringEngine
let videoDitheringEngine = VideoDitheringEngine()
// Create a video description
let videoDescription = VideoDescription(url: inputVideoURL)
// Set preferred output size.
videoDescription.renderSize = CGSize(width: 320, height: 568)
// Dither to quantized color with 5 bits using Floyd-Steinberg.
videoDitheringEngine.dither(
    videoDescription: videoDescription,  
    usingMethod: .floydSteinberg, 
    andPalette: .quantizedColor,
    withDitherMethodSettings: FloydSteinbergSettingsConfiguration(direction: .leftToRight), 
    andPaletteSettings: QuantizedColorSettingsConfiguration(bits: 5), 
    outputURL: outputURL, 
    progressHandler: progressHandler,   // Optional block to receive progress.
    completionHandler: completionHandler
)

抖动方法

以下是可用抖动方法的概述。

阈值

阈值给出图像中颜色与调色板中颜色最接近的匹配,而无需添加任何噪声或改进。

Threshold with default settings. CGA Mode 4 | Palette 0 | High

Token: .threshold
Settings: EmptyPaletteSettingsConfiguration

示例

let ditheringEngine = DitheringEngine()
try ditheringEngine.set(image: inputCGImage)
let cgImage = try ditheringEngine.dither(
    usingMethod: .threshold,
    andPalette: .cga,
    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),
    withPaletteSettings: CGASettingsConfiguration(mode: .palette0High)
)

Floyd-Steinberg

Floyd-Steinberg 抖动将减少像素颜色的误差扩散到相邻像素,从而使图像在细节丰富的区域(例如草地和树木)看起来接近原始图像,并在细节少的区域(例如天空)中产生有趣的伪影。

Floyd-Steinberg dithering with default settings. CGA Text Mode palette

Token: .floydSteinberg
Settings: FloydSteinbergSettingsConfiguration

名称 类型 默认值 描述
direction FloydSteinbergDitheringDirection .leftToRight 指定以什么顺序遍历图像的像素。这会影响误差的分布位置。
matrix [Int] [7, 3, 5, 1] 一个矩阵(四个数字的数组),用于指定给相邻像素的误差权重。权重是该数字与矩阵中所有数字之和的比率。 例如:在默认矩阵中,第一个数字的权重为 7/16。 该图像解释了矩阵中权重的分布方式。

FloydSteinbergDitheringDirection:

示例

let ditheringEngine = DitheringEngine()
try ditheringEngine.set(image: inputCGImage)
let cgImage = try ditheringEngine.dither(
    usingMethod: .floydSteinberg,
    andPalette: .cga,
    withDitherMethodSettings: FloydSteinbergSettingsConfiguration(direction: .leftToRight),
    withPaletteSettings: CGASettingsConfiguration(mode: .textMode)
)

Atkinson

Atkinson 抖动是 Floyd-Steinberg 抖动的一种变体,其工作原理是将减少像素颜色的误差扩散到相邻像素。 Atkinson 扩散的面积更大,但不分配全部误差,从而使与调色板匹配的颜色具有较少的噪声。

Atkinson dithering with default settings. CGA Text Mode

Token: .atkinson
Settings: EmptyPaletteSettingsConfiguration

示例

let ditheringEngine = DitheringEngine()
try ditheringEngine.set(image: inputCGImage)
let cgImage = try ditheringEngine.dither(
    usingMethod: .atkinson,
    andPalette: .cga,
    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),
    withPaletteSettings: CGASettingsConfiguration(mode: .textMode)
)

Jarvis-Judice-Ninke

Jarvis-Judice-Ninke 抖动是 Floyd-Steinberg 抖动的一种变体,其工作原理是将减少像素颜色的误差扩散到相邻像素。 此方法将误差分散到更大的区域,因此使图像看起来更平滑。

Jarvis-Judice-Ninke dithering with default settings. CGA Text Mode

Token: .jarvisJudiceNinke
Settings: EmptyPaletteSettingsConfiguration

示例

let ditheringEngine = DitheringEngine()
try ditheringEngine.set(image: inputCGImage)
let cgImage = try ditheringEngine.dither(
    usingMethod: .jarvisJudiceNinke,
    andPalette: .cga,
    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),
    withPaletteSettings: CGASettingsConfiguration(mode: .textMode)
)

Bayer

Bayer 抖动是一种有序抖动,它将预先计算的阈值添加到每个像素,从而嵌入一个特殊的图案。

Bayer dithering with default settings. CGA Mode 5 | High palette

Token: .bayer
Settings: BayerSettingsConfiguration

名称 类型 默认值 描述
thresholdMapSize Int 4 指定正方形阈值矩阵的大小。 默认值为 4x4。
intensity Float 1 指定噪声图案的强度。 强度从 thresholdMapSize 计算得出,此属性指定要应用的计算强度的一部分。
performOnCPU Bool false 确定是否在 CPU 上执行计算。 如果为 false,则使用 GPU 可以获得更快的性能。

示例

let ditheringEngine = DitheringEngine()
try ditheringEngine.set(image: inputCGImage)
let cgImage = try ditheringEngine.dither(
    usingMethod: .bayer,
    andPalette: .cga,
    withDitherMethodSettings: BayerSettingsConfiguration(),
    withPaletteSettings: CGASettingsConfiguration(mode: .mode5High)
)

白噪声

白噪声抖动在转换为所选调色板时,会向图像添加随机噪声,从而使图像看起来有颗粒感且杂乱。

White noise dithering with default settings. CGA Mode 5 | High palette

Token: .whiteNoise
Settings: WhiteNoiseSettingsConfiguration

名称 类型 默认值 描述
thresholdMapSize Int 7 指定正方形阈值矩阵的大小。 默认值为 128x128。
intensity Float 0.5 指定噪声图案的强度。
performOnCPU Bool false 确定是否在 CPU 上执行计算。 如果为 false,则使用 GPU 可以获得更快的性能。

示例

let ditheringEngine = DitheringEngine()
try ditheringEngine.set(image: inputCGImage)
let cgImage = try ditheringEngine.dither(
    usingMethod: .whiteNoise,
    andPalette: .apple2,
    withDitherMethodSettings: WhiteNoiseSettingsConfiguration(),
    withPaletteSettings: Apple2SettingsConfiguration(mode: .hiRes)
)

噪声

您可以提供自己的噪声纹理,以便在执行有序抖动时进行采样。

Bayer dithering with default settings. CGA Mode 5 | High palette

此图像使用蓝色噪声图案进行抖动处理,从而呈现出颗粒状、有机的外观。

Token: .noise
Settings: NoiseDitheringSettingsConfiguration

名称 类型 默认值 描述
noisePattern CGImage? nil 指定用于有序抖动的噪声图案。
intensity Float 0.5 指定噪声图案的强度。
performOnCPU Bool false 确定是否在 CPU 上执行计算。 如果为 false,则使用 GPU 可以获得更快的性能。

示例

let noisePatternImage: CGImage = ...
let ditheringEngine = DitheringEngine()
try ditheringEngine.set(image: inputCGImage)
let cgImage = try ditheringEngine.dither(
    usingMethod: .noise,
    andPalette: .gameBoy,
    withDitherMethodSettings: NoiseDitheringSettingsConfiguration(noisePattern: noisePatternImage),
    withPaletteSettings: EmptySettingsConfiguration()
)

内置调色板

以下是内置调色板的概述

黑与白

一个包含两种颜色的调色板:黑色和白色。

Floyd-Steinberg dithering with the black and white palette

Token: .bw
Settings: EmptyPaletteSettingsConfiguration

示例

let ditheringEngine = DitheringEngine()
try ditheringEngine.set(image: inputCGImage)
let cgImage = try ditheringEngine.dither(
    usingMethod: .floydSteinberg,
    andPalette: .bw,
    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),
    withPaletteSettings: EmptyPaletteSettingsConfiguration()
)

灰度

一个包含所有灰度阴影的调色板。

Floyd-Steinberg dithering with the grayscale palette

Token: .grayscale
Settings: QuantizedColorSettingsConfiguration

名称 类型 默认值 描述
bits Int 0 指定要量化的位数。 位数可以在 0 到 8 之间。灰度的阴影数由 2^n 给出,其中 n 是位数。

示例

let ditheringEngine = DitheringEngine()
try ditheringEngine.set(image: inputCGImage)
let cgImage = try ditheringEngine.dither(
    usingMethod: .floydSteinberg,
    andPalette: .grayscale,
    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),
    withPaletteSettings: EmptyPaletteSettingsConfiguration()
)

量化颜色

具有颜色通道量化位的调色板。 指定用于颜色的位数 - 从 0 到 8。 颜色数由 2^n 给出,其中 n 是位数。

Floyd-Steinberg dithering with the quantized palette. Here with 2 bits

Token: .quantizedColor
Settings: QuantizedColorSettingsConfiguration

名称 类型 默认值 描述
bits Int 0 指定要量化的位数。 位数可以在 0 到 8 之间。颜色数由 2^n 给出,其中 n 是位数。

示例

let ditheringEngine = DitheringEngine()
try ditheringEngine.set(image: inputCGImage)
let cgImage = try ditheringEngine.dither(
    usingMethod: .floydSteinberg,
    andPalette: .quantizedColor,
    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),
    withPaletteSettings: QuantizedColorSettingsConfiguration(bits: 2)
)

CGA

具有老式 CGA 调色板的调色板。 CGA 是一种于 1981 年推出的显卡,能够在 IBM PC 上显示颜色。 它使用 4 位接口(红色、绿色、蓝色、强度),总共可以显示 16 种颜色。 然而,由于视频内存有限,320x200 的最常见分辨率只能在屏幕上同时显示四种颜色。 在此模式下,d 开发人员可以从四个调色板中进行选择,这些调色板具有漂亮的颜色组合,例如黑色、青色、品红色和白色,或者黑色、绿色、红色和黄色。

Floyd-Steinberg dithering with the CGA palette. Here in mode 4 with pallet 1 high

Token: .cga
Settings: CGASettingsConfiguration

名称 类型 默认值 描述
mode CGAMode .palette1High 指定要使用的图形模式。 每种图形模式都有一组独特的颜色。 颜色最多的是 .textMode

CGAMode:

名称 颜色 图像
.palette0Low 黑色、绿色、红色、棕色 Palette 0 Low
.palette0High 黑色、浅绿色、浅红色、黄色 Palette 0 High
.palette1Low 黑色、青色、品红色、浅灰色 palette 1 Low
.palette1High 黑色、浅青色、浅品红色、白色 Palette 1 High
.mode5Low 黑色、青色、红色、浅灰色 Mode 5 Low
.mode5High 黑色、浅青色、浅红色、白色 Mode 5 High
.textMode 全部 16 种颜色 Text Mode

示例

let ditheringEngine = DitheringEngine()
try ditheringEngine.set(image: inputCGImage)
let cgImage = try ditheringEngine.dither(
    usingMethod: .floydSteinberg,
    andPalette: .quantizedColor,
    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),
    withPaletteSettings: CGASettingsConfiguration(mode: .palette1High)
)

Apple II

Apple II 是最早具有颜色的个人电脑之一。 与降低成本相关的技术挑战为图形启用了两种模式 - 一种是具有六种颜色的高分辨率模式,另一种是具有 16 种颜色的低分辨率模式。

Atkinson dithering with the Apple II palette. Here in HiRes graphics mode.

Token: .apple2
Settings: Apple2SettingsConfiguration

名称 类型 默认值 描述
mode Apple2Mode .hiRes 指定要使用的图形模式。 每种图形模式都有一组独特的颜色。

Apple2Mode:

名称 颜色数量 图像
.hiRes 6 种颜色 Hi-Res
.loRes 16 种颜色 Lo-Res

注意: Apple2 Lo-Res 调色板的 16 种颜色与 CGA 的文本模式调色板不同。

示例

let ditheringEngine = DitheringEngine()
try ditheringEngine.set(image: inputCGImage)
let cgImage = try ditheringEngine.dither(
    usingMethod: .atkinson,
    andPalette: .apple2,
    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),
    withPaletteSettings: Apple2SettingsConfiguration(mode: .hiRes)
)

Game Boy

老式四色绿色阴影单色显示器。

Atkinson dithering with the Game Boy palette.

Token: .gameBoy
Settings: EmptyPaletteSettingsConfiguration

示例

let ditheringEngine = DitheringEngine()
try ditheringEngine.set(image: inputCGImage)
let cgImage = try ditheringEngine.dither(
    usingMethod: .atkinson,
    andPalette: .gameBoy,
    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),
    withPaletteSettings: EmptyPaletteSettingsConfiguration()
)

Intellivision

Intellivision 是 70 年代后期的游戏机。 它的图形由标准电视接口芯片提供支持,该芯片带有 16 色调色板。

Atkinson dithering with the Game Boy palette.

Token: .intellivision
Settings: EmptyPaletteSettingsConfiguration

示例

let ditheringEngine = DitheringEngine()
try ditheringEngine.set(image: inputCGImage)
let cgImage = try ditheringEngine.dither(
    usingMethod: .atkinson,
    andPalette: .jarvisJudiceNinke,
    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),
    withPaletteSettings: EmptyPaletteSettingsConfiguration()
)

创建您自己的调色板

您可以使用适当的 API 创建自己的调色板。

Floyd-Steinberg dithering with a custom palette

调色板由 BytePalette 结构表示,该结构可以从查找表 (LUT) 和颜色集合 (LUTCollection) 构造。 最有用的可能是 LUTCollection。

如果您的调色板中包含 UIColors 数组,则首先需要将颜色值提取到 SIMD3<UInt8> 列表中。 这可以按如下方式完成

let entries = colors.map { color in
    var redNormalized: CGFloat = 0
    var greenNormalized: CGFloat = 0
    var blueNormalized: CGFloat = 0

    color.getRed(&redNormalized, green: &greenNormalized, blue: &blueNormalized, alpha: nil)

    let red = UInt8(clamp(redDouble * 255, min: 0, max: 255))
    let green = UInt8(clamp(greenDouble * 255, min: 0, max: 255))
    let blue = UInt8(clamp(blueDouble * 255, min: 0, max: 255))

    return SIMD3(x: red, y: green, z: blue)
}

之后,您可以创建一个 LUTCollection 并从中创建一个调色板

let collection = LUTCollection<UInt8>(entries: entries)
let palette = BytePalette.from(lutCollection: collection)

抖动图像时,请选择 .custom 调色板,并在 CustomPaletteSettingsConfiguration 中提供您的调色板

try ditheringEngine.dither(
    usingMethod: .floydSteinberg,
    andPalette: .custom,
    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),
    withPaletteSettings: CustomPaletteSettingsConfiguration(palette: palette)
)

完整示例

let entries = colors.map { color in
    var redNormalized: CGFloat = 0
    var greenNormalized: CGFloat = 0
    var blueNormalized: CGFloat = 0

    color.getRed(&redNormalized, green: &greenNormalized, blue: &blueNormalized, alpha: nil)

    let red = UInt8(clamp(redDouble * 255, min: 0, max: 255))
    let green = UInt8(clamp(greenDouble * 255, min: 0, max: 255))
    let blue = UInt8(clamp(blueDouble * 255, min: 0, max: 255))

    return SIMD3(x: red, y: green, z: blue)
}
let collection = LUTCollection<UInt8>(entries: entries)
let palette = BytePalette.from(lutCollection: collection)

let ditheringEngine = DitheringEngine()
try ditheringEngine.set(image: inputCGImage)
try ditheringEngine.dither(
    usingMethod: .floydSteinberg,
    andPalette: .custom,
    withDitherMethodSettings: EmptyPaletteSettingsConfiguration(),
    withPaletteSettings: CustomPaletteSettingsConfiguration(palette: palette)
)

视频抖动引擎

除了 DitheringEngine 抖动图像之外,还存在 VideoDitheringEngine 来抖动视频。 VideoDitheringEngine 的工作原理是将调色板和抖动方法应用于视频中的每一帧。 您还可以选择调整视频的大小作为此过程的一部分。

VideoDitheringEngine works by applying a palette and dither method to every frame in the video

VideoDitheringEngine works by applying a palette and dither method to every frame in the video

使用示例

// Create an instance of VideoDitheringEngine
let videoDitheringEngine = VideoDitheringEngine()
// Create a video description
let videoDescription = VideoDescription(url: inputVideoURL)
// Set preferred output size.
videoDescription.renderSize = CGSize(width: 320, height: 568)
// Dither to quantized color with 5 bits using Floyd-Steinberg.
videoDitheringEngine.dither(
    videoDescription: videoDescription,  
    usingMethod: .floydSteinberg, 
    andPalette: .quantizedColor,
    withDitherMethodSettings: FloydSteinbergSettingsConfiguration(direction: .leftToRight), 
    andPaletteSettings: QuantizedColorSettingsConfiguration(bits: 5), 
    outputURL: outputURL, 
    progressHandler: progressHandler,
    completionHandler: completionHandler
)

有序抖动更适合

使用有序抖动方法更快,并且会给出最佳结果,因为图案不会“移动”(像静态噪声)。

视频帧率

默认情况下,最终视频的帧率为 30。您可以在初始化 VideoDitheringEngine 时提供帧率来调整最终帧率。 最终帧率小于或等于指定值。

VideoDitheringEngine(frameRate: Int)

并发帧处理

默认情况下,视频帧是并发渲染的。 您可以禁用此行为,或使用 numberOfConcurrentFrames 属性更改同时处理的帧数。

将其设置为 1 将有效地禁用并发帧处理。 如果 CPU 有足够的内核来处理负载,则更高的数字会更快,但也会使用更多的内存。

视频抖动选项

抖动视频时,您可以提供视频应如何处理的选项。 以下选项可用

视频描述

您可以使用 VideoDescription 类型设置要用作输入的视频。这是 AVAsset 的一个方便的包装器,允许您设置首选的输出大小。

A VideoDescription can be made from an AVAsset, and is what you pass to VideoDitheringEngine in order to dither a video. A VideoDescription can be made from an AVAsset, and is what you pass to VideoDitheringEngine in order to dither a video.

属性

名称 类型 默认值 描述
renderSize CGSize? { get set } nil 指定渲染最终抖动视频的大小。
framerate Float? { get } nominalFrameRate 返回每秒帧数。如果资源不包含视频,则为 Nil。
transform CGAffineTransform? { get } preferredTransform 视频的变换(方向,缩放)。
duration TimeInterval { get } duration.seconds 返回视频的持续时间。
sampleRate Int? { get } naturalTimeScale 返回每秒音频采样数。如果资源不包含音频,则为 Nil。
size CGSize? { get } naturalSize 返回视频的大小。如果资源不包含视频,则为 Nil。

方法

/// Reads the first frame in the video as an image.
func getPreviewImage() async throws -> CGImage