屏幕不够,算法来凑(一):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 空间直接进行误差扩散或加减运算,会导致以下问题:
- 亮度不守恒:计算出的灰度值与物理亮度不符。
- 图像偏暗:阴影部分会由于缺乏补偿而变成死黑。
因此,处理前必须先进行 De-gamma 转换。
1 | function srgbToLinear(v) { |
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 | Browser: Chrome Engine (Any) |
参考资料
屏幕不够,算法来凑(一):Ditherpunk 抖动算法原理与 JS 实时演示
https://chaosgoo.com/2025/12/22/Ditherpunk-The-Art-of-Dithering/
