基础介绍
模板匹配是指在当前图像A里寻找与图像B最相似的部分,本文中将图像A称为模板图像,将图像B称为搜索匹配图像。
引言:一般在Opencv
里实现此种功能非常方便:直接调用
result = cv2.matchTemplate(templ, search, method)
- templ 为原始图像
- search 为搜索匹配图像,它的尺寸必须小于或等于原始图像
- method 表示匹配方式
method一般取值:
type CompareWay =| "CV_TM_SQDIFF"| "CV_TM_SQDIFF_NORMED"| "CV_TM_CCORR"| "CV_TM_CCORR_NORMED"| "CV_TM_CCOEFF"| "CV_TM_CCOEFF_NORMED";
当然这里我们不是主要讲Opencv的api的,只是单独提出来,说明在前端实现对应的算法,就能进行模板匹配。
比如以CV_TM_SQDIFF
算法为例:
- 遍历的起始坐标从原图A的左数第1个像素值开始
- 以搜索匹配B图的大小(w * h)匹配比较原图上对应空间上(w * h)的像素值
- 依次进行A图右移一像素去匹配B图,直到A图右侧(w)小于B图的w,然后换行再匹配
- 重复进行到A图距离底部不支持h大于B图的高度
- 最后找出最小误差值
我们的目标是实现这两张图的匹配:
这里实现对应的js算法
/*** 差值平方和匹配 CV_TM_SQDIFF* @param template 匹配的图片灰度值[x,x,x,...] w * h 长度的灰度图片数据* @param search 搜索的图片灰度值[x,x,x,...] w * h 长度的灰度图片数据* @param tWidth 匹配图片的width* @param tHeight 匹配图片的height* @param sWidth 搜索图片的width* @param sHeight 搜索图片的height*/
const cvTmSqDiff = (template, search, tWidth, tHeight, sWidth, sHeight) => {let minValue = Infinity;let x = -1;let y = -1;for (let th = 0; th < tHeight; th += 1) {for (let tW = 0; tW < tWidth; tW += 1) {if (tW + sWidth > tWidth || th + sHeight > tHeight) {continue;}let sum = 0;for (let sH = 0; sH < sHeight; sH += 1) {for (let sW = 0; sW < sWidth; sW += 1) {const tValue = template[(th + sH) * tWidth + tW + sW];const sValue = search[sH * sWidth + sW];sum += (tValue - sValue) * (tValue - sValue);}}if (minValue > sum) {minValue = sum;x = tW;y = th;}if (sum === 0) {return { x, y };}}}return { x, y };
};
因此根据上述算法的可行性,我们可以先将A图和B图进行RGB
值转Gary
值: 借鉴OpenCV中的转换方式
Gray = 0.299*r + 0.587*g + 0.114*b
再将转换好A图和B图的灰度值进行匹配比较:
const {x, y} = cvTmSqDiff(template, search, tWidth, tHeight, sWidth, sHeight);
得到的x
、y
则是在原图A上的对应匹配成功的坐标,加上对应B图的大小,我们则可以在原图的基础上画出一个矩形框表示匹配的区域:
前端分步实现
上面大概讲了匹配的大致实现思路,下面开始正式的js代码实现:
- 1、加载原图A和原图B
Promise.all([imgLoader("./lena.png"), imgLoader("./search.png")]).then((values: any) => {...}
);
- 2、得到图片数据的rgb值,并转化为灰度值
Promise.all([getImageData(values[0]), getImageData(values[1])]).then((dataValues: any) => {const model = rgbToGary(dataValues[0]);const search = rgbToGary(dataValues[1]);...}
);
- 3、获取对应的匹配坐标
const posi = getTemplatePos(model,search,dataValues[0].width,dataValues[0].height,dataValues[1].width,dataValues[1].height,"CV_TM_CCOEFF_NORMED"
);
- 4、绘制原图和匹配到矩形框
const canvas = document.createElement("canvas");
canvas.width = dataValues[0].width;
canvas.height = dataValues[0].height;
const ctx = canvas.getContext("2d");ctx.drawImage(values[0], 0, 0);
ctx.strokeStyle = "red";
ctx.strokeRect(posi.x,posi.y,dataValues[1].width,dataValues[1].height
);
document.body.appendChild(canvas);
上述所用的的函数imgLoader getImageData rgbToGary getTemplatePos
都可以在这里找到xy-imageloader
也可以npm安装:npm i xy-imageloader
完整代码
import imgLoader, { getImageData, rgbToGary } from "xy-imageloader";
import { getTemplatePos } from "xy-imageloader/lib/utils";
Promise.all([imgLoader("./lena.png"), imgLoader("./search.png")]).then((values: any) => {Promise.all([getImageData(values[0]), getImageData(values[1])]).then((dataValues: any) => {const model = rgbToGary(dataValues[0]);const search = rgbToGary(dataValues[1]);const posi = getTemplatePos(model,search,dataValues[0].width,dataValues[0].height,dataValues[1].width,dataValues[1].height,"CV_TM_CCOEFF_NORMED");const canvas = document.createElement("canvas");canvas.width = dataValues[0].width;canvas.height = dataValues[0].height;const ctx = canvas.getContext("2d");ctx.drawImage(values[0], 0, 0);ctx.strokeStyle = "red";ctx.strokeRect(posi.x,posi.y,dataValues[1].width,dataValues[1].height);document.body.appendChild(canvas);});}
);
附算法思想:
* CV_TM_SQDIFF
- CV_TM_SQDIFF_NORMED
- CV_TM_CCORR
- CV_TM_CCORR_NORMED
- CV_TM_CCOEFF
- CV_TM_CCOEFF_NORMED
算法具体实现可参考 xy-imageloader