引言:

在OpenCV中,模板匹配是一种图像处理技术,用于在一个大的图像中查找和定位一个小的目标图像(也称为模板)。

通俗而言,就是通过一张图片找到和另一张图片相似的部分。

从此章开始,opencv系列所有的之后更新的博客都会更注重实际应用,而不是仅仅简单讲解一个小方法是怎么应用的,会涉及到一些其他的方法,对于有些可能出现的代码看不懂的问题,我会放在文章的最后一节。

代码实战:

在模板匹配中,我们首先选定一个小的图像作为目标图像,然后在一个大的输入图像中滑动这个小的目标图像,从而寻找与其最相似的部分。具体实现时,可以使用多种算法来计算相似度,例如平方差和、相关系数、均方误差等等。

核心方法cv2.matchTemplate()是OpenCV中用于实现模板匹配的API函数。该函数可以在一张大图中查找一个小图像,并输出目标区域的位置。以下是cv2.matchTemplate()函数的详细讲解:

函数原型:

retval = cv2.matchTemplate(image, templ, method)

参数说明:

  • image: 大图像,类型为ndarray。
  • templ: 小图像(模板),类型为ndarray。
  • method: 匹配算法,可选值包括
    • cv2.TM_CCOEFF、
    • cv2.TM_CCOEFF_NORMED、
    • cv2.TM_CCORR、
    • cv2.TM_CCORR_NORMED、
    • cv2.TM_SQDIFF、
    • cv2.TM_SQDIFF_NORMED。

其中,cv2.TM_CCOEFF和cv2.TM_CCOEFF_NORMED表示相关系数匹配;cv2.TM_CCORR和cv2.TM_CCORR_NORMED表示相关性匹配;cv2.TM_SQDIFF和cv2.TM_SQDIFF_NORMED表示平方差匹配。有兴趣可以了解一下原理。

返回值说明:

retval是一个矩阵,大小为(N-M+1, M-N+1),其中N和M分别为大图像和小图像的高和宽。矩阵中的每个元素表示原始图像中与模板匹配度的一个指标,匹配越好,则值越大。

代码示例:

此次图像匹配我们用的素材如下:

大图:

小图:

 图像模板匹配就是在大图上找出小图的内容来。

完整代码:

import cv2

def cv_show(title,img):
    cv2.imshow(title,img)
    cv2.waitKey(0) 
    cv2.destroyAllWindows()
    return

#读取两个图像
template = cv2.imread('the_bird.png',0)#读取灰度图
cv_show('img',template)#展示图象
img = cv2.imread('bird.jpg',0)
cv_show('img',img)


# 获取小图像的高和宽
h, w = template.shape[:2]

#不同的方法模板匹配的方式不同
methodology =[cv2.TM_CCOEFF,cv2.TM_CCOEFF_NORMED,cv2.TM_CCORR,cv2.TM_CCORR_NORMED,cv2.TM_SQDIFF,cv2.TM_SQDIFF_NORMED]
# cv2.TM_CCOEFF:相关系数匹配。该方法计算输入图像和模板图像之间的相关系数,值越大表示匹配程度越好。
# cv2.TM_CCOEFF_NORMED:标准归一化相关系数匹配。该方法计算输入图像和模板图像之间的标准归一化相关系数,也就是相关系数除以两个图像各自的标准差的乘积。值越大表示匹配程度越好。
# cv2.TM_CCORR:相关性匹配。该方法计算输入图像和模板图像之间的相关性,值越大表示匹配程度越好。
# cv2.TM_CCORR_NORMED:标准归一化相关性匹配。该方法计算输入图像和模板图像之间的标准归一化相关性,也就是相关性除以两个图像各自的标准差的乘积。值越大表示匹配程度越好。
# cv2.TM_SQDIFF:平方差匹配。该方法计算输入图像和模板图像之间的平方差,值越小表示匹配程度越好。
# cv2.TM_SQDIFF_NORMED:标准归一化平方差匹配。该方法计算输入图像和模板图像之间的标准归一化平方差,也就是平方差除以两个图像各自的标准差的乘积。值越小表示匹配程度越好。

method = methodology[1]#选了一个cv2.TM_CCOEFF_NORMED方法进行图像匹配,匹配方式为比较模板和图像中各个区域的标准归一化相关系数
res = cv2.matchTemplate(img, template, method)#比较完成的结果存储在res中,是一个ndarray类型的

# 获取匹配结果中的最大值和最小值
#通过左上角点的位置,加上之前获取的小图像的宽和高h,w,就可以把图像在原图中的那个位置框出来了
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)#最主要的是知道图像位置_loc


#不同的匹配算法,计算匹配到的位置的方式不同
if method in [cv2.TM_SQDIFF,cv2.TM_SQDIFF_NORMED]:
    top_left = min_loc
else:
    top_left = max_loc

#通过左上角点的坐标和h,w计算右下角点的坐标
bottom_right = (top_left[0]+w, top_left[1]+h)#[0]为横坐标,[1]为纵坐标,横加宽纵加高就是右下角的点坐标

#绘制矩形
resoult_img = cv2.rectangle(img.copy(),top_left,bottom_right,255,1)

cv_show('img',resoult_img)

代码讲解:

  1. 读取两个图像:template表示要匹配的小图片,img表示需要在其中寻找小图片的大图片。

  2. 通过shape属性获取小图像的高h和宽w是几个像素点。

  3. 选择不同的方法模板匹配的方式,包括cv2.TM_CCOEFF、 cv2.TM_CCOEFF_NORMED、cv2.TM_CCORR、cv2.TM_CCORR_NORMED、cv2.TM_SQDIFF、cv2.TM_SQDIFF_NORMED。不同的方法模板匹配比较的东西也不同。概述我在代码标注中写了。此次实验中,我们选取cv2.TM_CCOEFF_NORMED方法进行图像匹配。

  4. 调用cv2.matchTemplate()函数进行图像匹配,比较完成的结果存储在res中,是一个ndarray类型的。具体来说就是原图中每一块区域所有像素点的比较结果,都存储在这个矩阵里。

  5. 获取匹配结果中的最大值和最小值以及对应的位置信息。最大值就代表最优匹配

  6. 不同的匹配算法,计算匹配到的位置的方式不同。如果是平方差匹配,则选取最小值点作为匹配结果;否则选取最大值点。匹配结果的坐标就是最优匹配的左上角的坐标。通过左上角点的位置,加上之前获取的小图像的宽和高h,w,就可以把图像在原图中的那个位置框出来了。

  7. 通过左上角点的坐标和h,w计算右下角点的坐标。

  8. 计算完右下角的点坐标之后,我们就有了左上角和右下角点的坐标。通过cv2.rectangle()方法再原图中将匹配的区域画出来。

返回结果:

先用cv_show方法展示了模板图像,此时咱们读取的是灰度图所以是灰色的。

 

按任意键继续,展示了需要进行样板匹配的图像:

 按任意键继续后,展示的图像是用矩形框将匹配的区域框选出来的图像

匹配多个对象:

上面的代码就是一个简单的图像匹配的具体流程。而再实际应用中,我们可能会面临一个情况就是我的图像中有多个区域(或者说对象)和模板匹配。那么此时我们就不是用

min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)

这行代码去进行找res对象中最佳的匹配了,而是设置一个阈值,找出相似程度超过阈值的区域。

此次我们示例图像如下:我用画图软件画的

 样板如下:

完整代码:

import cv2
import numpy as np

def cv_show(title,img):
    cv2.imshow(title,img)
    cv2.waitKey(0) 
    cv2.destroyAllWindows()
    return

#读取两个图像
template = cv2.imread('target.png',0)#读取灰度图目标
img = cv2.imread('targets.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)


# 获取小图像的高和宽
h, w = template.shape[:2]
method = cv2.TM_CCOEFF_NORMED

#进行匹配,匹配结果存放在res中
res = cv2.matchTemplate(gray, template, method)

#与之前直接读取最大最小值不同,此次我们需要的是res中多个目标的结果,所以在此设置一个阈值
threshold = 0.8 
loc = np.where(res >= threshold)#阈值为0.8,即取百分之80匹配的
for loc in zip(*loc[::-1]):
    bottom_right = (loc[0]+w, loc[1]+h)
    cv2.rectangle(img,loc, bottom_right, 255, 2 )

cv_show('resoult',img)

代码讲解:

  1. 此次我读取图像的方式有些不同,之前我是直接读取灰度图,但是这次我读取了BGR图像然后将转化成了灰度图。因为我注意到上一个项目示例中最终展示的结果不是彩色的
  2. 中间一段的代码和上文相同。h,w获取样板的宽和高的像素点,匹配方式和之前一样。大家也可以试试其他的method,通常情况下,归一化对比方法(后缀带_NORMED的方法)要比原方法效果好些,通过归一化可以消除图像亮度、对比度等因素的影响,使得匹配结果更加准确。但是,由于归一化处理可能会使得图像失去一些细节信息。
  3. 设置了一个阈值0.8,用np.where()方法找出res中超过阈值的区域坐标,遍历所有的坐标,将其绘制再原图上。需要注意的是,由于where方法返回的坐标是先纵坐标再横坐标,所以需要将其反转。
  4. 展示图片。

 

在本章节中可能遇到的问题的一些提示:

  1. cv_show()方法是我自己定义的一个方法,主要功能就是调用了一下opencv自带的图像展示函数cv2.imshow(),和一个按任意键继续的功能cv2.waitKey(0) ,以及关闭窗口cv2.destroyAllWindows(),对于没看过我前几篇文章的人可能会有些疑惑,在此讲一下。
  2. cv2.imread()中如果输入参数0,就会直接读取图像的灰度图,如果不传这个参数就是读取彩色图像BGR格式的。
  3. 对于cv2.rectangle()方法,这个方法会改变传入对象,就是他会在传入的img上画矩形,而不是返回一个新的img,所以一般情况下传入的图像是img.copy()。那在多对象匹配中,我利用for循环进行矩形框的绘制,此时就不能传入copy的图像了,如果用copy图像进行绘制,那么每次都只画一个框,最后结果也是只有最后遍历的那一个框。
  4. 对于zip(*loc[::-1]),其中loc[::-1]是对原坐标转置的一个操作,原因是因为where方法返回的是纵横坐标而不是横纵坐标。不转置其实也可以,但是跟咱代码习惯有些不同,后面的右下角坐标计算方式也有些不同,所以这边还是转置一下。而*是一个捷报操作,就是将矩阵拆成单独的一个个元素。而对于zip()方法就是将解包解出来的单个元素又拼在一块变成一个元组,以便for循环遍历。总之这一个代码其实就是对坐标进行一个转置的操作。
  5. opencv中,图像格式默认是BGR而不是RGB
  6. 关于坐标。在计算机图形学中,图像实际上是一个像素矩阵,而文章中说的坐标实际上就是矩阵中各个元素的索引。由于矩阵的索引是按行列从左到右,从上到下的形式进行的。所以计算机图形学中的坐标系和平时的平面直角坐标系不同,他的y轴是反的。

有什么问题可以在评论区讨论哈哈哈

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐