OpenCV基础续:轮廓检测与模板匹配
摘要:本文系统梳理了 OpenCV 中轮廓检测与分析的核心知识,涵盖轮廓检测、轮廓绘制、轮廓特征(面积/周长/外接圆/外接矩形)、轮廓近似以及模板匹配,并提供了完整的 Python 代码示例。
📌 前言
在上一篇中我们学习了图像形态学和边缘检测。当我们通过边缘检测或二值化找到了物体的边界后,下一步通常就是提取这些边界的轮廓——它们是目标识别、形状分析、物体计数等任务的重要基础。本文将结合实战代码,带你系统掌握 OpenCV 的轮廓检测、绘制、特征分析、近似以及模板匹配技术。
一、 轮廓检测
轮廓是指图像中具有相同颜色或强度的连续边界点集合。在 OpenCV 中使用cv2.findContours()函数进行轮廓检测。
函数签名与参数
contours, hierarchy = cv2.findContours(img, mode, method)注意:OpenCV 2~3 版本会返回
image、contours、hierarchy三个返回值,OpenCV 4 之后仅返回轮廓与层级。跨版本兼容写法:cv2.findContours(...)[-2]
输入参数:
| 参数 | 含义 |
|---|---|
img | 输入图像,轮廓检测前必须处理成仅有 0 和 255 的二值图像,白色像素视作检测目标 |
mode | 轮廓检索模式,共 4 类(见下表) |
method | 轮廓近似储存方式,共 2 种常用(见下表) |
mode(轮廓检索模式):
| 模式 | 原理描述 |
|---|---|
cv2.RETR_EXTERNAL | 只检测外层轮廓,忽略所有内部嵌套轮廓 |
cv2.RETR_LIST | 检出全部轮廓,不建立轮廓层级,所有轮廓处于同一层级 |
cv2.RETR_CCOMP | 获取全部轮廓,仅有两层层级。物体外轮廓属于第一层,物体内部孔洞轮廓为第二层,孔洞内物体轮廓再次归为第一层 |
cv2.RETR_TREE | 获取所有轮廓,搭建完整嵌套式树形层级结构 |
method(轮廓近似储存方式):
| 方式 | 原理描述 |
|---|---|
cv2.CHAIN_APPROX_NONE | 保存轮廓线上全部像素坐标点 |
cv2.CHAIN_APPROX_SIMPLE | 压缩多余点位,直线只保留端点,例如矩形轮廓仅储存四个角点,节省存储空间 |
返回参数:
| 返回值 | 含义 |
|---|---|
contours | 存储所有轮廓的列表,每一条轮廓是 numpy 数组,存放轮廓各个 (x,y) 坐标 |
hierarchy | 轮廓层级信息,每条轮廓对应数组[Next, Previous, First child, Parent]。Next:同一层级中下一条轮廓索引,无则为 -1;Previous:同一层级中上一条轮廓索引,无则为 -1;First child:当前轮廓第一个内层子轮廓索引,无子轮廓为 -1;Parent:当前轮廓对应的外层父轮廓索引,没有父轮廓则为 -1 |
⚠️ 重要注意事项:
- 该函数只会识别白色的前景目标,若目标为黑色需要图像像素取反
- 查找轮廓操作会改动原图,不想修改原图需要传入图像副本
- 单层外圈物体选用
RETR_EXTERNAL;多层嵌套轮廓使用RETR_TREE;日常优先使用CHAIN_APPROX_SIMPLE压缩轮廓点
代码示例
importcv2 phone=cv2.imread('Phone_pen.png')# 读取原图phone_gray=cv2.cvtColor(phone,cv2.COLOR_BGR2GRAY)# 灰度图的处理cv2.imshow('phone_gray',phone_gray)cv2.waitKey(0)ret,phone_binary=cv2.threshold(phone_gray,120,255,cv2.THRESH_BINARY)# 二值化,阈值120cv2.imshow('phone_binary',phone_binary)cv2.waitKey(0)# cv2.RETR_TREE:获取所有轮廓,搭建完整嵌套式树形层级结构。contours=cv2.findContours(phone_binary,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)[-2]# cv2.RETR_EXTERNAL:只检测外层轮廓,忽略所有内部嵌套轮廓;contours1=cv2.findContours(phone_binary,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]# cv2.CHAIN_APPROX_NONE:保存轮廓线上全部像素坐标点;contours2=cv2.findContours(phone_binary,cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE)[-2]print(len(contours))输出展示:
| 原图 | 灰度图(phone_gray) | 二值图(phone_binary) |
|---|---|---|
三种不同参数组合检测到的轮廓数量不同,绘制效果对比如下:
| RETR_TREE(完整层级,8个轮廓) | RETR_EXTERNAL(仅外层,4个轮廓) |
|---|---|
控制台输出:
8可以看到RETR_EXTERNAL只检出了 3 个最外层轮廓(手机外框、钢笔主体、笔帽),而RETR_TREE检出了 8 个轮廓,包含了内部的所有细节。
小结
轮廓检测的前置条件是先获得二值图(灰度化 → 二值化)。mode 参数决定了轮廓之间的层级关系结构,method 参数决定了轮廓点的存储方式。日常最常用的组合是RETR_TREE+CHAIN_APPROX_SIMPLE,既获取完整层级又节省内存。
二、 轮廓的绘制
检测到轮廓后,通常需要把它们画在图像上以便可视化观察。OpenCV 提供cv2.drawContours()函数完成这项工作。
函数签名与参数
cv2.drawContours(image, contours, contourIdx, color, thickness=None, lineType=None, hierarchy=None, maxLevel=None, offset=None)| 参数 | 含义 |
|---|---|
image | 要在其上绘制轮廓的输入图像(会被直接修改,建议传入副本) |
contours | 轮廓列表,通常由cv2.findContours()函数返回 |
contourIdx | 要绘制的轮廓的索引。如果为负数(如 -1),则绘制所有轮廓 |
color | 轮廓的颜色,以 BGR 格式表示。例如 (0, 255, 0) 表示绿色 |
thickness | 轮廓线的粗细。默认值为 1 |
lineType | 轮廓线的类型。默认值为cv2.LINE_8 |
hierarchy | 轮廓层次结构。通常由cv2.findContours()函数返回 |
maxLevel | 绘制的最大轮廓层级。默认值为 None,表示绘制所有层级 |
offset | 轮廓点的偏移量。默认值为 None |
代码示例
# 绘制 contours(RETR_TREE,全部8个轮廓)image_copy=phone.copy()# 创建副本,避免修改原图cv2.drawContours(image=image_copy,contours=contours,contourIdx=-1,color=(0,255,0),thickness=2)# -1表示绘制所有轮廓,绿色线宽2cv2.imshow('Contours_show',image_copy)cv2.waitKey(0)# 绘制 contours1(RETR_EXTERNAL,仅3个外层轮廓)image_copy1=phone.copy()cv2.drawContours(image=image_copy1,contours=contours1,contourIdx=-1,color=(0,255,0),thickness=2)cv2.imshow('Contours_show1',image_copy1)cv2.waitKey(0)# 再次绘制 contours1image_copy1=phone.copy()cv2.drawContours(image=image_copy1,contours=contours1,contourIdx=-1,color=(0,255,0),thickness=2)cv2.imshow('Contours_show1',image_copy1)cv2.waitKey(0)# 绘制 contours2(CHAIN_APPROX_NONE,全像素点保存)image_copy2=phone.copy()cv2.drawContours(image=image_copy2,contours=contours2,contourIdx=-1,color=(0,255,0),thickness=2)cv2.imshow('Contours_show1',image_copy2)cv2.waitKey(0)cv2.destroyAllWindows()# 销毁所有窗口输出展示:
| Contours_show(RETR_TREE 全轮廓) | Contours_show1(RETR_EXTERNAL 外轮廓) |
|---|---|
第三个窗口与第二个窗口效果相同(重复绘制了 contours1)。第四个窗口绘制的是CHAIN_APPROX_NONE的结果,视觉上与第一组差异不大,但内部存储的轮廓点数量要多得多(3238 个点 vs 538 个点):
| CHAIN_APPROX_SIMPLE(538个点) | CHAIN_APPROX_NONE(3238个点) |
|---|---|
小结
绘制轮廓前一定要用phone.copy()创建副本,避免污染原图。contourIdx=-1是最常用的参数,表示一次性画出所有轮廓。若只想画某一个特定轮廓,传入它在 contours 列表中的索引即可。
三、 轮廓特征
轮廓本身携带了大量几何信息,常用的轮廓特征包括面积、周长、外接圆和外接矩形。利用这些特征可以完成按面积筛选、定位最大目标、规则框出物体等实用操作。
函数签名与参数
1. 轮廓面积:
retval = cv2.contourArea(contour[, oriented])| 参数 | 含义 |
|---|---|
contour | 顶点构成的二维向量组(如轮廓列表 contours 中的一个轮廓) |
oriented | 定向区域标志,默认值为 False,返回面积的绝对值;True 时则根据轮廓方向返回带符号的数值 |
2. 轮廓周长:
retval = cv2.arcLength(curve, closed)| 参数 | 含义 |
|---|---|
curve | 输入的二维点集(轮廓顶点),可以是 vector 或 Mat 类型 |
closed | 用于指示曲线是否封闭 |
3. 外接圆:cv2.minEnclosingCircle(cnt)→ 返回((x, y), r),即圆心坐标和半径
4. 最小外接矩形:cv2.boundingRect(cnt)→ 返回(x, y, w, h),即左上角坐标和宽高
代码示例
phone=cv2.imread('Phone_pen.png')phone_gray=cv2.cvtColor(phone,cv2.COLOR_BGR2GRAY)ret,phone_binary=cv2.threshold(phone_gray,120,255,cv2.THRESH_BINARY)contours=cv2.findContours(phone_binary,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)[-2]# ---------- 1. 计算面积和周长 ----------area_0=cv2.contourArea(contours[0])# 第0个轮廓的面积print(area_0)area_1=cv2.contourArea(contours[1])# 第1个轮廓的面积print(area_1)# arcLength(InputArray curve, bool closed) 轮廓周长# curve,输入的二维点集(轮廓顶点),可以是 vector 或 Mat 类型。# closed,用于指示曲线是否封闭。length=cv2.arcLength(contours[0],True)# True表示轮廓是封闭的print(length)# ---------- 2. 根据面积筛选轮廓 ----------a_list=[]foriincontours:# 遍历所有轮廓ifcv2.contourArea(i)>10000:# 只保留面积大于10000的a_list.append(i)image_copy=phone.copy()image_copy=cv2.drawContours(image=image_copy,contours=a_list,contourIdx=-1,color=(0,255,0),thickness=3)cv2.imshow('Contours_show_10000',image_copy)cv2.waitKey(0)# ---------- 3. 轮廓定位好方法:根据轮廓面积进行排序 ----------sortcnt=sorted(contours,key=cv2.contourArea,reverse=True)[0]# 选取最大面积的轮廓image_contours=cv2.drawContours(image_copy,[sortcnt],contourIdx=-1,color=(0,0,255),thickness=3)# 红色粗线标注最大轮廓cv2.imshow('image_contours',image_contours)cv2.waitKey(0)# ---------- 4. 外接圆、外接矩形 ----------cnt=contours[6](x,y),r=cv2.minEnclosingCircle(cnt)# 计算轮廓的外接圆,返回圆心(x,y)和半径rphone_circle=cv2.circle(phone,(int(x),int(y)),int(r),(255,0,0),2)# 绘制外接圆,蓝色线宽2cv2.imshow('phone_circle',phone_circle)cv2.waitKey(0)x,y,w,h=cv2.boundingRect(cnt)# 计算轮廓的最小外接矩形,返回左上角(x,y)和宽高(w,h)phone_rectangle=cv2.rectangle(phone,(x,y),(x+w,y+h),(255,0,0),2)# 在图像上绘制矩形,蓝色线宽2cv2.imshow('phone_rectangle',phone_rectangle)cv2.waitKey(0)cv2.destroyAllWindows()输出展示:
(1)面积与周长(控制台输出):
852.0 638.0 236.5实际数值取决于二值化后的轮廓顺序,读者运行时以实际输出为准。
(2)面积大于 10000 的轮廓(Contours_show_10000):
绿色粗线标出了面积超过 10000 的 4 个轮廓,小面积的噪点轮廓被自动过滤。
(3)最大面积轮廓(image_contours):
红色粗线即为按面积排序后取出的最大轮廓,叠加在上一步绿色轮廓之上。
(4)外接圆与外接矩形:
| phone_circle(仅外接圆) | phone_rectangle(外接圆 + 外接矩形) |
|---|---|
代码中两次绘制操作都是在同一个phone原图上进行:先画外接圆得到phone_circle(左图,仅蓝色圆),再在同一张图上画外接矩形得到phone_rectangle(右图,蓝色圆和蓝色矩形同时存在)。
小结
contourArea()和arcLength()是最基础的两个数值特征,常用于筛选和阈值判断。按面积排序(sorted(key=cv2.contourArea, reverse=True))是定位目标最实用的技巧,一行代码即可找出整张图中最大的物体。minEnclosingCircle()和boundingRect()分别提供了圆形和轴对齐矩形两种"框出目标"的方式。
四、 轮廓近似
实际场景中检测到的轮廓可能包含成百上千个点,但如果目标本质上是一个规则多边形(矩形、三角形、六边形),我们完全可以用更少的点来近似它。OpenCV 使用 Douglas-Peucker 算法实现轮廓近似。
函数签名与参数
approx = cv2.approxPolyDP(curve, epsilon, closed)| 参数 | 含义 |
|---|---|
curve | 输入轮廓 |
epsilon | 近似精度,即两个轮廓之间最大的欧式距离。该参数越小,得到的近似结果越接近实际轮廓;反之,得到的近似结果会更加粗略 |
closed | 布尔类型参数,表示是否封闭轮廓。True 表示输入轮廓是封闭的,近似结果也会是封闭的;否则近似结果不封闭 |
| 返回值 approx | 近似结果,是一个 ndarray 数组,包含了被近似出来的轮廓上的点的坐标 |
💡 epsilon 经验设置方法:通常取轮廓周长的 1%~10%,即
epsilon = 0.01 * cv2.arcLength(cnt, True),这样可以自适应不同大小的物体。
代码示例
phone=cv2.imread('Phone_pen.png')phone_gray=cv2.cvtColor(phone,cv2.COLOR_BGR2GRAY)ret,phone_thresh=cv2.threshold(phone_gray,120,255,cv2.THRESH_BINARY)# 二值化# 获取轮廓(注意此处使用 CHAIN_APPROX_NONE 保留全部点)contours=cv2.findContours(phone_thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_NONE)[-2]epsilon=0.01*cv2.arcLength(contours[0],True)# 设置近似精度 = 周长的1% 【epsilon越小,点越多,越精确】apporx=cv2.approxPolyDP(contours[0],epsilon,True)# 对轮廓进行近似,True表示封闭轮廓print(contours[0].shape)# 原轮廓的形状(点数量)print(apporx.shape)# 近似后的形状(点数量)phone_new=phone.copy()image_contours=cv2.drawContours(phone_new,[apporx],-1,(255,0,0),3)# 绘制近似后的轮廓,蓝色线宽3cv2.imshow('phone',phone)# 先展示原图cv2.waitKey(0)cv2.imshow('image_contours',image_contours)# 再展示近似轮廓cv2.waitKey(0)cv2.destroyAllWindows()输出展示:
(1)控制台输出(形状对比):
(759, 1, 2) (8, 1, 2)实际运行时,contours[0](iPhone 灰色手机外轮廓)原本有 759 个点,经过 1% 周长精度的近似后,仅保留了8 个点(对应手机的四个圆角各两个点),压缩效果非常明显。
(2)视觉对比:
| 原图(phone 窗口) | 近似轮廓(image_contours 窗口,蓝色粗线) |
|---|---|
代码执行时先弹出phone窗口展示原图,按键后再弹出image_contours窗口,蓝色粗轮廓即为 approxPolyDP 近似后的结果(原轮廓被绘制了 8 个顶点的闭合多边形,完美贴合手机的圆角矩形轮廓)。
(3)不同 epsilon 精度对比:
为了更直观地看出 epsilon 对结果的影响,下方列出 1%、10% 两种精度的对比图(绿色为原轮廓,红色为近似结果,蓝色圆点为近似后保留的顶点):
| epsilon = 1% × 周长 | epsilon = 10% × 周长 |
|---|---|
| 759 点 →8点(贴合圆角) | 759 点 →4点(标准矩形) |
随着 epsilon 增大,保留的顶点越来越少,轮廓从贴近真实圆角形状逐渐变成了规整的矩形。
小结
轮廓近似的核心参数是 epsilon,它是一个距离阈值:原轮廓上的点到近似多边形的最大距离不超过 epsilon。经验公式epsilon = k * cv2.arcLength(cnt, True)(k 取 0.01~0.1)可以自适应不同大小的轮廓。近似后得到的点数可以直接用来做形状分类:例如 3 个点 → 三角形、4 个点 → 四边形。
五、 模板匹配
模板匹配是一种在大图(待搜索图)中寻找与小图(模板)最相似区域的方法。它无需训练、使用简单,在目标姿态固定的场景中非常实用。
函数签名与参数
result = cv2.matchTemplate(image, templ, method, result=None, mask=None)| 参数 | 含义 |
|---|---|
image | 待搜索图像(大图) |
templ | 模板图像(小图) |
method | 计算匹配程度的方法(见下表) |
6 种匹配方法:
| 方法 | 原理描述 | 最佳匹配值 |
|---|---|---|
TM_SQDIFF | 平方差匹配法:采用平方差进行匹配 | 越小越好 |
TM_CCORR | 相关匹配法:采用乘法运算 | 越大越好 |
TM_CCOEFF | 相关系数匹配法 | 越大越好 |
TM_SQDIFF_NORMED | 归一化平方差匹配法 | 越小越好 |
TM_CCORR_NORMED | 归一化相关匹配法 | 越大越好 |
TM_CCOEFF_NORMED | 归一化相关系数匹配法(推荐) | 越大越好 |
返回值result是一个匹配结果矩阵,其尺寸为(源图高 - 模板高 + 1, 源图宽 - 模板宽 + 1),矩阵中每个元素表示模板滑动到该位置时与源图的匹配程度。找出最佳匹配位置需要配合cv2.minMaxLoc()使用。
代码示例
importcv2# 1. 读取图像:kele 为待搜索的源图像(大图),template 为模板图像(小图)kele=cv2.imread('kele.png')template=cv2.imread('template.png')# 2. 先展示两张原图,方便观察对比cv2.imshow('kele',kele)cv2.imshow('template',template)cv2.waitKey(0)# 按任意键继续# 3. 获取模板的高 h 和宽 w# shape 返回 (高, 宽, 通道数),取前两位即为 h, w# 后面画矩形时需要用它计算右下角坐标h,w=template.shape[:2]# 4. 模板匹配核心函数 cv2.matchTemplate# 参数1: 源图像(大图)# 参数2: 模板图像(小图)# 参数3: 匹配方法。TM_CCOEFF_NORMED 为"归一化相关系数匹配"# 结果范围 [-1, 1],越接近 1 表示匹配度越高# 返回值 res 是一个矩阵,其尺寸为 (源图高-h+1, 源图宽-w+1)# 矩阵中每个元素表示模板滑动到该位置时与源图的匹配程度res=cv2.matchTemplate(kele,template,cv2.TM_CCOEFF_NORMED)# 5. 从匹配结果矩阵中找出最值# cv2.minMaxLoc 返回四个值:# min_val : 矩阵中的最小值# max_val : 矩阵中的最大值(即最佳匹配度)# min_loc : 最小值所在的坐标 (x, y)# max_loc : 最大值所在的坐标 (x, y) —— 即模板左上角在源图中的位置# 由于使用的是 TM_CCOEFF_NORMED(越大越匹配),所以取 max_loc 作为匹配位置min_val,max_val,min_loc,max_loc=cv2.minMaxLoc(res)# 6. 由左上角坐标和模板宽高,计算出匹配区域的右下角坐标top_left=max_loc# 匹配区域左上角 (x, y)bottom_right=(top_left[0]+w,top_left[1]+h)# 匹配区域右下角 (x+w, y+h)# 7. 在源图上绘制矩形框,标出匹配到的区域# cv2.rectangle(img, pt1, pt2, color, thickness)# pt1=左上角, pt2=右下角, color=(255,0,0) 为蓝色(BGR), 线宽 2# 注意: 该函数会在原图上直接绘制(in-place),建议用副本避免原图被修改kele_template=cv2.rectangle(kele,top_left,bottom_right,(255,0,0),2)# 8. 显示标注后的结果图cv2.imshow('kele_template',kele_template)cv2.waitKey(0)# 按任意键关闭cv2.destroyAllWindows()# 销毁所有窗口输出展示:
(1)两张原图:
| 待搜索源图 kele(大图) | 模板 template(小图) |
|---|---|
(2)匹配结果(kele_template):
蓝色矩形框准确标出了模板在源图中的匹配位置。TM_CCOEFF_NORMED匹配值为0.9990,非常接近 1,说明匹配度极高。
(3)三种归一化方法对比:
| TM_CCOEFF_NORMED(越大越好) | TM_CCORR_NORMED(越大越好) | TM_SQDIFF_NORMED(越小越好) |
|---|---|---|
| 匹配值:0.9990✅ | 匹配值:0.9998✅ | 匹配值:0.0004✅ |
三种归一化方法在本例中都成功定位到了目标位置。
小结
模板匹配的标准流程为:读图 → 取模板尺寸 →matchTemplate→minMaxLoc取最值 → 根据 method 选最佳位置 →rectangle画框。带_NORMED后缀的归一化方法对光照变化更鲁棒,优先使用TM_CCOEFF_NORMED。对于 SQDIFF 系列,匹配越好值越小,要用min_loc;其他方法匹配越好值越大,要用max_loc。模板匹配的局限性是对旋转、缩放、形变敏感。
全文总结
本文系统学习了 OpenCV 中轮廓检测与分析、模板匹配两大核心模块,核心知识点梳理如下:
| 模块 | 关键函数 | 核心用途 |
|---|---|---|
| 轮廓检测 | cv2.findContours() | 从二值图中提取所有边界轮廓 |
| 轮廓绘制 | cv2.drawContours() | 在图像上把检测到的轮廓可视化 |
| 轮廓特征 | contourArea()arcLength()minEnclosingCircle()boundingRect() | 获取面积、周长、外接圆、外接矩形等几何属性 |
| 轮廓近似 | cv2.approxPolyDP() | 用更少的点近似轮廓,用于形状识别 |
| 模板匹配 | cv2.matchTemplate()+cv2.minMaxLoc() | 从大图中快速查找模板小图的位置 |