目标
在本教程中,您将学习
- 注意
- 除了指导如何在 Node.js 中运行 OpenCV.js 以外,本教程的另一个目标是向用户介绍 emscripten API 的基本知识,例如 Module 和 文件系统,以及 Node.js。
最小示例
创建一个名为 example1.js
的文件,其内容如下
// 使用一个带有“onRuntimeInitialized”方法的全局变量 'Module'
Module = {
onRuntimeInitialized() {
// 这是我们的应用程序
console.log(cv.getBuildInformation())
}
}
// 将 'opencv.js' 的值分配给全局变量 'cv'
cv = require('./opencv.js')
执行它
- 将文件另存为
example1.js
。
- 确保文件
opencv.js
在同一文件夹中。
- 确保 Node.js 已安装在您的系统上。
以下命令应打印 OpenCV 构建信息
刚才发生了什么?
- **在第一条语句中:** 通过定义一个名为“Module”的全局变量,当库可以准备就绪时,emscripten 将调用
Module.onRuntimeInitialized()
。我们的程序在该方法中,并且像在浏览器中一样使用全局变量 cv
。
- 语句 **“cv = require('./opencv.js')”** 要求文件
opencv.js
并将返回值分配给全局变量 cv
。 require()
是用于加载模块和文件的 Node.js API。 在本例中,我们从当前文件夹加载文件 opencv.js
,如前所述,当它准备就绪时,emscripten 将调用 Module.onRuntimeInitialized()
。
- 有关更多详细信息,请参阅emscripten Module API。
处理图像
OpenCV.js 不支持图像格式,因此我们无法直接加载 png 或 jpeg 图像。在浏览器中,它使用 HTML DOM(例如 HTMLCanvasElement 和 HTMLImageElement,对图像进行解码和解码)。在 node.js 中,我们需要为此使用一个库。
在此示例中,我们使用 jimp,它支持常见的图像格式,并且非常易于使用。
示例设置
执行以下命令以创建一个新的 node.js 包并安装 jimp 依赖项
mkdir project1
cd project1
npm init -y
npm install jimp
示例
const Jimp = require('jimp');
async function onRuntimeInitialized(){
// 使用 jimp 加载本地图像文件。它支持 jpg、png、bmp、tiff 和 gif
var jimpSrc = await Jimp.read('./lena.jpg');
// `jimpImage.bitmap` 属性具有解码的 ImageData,我们可以使用它来创建 cv:Mat
var src = cv.matFromImageData(jimpSrc.bitmap);
// 以下行是 opencv.js 膨胀教程的复制和粘贴
let dst = new cv.Mat();
let M = cv.Mat.ones(5, 5, cv.CV_8U);
let anchor = new cv.Point(-1, -1);
cv.dilate(src, dst, M, anchor, 1, cv.BORDER_CONSTANT, cv.morphologyDefaultBorderValue());
// 现在我们已经完成,我们希望将 `dst` 写入文件 `output.png`。为此,我们创建一个 `Jimp`
// 一种将图像数据作为 [`Buffer`](https://node.org.cn/docs/latest-v10.x/api/buffer.html) 接受的图像。
// `write('output.png')` 会将其写入磁盘,并且 Jimp 会从给定的文件名推断输出格式
new Jimp({
width: dst.cols,
height: dst.rows,
data: Buffer.from(dst.data)
})
.write('output.png');
src.delete();
dst.delete();
}
// 最后,像以前一样加载 open.js。函数 `onRuntimeInitialized` 包含我们的程序。
Module = {
onRuntimeInitialized
};
cv = require('./opencv.js');
执行它
- 将文件保存为
exampleNodeJimp.js
。
- 确保示例图像
lena.jpg
存在于当前目录中。
以下命令应该生成文件 output.png
模拟 HTML DOM 和 canvas
正如你可能已经看到的,其余的示例使用诸如 cv.imread()
、cv.imshow()
来读取和写入图像的功能。不幸的是,如上所述,它们在 Node.js 上不起作用,因为没有 HTML DOM。
在本部分,您将了解如何在 Node.js 上使用 jsdom 和 node-canvas 模拟 HTML DOM,使这些函数正常工作。
示例设置
与之前一样,我们创建一个 Node.js 项目并安装我们需要的依赖项
mkdir project2
cd project2
npm init -y
npm install canvas jsdom
示例
const { Canvas, createCanvas, Image, ImageData, loadImage } = require('canvas');
const { JSDOM } = require('jsdom');
const { writeFileSync, existsSync, mkdirSync } = require("fs");
// 这是我们的程序。这一次,我们使用 JavaScript async / await 和 promises 来处理异步操作。
(async () => {
// 在加载 opencv.js 之前,我们模拟最小的 HTML DOM。见下面的函数声明。
installDOM();
await loadOpenCV();
// 使用 node-canvas,我们将图像文件转换为与 HTML DOM Image 兼容的对象,从而与 cv.imread() 兼容
const image = await loadImage('./lena.jpg');
const src = cv.imread(image);
const dst = new cv.Mat();
const M = cv.Mat.ones(5, 5, cv.CV_8U);
const anchor = new cv.Point(-1, -1);
cv.dilate(src, dst, M, anchor, 1, cv.BORDER_CONSTANT, cv.morphologyDefaultBorderValue());
// 我们创建一个与 HTMLCanvasElement 兼容的对象
const canvas = createCanvas(300, 300);
cv.imshow(canvas, dst);
writeFileSync('output.jpg', canvas.toBuffer('image/jpeg'));
src.delete();
dst.delete();
})();
// 加载 opencv.js,就像之前一样,但使用 Promise 代替回调
function loadOpenCV() {
return new Promise(resolve => {
global.Module = {
onRuntimeInitialized: resolve
};
global.cv = require('./opencv.js');
});
}
// 使用 jsdom 和 node-canvas,我们定义一些全局变量来模拟 HTML DOM。
// 虽然可以存档完整的模拟,但这里我们仅定义 cv.imread() 和 cv.imshow() 使用的那些全局变量。
// by cv.imread() and cv.imshow().
function installDOM() {
const dom = new JSDOM();
global.document = dom.window.document;
// 其余部分启用 DOM 图像和画布,并由 node-canvas 提供
global.Image = Image;
global.HTMLCanvasElement = Canvas;
global.ImageData = ImageData;
global.HTMLImageElement = Image;
}
运行
- 将文件另存为
exampleNodeCanvas.js
。
- 确保示例图像
lena.jpg
存在于当前目录中。
以下命令应生成文件 output.jpg
node exampleNodeCanvas.js
处理文件
在本教程中,您将学习如何配置 emscripten,使其使用本地文件系统进行文件操作,而不是使用内存。它还尝试描述 emscripten 应用程序如何支持文件
在 OpenCV 应用程序中通常需要访问 emscripten 文件系统,例如加载机器学习模型,例如在 Load Caffe framework models 和 How to run deep networks in browser 中使用的模型。
示例设置
在看示例之前,最值得考虑的便是文件是怎样在 emscripten 应用程序(如 OpenCV.js)中进行处理的。请记住,OpenCV 库是用 C++ 编写的,而 opencv.js 文件只是 emscripten C++ 编译器将 C++ 代码转换成 JavaScript 或者 WebAssembly 的结果。
这些 C++ 源代码使用标准 API 访问文件系统,这导致的最终结果常常是调用系统调用来读取硬盘中的文件。由于浏览器中的 JavaScript 应用程序无法访问本地文件系统,因此 emscripten 模拟了标准的文件系统,这样编译后的 C++ 代码无需任何配置就能正常工作。
浏览器中,此文件系统在内存中模拟,而在 Node.js 中,也可以直接使用本地文件系统。这通常是更理想的选择,因为它无需在内存中复制文件的内容。本节说明如何执行此操作,即配置 emscripten,以直接从我们的本地文件系统访问文件,并且相对路径与预期的一样匹配相对于当前本地目录的文件。
示例
以下是 使用 Haar 级联进行人脸检测 的演变。
const { Canvas, createCanvas, Image, ImageData, loadImage } = require('canvas');
const { JSDOM } = require('jsdom');
const { writeFileSync, existsSync, mkdirSync } = require('fs');
(async () => {
await loadOpenCV();
const image = await loadImage('lena.jpg');
const src = cv.imread(image);
let gray = new cv.Mat();
cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY, 0);
let faces = new cv.RectVector();
let eyes = new cv.RectVector();
let faceCascade = new cv.CascadeClassifier();
let eyeCascade = new cv.CascadeClassifier();
// 加载预先训练的分类器文件。请注意,我们如何引用本地文件,使用的相对路径就
// 像往常一样
faceCascade.load('./haarcascade_frontalface_default.xml');
eyeCascade.load('./haarcascade_eye.xml');
let mSize = new cv.Size(0, 0);
faceCascade.detectMultiScale(gray, faces, 1.1, 3, 0, mSize, mSize);
for (let i = 0; i < faces.size(); ++i) {
let roiGray = gray.roi(faces.get(i));
let roiSrc = src.roi(faces.get(i));
let point1 = new cv.Point(faces.get(i).x, faces.get(i).y);
let point2 = new cv.Point(faces.get(i).x + faces.get(i).width, faces.get(i).y + faces.get(i).height);
cv.rectangle(src, point1, point2, [255, 0, 0, 255]);
eyeCascade.detectMultiScale(roiGray, eyes);
for (let j = 0; j < eyes.size(); ++j) {
let point1 = new cv.Point(eyes.get(j).x, eyes.get(j).y);
let point2 = new cv.Point(eyes.get(j).x + eyes.get(j).width, eyes.get(j).y + eyes.get(j).height);
cv.rectangle(roiSrc, point1, point2, [0, 0, 255, 255]);
}
roiGray.delete();
roiSrc.delete();
}
const canvas = createCanvas(image.width, image.height);
cv.imshow(canvas, src);
writeFileSync('output3.jpg', canvas.toBuffer('image/jpeg'));
src.delete(); gray.delete(); faceCascade.delete(); eyeCascade.delete(); faces.delete(); eyes.delete()
})();
/**
* 加载 opencv.js。
*
* 安装 HTML Canvas 模拟,以支持 `cv.imread()` 和 `cv.imshow`
*
* 在 emscripten 文件系统的文件夹 `rootDir` 中挂载给定的本地文件夹 `localRootDir`。默认情况下,它将在 emscripten“/work”目录中挂载本地当前目录。这意味着“/work/foo.txt”将解析为本地文件“./foo.txt”
* @param {string} rootDir emscripten 文件系统中的目录,在此目录中将挂载本地文件系统。
* @param {string} localRootDir 要挂载到 emscripten 文件系统的本地目录。
* @returns {Promise} 当库可以正常使用时解析。
*/
function loadOpenCV(rootDir = '/work', localRootDir = process.cwd()) {
if(global.Module && global.Module.onRuntimeInitialized && global.cv && global.cv.imread) {
return Promise.resolve()
}
return new Promise(resolve => {
installDOM()
global.Module = {
onRuntimeInitialized() {
// 我们将 emscripten 当前工作目录更改为“rootDir”,以便相对于当前本地
// 文件夹解析相对路径,达到预期效果
cv.FS.chdir(rootDir)
resolve()
},
preRun() {
// preRun() 是类似于 onRuntimeInitialized() 的另一个回调,但它在
// 库代码运行之前调用。我们在此处在 emscripten 文件系统中挂载本地文件夹,并希望在
// 库执行之前执行此操作,以便文件系统可以从一开始就进行访问
const FS = global.Module.FS
// 如果不存在,则创建根目录
if(!FS.analyzePath(rootDir).exists) {
FS.mkdir(rootDir);
}
// 如果不存在,则创建 localRootFolder
if(!existsSync(localRootDir)) {
mkdirSync(localRootDir, { recursive: true});
}
// FS.mount() 类似于 Linux/POSIX 的 mount 操作。它基本上挂载具有给定格式的外部
// 文件系统,并保存在给定的当前文件系统目录中。
FS.mount(FS.filesystems.NODEFS, { root: localRootDir}, rootDir);
}
};
global.cv = require('./opencv.js')
});
}
function installDOM(){
const dom = new JSDOM();
global.document = dom.window.document;
global.Image = Image;
global.HTMLCanvasElement = Canvas;
global.ImageData = ImageData;
global.HTMLImageElement = Image;
}
执行
- 将文件另存为
exampleNodeCanvasData.js
。
- 确保项目目录中存在文件
aarcascade_frontalface_default.xml
和 haarcascade_eye.xml
␛。它们可以从 OpenCV 源 获得。
- 确保项目目录中存在样例图片文件
lena.jpg
。它应该显示人脸,才能使此示例有意义。以下图片可以正常使用
图片
下述命令应该可以生成文件 output3.jpg
node exampleNodeCanvasData.js