图像抖动算法
由于毕业设计,准备使用热敏打印纸,制作一个“拍立得”。其中关于热敏打印纸在生活中很容易遇见,比如在超市购物,去前台付完款后,会收到一个小票。而这个小票就是热敏纸,其原理就是遇热会变黑,比如用指甲一划就会看到印迹。用它作为“拍立得”的相纸一大优势就是成本低,且相比于传统的拍立得相机,尺寸可以压缩很小。但人家毕竟生产出来的功能是打印小票,所以劣势就是相纸不能长期保存(比如你可以看看1个月前的小票估计已经变得模糊),还有只能打印黑白颜色所以色深极浅。如果想打印一张图像,只简单的取阈值对图像进行二值化,其表现内容就很少,所以采取图像抖动来进行二值化操作来扩充图像信息表达。
关于此算法的相关代码,使用python
语言,使用的库包括opencv
和numpy
,默认读者已经对这两个库有一定的了解
img = cv.imread("src.jpg") #读取源图像 尺寸为400x400
img_gray = cv.cvtColor(img, cv.COLOR_RGB2GRAY) #对图像进行灰度处理
原图:
一.阈值二值化😀
这个算法是最简单的,对于灰色图像,单个像素的范围为0~255(其中0表示黑色,255表示白色)。而算法的原理就是设定一个值,如果该点像素小于该值就设置为0,如果大于或等于该值就设置为255,通常称这个值为threshold
,翻译成中文有门槛的意思还是挺贴近的。用数学公式表达为:
D
s
t
I
m
g
[
x
]
[
y
]
=
{
0
S
r
c
I
m
g
[
x
]
[
y
]
<
t
h
r
e
s
h
o
l
d
255
S
r
c
I
m
g
[
x
]
[
y
]
≥
t
h
r
e
s
h
o
l
d
DstImg[x][y]=\begin{cases} 0 & SrcImg[x][y] < threshold \\ 255 & SrcImg[x][y] ≥threshold\end{cases}
DstImg[x][y]={0255SrcImg[x][y]<thresholdSrcImg[x][y]≥threshold
阈值二值化代码实现:
#这里直接使用opencv二值化函数来实现
imgB = cv.threshold(img_gray, 127, 255, cv.THRESH_BINARY)[1]
效果图:
二.随机颜色抖动😁
对于人眼来说,很容易注意到发生改变或者与周围场景格格不如的东西。比如在一背景为白色的画里面,最先注意到的是上面一道黑。因此该算法就可以实现该效果,所以相比于直接阈值二值化,显示的内容就较多一些,但算法相比只多了一个改变即噪声。
用数学公式表达为:
D
s
t
I
m
g
[
x
]
[
y
]
=
{
0
S
r
c
I
m
g
[
x
]
[
y
]
+
n
o
s
i
e
(
)
<
t
h
r
e
s
h
o
l
d
255
S
r
c
I
m
g
[
x
]
[
y
]
+
n
o
s
i
e
(
)
≥
t
h
r
e
s
h
o
l
d
DstImg[x][y] = \begin{cases} 0 & SrcImg[x][y] + nosie() < threshold \\ 255 & SrcImg[x][y] + nosie() ≥ threshold\end{cases}
DstImg[x][y]={0255SrcImg[x][y]+nosie()<thresholdSrcImg[x][y]+nosie()≥threshold
随机颜色抖动代码实现:
'''
@param src 要处理的源图像
@param NoiseDegree 加入噪声的程度
'''
def Random_dither(src, NoiseDegree=50):
# 处理的对象得是一个灰度图 所以根据图像的shape属性来判断传入的参数是否为灰度图
if len(src.shape) != 2:
print("[Random_dither]:Require a Gray Image")
return src
# 由于python的数组类型是浅拷贝,可以理解成C语言的指针,防止对外部图像产生影响,所以需要单独复制一份
srcImg = src.copy()
srcImgHeight, srcImgWeight = srcImg.shape
for h in range(srcImgHeight):
for w in range(srcImgWeight):
# 因为源像素加入噪声 数值可能超过0~255这个范围 用来限制。
srcImg[h][w] = max(min(srcImg[h][w] + random.randint(-NoiseDegree, NoiseDegree), 255),0)
return cv.threshold(srcImg, 128, 255, cv.THRESH_BINARY)[1]
效果图:
相比之下图像表示的内容就更加丰富了。
三.有序颜色抖动😉
相比于阈值二值化和随机颜色抖动算法,图像二值化过程都是与一个固定阈值进行比较,而有序颜色抖动,是采取动态阈值,其根据一个事先确定的阈值矩阵,矩阵必须是方阵,且大小是2的幂,该矩阵每一点都有不同的阈值。将该矩阵与源图像进行与卷积运算类似的比较大小,从而实现二值化。
对于该阈值矩阵有一个递推公式:
M
2
n
=
∣
M
n
×
4
M
n
×
4
+
2
M
n
×
4
+
3
M
n
×
4
+
1
∣
M_{2n} = \left|\begin{matrix} M_{n}×4& M_{n}×4+2\\ M_{n}×4+3 & M_{n}×4+1 \end{matrix}\right|
M2n=
Mn×4Mn×4+3Mn×4+2Mn×4+1
由此公式便可得:
特殊的
M
0
=
∣
0
∣
特殊的M_0 = \left|\begin{matrix} 0 \end{matrix}\right|
特殊的M0=
0
M 2 = ∣ 0 2 3 1 ∣ M_2 = \left|\begin{matrix} 0 & 2\\ 3 & 1 \end{matrix}\right| M2= 0321
M 4 = ∣ 0 8 2 10 12 4 14 6 3 11 1 9 15 7 13 5 ∣ M_4 = \left|\begin{matrix} 0 & 8 & 2 & 10\\ 12 & 4 & 14 & 6\\ 3 & 11 & 1 & 9\\ 15 & 7 & 13 & 5\end{matrix}\right| M4= 0123158411721411310695
用代码实现阈值矩阵的构造:
'''
@param size 该阈值矩阵的尺寸
'''
def CreateOrderedDitherMatrix(size):
#用来判断传入的矩阵尺寸是否合法
if size <= 0 or (size & (size - 1)) != 0:
print("size不是2的幂数")
return None
#首先构造尺寸为0的阈值矩阵
Mdest = Mtemp = np.mat([[0]])
#根据传入的尺寸是2的几次幂为依据 来迭代递推目标尺寸的阈值矩阵
for _ in range(int(math.log(size, 2))):
#利用numpy的矩阵拼接函数 hstack实现水平拼接 vstack实现竖直拼接
temp1 = np.hstack((Mtemp * 4, Mtemp * 4 + 2))
temp2 = np.hstack((Mtemp * 4 + 3, Mtemp * 4 + 1))
Mdest = Mtemp = np.vstack((temp1, temp2))
return Mdest
通过该函数可以生成任意尺寸的阈值矩阵,这样就可以实现对目标图像的二值化。但在使用的时候可不是直接用该矩阵去进行比较,因为矩阵和图像的量纲不同,所以没有可以进行比较的依据。比如对于灰度图像其像素范围是0-255共256(28)个范围,而不同尺寸的矩阵范围为0-n2-1。现在要做的就是统一量纲,归一化。
对于图像
S
r
c
[
x
]
[
y
]
2
8
而对于矩阵
M
n
[
x
%
n
]
[
y
%
n
]
n
2
对于图像 \frac{Src[x][y]}{2^8} \quad 而对于矩阵\frac{M_n[x\%n][y\%n]}{n^2}
对于图像28Src[x][y]而对于矩阵n2Mn[x%n][y%n]
形式化简可得 S r c [ x ] [ y ] × n 2 2 8 与 M n [ x % n ] [ y % n ] 相比较 形式化简可得 Src[x][y]×\frac{n^2}{2^8}与M_n[x\%n][y\%n]相比较 形式化简可得Src[x][y]×28n2与Mn[x%n][y%n]相比较
最后该算法用数学公式表示为:
D
s
t
I
m
g
[
x
]
[
y
]
=
{
0
S
r
c
[
x
]
[
y
]
×
n
2
2
8
<
M
n
[
x
%
n
]
[
y
%
n
]
255
S
r
c
[
x
]
[
y
]
×
n
2
2
8
≥
M
n
[
x
%
n
]
[
y
%
n
]
DstImg[x][y] = \begin{cases} 0 & Src[x][y]×\frac{n^2}{2^8}<M_n[x\%n][y\%n] \\ 255 & Src[x][y]×\frac{n^2}{2^8}≥M_n[x\%n][y\%n]\end{cases}
DstImg[x][y]={0255Src[x][y]×28n2<Mn[x%n][y%n]Src[x][y]×28n2≥Mn[x%n][y%n]
有序颜色抖动代码实现:
'''
@param src 要处理的源图像
@param size 阈值矩阵的尺寸
'''
def Ordered_dither(src, size=8):
# 处理的对象得是一个灰度图 所以根据图像的shape属性来判断传入的参数是否为灰度图
if len(src.shape) != 2:
print("[Random_dither]:Require a Gray Image")
return src
# 由于python的数组类型是浅拷贝,可以理解成C语言的指针,防止对外部图像产生影响,所以需要单独复制一份
srcImg = src.copy()
# 构造阈值矩阵
ODMatix = CreateOrderedDitherMatrix(size)
# 根据尺寸确定上面公式的参数 n²/2⁸
ratio = size ** 2 / 256
srcImgHeight, srcImgWeight = srcImg.shape
for h in range(srcImgHeight):
for w in range(srcImgWeight):
# 实现该分段函数
srcImg[h][w] = 255 if srcImg[h][w] * ratio > ODMatix[h % size, w % size] else 0
return srcImg
效果图:
可以看到二值化后的效果图与原图信息所表示的内容几乎差不多,如果仔细观察像素的分布很规范,可能就也是其名为有序颜色抖动的原因吧。
四.误差扩散颜色抖动😍
这个我自认为是效果最好的抖动算法,其中最经典的为Floyd–Steinberg
算法。其算法原理是把一点像素二值化产生的误差值即(二值化之前的像素值与二值化之后像素值的差值 会出现负值的可能)扩散给周边相邻像素,可能就也是其名为误差扩散颜色抖动的原因吧。
其中扩散的权重用矩阵可以表示为:
[
∗
7
16
3
16
5
16
1
16
]
\left[\begin{matrix} \\ & * & \frac{7}{16} \\ \frac{3}{16} & \frac{5}{16} & \frac{1}{16}\end{matrix}\right]
163∗165167161
*表示当前处理的像素位置,其误差分别向右方和下方扩散。
误差扩散颜色抖动代码实现:
'''
@param src 要处理的源图像
@param thresh 二值化的阈值
'''
def Error_diffusion(src, thresh=128):
# 处理的对象得是一个灰度图 所以根据图像的shape属性来判断传入的参数是否为灰度图
if len(src.shape) != 2:
print("[Random_dither]:Require a Gray Image")
return src
# 由于python的数组类型是浅拷贝,可以理解成C语言的指针,防止对外部图像产生影响,所以需要单独复制一份
srcImg = src.copy()
srcImgHeight, srcImgWeight = srcImg.shape
# 因为对于图像的最左边是没有右边的 在误差扩散的时候会造成超出索引越界错误 同理图像最下面也是 所以对图像最右和最下扩充一位
srcImg = cv.copyMakeBorder(srcImg, 0, 1, 0, 1, cv.BORDER_DEFAULT)
# 由于对于误差的扩散会涉及浮点数运算,对图像像素的数值类型进行重新定义,如果不这样做的话会导致实际运行效果很慢
# 对于400x400的图像不更改的话耗时高达3s 而重新定义之后耗时就仅仅0.5s
srcImg = np.array(srcImg, dtype=np.float)
for h in range(srcImgHeight):
for w in range(srcImgWeight):
# 记录二值化之前的像素值
oldPixel = srcImg[h, w]
srcImg[h, w] = 0 if oldPixel < thresh else 255
# 进行比较最后得到要扩散的误差
errValue = oldPixel - srcImg[h, w]
# 误差扩散
srcImg[h, w + 1] += errValue * 7.0 / 16
srcImg[h + 1, w] += errValue * 5.0 / 16
srcImg[h + 1, w - 1] += errValue * 3.0 / 16
srcImg[h + 1, w + 1] += errValue * 1.0 / 16
return srcImg.astype(np.uint8)[0:srcImgHeight, 0:srcImgWeight]
效果图:
可以看到二值化后的效果图与原图信息所表示的内容不是几乎差不多,而是简直一模一样,根本看不出来该图像只有黑白两个像素表示。为了防止有人说虚假营销。
局部放大图:
在不信就真的没办法了。
五.算法用时🤗
参考
关于颜色抖动(dithering) - 知乎 (zhihu.com)