更全更详细的博客
目标
• 理解什么是轮廓
• 学习找轮廓,绘制轮廓等
• 函数:cv2.findContours(),cv2.drawContours()
前面所提到的边缘检测虽然能够检测出图像的边缘,但是边缘是不连续的,检测到的边缘并不是一个整体。图像轮廓是指将边缘连接起来形成一个整体。
边缘是不连续的,不是一个整体
图像轮廓是将边缘连接起来形成一个整体。
图像轮廓是图像中重要的特征信息,通过对图像轮廓的操作我们能够获取目标图像的大小、位置、方向
1.1 初始轮廓
1.1.1 什么是轮廓
- 轮廓可以简单认为成将连续的点(连着边界)连在一起的曲线,具有相同的颜色或者灰度。
- 为了更加准确,要使用二值化图像。在寻找轮廓之前,要进行
阈值化处理或者 Canny 边界检测
。 - 查找轮廓的函数会修改原始图像。如果你在找到轮廓之后还想使用原始图像的话,你应该将原始图像存储到其他变量中。
- 在 OpenCV 中,查找轮廓就像在黑色背景中超白色物体。你应该记住,要找的物体应该是白色而背景应该是黑色。
cv2.findContours()
- 第一个是输入图像
- 第二个是轮廓检索模式
- 第三个是轮廓近似方法
返回值有三个,第一个是图像,第二个是轮廓,第三个是(轮廓的)层析结构.轮廓(第二个返回值)是一个 Python列表,其中存储这图像中的所有轮廓。每一个轮廓都是一个 Numpy 数组,包含对象边界点(x,y)的坐标。
如何绘制轮廓
cv2.findContours(img,mode, method)
-
img表示输入的图片
-
mode表示
轮廓检索模式
,通常都使用RETR_TREE
找出所有的轮廓值
-
method表示
轮廓逼近方法
,使用NONE表示所有轮廓都显示.轮廓是一个形状具有相同灰度值的边界。它会存贮形状边界上所有的 (x,y) 坐标
cv2.drawContours(img, contours, -1, (0, 0, 255), 2)
它可以根据你提供的边界点绘制任何形状
- img表示输入的需要画的图片
- contours表示轮廓值,一个 Python 列表。
- -1表示轮廓的索引,第三个参数是轮廓的索引(在绘制独立轮廓是很有用,当设置为 -1 时绘制所有轮廓)
- 接下来的参数是轮廓的颜色和厚度等 (0, 0, 255)表示颜色,2表示线条粗细在一幅图像上绘制所有的轮廓
img = cv2.imread('Pytorch.png')
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(imgray, 127, 255, 0)
cv2.imshow('thresh', thresh)
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
'''
opencv2返回两个值:一个是轮廓本身contours,还有一个是每条轮廓对应的属性hierarchy。
opencv3返回三个值:img, countours, hierarchy
'''
print(len(contours))
img2 = cv2.drawContours(img, contours, -1, (0, 0, 0), 2)
cv2.imshow('img2', img2)
cv2.waitKey(0)
大坑:绘制轮廓要在原图像上绘制
轮廓特征
• 查找轮廓的不同特征,例如面积,周长,重心,边界框等。
• 你会学到很多轮廓相关函数
轮廓矩
- 函数 cv2.moments() 会将计算得到的矩以一个字典的形式返回。
- 在OpenCV中可以通过cv2.moments()函数来获取图像的轮廓特征,通常情况下,我们将获取到的轮廓特征称为轮廓矩。轮廓矩描述了一个轮廓的重要特征,使用轮廓矩可以很方便的比较两个轮廓
轮廓面积
- 轮廓的面积可以使用函数 cv2.contourArea() 计算得到,也可以使用矩(0 阶矩),M[‘m00’]。
- area = cv2.contourArea(cnt)
轮廓的周长
- cv2.arcLength()
- 这个函数的第二参数可以用来指定对象的形状是闭合的(True),还是打开的(一条曲线)。
轮廓近似
- 将轮廓形状近似到另外一种由更少点组成的轮廓形状,新轮廓的点的数目由我们设定的准确度来决定
- 假设我们要在一幅图像中查找一个矩形,但是由于图像的种种原因,我们不能得到一个完美的矩形,而是一个“坏形状”(如下图所示)现在你就可以使用这个函数来近似这个形状()了。这个函数的第二个参数叫epsilon,它是从原始轮廓到近似轮廓的最大距离。它是一个准确度参数。选择一个好的 epsilon 对于得到满意结果非常重要。
凸包
- 凸包与轮廓近似相似,但不同,虽然有些情况下它们给出的结果是一样的。函数 cv2.convexHull() 可以用来检测一个曲线是否具有凸性缺陷,并能纠正缺陷。一般来说,凸性曲线总是凸出来的,至少是平的。如果有地方凹进去了就被叫做凸性缺陷。例如下图中的手。红色曲线显示了手的凸包,凸性缺陷被双箭头标出来了。
-
hull = cv2.convexHull(points[, hull[, clockwise[, returnPoints]]
- points 我们要传入的轮廓
- hull 输出,通常不需要
- clockwise 方向标志。如果设置为 True,输出的凸包是顺时针方向的。否则为逆时针方向。
- returnPoints 默认值为 True。它会返回凸包上点的坐标。如果设置为 False,就会返回与凸包点对应的轮廓上的点。但是如果你想获得凸性缺陷,需要把 returnPoints 设置为 False。以上面的矩形为例,首先我们找到他的轮廓 cnt。现在我把 returnPoints 设置为 True 查找凸包,
凸性检测
- 函数 cv2.isContourConvex() 可以可以用来检测一个曲线是不是凸的。它只能返回 True 或 False。
边界矩形
- 直边界矩形 一个直矩形(就是没有旋转的矩形)。它不会考虑对象是否旋转。所以边界矩形的面积不是最小的。可以使用函数
cv2.boundingRect()
查找得到。(x,y)为矩形左上角的坐标,(w,h)是矩形的宽和高。
x,y,w,h = cv2.boundingRect(cnt)
img = cv2.rectangle(img,(x,y),(x+w,y+h),(0,255,0),2)
- 这个边界矩形是面积最小的,因为它考虑了对象的旋转。用到的函数为
cv2.minAreaRect()
。返回的是一个 Box2D 结构,其中包含矩形左上角角点的坐标(x,y),矩形的宽和高(w,h),以及旋转角度。但是要绘制这个矩形需要矩形的 4 个角点,可以通过函数cv2.boxPoints()
获得。
x,y,w,h = cv2.boundingRect(cnt)
img = cv2.rectangle(img,(x,y),(x+w,y+h),(0,255,0),2)
最小外接圆
- 函数
cv2.minEnclosingCircle()
可以帮我们找到一个对象的外切圆。它是所有能够包括对象的圆中面积最小的一个。
(x,y),radius = cv2.minEnclosingCircle(cnt)
center = (int(x),int(y))
radius = int(radius)
img = cv2.circle(img,center,radius,(0,255,0),2)
椭圆拟合
- 使用的函数为 cv2.ellipse(),返回值其实就是旋转边界矩形的内切圆。
ellipse = cv2.fitEllipse(cnt)
im = cv2.ellipse(im,ellipse,(0,255,0),2)
直线拟合
- 我们可以根据一组点拟合出一条直线,同样我们也可以为图像中的白色点拟合出一条直线。
轮廓特征
长宽比
x,y,w,h = cv2.boundingRect(cnt)
aspect_ratio = float(w)/h
Extent
- 轮廓面积与边界矩形面积的比。
area = cv2.contourArea(cnt)
x,y,w,h = cv2.boundingRect(cnt)
rect_area = w*h
extent = float(area)/rect_area
Solidity
- 轮廓面积与凸包面积的比。
area = cv2.contourArea(cnt)
hull = cv2.convexHull(cnt)
hull_area = cv2.contourArea(hull)
solidity = float(area)/hull_area
Equivalent Diameter
area = cv2.contourArea(cnt)
equi_diameter = np.sqrt(4*area/np.pi)
方向
- 对象的方向,下面的方法还会返回长轴和短轴的长度
(x,y),(MA,ma),angle = cv2.fitEllipse(cnt)
掩模和像素点
- 有时我们需要构成对象的所有像素点
- 这里我们是用来两种方法,第一种方法使用了 Numpy 函数,第二种使用
了 OpenCV 函数。结果相同,但还是有点不同。Numpy 给出的坐标是row,colum)形式的。而 OpenCV 给出的格式是(x,y)形式的。所以这两个结果基本是可以互换的。row=x,colunm=y。
最大值
- 我们可以使用掩模图像得到这些参数。
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(imgray,mask = mask)
平均颜色及平均灰度值
mean_val = cv2.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])
轮廓更多函数
目标
我们要学习
• 凸缺陷,以及如何找凸缺陷
• 找某一点到一个多边形的最短距离
• 不同形状的匹配
凸缺陷
- 前面我们已经学习了轮廓的凸包,对象上的任何凹陷都被成为凸缺陷。
- OpenCV 中有一个函数 cv.convexityDefect() 可以帮助我们找到凸缺陷。函数调用如下:
hull = cv2.convexHull(cnt,returnPoints = False)
defects = cv2.convexityDefects(cnt,hull)
它会返回一个数组,其中每一行包含的值是 [起点,终点,最远的点,到最远点的近似距离]。我们可以在一张图上显示它。我们将起点和终点用一条绿线连接,在最远点画一个圆圈,要记住的是返回结果的前三个值是轮廓点的索引。所以我们还要到轮廓点中去找它们
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)
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()
点到轮廓的最短距离
- 求解图像中的一个点到一个对象轮廓的最短距离。如果点在轮廓的外部,返回值为负。如果在轮廓上,返回值为 0。如果在轮廓内部,返回值为正。
dist = cv2.pointPolygonTest(cnt,(50,50),True)
- 此函数的第三个参数是 measureDist。如果设置为 True,就会计算最短距离。如果是 False,只会判断这个点与轮廓之间的位置关系
形状匹配
- 函数 cv2.matchShape() 可以帮我们比较两个形状或轮廓的相似度。如果返回值越小,匹配越好。它是根据 Hu 矩来计算的。文档中对不同的方法都有解释。
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)
contours,hierarchy = cv2.findContours(thresh,2,1)
cnt1 = contours[0]
contours,hierarchy = cv2.findContours(thresh2,2,1)
cnt2 = contours[0]
ret = cv2.matchShapes(cnt1,cnt2,1,0.0)
注意:Hu 矩是归一化中心矩的线性组合,之所以这样做是为了能够获取代表图像的某个特征的矩函数,这些矩函数对某些变化如缩放,旋转,镜像映射(除了 h1)具有不变形。
案例:
- 创建一个小程序,可以将图片上的点绘制成不同的颜色,颜色是根据这个
点到轮廓的距离来决定的。要使用的函数:cv2.pointPolygonTest()。
img = cv2.imread('./star.png',)
imgray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(imgray, 127, 255, 0)
print('thresh.shape',thresh.shape)
cnts, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
x,y = imgray.shape
for i in range(x):
for j in range(y):
dst0 = cv2.pointPolygonTest(cnts[1], (i, j), 1)
imgray[i, j] = (dst0) # 根据距离轮廓的距离的绝对值来确定点的颜色
cv2.imshow('show',imgray)
cv2.waitKey(0)
案例:
使用函数 cv2.matchShapes() 匹配带有字母或者数字的图片。
识别原理
- 将待识别图像 -> 灰度图像 -> 二值图像
- 通过轮廓检索函数 cv.findContours 找到待识别图像所有轮廓
- 模板图像 -> 灰度图像 -> 二值图像
- 通过轮廓检索函数 cv.findContours 找到模板图像中字母 A 的外轮廓
- 将第2步得到的轮廓逐一和第4步得到的轮廓 通过 cv.matchShapes 函数进行形状匹配。找到其中最小值,最小值对应的待识别图像中的轮廓即为匹配到的模板图像
- 标出在待识别图像中找到的模板图像
import cv2 as cv
import numpy as np
# 图像匹配 : 在img中找img_a
# 载入原图
img = cv.imread('abc.jpg', 0)
# 二值化图像
_, thresh = cv.threshold(img, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)
# 搜索轮廓
contours, hierarchy = cv.findContours(thresh, 3, 2)
hierarchy = np.squeeze(hierarchy) # 去掉维度为1的
# 载入标准模板图
img_a = cv.imread('a.jpg')
img_gray = cv.cvtColor(img_a, cv.COLOR_BGR2GRAY)
_, thresh = cv.threshold(img_gray, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU)
contours1, hierarchy1 = cv.findContours(thresh, cv.RETR_LIST, 2)
cv.drawContours(img_a, contours1, 1, (0, 0, 255), 2)
# 字母A的轮廓
template_a = contours1[1] # 最外圈的索引是1
# 记录最匹配的值的大小和位置
min_pos = -1
min_value = 2
for i in range(len(contours)):
# 参数3:匹配方法;参数4:opencv预留参数
value = cv.matchShapes(template_a, contours[i], 1, 0.0)
if value < min_value:
min_value = value
min_pos = i
# 参数3为0表示绘制本条轮廓contours[min_pos]
cv.drawContours(img, [contours[min_pos]], 0, [255, 0, 0], 3)
cv.imshow('result', img)
cv.waitKey(0)
cv.destroyAllWindows()
http://www.newxtc.com/article.php?id=237
案例3:找出最大轮廓
import cv2 as cv
import numpy as np
image = cv.imread('a.jpg')
img_copy = image.copy()
area = []
# 灰度化
img_gray = cv.cvtColor(image,cv.COLOR_BGR2GRAY)
# 二值化
ret, binary = cv.threshold(img_gray, 0, 130, cv.THRESH_BINARY | cv.THRESH_OTSU)
# 寻找轮廓
contours, hierarchy = cv.findContours(binary, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
# 画出所有轮廓
cv.drawContours(img_copy, contours, -1, (0, 255, 0), 3)
cv.imshow("all", img_copy)
# 找到最大的轮廓
for k in range(len(contours)):
area.append(cv.contourArea(contours[k]))
print(k)
max_idx = np.argmax(np.array(area)) # 用于返回一个numpy数组中最大值的索引值
# 最大轮廓面积计算
area_black = cv.contourArea(contours[max_idx])
# Draw contours
cv.drawContours(image, contours, max_idx, (0, 255, 0), 3)
cv.imshow("max", image)
cv.waitKey(0)
轮廓的层次结构
目标
现在我们要学习轮廓的层次结构了,比如轮廓之间的父子关系。
什么是层次结构
- 通常我们使用函数 cv2.findContours 在图片中查找一个对象。有时对象可能位于不同的位置。还有些情况,一个形状在另外一个形状的内部。这种情况下我们称外部的形状为父,内部的形状为子。按照这种方式分类,一幅图像中的所有轮廓之间就建立父子关系。这样我们就可以确定一个轮廓与其他轮廓是怎样连接的,比如它是不是某个轮廓的子轮廓,或者是父轮廓。这种关系就成为组织结构
OPencv中层次结构
组织结构数组:[Next,Previous,First_Child,Parent]
- 不管层次结构是什么样的,每一个轮廓都包含自己的信息:谁是父,谁是子等。OpenCV 使用一个含有四个元素的数组表示。[Next,Previous,First_Child,Parent]。按照从上往下,从左往右的顺序排序
- Next 表示同一级组织结构中的下一个轮廓。当本级只有一个轮廓时,next=-1
- Previous 表示同一级结构中的前一个轮廓。
- First_Child 表示它的第一个子轮廓。
- Parent 表示它的父轮廓。与 First_Child 刚好相反。
按照从上往下,从左往右的顺序排序.如果没有父或子,就为 -1。
轮廓的检索模式
RETR_LIST
从解释的角度来看,这中应是最简单的。它只是提取所有的轮廓,而不去创建任何父子关系。换句话说就是“人人平等”,它们属于同一级组织轮廓。组织结构数组的第三和第四个数都是 -1RETR_EXTERNAL
只会返回最外边的的轮廓,所有的子轮廓都会被忽略掉RETR_CCOMP
在这种模式下会返回所有的轮廓并将轮廓分为两级组织结构。RETR_TREE
会返回所有轮廓,并且创建一个完整的组织结构列表。它甚至会告诉你谁是爷爷,爸爸,儿子,孙子等