5月28日 02:37

OpenCV.js 中的 Mat 对象是什么,如何创建和管理?

Mat 的基本概念

Mat(Matrix)是 OpenCV.js 中存储图像和矩阵数据的核心结构。底层是一个 n 维数组,支持单通道或多通道数据,常见类型包括:

类型常量含义典型场景
cv.CV_8UC18位无符号单通道灰度图
cv.CV_8UC38位无符号三通道RGB 图
cv.CV_8UC48位无符号四通道RGBA 图
cv.CV_32FC132位浮点单通道计算中间结果

mat.type() 可以在调试时确认当前 Mat 的数据类型——OpenCV.js 中大量报错都源于类型不匹配。

创建 Mat 的六种方式

1. 空矩阵与指定尺寸矩阵

javascript
let empty = new cv.Mat(); // 空 Mat let black = new cv.Mat(480, 640, cv.CV_8UC3); // 640x480 黑色 RGB 图

2. 带初始值的矩阵

javascript
let blue = new cv.Mat(480, 640, cv.CV_8UC3, new cv.Scalar(255, 0, 0));

cv.Scalar 按通道顺序赋值,三通道时依次为 B、G、R(OpenCV 默认 BGR 排列)。

3. 特殊矩阵

javascript
let 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 数组创建

javascript
let mat = cv.matFromArray(2, 2, cv.CV_8UC1, [1, 2, 3, 4]);

matFromArray 适合将已有数值数据灌入 Mat,在做矩阵运算或构造卷积核时常用。

5. 从 ImageData 创建

javascript
let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); let mat = cv.matFromImageData(imgData);

这种方式可以从任意 Canvas 2D 上下文直接拿到像素数据。

6. 从 HTML 图像元素创建

javascript
let 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) 读取。

获取原始数据指针

javascript
let 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)

javascript
let roi = mat.roi(new cv.Rect(x, y, width, height));

ROI 与原始 Mat 共享底层数据,修改 ROI 会同步影响原图。如需独立副本,调用 roi.clone()

类型转换

javascript
let floatMat = new cv.Mat(); mat.convertTo(floatMat, cv.CV_32FC1);

在做除法或需要小数精度的运算前,通常需要将 8 位整数 Mat 转为 32 位浮点型。

颜色空间转换

javascript
let gray = new cv.Mat(); cv.cvtColor(mat, gray, cv.COLOR_RGBA2GRAY);

内存管理:必须手动 delete

OpenCV.js 通过 Emscripten 编译为 WebAssembly,Mat 的内存分配在 WASM 堆上,不受 JavaScript 垃圾回收器管理。不再使用的 Mat 必须手动调用 delete() 释放,否则会造成内存泄漏。

推荐的 try-finally 模式

javascript
let 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(); }

封装辅助函数减少遗漏

javascript
function 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() 做深拷贝。

标签:Opencv.js