问题场景
图 A(已标注,尺寸 W1×H1)在图 B(待匹配,尺寸 W2×H2)中找对应区域。如果 W1≠W2,直接用原始模板做 matchTemplate 会因为尺寸不匹配找错位置。
核心函数
/**
* 等比例自适应模板匹配
* @param {HTMLCanvasElement} canvasA - 模板来源图(图A)
* @param {HTMLCanvasElement} canvasB - 待搜索图(图B)
* @param {{x,y,w,h}} cropRect - 图A中框选的区域
* @param {number} padding - 搜索区域外扩像素(默认150)
*/
function matchCanvasRegion(canvasA, canvasB, cropRect, padding = 150) {
let matA = cv.imread(canvasA);
let matB = cv.imread(canvasB);
// 计算两图的尺寸比例
const scale = ((matB.cols / matA.cols) + (matB.rows / matA.rows)) / 2;
// 从图A切出模板,并按比例缩放
let rawTempl = matA.roi(new cv.Rect(
Math.round(cropRect.x), Math.round(cropRect.y),
Math.round(cropRect.w), Math.round(cropRect.h)
));
let templ = new cv.Mat();
cv.resize(rawTempl, templ,
new cv.Size(
Math.max(1, Math.round(cropRect.w * scale)),
Math.max(1, Math.round(cropRect.h * scale))
), 0, 0, cv.INTER_LINEAR
);
rawTempl.delete();
// 在图B中计算模板的理论位置,加 padding 限定搜索 ROI
const mappedX = cropRect.x * scale;
const mappedY = cropRect.y * scale;
const roiX = Math.max(0, Math.round(mappedX - padding));
const roiY = Math.max(0, Math.round(mappedY - padding));
const roiW = Math.min(matB.cols - roiX, templ.cols + padding * 2);
const roiH = Math.min(matB.rows - roiY, templ.rows + padding * 2);
let roi = matB.roi(new cv.Rect(roiX, roiY, roiW, roiH));
// 模板匹配(TM_SQDIFF_NORMED 取最小值)
let resultMat = new cv.Mat();
let mask = new cv.Mat();
cv.matchTemplate(roi, templ, resultMat, cv.TM_SQDIFF_NORMED, mask);
const result = cv.minMaxLoc(resultMat);
// 局部坐标转全图坐标
const finalX = result.minLoc.x + roiX;
const finalY = result.minLoc.y + roiY;
// 清理 Mat,避免内存泄漏
roi.delete(); resultMat.delete(); mask.delete();
templ.delete(); matA.delete(); matB.delete();
return {
x: finalX, y: finalY,
width: templ.cols, height: templ.rows,
score: result.minVal, // 越小越匹配
scale
};
}
Canvas 框选实现
let isDragging = false;
let startX, startY;
canvas.addEventListener("mousedown", e => {
isDragging = true;
const rect = canvas.getBoundingClientRect();
startX = e.clientX - rect.left;
startY = e.clientY - rect.top;
});
canvas.addEventListener("mousemove", e => {
if (!isDragging) return;
const rect = canvas.getBoundingClientRect();
const curX = e.clientX - rect.left;
const curY = e.clientY - rect.top;
// 绘制选择框预览
drawSelectionBox(startX, startY, curX - startX, curY - startY);
});
canvas.addEventListener("mouseup", e => {
isDragging = false;
const rect = canvas.getBoundingClientRect();
const endX = e.clientX - rect.left;
const endY = e.clientY - rect.top;
cropRect = {
x: Math.min(startX, endX),
y: Math.min(startY, endY),
w: Math.abs(endX - startX),
h: Math.abs(endY - startY)
};
});
canvas 显示尺寸和图像实际尺寸可能不同(canvas.width vs CSS 宽度),传给 matchCanvasRegion 前需换算:
const scaleX = canvas.width / canvas.offsetWidth;
const scaleY = canvas.height / canvas.offsetHeight;
const realCrop = {
x: cropRect.x * scaleX,
y: cropRect.y * scaleY,
w: cropRect.w * scaleX,
h: cropRect.h * scaleY
};
关键设计点
为什么用 TM_SQDIFF_NORMED:该方法返回差异值(越小越匹配),对光照变化比 TM_CCOEFF_NORMED 稍宽容,且结果在 0~1 之间便于设置阈值。
为什么限定 ROI 搜索:全图 matchTemplate 在高分辨率图上耗时长。图 A 和图 B 通常是同一场景的不同分辨率版本,目标位置按比例平移,padding 150px 足以覆盖拍摄角度差异。
内存管理:OpenCV.js 是 WebAssembly,new cv.Mat() 分配的内存不受 JS GC 管理,每次使用后必须手动 .delete(),否则会逐渐 OOM。
