Python版本是Python3.7.3,OpenCV版本OpenCV3.4.1,开发环境为PyCharm
15.2 多模板匹配
在前面的例子中,我们在输入图像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
因此,在获取匹配值的索引集合后,可以采用如下语句遍历所有匹配的位置,对这些位置做标记:
for i in 匹配位置集合:
标记匹配位置。
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)
因此,如果希望循环遍历由np.where()返回的模板匹配索引集合,可以采用的语句为:
for i in zip(*模板匹配索引集合):
标记处理
例如,对于前面提到的数组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)
eg1:使用模板匹配方式,标记在输入图像内与模板图像匹配的多个子图像。
代码如下:
import cv2
import numpy as np
from matplotlib import pyplot as plt
img = cv2.imread('lena4.bmp',0)
template = cv2.imread('lena4Temp.bmp',0)
w, h = template.shape[::-1]
res = cv2.matchTemplate(img,template,cv2.TM_CCOEFF_NORMED)
threshold = 0.9
loc = np.where( res >= threshold)
for pt in zip(*loc[::-1]):
cv2.rectangle(img, pt, (pt[0] + w, pt[1] + h), 255, 1)
plt.imshow(img,cmap = 'gray')
plt.xticks([]), plt.yticks([])
plt.show()
本例所使用的输入图像及模板图像如下图所示,输入图像由4幅lena图像构成。
运行上述代码,得到如下图所示结果,可以看到输入图像内多个与模板图像匹配的子图被标记出来。
大家可能已经注意到了,本来在函数cv2.rectangle()中设置的边界宽度为1,但实际上标记出来的宽度远远大于1。这是因为在当前的区域内,存在多个大于当前指定阈值(0.9)的情况,所以将它们都做了标记。这样,多个宽度为1的矩形就合在了一起,显得边界比较粗。读者可以尝试修改阈值,调整宽度,观察不同的演示效果。