在之前的教程中,我们谈到了轮廓的一些基本特征,包括有图像的矩、轮廓区域的面积、轮廓的周长、轮廓的外接图形等等。今天我们仍然讨论几种轮廓的特征,它们将很具有实战意义,我们将从综合方面讲述。
轮廓近似
通常在某些特定场合,我们并不需要太过精确的轮廓信息,而只需要大概的信息,这个时候我们就需要对轮廓进行近似处理,实际上也称之为多边形拟合。
我们接下来将会做一个综合性实验,从而完善对轮廓近似的实战学习。先来看相关的函数原型:
cv2.approxPolyDP(curve, epsilon, closed)
curve
需要进行近似的轮廓
epsilon
指定近似精度的参数ε,这是原始曲线与其近似值之间的最大距离。参数越小,两直线越接近
closed
若为true,曲线第一个点与最后一个点连接形成闭合曲线,若为false,曲线不闭合。
我们先来看一幅图像:
这个实际上是一个综合性的实战项目——OCR文档识别,但是在这里我们只做一些前期的操作,我们现在的目标就是将这幅图像进行校正,这可以通过OpenCV的****变换进行完成,但是在前面我们讲述过,OpenCV的****变化需要四个坐标点,所以我们需要将文档的四个顶点的坐标提取出来,那么现在,我们有了明确的目标,就是将图像的四个顶点找出来。
我们先来看代码:importcv2
importnumpyasnp
defresize(image,width=None,height=None,inter=cv2.INTER_AREA):
dim=None
(h,w)=image.shape[:2]
ifwidthisNoneandheightisNone:
returnimage
ifwidthisNone:
r=height/float(h)
dim=(int(w*r),height)
else:
r=width/float(w)
dim=(width,int(h*r))
resized=cv2.resize(image,dim,interpolation=inter)
returnresized
defshow(img):
cv2.imshow("res",img)
cv2.waitKey(0)
cv2.destroyAllWindows()
img=cv2.imread("page.jpg")
ratio=img.shape[0]/500.0
orig=img.copy()
img=resize(img,height=500)
show(img)
gray=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
show(gray)
gray=cv2.GaussianBlur(gray,(5,5),0)
show(gray)
edged=cv2.Canny(gray,45,230)
show(edged)
contours=cv2.findContours(edged,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[0]
print(len(contours[0]))
cv2.drawContours(img,contours,-1,(0,0,255),2)
show(img)
我们会进行一步步的分析,首先我们需要对图像进行尺寸统一,在这里我们自定义函数加上OpenCV的函数进行的尺寸自适应的统一,这些我们在前面都已经讲过,现在都综合起来。
这是重新定义尺寸之后的图像,在这里定义为高度为500,宽度等比例缩小:
接下来是对图像进行灰度化,方便后期的处理:
高斯模糊,消除噪声的影响,方便后面的边缘检测:
使用Canny边缘检测算法对高斯模糊后的图像进行边缘检测:
于此同时,我们检测最外层的轮廓,然后将轮廓绘制出来:
打印出来轮廓的所有的坐标点:
可以看到,坐标点总共有185个,这样根本没有办法进行****变换,****变换只允许四个坐标点,这个时候我们本次教程介绍的轮廓近似的作用就已经显现出来了,我们来看代码:importcv2
importnumpyasnp
defresize(image,width=None,height=None,inter=cv2.INTER_AREA):
dim=None
(h,w)=image.shape[:2]
ifwidthisNoneandheightisNone:
returnimage
ifwidthisNone:
r=height/float(h)
dim=(int(w*r),height)
else:
r=width/float(w)
dim=(width,int(h*r))
resized=cv2.resize(image,dim,interpolation=inter)
returnresized
defshow(img):
cv2.imshow("res",img)
cv2.waitKey(0)
cv2.destroyAllWindows()
img=cv2.imread("page.jpg")
ratio=img.shape[0]/500.0
orig=img.copy()
img=resize(img,height=500)
gray=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
gray=cv2.GaussianBlur(gray,(5,5),0)
edged=cv2.Canny(gray,45,230)
contours=cv2.findContours(edged,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[0]
print(len(contours[0]))
epsilon=0.1*cv2.arcLength(contours[0],True)
approx=cv2.approxPolyDP(contours[0],epsilon,True)
cv2.drawContours(img,[approx],-1,(0,0,255),2)
print(len(approx))
show(img)
我们可以看到,在进行了轮廓近似之后,坐标点由185变为了四个,这极大的方便了之后的****变换:
通过这个实战,我们既熟练了之前所讲述的内容,同时也体会到了新内容的实用性,现在我们来看看轮廓的其他特征。
凸包检测
事实上,OpenCV轮廓的凸包检测也是十分实用的,凸包(Convex Hull)是一个计算几何(图形学)中的概念,它的严格的数学定义为:在一个向量空间V中,对于给定集合X,所有包含X的凸集的交集S被称为X的凸包。
凸包检测原理:
思路:Graham扫描的思想和Jarris步进法类似,也是先找到凸包上的一个点,然后从那个点开始按逆时针方向逐个找凸包上的点。
操作步骤大概可分为两步
第一步:排序
第二步:扫描
步骤:
1.把所有点放在二维自然直角坐标系中(注意与图片像素坐标系区分),则纵坐标最小的点一定是凸包上的点,如图中的P0。
2.那么以P0作为坐标参考点,如上图所示:
计算各个点相对于P0的幅角α,按从小到大的顺序对各个点排序。当α相同时,距离P0比较近的排在前面。例如上图得到的结果为P1,P2,P3,P4,P5,P6,P7,P8。我们由定义可以知道,结果中第一个点P1和最后一个点P8一定是凸包上的点。
3.以上,我们已经知道了凸包上的第一个点P0和第二个点P1,我们把它们放在栈里面,并令P2为当前点.
4.根据栈顶的前两个点作出有向直线,记PX-1->PX,看当前点是在有向直线L的右边还是左边。如果在直线的右边就执行步骤5;如果在直线上,或者在直线的左边就执行步骤6。
5.栈顶的那个元素不是凸包上的点,把栈顶元素出栈,当前点不变,执行步骤4。
6.当前点是凸包上的点,把它压入栈,执行步骤7。
7.检查当前的点是不是步骤3那个结果的最后一个元素:当前点是最后一个元素的话就结束,如果不是的话就将下一个点作为新的当前点,返回步骤4。
静态求解过程如下:
实战
在图像处理过程中,我们常常需要寻找图像中包围某个物体的凸包。凸包跟多边形逼近很像,只不过它是包围物体最外层的一个凸集,这个凸集是所有能包围这个物体的凸集的交集。如下图所示:
在上图中,绿色线条所包围的凸集即为白色图形的凸包。
在OpenCV中,通过函数convexHulll能很容易的得到一系列点的凸包,比如由点组成的轮廓,通过convexHull函数,我们就能得到轮廓的凸包。寻找图像的凸包,能够让我们做一些有意思的事情,比如手势识别等。
函数原型:
hull = cv.convexHull( points[, hull[, clockwise[, returnPoints]]] )
第一个参数:输入的点集,可以是矩阵。
第二个参数:可以为vector,此时返回的是每个凸包点在原轮廓点容器中的索引,也可以为vector,此时存放的是凸包点的位置,即点(x,y)坐标。
第三个参数:bool变量,表示输出的凸包是顺时针方向还是逆时针方向,true是顺时针方向,false为逆时针方向,默认值是true,即顺时针方向输出凸包。
第四个参数:bool型变量returnPoints,表示第二个参数的输出类型,默认为true,即返回凸包的各个点,设置为false时,表示返回凸包各个点的索引。当第二个参数类型为std::vector,则该标志位被忽略,就是以第二个参数为准,即为vector,此时返回的是每个凸包点在原轮廓点容器中的索引,为vector时,此时存放的是凸包点的位置,即点(x,y)坐标。
下面将会通过两个简单例子来展示如何用OpenCV来寻找图像的凸包。
首先,我们用以下的Python代码来自己绘制一张简单的多边形的图片(tubao.png),代码如下:importcv2
importnumpyasnp
#新建512*512的空白图片
img=np.zeros((512,512,3),np.uint8)
#平面点集
pts=np.array([[200,250],[250,300],[300,270],[270,200],[120,240]],np.int32)
pts=pts.reshape((-1,1,2))
#绘制填充的多边形
cv2.fillPoly(img,[pts],(255,255,255))
#保存图片
cv2.imshow("res",img)
cv2.imwrite('tubao.png',img)
cv2.waitKey(0)
接着我们需要寻找这个多边形的凸包,利用OpenCV的convexHull函数,然后再将这个凸包绘制出来,得到直观的展示结果。处理的Python代码如下:importcv2
#读取图片并转至灰度模式
img=cv2.imread("tubao.png",1)
gray=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
#二值化
ret,thresh=cv2.threshold(gray,127,255,cv2.THRESH_BINARY)
#图片轮廓
contours,hierarchy=cv2.findContours(thresh,2,1)
cnt=contours[0]
#寻找凸包并绘制凸包(轮廓)
hull=cv2.convexHull(cnt)
print(hull)
length=len(hull)
foriinrange(len(hull)):
cv2.line(img,tuple(hull[i][0]),tuple(hull[(i+1)%length][0]),(0,255,0),2)
#显示图片
cv2.imshow('line',img)
cv2.waitKey()
这是凸包所在的轮廓的点集集合,有了它,我们就能绘制出凸包的轮廓了,如下:
接下来,我们将介绍一张稍微难一点的图片——手势图片(finger.jpg),如下所示:
我们将会来寻找这个手势的凸包。基本的处理思路还是和之前的一致,只是要在二值化以及凸包点集集合的大小上做一些处理,取二值化的阈值为235,凸包点集中的点个数大于5,完整的Python代码如下:importcv2
#读取图片并转至灰度模式
imagepath='F://finger.jpg'
img=cv2.imread(imagepath,1)
gray=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
#二值化,取阈值为235
ret,thresh=cv2.threshold(gray,235,255,cv2.THRESH_BINARY)
#寻找图像中的轮廓
image,contours,hierarchy=cv2.findContours(thresh,2,1)
#寻找物体的凸包并绘制凸包的轮廓
forcntincontours:
hull=cv2.convexHull(cnt)
length=len(hull)
#如果凸包点集中的点个数大于5
iflength>5:
#绘制图像凸包的轮廓
foriinrange(length):
cv2.line(img,tuple(hull[i][0]),tuple(hull[(i+1)%length][0]),(0,0,255),2)
cv2.imshow('finger',img)
cv2.waitKey()
可以发现,一共检测到2个凸包,一个是整个手势外围的凸包,正好包围整个手,另一个是两个手指形成的内部的图形,类似于O的凸包,这符合我们的预期。
至此,OpenCV轮廓的特征就全部介绍完毕,它们的用途极为广泛,其重要性不言而喻。
如果觉得《python二值化特征_OpenCV-Python系列之轮廓特征高阶》对你有帮助,请点赞、收藏,并留下你的观点哦!