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压缩轮廓点
代码示例
import cv2
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) # 二值化,阈值120
cv2.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表示绘制所有轮廓,绿色线宽2
cv2.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)
# 再次绘制 contours1
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)
# 绘制 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 = []
for i in contours: # 遍历所有轮廓
if cv2.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)和半径r
phone_circle = cv2.circle(phone, (int(x), int(y)), int(r), (255, 0, 0), 2) # 绘制外接圆,蓝色线宽2
cv2.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) # 在图像上绘制矩形,蓝色线宽2
cv2.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) # 绘制近似后的轮廓,蓝色线宽3
cv2.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() 使用。
代码示例
import cv2
# 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() | 从大图中快速查找模板小图的位置 |





















2万+

被折叠的 条评论
为什么被折叠?



