学习如何寻找并绘制轮廓
学习计算轮廓特征,如面积、周长、最小外接矩形
目录
目标
轮廓
寻找轮廓
绘制轮廓
轮廓特征
轮廓面积
轮廓周长
图像矩
外接矩形
最小外接圆
拟合椭圆
形状匹配
练习
小结
目标
1.了解轮廓概念
2.寻找并绘制轮廓
3.OpenCV函数:cv2.findContours(),cv2.drawContours()
4.计算物体的周长、面积、质心、最小外接矩形等
5.OpenCV函数:cv2.contourArea(),cv2.arcLength(),cv2.approxPolyDP()等
轮廓
轮廓是一系列相连的点组成的曲线,代表了物体的基本外形。
谈起轮廓不免想到边缘,它们确实很像。简单的说,轮廓是连续的,边缘并不全都连续(下图)。其实边缘主要是作为图像的特征使用,比如可以用边缘特征可以区分脸和手,而轮廓主要用来分析物体的形态,比如物体的周长和面积等,可以说边缘包括轮廓
寻找轮廓的操作一般用于二值化图,所以通常会使用阈值分割或Canny边缘检测先得到二值图
注:寻找轮廓是针对白色物体的,一定要保证物体是白色,而背景是黑色,不然很多人在寻找轮廓时会找到图片最外面的一个框
寻找轮廓
使用cv2.findContours()寻找轮廓:
import cv2
img = cv2.imread('handwriting.jpg')
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(img_gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
# 寻找二值化图中的轮廓
image, contours, hierarchy = cv2.findContours(
thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
print(len(contours)) # 结果应该为2
参数2:轮廓的查找方式,一般使用cv2.RETR_TREE,表示提取所有的轮廓并建立轮廓间的层级。更多请参考:OpenCV: Structural Analysis and Shape Descriptors
参数3:轮廓的近似方法。比如对于一条直线,我们可以存储该直线的所有像素点,也可以只存储起点和终点。使用cv2.CHAIN_APPROX_SIMPLE就表示用尽可能少的像素点表示轮廓。更多请参考:OpenCV: Structural Analysis and Shape Descriptors
简便起见,这两个参数也可以直接用真值3和2表示
函数有3个返回值,image还是原来的二值化图片,hierarchy是轮廓间的层级关系,暂时不用理会。主要看contours,它就是找到的轮廓了,以数组形式存储,记录了每条轮廓的所有像素点的坐标(x,y)
绘制轮廓
轮廓找出来后,为了方便观看,可以像前面图中那样用红色画出来:cv2.drawContours()
cv2.drawContours(img, contours, -1, (0, 0, 255), 2)
其中参数2就是得到的contours,参数3表示要绘制哪一条轮廓,-1表示绘制所有轮廓,参数4是颜色(B/G/R通道,所以(0,0,255)表示红色),参数5是线宽,之前在绘制图形中介绍过
一般情况下,会首先获得要操作的轮廓,再进行轮廓绘制及分析:
cnt = contours[1]
cv2.drawContours(img, [cnt], 0, (0, 0, 255), 2)
轮廓特征
在计算轮廓特征之前,先把轮廓找到:
import cv2
import numpy as np
img = cv2.imread('handwriting.jpg', 0)
_, thresh = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
image, contours, hierarchy = cv2.findContours(thresh, 3, 2)
# 以数字3的轮廓为例
cnt = contours[0]
为了便于绘制,创建出两幅彩色图,并把轮廓画在第一幅图上:
img_color1 = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
img_color2 = np.copy(img_color1)
cv2.drawContours(img_color1, [cnt], 0, (0, 0, 255), 2)
轮廓面积
area = cv2.contourArea(cnt) # 4386.5
注意轮廓特征计算的结果并不等同于像素点的个数,而是根据几何方法算出来的,所以有小数
轮廓周长
perimeter = cv2.arcLength(cnt, True) # 585.7
参数2表示轮廓是否封闭,显然我们的轮廓是封闭的,所以是True
图像矩
矩可以理解为图像的各类几何特征,
M = cv2.moments(cnt)
M中包含了很多轮廓的特征信息,比如M['m00']表示轮廓面积,与前面cv2.contourArea()计算结果是一样的。质心也可以用它来算:
cx, cy = M['m10'] / M['m00'], M['m01'] / M['m00'] # (205, 281)
外接矩形
形状的外接矩形有两种,如下图,绿色的叫外接矩形,表示不考虑旋转并且能包含整个轮廓的矩形。蓝色的叫最小外接矩,考虑了旋转:
x, y, w, h = cv2.boundingRect(cnt) # 外接矩形
cv2.rectangle(img_color1, (x, y), (x + w, y + h), (0, 255, 0), 2)
rect = cv2.minAreaRect(cnt) # 最小外接矩形
box = np.int0(cv2.boxPoints(rect)) # 矩形的四个角点取整
cv2.drawContours(img_color1, [box], 0, (255, 0, 0), 2)
其中np.int0(x)是把x取整的操作,比如377.93就会变成377,也可以用x.astype(np.int)
最小外接圆
外接圆跟外接矩形一样,找到一个能包围物体的最小圆:
(x, y), radius = cv2.minEnclosingCircle(cnt)
(x, y, radius) = np.int0((x, y, radius)) # 圆心和半径取整
cv2.circle(img_color2, (x, y), radius, (0, 0, 255), 2)
拟合椭圆
我们可以用得到的轮廓拟合出一个椭圆:
ellipse = cv2.fitEllipse(cnt)
cv2.ellipse(img_color2, ellipse, (255, 255, 0), 2)
形状匹配
cv2.matchShapes()可以检测两个形状之间的相似度,返回值越小,越相似。先读入下面这张图片:
img = cv2.imread('shapes.jpg', 0)
_, thresh = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
image, contours, hierarchy = cv2.findContours(thresh, 3, 2)
img_color = cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR) # 用于绘制的彩色图
图中有3条轮廓,我们用A/B/C表示:
cnt_a, cnt_b, cnt_c = contours[0], contours[1], contours[2]
print(cv2.matchShapes(cnt_b, cnt_b, 1, 0.0)) # 0.0
print(cv2.matchShapes(cnt_b, cnt_c, 1, 0.0)) # 2.17e-05
print(cv2.matchShapes(cnt_b, cnt_a, 1, 0.0)) # 0.418
可以看到BC相似程度比AB高很多,并且图形的旋转或缩放并没有影响。
其中,参数3是匹配方法,详情可参考:OpenCV: Structural Analysis and Shape Descriptors
参数4是OpenCV的预留参数,暂时没有实现,可以不用理会
形状匹配是通过图像的Hu矩来实现的cv2.HuMoments()
练习
前面是对图片中的数字3进行轮廓特征计算的,换成数字1
import cv2
import numpy as np
# 载入手写数字图片
img = cv2.imread('handwriting.jpg', 0)
_, thresh = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
image, contours, hierarchy = cv2.findContours(thresh, 3, 2)
# 创建出两幅彩色图用于绘制
img_color1 = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
img_color2 = np.copy(img_color1)
# 计算数字1的轮廓特征
cnt = contours[1]
cv2.drawContours(img_color1, [cnt], 0, (0, 0, 255), 2)
# 1.轮廓面积
area = cv2.contourArea(cnt) # 6289.5
print(area)
# 2.轮廓周长
perimeter = cv2.arcLength(cnt, True) # 527.4041
print(perimeter)
# 3.图像矩
M = cv2.moments(cnt)
print(M)
print(M['m00']) # 同前面的面积:6289.5
cx, cy = M['m10'] / M['m00'], M['m01'] / M['m00'] # 质心
print(cx, cy)
# 4.图像外接矩形和最小外接矩形
x, y, w, h = cv2.boundingRect(cnt) # 外接矩形
cv2.rectangle(img_color1, (x, y), (x + w, y + h), (0, 255, 0), 2)
rect = cv2.minAreaRect(cnt) # 最小外接矩形
box = np.int0(cv2.boxPoints(rect)) # 矩形的四个角点并取整
cv2.drawContours(img_color1, [box], 0, (255, 0, 0), 2)
cv2.imshow('contours', img_color1)
cv2.waitKey(0)
# 5.最小外接圆
(x, y), radius = cv2.minEnclosingCircle(cnt)
(x, y, radius) = map(int, (x, y, radius)) # 这也是取整的一种方式
cv2.circle(img_color2, (x, y), radius, (0, 0, 255), 2)
# 6.拟合椭圆
ellipse = cv2.fitEllipse(cnt)
cv2.ellipse(img_color2, ellipse, (0, 255, 0), 2)
cv2.imshow('contours2', img_color2)
cv2.waitKey(0)
小结
1.轮廓特征非常有用,使用cv2.findContours()寻找轮廓,cv2.drawContours()绘制轮廓
2.常用的轮廓特征:
cv2.contourArea()算面积,cv2.arcLength()算周长,cv2.boundingRect()算外接矩
cv2.minAreaRect()算最小外接矩,cv2.minEnclosingCircle()算最小外接圆
cv2.matchShapes()进行形状匹配