本文参考知乎博客:图像处理之 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。
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)
抖动效果如下所示,可以看到,这里原始细节信息损失很严重。
2. 独立随机抖动
对于每个像素点
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)
抖动效果如下所示,可以看到,效果比固定阈值抖动好很多,但有很多噪点。
3. 多次独立随机抖动
直接上代码,这里对每个像素生成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本身的像素值。
因此,这个算法其实并不实用,因为颜色值已经不再是调色板中的颜色值,而是接近像素值本身。。。