1. Canny边缘检测算法实现
姓名:许展风 学号:3210100658
电子邮箱:zhanfeng_xu@outlook.com 联系电话:15224131655
老师:潘纲老师 报告日期:2023年11月22日
1.1. 一、功能简述及运行说明
1.1.1. 1.1 功能简述
对输入的一张彩色图像,通过Canny Edge 检测算法实现边缘检测,输出中间结果与最终结果图。
1.1.2. 1.2 运行说明
输入:将彩色图片放在与二进制源程序同一文件夹,运行程序,会跳出命令框,显示”the name of picture fire:“,此时输入彩色图片的文件名(包括后缀),回车后,显示”input the highThreshold:“,输入高阈值,回车后,显示”input the lowThreshold:“输入低阈值,再次回车,程序开始计算,计算完毕将显示中间结果图,包括梯度幅值图、非极大值抑制后结果图、最终结果图,按下Esc关闭图像,将显示最终彩色边缘图,按下Esc关闭图像,程序自动将显示的结果图片保存于同一文件夹。

1.2. 二、开发与运行环境
编程语言:python 3.10.6
编程环境:VScode+Jupyter Notebook
运行环境:Windows
1.3. 三、算法原理
1.3.1. 3.1 算法流程图
graph LR
A[彩色图片]
A --> C[灰度化处理]
C --> D[高斯滤波]
D --> E[计算图像梯度]
E --> F[非极大值抑制]
F --> G[双阈值判断连接边缘]
G --> H[覆盖彩色图片]
1.3.2. 3.2 具体原理介绍
1.3.2.1. 1. 灰度化处理
$$ Image_{gray} = 0.29 \times I_R + 0.587 \times I_G + 0.114 \times I_B $$
灰度化可由,原图像红、绿、蓝三个维度上的像素值线性变换实现。
1.3.2.2. 2. 高斯滤波
高斯滤波利用高斯函数生成的高斯核(高斯模板)对图像进行卷积。每个像素点由周围点的加权平均得到,加权系数则由高斯核决定。通过高斯滤波能对图像进行初步降噪,除去噪声可能引起的边缘。
1.3.2.3. 3. 计算图像梯度
$$ G_x = \left[ \begin{array}{c} -1 & 0 & 1 \ -2 & 0 & 2 \ -1 & 0 & 1 \end{array} \right] \otimes Image, G_y = \left[ \begin{array}{c} 1 & 2 & 1 \ 0 & 0 & 0 \ -1 & 2 & -1 \end{array} \right] \otimes Image $$
使用Sobel算子计算灰度图像在水平和垂直方向的梯度值,从而得到每个像素点的梯度强度和方向。计算方式同样是卷积。通过$G_x$与$G_y$进一步得到图像梯度幅值与梯度方向。通过梯度计算得到的点集包括了所求边缘。 $$ M(x,y)=\sqrt{G^2_x(x,y)+G^2_y(x,y)} \ \theta(x,y)=arctan(\frac{G^2_y(x,y)}{G^2_x(x,y)}) $$
1.3.2.4. 4. 非极大值抑制

将梯度方向离散化,分类为4个方向,对于任意像素点,在其对应的梯度方向上比较相邻的像素点,若其不是其中的最大值,则抑制该像素点。非极大值抑制可以将梯度图中的边缘突出,使得边缘检测精度更高。
1.3.2.5. 5. 双阈值判断连接边缘
设定两个阈值,一个为高阈值,一个低阈值,将高于高阈值像素的点标记为强边缘,低于高阈值、高于低阈值的点标为弱边缘,将低于低阈值的点标记为非边缘。抑制所有非边缘,保留所有强边缘。考察弱边缘,若其周围像素点中有强边缘,则保留,否则抑制。合适的阈值选择可以除去内部不需要的弱边缘,并连接不连续的边缘。
1.4. 四、具体实现
1.4.1. 4.1 灰度化处理
# 图片转灰度
imageGray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
直接使用openCV函数库中的函数,得到灰度图片。
1.4.2. 4.2 高斯滤波
# 进行高斯模糊处理
imageGauss = cv2.GaussianBlur(imageGray, (3, 3), 1)
使用openCV函数库中的函数,其中(3,3)指的是大小为3×3的高斯核。
1.4.3. 4.3 计算图像梯度
for i in range(1, Size[0] - 1):
for j in range(1, Size[1] - 1):
P[i, j] = 2 * (image[i + 1, j] - image[i - 1, j]) +
image[i + 1, j + 1] - image[i - 1, j + 1] +
image[i + 1, j - 1] - image[i - 1, j - 1]
Q[i, j] = 2 * (image[i, j + 1] - image[i, j - 1]) +
image[i + 1, j + 1] - image[i + 1, j - 1] +
image[i - 1, j + 1] - image[i - 1, j - 1]
M[i, j] = np.sqrt(P[i, j]**2 + Q[i, j]**2)
if (P[i, j] != 0):
theta[i, j] = np.arctan(Q[i, j] / P[i, j])
else:
theta[i, j] = np.pi / 2
x、y方向上的梯度可直接由公式计算,同时计算梯度方向时,要单独考虑x方向为0的情况,此时方向为$\frac{\pi}{2}$。
1.4.4. 4.4 非极大值抑制
通过方向角离散化,将方向角分类并标记,考虑到np.arctan函数的返回值是$(-\frac{\pi}{2},\frac{\pi}{2}]$,因此只需对该范围内进行离散。
非极大抑制函数中,通过方向角标记决定像素点值比较的方向,根据比较结果决定是否抑制。
1.4.5. 4.5 双阈值连接边缘
# 双阈值连接边缘
def EdgeJoint(M, highThreshold, lowThreshold):
'''双阈值判断后连接边缘
:M: 输入图像
:highThreshold: 高阈值
:lowThreshold: 低阈值
:return M: 输出处理后图像
'''
Size = np.shape(M)
high = np.zeros(np.shape(M))
low = np.zeros(np.shape(M))
for i in range(1, Size[0] - 1): # 判定得到分布图
for j in range(1, Size[1] - 1):
if (M[i, j] > highThreshold):
high[i, j] = 1
elif (M[i, j] > lowThreshold):
low[i, j] = 1
for i in range(1, Size[0] - 1):
for j in range(1, Size[0] - 1):
if (high[i, j] == 1 and low[i, j] != 1): # 遍历所有强边缘,以其为起点进行边缘连接
[high, low] = Joint(high, low, i, j)
return np.uint8(high * M)
def Joint(high, low, i, j):
'''连接指定点的边缘
:high: 高阈值分布图
:low: 低阈值分布图
:i: 目标点横坐标
:j: 目标点纵坐标
:return [high, low]: 高、低阈值分布图
'''
Size = np.shape(high)
if (i != 0 and j != 0 and i != Size[0] - 1 and j != Size[0] - 1):
for k in range(0, 3):
for m in range(0, 3):
if (high[i - 1 + k, j - 1 + m] != 1
and low[i - 1 + k, j - 1 + m] == 1): # 将弱梯度点标记为强梯度,同时对该弱梯度作为新的连接起点
high[i - 1 + k, j - 1 + m] = 1
[high, low] = Joint(high, low, i - 1 + k, j - 1 + m)
return [high, low]
先根据双阈值判断得到两张图,一张标记了强边缘点,一张标记了弱边缘点。主函数遍历所有强边缘点,对于任意强边缘通过函数Joint对周围的弱边缘点进行连接。而Joint函数,扫描输入点的周围像素,发现还未标为强边缘的弱边缘,则将其标记并以此像素点作为起点再次通过Joint函数连接边缘,形成迭代。
1.4.6. 4.6 彩色边缘图片获取
def mask(maskImage,image):
try:
if (np.shape(maskImage)!=np.shape(image)[0:2]):
raise ValueError('!图片大小不一致!')
except Exception as e:
print(e)
return
Size = np.shape(image)
for i in range(0,Size[0]):
for j in range(0,Size[1]):
if(maskImage[i,j]==0):
for k in range(0,3):
image[i,j,k] = 0
return np.uint8(image)
判断两张输入图片大小是否一致,一致后执行覆盖操作,相当于对于原彩色图像,当覆盖图像像素值为0,则抑制对应处的彩色图像像素,由于彩色图片是三维度,在第三个维度有RGB三个通道,因此需要将三个通道都抑制。
1.5. 五、实验结果与分析
1.5.1. 5.1 梯度计算结果
lena图片:


梯度幅值反映了每个像素点周边的像素变化大小,处于边缘的像素点梯度值大,则在灰度显示中越白,变化越小,则越黑。因此梯度幅值图中白线勾勒出图片的边缘。可以看到此时的边缘线较粗,勾勒出了大部分轮廓。
个人图片:


将个人照片进行梯度计算,得到梯度幅值图,可以看到白线也较为明显地勾勒出边缘。对比图中肩膀的部分,可以看到原图中因为拍摄时光线原因,左边轮廓更明显,右边衣服与背景颜色相近轮廓不明显。因此在梯度幅值图中,左边边缘更亮更明显,而右边几乎没有轮廓。
1.5.2. 5.2 非极大值抑制处理结果
lena图片:


非极大值抑制处理后,可以看到边缘线条得到了精确与细化,特别是lena的头发处的大片粗线条,抑制为更精致的细线条。lena的鼻子、嘴处的部分,从高梯度区域识别为线条化的边缘。
个人图片:


对个人照片可以看到有同样的细化精确的效果。
1.5.3. 5.3 双阈值判断连接边缘处理结果
lena图片:
H=50, L=20 | H=150, L=200 | H=230, L=20 | H=230, L=100 |
---|




用不同的高低阈值选择,得到不同的边缘连接结果:像素点的幅值大小是0-255,当H=50,L=20时,高低阈值的选择都偏低,有大部分的边缘被确定为强边缘而保留,且更多的弱边缘被连接,基本保留了大部分上一处理结果的像素;当H=150,L=100时,小于100的像素将被抑制,因此去除了图中原本‘灰色’的内部边缘;当H=230,L=20时,高阈值被定位很高,但低阈值很低,由此将保留少部分特别强的边缘以及连接上其周围的大部分弱边缘,所以细节更少但是能保留较为完整的边缘线条;当H=230,L=100时,只有较强的像素点被保留为强边缘或可连接的弱边缘,因此几乎只有强边缘被保留,很少的较强弱边缘能被保留,因此图中形成不完整的边缘线条,几乎看不到灰色的弱边缘线条。
个人图片:
H=50, L=20 | H=150, L=200 | H=230, L=20 | H=230, L=100 |
---|




用相同的分析方法能得到与Lena图片相同的结论,但是可以看到在lena图片,H=150,L=100可以得到一个较好的边缘检测结果,而在个人图片中,H=50,L=20会更好一些。因为不同图片的梯度范围是不同的,所需的目标边缘所处的像素值区间不同,因此想得到一个好的边缘检测结果,需要针对图片进一步调整阈值。
1.5.4. 5.4 最终彩色图片
lena图片:


将彩色图片覆盖后,得到彩色的边缘图,原边缘图中像素值存在的点被替换为彩色图片的原像素值。
个人图片:


1.6. 六、结论与心得体会
1.6.1. 结论
Canny算法有效地得到了一个彩色图片的边缘,实现了边缘检测。在使用Canny边缘检测算法时,有两个可调参数是双阈值检测的高低阈值,要根据实际的目标图片调整高低阈值的大小,才能得到更符合要求的边缘检测图片。
1.6.2. 心得体会
在编写Canny边缘检测算法的过程中,进一步理解了算法的主要步骤及其意义。
当编写图像梯度计算函数时,发现了在计算过程中要使用浮点类型的像素值,而在图片展示函数时,图片像素应为整形,因此图像的输入时,要将图像明确为float,而在函数输出图象时,应输出为int类型。
双阈值连接边缘的函数编写过程中,反复思考了如何实现强边缘对弱边缘的连接,最后仍是需要用比较多的空间(两个标记矩阵)以及反复迭代的方法来实现,特别是迭代时,会有一种类似栈或树的结构的感觉。也许用C++等语言,利用链表可以更好地编写这一函数。