OpenCV.js は画像フォーマットをサポートしないため、png や jpeg 画像を直接読み込むことはできない。ブラウザでは HTML DOM (HTMLCanvasElement や HTMLImageElement など) を使用して画像のデコードを行う。node.js ではこのためにライブラリを使用する必要がある。
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;
}
例に入る前に、まず OpenCV.js のような emscripten アプリケーションでファイルがどのように扱われるかを考えておく価値がある。OpenCV ライブラリは C++ で書かれており、opencv.js ファイルはその C++ コードが emscripten の C++ コンパイラによって JavaScript または WebAssembly に変換されたものに過ぎないことを思い出してほしい。
これらの C++ ソースは標準 API を使ってファイルシステムにアクセスし、その実装は最終的にハードドライブ上のファイルを読むシステムコールに行き着くことが多い。ブラウザ内の JavaScript アプリケーションはローカルファイルシステムにアクセスできないため、emscripten は標準ファイルシステムをエミュレートすることで、コンパイルされた C++ コードがそのまま動作するようにしている。
ブラウザではこのファイルシステムはメモリ上でエミュレートされるが、Node.js ではローカルファイルシステムを直接利用する選択肢もある。ファイルの内容をメモリにコピーする必要がないため、この方法がしばしば望ましい。本節では、まさにそれを行う方法、すなわちファイルがローカルファイルシステムから直接アクセスされ、相対パスが現在のローカルディレクトリからの相対ファイルに期待どおり一致するように emscripten を設定する方法を説明する。
const { Canvas, createCanvas, Image, ImageData, loadImage } = require('canvas');
const { JSDOM } = require('jsdom');
const { writeFileSync, existsSync, mkdirSync } = require('fs');
const https = require('https');
(async () => {
const createFileFromUrl = function (path, url, maxRedirects = 10) {
console.log('Downloading ' + url + '...');
return new Promise((resolve, reject) => {
const download = (url, redirectCount) => {
if (redirectCount > maxRedirects) {
reject(new Error('Too many redirects'));
} else {
let connection = https.get(url, (response) => {
if (response.statusCode === 200) {
let data = [];
response.on('data', (chunk) => {
data.push(chunk);
});
response.on('end', () => {
try {
writeFileSync(path, Buffer.concat(data));
resolve();
} catch (err) {
reject(new Error('Failed to write file ' + path));
}
});
} else if (response.statusCode === 302 || response.statusCode === 301) {
connection.abort();
download(response.headers.location, redirectCount + 1);
} else {
reject(new Error('Failed to load ' + url + ' status: ' + response.statusCode));
}
}).on('error', (err) => {
reject(new Error('Network Error: ' + err.message));
});
}
};
download(url, 0);
});
};
if (!existsSync('./face_detection_yunet_2023mar.onnx')) {
await createFileFromUrl('./face_detection_yunet_2023mar.onnx', 'https://media.githubusercontent.com/media/opencv/opencv_zoo/main/models/face_detection_yunet/face_detection_yunet_2023mar.onnx')
}
if (!existsSync('./opencv.js')) {
await createFileFromUrl('./opencv.js', 'https://docs.opencv.org/5.x/opencv.js')
}
if (!existsSync('./lena.jpg')) {
await createFileFromUrl('./lena.jpg', 'https://docs.opencv.org/5.x/lena.jpg')
}
await loadOpenCV();
const image = await loadImage('./lena.jpg');
const src = cv.imread(image);
let srcBGR = new cv.Mat();
cv.cvtColor(src, srcBGR, cv.COLOR_RGBA2BGR);
// Load the deep learning model file. Notice how we reference local files using relative paths just
// like we normally would do
let netDet = new cv.FaceDetectorYN("./face_detection_yunet_2023mar.onnx", "", new cv.Size(320, 320), 0.9, 0.3, 5000);
netDet.setInputSize(new cv.Size(src.cols, src.rows));
let out = new cv.Mat();
netDet.detect(srcBGR, out);
let faces = [];
for (let i = 0, n = out.data32F.length; i < n; i += 15) {
let left = out.data32F[i];
let top = out.data32F[i + 1];
let right = (out.data32F[i] + out.data32F[i + 2]);
let bottom = (out.data32F[i + 1] + out.data32F[i + 3]);
left = Math.min(Math.max(0, left), src.cols - 1);
top = Math.min(Math.max(0, top), src.rows - 1);
right = Math.min(Math.max(0, right), src.cols - 1);
bottom = Math.min(Math.max(0, bottom), src.rows - 1);
if (left < right && top < bottom) {
faces.push({
x: left,
y: top,
width: right - left,
height: bottom - top,
x1: out.data32F[i + 4] < 0 || out.data32F[i + 4] > src.cols - 1 ? -1 : out.data32F[i + 4],
y1: out.data32F[i + 5] < 0 || out.data32F[i + 5] > src.rows - 1 ? -1 : out.data32F[i + 5],
x2: out.data32F[i + 6] < 0 || out.data32F[i + 6] > src.cols - 1 ? -1 : out.data32F[i + 6],
y2: out.data32F[i + 7] < 0 || out.data32F[i + 7] > src.rows - 1 ? -1 : out.data32F[i + 7],
x3: out.data32F[i + 8] < 0 || out.data32F[i + 8] > src.cols - 1 ? -1 : out.data32F[i + 8],
y3: out.data32F[i + 9] < 0 || out.data32F[i + 9] > src.rows - 1 ? -1 : out.data32F[i + 9],
x4: out.data32F[i + 10] < 0 || out.data32F[i + 10] > src.cols - 1 ? -1 : out.data32F[i + 10],
y4: out.data32F[i + 11] < 0 || out.data32F[i + 11] > src.rows - 1 ? -1 : out.data32F[i + 11],
x5: out.data32F[i + 12] < 0 || out.data32F[i + 12] > src.cols - 1 ? -1 : out.data32F[i + 12],
y5: out.data32F[i + 13] < 0 || out.data32F[i + 13] > src.rows - 1 ? -1 : out.data32F[i + 13],
confidence: out.data32F[i + 14]
})
}
}
out.delete();
faces.forEach(function(rect) {
cv.rectangle(src, {x: rect.x, y: rect.y}, {x: rect.x + rect.width, y: rect.y + rect.height}, [0, 255, 0, 255]);
if(rect.x1>0 && rect.y1>0)
cv.circle(src, {x: rect.x1, y: rect.y1}, 2, [255, 0, 0, 255], 2)
if(rect.x2>0 && rect.y2>0)
cv.circle(src, {x: rect.x2, y: rect.y2}, 2, [0, 0, 255, 255], 2)
if(rect.x3>0 && rect.y3>0)
cv.circle(src, {x: rect.x3, y: rect.y3}, 2, [0, 255, 0, 255], 2)
if(rect.x4>0 && rect.y4>0)
cv.circle(src, {x: rect.x4, y: rect.y4}, 2, [255, 0, 255, 255], 2)
if(rect.x5>0 && rect.y5>0)
cv.circle(src, {x: rect.x5, y: rect.y5}, 2, [0, 255, 255, 255], 2)
});
const canvas = createCanvas(image.width, image.height);
cv.imshow(canvas, src);
writeFileSync('output3.jpg', canvas.toBuffer('image/jpeg'));
console.log('The result is saved.')
src.delete(); srcBGR.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) {
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;
}