返回

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

发布于:  at  06:15 下午
更新于:  at  03:40 上午
⏱️ 约 14682 字 • 预计阅读 37 分钟

Read in English

在浏览器中对照阈值、Bayer、Floyd-Steinberg 与 Atkinson 抖动算法,切换线性亮度处理并下载 PNG。图片只在本地处理,附原理与 ESP32 硬件实现入口。

环境与范围: 原生 JavaScript / Canvas;默认使用站内示例图,上传图片仅在浏览器处理。导出是黑白像素的 PNG,不是屏幕驱动所需的 1-bit 打包数据。

需要将自己的图片用于单色屏?打开图片抖动取模器,设置尺寸与算法,预览后导出 PNG、C 数组或 BIN;可选择逐行、逐列或分页取模。

背景

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

  • 经典的 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 空间中。sRGB 为了适配人眼对暗部细节的敏感性,采用了非线性映射(约 Gamma 2.2)。

为什么我们需要如此麻烦地进行 Gamma 校正?这源于人类进化的结果:我们的眼睛对暗部变化的敏感度远高于亮部。在物理世界中,光子数量增加一倍,能量就增加一倍;但在人类感知中,亮度并不是线性增加的。sRGB 标准通过一条幂律曲线(约 )将更多的位深分配给了暗部信息。如果我们在处理抖动时不进行 De-gamma,算法会错误地将“感知上的中灰”当作“物理上的中灰”来分摊误差,导致黑白像素的面积比例不再近似对应原图的物理亮度。实际观感还取决于屏幕、观看距离与目标风格。

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

  1. 亮度不守恒:计算出的灰度值与物理亮度不符。
  2. 观感变化:直接以 sRGB 数值分配黑白面积,通常会得到更多白点;转换为线性亮度后输出可能更暗,不能将 Gamma 开关简单理解为“提亮”。

若要保持近似物理亮度,先对每个 sRGB 通道做 De-gamma,再按亮度权重合成灰度。演示中的关闭选项用于对照非线性空间的处理效果。

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)

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

let newPixel = oldPixelLinear + (Math.random() - 0.5) > 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 低差异序列阈值(非严格蓝噪声)

演示范围与导出说明

上面的 R2 阈值函数是低差异序列演示,不是经过频谱验证的蓝噪声生成器,也不代表所有蓝噪声方法的性能。真正的蓝噪声阈值图可以预计算,逐像素使用时无需高昂的在线生成成本。

全部演示使用同一张默认图;也可以上传自己的图片。默认样例路径为 /images/archives/ditherpunk/sample.jpg,页面题图来自《奥伯拉丁的回归》,用于介绍黑白视觉风格,并非本站原创游戏画面。上传前请确认你有权使用图片。

下载按钮导出当前算法与 Gamma 选项对应的黑白 PNG,不包含鼠标对照镜。长边最多 600 像素,透明区域先合成到白色背景;导出文件不是 1-bit 打包帧缓冲,驱动屏幕时仍需按目标分辨率、位序和行布局转换。算法实现为了演示清晰使用了完整浮点缓冲,内存占用不能直接当成 MCU 移植方案。

总结

算法名称核心特征视觉风格计算复杂度适合场景
Threshold固定阈值高对比度、块状极低纯文本、高反差 Logo
Bayer矩阵映射规律网格、像素风低动态 UI、复古游戏
Floyd-S误差扩散细腻、类素描中摄影照片、电子相框
Atkinson局部扩散干净、高细节中艺术扫描、低分辨率屏
R2 阈值演示低差异序列分散的点状纹理低对照实验,非严格蓝噪声

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

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

环境

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

参考资料


在以下平台分享此文章:

评论


上一篇
屏幕不够,算法来凑(二):ESP32 单色屏上的 Ditherpunk 实战
下一篇
ESP32 连接 4G 模块:ESP-IDF PPPoS 拨号与 Air780EG 实战