编辑者:廿瓶鲸(和鲸社区Siby团队成员)
轮廓线可以简单地解释为连接所有连续点(沿边界)的曲线,具有相同的颜色或灰度。轮廓线是形状分析和物体检测与识别的一个有用工具。
让我们来看看如何找到二进制图像的轮廓线。
import numpy as np
import cv2 as cv
im = cv.imread('test.jpg')
imgray = cv.cvtColor(im, cv.COLOR_BGR2GRAY)
ret, thresh = cv.threshold(imgray, 127, 255, 0)
contours, hierarchy = cv.findContours(thresh, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
cv.findContours()函数中有三个参数,第一个是源图像,第二个是轮廓检索模式,第三个是轮廓逼近方法。然后它输出轮廓线和层次结构。轮廓线是一个包含图像中所有轮廓线的Python列表。每个单独的轮廓线是一个Numpy数组,包含物体边界点的(x,y)坐标。
注意:我们将在后面详细讨论第二个和第三个参数以及层次结构。在那之前,代码样本中给它们的值对所有的图像都能正常工作。
为了绘制轮廓线,我们使用了cv.drawContours函数。它也可以用来绘制任何形状,只要你有它的边界点。它的第一个参数是源图像,第二个参数是轮廓线,应该以Python列表的形式传递,第三个参数是轮廓线的索引(在绘制单个轮廓线时很有用。 要绘制所有轮廓线,传递-1),其余参数是颜色、厚度等。
cv.drawContours(img, contours, -1, (0,255,0), 3)
cv.drawContours(img, contours, 3, (0,255,0), 3)
cnt = contours[4]
cv.drawContours(img, [cnt], 0, (0,255,0), 3)
注意事项:最后两种方法是一样的,但是你会发现最后一种方法更有用。
这是cv.findContours函数的第三个参数。它实际上表示什么呢?
上面我们说过,轮廓线是具有相同灰度的形状的边界。它存储了一个形状的边界的(x,y)坐标。但它是否存储了所有的坐标?这是由这个轮廓逼近方法指定的。
如果你传递cv.CHAIN_APPROX_NONE,所有的边界点都会被存储。但实际上我们需要所有的点吗?例如,你找到了一条直线的轮廓。你需要这条线上的所有点来表示这条直线吗?不,我们只需要那条线的两个端点。这就是cv.CHAIN_APPROX_SIMPLE的作用。它删除了所有多余的点并压缩了轮廓,从而节省了内存。
下面是一个矩形的图片,演示了这个技术。只要在轮廓线数组中的所有坐标上画一个圆(用蓝色画)。第一张图片显示了我用cv.CHAIN_APPROX_NONE得到的点(734个点),第二张图片显示了用cv.CHAIN_APPROX_SIMPLE的点(只有4个点)。看,它节省了多少内存!!!。
在这篇文章中,我们将学习
图像矩帮助你计算一些特征,如物体的质心、物体的面积等。
函数cv.ments()给出了一个所有计算出的矩的字典。见下文:
import numpy as np
import cv2 as cv
img = cv.imread('star.jpg',0)
ret,thresh = cv.threshold(img,127,255,0)
contours,hierarchy = cv.findContours(thresh, 1, 2)
cnt = contours[0]
M = cv.moments(cnt)
print(M)
从这个矩,你可以提取有用的数据,如面积、中心点等。中心点是由Cx=M10/M00和Cy=M01/M00的关系给出的。这可以按以下方式进行。
cx = int(M['m10']/M['m00'])
cy = int(M['m01']/M['m00'])
轮廓线面积由函数cv.contourArea()或从矩M[‘m00’]给出。
area = cv.contourArea(cnt)
它也被称为弧长。它可以用cv.arcLength()函数计算出来。第二个参数指定形状是一个封闭的轮廓(如果传递的是True),还是只是一条曲线。
perimeter = cv.arcLength(cnt,True)
它根据我们指定的精度,将一个轮廓形状逼近到另一个顶点数量较少的形状。它是Douglas-Peucker算法的一个实现。
为了理解这一点,假设你试图在图像中找到一个正方形,但由于图像中的一些问题,你没有得到一个完美的正方形,而是一个 “坏形状”(如下图所示)。现在,你可以用这个函数来近似地处理这个形状。在这个函数中,第二个参数叫做epsilon,它是轮廓到近似轮廓的最大距离。它是一个精度参数。为了得到正确的输出,需要明智地选择epsilon。
epsilon = 0.1*cv.arcLength(cnt,True)
approx = cv.approxPolyDP(cnt,epsilon,True)
下面,在第二张图片中,绿线显示了epsilon为弧长的10%时的近似曲线。第三张图显示的是epsilon为弧长的1%时的情况。第三个参数指定曲线是否是封闭的。
凸面体看起来与轮廓逼近相似,但它不是(两者在某些情况下可能提供相同的结果)。在这里,cv.convexHull()函数检查曲线是否有凸性缺陷并进行修正。一般来说,凸形曲线是指总是凸出来的曲线,或者至少是平的。而如果是向内隆起,则被称为凸性缺陷。例如,请看下面的手的图片。红线表示手的凸体。双面的箭头标志显示了凸性缺陷,这是局部最大凸包与轮廓的偏差。
关于它的语法,有一点需要讨论。
hull = cv.convexHull(point[, hull[, clockwise[, returnPoints])
参数细节:
因此,要得到上图中的凸包,只需按以下方法即可:
hull = cv.convexHull(cnt)
但是如果你想找到凸性缺陷,你需要传递returnPoints = False。为了理解它,我们将采取上面的矩形图像。首先,我发现它的轮廓为cnt。现在我用returnPoints = True找到了它的凸面,我得到了以下值。[[234 202]], [[51 202]], [[51 79]], [[234 79]]是矩形的四个角点。现在如果用returnPoints = False做同样的事情,我得到的结果是:[[129], [67], [0], [142]]。这些是轮廓线中相应的点的索引。例如,检查第一个值:cnt[129] = [[234, 202]],这与第一个结果相同(其他的也是如此)。
当我们讨论凸性缺陷时,你会再次看到它。
有一个函数可以检查一条曲线是否是凸的,即cv.isContourConvex()。它只是返回True或False。没什么大不了的。
k = cv.isContourConvex(cnt)
有两种类型的边界矩形。
7.a. 直线边界矩形
这是一个直线矩形,它不考虑物体的旋转。因此,边界矩形的面积不会是最小的。它是由函数cv.boundingRect()找到的。
(x,y)为矩形的左上角坐标,(w,h)为其宽度和高度。
x,y,w,h = cv.boundingRect(cnt)
cv.rectangle(img,(x,y),(x+w,y+h),(0,255,0),2)
7.b. 旋转的矩形
这里,边界矩形是以最小面积绘制的,所以它也考虑了旋转。使用的函数是cv.minAreaRect()。它返回一个包含以下细节的Box2D结构–(中心(x,y),(宽度,高度),旋转的角度)。但是要画这个矩形,我们需要矩形的4个角。它可以通过函数cv.boxPoints()获得
rect = cv.minAreaRect(cnt)
box = cv.boxPoints(rect)
box = np.int0(box)
cv.drawContours(img,[box],0,(0,0,255),2)
两个矩形都显示在一张图片上。绿色矩形显示的是正常的边界矩形。红色矩形是旋转后的矩形。
接下来,我们使用cv.minEnclosingCircle()函数找到一个物体的圆。它是一个以最小面积完全覆盖物体的圆。
(x,y),radius = cv.minEnclosingCircle(cnt)
center = (int(x),int(y))
半径 = int(radius)
cv.circle(img,center,radius,(0,255,0),2)
下一个是将一个椭圆拟合到一个物体上。它返回旋转后的矩形以及内接的椭圆。
ellipse = cv.fitEllipse(cnt)
cv.ellipse(img,ellipse,(0,255,0),2)
同样地,我们可以将一条线拟合到一组点上。下面的图片包含一组白色的点。我们可以对它进行近似的直线拟合。
rows,cols = img.shape[:2] 。
[vx,vy,x,y] = cv.fitLine(cnt, cv.DIST_L2,0,0.01,0.01)
lefty = int((-x*vy/vx) + y)
righty = int(((cols-x)*vy/vx)+y)
cv.line(img,(cols-1,righty),(0,lefty),(0,255,0) ,2)
在这里,我们将学习如何提取一些常用的物体属性,如实体性、等效直径、掩膜图像、平均灰度等。更多的特征可以在Matlab regionprops文档中找到。
(注意:中心点、面积、周长等也属于这一类,但我们在上一章已经看到了)
它是物体的边界矩形的宽度和高度的比率。
A s p e c t R a t i o = W i d t h H e i g h t Aspect \; Ratio = \frac{Width}{Height} AspectRatio=HeightWidth
x,y,w,h = cv.boundingRect(cnt)
aspect_ratio = float(w)/h
外延是指轮廓线面积与边界矩形面积的比率。
E x t e n t = O b j e c t A r e a B o u n d i n g R e c t a n g l e A r e a Extent = \frac{Object \; Area}{Bounding \; Rectangle \; Area} Extent=BoundingRectangleAreaObjectArea
area = cv.contourArea(cnt)
x,y,w,h = cv.boundingRect(cnt)
rect_area = w*h
extent = float(area)/rect_area
实体性是指轮廓面积与凸包面积的比率。
S o l i d i t y = C o n t o u r A r e a C o n v e x H u l l A r e a Solidity = \frac{Contour \; Area}{Convex \; Hull \; Area} Solidity=ConvexHullAreaContourArea
area = cv.contourArea(cnt)
hull = cv.convexHull(cnt)
hull_area = cv.contourArea(hull)
solidity = float(area)/hull_area
等效直径是指其面积与轮廓面积相同的圆的直径。
E q u i v a l e n t D i a m e t e r = 4 × C o n t o u r A r e a π Equivalent \; Diameter = \sqrt{\frac{4 \times Contour \; Area}{\pi}} EquivalentDiameter=π4×ContourArea
area = cv.contourArea(cnt)
equi_diameter = np.sqrt(4*area/np.pi)
方向是指物体指向的角度。以下方法也给出了主轴和次轴的长度。
(x,y),(MA,ma),angle = cv.fitEllipse(cnt)
在某些情况下,我们可能需要包括该对象的所有点。可以按以下方式进行:
mask = np.zeros(imgray.shape,np.uint8)
cv.drawContours(mask,[cnt],0,255,-1)
pixelpoints = np.transpose(np.nonzero(mask))
#pixelpoints = cv.findNonZero(mask)
这里给出了两种方法,一种是使用Numpy函数,另一种是使用OpenCV函数(最后一行注释)来做同样的事情。结果也是一样的,但有一点不同。Numpy给出的坐标是(行,列)格式,而OpenCV给出的坐标是(x,y)格式。所以基本上答案会互换。注意,row=y,column=x。
我们可以用掩膜图像找到这些参数。
min_val, max_val, min_loc, max_loc = cv.minMaxLoc(imgray,mask = mask)
在这里,我们可以找到一个物体的平均颜色。也可以是灰度模式下物体的平均灰度。我们再次使用相同的掩膜来做这件事。
mean_val = cv.mean(im,mask = mask)
极点指的是物体的最上面、最下面、最右边和最左边的点。
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])
例如,如果我把它应用于印度地图,我得到以下结果。