第五节、轮廓检测、直线和圆、多边形检测
目录
一、轮廓检测
在计算机视觉中,轮廓检测是另一个比较重要的任务,不单是用来检测图像或者视频帧中物体的轮廓,而且还有其他操作与轮廓检测相关。这些操作中,计算多边形边界,形状逼近和计算机感 兴趣区域。
这是与图像数据交互时的简单操作,因为numpy
中的矩阵中的矩形区域可以使用数组切片(slice
)定义。在介绍物体检测(包括人脸)和物体跟踪的概念时会大量使用这种技术。
1.1 图像阈值操作
为了从一幅图像中提取我们需要的部分,应该用图像中的每一个像素点的灰度值与选取的阈值进行比较,并作出相应的判断(阈值的选取依赖于具体的问题,物体在不同的图像中可能会有不同的灰度值)。
opencv
提供了threshold()
函数对图像的阈值进行处理,图像阈值操作函数原型:
cv2.threshold(src,thresh,maxval,type[,dst])
threshold()
共支持五中类型的阈值化方式,分别是:
- 二进制阈值化;
- 反二进制阈值化;
- 截断阈值化;
- 阈值化为0和反阈值化为0。
函数返回阈值操作后的图像。函数参数如下:
src
: 输入图像,图像必须为单通道8位或32位浮点型图像;thresh
: 设定的阈值;maxval
::使用cv2.THRESH_BINARY
和cv2.THRESH_BINARY_INV
类型的最大值;type
: 阈值化类型,可以通过ThresholdTypes
查看,下面给出opencv
中五种阈值化类型及其对应公式:

dst
: 输出图像,与输入图像尺寸和类型相同;
1.2 寻找图像轮廓
opencv
中提供findContours()
函数来寻找图像中物体的轮廓,并结合drawContours()
函数将找到的轮廓绘制出。寻找图像轮廓函数原型:
cv2.findContours(image,mode,method[,contours,hierarchy[,offset]])
这个函数会修改输入图像,因此建议使用原始图像的一份拷贝(比如说img.copy()
作为输入图像)。函数返回三个值:返回修改后的图像,图像的轮廓以及它们的层次。
函数参数:
(1) image
:输入图像,函数接受的参数是二值图,即黑白的(不是灰度图),我们同样可以使用cv2.compare
,cv2.inRange
,cv2.threshold
,cv2.adaptiveThreshold
,cv2.Canny
等函数来创建二值图像,如果第二个参数为cv2.RETR_CCOMP
或cv2.RETR_FLOODFILL
,输入图像可以是32-bit
整型图像(cv2.CV_32SC1
);
(2) mode
:轮廓检索模式,如下:

其中:
RETR_EXTERNAL
:表示只检测最外层轮廓,这对消除包含在其他轮廓中的轮廓很有用(比如在大多数情况下,不需要检测一个目标包含在另一个与之相同的目标里面),对所有轮廓设置hierarchy[i][2]=hierarchy[i][3]=-1
;RETR_LIST
:提取所有轮廓,检测的轮廓不建立等级关系;RETR_CCOMP
:提取所有轮廓,并将轮廓组织成双层结构(two-level hierarchy
),顶层为连通域的外围边界,次层位内层边界;RETR_TREE
:提取所有轮廓并重新建立网状轮廓结构;RETR_FLOODFILL
:官网没有介绍,应该是洪水填充法;
(3) method
:轮廓近似方法,如下:

其中:
CHAIN_APPROX_NONE
:获取每个轮廓的每个像素,相邻的两个点的像素位置差不超过1;CHAIN_APPROX_SIMPLE
:压缩水平方向,垂直方向,对角线方向的元素,值保留该方向的重点坐标,如果一个矩形轮廓只需4个点来保存轮廓信息;CHAIN_APPROX_TC89_L1
和CHAIN_APPROX_TC89_KCOS
使用Teh-Chinl
链逼近算法中的一种;contours
:检测到的轮廓(list
),每个轮廓都是一个ndarray
,每个ndarray
是一个轮廓上点的集合。一个轮廓并不是存储轮廓上所有的点,而是只存储可以用直线描述轮廓的点,比如一个正方形,只需要四个顶点就能描述轮廓了;hierarchy
:函数返回一个可选的hierarchy
结果,这是一个ndarray
,形状为[1,轮廓个数,4],其中hierarchy[0]
元素的个数和轮廓个数相同。每个轮廓contours[0][i]
对应4个hierarchy
元素hierarchy[0][i][0]~hierarchy[0][i][3]
,分别表示后一个轮廓,前一个轮廓,父轮廓,内嵌轮廓的索引,如果没有对应项,则相应的hierarchy[0][i]
设置为负数;
(4) offset
:轮廓点可选偏移量,有默认值。
1.3 轮廓绘制
轮廓绘制函数原型:
cv2.drawContours(image,contours,contourIdx,color[,thickness[,lineType[,hierarchy[,maxLevel[,offset]]]]])
该函数返回绘制有轮廓的图像。
image
:输入/输出图像,指明在哪个图像上绘制轮廓。并且该函数会修改源图像image
;contours
:使用findContours
检测到的轮廓数据,传入一个list
。contourIdx
:绘制轮廓的索引变量(表示绘制第几个轮廓),如果为负值则绘制所有输入轮廓;color
:轮廓颜色;thickness
:绘制轮廓所用线条粗细度,如果值为负值,则在轮廓内部绘制;lineTpye
:线条类型,有默认值LINE_8,有如下可选类型;hierarchy
:可选层次结构信息;maxLevel
:用于绘制轮廓的最大等级;offset
:可选轮廓便宜参数,用制定偏移量offset=(dx, dy)
给出绘制轮廓的偏移量;
1.4 示例程序
轮廓检测示例:
'''
轮廓检测
'''
#加载图像img
img = cv2.imread('./image/img6.jpg',cv2.IMREAD_COLOR)
cv2.imshow('img',img)
#转换为灰色gray_img
gray_img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
cv2.imshow('gray_img',gray_img)
#对图像二值化处理 输入图像必须为单通道8位或32位浮点型
ret,thresh = cv2.threshold(gray_img,127,255,0)
cv2.imshow('thresh',thresh)
#寻找图像轮廓 返回修改后的图像 图像的轮廓 以及它们的层次
image,contours,hierarchy = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
cv2.imshow('image',image)
print('contours[0]:',contours[0])
print('len(contours)',len(contours))
print('hierarchy,shape',hierarchy.shape)
print('hierarchy[0]:',hierarchy[0])
#在原图img上绘制轮廓contours
img = cv2.drawContours(img,contours,-1,(0,255,0),2)
cv2.imshow('contours',img)
cv2.waitKey()
cv2.destroyAllWindows()


二、查找最小闭圆轮廓案例
找到一个正方形轮廓很简单,要找到到不规则的,歪斜的以及旋转的形状,可以用OpenCV
的cv2.findContours()
函数,它能得到最好的结果,下面来看一副图:
现实的应用会对目标的边界框,最小矩形面积,最小闭圆特别感兴趣,将cv2.findContours()
函数和少量的OpenCV
的功能相结合就非常容易实现这些功能:
使用boundingRect()
函数计算包围轮廓的矩形框,使用minEnclosingCircle()
函数计算包围轮廓的最小圆包围。
2.1 计算矩形边界框
cv2.boundingRect(img)
函数 计算并返回点集最外面的矩形边界,参数一般传入一个轮廓,contours[0]
;
x,y,w,h = cv2.boundingRect(c)
函数返回四个值,分别是x,y,w,h
。x,y
是矩阵左上点的左边,w,h
是矩阵的宽和高。
然后画出这个矩形(在原图img
上绘制):这个操作非常简单,它将轮廓信息转换为(x,y)
坐标,并加上矩形的高度和宽度。
cv2.rectangle(img,(x,y),(x+w,y+h),(0,255,0),2)
下面来将如何找到一个旋转的矩阵和圆形轮廓。 首先加载图片,然后在源图像的灰度图像上面执行一个二值化操作。这样之后,可在这个灰度图像上执行所有计算轮廓的操作,但在源图像上可利用色彩信息来画这些轮廓。
2.2 计算最小矩形边界框
计算包含出包围目标的最小矩形区域(旋转矩形):
#找到最小区域
rect = cv2.minAreaRect(c)
#计算最小矩形的坐标
box = cv2.boxPoints(rect)
#坐标转换为整数
box = np.int0(box)
这里用到一个非常有趣的机制:OpenCV
没有函数能直接从轮廓信息中计算出最小矩形顶点的坐标。所以需要计算最小矩形区域,然后计算这个矩形的顶点。注意计算出来的顶点左边是浮点型,但是所得像素的坐标值是整数,所以需要做一个转换。
函数cv2.minAreaRect()
返回一个tuple
:(最小外接矩形的中心(x,y)
,(宽度,高度),旋转角度)。
但是要绘制这个矩形,我们需要矩形的4个顶点坐标box
, 通过函数cv2.cv.BoxPoints()
获得,box
:[ [x0,y0], [x1,y1], [x2,y2], [x3,y3] ]
;
最小外接矩形的4个顶点顺序、中心坐标、宽度、高度、旋转角度(是度数形式,不是弧度数)的对应关系如下:

注意:旋转角度θ
是水平轴(x
轴)逆时针旋转,与碰到的矩形的第一条边的夹角。并且这个边的边长是width
,另一条边边长是height
。也就是说,在这里,width
与height
不是按照长短来定义的。
在opencv
中,坐标系原点在左上角,相对于x轴,逆时针旋转角度为负,顺时针旋转角度为正。在这里,θ∈(-90度,0]
。
然后画出这个矩形(在原图img
上绘制):
cv2.drawContours(img,[box],0,(255,0,0),3)
首先,该函数与所有绘图函数一样,它会修改源,其次该函数的第二个参数接收一个保存着轮廓的数组,从而可以在一次操作中绘制一系列的轮廓。因此如果只有一组点来表示多边形轮廓,可以把这组点放到一个list
中,就像前面例子里处理方框(box
)那样。这个函数第三个参数是绘制的轮廓数组的索引,-1表示绘制所有的轮廓,否则只绘制轮廓数组里指定的轮廓。
大多数绘图函数把绘图的颜色和线宽放在最后两个参数里。
2.3 计算最小闭圆
cv2.minEnclosingCircle
( points )函数 利用迭代算法,对给定的二维点集寻找计算可包围点集的最小圆形;
其中参数有:
points
: 输入的二维点集,一般传入一个轮廓contours[0]
;
函数会返回一个元组,第一个元素为圆心的坐标组成的元素,第二个元素为圆的半径值。把这些值转换为整数后就能很容易地绘制出圆来。
代码如下:
#计算闭圆中心店和和半径
(x,y),radius = cv2.minEnclosingCircle(c)
#转换为整型
center = (int(x),int(y))
radius = int(radius)
#绘制闭圆(在原图img上绘制)
img = cv2.circle(img,center,radius,(0,255,0),2)
2.4 完整代码
完整代码如下:
'''
边框 最小矩形区域和最小闭圆的轮廓
'''
img = cv2.pyrDown(cv2.imread('./image/img16.jpg',cv2.IMREAD_UNCHANGED))
#转换为灰色gray_img
gray_img = cv2.cvtColor(img.copy(),cv2.COLOR_BGR2GRAY)
#对图像二值化处理 输入图像必须为单通道8位或32位浮点型
ret,thresh = cv2.threshold(gray_img,127,255,cv2.THRESH_BINARY)
#寻找最外面的图像轮廓 返回修改后的图像 图像的轮廓 以及它们的层次
image,contours,hierarchy = cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
print(type(contours))
print(type(contours[0]))
print(len(contours))
#遍历每一个轮廓
for c in contours:
#找到边界框的坐标
x,y,w,h = cv2.boundingRect(c)
#在img图像上 绘制矩形 线条颜色为green 线宽为2
cv2.rectangle(img,(x,y),(x+w,y+h),(0,255,0),2)
#找到最小区域
rect = cv2.minAreaRect(c)
#计算最小矩形的坐标
box = cv2.boxPoints(rect)
#坐标转换为整数
box = np.int0(box)
#绘制轮廓 最小矩形 blue
cv2.drawContours(img,[box],0,(255,0,0),3)
#计算闭圆中心店和和半径
(x,y),radius = cv2.minEnclosingCircle(c)
#转换为整型
center = (int(x),int(y))
radius = int(radius)
#绘制闭圆
img = cv2.circle(img,center,radius,(0,255,0),2)
cv2.drawContours(img,contours,-1,(0,0,255),2)
cv2.imshow('contours',img)
运行后的结果:
三、凸轮廓与Douglas-Peucker
算法
大多数处理轮廓的时候,图的形状(包括凸形状)都是变化多样的。凸形状内部的任意两点的连线都在该形状内部。
3.1 cv2.approxPloyDP
cv2.approxPloyDP
函数,它用来计算近似的多边形框。该函数有三个参数:
- 第一个参数为轮廓;
- 第二个参数为
ε
值,它表示源轮廓与近似多边形的最大差值(这个值越小,近似多边形与源轮廓越接近); - 第三个参数为布尔标记,它表示这个多边形是否闭合;
ε
值对获取有用的轮廓非常重要,所以需要理解它表示什么意思。ε
是为所得到的近似多边形周长与源轮廓周长之间的最大差值,这个值越小,近似多边形与源轮廓就越相似。
为什么有了一个精确表示的轮廓却还需要得到一个近似多边形呢?这是因为一个多边形由一组直线构成,能够在一个区域里定义多边形,以便于之后进行操作与处理,这在许多计算机视觉任务中非常重要。
3.2 cv2.arcLength
在了解了ε
值是什么之后,需要得到轮廓的周长信息来作为参考值。这可以通过cv2.arcLength
函数来完成:
#arcLength获取轮廓的周长
epsilon = 0.01*cv2.arcLength(cnt,True)
#计算矩形的多边形框
approx = cv2.approxPolyDP(cnt,epsilon,True)
3.3 cv2.convexHull
可以通过OpenCV
来有效地计算一个近似多边形。为了计算凸形状,需要利用cv2.convexHull
来处理获取的轮廓信息。
#从轮廓信息中计算得到凸形状
hull = cv2.convexHull(cnt)
3.4 测试
为了理解源轮廓、近似多边形和凸包的不同之处,可以把他们放在一副图片中进行观察:
img = cv2.imread('./image/img18.jpg',cv2.IMREAD_COLOR)
img = cv2.resize(img,None,fx=0.6,fy=0.6,interpolation=cv2.INTER_CUBIC)
#创建一个空白图像,用来绘制轮廓
canvas = np.zeros(img.shape,np.uint8)
#转换为灰色gray_img
gray_img = cv2.cvtColor(img.copy(),cv2.COLOR_BGR2GRAY)
#进行均值滤波,去除一些噪声
kernel = np.ones((3,3),np.float32)/9
gray_img = cv2.filter2D(gray_img,-1,kernel)
#cv2.imshow('gray_img',gray_img)
#对图像二值化处理 输入图像必须为单通道8位或32位浮点型 像素>125 设置为0(黑) 否则设置为255(白)
ret,thresh = cv2.threshold(gray_img,125,255,cv2.THRESH_BINARY_INV)
#cv2.imshow('thresh',thresh)
#寻找图像轮廓 返回修改后的图像 图像的轮廓 以及它们的层次
image,contours,hierarchy = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
#获取最大的一个轮廓
cnt = contours[0]
max_area = cv2.contourArea(cnt)
#对每一个轮廓进行遍历
for cont in contours:
if cv2.contourArea(cont) > max_area:
cnt = cont
max_area = cv2.contourArea(cont)
print('max_area',max_area)
'''计算最大轮廓的多边形框'''
#arcLength获取轮廓的周长
epsilon = 0.01*cv2.arcLength(cnt,True)
#计算矩形的多边形框
approx = cv2.approxPolyDP(cnt,epsilon,True)
#从轮廓信息中计算得到凸形状
hull = cv2.convexHull(cnt)
print('contours',len(contours),type(contours))
print('cnt.shape',cnt.shape,type(cnt))
print('approx.shape',approx.shape,type(approx))
print('hull.shape',hull.shape,type(hull))
#在源图像中绘制所有轮廓 传入的死一个list
cv2.drawContours(img,contours,-1,(0,255,0),2) #GREEN 绘制所有的轮廓
cv2.drawContours(canvas,[cnt],-1,(0,255,0),2) #GREEN 绘制最大的轮廓
cv2.drawContours(canvas,[approx],-1,(0,0,255),2) #RED 绘制最大轮廓对应的多边形框
cv2.drawContours(canvas,[hull],-1,(255,0,0),2) #BLUE 绘制最大轮廓对应的凸包
cv2.imshow('img',img)
cv2.imshow('ALL',canvas)
cv2.waitKey()
cv2.destroyAllWindows()

如上图所示,凸包是由蓝色表示,然后里面是近似多边形,使用红色表示,在两者之间的是源图片中一个最大的轮廓,它主要由弧线构成。
四、直线和圆检测
检测边缘和轮廓不仅重要,还经常用到,它们也是构成其他复杂操作的基础。直线和形状检查与边缘和轮廓检测有密切的关系。
Hough
变换是直线和形状检测背后的理论基础,它由Richard Duda
和Peter Hart
发明,他们是对Paul Hough
在20世纪60年代早期所做工作的扩展。
Hough
变换的原理实际上是将直线坐标问题转换为参数平面问题,具体实现原理可以参考:《史上最详细的Hough
直线检测》。
4.1 直线检测
亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。
日期 | 姓名 | 金额 |
---|---|---|
2023-09-06 | *源 | 19 |
2023-09-11 | *朝科 | 88 |
2023-09-21 | *号 | 5 |
2023-09-16 | *真 | 60 |
2023-10-26 | *通 | 9.9 |
2023-11-04 | *慎 | 0.66 |
2023-11-24 | *恩 | 0.01 |
2023-12-30 | I*B | 1 |
2024-01-28 | *兴 | 20 |
2024-02-01 | QYing | 20 |
2024-02-11 | *督 | 6 |
2024-02-18 | 一*x | 1 |
2024-02-20 | c*l | 18.88 |
2024-01-01 | *I | 5 |
2024-04-08 | *程 | 150 |
2024-04-18 | *超 | 20 |
2024-04-26 | .*V | 30 |
2024-05-08 | D*W | 5 |
2024-05-29 | *辉 | 20 |
2024-05-30 | *雄 | 10 |
2024-06-08 | *: | 10 |
2024-06-23 | 小狮子 | 666 |
2024-06-28 | *s | 6.66 |
2024-06-29 | *炼 | 1 |
2024-06-30 | *! | 1 |
2024-07-08 | *方 | 20 |
2024-07-18 | A*1 | 6.66 |
2024-07-31 | *北 | 12 |
2024-08-13 | *基 | 1 |
2024-08-23 | n*s | 2 |
2024-09-02 | *源 | 50 |
2024-09-04 | *J | 2 |
2024-09-06 | *强 | 8.8 |
2024-09-09 | *波 | 1 |
2024-09-10 | *口 | 1 |
2024-09-10 | *波 | 1 |
2024-09-12 | *波 | 10 |
2024-09-18 | *明 | 1.68 |
2024-09-26 | B*h | 10 |
2024-09-30 | 岁 | 10 |
2024-10-02 | M*i | 1 |
2024-10-14 | *朋 | 10 |
2024-10-22 | *海 | 10 |
2024-10-23 | *南 | 10 |
2024-10-26 | *节 | 6.66 |
2024-10-27 | *o | 5 |
2024-10-28 | W*F | 6.66 |
2024-10-29 | R*n | 6.66 |
2024-11-02 | *球 | 6 |
2024-11-021 | *鑫 | 6.66 |
2024-11-25 | *沙 | 5 |
2024-11-29 | C*n | 2.88 |

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了