初级项目_边缘检测

任务:编写一个钱币定位系统,其不仅能够检测出输入图像中各个钱币的边缘,同时,还能给出各个钱币的圆心坐标与半径。

代码撰写说明:

  • 代码可以使用C++或者python语言进行编写,推荐使用python;

  • 可直接调用Opencv的Canny与HoughCircle算法完成系统设计;

设计文档要求:介绍算法整体流程,各个函数的功能说明,函数的输入参数说明,给出最终拟合结果图,分析各个参数对于最终定位结果的影响。


参考链接:

一、实验原理

1.Canny算法

Canny边缘提取算法是一个经典的边缘提取算法,主要贡献是双门限和非最大化抑制思想,一个完整的Canny算法实现步骤依次为:

  1. 先使用高斯偏导滤波器进行偏导滤波;
  2. 计算每个点的梯度;
  3. 进行非最大化抑制;
  4. 双门限法;

边缘和图像其他部分的区别主要在于边缘部分的信号是突变的,那么数学中应该如何找信号突变? –- 这转化为导数操作,即对信号求导,找到其极值点,但二维空间中的偏导数计算公式计算较难,计算机视觉中的近似导数公式如下

上述公式表明了边缘提取任务可以使用卷积来计算,即对x方向的偏导数和对y方向的偏导数分别用在x方向的卷积核和y方向的卷积核来计算;

对于有噪声的原始信号,直接用卷积核求偏导会出现问题,需要先使用高斯平滑核进行平滑去噪;高斯偏导核是高斯平滑核的一种变形,即对高斯平滑核求偏导;

尽管高斯偏导核可以进行一定程度的平滑,但还是会提取一些无关紧要的噪声,因为某些区域的幅值较小可能是由于噪声引起的,因此设置一个门限将低于该门限的边缘去除。门限设置过高会导致原本某些边会被去除,但是门限设置的过低又会导致某些假边存在(噪声边),双门限法的思想在于,先用高门限将粗狂的边检测出来 – 这些边是噪声的可能性较低,接着降低门限使得较弱的边显现,只选择与粗狂边有连接的边保留;

边缘提取的另一个问题在于直接提取得到的边是粗边,这是由于信号缓慢增强因此求导后的值也是缓慢变换而非峰值,因此设置的门限以内的值都被认为是边,解决方法是对于粗边上的每一个像素点,与其梯度方向的前后点的梯度比较,保留值较大者,这就是非最大化抑制;

2.Hough变换

仅靠边缘提取不能对物体进行整体的描述,提取完边缘后如何使用数学模型来描述边缘?这就需要使用拟合,Hough变换是一种常用的拟合方式,其基本思想为:

  • 霍夫变换同样是使用图像上的所有点为直线模型投票,选择票数最高的模型进行输出;
  • 噪声点不会有一致性的答案;
  • 即使中间的某些点被遮挡也能拟合一个好的模型;

霍夫变换的原理是图像空间中的一个点对应参数空间的一条直线,那么图像空间中的两个点对应参数空间中的两条直线,参数空间中这两条直线的交点对应的参数实际就是图像空间中两点确定的直线的参数;

当图像空间中的点足够多的时候,参数空间中会存在多条直线,这些直线的交点(理解为得分最高的grid)就是图像空间中拟合的直线模型的参数;

使用Hough变换需要注意以下几点:

  • 选取合适大小的grid,即对参数空间进行适当的离散化;
  • 投票的时候采取“软投票”策略:即不仅仅只对中心网格投票,也适当的按照一定比例给网格周围的grid投票;
  • Canny算子
    • 只采用检测出的边上的点进行投票;
    • 因为在Candy算子中得到点时就知道了梯度方向,相应的边缘方向的范围就大概确认了,故可以缩小θ的范围,从而解决了噪声的影响,也简化了计算;

一个简单的Hough变换的算法代码如下

我们的任务是在边缘图上利用Hough变换计算圆心和半径,圆拟合的基本思想为:给圆上一点和半径r,可以在参数空间对应两个点(圆心位置为圆周点加减半径,方向由梯度方向确定),圆心必定是投票数最高的地方,此时的(x,y,r)就是圆心坐标和圆半径

因为在实际的求解中并不知道圆周上每一点(x,y)以及半径,所以需要穷举所有的r(大于0小于图像长度),遍历圆上每一点进行投票;

二、程序设计

1.算法流程

本次试验的目的是编写一个钱币定位系统,其不仅能够检测出输入图像中各个钱币的边缘,同时,还能给出各个钱币的圆心坐标与半径;

我们将该任务初步分为两个部分,第一个部分是检测输出图像中各个钱币的边缘(更通俗的说是检测图像中各个圆的边缘),第二部分是给出检测出的圆的圆心坐标和半径;

第一部分需要使用Canny边缘检测算法和Hough圆检测算法,其流程可描述为:

1
读取图像 --> 图像预处理(灰度化、滤波处理(采用高斯滤波))--> 边缘检测(Canny算法) --> Hough圆检测 --> 在图像中标出圆形区域

第二部分输出检测出的圆的圆心坐标和半径,经过了解得知可以将第二个任务包含到第一个任务中(因为cv2.HoughCircles()函数返回的是一个三维数组,其中每个元素包含三个值,即圆心的x坐标、y坐标和半径,可以通过遍历这个数组来获取所有检测到的圆的圆心坐标和半径),因此我们的最终算法流程如下:

1
读取图像 --> 图像预处理(灰度化、滤波处理(采用高斯滤波))--> 边缘检测(Canny算法) --> Hough圆检测 --> 输出检测到的圆的信息(例如圆心坐标和半径)--> 在图像中标出圆形区域

整体算法流程描述为:初始读入待检测图像,将其转换为灰度图并使用高斯滤波核进行滤波处理以减少噪声影响,接着使用Canny算法对图像进行边缘检测,找出图像中的所有边缘,接着在提取出边缘的基础上使用Hough圆检测算法检测图像中的圆并输出已检测出的圆心坐标和半径,并在原图中标示出圆形区域;

2.函数说明

在实现过程中除了Canny算法和Hough算法以外还使用了大量opencv库中的函数,如imread、imwrite等,这些函数的使用方式参考这篇文章:Py之cv2

2.1 Canny边缘提取

直接调用Opencv库分别实现Canny边缘检测和Hough圆检测,首先导入必要的库以及待检测图像

1
2
3
4
5
6
7
import cv2
import numpy as np

image_path='D:\My_code\jupyter notebook\Data_Pictures\coin.jpg'
save_path='D:/My_code/jupyter notebook/Data_Pictures/'
# 读入图像
image_origin=cv2.imread(image_path)

接着利用opencv库中的Canny算法实现边缘提取,Canny算法的参数列表如下

1
cv2.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]])

其参数意义分别为

  • image:输入图像,需要是单通道的灰度图像。
  • threshold1:边缘检测的低阈值。
  • threshold2:边缘检测的高阈值。
  • edges:输出的边缘图像。
  • apertureSize:Sobel算子的大小,一般默认为3。
  • L2gradient:指定计算梯度大小的方法,如果为True,则使用L2范数,否则使用L1范数。

在调用Canny算法之前需要对原始图像做一些预处理(转换为灰度图、高斯模糊)并计算Canny检测中的双门限的上下限阈值(当然也可以直接设置)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#该函数的输入是一张彩色图像,输出是一张二值化边缘图像
def canny_edge_detection(image_origin, save_path, sigma=0.33):
# 将图像转换为灰度
image_gray = cv2.cvtColor(image_origin, cv2.COLOR_BGR2GRAY)
# 应用高斯模糊以减少噪声
image_blurred = cv2.GaussianBlur(image_gray, (3, 3), 0)
# 计算单通道像素强度的中值
v = np.median(image_blurred)
# 计算Canny边缘检测的下限和上限阈值
thr_lower = int(max(0, (1.0 - sigma) * v))
thr_upper = int(min(255, (1.0 + sigma) * v))
# Canny边缘检测
edges = cv2.Canny(image_blurred, thr_lower, thr_upper)
#保存边缘图像
cv2.imwrite(save_path+"canny_coin.jpg", edges)
# 输出edges
return edges

上述canny_edge_detection函数实现了对输入图像的边缘检测,输入为图像image_origin、保存路径save_path和高斯方差sigma,输出为二值化边缘图像edges(作为Hough检测函数的输入),edges图像的保存路径为savepath;

2.2 Hough圆检测

当拥有了edges也就是Canny算法检测出边缘之后,就可以直接调用opencv库中的cv2.HoughCircles算法实现圆的检测,HoughCircles算法的参数列表如下

1
circles = cv2.HoughCircles(image, method, dp, minDist, circles=None, param1=None, param2=None, minRadius=None, maxRadius=None)

其参数意义分别为

  • image:8位,单通道图像,用于检测圆形目标。一般来说,可以使用cv2.imread()函数加载图像。
  • method:定义霍夫变换的检测方法,常用的有两种方法:
    • cv2.HOUGH_GRADIENT:使用基于图像梯度的方法进行霍夫变换,一般使用这种方法。
    • cv2.HOUGH_STANDARD:标准霍夫变换。
  • dp:累加器图像的分辨率与输入图像分辨率的比例。在OpenCV中,dp的值必须是大于1的整数。
  • minDist:检测到的圆心之间的最小距离。如果设置为0,则OpenCV函数将返回大量的圆心。如果设置为过大的值,则可能会遗漏一些圆心。
  • circles:用于存储检测到的圆形的输出向量。每个向量包含三个浮点数:x坐标,y坐标和半径。
  • param1:用于确定Canny边缘检测阈值的参数。这个参数对于不同的图像可能有不同的值。一般情况下,param1的值是高斯滤波后的灰度图像的阈值上限。如果不确定该参数的值,可以试着将其设置为500或更大的值,然后逐渐减小,直到满足要求。
  • param2:用于确定霍夫变换累加器阈值的参数。累加器中的值大于此阈值时才认为是检测到的圆形。这个值越小,则检测到的圆形越多。反之,这个值越大,则检测到的圆形越少。
  • minRadius:要检测的最小圆半径。
  • maxRadius:要检测的最大圆半径。

检测出圆之后为了能够直观上观察到检测的边缘和原图的关系,因此最后输出圆的相关信息,并在原始图像上绘制检测结果,实现代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 该函数的输入是使用Canny算法提取得到的边缘、最小半径和最大半径,在找到圆后,将其绘制在原始图像上并返回包含检测圆的图像
def detect_circles(edges, save_path, min_radius=10, max_radius=100):
# 直接调用Hough圆检测算法
circles = cv2.HoughCircles(edges, cv2.HOUGH_GRADIENT, dp=1, minDist=50,
param1=100, param2=30, minRadius=min_radius, maxRadius=max_radius)

# 在原始图像上绘制检测结果
if circles is not None:
circles = np.round(circles[0, :]).astype("int")
for (x, y, r) in circles:
print("圆心坐标:({}, {}),半径:{}".format(x, y, r))
cv2.circle(image_origin, (x, y), r, (0, 255, 0), 2)
# 保存图像
cv2.imwrite(save_path+"hough_coin.jpg", image_origin)

return image_origin

上述detect_circles函数实现了在基于Canny算法提取出图像边缘的前提下对图像进行圆检测,输入为edges边缘图、输出图像保存路径save_path和默认最小检测半径、默认最大检测半径,输出为检测出的圆心坐标和圆半径,并返回包含检测圆的图像;

3.实验结果

3.1 图像检测

原始图像如下(这里为了检测算法的有效性一开始并没有给出很多硬币,之后会检测多硬币的情况)

使用Canny算法提取得到的边缘图edges如下

对边缘图使用Hough圆检测算法得到检测圆

可以看到,在只有一个硬币的简单情况下算法成功的检测并提取出了图像中的圆,同时Hough算法给出了上述圆的圆心坐标和半径如下

3.2 对比试验

在最开始设计算法的时候,并没有考虑对图像进行高斯平滑,而是将其转换为灰度图后直接利用Canny算法提取边缘,同时使用的双门限的上下限都是直接指定的,这就导致提取得到的边缘图有好有坏,下面给出不同上下限的Canny边缘提取得到的edges边缘图;

这是原始代码提取得到的边缘图,也就是先使用高斯平滑再通过单通道像素强度中值计算得到边缘检测的下限和上限阈值作为Canny算法的输入,最终输出的edges边缘图

一般来说在调用Canny算法时,都会先进行高斯平滑处理,然后再进行边缘检测,以避免出现噪声干扰或边缘不连续等问题,下面这幅图是未使用高斯平滑得到的边缘图

但是在上面这幅图中明显观察到不使用高斯平滑的效果更好,主要原因一方面是图像并不存在什么噪声,因此使用高斯模糊反而破坏了图像中的某些细节问题;另一方面待检测的边较细,高斯模糊处理后反而是一种负优化,导致边缘检测的效果变差;

接着我们分别指定不同的门限,分别是[50,200],[100,200],[150,200],[200,250]

可以看到当下限指定较小的时候无法有效的过滤掉无关的边缘,当上限指定较大的时候会将完整的边缘信息破坏,因此需要一个合适的方法对计算双门限的上下限;

由之前的介绍已经知道下限阈值用于定义图像可能的边缘,上限阈值用于定义真实的边缘,计算下限阈值的时候通常使用Otsu算法或经验式,代码实现中我们使用的是经验式的方式

1
2
v = np.median(img)
low_threshold = int(max(0, (1.0 - sigma) * v))

其中img是输入的图像,v是图像的中值,sigma是一个参数用于控制下限阈值的大小,公式中的(1.0 - sigma) * v表示将中值v乘以一个小于1的系数(1.0 - sigma),从而得到一个较小的阈值,将该阈值与0进行比较取较大值,确保下限阈值不小于0,最后使用int对下限阈值取整得到最终下限阈值;

参考上述下限阈值的计算方式,可以给出上限阈值的计算方式

1
upper_threshold = int(min(255, (1.0 + sigma) * v))

然而使用这种方式计算得到的上限阈值通常会偏大,这可能导致更多的噪声点被误认为是边缘点,因此如果需要减少噪声干扰,一般使用的上限阈值计算方式都是根据下限阈值进行倍数缩放

1
high_threshold = int(min(255, low_threshold * ratio))

其中ratio是倍数因子,通常取值为3~4之间,但上限阈值的最大值只能是255;

下图是ratio取值为3的结果

可以看出效果并不是特别好,这也表明理论和实践是存在一定差异的,需要根据具体的图像和需求来进行决定选择使用哪种计算方式;

3.3 多个硬币

下面使用编写好的代码对含有多个硬币的图像进行检测,原始图像如下

使用Canny边缘提取算法过后得到的边缘图如下

最后调用Hough圆检测得到如下结果和输出

结果很让人意外,理论上只有四个圆,但是Hough检测出了七个圆,仔细观察不难发现它是将Canny提取的边缘图中的第一个硬币中的曲线误投票为圆(尽管以人类的角度很难认为是圆),那么是否存在优化方法呢?

Hough函数的参数中有两个非常重要的参数:

  • param1:用于确定Canny边缘检测阈值的参数。这个参数对于不同的图像可能有不同的值。一般情况下,param1的值是高斯滤波后的灰度图像的阈值上限。如果不确定该参数的值,可以试着将其设置为500或更大的值,然后逐渐减小,直到满足要求。
  • param2:用于确定霍夫变换累加器阈值的参数。累加器中的值大于此阈值时才认为是检测到的圆形。这个值越小,则检测到的圆形越多。反之,这个值越大,则检测到的圆形越少。

不妨调整参数param2的大小,使得刚好能够检测出四个圆(从60依次减小观察结果)

param2=60param2=45param2=35

当param2取值为35的时候能够刚好检测出四个硬币同时给出其圆心坐标和半径

这四个圆的结果与之前的结果基本相同(略有差异可能是因为硬币本身摆放的角度对检测有一定干扰),验证了Hough圆检测算法的有效性;

4.实验总结

本次实验借助Opencv库中的函数实现了Canny算法和Hough算法进行边缘提取和圆检测的任务,同时给出了检测圆的圆心坐标和圆半径。

Canny算法主要包含了高斯滤波、梯度计算、非最大化抑制和双门限处理,本次实验借助Opencv库中的cv2.Canny()函数来实现Canny算法,并根据实际需求调整阈值参数以获得最佳的边缘检测效果。

Hough算法的主要思想是将图像中的形状转化为参数空间中的曲线,在参数空间中检测出形状的参数,本次实验借助Opencv库中的cv2.HoughCircles()函数实现圆检测,同时调整了相应的参数以获得最佳检测效果。

通过本次实验,将Canny算法和Hough算法结合可以实现图像的多个圆形边缘检测,同时不同的变量计算方式、不同的参数值也会影响最终的检测结果,这启发我们应当根据具体任务进行具体的分析和多次调整使得算法取得最佳检测效果。


初级项目_边缘检测
https://gintoki-jpg.github.io/2023/03/28/项目_边缘检测/
作者
杨再俨
发布于
2023年3月28日
许可协议