15 绘制图像轮廓
15.1 什么是轮廓
轮廓是一系列相连的点组成的曲线,代表了物体的基本外形。相对于边缘,轮廓是连续的,边缘不一定连续,如下图所示。轮廓是一个闭合的、封闭的形状。
-
轮廓的作用:
-
形状分析
-
目标识别
-
图像分割
-
15.2 寻找轮廓
在OpenCV中,使用cv2.findContours()来进行寻找轮廓,其原理过于复杂,这里只进行一个简单的介绍,具体的实现原理可参考:
https://zhuanlan.zhihu.com/p/107257870
寻找轮廓需要将图像做一个二值化处理,并且根据图像的不同选择不同的二值化方法来将图像中要绘制轮廓的部分置为白色,其余部分置为黑色。也就是说,我们需要对原始的图像进行灰度化、二值化的处理,令目标区域显示为白色,其他区域显示为黑色,如下图所示。
之后,对图像中的像素进行遍历,当一个白色像素相邻(上下左右及两条对角线)位置有黑色像素存在或者一个黑色像素相邻(上下左右及两条对角线)位置有白色像素存在时,那么该像素点就会被认定为边界像素点,轮廓就是有无数个这样的边界点组成的。
下面具体介绍一下cv2.findContours()函数,其函数原型为:
contours,hierarchy = cv2.findContours(image,mode,method)
返回值:[ 轮廓点坐标 ] 和 [ 层级关系 ]。
contours:表示获取到的轮廓点的列表。检测到有多少个轮廓,该列表就有多少子列表,每一个子列表都代表了一个轮廓中所有点的坐标。
hierarchy:表示轮廓之间的关系。对于第i条轮廓,hierarchy[i][0], hierarchy[i][1] , hierarchy[i][2] , hierarchy[i][3]分别表示其后一条轮廓、前一条轮廓、(同层次的第一个)子轮廓、父轮廓的索引(如果没有相应的轮廓,则对应位置为-1)。该参数的使用情况会比较少。
image:表示输入的二值化图像。
mode:表示轮廓的检索模式。
method:轮廓的表示方法。
15.2.1 mode参数
轮廓查找方式。返回不同的层级关系。
mode参数共有四个选项分别为:RETR_LIST,RETR_EXTERNAL,RETR_CCOMP,RETR_TREE。
RETR_EXTERNAL
表示只查找最外层的轮廓。并且在hierarchy里的轮廓关系中,每一个轮廓只有前一条轮廓与后一条轮廓的索引,而没有父轮廓与子轮廓的索引。
2.3.4.会查找所有轮廓,但会有层级关系。
RETR_LIST
表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,每一个轮廓只有前一条轮廓与后一条轮廓的索引,而没有父轮廓与子轮廓的索引。
RETR_CCOMP
表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,轮廓会按照成对的方式显示。
在 RETR_CCOMP
模式下,轮廓被分为两个层级:
层级 0:所有外部轮廓(最外层的边界)。
层级 1:所有内部轮廓(孔洞或嵌套的区域)。
RETR_TREE
表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,轮廓会按照树的方式显示,其中最外层的轮廓作为树根,其子轮廓是一个个的树枝。
15.2.2 method参数
轮廓存储方法。轮廓近似方法。决定如何简化轮廓点的数量。就是找到轮廓后怎么去存储这些点。
method参数有三个选项:CHAIN_APPROX_NONE、CHAIN_APPROX_SIMPLE、CHAIN_APPROX_TC89_L1。
CHAIN_APPROX_NONE
表示将所有的轮廓点都进行存储
CHAIN_APPROX_SIMPLE
表示只存储有用的点,比如直线只存储起点和终点,四边形只存储四个顶点,默认使用这个方法;
对于mode和method这两个参数来说,一般使用RETR_EXTERNAL和CHAIN_APPROX_SIMPLE这两个选项。
15.3 绘制轮廓
轮廓找出来后,其实返回的是一个轮廓点坐标的列表,因此我们需要根据这些坐标将轮廓画出来,因此就用到了绘制轮廓的方法。
cv2.drawContours(image, contours, contourIdx, color, thickness)
image:原始图像,一般为单通道或三通道的 numpy 数组。
contours:包含多个轮廓的列表,每个轮廓本身也是一个由点坐标构成的二维数组(numpy数组)。
contourIdx:要绘制的轮廓索引。如果设为 -1
,则会绘制所有轮廓。根据索引找到轮廓点绘制出来。默认是-1。
color:绘制轮廓的颜色,可以是 BGR 值或者是灰度值(对于灰度图像)。
thickness:轮廓线的宽度,如果是正数,则画实线;如果是负数,则填充轮廓内的区域。
img = cv.imread('images/de.jpg')
#灰度处理
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
#二值化 用反阈值法
ret, binary = cv.threshold(gray, 127, 255, cv.THRESH_BINARY_INV)
#查找轮廓
contours, hierarchy = cv.findContours(binary, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)
#绘制轮廓
cv.drawContours(img, contours, -1, (100, 100, 255), 3)
#显示结果
cv.imshow('Original Image', img)
cv.imshow('Binary Image', binary)
cv.waitKey(0)
cv.destroyAllWindows()
16 凸包特征检测
在进行凸包特征检测之前,首先要了解什么是凸包。通俗的讲,凸包其实就是将一张图片中物体的最外层的点连接起来构成的凸多边形,它能包含物体中所有的内容。
一般来说,凸包都是伴随着某类点集存在的,也被称为某个点集的凸包。
对于一个点集来说,如果该点集存在凸包,那么这个点集里面的所有点要么在凸包上,要么在凸包内。
凸包检测常用在物体识别、手势识别、边界检测等领域。
穷举法
QuickHull法
1.穷举法
将集中的点进行两两配对,并进行连线,对于每条直线,检查其余所有的点是否处于该直线的同一侧,如果是,那么说明构成该直线的两个点就是凸包点,其余的线依次进行计算,从而获取所有的凸包点。
用向量的思想,点都是有坐标的,连起来就可以构成一个向量。再以其中一个点,连接另一个点,构成另一个向量,让两个向量做外积,就是叉积。也就是std=|向量a|*|向量b|*sin(\theta),能控制std的正负的只能是\theta,如果计算出来的std的正负都相同,说明这些点都在这条直线的同一侧,那么这两个点就是凸包的边界点。然后换两个点,就是说换一条直线,换一个向量,继续进行检测,直到找到凸包的所有的边界点。
缺点:时间复杂度高,不断使用for循环,耗时。
2.QuickHull法
将所有点放在二维坐标系中,找到横坐标最小和最大的两个点P1和P2并连线。此时整个点集被分为两部分,直线上为上包,直线下为下包。
以上保暖为例,找到上包中的点距离该直线最远的点P3,连线并寻找直线P1P3左侧的点和P2P3右侧的点,然后重复本步骤,直到找不到为止。对下包也是这样操作。
我们以点集来举例,假如有这么一些点,其分布如下图所示:
那么经过凸包检测并绘制之后,其结果应该如下图所示:
可以看到,原图像在经过凸包检测之后,会将最外围的几个点进行连接,剩余的点都在这些点的包围圈之内。那么凸包检测到底是怎么检测出哪些点是最外围的点呢?
我们还是以上面的点集为例,假设我们知道这些点的坐标,那么我们就可以找出处于最左边和最右边的点,如下图所示:
接着将这两个点连接,并将点集分为上半区和下半区,我们以上半区为例:
找到上面这些点离直线最远的点,其中,这条直线由于有两个点的坐标,所以其表示的直线方程是已知的,并且上面的点的坐标也是已知的,那么我们就可以根据点到直线的距离公式来进行计算哪个点到直线的距离最远,假设直线的方程为:A x+B y+C=0,那么点(x0,y0)到直线的距离公式为:
然后我们就可以得到距离这条线最远的点,将其与左右两点连起来,并分别命名为y1和y2,如下图所示:
然后分别根据点的坐标求出y1和y2的直线方程,之后将上半区的每个点的坐标带入下面公式中:
当d=0时,表明该点在直线上;当d>0时,表明点在直线的上方,在这里就代表该点在上图所围成的三角形的外面,也就意味着该三角形并没有完全包围住上半区的所有点,需要重新寻找凸包点;当d<0时,表明点在直线的下方,在这里就代表该点在上图所围成的三角形的里面,也就代表着这类点就不用管了。
当出现d>0时,我们需要将出现这种结果的两个计算对象:某点和y1或y2这条线标记,并在最后重新计算出现这种现象的点集到y1或y2的距离来获取新的凸包点的坐标。在本例子中,也就是如下图所示的点和y2这条直线:
由于本例子中只有这一个点在这个三角形之外,所以毫无疑问的它就是一个凸包点,因此直接将它与y2直线的两个端点相连即可。当有很多点在y2直线外时,就需要计算每个点到y2的距离,然后找到离得最远的点与y2构建三角形,并重新计算是否还有点在该三角形之外,如果没有,那么这个点就是新的凸包点,如果有,那就需要重复上面的步骤,直到所有的点都能被包围住,那么构建直线的点就是凸包点。这是上半区寻找凸包点的过程,下半区寻找凸包点的思路与此一模一样,只不过是需要筛选d<0(也就是点在直线的下方)的点,并重新构建三角形,寻找新的凸包点。
上面的过程都是基于我们知道点的坐标进行的,实际上,对于未经处理的图像,我们无法直接获取点的坐标。特别是对于彩色图像,我们需要将其转换为二值图像,并使用轮廓检测技术来获取轮廓边界的点的坐标。然后,我们才能进行上述寻找凸包点的过程。因此,在处理图像时,我们需要将彩色图像转换为二值图像,并通过轮廓检测技术来获取轮廓边界的点的坐标,然后才能进行凸包点的寻找过程。
16.1 获取凸包点
cv2.convexHull(points)
points
:输入参数,图像的轮廓
16.2 绘制凸包
cv2.polylines(image, pts, isClosed, color, thickness=1)
image
:要绘制线条的目标图像,它应该是一个OpenCV格式的二维图像数组(如numpy数组)。
pts
:一个二维 numpy 数组,每个元素是一维数组,代表一个多边形的一系列顶点坐标。
isClosed
:布尔值,表示是否闭合多边形,如果为 True,会在最后一个顶点和第一个顶点间自动添加一条线段,形成封闭的多边形。
color
:线条颜色,可以是一个三元组或四元组,分别对应BGR或BGRA通道的颜色值,或者是灰度图像的一个整数值。
thickness
(可选):线条宽度,默认值为1。
img = cv.imread('images/menghuwang.jpg')
#灰度图
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
#二值化处理,阈值法 因为目标是白色我们需要白色
ret, binary = cv.threshold(gray, 200, 255, cv.THRESH_BINARY_INV)
#查找轮廓
contours, hierarchy = cv.findContours(binary, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
for i in range(len(contours)):#查找凸包hull = cv.convexHull(contours[i])#绘制凸包cv.polylines(img, [hull], True, (100, 100, 255), 3)
print(hull)
cv.imshow('1', img)
cv.imshow('2', binary)
cv.waitKey(0)
cv.destroyAllWindows()