ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

opencv C++模板匹配的简单实现

opencv C++模板匹配的简单实现 前言模板匹配template matching要解决的问题很具体在一张大图里找出与一张小图 模板最相似的位置。它是缺陷检测、UI 元素定位、工件对位、简易「找目标」程序 最常用的第一步OpenCV 用两个函数就能跑起来cv::matchTemplate做相似度计算cv::minMaxLoc把最高分的位置取出来。初学者最常见的三个误解「matchTemplate的返回值就是匹配到的矩形」——不是。它返回的是一张分数图CV_32FC1图的尺寸比原图小值是每个候选位置的相似度需要自己再取极值点。「所有方法都是取最大值」——不是。TM_SQDIFF和TM_SQDIFF_NORMED是「差异」要取最小值其余四种才是取最大值。取错方向得到的恰好是最不匹配的位置。「模板匹配能处理缩放和旋转」——不能。它本质上是一次纯平移的暴力搜索模板转个角度分数就会掉下去。下面按「原理与 API → 最小可运行示例 → 多目标与阈值 → 局限」的顺序讲。 代码基于OpenCV 4.x 的 C 接口示例按C17标准编写 目标编译器为 GCC 13 / Clang 17 / MSVC 19.3x。一、原理与 APImatchTemplate的做法是把模板在整张图上从左到右、从上到下逐像素滑动 在每个位置上按所选方法算一个相似度分数写进结果矩阵。因此输入图像尺寸W × H 模板尺寸 w × h 结果矩阵尺寸(W - w 1) × (H - h 1)类型 CV_32FC1函数原型OpenCV 4.xopencv2/imgproc.hppvoid cv::matchTemplate(cv::InputArray image, cv::InputArray templ, cv::OutputArray result, int method, cv::InputArray mask cv::noArray());result由函数内部按正确尺寸分配不要自己预先分配成和原图一样大。mask参数并非对所有方法都有效这一点后面单独说。支持的方法一共六种选错方向是最常见的低级错误方法含义最佳值归一化值域TM_SQDIFF平方差之和最小值否非负TM_SQDIFF_NORMED归一化平方差最小值是约 [0, 1]TM_CCORR相关点乘最大值否与亮度相关TM_CCORR_NORMED归一化相关最大值是约 [0, 1]TM_CCOEFF去均值相关最大值否可正可负TM_CCOEFF_NORMED归一化去均值相关最大值是约 [-1, 1]为什么实际项目里最常用TM_CCOEFF_NORMED因为它先把图像和模板各自减去均值 消掉了「整体加一个常数亮度平移」的影响再做归一化所以对亮度的线性变化不敏感 而且分数落在 [-1, 1] 这个便于设阈值的范围里。它的分数 1 表示与模板完全正相关 -1 表示完全负相关0 附近表示两者没有线性关系。需要强调这个分数不是概率也不是置信度。给一个固定的阈值比如 0.8 只是「在你这批数据上试出来的经验值」换一批图、换一种光照就要重新调。 不要在任何文档里把某个具体数字写成通用标准。二、最小可运行示例#include opencv2/core.hpp #include opencv2/imgcodecs.hpp #include opencv2/imgproc.hpp #include iostream int main(int argc, char** argv) { if (argc 3) { std::cerr 用法: argv[0] 大图路径 模板路径\n; return 1; } cv::Mat image cv::imread(argv[1], cv::IMREAD_COLOR); cv::Mat templ cv::imread(argv[2], cv::IMREAD_COLOR); if (image.empty() || templ.empty()) { std::cerr 图片读取失败\n; // imread 读不到文件时返回空 Mat不会抛异常 return 1; } if (templ.cols image.cols || templ.rows image.rows) { std::cerr 模板比大图还大无法匹配\n; return 1; } if (templ.channels() ! image.channels()) { std::cerr 通道数不一致请都读成彩色或都读成灰度\n; return 1; } const int method cv::TM_CCOEFF_NORMED; cv::Mat result; cv::matchTemplate(image, templ, result, method); // result: CV_32FC1 double min_val 0.0; double max_val 0.0; cv::Point min_loc; cv::Point max_loc; cv::minMaxLoc(result, min_val, max_val, min_loc, max_loc); const bool take_min (method cv::TM_SQDIFF || method cv::TM_SQDIFF_NORMED); const cv::Point best take_min ? min_loc : max_loc; const double score take_min ? min_val : max_val; std::cout 最佳分数: score \n; std::cout 左上角坐标: ( best.x , best.y )\n; // 注意best 是模板左上角在图像中的位置不是中心 cv::rectangle(image, cv::Rect(best.x, best.y, templ.cols, templ.rows), cv::Scalar(0, 0, 255), 2, cv::LINE_8); cv::imwrite(result.png, image); return 0; }几处要点cv::minMaxLoc的参数顺序是(src, minVal, maxVal, minLoc, maxLoc)最小值在前。传nullptr表示不关心那一项例如只想知道最大值就传cv::minMaxLoc(result, nullptr, max_val, nullptr, max_loc);。max_loc给出的是模板左上角的坐标不是中心点。想画以中心对齐的框要把坐标减去半个模板尺寸并且注意边界不要越界。cv::imread失败时返回空的cv::Matempty()为真不会抛异常所以必须自己检查。后续对空Mat做任何处理都会触发断言。颜色顺序是 BGR不是 RGB。cv::Scalar(0, 0, 255)是红色。编译时需要链接 core、imgproc、imgcodecs 三个模块。用 pkg-config 的话g -stdc17 match.cpp -o match $(pkg-config --cflags --libs opencv4)在 Windows 上用 MSVC 时别忘了把 OpenCV 的bin目录加进PATH 否则运行时会因为找不到 DLL 直接失败。三、多目标与阈值筛选真实场景里往往有多个目标。思路是取一次最高分 → 记下位置 → 把那块区域的分数压下去 → 再取一次循环到分数低于阈值为止。这段代码里的边界裁剪用if手写而不是std::min/std::max是为了避开 Windows 上windows.h定义的min/max宏 OpenCV 的某些头文件会间接引入它从而把std::min破坏掉#include opencv2/core.hpp #include opencv2/imgcodecs.hpp #include opencv2/imgproc.hpp #include iostream #include vector int main(int argc, char** argv) { if (argc 3) { return 1; } cv::Mat image cv::imread(argv[1], cv::IMREAD_COLOR); cv::Mat templ cv::imread(argv[2], cv::IMREAD_COLOR); if (image.empty() || templ.empty() || templ.cols image.cols || templ.rows image.rows || templ.channels() ! image.channels()) { std::cerr 输入不合法\n; return 1; } cv::Mat result; cv::matchTemplate(image, templ, result, cv::TM_CCOEFF_NORMED); const double threshold 0.80; // 经验阈值务必按自己的数据调整 const int max_count 10; std::vectorcv::Point hits; for (int k 0; k max_count; k) { double min_val 0.0; double max_val 0.0; cv::Point min_loc; cv::Point max_loc; cv::minMaxLoc(result, min_val, max_val, min_loc, max_loc); if (max_val threshold) { break; // TM_CCOEFF_NORMED 的值域在 [-1, 1] } hits.push_back(max_loc); // 把命中位置附近的分数压到 -1避免同一个目标被反复选中 int x0 max_loc.x - templ.cols / 2; int y0 max_loc.y - templ.rows / 2; int x1 max_loc.x templ.cols / 2; int y1 max_loc.y templ.rows / 2; if (x0 0) { x0 0; } if (y0 0) { y0 0; } if (x1 result.cols - 1) { x1 result.cols - 1; } if (y1 result.rows - 1) { y1 result.rows - 1; } result(cv::Rect(x0, y0, x1 - x0 1, y1 - y0 1)).setTo(-1.0); } for (const cv::Point p : hits) { cv::rectangle(image, cv::Rect(p.x, p.y, templ.cols, templ.rows), cv::Scalar(0, 0, 255), 2, cv::LINE_8); } std::cout 命中 hits.size() 处\n; cv::imwrite(result_multi.png, image); return 0; }这里的「抑制」手法是非极大值抑制non-maximum suppressionNMS的简化版 只把命中点周围一个小窗口压掉。如果两个目标靠得比模板还近这个简化版会把它们误合并成一个 要做得严谨应该记录所有候选峰再按「分数高的优先、与已保留框的重叠超过阈值就丢弃」 的规则筛选。setTo(-1.0)之所以安全是因为TM_CCOEFF_NORMED的分数量级不可能低到 -1 以下。四、局限与改进方向模板匹配的能力边界非常清晰超出边界就该换工具变化类型模板匹配能处理吗说明平移✅ 能这正是它唯一直接解决的问题亮度整体加减一个常数✅ 基本能用TM_CCOEFF_NORMED因为减掉了均值亮度线性缩放部分能归一化能缓解但不是严格不变缩放大小变了❌ 不能需要图像金字塔 多尺度循环旋转❌ 不能需要多角度循环或改用特征点方法遮挡部分能用mask排除模板中的无效区域关于maskOpenCV 的官方文档说明当前matchTemplate的mask参数只在TM_SQDIFF和TM_CCORR_NORMED两种方法下被支持 用在别的方法上会得到不符合预期的结果或直接报错。而且mask的尺寸必须与templ相同 通道数要么与模板一致要么是单通道单通道时会广播到所有通道。关于代价暴力搜索的运算量正比于「候选位置数 × 模板像素数」 也就是大致(W - w 1) × (H - h 1) × w × h这个量级。 模板越大、图越大耗时增长得非常快这是原理上的限制不是 OpenCV 实现不好。 工程上的常见做法是在缩小后的图上先粗定位再在原图上精定位。常见坑点场景❌ 错误写法✅ 正确写法结果图尺寸以为result和原图一样大用max_loc直接索引原图并假设它覆盖全图记住result是(W-w1) × (H-h1)极值方向对TM_SQDIFF取max_locTM_SQDIFF/TM_SQDIFF_NORMED取min_loc其余取max_loc坐标含义把max_loc当成目标中心点它是模板左上角的坐标尺寸检查模板大于图像时不检查OpenCV 内部断言失败程序直接终止先用templ.cols image.cols判断通道检查灰度图配彩色模板断言失败两者都用cv::IMREAD_COLOR或都用cv::IMREAD_GRAYSCALE读取失败不检查imread返回值就开始处理先判断image.empty()阈值照搬直接把网上的 0.9 拿来当阈值按自己的数据统计分数分布后再定多目标只调一次minMaxLoc就以为找到所有目标循环 抑制已命中区域掩码方法给TM_CCOEFF_NORMED传mask文档明确只有TM_SQDIFF、TM_CCORR_NORMED支持 maskROI 中的匹配在 ROI 上匹配后直接把坐标当原图坐标加上 ROI 左上角在原图中的偏移分数理解把匹配分数当成「匹配概率」它只是一个相似度度量量纲随方法而变有一点需要单独提醒在 ROI感兴趣区域上调matchTemplate时 返回的坐标是相对于 ROI 的画框之前必须先加上 ROI 的偏移量。这是实际项目里 最常出现的「明明分数很高、框却画到别处去了」的原因。总结步骤关键点读取图像imread失败返回空Mat必须检查empty()前置校验模板不能比图像大通道数要一致计算相似度matchTemplate(image, templ, result, method)结果为CV_32FC1取极值TM_SQDIFF*取最小值其余取最大值定位max_loc/min_loc是模板左上角坐标多目标循环取极值 抑制已命中区域NMS 简化版适用边界只处理平移缩放和旋转要靠金字塔、多角度或特征点方法模板匹配的价值在于简单、可控、无需训练只要目标没有缩放和旋转 它往往是最省事的方案而且分数图可以拿来做可视化出了问题时特别容易排查。 反过来一旦目标在尺度或角度上有变化就应该及时换成特征点匹配或深度学习检测 而不是在模板匹配里堆参数。
返回列表