基礎介紹
模板匹配是指在當前圖像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