OpenCV.js 中的 Mat 对象是什么,如何创建和管理?
Mat 的基本概念
Mat(Matrix)是 OpenCV.js 中存储图像和矩阵数据的核心结构。底层是一个 n 维数组,支持单通道或多通道数据,常见类型包括:
| 类型常量 | 含义 | 典型场景 |
|---|---|---|
cv.CV_8UC1 | 8位无符号单通道 | 灰度图 |
cv.CV_8UC3 | 8位无符号三通道 | RGB 图 |
cv.CV_8UC4 | 8位无符号四通道 | RGBA 图 |
cv.CV_32FC1 | 32位浮点单通道 | 计算中间结果 |
用 mat.type() 可以在调试时确认当前 Mat 的数据类型——OpenCV.js 中大量报错都源于类型不匹配。
创建 Mat 的六种方式
1. 空矩阵与指定尺寸矩阵
javascriptlet empty = new cv.Mat(); // 空 Mat let black = new cv.Mat(480, 640, cv.CV_8UC3); // 640x480 黑色 RGB 图
2. 带初始值的矩阵
javascriptlet blue = new cv.Mat(480, 640, cv.CV_8UC3, new cv.Scalar(255, 0, 0));
cv.Scalar 按通道顺序赋值,三通道时依次为 B、G、R(OpenCV 默认 BGR 排列)。
3. 特殊矩阵
javascriptlet zeros = cv.Mat.zeros(3, 3, cv.CV_8UC1); // 全零 let ones = cv.Mat.ones(3, 3, cv.CV_8UC1); // 全一 let eye = cv.Mat.eye(3, 3, cv.CV_32FC1); // 单位矩阵
4. 从 JavaScript 数组创建
javascriptlet mat = cv.matFromArray(2, 2, cv.CV_8UC1, [1, 2, 3, 4]);
matFromArray 适合将已有数值数据灌入 Mat,在做矩阵运算或构造卷积核时常用。
5. 从 ImageData 创建
javascriptlet imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); let mat = cv.matFromImageData(imgData);
这种方式可以从任意 Canvas 2D 上下文直接拿到像素数据。
6. 从 HTML 图像元素创建
javascriptlet img = document.getElementById('image'); img.onload = function() { let mat = cv.imread(img); // 处理 mat... mat.delete(); };
cv.imread 同时支持 <img> 和 <canvas> 元素。注意图像加载是异步的,必须在 onload 回调里操作 Mat。
像素读写与通道操作
读取像素值
javascript// 单通道灰度图 let val = mat.ucharAt(row, col); // 三通道 RGB 图,需逐通道读取 let r = mat.ucharAt(row, col * 3); let g = mat.ucharAt(row, col * 3 + 1); let b = mat.ucharAt(row, col * 3 + 2);
ucharAt 只适用于 8 位无符号类型。32 位浮点数据用 mat.floatAt(row, col) 读取。
获取原始数据指针
javascriptlet data = mat.data; // Uint8Array 视图
直接操作 mat.data 在大批量像素遍历时性能远优于逐像素调用 ucharAt。
复制 Mat:clone 与 copyTo
javascript// 深拷贝,生成完全独立的副本 let copy = mat.clone(); // 带掩码复制,只复制掩码非零区域 let mask = cv.Mat.zeros(mat.rows, mat.cols, cv.CV_8UC1); mat.copyTo(dst, mask);
clone() 总是完整深拷贝;copyTo() 支持掩码参数,适合选择性复制。
感兴趣区域(ROI)
javascriptlet roi = mat.roi(new cv.Rect(x, y, width, height));
ROI 与原始 Mat 共享底层数据,修改 ROI 会同步影响原图。如需独立副本,调用 roi.clone()。
类型转换
javascriptlet floatMat = new cv.Mat(); mat.convertTo(floatMat, cv.CV_32FC1);
在做除法或需要小数精度的运算前,通常需要将 8 位整数 Mat 转为 32 位浮点型。
颜色空间转换
javascriptlet gray = new cv.Mat(); cv.cvtColor(mat, gray, cv.COLOR_RGBA2GRAY);
内存管理:必须手动 delete
OpenCV.js 通过 Emscripten 编译为 WebAssembly,Mat 的内存分配在 WASM 堆上,不受 JavaScript 垃圾回收器管理。不再使用的 Mat 必须手动调用 delete() 释放,否则会造成内存泄漏。
推荐的 try-finally 模式
javascriptlet mat = new cv.Mat(100, 100, cv.CV_8UC3); let dst = new cv.Mat(); try { cv.cvtColor(mat, dst, cv.COLOR_BGR2GRAY); // 使用 dst 做后续处理... } finally { mat.delete(); dst.delete(); }
封装辅助函数减少遗漏
javascriptfunction withMat(fn) { let mats = []; let wrap = (m) => { mats.push(m); return m; }; try { return fn(wrap); } finally { mats.forEach(m => m.delete()); } } // 使用示例 withMat(wrap => { let src = wrap(cv.imread(canvas)); let gray = wrap(new cv.Mat()); cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY); cv.imshow('output', gray); });
常见内存错误
| 错误 | 表现 | 修正 |
|---|---|---|
忘记 delete() | 页面长时间运行后卡顿或崩溃 | try-finally 保证释放 |
重复 delete() | 抛出运行时异常 | delete 后将变量置为 null |
| ROI 未 delete | 原图数据被释放但 ROI 头未释放 | ROI 也是 Mat,必须单独 delete |
| 返回局部 Mat | 函数返回后 Mat 已 delete,调用方拿到空引用 | 返回 clone() 副本而非引用 |
面试追问
Q: OpenCV.js 的 Mat 和原生 OpenCV 的 cv::Mat 有什么区别?
底层数据结构一致,但 OpenCV.js 的 Mat 通过 Emscripten 暴露给 JavaScript,没有引用计数机制,必须手动 delete();而原生 C++ 的 Mat 析构时自动递减引用计数,计数归零才释放数据。
Q: 为什么 mat.ucharAt 读取三通道图像时要乘以 3?
因为 ucharAt(row, col) 按像素索引访问,而三通道图像在内存中每个像素占 3 字节连续存储,所以列号需要乘以通道数再偏移到对应通道。
Q: ROI 修改后原图为什么也变了?如何避免?
ROI 和原图共享同一块底层数据缓冲区,只是起止位置不同。需要独立副本时调用 roi.clone() 做深拷贝。