【OpenCV】绘制图像轮廓和凸包检测
轮廓是一系列相连的点组成的曲线,代表了物体的基本外形。相对于边缘,轮廓是连续的,边缘不一定连续,如下图所示。其实边缘主要是作为图像的特征使用,比如可以用边缘特征可以区分脸和手,而轮廓主要用来分析物体的形态,比如物体的周长和面积等,可以说边缘包括轮廓。
绘制图像轮廓
1. 什么是轮廓?
轮廓是一系列相连的点组成的曲线,代表了物体的基本外形。相对于边缘,轮廓是连续的,边缘不一定连续,如下图所示。其实边缘主要是作为图像的特征使用,比如可以用边缘特征可以区分脸和手,而轮廓主要用来分析物体的形态,比如物体的周长和面积等,可以说边缘包括轮廓。

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]分别表示其后一条轮廓、前一条轮廓、(同层次的第一个)子轮廓、父轮廓的索引(如果没有对应的索引,则为负数)。该参数的使用情况会比较少。
- image:表示输入的二值化图像。
- mode:表示轮廓的检索模式。
- method:轮廓的表示方法。
2.1 mode参数
mode参数共有四个选项分别为:RETR_LIST,RETR_EXTERNAL,RETR_CCOMP,RETR_TREE。
RETR_LIST
表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,每一个轮廓只有前一条轮廓与后一条轮廓的索引,而没有父轮廓与子轮廓的索引。
RETR_EXTERNAL
表示只列出最外层的轮廓。并且在hierarchy里的轮廓关系中,每一个轮廓只有前一条轮廓与后一条轮廓的索引,而没有父轮廓与子轮廓的索引。
RETR_CCOMP
表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,轮廓会按照成对的方式显示。
RETR_TREE
表示列出所有的轮廓。并且在hierarchy里的轮廓关系中,轮廓会按照树的方式显示,其中最外层的轮廓作为树根,其子轮廓是一个个的树枝。
2.2 method参数
method参数有三个选项:CHAIN_APPROX_NONE、CHAIN_APPROX_SIMPLE、CHAIN_APPROX_TC89_L1。
CHAIN_APPROX_NONE表示将所有的轮廓点都进行存储;
CHAIN_APPROX_SIMPLE表示只存储有用的点,比如直线只存储起点和终点,四边形只存储四个顶点,默认使用这个方法;CHAIN_APPROX_TC89_L1表示使用Teh-Chin链逼近算法进行轮廓逼近。这种方法使用的是Teh-Chin链码,它是一种边缘检测算法,可以对轮廓进行逼近,减少轮廓中的冗余点,从而更加准确地表示轮廓的形状。CHAIN_APPROX_TC89_L1是一种较为精确的轮廓逼近方法,适用于需要较高精度的轮廓表示的情况。
对于mode和method这两个参数来说,一般使用RETR_EXTERNAL和CHAIN_APPROX_SIMPLE这两个选项。
3. 绘制轮廓
轮廓找出来后,其实返回的是一个轮廓点坐标的列表,因此我们需要根据这些坐标将轮廓画出来,因此就用到了绘制轮廓的方法。
cv2.drawContours(image, contours, contourIdx, color, thickness)
- image:原始图像,一般为单通道或三通道的 numpy 数组。
- contours:包含多个轮廓的列表,每个轮廓本身也是一个由点坐标构成的二维数组(numpy数组)。
- contourIdx:要绘制的轮廓索引。如果设为
-1,则会绘制所有轮廓。 - color:绘制轮廓的颜色,可以是 BGR 值或者是灰度值(对于灰度图像)。
- thickness:轮廓线的宽度,如果是正数,则画实线;如果是负数,则填充轮廓内的区域。
示例代码:
import cv2
image_np=cv2.imread("../src/num.png")
#进行灰度化
gray=cv2.cvtColor(image_np,cv2.COLOR_BGR2GRAY)
#二值化:轮廓为白色,其他为黑色
_,binary=cv2.threshold(gray,127,255,cv2.THRESH_BINARY_INV)
#查找轮廓
contours,hierarchy=cv2.findContours(binary,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
#绘制轮廓:image_np原始图像,contours查找到的所有轮廓的坐标点,contourldx第几个轮廓(2指的是第3个轮廓)
cv2.drawContours(image_np,contours,2,(255,0,0),2)
cv2.imshow("image_np",image_np)
cv2.imshow("binary",binary)
cv2.waitKey(0)
cv2.destroyAllWindows()

凸包特征检测
在进行凸包特征检测之前,首先要了解什么是凸包。通俗的讲,凸包其实就是将一张图片中物体的最外层的点连接起来构成的凸多边形,它能包含物体中所有的内容。
我们以点集来举例,假如有这么一些点,其分布如下图所示:

那么经过凸包检测并绘制之后,其结果应该如下图所示:

可以看到,原图像在经过凸包检测之后,会将最外围的几个点进行连接,剩余的点都在这些点的包围圈之内。那么凸包检测到底是怎么检测出哪些点是最外围的点呢?
我们还是以上面的点集为例,假设我们知道这些点的坐标,那么我们就可以找出处于最左边和最右边的点,如下图所示:

接着将这两个点连接,并将点集分为上半区和下半区,我们以上半区为例:

找到上面这些点离直线最远的点,其中,这条直线由于有两个点的坐标,所以其表示的直线方程是已知的,并且上面的点的坐标也是已知的,那么我们就可以根据点到直线的距离公式来进行计算哪个点到直线的距离最远,假设直线的方程为:Ax+By+C=0A x+B y+C=0Ax+By+C=0,那么点(x0,y0)(x_{0},y_{0})(x0,y0)到直线的距离公式为:
d=∣Ax0+By0+C∣A2+B2 d={\frac{|A x_{0}+B y_{0}+C|}{\sqrt{A^{2}+B^{2}}}} d=A2+B2∣Ax0+By0+C∣
然后我们就可以得到距离这条线最远的点,将其与左右两点连起来,并分别命名为y1和y2,如下图所示:

然后分别根据点的坐标求出y1和y2的直线方程,之后将上半区的每个点的坐标带入下面公式中:
d=Ax0+By0+CA2+B2 d={\frac{A x_{0}+B y_{0}+C}{\sqrt{A^{2}+B^{2}}}} d=A2+B2Ax0+By0+C
当d=0时,表明该点在直线上;当d>0时,表明点在直线的上方,在这里就代表该点在上图所围成的三角形的外面,也就意味着该三角形并没有完全包围住上半区的所有点,需要重新寻找凸包点;当d<0时,表明点在直线的下方,在这里就代表该点在上图所围成的三角形的里面,也就代表着这类点就不用管了。
当出现d>0时,我们需要将出现这种结果的两个计算对象:某点和y1或y2这条线标记,并在最后重新计算出现这种现象的点集到y1或y2的距离来获取新的凸包点的坐标。在本例子中,也就是如下图所示的点和y2这条直线:
由于本例子中只有这一个点在这个三角形之外,所以毫无疑问的它就是一个凸包点,因此直接将它与y2直线的两个端点相连即可。当有很多点在y2直线外时,就需要计算每个点到y2的距离,然后找到离得最远的点与y2构建三角形,并重新计算是否还有点在该三角形之外,如果没有,那么这个点就是新的凸包点,如果有,那就需要重复上面的步骤,直到所有的点都能被包围住,那么构建直线的点就是凸包点。这是上半区寻找凸包点的过程,下半区寻找凸包点的思路与此一模一样,只不过是需要筛选d<0(也就是点在直线的下方)的点,并重新构建三角形,寻找新的凸包点。
上面的过程都是基于我们知道点的坐标进行的,实际上,对于未经处理的图像,我们无法直接获取点的坐标。特别是对于彩色图像,我们需要将其转换为二值图像,并使用轮廓检测技术来获取轮廓边界的点的坐标。然后,我们才能进行上述寻找凸包点的过程。因此,在处理图像时,我们需要将彩色图像转换为二值图像,并通过轮廓检测技术来获取轮廓边界的点的坐标,然后才能进行凸包点的寻找过程。
获取凸包点
cv2.convexHull(points, hull=None, clockwise=False, returnPoints=True)
points:输入参数,图像的轮廓hull(可选):输出参数,用于存储计算得到的凸包顶点序列,如果不指定,则会创建一个新的数组。clockwise(可选):布尔值,如果为True,则计算顺时针方向的凸包,否则默认计算逆时针方向的凸包。returnPoints(可选):布尔值,如果为True(默认),则函数返回的是原始点集中的点构成的凸包顶点序列;如果为False,则返回的是凸包顶点对应的边界框内的索引。
绘制凸包
cv2.polylines(image, pts, isClosed, color, thickness=1)
image:要绘制线条的目标图像,它应该是一个OpenCV格式的二维图像数组(如numpy数组)。pts:一个二维 numpy 数组,每个元素是一维数组,代表一个多边形的一系列顶点坐标。isClosed:布尔值,表示是否闭合多边形,如果为 True,会在最后一个顶点和第一个顶点间自动添加一条线段,形成封闭的多边形。color:线条颜色,可以是一个三元组或四元组,分别对应BGR或BGRA通道的颜色值,或者是灰度图像的一个整数值。thickness(可选):线条宽度,默认值为1。
示例代码:
import cv2
image_np=cv2.imread("../src/tu.png")
#浅拷贝一份原始图像
convex_image=image_np.copy()
image_np_gray=cv2.cvtColor(image_np,cv2.COLOR_BGR2GRAY)
_,image_np_thresh=cv2.threshold(image_np_gray,127,255,cv2.THRESH_BINARY)
#查找轮廓
contours,hierachy=cv2.findContours(image_np_thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
# print(contours)
#寻找凸包
hull=cv2.convexHull(contours[0])
# print(hull)
#绘制凸包
cv2.polylines(convex_image,[hull],True,(255,0,0),2)
cv2.imshow("convex_image",convex_image)
cv2.waitKey(0)

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)