4. OpenCV模板匹配函数:cv2.matchTemplate详解
模板匹配,说白了就是在一张大图里找一张小图。你想想看,在工业视觉里,我们经常要定位一个芯片、一个螺丝孔,或者一个PCB板上的mark点。这时候模板匹配就是最直接的办法。
OpenCV里实现这个功能的核心函数就是cv2.matchTemplate。我个人习惯叫它「滑窗匹配」——因为它的原理真的就是拿模板当窗口,在图像上从左到右、从上到下地滑动,每到一个位置就算一下相似度。
4.1 函数签名与参数
先看函数长什么样:
result = cv2.matchTemplate(image, templ, method[, mask])
参数其实不多,但每个都很关键:
- image:输入图像,也就是你要搜索的大图。必须是8位或32位浮点型。
- templ:模板图像。注意,它的宽高必须小于等于输入图像。
- method:匹配方法。这是今天的重头戏,下面细讲。
- mask:掩码。我坦白说,这个参数我用了好几年才真正用上。它允许你只匹配模板的某些区域,比如忽略背景。
返回值result是一个单通道的浮点型矩阵。它的尺寸是(W - w + 1) x (H - h + 1),其中W、H是输入图像的宽高,w、h是模板的宽高。每个像素值代表那个位置的匹配程度。
小技巧:我在项目中遇到过一个问题——模板比目标物体大了一点点,结果死活匹配不上。后来才意识到,模板尺寸必须严格小于等于搜索区域。嗯,这个坑我替你们踩过了。
4.2 method参数详解
method参数决定了「怎么算相似度」。OpenCV提供了6种方法,我按使用频率把它们分成三组来讲。
4.2.1 平方差匹配(TM_SQDIFF 和 TM_SQDIFF_NORMED)
这个方法的逻辑很直白:计算模板和图像对应区域的像素差值的平方和。值越小,说明越相似。完全匹配时值为0。
R(x,y) = Σ [T(x',y') - I(x+x', y+y')]²
归一化版本:
R(x,y) = Σ [T(x',y') - I(x+x', y+y')]² / √(Σ T² · Σ I²)
我个人的经验:在光照稳定的工业场景下,我更喜欢用非归一化的版本。为什么?因为计算快,而且归一化有时候会把噪声也「归一化」进去。但如果你在户外或者光照变化大的环境,归一化版本更靠谱。
注意:使用TM_SQDIFF时,要找最小值而不是最大值。很多新手在这里翻车——我刚开始也犯过这个错,找了半天最大值,结果匹配到的全是白墙。
4.2.2 相关匹配(TM_CCORR 和 TM_CCORR_NORMED)
相关匹配就是计算模板和图像区域的点积。值越大,匹配度越高。
R(x,y) = Σ [T(x',y') · I(x+x', y+y')]
归一化版本:
R(x,y) = Σ [T(x',y') · I(x+x', y+y')] / √(Σ T² · Σ I²)
说实话,TM_CCORR我很少单独用。因为它对亮度太敏感了——图像亮一点,结果值就大一点,这不一定是真的匹配上了。归一化版本会好一些,但也不是最优选择。
4.2.3 相关系数匹配(TM_CCOEFF 和 TM_CCOEFF_NORMED)
这是我最常用的方法,没有之一。它先把模板和图像区域都减去各自的均值,再做相关计算。说白了,就是去除了亮度的影响,只比较「形状」的相似度。
T'(x,y) = T(x,y) - mean(T)
I'(x,y) = I(x+x', y+y') - mean(I)
R(x,y) = Σ [T'(x',y') · I'(x+x', y+y')]
归一化版本:
R(x,y) = Σ [T' · I'] / √(Σ T'² · Σ I'²)
为什么我偏爱它? 有一次我在做手机屏幕的定位项目,光照条件从早到晚变化很大。用TM_CCORR的结果飘得离谱,换成TM_CCOEFF_NORMED之后,稳如老狗。值域固定在[-1, 1]之间,1表示完全匹配,-1表示完全相反,0表示不相关。这个特性在做阈值判断时特别方便。
| 方法 | 值域 | 最佳匹配 | 抗亮度变化 | 计算速度 |
|---|---|---|---|---|
| TM_SQDIFF | [0, +∞) | 最小值 | 差 | 快 |
| TM_SQDIFF_NORMED | [0, 1] | 最小值 | 中 | 中 |
| TM_CCORR | (-∞, +∞) | 最大值 | 差 | 快 |
| TM_CCORR_NORMED | [0, 1] | 最大值 | 中 | 中 |
| TM_CCOEFF | (-∞, +∞) | 最大值 | 好 | 中 |
| TM_CCOEFF_NORMED | [-1, 1] | 最大值 | 好 | 慢 |
4.3 minMaxLoc函数
匹配完了,怎么找到最佳位置?这时候就需要cv2.minMaxLoc了。
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
这个函数干的事很简单:在result矩阵里找到最小值和最大值,以及它们对应的坐标。
- min_val:最小值
- max_val:最大值
- min_loc:最小值的位置 (x, y)
- max_loc:最大值的位置 (x, y)
怎么用?看匹配方法:
- 如果是TM_SQDIFF或TM_SQDIFF_NORMED,用
min_loc - 其他方法,用
max_loc
拿到min_loc或max_loc之后,这个点就是模板左上角在图像中的位置。要画矩形框的话:
top_left = max_loc # 假设用的是TM_CCOEFF_NORMED
h, w = template.shape[:2]
bottom_right = (top_left[0] + w, top_left[1] + h)
cv2.rectangle(image, top_left, bottom_right, (0, 255, 0), 2)
避坑指南:我曾经在一个项目里发现匹配结果总是偏了几个像素。排查了半天,原来是模板的宽高搞反了——OpenCV里坐标是(x, y),但图像shape是(height, width)。记住:result.shape是(行, 列),而min_loc是(列, 行)。这个细节不注意,定位就会偏。
4.4 完整示例代码
最后,给一个完整的例子。假设我们要在电路板图像里找一个电容的位置:
import cv2
import numpy as np
# 读取图像和模板
image = cv2.imread('circuit_board.jpg', 0)
template = cv2.imread('capacitor_template.jpg', 0)
# 获取模板尺寸
h, w = template.shape
# 执行匹配
result = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED)
# 找最佳匹配位置
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
# TM_CCOEFF_NORMED用最大值
top_left = max_loc
bottom_right = (top_left[0] + w, top_left[1] + h)
# 画框
image_color = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
cv2.rectangle(image_color, top_left, bottom_right, (0, 255, 0), 2)
# 显示结果
cv2.imshow('Result', image_color)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 打印匹配分数
print(f'匹配分数: {max_val:.3f}')
这段代码我几乎每个项目都会用到。你把它封装成一个函数,传入图像和模板,返回位置和分数,以后用起来就方便了。
嗯,关于matchTemplate的核心内容就这些。下一章我们会聊多尺度匹配——当模板和目标的尺寸不一致时该怎么办。那才是真正考验工程能力的地方。