万变不离其宗在学习OpenCV中的轮廓之前,我们先来了解一下什么是轮廓,轮廓可以简单地解释为连接具有相同颜色或强度的所有连续点(沿边界)的曲线,轮廓是用于形状分析以及对象及检测和识别的有用工具
想要找到二进制图像的图像,我们需要用到一个函数:cv.findContours(),这个函数中包括三个参数,第一个是原图像,第二个是轮廓检索模式,第三个是轮廓逼近方法,这个函数输出等高线和层次结构,轮廓指的是图像中所有图像的Python列表,每个单独的轮廓是一个(x,y)坐标的Numpy数组的边界点的对象
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)
第二个参数轮廓检索模式的取值可以为:
RETR_EXTERNAL:只检索最外面的轮廓
RETR_LIST:检索所有的轮廓,并将其保存到一条链表中
RETR_CCOMP:检索所有的轮廓,并将它们组织为两层:顶层是各部分的外部边界,第二层是空洞的边界
RETR_TREE:常用,检索所有的轮廓,并重构嵌套轮廓的整个层次。保存了所有的轮廓
第三个参数轮廓逼近方法的取值可以为:
CHAIN_APPROX_NONE:以Freeman链码的方式输出轮廓,所有其他方法输出多边形(顶点的序列)。正常画出所有轮廓
CHAIN_APPROX_SIMPLE:压缩水平的、垂直的、斜的部分,即函数只保留他们的终点坐标。压缩得到更精简的结果,例如一个矩形轮廓只需4个点来保存轮廓信息
资料参考来自文章:【opencv】(6) 图像轮廓处理
通过cv.findContours()函数找到轮廓后,我们一般通过cv.drawContours()函数将轮廓绘制出来,只要有边界点,它也可以用来你绘制任何形状,这个函数的第一个参数为图像资源,第二个参数是应该作为Python列表传递的轮廓,第三个参数是轮廓的索引(在绘制单个轮廓时有用,如果需要绘制所有的轮廓,传入参数-1即可),其余的参数是颜色厚度等公共参数
import cv2 as cv
import numpy as np
from matplotlib import pyplot as plt
img = cv.imread('E:/image/test09.png')
# 在寻找轮廓时要传入单通道图像
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
ret, threshold = cv.threshold(gray, 127, 255, cv.THRESH_BINARY)
contours, hierarchy = cv.findContours(threshold, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
# 绘制所有轮廓
img1 = cv.drawContours(img, contours, -1, (0, 255, 0), 3)
# 绘制单个轮廓
img2 = cv.drawContours(img, contours, 3, (0, 255, 0), 3)
# 常用的绘制轮廓的方法
cnt = contours[3]
img3 = cv.drawContours(img, [cnt], 0, (0, 255, 0), 3)
plt.subplot(1, 3, 1), plt.imshow(img, cmap='gray')
plt.title('Original'), plt.xticks([]), plt.yticks([])
plt.subplot(1, 3, 2), plt.imshow(img, cmap='gray')
plt.title('ALL_IMG'), plt.xticks([]), plt.yticks([])
plt.subplot(1, 3, 3), plt.imshow(img, cmap='gray')
plt.title('Usually'), plt.xticks([]), plt.yticks([])
plt.show()
我们前面说到的cv.findContours()函数有三个参数传递,第三个参数是轮廓逼近方法,也就是我们这里要说的轮廓近似方法。轮廓是强度相同的形状的边界,它存储形状边界的(x,y)坐标,但是它也并不是存储所有的坐标
而它存不存储所有的坐标,就取决于轮廓逼近方法的使用,如果我们传入了cv.CHAIN_APPROX_NONE参数,那么轮廓将存储所有的坐标,但是在实际情况中,我们有必要存储所有的坐标吗?
比如我们找到了一个矩形的轮廓,存储所有的坐标会浪费很多空间,我们只存储四个顶点的坐标就可以了,这个操作就是参数cv.CHAIN_APPROX_SIMPLE,这种轮廓逼近方法可以满足我们的需求
在学习特征矩之前我们先要了解它的概念,首先特征矩代表了一个轮廓、一幅图像的全局特征,矩信息包含了对应对象不同类型的几何特征,特征矩分为三种:空间矩、中心矩和归一化中心矩
中心矩:对于高阶图像,特征矩会随着位置的变化而变化,为了解决这个问题中心矩就应运而生,它通过减去均值而获取平移的不变性,因而能比较不同位置的两个对象是否一致,即中心矩具有平移不变性特征
归一化中心矩:除平移之外,有些图像我们还会碰到缩放的情况,即在缩放后也能判断其特征,归一化中心矩通过除以物体总尺寸而获得缩放不变性
回到我们OpenCV的特征矩,它可以帮助我们计算一些特征,例如质心、面积等,一个重要的函数cv.moments()提供了所有计算出矩值的字典
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 )
轮廓区域由函数cv.contourArea()或从矩 M[‘m00’] 中给出
周长也称为弧长,可以使用cv.arcLength()函数找到它。第二个参数指定形状是闭合轮廓( True )还是曲线
area = cv.contourArea(cnt)
perimeter = cv.arcLength(cnt,True)
轮廓近似就是根据我们指定的精读,通过道格拉斯-普客算法,将轮廓形状近似为顶点数量较少的其他形状
说到道格拉斯-普客算法,那必须来瞧瞧它的本质:将图像数字化时,对曲线进行采样,即在曲线上取有限个点,将其连接变成折线,并且在一定程度上保持原有的形状
import cv2
import numpy as np
from matplotlib import pyplot as plt
src = cv2.imread('E:/image/test10.png')
gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(~thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cnt = contours[0]
img1 = src.copy()
epslion1 = 0.05 * cv2.arcLength(cnt, True)
approx1 = cv2.approxPolyDP(cnt, epslion1, True)
img1 = cv2.drawContours(img1, [approx1], 0, (0, 0, 255), 2)
img2 = src.copy()
epslion2 = 0.01 * cv2.arcLength(cnt, True)
approx2 = cv2.approxPolyDP(cnt, epslion2, True)
img2 = cv2.drawContours(img2, [approx2], 0, (0, 255, 0), 2)
plt.subplot(1, 2, 1), plt.imshow(img1, cmap='gray')
plt.title('JinSi1'), plt.xticks([]), plt.yticks([])
plt.subplot(1, 2, 2), plt.imshow(img2, cmap='gray')
plt.title('JInSi2'), plt.xticks([]), plt.yticks([])
plt.show()
作为前面知识的回顾,我们分析绘制出图像的两个精度不同的近似轮廓的全过程,首先在加载一些我们需要的第三方库,方便我们在代码中使用其方法等,然后在第5行代码我们通过cv,imread()函数读取了一个图像,然后使用了cv.cvtColor()函数将其转化为灰度图,为什么要转化为灰度图呢?因为我们接下来要使用图像阈值!在全局阈值函数cv.threshold()函数中我们要传入的参数有四个,第一个参数是我们的源图像,它一般是灰度图像,这就是为什么要把img变为gray,第二个参数是我们设置的阈值,它用于把图像中的像素做一个分类,大于阈值的像素都将被设置为最大值,第三个参数就是我们设置的最大值,第四个参数是一个表示不同类型的标志
代码到了第8行,从这里开始就要涉及到我们这一部分学到的内容了,首当其冲就是找到轮廓,我们使用了cv.findContours()函数,这个函数的三个参数分别是图像、轮廓检索模式和轮廓逼近方法
到了11行,一个陌生的东西出现了,它就是轮廓近似的核心代码之一,epslion是一个与近似精度密切相关的参数,它表示从轮廓到近似轮廓的最大距离,怎么算这个最大距离呢,那就要用到一个函数了:cv.arcLength(),是不是很眼熟,它就是我们上面说过的求轮廓周长的函数,我们需要选择正确的epsilon才能获得正确的输出
如果我们把11行代码的0.05改成0.1,我们就会发现得不到我们想要的输出结果,这就是这个参数epsilon太粗糙了,这个参数越小,我们获得的轮廓就越近似
轮廓凸包与轮廓近似看起来像,其实一点儿关系没有,关于轮廓凸包我们也有一个重要的函数cv.convexHull()函数,它用来检查曲线是否存在凸凹缺陷并对其进行校正,而校不校正呢,这就由这个函数的参数说了算
一般来说凸曲线一般都是凸出或平坦的曲线,如果在内部凸出了(凹进去了)我们就称其为凸度缺陷,凸度缺陷我们后面再细说
关于函数cv.convexHull()的语法我们摊开说说,它本来长这样:
hull = cv.convexHull(points[, hull[, clockwise[, returnPoints]]
point是我们传递的轮廓(为什么用的是点(point)呢?是不是忘了轮廓的实质是由具有相同颜色或强度的所有连续点连接起来的曲线),hull(凸出)是输出,我们一般都忽略它,clockwise是方向标记,如果传入True则是顺时针,反之为逆时针,returnPoint默认情况下是True,它返回凸包的坐标,如果为False,则返回与凸包点相对应的轮廓点的索引
然后上例子!(不断实践才能成长!)
import cv2
import numpy as np
from matplotlib import pyplot as plt
src = cv2.imread('E:/image/test11.png')
gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY)
ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
# 寻找轮廓
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
hull = cv2.convexHull(cnt)
length = len(hull)
# 如果凸包点集中的点个数大于5
if length > 5:
# 绘制图像凸包的轮廓
for i in range(length):
cv2.line(src, tuple(hull[i][0]), tuple(hull[(i + 1) % length][0]), (0, 0, 255), 2)
cv2.imshow('line', src)
cv2.waitKey()
文章借鉴来源:OpenCV入门之寻找图像的凸包(convex hull)
直角矩形不考虑物体的旋转,所以直角边界矩形的面积不是最小的,寻找这个矩形涉及了一个函数: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)
旋转边界矩形是用最小面积绘制的,所以它考虑了物体的旋转,寻找旋转边界矩形涉及的函数是cv.minAreaRect(),它返回的是一个Box2D结构,其中包含了坐标、宽高和旋转角度,但是我们想要得到这个矩形还是比较麻烦的,我们需要四个顶点的坐标
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))
radius = int(radius)
cv.circle(img,center,radius,(0,255,0),2)
拟合一个对象的椭圆会使用到函数:cv.fitEllipse()
ellipse = cv.fitEllipse(cnt)
cv.ellipse(img,ellipse,(0,255,0),2)
我们来看看应用代码:
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('E:/image/test13.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 设置阈值
ret, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cnt = contours[0]
x, y, w, h = cv2.boundingRect(cnt)
img = cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
rect = cv2.minAreaRect(cnt)
box = cv2.boxPoints(rect)
box = np.int0(box)
img = cv2.drawContours(img, [box], 0, (0, 0, 255), 2)
cv2.imshow('img', img)
cv2.waitKey(0)
cv2.destroyWindow()
(注:文章内容参考OpenCV4.1中文官方文档)
如果文章对您有所帮助,记得一键三连支持一下哦