1. 寻找和绘制轮廓

1.1 寻找轮廓

轮廓可以简单地解释为连接所有连续点(沿边界)的曲线,具有相同的颜色或强度,是边缘的一部分。轮廓是形状分析和目标检测与识别的有效工具。

为了获得更高的精度,一般会使用二值化的图像来进行处理,在寻找轮廓之前,首先需要检测一下边缘,如前面文档中提到的canny边缘检测算法。

opencv通过findContos函数来寻找物体轮廓,从OpenCV 3.2开始,此函数不再修改源图像,而是将修改后的图像作为三个返回参数中的第一个参数返回。

在OpenCV中,寻找轮廓就像从黑色背景中寻找白色物体一样,要找到的物体应该是白色的,背景应该是黑色的。

下面是一个例子:

import numpy as np
import cv2
im = cv2.imread('test.jpg')
imgray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(imgray, 127, 255, 0)
im2, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

输入有三个参数,一个是原图二值化图像,第二个是轮廓检索模式,第三个是轮廓逼近方法。并输出修改后的图像、轮廓和层次结构。输出的轮廓是图片中所有轮廓的一个列表,每个单独的轮廓都是对象边界点(x,y)坐标的Numpy数组。

1.2 绘制轮廓

opencv使用cv2.drawContours函数来绘制轮廓,它也可以用来绘制任何形状,只要你有它的边界点。第一个参数是原图像,第二个参数是轮廓列表,第三个参数是要绘制的轮廓索引(在绘制单个轮廓时有用),若要绘制所有轮廓,则传入-1。剩余的参数是颜色,厚度等。

# 绘制图片中所有轮廓
cv2.drawContours(img, contours, -1, (0,255,0), 3)
# 绘制单个轮廓,比如第4个轮廓
cv2.drawContours(img, contours, 3, (0,255,0), 3)
#但是更加推荐使用下面的方法。这俩看上去是一样的,后面会提到为啥下面这种更好
cnt = contours[4]
cv2.drawContours(img, [cnt], 0, (0,255,0), 3)

1.3 轮廓逼近方法

轮廓逼近方法是cv2.findContos函数中的第三个参数。它有什么作用呢?

上面关于轮廓的定义中提到,轮廓是一个形状的具有相同强度的边界。它存储着形状边界的(x,y)坐标。但是它能存储所有的坐标吗?这就是由轮廓逼近方法指定的。

如果传入cv2.CHAIN_Approx_NONE,则存储所有边界点。但我们真的需要所有的顶点吗?例如,你找到了一条直线的轮廓线。你需要这条线上的所有点来表示这条线吗?不,我们只需要那条线的两个端点,这就是cv2.CHAIN_Approx_Simple所做的事情,它删除所有冗余点并压缩轮廓,从而节省内存。

下面的矩形图像演示了这个配置,在轮廓数组中的所有坐标上画一个圆圈(用蓝色绘制)。第一张图是用cv2.CHAIN_Approx_NONE获得的所有轮廓点(734点),第二张图绘制cv2.CHAIN_Approx_SIMPLE(只有4个点)的点,这种情况下可以节省大量的内存!

image.png

2. 轮廓特征

2.1 图像矩

图像矩可以用来计算出物体的质心、面积等特征,更多的用途可以在其wiki页上看到。

opencv用cv2.moments()函数来计算所有矩值:

import cv2
import numpy as np
img = cv2.imread('star.jpg',0)
ret,thresh = cv2.threshold(img,127,255,0)
im2,contours,hierarchy = cv2.findContours(thresh, 1, 2)
cnt = contours[0]
M = cv2.moments(cnt)
print( M )

通过这些矩值,可以计算很多有用的数据,如面积,质心等。质心公式:
image.png

cx = int(M['m10']/M['m00'])
cy = int(M['m01']/M['m00'])

2.2 轮廓面积

Contour area is given by the function cv2.contourArea() or from moments, M[‘m00’].
轮廓的面积可以用函数cv2.contourArea()函数来计算,或者可以直接读取矩值中的M['m00']

area = cv2.contourArea(cnt)

2.3 轮廓周长

轮廓周长也称为弧长,可以使用cv2.arcLength()函数计算。第二个参数指定轮廓是封闭的形状(True),还是不封闭的曲线。

perimeter = cv2.arcLength(cnt,True)

2.4 轮廓近似

轮廓近似法根据我们设置的精度,将轮廓近似成另一个顶点数较少的其它形状。它是Douglas-Peucker算法(wiki)的实现。

为了理解这一点,假设你试图在图像中找到一个正方形,但是由于图像形状的问题,你没有得到一个完美的正方形,而是一个“糟糕的形状”(如下面的第一张图片所示)。这种情况下就可以使用这个函数来得到一个近似形状。决定近似程度的参数称为epsilon,它是从轮廓到近似轮廓的最大距离。这是一个精确的参数。为了得到正确的输出,需要传入合适的epsilon。

下面的例子中,第一张图是原图,第二张图是将epsilon设置为周长10%的结果,第二张图是设置为周长的1%。

image.png

epsilon = 0.1*cv2.arcLength(cnt,True)
approx = cv2.approxPolyDP(cnt,epsilon,True) #第三个参数为轮廓是否封闭

2.5 凸包

凸包看起来有点像轮廓近似,但事实并非如此(在某些情况下两者可能提供相同的结果)。cv2.conexHull()函数检查曲线的凸性缺陷并加以修正。一般说来,凸曲线是指凸出的,或者至少是平坦的曲线。如果存在向内部凹陷的部分,就叫做凸性缺陷。例如下面的手的图像,红线显示手的凸包,双面箭头表示凸性缺陷,这是凸包和轮廓的局部最大偏差。

image.png

hull = cv2.convexHull(points[, hull[, clockwise[, returnPoints]]
  • points 传入的轮廓
  • hull 输出凸包结果,一般不传,从返回值获取
  • clockwise 是否顺时针
  • returnPoints 默认是True,结果会返回凸包中的顶点列表,若设置为False,则只返回轮廓顶点中,处于凸包中顶点的索引列表

可以通过下面的代码计算得到凸包:

hull = cv2.convexHull(cnt)

但是,如果要查找凸性缺陷,则需要传入returPoint=false。为了理解它,例如上面的矩形图像,首先找到它的轮廓,随后找到它的凸包,它的返回点为True,得到了下面的值:[234 202],[51 202],[51 79],[234 79],这些值是矩形的四个角点,即:[234 202],[51 202],[51 79],[234 79]。现在,如果对returPoint=false执行同样的操作,我将得到以下结果:[129],[67],[0],[142]。这些是轮廓中对应点的索引。例如第一个值:CNT[129]=[234,202],这与第一个结果相同(对于其他结果依此类推)。

2.6 检查凸性

可以用cv2.isConourConvex()函数来检查曲线是否是凸的,它只会返回True或False。

k = cv2.isContourConvex(cnt)

2.7 矩形边框

矩形边框有两种:
水平/垂直矩形边框,它不考虑对象的旋转,因此矩形面积可能不是最小的包裹矩形面积,可以用cv2.boundingRect()函数计算的到。

旋转矩形边框,边框矩形是按最小的面积绘制的,考虑了旋转,可以用cv2.minAreaRect()函数计算得到。它返回一个Box2D结构,包含下面这些属性:中心点(x,y),宽度,高度,旋转角度。绘制这个矩形的时候,需要的是4个角点坐标,可以由函数cv2.boxPoint()计算得到。

rect = cv2.minAreaRect(cnt)
box = cv2.boxPoints(rect)
box = np.int0(box)
cv2.drawContours(img,[box],0,(0,0,255),2)

两种矩形都显示在了下面的图像中,绿色矩形是正常边界矩形,红色矩形是旋转的矩形:
image.png

2.8 最小包围圆

使用函数cv2. minEnclosingCircle()可以找到包围对象的最小的圆。

(x,y),radius = cv2.minEnclosingCircle(cnt)
center = (int(x),int(y))
radius = int(radius)
cv2.circle(img,center,radius,(0,255,0),2)

image.png

2.9 拟合椭圆

cv2.fitEllipse()返回拟合到对象区域的椭圆,函数返回椭圆内接的旋转矩形。

ellipse = cv2.fitEllipse(cnt)
cv2.ellipse(img,ellipse,(0,255,0),2)

image.png

2.10 拟合直线

类似地,可以将一条直线拟合到一组点上,下图包含一组白点,可以近似拟合出一条直线。

ows,cols = img.shape[:2]
[vx,vy,x,y] = cv2.fitLine(cnt, cv2.DIST_L2,0,0.01,0.01)
lefty = int((-x*vy/vx) + y)
righty = int(((cols-x)*vy/vx)+y)
cv2.line(img,(cols-1,righty),(0,lefty),(0,255,0),2)

image.png

3. 轮廓属性

opencv可以提取一些常用的对象属性,如紧密性,等效直径,图像掩码,平均强度等。更多特性可以在Matlab regionprops 文档中找到。

(注:第2节中提到的质心、面积、周长等也属于轮廓属性,这里就不再重复提及了)

3.1 纵横比

轮廓的宽高比值

x,y,w,h = cv2.boundingRect(cnt)
aspect_ratio = float(w)/h

3.2 面积区域

面积区域是轮廓面积与轮廓包围矩形的面积之比

image.png

area = cv2.contourArea(cnt)
x,y,w,h = cv2.boundingRect(cnt)
rect_area = w*h
extent = float(area)/rect_area

3.3 紧密性

紧密性是轮廓面积与其凸包面积之比。

image.png

area = cv2.contourArea(cnt)
hull = cv2.convexHull(cnt)
hull_area = cv2.contourArea(hull)
solidity = float(area)/hull_area

3.4 等效直径

等效直径是面积与轮廓面积相同的圆的直径。

image.png

area = cv2.contourArea(cnt)
equi_diameter = np.sqrt(4*area/np.pi)

3.5 方向

方向是对象定向的角度,虾米东东函数还提供了长轴和次轴的长度。

(x,y),(MA,ma),angle = cv2.fitEllipse(cnt)

3.6 掩码和像素点

在某些情况下,我们可能需要构成该对象的所有点,可以通过下面的操作得到:

mask = np.zeros(imgray.shape,np.uint8)
cv2.drawContours(mask,[cnt],0,255,-1)
pixelpoints = np.transpose(np.nonzero(mask))
#pixelpoints = cv2.findNonZero(mask)

代码给出了两种方法来获取掩码像素点,一种是使用Numpy函数,另一种是使用OpenCV函数(最后一个注释行)。结果是一样的,但略有所不同,Numpy以(行、列)格式提供坐标,而OpenCV以(x,y)格式提供坐标。所以基本上结果会是相反的(行=x和列=y)。

3.7 最大值最小值及其位置

可以通过掩码来获取这些特征

min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(imgray,mask = mask)

3.8 平均颜色或平均强度

可以计算对象的平均颜色,也可以是灰度模式下对象的平均强度,同样也是借助掩码来计算。

mean_val = cv2.mean(im,mask = mask)

3.9 极值点

极值点是指对象的最高点、最底点、最右侧点和最左侧点。

leftmost = tuple(cnt[cnt[:,:,0].argmin()][0])
rightmost = tuple(cnt[cnt[:,:,0].argmax()][0])
topmost = tuple(cnt[cnt[:,:,1].argmin()][0])
bottommost = tuple(cnt[cnt[:,:,1].argmax()][0])

例如,对印度地图求极值点,会得到以下结果:

image.png

4. 其它相关函数

4.1 凸性缺陷检测

在第2节中,描述了什么是凸包。物体与凸包之间的任何偏离均可视为凸性缺陷。

OpenCV提供了cv2.conexityDefects()函数用来检测凸性缺陷:

hull = cv2.convexHull(cnt,returnPoints = False)
defects = cv2.convexityDefects(cnt,hull)

这里要注意,在计算凸包时,必须传参returPoint=False,以检测凸性缺陷。

此函数返回一个数组,其中每行都包含这些值:轮廓上起点、轮廓上终点、轮廓上最远点、到最远点的近似距离。用图像来表示就是,画一条线连接起点和终点,然后在最远的点画一个与连线相切的圆,半径就是到最远点的近似距离。注意返回的前三个值是轮廓cnt的索引,必须从cnt数据中得到这些值。

import cv2
import numpy as np
img = cv2.imread('star.jpg')
img_gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
ret,thresh = cv2.threshold(img_gray, 127, 255,0)
im2,contours,hierarchy = cv2.findContours(thresh,2,1)
cnt = contours[0]
hull = cv2.convexHull(cnt,returnPoints = False)
defects = cv2.convexityDefects(cnt,hull)
for i in range(defects.shape[0]):
    s,e,f,d = defects[i,0]
    start = tuple(cnt[s][0])
    end = tuple(cnt[e][0])
    far = tuple(cnt[f][0])
    cv2.line(img,start,end,[0,255,0],2)
    cv2.circle(img,far,5,[0,0,255],-1)
cv2.imshow('img',img)
cv2.waitKey(0)
cv2.destroyAllWindows()

image.png

4.2 点到多边形的最短距离

opencv提供了cv2.pointPolygonTest函数来计算图像中的点与轮廓之间的最短距离,当点在轮廓内部时返回负数,在外部时返回正数,如果在轮廓上则为0。

例如,计算点(50,50)到轮廓的最短距离:

dist = cv2.pointPolygonTest(cnt,(50,50),True)

在函数中,第三个参数表示是否计算距离,如果为True,则返回带符号的距离,如果为False,则会计算该点是在轮廓内部还是外部还是在轮廓上(分别返回+1、-1、0)。
如果不想计算距离,第三个参数一定要传False,因为距离计算是一个耗时的过程,设置为False将提供大约2-3倍的性能提升。

4.3 形状匹配

OpenCV提供了函数cv2.matShapes()来比较两个形状或两个轮廓,并返回一个表示相似性的值,值越小,表示匹配度越好。相似度是基于hu-moment值来计算的,对计算结果进行分析得到相似度,文档中描述了不同的计算方法。

import cv2
import numpy as np
img1 = cv2.imread('star.jpg',0)
img2 = cv2.imread('star2.jpg',0)
ret, thresh = cv2.threshold(img1, 127, 255,0)
ret, thresh2 = cv2.threshold(img2, 127, 255,0)
im2,contours,hierarchy = cv2.findContours(thresh,2,1)
cnt1 = contours[0]
im2,contours,hierarchy = cv2.findContours(thresh2,2,1)
cnt2 = contours[0]
ret = cv2.matchShapes(cnt1,cnt2,1,0.0)
print( ret )

在下面三个形状中进行匹配:

image.png

结果:

  • A,A: 0.0
  • A,B: 0.001946
  • A,C: 0.326911

从结果中可以看到,这个匹配是具有一定的旋转不变性的。
(Hu-Moments矩阵是对平移、旋转和缩放不变的七个值,第七个是斜不变量。可以使用cv2.HuMoments函数来计算这些值)

5. 轮廓层次

本节讨论对象轮廓的层次结构,即多个轮廓间的父子关系。

在前面几节中,使用了OpenCV提供的与轮廓相关的一些函数。但是,当使用cv2.findContures()函数在图像中找到轮廓时,传入了一个参数,轮廓检索模式,通常使用cv2.RETR_LIST或cv2.RETR_tree,一般能取得不错的效果,但这到底意味着什么呢?

此外,在输出中,得到了三个数组,第一个是图像,第二个是轮廓,还有一个我们命名为层次结构的输出(前面小节的代码中可见)。不过在之前的章节中,从来没有使用过这个层次结构数据。那么这个层次是什么,能用来做什么,与传入的轮廓模式参数有什么关系呢?

5.1 轮廓层次概念

使用cv2.findContures函数寻找轮廓时,某些形状可能会位于其他形状的内部,就像嵌套的图形一样。在这种情况下,我们称外部为父级,内部为子级。这样,图像中的轮廓彼此之间就有了某种关系,此时可以描述轮廓间是如何相互连接的,比如是其他轮廓的子轮廓,还是父轮廓等等。这种关系的表示就是层次结构。例如:

image.png

在这张图中,有几个形状,分别从0到5编号。2和2a表示最外框的外部轮廓和内部轮廓。

在这里,轮廓0,1,2是外部的或或者说是最外面的轮廓,它们就在层级0中,简而言之,它们处于相同的层次结构级别。

接下来是轮廓2a,可以将其视为轮廓2的子级(或者说,轮廓2是轮廓2a的父级),所以让它处于层级1。类似地,轮廓3是轮廓2的子级,它位于下一个层次中。最后,轮廓4,5是轮廓3a的子级,它们位于最后一个层次级别。

5.2 轮廓层次在opencv中的表示

每个轮廓都有自己的层次信息,它是什么层次,谁是它的子对象,谁是它的父对象等等。OpenCV将其表示为一个由四个值组成的数组:[Next,Previous,First_Child,Parent],next/previous表同一层级的下一个/上一个轮廓。First_Child表示第一个子轮廓, Parent表示福轮廓,如果这些轮廓不存在,那么值都是-1.

例如在上图中取轮廓0。谁是它的同一层级的下一个轮廓呢?答案是是轮廓1。也就是说,next=1。同样,对于轮廓1,下一个是轮廓2,所以next=2。轮廓2同一级别中没有下一个轮廓,则next=-1。轮廓4与轮廓5处于同一层级,它的下一个轮廓是轮廓5,next=5。previous同理。

OpenCV对层次结构的表示已经明了,我们可以借助上面的相同图像来测试OpenCV中的轮廓检索模式,例如cv2.RETR_LIST、cv2.RETR_TREE、cv2.RETR_CCOMP、cv2.RETR_EXTERNAL等。

5.3 轮廓检索模式

5.3.1 RETR_LIST

这是四个检索中最好理解的一个,它只检索所有轮廓,但不创建任何父子关系。在这种模式下,父轮廓和子轮廓是平级的,都属于同一层次。

因此,层次数组中的第三项和第四项总是-1。而next和previous还是会有其相应的值。

下面是示例结果,每一行都是相应轮廓的层次细节。例如,第一行对应于轮廓0,下一个轮廓是轮廓1,所以next=1,没有上一个轮廓,因此previous=-1,parent和first_child都是-1。

array([[[ 1, -1, -1, -1],
        [ 2,  0, -1, -1],
        [ 3,  1, -1, -1],
        [ 4,  2, -1, -1],
        [ 5,  3, -1, -1],
        [ 6,  4, -1, -1],
        [ 7,  5, -1, -1],
        [-1,  6, -1, -1]]])

如果不使用层次特征的话,代码中推荐使用这种模式。

5.3.2 RETR_EXTERNAL

这种模式仅返回最外部轮廓,所有的子轮廓都会被丢弃,即只返回层级为0的轮廓。刚刚的例子中,就只会返回轮廓0,1,2。

array([[[ 1, -1, -1, -1],
        [ 2,  0, -1, -1],
        [-1,  1, -1, -1]]])

5.3.3 RETR_CCOMP

此模式会检索所有轮廓,并将其排列为两级层次结构。对象的外部轮廓放置在”层次1“中。对象内部孔的轮廓(如果有的话)放置在“层次2”中。如果对象中还有子对象,则其轮廓将再次放置在“层次1”中,内部空的轮廓放在层次2中,以此类推。

如下图,红色标记了轮廓的顺序,绿色标记了轮廓所属的层次(1或2)。该顺序与OpenCV检测轮廓的顺序相同。

image.png

得到的结果:

array([[[ 3, -1,  1, -1],
        [ 2, -1, -1,  0],
        [-1,  1, -1,  0],
        [ 5,  0,  4, -1],
        [-1, -1, -1,  3],
        [ 7,  3,  6, -1],
        [-1, -1, -1,  5],
        [ 8,  5, -1, -1],
        [-1,  7, -1, -1]]])

5.3.4 RETR_TREE

此模式检索所有轮廓并创建一个完整的层次列表,包含前面说的next,previous,parent,first_child。

image.png

最终得到的结果:

array([[[ 7, -1,  1, -1],
        [-1, -1,  2,  0],
        [-1, -1,  3,  1],
        [-1, -1,  4,  2],
        [-1, -1,  5,  3],
        [ 6, -1, -1,  4],
        [-1,  5, -1,  4],
        [ 8,  0, -1, -1],
        [-1,  7, -1, -1]]])

☞ 参与评论