在本期博客中,我们将探讨如下问题:
Let'sbegin!!
我们来看一下维基百科对于ImageGradient(图像梯度)的解释。
"Animagegradientisadirectionalchangeintheintensityorcolorinanimage.Thegradientoftheimageisoneofthefundamentalbuildingblocksinimageprocessing.Forexample,theCannyedgedetectorusesimagegradientforedgedetection.Ingraphicssoftwarefordigitalimageediting,thetermgradientorcolorgradientisalsousedforagradualblendofcolorwhichcanbeconsideredasanevengradationfromlowtohighvalues,asusedfromwhitetoblackintheimagestotheright.Mathematically,thegradientofatwo-variablefunction(heretheimageintensityfunction)ateachimagepointisa2Dvectorwiththecomponentsgivenbythederivativesinthehorizontalandverticaldirections.Ateachimagepoint,thegradientvectorpointsinthedirectionoflargestpossibleintensityincrease,andthelengthofthegradientvectorcorrespondstotherateofchangeinthatdirection."
"图像梯度是指图像强度或颜色的方向变化。图像梯度是图像处理的基础框架之一。例如,Canny边缘检测器使用图像梯度进行边缘检测。在用于数字图像编辑的图形软件中,梯度或颜色梯度一词也用于表示颜色的逐渐混合,可以认为是从低到高值的均匀渐变,如下图中从白色到黑色所使用的渐变。数学上,一个双变量函数(这里是图像强度函数)在每个图像点处的梯度是一个二维向量,其分量由水平和垂直方向上的导数给出。在每个图像点上,梯度向量指向可能最大强度增加的方向,梯度向量的长度对应于该方向的变化率。"
上述内容非常严谨地定义了图像和视觉中的图像梯度,但是仍然需要指出,蓝字部分的内容存在一些难以理解的部分,如颜色的逐渐混合,其实这里指的是Photoshop(或其他的图形编辑软件)中渐变的效果,当然,这些渐变也属于梯度。
维基百科用了较通俗的语言去描述图像梯度,相比之下,百度百科对图像梯度的解释就显得更专业,更容易理解,如下:"图像梯度可以把图像看作二维离散函数,图像梯度其实就是这个二维离散函数的求导:图像梯度:
我更喜欢百度百科更接近本质的解释,简单来说,图像梯度就是图像在不同方向上的变化趋势,可以将图像视为只存在水平和垂直两方向存在梯度变化,进行临近像素的梯度提取。当然,这么做会存在一定的问题,那就是这样求解得到的梯度信息对噪声敏感,即对噪声鲁棒性差。
接下来,将以实际的图像为例,更进一步的解释1.1图像梯度、图像边缘及梯度提取算子,并介绍目前最广泛应用的1.2canny边缘检测算法原理及实现。
说起图像梯度,其实我们得好好谈一谈图像边缘,从许多曾经的视觉研究实验中,我们可以发现,边缘是很特殊的一种信息,因为人眼识别物体,靠的正是边缘,而非颜色、纹理。在绘画中我们可以通过线条去绘制任何一样物体,但若该手绘物体缺失了部分边缘,我们就难以识别。但相反的,我们对缺失了纹理、色彩的物体,却依然具备强大的辨别能力。
Walther等人在2011年进行了这样一个视觉研究,让视觉受试者观看海滩、城市街道、森林、高速公路、山脉和办公室的照片和线条图时收集功能磁共振成像数据。尽管在场景统计数据上有明显的差异,但研究者们还是能够通过PPA(theparahippocampalplacearea,海马旁区和RSC(theret-rosplenialcortex,脾叶皮层)解码初级视觉皮层中线条图的fMRI(Functionalmagneticresonanceimaging,功能磁共振成像)数据和彩色照片的活动数据的场景类别。更值得注意的是,在PPA和RSC中,从线条图识别的错误模式与从彩色照片识别的错误模式非常相似。这些数据表明,在这些区域,用于区分场景类别的信息对于线条图和照片来说是相似的。
边缘的由来主要源于:
而在实际编程运算时,我们通常采用算子通过卷积获得一幅图像的梯度信息,如果不明白何为图像中的卷积运算,可以看看下图。
以最简单的Prewitt算子为例,利用算子卷积的图像梯度结果如下:
不难发现,在梯度图像中,值越高的部分,越接近人眼视觉所观察到的边缘。因此,我们可以用梯度更直接的解释边缘:边缘是图像能量急剧变化的地方,是图像梯度高的地方。
但这也存在一个问题,图像的梯度是对邻域像素值变化敏感的,而图像内的噪声则会导致部分非边缘区域同样具有较高的梯度,这会导致边缘检测的噪声鲁棒性差。
为了提高噪声鲁棒性,Canny在1986年提出了一种边缘检测算法,该算法是边缘检测中最经典,最具影响力的方法,至今还有非常广泛的应用,是计算机视觉领域的基础框架组成之一,论文被引超20000余次。
Canny边缘检测算法可以分为四步:
对于噪声抑制和梯度及梯度方向的计算,Canny边缘检测算法并没有进行创新,只是做了单纯的高斯核卷积平滑,以此进行噪声抑制,并通过差分运算得到梯度及梯度方向。Canny边缘检测突破性的创新是集中在后三步上的。
非极大值抑制的思想在于:当图像的梯度到达局部(特定大小的邻域)最大值的时候,我们认为它是边缘;对于非极大值的点,即使它超过了我们所定义的“边缘梯度阈值”,仍不认为它是边缘。并且,预定义了八个可能的方向(左上、上、右上、右、右下、下、左下、左),对于该方向上的梯度值,将非极大值归零。
具体操作:1.筛选幅值大于阈值的像素点,设为待验证点;2.遍历待验证点,若为局部最大值,保留,反之去归零;3.保留下来的点就是非极大值抑制后的边缘点。
说到双阈值检测,我们得先说采用单一阈值的缺陷,如下图所示。
显而易见,采用单一阈值会有两种问题随之产生:1.阈值过高,会导致部分梯度较低的边缘被当成噪声剔除,导致边缘不连续;2.阈值过低,会有过多不应该作为边缘的梯度信息被误判为边缘而包含进边缘图。
而双阈值解决了单阈值的上述问题。双阈值定义了两种阈值,低阈值与高阈值:若梯度小于低阈值,则不是边缘;若梯度大于高阈值,则为强边缘;若梯度在高低阈值间,则为弱边缘。
至此,就是双阈值检测的部分,乍一看仿佛与单一阈值没有区别,只要高于低阈值就能作为弱边缘,那这么一来好像跟单阈值就没什么区别了。诚然,如果缺少了“边缘连接”这下一步骤,双阈值和单阈值的本质确实是一样的。
在开始解释“边缘连接”这一步骤前,我们不妨来仔细观察一下“单阈值”与“双阈值”的图像,这对于“边缘连接”原理的理解非常有帮助。
在单阈值图像中,存在部分“离群”的线条,其中有一部分其实并非真实的边缘,而是被误判的高梯度位置;其中也有一部分不连通的真实边缘,这又导致了边缘的不连续。
而在双阈值图像中,较亮的线条为强边缘,稍暗一些的线条为弱边缘。不难发现,一条不与强边缘相连的弱边缘,基本都是被误判为边缘的区域。相反,与强边缘相连的弱边缘,则囊括了绝大多数单阈值所没有判定包含的“真实边缘”。
那么现在,我们可以明白边缘连接的原理了:利用高阈值来确定主要边缘轮廓(强边缘),并利用与强边缘连接的低阈值所得弱边缘来填补缺失边缘,并防止引入噪声(不与强边缘相连的弱边缘就归零)。
由此,完整地解释了Canny边缘检测算法的原理,我们进入代码部分。
fromedgeimportconv,gaussian_kernel#定义一个3x3的高斯kernel,并将其sigma值设为1kernel=gaussian_kernel(3,1)kernel_test=np.array([[0.05854983,0.09653235,0.05854983],[0.09653235,0.15915494,0.09653235],[0.05854983,0.09653235,0.05854983]])print(kernel)#检测生成的高斯kernel是否正确ifnotnp.allclose(kernel,kernel_test):print('Incorrectvalues!Pleasecheckyourimplementation.')输出结果:
[[0.058549830.096532350.05854983][0.096532350.159154940.09653235][0.058549830.096532350.05854983]]找一幅图像进行测试。
#用不同的尺寸以及sigma值来进行测试kernel_size=5sigma=1.4#载入图片img=io.imread('iguana.png',as_grey=True)#生成高斯kernelkernel=gaussian_kernel(kernel_size,sigma)#利用kernel来对图片进行平滑smoothed=conv(img,kernel)plt.subplot(1,2,1)plt.imshow(img)plt.title('Originalimage')plt.axis('off')plt.subplot(1,2,2)plt.imshow(smoothed)plt.title('Smoothedimage')plt.axis('off')plt.show()
会导致中心像素点受临域像素值的影响更大,从到导致高斯滤波的模糊效果更佳明显。
若减小,会使高斯滤波核中心权重占比变大,中心位置临域的权重占比都减小,若sigma趋于0,则高斯滤波基本没有效果;若增大,会使高斯滤波核的中心位置权重占比减小,中心位置领域的权重占比增大,若sigma趋于无穷,则高斯滤波等于均值滤波。
fromedgeimportpartial_x,partial_y#测试案例I=np.array([[0,0,0],[0,1,0],[0,0,0]])#希望的输出结果I_x_test=np.array([[0,0,0],[0.5,0,-0.5],[0,0,0]])I_y_test=np.array([[0,0.5,0],[0,0,0],[0,-0.5,0]])#计算梯度I_x=partial_x(I)I_y=partial_y(I)print(I_x)print(I_y)#确定partial_xandpartial_y是否编写正确ifnotnp.all(I_x==I_x_test):print('partial_xincorrect')ifnotnp.all(I_y==I_y_test):print('partial_yincorrect')输出结果:
[[0.0.0.][0.50.-0.5][0.0.0.]][[0.0.50.][0.0.0.][0.-0.50.]]计算x、y方向上的梯度
#计算平滑后的图像的差分Gx=partial_x(smoothed)Gy=partial_y(smoothed)plt.subplot(1,2,1)plt.imshow(Gx)plt.title('Derivativeinxdirection')plt.axis('off')plt.subplot(1,2,2)plt.imshow(Gy)plt.title('Derivativeinydirection')plt.axis('off')plt.show()结果如下
现在,让我们用两个方向的差分来计算图片的梯度大小以及方向,完成edge.py所定义的梯度和梯度方向的求解。
fromedgeimportnon_maximum_suppression#测试例g=np.array([[0.4,0.5,0.6],[0.3,0.5,0.7],[0.4,0.5,0.6]])#输出非极大值抑制的结果#改变梯度方向:通过四个方向来测试,即0,45,90,135。你可以将输出结果与自己笔算的结果做对比。forangleinrange(0,180,45):print('Thetas:',angle)t=np.ones((3,3))*angle#Initializetheta#print(t)print(non_maximum_suppression(g,t))输出结果:
Thetas:0[[0.0.0.][0.0.0.][0.0.0.]]Thetas:45[[0.0.0.][0.0.0.7][0.0.0.]]Thetas:90[[0.0.0.][0.0.0.7][0.0.0.]]Thetas:135[[0.0.0.][0.0.0.7][0.0.0.]]nms=non_maximum_suppression(G,theta)plt.imshow(nms)plt.title('Non-maximumsuppressed')plt.axis('off')plt.show()
在经过非极大值抑制后,仍然存在许多像素点。这些像素点有些是边缘,但有些是由噪声或者颜色变化(例如粗糙的表面)所导致的。要消除这部分影响,最简单的方式是增加一个阈值判定,只允许相应强度大于阈值的点被标记成边缘。Canny边缘检测算法采用了双阈值算法。大于高阈值的像素点被标记为强边缘,小于低阈值的像素点可以认为是非边缘并且被移除,而在高低阈值之间的点被标记为弱边缘。
通常而言,强边缘被认为是“肯定的边缘”,因此可以直接认为该像素点就是边缘.而弱边缘则需要进一步判断。如果弱边缘点与边缘相连接,则认为该像素点是边缘。这背后的逻辑是这样的:噪声或者颜色变化点不太可能产生强边缘(如果设定好恰当对的阈值范围),因此强边缘只能由源图像中的边缘产生。而弱边缘可由边缘产生,也可以由噪声和颜色变化产生。而由噪声和颜色变化产生的弱边缘点,一般来说都均有分布在整幅图片上,只有一小部分是与强边缘相连接的,大部分与强边缘相连接的点都是真实的边缘像素点。
先用一个简单测试例来展示强弱边缘连接及判别的过程。
fromedgeimportget_neighbors,link_edgestest_strong=np.array([[1,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,1]])test_weak=np.array([[0,0,0,1],[0,1,0,0],[1,0,0,0],[0,0,1,0]])test_linked=link_edges(test_strong,test_weak)plt.subplot(1,3,1)plt.imshow(test_strong)plt.title('Strongedges')plt.subplot(1,3,2)plt.imshow(test_weak)plt.title('Weakedges')plt.subplot(1,3,3)plt.imshow(test_linked)plt.title('Linkededges')plt.show()
接下来,我们在图像中进行边缘连接
edges=link_edges(strong_edges,weak_edges)plt.imshow(edges)plt.axis('off')plt.show()
测试一组不同超参数选取下的Canny边缘检测结果
fromedgeimportcanny#载入图像img=io.imread('iguana.png',as_grey=True)#运行canny边缘检测器edges=canny(img,kernel_size=5,sigma=1.4,high=0.03,low=0.02)print(edges.shape)plt.subplot(1,3,1)plt.imshow(edges)plt.axis('off')plt.title('Yourresult')
图像的特征主要有图像的颜色特征、纹理特征、几何特征和空间关系特征。其中,几何特征也称形状特征。在物体识别、检测领域有非常重要的作用。Hough变换是其中非常经典的方法。
Hough变换是在1962年由Hough所发明,随后在1972年Duda第一次用它检测图像中的直线。该变换的目的是为了寻找图像中的直线结构,但需要注意的是,Hough变换同样可以检测圆等其他几何结构,只要其参数方程是已知的。
假设我们现在已经通过边缘检测算法得到了图像完整的边缘结构,可以发现,某些像素点构成了图像的几何结构。
接下来,我们将以直线检测为例,说明Hough变换的原理。
基于上述特性,我们可以实现一种基于Hough变换的直线检测,步骤如下:
下图是一个Hough变换的结果例,右图显示了计数值前20的直线
由此,完整地解释了Hough变换在直线检测中的原理,在其中若对参数方程进行修改,就可以实现对其他几何形状的检测。
fromedgeimportcanny#载入图像img=io.imread('road.jpg',as_grey=True)#利用Canny检测器进行边缘检测edges=canny(img,kernel_size=5,sigma=1.4,high=0.03,low=0.02)plt.subplot(211)plt.imshow(img)plt.axis('off')plt.title('InputImage')plt.subplot(212)plt.imshow(edges)plt.axis('off')plt.title('Edges')plt.show()
可以发现,Canny算法能够找出图片中的道路。但是,我们也可以看到这里面出现了很多我们不需要的物体的边缘。考虑到所抓取的图片的空间位置,我们清楚道路总是在图片的下半部分,因此可以利用这一点消除一部分不希望得到的边缘。下面的代码定义了一个二值化的模板(mask),用来抓取ROI(Regionofinterest,感兴趣区域)的边缘。
H,W=img.shape#GeneratemaskforROI(RegionofInterest)mask=np.zeros((H,W))foriinrange(H):forjinrange(W):ifi>(H/W)*jandi>-(H/W)*j+H:mask[i,j]=1#ExtractedgesinROIroi=edges*maskplt.subplot(1,2,1)plt.imshow(mask)plt.title('Mask')plt.axis('off')plt.subplot(1,2,2)plt.imshow(roi)plt.title('EdgesinROI')plt.axis('off')plt.show()
由此,我们完成了本期对于图像梯度、图像边缘、几何特征、检测与提取的原理与实现。