目标
在本教程中,您将学习
- 注意
- 除了提供在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。
在本节中,您将学习如何使用jsdom和node-canvas在Node.js上模拟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和promise来处理异步性。
(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应用程序如何支持文件
访问Emscripten文件系统通常在OpenCV应用程序中是必需的,例如加载机器学习模型,如加载Caffe框架模型和如何在浏览器中运行深度网络中使用的那些模型。
示例设置
在讲解示例之前,值得先考虑一下如何在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
// 如果rootDir不存在,则创建它
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。为了使本示例有意义,它应该显示人物的面部。已知以下图像有效
image
以下命令应该生成文件output3.jpg
node exampleNodeCanvasData.js