做图像处理的朋友应该都有过这种经历:甲方丢过来一张产品照片,说“帮我把里面的logo找出来”;或者一批扫描件里要找出盖了红章的那几页;又或者做UI自动化的时候要在截图上定位某个按钮。这种需求听起来简单,真正动手的时候,很多人第一反应是上深度学习目标检测,YOLO、SSD一套组合拳,结果训练样本只有几张图画着框,模型还没收敛,需求方已经站着身后等了。实际上OpenCV里有个历史很悠久但依然好用的函数——cv2.matchTemplate(),也就是模板匹配。它不需要训练、几分钟就能跑通,在目标区域形状稳定、变化不大的场景下,比一套深度学习方案划算得多。这篇文章就把模板匹配从原理、API、坑位到工程化实战一次讲透,适合已经有OpenCV基础、想快速落地目标定位需求的读者,也适合刚入门、想搞懂“图像里怎么找一个已知小图”的同学。
1. 模板匹配到底能干什么:从场景到算法边界
1.1 一句话说清模板匹配在做什么
模板匹配的核心思路非常直白:拿一张已知的“模板小图”,在待搜索的“大图”上从左到右、从上到下地滑动,每滑动到一个位置就计算一次模板和覆盖区域的相似度,最后找到相似度最高的位置,这个位置就是目标的坐标。
这里的关键在于“滑动”两个字。它不像深度学习那样先提取高层语义特征再回归边界框,而是用最朴素的像素级比较来回答问题——模板和图像某一块区域像不像?像的话到底有多像?OpenCV的cv2.matchTemplate()把整个滑窗比较过程封装好,我们只需要传入原图、模板、匹配方法三个参数,就能拿到一张“相似度响应图”,剩下的定位工作交给坐标计算和阈值筛选。
所以从本质上讲,模板匹配也是一种特征提取——它把整张模板当作一个最原始的“特征描述子”,直接和图像局部区域做匹配。理解这一点很重要,因为后面遇到复杂场景需要换方案时,你会发现它和SIFT、ORB这些特征点匹配并没有本质上的割裂,只是一个用全图做特征,一个用关键点做特征。
1.2 适合硬刚模板匹配的典型场景
不是所有目标检测问题都值得上神经网络,以下这些场景用cv2.matchTemplate()就能解决得很好:
- 工业视觉中的固定工件定位。比如流水线上工件位置基本固定,只需要找到偏移量来引导机械臂抓取,模板匹配速度极快,精度可以做到亚像素级别。
- 印刷品和票据检测。在一张扫描的合同里找公章位置、在一张答题卡里找定位黑块,目标形状固定、颜色固定,模板匹配很稳。
- UI自动化与游戏脚本。在截图上找“开始按钮”“确定按钮”,这类目标往往是固定的图标或文字,直接用模板匹配就行,很多自动化测试框架底层就是这个思路。
- OCR预处理中的字符或字段定位。先用模板匹配定位到要识别的区域,再裁剪出来交给OCR引擎,能大幅降低误识别。
这些场景的共同特点是:目标外观稳定、没有严重的透视形变、光照基本可控。说白了,模板匹配适合的是“找已知形状”的问题,而不是“理解内容”的问题。
1.3 边界意识:什么时候不该用模板匹配
很多刚接触OpenCV的朋友容易把模板匹配当成万能药,一旦目标旋转、缩放、光照变化,效果立刻崩盘。模板匹配最怕三件事:
- 目标尺度变化。模板是固定尺寸的,大图里目标变大或变小,匹配分数会急剧下降。
- 目标旋转或透视形变。模板是正放的,目标转了30度,相似度就废了。
- 光照变化剧烈。非归一化的匹配方法对亮度极其敏感,同一个场景开灯和关灯拍两张图,匹配结果可能完全不一样。
另外还有一个老生常谈的问题:模板拼接。如果你要求的是“识别出这张图里有没有猫”,那模板匹配做不了,因为猫的形态千变万化,你需要的是目标检测模型。这类“理解语义”的任务还是老老实实走深度学习的路子,别拿模板匹配硬扛。
2. 核心API拆解与单目标检测落地
2.1 函数签名与参数逐个分析
cv2.matchTemplate()在OpenCV 4.x中的完整调用方式是:
result = cv2.matchTemplate(image, templ, method[, result[, mask]])四个参数里面,前面三个是必须的,mask是可选的:
- image:待搜索的大图。可以是灰度图,也可以是彩色图,但必须和模板的通道数一致。
- templ:模板小图。注意,OpenCV要求模板尺寸不能大于原图,否则会直接抛异常。
- method:匹配方法,官方提供了6种,下一章展开讲。
- mask:掩膜图,OpenCV 3.2之后才支持。作用是在模板匹配时只关注模板的某些区域,比如忽略背景、只匹配前景轮廓。需要注意的是,mask只在TM_SQDIFF和TM_CCORR_NORMED两种方法下才有效,用TM_CCOEFF_NORMED时传mask会报错。
有一点操作上的细节容易被忽略:image和templ最好都转成灰度图再匹配。彩色图匹配不是不行,但对光照更敏感,计算量也更大,绝大多数实战项目里灰度匹配已经足够。转灰度用:
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) templ_gray = cv2.cvtColor(templ, cv2.COLOR_BGR2GRAY)2.2 输出结果矩阵的形状到底是什么意思
很多教程只告诉你“使用cv2.minMaxLoc()找最大值位置”,却不解释返回值到底是什么。这一步搞不懂,后面多目标检测一定会懵。
cv2.matchTemplate()返回的result是一张单通道浮点型矩阵,它的尺寸是:
result.shape = (image_h - templ_h + 1, image_w - templ_w + 1)也就是说,result里的每个像素点(x, y),代表的是模板左上角放在原图(x, y)位置时,模板与原图这个局部区域的相似度。模板每向右移动一个像素,result就会多一列;每向下移动一个像素,result就会多一行。所以result可以理解为一张“相似度热力图”,值越高的位置,说明那里长得很像模板。
还要强调一点:result是float32类型,不是uint8,所以如果你想用cv2.imshow()直接显示它,需要先归一化到0~255再转成uint8,否则看到的是一张全黑的图。
2.3 单目标定位:minMaxLoc的用法
如果是单目标检测,也就是确定图里只有一个目标,直接找到result里的最大值或最小值位置就行。具体用最大值还是最小值,取决于匹配方法的定义,这一点非常容易踩坑。
用cv2.minMaxLoc()可以一次性拿到最大值、最小值以及它们各自的位置:
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)其中min_loc和max_loc都是(x, y)格式的点坐标。注意:如果用的是TM_SQDIFF或TM_SQDIFF_NORMED,最佳匹配在min_loc;其余四种方法最佳匹配在max_loc。
这个顺序搞反的话,你画出来的框会跑到图上最不像目标的位置去,而且很难排查,因为代码不报错、运行也流畅,就是结果完全不对。当年我第一次用时就在这里浪费了近一个小时。
2.4 一个最简可跑通的单目标检测代码
把整个流程串起来,一个完整的单目标检测程序长这样:
import cv2 import numpy as np # 读取原图和模板 img = cv2.imread("scene.jpg") templ = cv2.imread("target.jpg") h, w = templ.shape[:2] # 统一转灰度 img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) templ_gray = cv2.cvtColor(templ, cv2.COLOR_BGR2GRAY) # 模板匹配,使用归一化相关系数 res = cv2.matchTemplate(img_gray, templ_gray, cv2.TM_CCOEFF_NORMED) min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) # 相关系数方法:最大值位置即最佳匹配位置 top_left = max_loc bottom_right = (top_left[0] + w, top_left[1] + h) # 画框 cv2.rectangle(img, top_left, bottom_right, (0, 0, 255), 2) cv2.imshow("Result", img) cv2.waitKey(0) cv2.destroyAllWindows()这个代码跑通之后,你会发现max_val通常是一个接近1的数,比如0.96、0.98,这个值就是模板和最佳匹配区域的相似度分数。分数越高越可信,具体阈值怎么卡,后面第6章再细说。
3. 六种匹配方法的原理对比与选型
3.1 官方提供的六个method到底差在哪
OpenCV的cv2.matchTemplate()支持6种匹配方法,名字很长,但本质可以分成三个系列:平方差、相关性、相关系数。每个系列都有原始版本和归一化版本。
先看平方差系列:
- TM_SQDIFF:计算模板与图像区域每个像素差的平方和。如果两个区域完全一样,结果为0;差异越大,结果越大。所以这个方法的匹配方向是值越小越好。
- TM_SQDIFF_NORMED:对平方差结果做归一化,把数值压缩到一个相对稳定的范围,依然越小越匹配。
平方差系列给我的感觉是“最诚实”的方法,它对像素值的绝对差异非常敏感,好处是定位精度高,坏处是一点点亮度变化就能把匹配分数打到惨不忍睹。
再看相关性系列:
- TM_CCORR:计算模板与图像区域的乘积和。完全一样的区域会得到较大的正响应,但问题是,即使图像区域很亮、没有任何结构,只要像素值整体偏大,乘积和也会很大。它很容易把高亮区域误判为目标。
- TM_CCORR_NORMED:做了归一化后相对好一些,但仍然对亮度和对比度变化比较敏感,实际工程中我很少用它。
最后是相关系数系列,这也是我最常用的两个方法:
- TM_CCOEFF:在计算之前,先对模板和图像区域分别减去各自的均值。这一步非常关键,相当于把“整体亮度”这个因素先剔除掉,然后再做相关计算。它评估的是模板和图像区域的结构相似性,而不是绝对灰度相似性。因此对光照变化有一定鲁棒性。
- TM_CCOEFF_NORMED:在TM_CCOEFF基础上继续除以归一化因子,得到的分数范围在0到1之间,1表示完全匹配,0表示毫无关系,负值表示负相关。数值语义清楚,方便卡阈值。
3.2 归一化到底救了什么
很多初学者不理解归一化版本和原始版本的区别。我打个比方:平方差和相关性这类原始方法,比的是“像素值本身像不像”;而减去均值之后的方法,比的是“像素值的变化趋势像不像”。
举一个实际场景:白天拍了一张产品图,模板是从晚上打光环境下截的,两者整体亮度差了50。用TM_SQDIFF匹配,每个位置的像素差都很大,最小值可能依然很高,阈值根本没法卡。但用TM_CCOEFF_NORMED,因为先减掉了均值,整体亮度偏移被消除了,模板的纹理特征依然能匹配得上。
所以,除非你能严格保证拍摄光照绝对一致,否则不要用非归一化方法。这也是为什么99%的模板匹配教程最后都会推荐TM_CCOEFF_NORMED。
3.3 方法一栏表与选型建议
我把自己常用的选型思路整理成一个表,方便对照:
| 方法 | 匹配方向 | 归一化 | 对光照敏感性 | 使用建议 |
|---|---|---|---|---|
| TM_SQDIFF | 越小越好 | 否 | 高 | 极少直接用,除非灰度可控 |
| TM_SQDIFF_NORMED | 越小越好 | 是 | 中 | 需要mask时使用 |
| TM_CCORR | 越大越好 | 否 | 极高 | 不推荐 |
| TM_CCORR_NORMED | 越大越好 | 是 | 中高 | 一般不用 |
| TM_CCOEFF | 越大越好 | 否 | 中 | 分数无上界,不好卡阈值 |
| TM_CCOEFF_NORMED | 越大越好 | 是 | 低 | 默认首选,多目标检测也用这个 |
实际项目里我的默认选择始终是TM_CCOEFF_NORMED。只有在需要用到mask参数、必须屏蔽模板某些区域时,才会被迫切到TM_SQDIFF_NORMED。
4. 多目标检测:阈值筛选与重叠框合并
4.1 为什么minMaxLoc只能锁一个目标
单目标检测找最大值/最小值位置就够了,但真实场景往往是“图片里有多张相同的图标”“画面中有多个同类工件”,这时候再用minMaxLoc就废了,因为它只会返回响应最大的那一个。
多目标检测的思路其实很朴素:既然result是一张“相似度热力图”,那只要把所有相似度分数超过某个阈值的位置全部找出来,每个位置画一个框,目标不就都出来了吗?关键操作就是两个:np.where做条件筛选,以及处理相邻位置重复框的合并。
4.2 用np.where批量获取候选点
threshold一般取0.8左右,具体看你的场景对误检和漏检的容忍度。获取候选位置的代码:
import numpy as np res = cv2.matchTemplate(img_gray, templ_gray, cv2.TM_CCOEFF_NORMED) threshold = 0.8 # 找出所有分数大于等于阈值的位置 loc = np.where(res >= threshold) for pt in zip(*loc[::-1]): cv2.rectangle(img, pt, (pt[0] + w, pt[1] + h), (0, 255, 0), 2)注意一个非常经典的坑:np.where返回的是数组的索引,格式是(行, 列),也就是(y, x)顺序。而OpenCV的画框函数需要的是(x, y)顺序,所以必须用loc[::-1]把行列反转,再zip打包成坐标点数组。这里反了的话,画出来的框会全部跑到对称位置去,而且边界越小的图越诡异,排查起来非常费劲。
4.3 重叠框合并:给模板匹配做简易NMS
上面的代码跑一遍,你可能会发现同一个目标被框了好几次,而且框之间是交错的。原因很简单:目标边缘的响应值也超过阈值了,比如模板是50×50,目标中心位置响应0.95,往左偏移2个像素响应0.85,这2个像素位置都被判定为候选。最后画出来就是一堆互相重叠的框。
解决思路和YOLO等目标检测算法里的NMS(非极大值抑制)完全一致:
- 把所有候选矩形按分数从高到低排序。
- 取分数最高的框,保留它。
- 删除所有与这个框重叠面积过大的其它候选框。
- 重复以上过程,直到没有候选框剩余。
这里给出一个适合模板匹配的极简NMS实现,算法本身不复杂,核心就是计算两个矩形框的交并比IoU:
def py_nms(rects, scores, iou_thresh=0.3): """ rects: [[x1, y1, x2, y2], ...] scores: [score, ...] """ rects = np.array(rects) scores = np.array(scores) order = scores.argsort()[::-1] # 按分数降序排列 x1 = rects[:, 0] y1 = rects[:, 1] x2 = rects[:, 2] y2 = rects[:, 3] areas = (x2 - x1 + 1) * (y2 - y1 + 1) keep = [] while order.size > 0: i = order[0] keep.append(i) # 计算当前框与其余所有框的相交区域 xx1 = np.maximum(x1[i], x1[order[1:]]) yy1 = np.maximum(y1[i], y1[order[1:]]) xx2 = np.minimum(x2[i], x2[order[1:]]) yy2 = np.minimum(y2[i], y2[order[1:]]) inter_w = np.maximum(0.0, xx2 - xx1 + 1) inter_h = np.maximum(0.0, yy2 - yy1 + 1) inter = inter_w * inter_h iou = inter / (areas[i] + areas[order[1:]] - inter) inds = np.where(iou <= iou_thresh)[0] order = order[inds + 1] return keep用的时候,先收集阈值以上的所有矩形和对应分数,再调py_nms去重:
rects = [] scores = [] for pt in zip(*loc[::-1]): score = res[pt[1], pt[0]] rects.append([pt[0], pt[1], pt[0] + w, pt[1] + h]) scores.append(score) keep = py_nms(rects, scores, iou_thresh=0.3) for idx in keep: x1, y1, x2, y2 = rects[idx] cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2)这套NMS逻辑在目标数量少的时候完全够用,不需要引入复杂依赖。如果你做的是工业项目、目标数量大且对实时性有要求,可以直接用OpenCV 4.5+里提供的cv2.dnn.NMSBoxes(),接口更成熟,但思路和这段自定义实现是一样的。
4.4 多目标匹配的实际效果和阈值调试
多目标匹配的成败,很大程度取决于threshold的选取。阈值太高会漏检,阈值太低会出现大量误检框。我的习惯是先设0.85,打印出所有局部极大值范围内的分数,观察目标位置的分数和背景位置的分数差距。如果两者差距很大,阈值随便卡;如果差距很小,说明模板本身区分度就不够,这时候调阈值只是治标,更该做的是换更干净的模板或者预处理图像。
还有一个小技巧:使用cv2.dilate()对result做一次膨胀操作,可以把相邻的局部极大值区域连成一片,再通过cv2.findNonZero()找连通域的质心作为候选点。这种方法本质上也是一种去重手段,在追求极简代码时可以考虑。
5. 尺度与旋转变化:工程化的下一步
5.1 多尺度模板匹配的思路与代码
模板匹配最让人头疼的弱点就是尺度敏感。实际场景里,摄像机距离变了、相机分辨率变了、目标本身大小变了,模板就失效了。最直接的工程化解法就是多尺度匹配:把一张模板缩放成多个尺寸,分别做一次匹配,取所有尺度里响应最高的结果。
import cv2 import numpy as np def match_multiscale(img_gray, templ_gray, scale_range=(0.5, 1.5), step=0.05): best_score = -1 best_scale = 1.0 best_loc = None th, tw = templ_gray.shape[:2] for scale in np.arange(scale_range[0], scale_range[1], step): new_w = int(tw * scale) new_h = int(th * scale) if new_w < 10 or new_h < 10: continue resized_templ = cv2.resize(templ_gray, (new_w, new_h)) res = cv2.matchTemplate(img_gray, resized_templ, cv2.TM_CCOEFF_NORMED) min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) if max_val > best_score: best_score = max_val best_scale = scale best_loc = max_loc # 换算回原图坐标 new_w = int(tw * best_scale) new_h = int(th * best_scale) top_left = best_loc bottom_right = (top_left[0] + new_w, top_left[1] + new_h) return top_left, bottom_right, best_score, best_scale这里有几个工程细节需要留意:
- scale_range的选取要结合目标实际大小和模板大小的比例关系来定。比如模板是100×100,实际目标可能只有60,那scale下限要放到0.6以下。
- step步长决定了尺度的粗细程度。step越小越精确,但计算量是成倍增长的。0.05是一个相对平衡的默认值。
- 每轮resize之后模板宽高要转成整数,OpenCV的resize不接受浮点尺寸。
- 匹配到的坐标是基于当前尺度模板在原始大图上的位置,如果模板图像缩放了而大图没变,坐标可以直接使用;如果反过来缩放的是大图,那坐标还要除以缩放比换算回去,很多人在这步容易出问题。
多尺度匹配的缺点是慢。模板尺寸大、尺度范围宽的时候,一轮匹配可能要执行几十次。不过它这个慢是有规律可循的,后面优化章节会讲怎么加速。
5.2 旋转问题:多角度模板的工程套路
旋转问题比尺度问题更棘手,因为每转一个角度就要重新生成一个模板,角度稍多计算量就直接爆炸。简单场景下我一般这么处理:
- 已知目标大致旋转角度范围,比如±15度。在这个范围内每5度生成一个旋转模板,总共7个模板,分别做一次匹配,取最优结果。这7次匹配可以放到多线程里并行跑,单次耗时可接受。
- 目标旋转角度不确定,比如任意角度,模板匹配就不太合适了,应该转向特征点匹配或者轮廓匹配。旋转问题本质上改变的是模板的几何结构,像素级的滑窗比较很难覆盖所有角度形态。
旋转模板的生成代码不算复杂,写一个生成旋转模板的工具函数就行了:
def rotate_template(templ, angle): h, w = templ.shape[:2] center = (w // 2, h // 2) matrix = cv2.getRotationMatrix2D(center, angle, 1.0) rotated = cv2.warpAffine(templ, matrix, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE) return rotated需要注意旋转后模板边缘会被裁掉或者出现无效区域,建议配合mask参数使用,让旋转后不自然的边缘区域不参与匹配计算。
5.3 什么时候应该转向特征匹配
多尺度和多角度说到底都是在给模板匹配“打补丁”,补到后面计算开销越来越大,反而失去了模板匹配本身的低价优势。一旦出现以下情况,我的建议是直接换方案:
- 目标有较大的透视形变,例如拍一张倾斜的书封面。
- 目标纹理稀疏,整块区域都是纯色,模板匹配的响应分不出差别。
- 要求在旋转、缩放同时存在的条件下稳定识别,且实时性要求高。
这时候一般考虑使用SIFT、ORB等特征点检测与匹配。它们的思路和模板匹配完全相反:先从图像中提取出关键点,再用描述子表示这些关键点周边的局部特征,最后通过描述子之间的距离来判断匹配关系。这个过程对旋转、缩放天然鲁棒,代价是计算量更高、实现更复杂。
所以模板匹配和特征匹配之间没有绝对的优劣,只有合适不合适。工程选型的本质就是在精度、鲁棒性、计算成本之间找平衡点。
6. 实战中的坑位与性能优化
6.1 我踩过的坑:一份拿来即用的避坑清单
这么多年用下来,模板匹配的坑基本都集中在那么几处,列出来供大家直接避开:
- 通道数不一致报错或匹配结果异常。image是三通道、templ是灰度图,OpenCV有的版本会直接抛异常,有的版本匹配结果完全错误。最稳妥的做法是匹配前统一通道。
- 模板尺寸大于原图。必然报错,而且提示信息不够直观。建议先判断一下形状,或者先用try包一层。
- TM_SQDIFF系列的最佳匹配点用错方向。记住,平方差是最小值,相关系数是最大值。
- np.where结果忘记反转坐标。这个坑我上面提过了,再强调一次是因为它太常见且太隐蔽。
- 彩色图匹配对色彩通道顺序敏感。OpenCV读入的彩色图是BGR通道顺序,如果你用PIL读图再转成numpy数组,通道顺序是RGB,直接匹配会得到完全错误的分数。这种情况用cv2.COLOR_RGB2BGR转换后再操作。
- 模板边界包含过多背景信息。裁剪模板时尽量贴住目标边缘,背景越多,目标周围相似背景的区域越容易被误检。
6.2 性能优化:图像金字塔与ROI裁剪
模板匹配的计算量跟图像尺寸直接相关。假设原图是1920×1080,模板是100×100,一次matchTemplate需要计算的滑窗位置数大约是(1920-100+1)×(1080-100+1),约178万个位置,每个位置都要做100×100次像素运算。这个量级在Python里跑一次大约几百毫秒,实时性直接不合格。
最常见的加速手段是图像金字塔,思路是先小后大:
- 把原图缩小到原来的1/4(宽高各缩小一半),模板也同步缩小。
- 在缩小图上跑一次模板匹配,粗定位到目标的大致区域。
- 把这个区域映射回原图坐标,加上一个扩展余量。
- 只在原图的这个扩展区域里做一次细匹配。
这样做的好处是,第一步粗匹配的计算量只有原来的十六分之一,第二步细匹配的搜索范围非常小,整体耗时往往能降低一个数量级。
ROI裁剪是另一个简单有效的优化手段。如果目标只会出现在画面某个区域,比如流水线上工件永远不会跑到左上角,那就先把搜索区域裁剪出来,再用cv2.matchTemplate()在裁剪区域上匹配,计算量立刻降下来。
另外,多尺度多模板并行时,Python的GIL对多线程不太友好,建议用multiprocessing或者OpenCV本身的并行能力。OpenCV的matchTemplate底层本身就是并行优化的,多模板时可以考虑用cv2.dnn.blobFromImages把多个模板拼成batch一次算,不过这套玩法比较复杂,一般项目用不到。
6.3 关于阈值、预处理与最终效果的个人心得
最后分享一点我自己比较依赖的经验。很多人把模板匹配的失败归咎于算法,其实大多数时候问题出在“直接用原图匹配”上。模板匹配是对像素灰度非常敏感的算法,预处理往往比换匹配方法更有效。
比如光照不均的场景,先做一次直方图均衡化,让整体亮度更均匀;或者用cv2.Canny提取边缘图,在边缘图上做模板匹配,这样匹配的是结构轮廓而不是灰度值,抗光照能力会强很多。再比如目标与背景颜色相近时,可以考虑先转到HSV空间,只用某个通道做匹配,往往效果比灰度图更好。
阈值卡多少这个问题,千万不要照搬别人的数值。同一套模板,换了相机和光源之后,目标位置的分数可能从0.97跌到0.82。有条件的项目,建议做一个小的标定过程:拿几十张正样本跑一遍,统计目标位置的最低分数,以这个分数再减去固定的余量作为阈值。
总的来说,cv2.matchTemplate()这个函数虽然基础、虽然古老,但只要理解了它的算法原理,清楚它的能力边界,掌握多尺度多目标这些工程化扩展手段,它在实际项目中依然能扛下大量“又快又省”的目标定位需求。做图像处理的人,心里始终要放一把尺子:什么情况用什么尺度的工具,而不是什么情况都上深度学习。模板匹配就是这把尺子上精度足够、成本最低的那一格。