图像锐化是和平滑相反的操作,目的是为了使模糊的图像变得清晰。既然是相反的目的,那么操作应该也是相反的,平滑使用的是图像邻域的加权求和或者是积分运算,而锐化则是使用求导数(梯度)或有限差分来实现。
对于二维的连续函数,在点(x,y)处的梯度为
其中两个偏导数的计算公式为
梯度的幅值作为变化率的度量,其值如下。为什么要求梯度的幅值?想一下平滑的处理思路,最简单的平均平滑,在简单的一维情况下就是就某个位置上的数值替换成基于这个位置的邻域的积分值;而积分的逆变换就是微分,也就是将该位置的数值替换成这个位置的效斜率。所以二维情况下,就是将梯度幅值的值作为新的值。
对应到我们的二维离散图像中,就用差分来代替求导
上式中涉及平方和开方的运算,可以近似为绝对值的形式
而在实际应用中,经常被采用的是另一种近似梯度——Robert交叉梯度。可以将第一项和第二项都用模板的形式展示,如下所示。
使用模板参与计算的方法和前面一致,只是根据我们的习惯,更偏向于采用奇数尺寸的模板。所以有一种计算Sobel梯度的Sobel模板更加常用。我看的书中也没有关于Sobel梯度的推导,百度了一下也似乎没看到有人怎么详细的写。所以就自己推了一下。基本的思想和推导Robert交叉梯度是相同的,都是用绝对值的和来代替平方和开方,减少计算量。区别在于将2x2的模板扩张成3x3,很直观的:
此时我们可以将w1写成
进而继续调整,考虑到,即一个点的数值近似与它两侧(可以是上下也可以是左右)的点的数值的均值,适当变形代入上式中可得
整理成模板,即为
同理我们可以推导出
接下来我们试试将我们推到得到的Sobel模板,用于一张相对简单的图片来看看效果。为了计算方便,我们可以直接用前面写的一些函数拿来,然后修改其中的滤波模板,就可以达到效果。注意边界填充的方式,之前我们讲到,平滑的目的是为了消除噪声而不是突出边缘,所以各种各样的填充方法都可以尝试,影响不大;但是现在是为了突出图像中有意义的边缘,所以当我们在边界是做全0填充时,很有可能填充的部分和图像的边界就形成了变化剧烈的边缘,进而较大的梯度幅值在我们调整灰度范围时,将真正的图像中有意义的边缘的数值范围压缩到很小的范围,导致图像不清晰。所以最好要使用重复填充或者是镜像填充。
注意一下,因为我们的模板中含有负数,而图像中的像素点的数值类型是uint8,在相乘时很可能会发生截断。所以首先要确保先将图像中的像素点的数值类型在与模板相乘时转化成整数型,或者是在读取图像时做一下设定如下:
from PIL import Image
import numpy as np
im = Image.open(...)
arr = np.array(im, dtype = "int16")
然后计算完的数值,有可能大于0也有可能小于0,我们关心的是绝对值的大小所以可以取绝对值。最后需要将数值的范围调整到0-255之间即可。实现的代码如下:
from PIL import Image
import numpy as np
def SobelFilter(src, dst, filter_kind = 1, padding = "replicate"):
imarray = np.array(Image.open(src),dtype='double')
#print(imarray.dtype)
height, width = imarray.shape
new_arr = np.zeros((height, width), dtype = "uint16")
filter1 = np.array([[-1,0,1],[-2,0,2],[-1,0,1]])
filter2 = np.array([[-1,-2,-1],[0,0,0],[1,2,1]])
if filter_kind == 1:
filter = filter1
elif filter_kind == 2:
filter = filter2
k = filter.shape[0]
for i in range(height):
for j in range(width):
total = 0
for n in range(pow(k,2)):
'''
k = 3, n = 0, 1, 2 ..., 8, a = -1, 0, 1, b = -1, 0, 1
k = 5, n = 0, 1, 2, 3 ..., 24, a = -2, -1, 0, 1, 2
'''
a, b = int(n//k - (k-1)/2), int(n%k - 1)
#filter_value
aa, bb = int(n//k), int(n%k)
f_value = filter[aa, bb]
if i + a <= 0:
if j + b <= 0:
total += imarray[0, 0]*f_value
elif j + b >= width - 1:
total += imarray[0, -1]*f_value
else:
total += imarray[0, j + b]*f_value
elif i + a >= height - 1:
if j + b <= 0:
total += imarray[-1, 0]*f_value
elif j + b >= width - 1:
total += imarray[-1, -1]*f_value
else:
total += imarray[-1, j + b]*f_value
else:
if j + b <= 0:
total += imarray[i + a, 0]*f_value
elif j + b >= width - 1:
total += imarray[i + a, -1]*f_value
else:
total += imarray[i + a, j + b]*f_value
new_arr[i, j] = abs(total)
max = np.max(new_arr)
min = np.min(new_arr)
final_arr = np.zeros((height, width), dtype = "uint8")
for i in range(height):
for j in range(width):
final_arr[i, j] = 255*(new_arr[i, j] - min)/(max - min)
final_im = Image.fromarray(final_arr)
final_im.save(dst)
#print("Suceess.")
src = "C:/Users/60214/Desktop/python_work/DigitalExecution/geometry.jpg"
dst1 = "C:/Users/60214/Desktop/python_work/DigitalExecution/sobel1.jpg"
dst2 = "C:/Users/60214/Desktop/python_work/DigitalExecution/sobel2.jpg"
SobelFilter(src, dst1, 1)
SobelFilter(src, dst2, 2)
得到的结果如下。可以看到,
,这两个不同的滤波模板,前者突出了图像中竖直方向上的边缘,而后者突出的是水平方向上的边缘,各有特长。
二维函数f(x,y)的二阶微分定义为
同理的,我们要试图用差分来代替微分
即
两式相加就得到了用于图像锐化的拉普拉斯算子
对应的滤波模板为
记得在一维微分的锐化中,我们对响应都取了绝对值,这意味着滤波模板与图像图像像素的计算值,绝对值相同的正值和负值表示相同的响应,所以也等同于使用下面的模板。
然后来分析模板的结构。所谓模板的结构,例如W1,模板如果旋转90度,实际上还是同一个模板;而Sobel算子,旋转90度会变成一组的另一个算子,这就对应了,一个水平一个竖直的特性。那么拉普拉斯算子,既然旋转90度等于自身,也就说明它对接近水平和接近竖直方向的边缘都有很好的加强。更进一步,我们构造对于45度旋转各向同性的滤波器如下:
计算的代码和上面的一致,只是模板修改了一下下。但从结果中,看到效果并不好,甚至非常的模糊,这是因为我第一次实践使用的图片是素描作品,图像比较粗糙,不适合用来做这种操作。
filter1 = np.array([[0,1,0],[1,-4,1],[0,1,0]])
filter2 = np.array([[1,1,1],[1,-8,1],[1,1,1]])
filter3 = np.array([[1,4,1],[4,-20,4],[1,4,1]])
用回之前的动漫人物,处理的结果如下。可以看到,大致勾画出了人物的边缘,是比较准确有效的,其中,第二幅图和第三幅图都比第一幅图效果要好,是因为它们俩对于倾斜的45度走向的四个方向的边缘的捕捉能力要强于主要突出水平和竖直的第一种滤波模板。
一阶导数和二阶导数都有锐化图像,突出边缘的作用。区别可以归纳成三个:第一,一阶导数通常会产生较宽的边缘;第二,二阶导数对于阶跃性边缘中心产生零交叉,而对屋顶状的边缘,二阶导数取极值;第三,二阶导数对细节有较强的响应,如细线和孤立噪声点。