文章目录
- Canny算子
- 非极大值抑制
- 非极大值抑制中的插值
- 滞后阈值
- 实际应用
- 直接使用Canny算子
- 使用膨胀
- 先阈值分割
Canny算子
上一篇说到,我在一个小项目里需要在一幅图像中提取一根试管里的两种液体的截面。为了达到这个目的使用传统图像里的区域分割技术,实际上就是想把这个图像分成两类,然后再找到这个两个类的边界。
上一张最后提到,我是使用一种拟合的方法来做的边界的判断,后来突然想到,opencv里面提供了现成的方法:边缘检测的Canny算子,直接就可以提取图像的边界。
Canny算子在官网上有介绍:
- 调用方式:edges = cv2.Canny(image, threshold1, threshold2, apertureSize, L2gradient)
- 参数一:输入图像,为二值图像。
- 参数二:阈值1,和阈值2一样,是用于控制边缘检测的度的,后面在详细过程说明中来描述。
- 参数三:阈值2
- 参数四:apertureSize,sobel算子的卷积大小。sobel算子用于计算图像的梯度,参考sobel算子的文章(https://blog.csdn.net/pcgamer/article/details/127942102?spm=1001.2014.3001.5502)
- 参数五:L2gradient,是否使用二级梯度计算。实际上就是使用Laplacian算子进行二阶梯度计算,同样可以参考上面的那片文章。
- 返回值就是一个只有边缘信息的二值图像。
Canny算子的一般介绍中,主要提到了一下几个步骤:
- 噪声去除
- 计算梯度
- 非极大值抑制
- 滞后阈值
非极大值抑制
噪声去除这个步骤一般是采用高斯模糊在做的,这个在滤波的那一篇中已经提到过,这里就不多说了,可以参考:
https://blog.csdn.net/pcgamer/article/details/124989015?spm=1001.2014.3001.5502
计算梯度之前也提过了,这里也不多说。
所以从非极大值抑制这里继续。
非极大值抑制,用人话说就是只留下最大值。那么,这里有几个问题:
- 为什么留下最大值?
- 留下什么最大值?就是这个最大值怎么定义?
- 怎么留下最大值?
首先回答为什么的问题,在图像处理中,一般来说,边缘就是像素值变化最大的地方,这个很容易理解。
所以第二个问题也很简单,这里的最大值就是上文提到的梯度最大。而且是在梯度方向上梯度最大。
在写清晰度的那片文章中提到了梯度方向的计算方式,其实可以理解成当前边缘方向的法线放下那个,有点绕吧,用几个图来说明一下:
首先,梯度方向定义为
a
r
c
t
a
n
(
g
x
g
y
)
arctan(\frac{g_x}{g_y})
arctan(gygx),也就是
放大到一整张图中:
黑线指向的方向就是梯度的方向。
那么上面说到非极大值抑制,就是要确定某一个点在这条梯度方向是是附近(局部)的最大值。一般来说,这个附近就是旁边的一个像素值。
非极大值抑制中的插值
虽然就是比较这个梯度方向上的三个像素点,但是这不是能直接比较的。
-
首先,上面的梯度方向不是固定的,只有当这个梯度方向恰好是45度的整数倍时,才恰好对应到一个像素点:
- 可以从图中看到,如果是梯度方向是45,225的时候,中心像素点就是和对角线上的两个像素点比较(右上和左下)
- 如果是梯度方向是135,315的时候,中心像素点就是和对角线上的两个像素点比较(左上和右下)
- 如果是0,180,就是和左右两个像素点做比较
- 如果是90和270度,就是和上下两个像素点做比较。
- 上面的几种情况都是比较巧合的情况,但是如果不是这些角度呢?
-
如果不是这些角度,就需要进行插值了,见下图:
比如上面的dTemp1这个点,很明显不是一个实际存在的像素点,这个像素点可以被称作一个亚像素(sub pixel),这个点的像素值可以根据旁边的两个实际像素点来插值计算,在canny算子中,插值方法就是普通的线性插值:
这个亚像素的dTemp1的像素值就是:
d T e m p 1 = p 1 + L d T e m p 1 − p 1 p 2 − p 1 dTemp1 = p1 + \frac{L_{dTemp1} - p1}{p2-p1} dTemp1=p1+p2−p1LdTemp1−p1
上面的 L d T e m p 1 L_{dTemp1} LdTemp1可以根据梯度方向的 g x g y \frac{g_x}{g_y} gygx来计算出来同样,下面的dTemp2也可以通过插值方法计算出来。
-
插值计算完成后,就可以完成非极大值抑制的比较计算了。如果中心像素是最大的,那么就保留,不是极大值,则赋值为0。
-
以上面的逻辑完成整张图像的计算,就完成了非极大值抑制这个过程的计算。
滞后阈值
完成非极大值抑制后,边缘已经精细了很多了,但是还不保证所有留下的像素点都是边缘,所以最后一步是通过阈值来控制这些留下的梯度值(从非极大值抑制出来的数据已经不是像素值,而是梯度值矩阵),单独通过一个阈值来控制过于简单粗暴,所以Canny算子用了两个。。。。
- 如果高于T1,也就是两个阈值中较高的阈值,则保留。
- 如果低于T2,也就是两个阈值中较低的阈值,则丢弃。
- 中间的怎么办呢?从所有的高于T1的梯度值出发,如果能连接的上的中间值,则保留,否则就丢弃。
- 一般来说T1 = 2T2
完成上面的滞后阈值计算后,就完成了Canny算子的边缘检测(当然最后是输出像素值的二值图像)
实际应用
直接使用Canny算子
不管三七二十一,我把之前的图转换成灰度图后,直接调用Canny算子:
img = cv2.imread("./images/tubeImg.jpeg")
grey = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edge = cv2.Canny(grey, 120, 54)
cv2.imshow("origin", grey)
cv2.imshow("result", edge)
效果非常的糟糕:
使用膨胀
因为Canny算子中的高斯模糊针对的是微小的高斯噪声,这个图像中的噪声都是大块的噪声,所以我就想着用膨胀函数试一下:
img = cv2.imread("./images/tubeImg.jpeg")
grey = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 做一把膨胀
kernel_e = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
bin_clo = cv2.dilate(grey, kernel_e, iterations=10)
edge = cv2.Canny(bin_clo, 120, 54)
cv2.imshow("origin", grey)
cv2.imshow("result", edge)
效果好了一点:
先阈值分割
但是还是去的不干净,而且最上面的分界线没有弄完成,突然想到上一次用大津法的阈值分割基本已经完成了前景和背景的分割,再加上一次阈值分割,用膨胀再清除一把,最后再做边缘检测:
img = cv2.imread("./images/tubeImg.jpeg")
grey = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 如果大于阈值,赋值为255,小于阈值,赋值为0
# ret, binary = cv2.threshold(grey, 60, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
#
# 做一把腐蚀
kernel_e = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
bin_clo = cv2.dilate(grey, kernel_e, iterations=10)
edge = cv2.Canny(bin_clo, 120, 54)
cv2.imshow("origin", grey)
cv2.imshow("result", edge)
效果妥妥的:
大功告成!