我非常喜欢复古的像素艺术。前段时间了解了有关早期Windows系统的屏幕保护程序的故事,其中提到的一款发布于1990年代的屏保After Dark: Starry Night,在黑色的屏幕上随机地绘制闪光的像素形成星空和星空下的城市,我很喜欢。

starry night

于是我以此为灵感,想要做一个类似的屏幕保护程序(虽然现在已经没人用屏保了):输入一张图片,通过一些风格化处理转换成单色像素效果,然后进行随机绘制。然而我尝试了几种方法风格化效果都不好,Starry Night的算法也不能提供参考。最后在和AI的对话中我了解到Bayer抖动算法。简单搜索后,我醍醐灌顶。这种规则而不失艺术的纹理,不正是我一直想要达到的效果。

bayer ordered dither

来源:ditherit.com

Bayer有序抖动

Bayer有序抖动是一种经典的Halftone技术,能将高色深图像按灰度转换成只有两种颜色的1位图像,同时让人眼感受到连续的色阶变化。另一种方法是误差扩散,例如Floyd-Steinberg算法,通过将像素二值化后产生的误差扩散到周围的像素,能够得到一种更自然的结果。而Bayer算法的特点在于会形成一系列类似网格或棋盘的规则纹理,虽然被认为效果不如误差扩散方法,但反而满足了我对复古像素风格的喜好。

ordered dither and error dither

来源:Wikipedia

为什么有序

Bayer算法之所以有序,就是源于其核心生成逻辑:Bayer矩阵。这个矩阵的大小通常是 $2^n \times 2^n$,包含$0$到$2^{n+1}-1$的所有整数。通过这$2^{n+1}$个阈值,就能在一个矩阵内模拟$2^{n+1}$种灰度级别。

Bayer矩阵是通过递归生成的。首先,定义最基础的 $2 \times 2$ 矩阵 $M_2$:
$$
M_2 = \begin{bmatrix} 0 & 2 \ 3 & 1 \end{bmatrix}
$$
为什么是这样?这个排列是为了让相近的阈值在空间上尽量分散,形成黑白交替的高频噪点,从而欺骗人眼,更好地模拟灰度。这是Bayer算法的核心思想。

然后,通过以下递归公式生成更大的矩阵 $M_{2n}$:
$$
M_{2n} = \begin{bmatrix} 4 \times M_n & 4 \times M_n + 2 \ 4 \times M_n + 3 & 4 \times M_n + 1 \end{bmatrix}
$$
让我们推导一下 $4 \times 4$ 的 Bayer 矩阵 $M_4$:

  • 左上角:$4 \times M_2 = \begin{bmatrix} 0 & 8 \ 12 & 4 \end{bmatrix}$
  • 右上角:$4 \times M_2 + 2 = \begin{bmatrix} 2 & 10 \ 14 & 6 \end{bmatrix}$
  • 左下角:$4 \times M_2 + 3 = \begin{bmatrix} 3 & 11 \ 15 & 7 \end{bmatrix}$
  • 右下角:$4 \times M_2 + 1 = \begin{bmatrix} 1 & 9 \ 13 & 5 \end{bmatrix}$

将它们拼在一起,就得到了 $M_4$:
$$
M_4 = \begin{bmatrix}
0 & 8 & 2 & 10 \
12 & 4 & 14 & 6 \
3 & 11 & 1 & 9 \
15 & 7 & 13 & 5
\end{bmatrix}
$$
这个矩阵包含了从 0 到 15 的所有整数,并且形成一种分形,在局部和整体都满足相近阈值尽量分散的思想,从而模拟17种灰度。

rank 4 bayer matrix

4阶Bayer矩阵的所有模式

算法执行部分,只需将灰度图的灰度级别和Bayer矩阵阈值进行归一化处理,然后将每个像素的坐标进行取模处理,找到Bayer矩阵中的对应位置(即将Bayer矩阵铺满图像),最后将灰度与阈值进行比较并二值化输出。

最终我便用Bayer算法实现了我的屏保想法。

demo