OpenCV基础续:轮廓检测与模板匹配

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

⚠️ 重要注意事项:

  1. 该函数只会识别白色的前景目标,若目标为黑色需要图像像素取反
  2. 查找轮廓操作会改动原图,不想修改原图需要传入图像副本
  3. 单层外圈物体选用 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 ✅ |

三种归一化方法在本例中都成功定位到了目标位置。

小结

模板匹配的标准流程为:读图 → 取模板尺寸 → matchTemplateminMaxLoc 取最值 → 根据 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()从大图中快速查找模板小图的位置

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值