屏幕不够,算法来凑(一):Ditherpunk 抖动算法原理与 JS 实时演示

屏幕不够,算法来凑(一):Ditherpunk 抖动算法原理与 JS 实时演示

背景

在嵌入式开发领域,我们经常会遇到色彩位数极低的显示设备:

  • 经典的 SSD1306 (0.96寸 OLED),仅支持黑白两色。
  • 电子墨水屏 (E-Ink),通常只有黑白,且刷新率极低。

如果直接将 24 位真彩图片进行量化处理,其结果往往如同烧焦的木炭,细节丢失殆尽。但若引入 抖动算法(Dithering),这些 1-Bit 屏幕便能模拟出细腻的灰度感。

此前我曾尝试用 Rust 实现过一个版本,但作为博客演示,使用 JavaScript 与 Canvas 在浏览器中直接进行仿真最为直观。本文将介绍几种主流抖动算法的原理及其 JS 实现。


实验室控制台

在此上传图片,下文的所有算法演示将同步生效。建议开启 Gamma 校正观摩图像暗部细节的差异。

* 图片将仅在本地处理,不会上传服务器

0x01 线性空间 (Linear Space)

这是本文最重要的核心内容。

很多开发者在处理图像时会直接进行 if (gray > 0.5) 的判断。
这是错误的。

sRGB 的非线性特性

主流图像格式通常存储在 sRGB 空间中。sRGB 为了适配人眼对暗部细节的敏感性,采用了非线性映射(约 Gamma 2.2)。

为什么我们需要如此麻烦地进行 Gamma 校正?这源于人类进化的结果:我们的眼睛对暗部变化的敏感度远高于亮部。在物理世界中,光子数量增加一倍,能量就增加一倍;但在人类感知中,亮度并不是线性增加的。sRGB 标准通过一条幂律曲线(约 $\gamma = 2.2$)将更多的位深分配给了暗部信息。如果我们在处理抖动时不进行 De-gamma,算法会错误地将“感知上的中灰”当作“物理上的中灰”来分摊误差,导致图像在 1-bit 屏幕上看起来比原图暗得多,阴影细节会像沉入黑洞一样消失。

如果在 sRGB 空间直接进行误差扩散或加减运算,会导致以下问题:

  1. 亮度不守恒:计算出的灰度值与物理亮度不符。
  2. 图像偏暗:阴影部分会由于缺乏补偿而变成死黑。

因此,处理前必须先进行 De-gamma 转换。

1
2
3
4
function srgbToLinear(v) {
v /= 255;
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
}

0x02 阈值裁剪 (Thresholding)

这是最原始、最简单的方案:设定一个固定阈值(通常为 0.5),高于该值设为 1(白),低于则设为 0(黑)。

结果: 细节丢失严重。由于强制舍弃了所有中间调,会导致严重的量化噪声,图像面貌全非。

演示:阈值裁剪 (Fixed Threshold 0.5)

0x03 随机抖动 (Random Dithering)

为了缓解量化误差,最直观的方法是在对比前引入随机噪声。

1
let newPixel = (oldPixelLinear + (Math.random() - 128) / 255) > 0.5 ? 1.0 : 0.0;

结果: 虽然保留了部分细节,但图像布满了均匀分布的“雪花点”。这种 白噪声 (White Noise) 在视觉上非常刺眼,因为它在所有频率上都有能量分布。

演示:随机白噪声抖动

0x04 有序抖动 (Ordered Dithering)

有序抖动不再依赖随机性,而是使用特定的阈值矩阵进行循环铺设。最典型的是 Bayer 矩阵

Bayer 矩阵原理

Bayer 矩阵是一种分形结构。其核心设计目标是:在空间上尽可能均匀地分散不同等级的阈值点。

  • 优点: 效率极高,无需处理邻近像素,极其适合计算资源匮乏的单片机。
  • 缺点: 会产生规律性的十字网点或条纹。

演示:Bayer 4x4 有序抖动

0x05 误差扩散 (Error Diffusion)

这是目前 1-Bit 图像处理的通用方案。其核心逻辑是:将当前像素量化产生的误差,按照特定权重分摊给邻近尚未处理的像素。

Floyd-Steinberg 算法

这是最知名的误差扩散算子。它将误差以 7/16、3/16、5/16、1/16 的比例分摊至右方和下方的四个邻居。

演示:Floyd-Steinberg 误差扩散

Atkinson 算法

由 Apple 工程师 Bill Atkinson 在早期麦金塔开发中提出。它只分摊 75% 的误差,且扩散范围更广(影响 6 个邻居)。这使得图像对比度更高,视觉上更清晰。

演示:Atkinson (Macintosh 风格)

0x06 蓝噪声 (Blue Noise)

如果你追求艺术级质感,蓝噪声是终极答案。

  • 白噪声:能量分布均匀,看起来杂乱。
  • 蓝噪声:去除了低频成分,像素点之间相互“排斥”,分布极其均匀。

使用蓝噪声纹理作为阈值图,可以产生类似电影胶片颗粒的效果,完全消除了规律性网点。

演示:基于 R2 序列模拟的蓝噪声

总结

算法名称 核心特征 视觉风格 计算复杂度 适合场景
Threshold 固定阈值 高对比度、块状 极低 纯文本、高反差 Logo
Bayer 矩阵映射 规律网格、像素风 动态 UI、复古游戏
Floyd-S 误差扩散 细腻、类素描 摄影照片、电子相框
Atkinson 局部扩散 干净、高细节 艺术扫描、低分辨率屏
Blue Noise 空间排斥 胶片颗粒感 极高 高级滤镜、印刷出版

从原始的阈值裁剪到精密的误差扩散,抖动算法在显示资源受限的情况下寻找到了人类视觉审美的平衡点。随着硬件资源的丰富,这些算法已经从“不得不选”变成了提升质感的“艺术风格”。

下一篇我们将深入讨论硬件实战:如何在内存资源匮乏的 ESP32 上实时生成高分辨率图像的抖动输出?


环境

1
2
3
4
Browser: Chrome Engine (Any)
Language: JavaScript (ES6+)
Framework: Vanilla JS / HTML5 Canvas
Target: PC / Embedded (SSD1306/EPD)

参考资料

屏幕不够,算法来凑(一):Ditherpunk 抖动算法原理与 JS 实时演示

https://chaosgoo.com/2025/12/22/Ditherpunk-The-Art-of-Dithering/

作者

Chaos Goo

发布于

2025-12-22

更新于

2025-12-22

许可协议