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图像构成。

opencv图片权重叠加 opencv多个图像识别与定位_图像处理

opencv图片权重叠加 opencv多个图像识别与定位_多模板匹配_02


运行上述代码,得到如下图所示结果,可以看到输入图像内多个与模板图像匹配的子图被标记出来。

opencv图片权重叠加 opencv多个图像识别与定位_模板匹配_03


大家可能已经注意到了,本来在函数cv2.rectangle()中设置的边界宽度为1,但实际上标记出来的宽度远远大于1。这是因为在当前的区域内,存在多个大于当前指定阈值(0.9)的情况,所以将它们都做了标记。这样,多个宽度为1的矩形就合在了一起,显得边界比较粗。读者可以尝试修改阈值,调整宽度,观察不同的演示效果。