opencv之模板匹配


前言

模板匹配是指在当前图像A内寻找与图像B最相似的部分,一般将图像A称为输入图像,将图像B称为模板图像。模板匹配的操作方法是将模板图像B在图像A上滑动,遍历所有像素以完成匹配。

例如,在图1中,希望在图中的大图像“lena”内寻找左上角的“眼睛”图像。此时,大图像“lena”是输入图像,“眼睛”图像是模板图像。查找的方式是,将模板图像在输入图像内从左上角开始滑动,逐个像素遍历整幅输入图像,以查找与其最匹配的部分。

在这里插入图片描述

图1

模板匹配基础

在OpenCV内,模板匹配是使用函数cv2.matchTemplate()实现的。该函数的语法格式为:

result = cv2.matchTemplate(image, templ, method[, mask ] )

其中:

  • image为原始图像,必须是8位或者32位的浮点型图像。
  • templ为模板图像。它的尺寸必须小于或等于原始图像,并且与原始图像具有同样的类型。
  • method为匹配方法。该参数通过TemplateMatchModes实现,有6种可能的值,如表1所示。
    在这里插入图片描述
表1

其具体对应的计算公式如表2所示。
在这里插入图片描述

表2
  • mask为模板图像掩模。它必须和模板图像templ具有相同的类型和大小。通常情况下该值使用默认值即可。当前,该参数仅支持TM_SQDIFF和TM_CCORR_NORMED两个值。

函数cv2.matchTemplate()的返回值result是由每个位置的比较结果组合所构成的一个结果集,类型是单通道32位浮点型。如果输入图像(原始图像)尺寸是WH,模板的尺寸是wh,则返回值的大小为(W-w+1)*(H-h+1)。

在进行模板匹配时,模板在原始图像内遍历。在水平方向上:

  • 遍历的起始坐标是原始图像左数第1个像素值(序号从1开始)。
  • 最后一次比较是当模板图像位于原始图像的最右侧时,此时其左上角像素点所在的位置是W-w+1。

因此,返回值result在水平方向上的大小是W-w+1(水平方向上的比较次数)。

在垂直方向上:

  • 遍历的起始坐标从原始图像顶端的第1个像素开始。
  • 最后一次比较是当模板图像位于原始图像的最下端时,此时其左上角像素点所在位置是H-h+1。

所以,返回值result在垂直方向上的大小是H-h+1(垂直方向上的比较次数)。

如果原始图像尺寸是WH,模板的尺寸是wh,则返回值的大小为(W-w+1)(H-h+1)。也就是说,模板图像要在输入图像内比较(W-w+1)(H-h+1)次。

例如,在图2中,左上方的2×2小方块是模板图像,右下方的10×10图像是输入图像(原始图像)。在进行模板匹配时:
在这里插入图片描述

图2
  • 首先将模板图像置于输入图像的左上角。
  • 模板图像在向右移动时,最远只能位于输入图像的最右侧边界处,此时模板图像左上角的像素对应着输入图像的第9列(输入图像宽度-模板图像宽度+1=10-2+1=9)。
  • 模板图像在向下移动时,最远只能位于输入图像最下端的边界处。此时模板图像左上角的像素对应着输入图像的第9行(输入图像高度-模板图像高度+1=10-2+1=9)。

根据上述分析可知,比较结果result的大小满足(W-w+1)*(H-h+1),在上例中就是(10-2+1) ×(10-2+1),即9×9。也就是说,模板图像要在输入图像内总计比较9×9=81次,这些比较结果将构成一个9×9大小的二维数组。

这里需要注意的是,函数cv2.matchTemplate()通过参数method来决定使用不同的查找方法。对于不同的查找方法,返回值result具有不同的含义。例如:

  • method的值为cv2.TM_SQDIFF和cv2.TM_SQDIFF_NORMED时,result值为0表示匹配度最好,值越大,表示匹配度越差。
  • method的值为cv2.TM_CCORR、cv2.TM_CCORR_NORMED、cv2.TM_CCOEFF和cv2.TM_CCOEFF_NORMED时,result的值越小表示匹配度越差,值越大表示匹配度越好。

从上述分析可以看出,查找方法不同,结果的判定方式也不同。在查找最佳匹配时,首先要确定使用的是何种method,然后再确定到底是查找最大值,还是查找最小值。

查找最值(极值)与最值所在的位置,可以使用cv2.minMaxLoc()函数实现。该函数语法格式如下:

minVal, maxVal, minLoc, maxLoc   = cv2.minMaxLoc( src [, mask] )

其中:

  • src为单通道数组。
  • minVal为返回的最小值,如果没有最小值,则可以是NULL(空值)。
  • maxVal为返回的最大值,如果没有最小值,则可以是NULL。
  • minLoc为最大值的位置,如果没有最大值,则可以是NULL。
  • maxLoc为最大值的位置,如果没有最大值,则可以是NULL。
  • mask为用来选取掩模的子集,可选项。

函数cv2.minMaxLoc()能够查找整个数组内的最值及它们的位置,并且可以根据当前的掩模集来选取特定子集的极值。

综上所述,函数cv2.matchTemplate()返回值中的最值位置就是模板匹配的位置。当然,选用表1中的不同参数值,匹配位置可能位于最大值所在的位置也可能位于最小值所在的位置。通过函数cv2.minMaxLoc()来查找函数cv2.matchTemplate()返回值中的最值位置,就可以找到最佳模板匹配的位置。

例如,当method的值为cv2.TM_SQDIFF和cv2.TM_SQDIFF_NORMED时,0表示最佳匹配,值越大,则表示匹配效果越差。因此,在使用这两种方法时,要寻找最小值所在的位置作为最佳匹配。如下语句能够找到cv2.matchTemplate()函数返回值中最小值的位置:

minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(matchTemplate函数的返回值)
topLeft = minLoc                          # 查找最小值所在的位置

以topLeft点为模板匹配位置的左上角坐标,结合模板图像的宽度w和高度h可以确定匹配位置的右下角坐标,代码如下所示:

bottomRight = (topLeft[0] + w, topLeft[1] + h)    #w和h是模板图像的宽度和高度

当method的值为cv2.TM_CCORR、cv2.TM_CCORR_NORMED、cv2.TM_CCOEFF和cv2.TM_CCOEFF_NORMED时,cv2.matchTemplate()函数的返回值越小,表示匹配度越差,而返回值越大则表示匹配度越好。此时,要寻找最大值所在的位置作为最佳匹配。如下语句能够找到模板匹配返回值中最大值的位置,并以该点为左上角,结合模板的宽度w和高度h确定匹配位置的右下角坐标。

minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(matchTemplate函数的返回值)
topLeft = maxLoc         # 查找最大值所在的位置
bottomRight = (topLeft[0] + w, topLeft[1] + h)        # w和h是模板的宽度和高度

通过上述方式,我们确定了模板匹配的矩形对角坐标位置,接下来可以借助函数cv2.rectangle()将该位置用白色标记出来。函数cv2.rectangle的语法格式为:

Img = cv.rectangle( img, pt1, pt2, color[, thickness])

式中各个参数的含义为:

  • img表示要标记的目标图像。
  • pt1是矩形的顶点。
  • pt2是pt1的对角顶点。
  • color是要绘制矩形的颜色或灰度级(灰度图像)。
  • thickness是矩形边线的宽度。

因此,使用的标记语句为:

cv2.rectangle(img, topLeft, bottomRight, 255, 2)

该语句表示,在img内标记一个矩形,矩形的两个对角顶点为topLeft和bottomRight,矩形颜色为白色(255),宽度为2。

代码

【例1】使用函数cv2.matchTemplate()进行模板匹配。要求参数method的值设置为cv2.TM_SQDIFF,显示函数的返回结果及匹配结果。

根据题目的要求,编写代码如下:

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('C:\\Users\\Administrator\\Desktop\\lena.png', 0)
template = cv2.imread('C:\\Users\\Administrator\\Desktop\\eye.png', 0)
th, tw = template.shape[::]
rv = cv2.matchTemplate(img, template, cv2.TM_SQDIFF)
minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(rv)
topLeft = minLoc
bottomRight = (topLeft[0] + tw, topLeft[1] + th)
cv2.rectangle(img, topLeft, bottomRight, 255, 2)
plt.subplot(121), plt.imshow(rv, cmap='gray')
plt.title('Matching Result'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(img, cmap='gray')
plt.title('Detected Point'), plt.xticks([]), plt.yticks([])
plt.show()

本例中所使用的输入图像是ena.png,模板图像是从输入图像中截取的其眼部子图,如图3所示。
在这里插入图片描述

图3

运行上述代码,得到如图4所示结果,其中左图是函数cv2.matchTemplate()的返回值,右图是模板匹配的结果。
在这里插入图片描述

图4

这里需要注意,在计算模板图像的宽度时,使用的语句为:

th, tw = template.shape[::]

返回值中的th是模板图像的高度,tw是模板图像的宽度。在OpenCV官网的示例中,使用的语句形式为:

tw, th = template.shape[::-1]

该语句返回的也是模板图像的宽度和高度,只不过语句template.shape[::-1]将宽度和高度的顺序进行了调换。

下面通过一段代码来说明语句template.shape[::-1]的含义。例如:

import numpy as np
list2d =np.arange(18).reshape(3,6)
print(list2d)

####################result##################
D:\Code\py_demo\.venv\Scripts\python.exe D:\Code\py_demo\operator_cv\模板匹配.py 
[[ 0  1  2  3  4  5]
 [ 6  7  8  9 10 11]
 [12 13 14 15 16 17]]

从其定义及返回值可以观察到,list2d是一个高度为3个像素,宽度为6个像素的模板图像。首先,使用list2d.shape[::]返回其shape值,并将该值打印出来:

h, w=list2d.shape[::]
print(h, w)

此时,会得到打印结果:“3 6”。返回值中,第1个值是高度,第2个值是宽度。

接下来,使用list2d.shape[::-1]返回模板图像的shape值,并将该值打印出来:

w, h=list2d.shape[::-1]
print(w, h)

此时,会得到打印结果:“6 3”。返回值中,第1个值是宽度,第2个值是高度。

代码

【例2】使用cv2.matchTemplate()函数进行模板匹配。要求参数method的值设置为cv2.TM_CCOEFF,显示函数的返回结果及匹配结果。

根据题目的要求,编写代码如下:

import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('C:\\Users\\Administrator\\Desktop\\lena.png', 0)
template = cv2.imread('C:\\Users\\Administrator\\Desktop\\eye.png', 0)
tw, th = template.shape[::-1]
rv = cv2.matchTemplate(img, template, cv2.TM_CCOEFF)
minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(rv)
topLeft = maxLoc
bottomRight = (topLeft[0] + tw, topLeft[1] + th)
cv2.rectangle(img, topLeft, bottomRight, 255, 2)
plt.subplot(121), plt.imshow(rv, cmap='gray')
plt.title('Matching Result'), plt.xticks([]), plt.yticks([])
plt.subplot(122), plt.imshow(img, cmap='gray')
plt.title('Detected Point'), plt.xticks([]), plt.yticks([])
plt.show()

本例使用的输入图像及模板图像与例1的相同,如图3所示。

运行上述代码,得到如图5所示的结果,其中左图是函数cv2.matchTemplate()的返回值,右图是模板匹配的结果。
在这里插入图片描述

图5

在本节的两个例子中,使用cv2.matchTemplate()函数进行模板匹配时,查找最值(极值)的方式是不一样的:

  • 在例15.1中,参数method的值为cv2.TM_SQDIFF,查找的是最小值所在的位置。
  • 在例15.2中,参数method的值为cv2.TM_CCOEFF,查找的是最大值所在的位置。

多模板匹配

在前面的例子中,我们在输入图像lena中搜索其眼部子图,该子图在整个输入图像内仅出现了一次。但是,有些情况下,要搜索的模板图像很可能在输入图像内出现了多次,这时就需要找出多个匹配结果。而函数cv2.minMaxLoc()仅仅能够找出最值,无法给出所有匹配区域的位置信息。所以,要想匹配多个结果,使用函数cv2.minMaxLoc()是无法实现的,需要利用阈值进行处理。

下面分步骤介绍如何获取多模板匹配的结果。

1.获取匹配位置的集合

函数where()能够获取模板匹配位置的集合。对于不同的输入,其返回的值是不同的。

  • 当输入(参数)是一维数组时,返回值是一维索引,只有一组索引数组。
  • 当输入是二维数组时,返回的是匹配值的位置索引,因此会有两组索引数组表示返回值的位置。

以下代码查找在一维数组a中,数值大于5的元素的索引(即该元素所在的位置,数组的索引从0开始):

import numpy as np
a=np.array([3,6,8,1,2,88])
b=np.where(a>5)
print(b)

该段代码返回的结果为:

(array([1, 2, 5], dtype=int64), )

说明索引值为1、2、5的数组元素,它们的值是大于5的。

上面介绍的是输入值为一维数组时的情况。当输入值是二维数组时,函数where()会返回满足条件的值在二维数组中的索引。例如,以下代码查找在二维数组am中,值大于5的元素的索引:

import numpy as np
am=np.array([[3,6,8,77,66], [1,2,88,3,98], [11,2,67,5,2]])
b=np.where(am>5)
print(b)

该段代码返回的结果为:

(array([0, 0, 0, 0, 1, 1, 2, 2], dtype=int64),
 array([1, 2, 3, 4, 2, 4, 0, 2], dtype=int64))

上述结果说明,存在二维数组am,它的值为:

[[ 3  6  8 77 66]
 [ 1  2 88  3 98]
 [11  2 67  5  2]]

其中,位置[0, 1]、[0, 2]、[0, 3]、[0, 4]、[1, 2]、[1, 4]、[2, 0]、[2, 2]上的元素值大于5。

综上所述,函数np.where()可以找出在函数cv2.matchTemplate()的返回值中,哪些位置上的值是大于阈值threshold的。在具体实现时,可以采用的语句为:

loc = np.where( res >= threshold)

式中:

  • res是函数cv2.matchTemplate()进行模板匹配后的返回值。
  • threshold是预设的阈值
  • loc是满足“res >= threshold”的像素点的索引集合。例如,在上面的二维数组am中,返回的大于5的元素索引集合为(array([0,0, 0, 0, 1, 1, 2, 2], dtype=int64), array([1, 2, 3, 4, 2, 4, 0, 2],dtype=int64))。返回值loc中的两个元素,分别表示匹配值的行索引和列索引。

2.循环

要处理多个值,通常需要用到循环。例如,有一个列表,其中的值为71、23、16,希望将这些值逐个输出,可以这样写代码:

value = [71,23,16]
for i in value:
    print('value内的值:', i)

运行上述代码,得到的输出结果为:

value内的值: 71
value内的值: 23
value内的值: 16

3.在循环中使用函数zip()

函数zip()用可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。

例如,以下代码使用函数zip()将t内对应的元素打包成一个个元组,并打印了由这些元组组成的列表:

x = [1,2,3]
y = [4,5,6]
z = [7,8,9]
t = (x, y, z)
print(t)
for i in zip(*t):
    print(i)

上述代码中,语句print(t)将t内的元素输出,结果为:

([1, 2, 3], [4, 5, 6], [7, 8, 9])

循环语句for i in zip(*t)将t内的元素打包成元组后输出,结果为:

(1, 4, 7)
(2, 5, 8)
(3, 6, 9)

例如,对于前面提到的数组am,使用函数zip()循环,就可以得到其中大于5的元素索引的集合:

import numpy as np
am=np.array([[3,6,8,77,66], [1,2,88,3,98], [11,2,67,5,2]])
print(am)
b=np.where(am>5)
for i in zip(*b):
    print(i)

上述代码的输出结果为:

[[ 3  6  8 77 66]
 [ 1  2 88  3 98]
 [11  2 67  5  2]]
(0, 1)
(0, 2)
(0, 3)
(0, 4)
(1, 2)
(1, 4)
(2, 0)
(2, 2)

4.调整坐标

函数numpy.where()可以获取满足条件的模板匹配位置集合,然后可以使用函数cv2.rectangle()在上述匹配位置绘制矩形来标注匹配位置。

使用函数numpy.where()在函数cv2.matchTemplate()的输出值中查找指定值,得到的形式为“(行号,列号)”的位置索引。但是,函数cv2.rectangle()中用于指定顶点的参数所使用的是形式为“(列号,行号)”的位置索引。所以,在使用函数cv2.rectangle()绘制矩形前,要先将函数numpy.where()得到的位置索引做“行列互换”。可以使用如下语句实现loc内行列位置的互换:

loc[::-1]

如下语句将loc内的两个元素交换位置:

import numpy as np
loc = ([1,2,3,4], [11,12,13,14])
print(loc)
print(loc[::-1])

其中,语句print(loc)所对应的输出为:

([1, 2, 3, 4], [11, 12, 13, 14])

语句print(loc[::-1])所对应的输出为:

([11, 12, 13, 14], [1, 2, 3, 4])

5.标记匹配图像的位置

函数cv2.rectangle()可以标记匹配图像的具体位置,分别指定要标记的原始图像、对角顶点、颜色、矩形边线宽度即可。关于矩形的对角顶点:

  • 其中的一个对角顶点A可以通过for循环语句从确定的满足条件的“匹配位置集合”内获取。
  • 另外一个对角顶点,可以通过顶点A的位置与模板的宽(w)和高(h)进行运算得到。

因此,标记各个匹配位置的语句为:

for i in 匹配位置集合:
	cv2.rectangle(输入图像,i, (i[0] + w, i[1] + h), 255, 2)

代码

【例3】使用模板匹配方式,标记在输入图像内与模板图像匹配的多个子图像。

import cv2 as cv2
import numpy as np


# 多个模板匹配
def more_match(image, templ):
    img = cv2.imread(image)
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    template = cv2.imread(templ, 0)
    h, w = template.shape[:2]

    res = cv2.matchTemplate(img_gray, template, cv2.TM_CCOEFF_NORMED)
    # 取匹配程度大于%90的坐标
    threshold = 0.9
    # np.where返回的坐标值(x,y)是(h,w),注意h,w的顺序
    loc = np.where(res >= threshold)
    for pt in zip(*loc[::-1]):
        bottom_right = (pt[0] + w, pt[1] + h)
        cv2.rectangle(img, pt, bottom_right, (255, 0, 0), 1)
        print(pt, bottom_right)
    cv2.imshow('img_rgb', img)
    cv2.waitKey(0)
    pass


if __name__ == '__main__':
    print("———————————————————— start ————————————————————\n")
    # 图片路径自己设置,下面是我本地的路径,记得替换!!!
    more_match('C:\\Users\\Administrator\\Desktop\\8.png', 'C:\\Users\\Administrator\\Desktop\\tt.png')
    print("———————————————————— end ————————————————————\n")

为1,但实际上标记出来的宽度远远大于1。这是因为在当前的区域内,存在多个大于当前指定阈值(0.9)的情况,所以将它们都做了标记。这样,多个宽度为1的矩形就合在了一起,显得边界比较粗。读者可以尝试修改阈值,调整宽度,观察不同的演示效果。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hola173841439

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值