本文参考知乎博客:图像处理之 Dithering(https://zhuanlan.zhihu.com/p/110104674)
图像抖动(dithering)常用于颜色量化(color quantization)的后处理,即去除颜色量化产生的一些视觉上不合理的“色带”。如下所示:左侧是原图,右侧是颜色量化的结果,可以看到猫的脖子以及头部有很多不合理的色带,颜色抖动的目标就是去除这些色带,使之更加平滑。
图像抖动最经典的技术为误差扩散方法,可以参考博客:Floyd-Steinberg扩散抖动算法。
这里介绍另一种简单且好玩的抖动算法,随机抖动。
假设现在给定如上所示的图像,不失一般性,将颜色归一化到[0,1]范围,我们给定8种颜色的调色板正好对应RGB cube的8个顶点),分别为: ( 0 , 0 , 0 ) , ( 1 , 0 , 0 ) , ( 0 , 1 , 0 ) , ( 0 , 0 , 1 ) , ( 0 , 1 , 1 ) , ( 1 , 1 , 0 ) , ( 1 , 0 , 1 ) , ( 1 , 1 , 1 ) (0,0,0),(1,0,0),(0,1,0),(0,0,1),(0,1,1),(1,1,0),(1,0,1),(1,1,1) (0,0,0),(1,0,0),(0,1,0),(0,0,1),(0,1,1),(1,1,0),(1,0,1),(1,1,1), 也就是说8个颜色,每种颜色的三通道取值要么是0,要么是1。
给定固定值 x = 0.5 x = 0.5 x=0.5, 对于每个像素点 I p I_p Ip, 分别检查它的三个通道,并重新赋值:
I p ( r / g / b ) = { 0 I p ( r / g / b ) < x 1 I p ( r / g / b ) ≥ x I_p(r/g/b)=\left\{ \begin{aligned} 0\ \ \ \ I_p(r/g/b) \lt x\\ 1\ \ \ \ I_p(r/g/b) \geq x \end{aligned} \right. Ip(r/g/b)={0 Ip(r/g/b)<x1 Ip(r/g/b)≥x
固定阈值抖动代码:
def dithering_v1(input_img, save_path):
quant_img = input_img.copy()
quant_img[input_img > 0.5] = 1
quant_img[input_img < 0.5] = 0
cv2.imwrite(save_path, quant_img*255)
对于每个像素点 I p I_p Ip, 生成对应的随机数值 x = r a n d ( ) x = rand() x=rand(), 注意这里每个像素生成一个独立的随机 x x x. 分别检查它的三个通道,并重新赋值:
I p ( r / g / b ) = { 0 I p ( r / g / b ) < x 1 I p ( r / g / b ) ≥ x I_p(r/g/b)=\left\{ \begin{aligned} 0\ \ \ \ I_p(r/g/b) \lt x\\ 1\ \ \ \ I_p(r/g/b) \geq x \end{aligned} \right. Ip(r/g/b)={0 Ip(r/g/b)<x1 Ip(r/g/b)≥x
随机抖动代码1:
def dithering_v2(input_img, save_path):
shape = input_img.shape
input_img = input_img.flatten()
quant_img = input_img.copy()
for i in range(input_img.shape[0]):
x = np.random.rand()
if quant_img[i] > x: quant_img[i] = 1
else: quant_img[i] = 0
cv2.imwrite(save_path,quant_img.reshape(shape)*255)
抖动效果如下所示,可以看到,效果比固定阈值抖动好很多,但有很多噪点。
直接上代码,这里对每个像素生成20个随即数 x x x,生成20个颜色值并求平均。
def dithering_v3(input_img, save_path):
shape = input_img.shape
input_img = input_img.flatten()
quant_img = input_img.copy()
for i in range(input_img.shape[0]):
color = 0
n = 20
for k in range(n):
if quant_img[i] > np.random.rand(): color += 1
quant_img[i] = color / n
cv2.imwrite(save_path, quant_img.reshape(shape)*255)
注意到这里每个像素点 I p I_p Ip 的颜色是这样计算的:
令: c = 0 c = 0 c=0
第1次: x 1 = r a n d ( ) , c + = I p > x 1 x1 = rand(), c += I_p > x1 x1=rand(),c+=Ip>x1
第2次: x 2 = r a n d ( ) , c + = I p > x 2 x2 = rand(), c += I_p > x2 x2=rand(),c+=Ip>x2
第3次: x 3 = r a n d ( ) , c + = I p > x 3 x3 = rand(), c += I_p > x3 x3=rand(),c+=Ip>x3
…
第n次: x n = r a n d ( ) , c + = I p > x n xn = rand(), c += I_p > xn xn=rand(),c+=Ip>xn
最终, I p = c / n I_p = c / n Ip=c/n
任意一次, I p > x i , x i = r a n d ( ) I_p > xi, xi = rand() Ip>xi,xi=rand(), 可以发现 x i = r a n d ( ) < I p xi = rand() < I_p xi=rand()<Ip 的概率就是 I p I_p Ip, 比如 I p = 0.8 I_p = 0.8 Ip=0.8, 那么在[0,1]范围生成随即数 x < 0.8 x < 0.8 x<0.8的概率就是0.8,为此, I p = 1 I_p = 1 Ip=1的概率就是 0.8. 通过多次随机采样,最终的期望值就等于 I p I_p Ip本身的像素值。
因此,这个算法其实并不实用,因为颜色值已经不再是调色板中的颜色值,而是接近像素值本身。。。