目標
このチュートリアルでは、次のことを学ぶ:
- 覚え書き
- OpenCV.js を Node.js で実行する手順を示すことに加えて、このチュートリアルのもう1つの目的は、emscripten API の基礎 (たとえば Module や File System など) と Node.js をユーザーに紹介することである。
最小限の例
次の内容で example1.js というファイルを作成する:
// Define a global variable 'Module' with a method 'onRuntimeInitialized':
Module = {
onRuntimeInitialized() {
// this is our application:
console.log(cv.getBuildInformation())
}
}
// Load 'opencv.js' assigning the value to the global variable 'cv'
cv = require('./opencv.js')
実行する
- ファイルを
example1.js として保存する。
opencv.js ファイルが同じフォルダにあることを確認する。
- Node.js がシステムにインストールされていることを確認する。
次のコマンドを実行すると、OpenCV のビルド情報が表示されるはずである:
何が起きたのか?
- 最初の文では:、'Module' という名前のグローバル変数を定義することで、ライブラリが使用可能になったときに emscripten が
Module.onRuntimeInitialized() を呼び出す。我々のプログラムはそのメソッド内にあり、ブラウザの場合と同じようにグローバル変数 cv を使用する。
- "cv = require('./opencv.js')" という文は
opencv.js ファイルを require し、その戻り値をグローバル変数 cv に代入する。Node.js の API である require() は、モジュールやファイルを読み込むために使用される。ここではカレントフォルダから 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(){
// load local image file with jimp. It supports jpg, png, bmp, tiff and gif:
var jimpSrc = await Jimp.read('./lena.jpg');
// `jimpImage.bitmap` property has the decoded ImageData that we can use to create a cv:Mat
var src = cv.matFromImageData(jimpSrc.bitmap);
// following lines is copy&paste of opencv.js dilate tutorial:
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());
// Now that we are finish, we want to write `dst` to file `output.png`. For this we create a `Jimp`
// image which accepts the image data as a [`Buffer`](https://nodejs.org/docs/latest-v10.x/api/buffer.html).
// `write('output.png')` will write it to disk and Jimp infers the output format from given file name:
new Jimp({
width: dst.cols,
height: dst.rows,
data: Buffer.from(dst.data)
})
.write('output.png');
src.delete();
dst.delete();
}
// Finally, load the open.js as before. The function `onRuntimeInitialized` contains our program.
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");
// This is our program. This time we use JavaScript async / await and promises to handle asynchronicity.
(async () => {
// before loading opencv.js we emulate a minimal HTML DOM. See the function declaration below.
installDOM();
await loadOpenCV();
// using node-canvas, we an image file to an object compatible with HTML DOM Image and therefore with 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());
// we create an object compatible HTMLCanvasElement
const canvas = createCanvas(300, 300);
cv.imshow(canvas, dst);
writeFileSync('output.jpg', canvas.toBuffer('image/jpeg'));
src.delete();
dst.delete();
})();
// Load opencv.js just like before but using Promise instead of callbacks:
function loadOpenCV() {
return new Promise(resolve => {
global.Module = {
onRuntimeInitialized: resolve
};
global.cv = require('./opencv.js');
});
}
// Using jsdom and node-canvas we define some global variables to emulate HTML DOM.
// Although a complete emulation can be archived, here we only define those globals used
// by cv.imread() and cv.imshow().
function installDOM() {
const dom = new JSDOM();
global.document = dom.window.document;
// The rest enables DOM image and canvas and is provided by 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 アプリケーションでは、たとえば Load Caffe framework models や ブラウザでディープネットワークを実行する方法 で使われるような機械学習モデルを読み込むために、emscripten ファイルシステムへのアクセスがしばしば必要になる。
セットアップ例
例に入る前に、まず OpenCV.js のような emscripten アプリケーションでファイルがどのように扱われるかを考えておく価値がある。OpenCV ライブラリは C++ で書かれており、opencv.js ファイルはその C++ コードが emscripten の C++ コンパイラによって JavaScript または WebAssembly に変換されたものに過ぎないことを思い出してほしい。
これらの C++ ソースは標準 API を使ってファイルシステムにアクセスし、その実装は最終的にハードドライブ上のファイルを読むシステムコールに行き着くことが多い。ブラウザ内の JavaScript アプリケーションはローカルファイルシステムにアクセスできないため、emscripten は標準ファイルシステムをエミュレートすることで、コンパイルされた C++ コードがそのまま動作するようにしている。
ブラウザではこのファイルシステムはメモリ上でエミュレートされるが、Node.js ではローカルファイルシステムを直接利用する選択肢もある。ファイルの内容をメモリにコピーする必要がないため、この方法がしばしば望ましい。本節では、まさにそれを行う方法、すなわちファイルがローカルファイルシステムから直接アクセスされ、相対パスが現在のローカルディレクトリからの相対ファイルに期待どおり一致するように emscripten を設定する方法を説明する。
サンプル
以下は Haar Cascadeを用いた顔検出 を応用したものである。
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();
// Load pre-trained classifier files. Notice how we reference local files using relative paths just
// like we normally would do
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()
})();
/**
* Loads opencv.js.
*
* Installs HTML Canvas emulation to support `cv.imread()` and `cv.imshow`
*
* Mounts given local folder `localRootDir` in emscripten filesystem folder `rootDir`. By default it will mount the local current directory in emscripten `/work` directory. This means that `/work/foo.txt` will be resolved to the local file `./foo.txt`
* @param {string} rootDir The directory in emscripten filesystem in which the local filesystem will be mount.
* @param {string} localRootDir The local directory to mount in emscripten filesystem.
* @returns {Promise} resolved when the library is ready to use.
*/
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() {
// We change emscripten current work directory to 'rootDir' so relative paths are resolved
// relative to the current local folder, as expected
cv.FS.chdir(rootDir)
resolve()
},
preRun() {
// preRun() is another callback like onRuntimeInitialized() but is called just before the
// library code runs. Here we mount a local folder in emscripten filesystem and we want to
// do this before the library is executed so the filesystem is accessible from the start
const FS = global.Module.FS
// create rootDir if it doesn't exists
if(!FS.analyzePath(rootDir).exists) {
FS.mkdir(rootDir);
}
// create localRootFolder if it doesn't exists
if(!existsSync(localRootDir)) {
mkdirSync(localRootDir, { recursive: true});
}
// FS.mount() is similar to Linux/POSIX mount operation. It basically mounts an external
// filesystem with given format, in given current filesystem directory.
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 sources から入手できる。
- サンプル画像ファイル
lena.jpg がプロジェクトのディレクトリに存在することを確認する。この例が意味を持つためには、人の顔が写っている必要がある。次の画像は動作することが確認されている:
image
次のコマンドでファイル output3.jpg が生成されるはずである:
node exampleNodeCanvasData.js