1Using OpenCV.js In Node.js {#tutorial_js_nodejs} 2========================== 3 4Goals 5----- 6 7In this tutorial, you will learn: 8 9- Use OpenCV.js in a [Node.js](https://nodejs.org) application. 10- Load images with [jimp](https://www.npmjs.com/package/jimp) in order to use them with OpenCV.js. 11- Using [jsdom](https://www.npmjs.com/package/canvas) and [node-canvas](https://www.npmjs.com/package/canvas) to support `cv.imread()`, `cv.imshow()` 12- The basics of [emscripten](https://emscripten.org/) APIs, like [Module](https://emscripten.org/docs/api_reference/module.html) and [File System](https://emscripten.org/docs/api_reference/Filesystem-API.html) on which OpenCV.js is based. 13- Learn Node.js basics. Although this tutorial assumes the user knows JavaScript, experience with Node.js is not required. 14 15@note Besides giving instructions to run OpenCV.js in Node.js, another objective of this tutorial is to introduce users to the basics of [emscripten](https://emscripten.org/) APIs, like [Module](https://emscripten.org/docs/api_reference/module.html) and [File System](https://emscripten.org/docs/api_reference/Filesystem-API.html) and also Node.js. 16 17 18Minimal example 19----- 20 21Create a file `example1.js` with the following content: 22 23@code{.js} 24// Define a global variable 'Module' with a method 'onRuntimeInitialized': 25Module = { 26 onRuntimeInitialized() { 27 // this is our application: 28 console.log(cv.getBuildInformation()) 29 } 30} 31// Load 'opencv.js' assigning the value to the global variable 'cv' 32cv = require('./opencv.js') 33@endcode 34 35### Execute it ### 36 37- Save the file as `example1.js`. 38- Make sure the file `opencv.js` is in the same folder. 39- Make sure [Node.js](https://nodejs.org) is installed on your system. 40 41The following command should print OpenCV build information: 42 43@code{.bash} 44node example1.js 45@endcode 46 47### What just happened? ### 48 49 * **In the first statement**:, by defining a global variable named 'Module', emscripten will call `Module.onRuntimeInitialized()` when the library is ready to use. Our program is in that method and uses the global variable `cv` just like in the browser. 50 * The statement **"cv = require('./opencv.js')"** requires the file `opencv.js` and assign the return value to the global variable `cv`. 51 `require()` which is a Node.js API, is used to load modules and files. 52 In this case we load the file `opencv.js` form the current folder, and, as said previously emscripten will call `Module.onRuntimeInitialized()` when its ready. 53 * See [emscripten Module API](https://emscripten.org/docs/api_reference/module.html) for more details. 54 55 56Working with images 57----- 58 59OpenCV.js doesn't support image formats so we can't load png or jpeg images directly. In the browser it uses the HTML DOM (like HTMLCanvasElement and HTMLImageElement to decode and decode images). In node.js we will need to use a library for this. 60 61In this example we use [jimp](https://www.npmjs.com/package/jimp), which supports common image formats and is pretty easy to use. 62 63### Example setup ### 64 65Execute the following commands to create a new node.js package and install [jimp](https://www.npmjs.com/package/jimp) dependency: 66 67@code{.bash} 68mkdir project1 69cd project1 70npm init -y 71npm install jimp 72@endcode 73 74### The example ### 75 76@code{.js} 77const Jimp = require('jimp'); 78 79async function onRuntimeInitialized(){ 80 81 // load local image file with jimp. It supports jpg, png, bmp, tiff and gif: 82 var jimpSrc = await Jimp.read('./lena.jpg'); 83 84 // `jimpImage.bitmap` property has the decoded ImageData that we can use to create a cv:Mat 85 var src = cv.matFromImageData(jimpSrc.bitmap); 86 87 // following lines is copy&paste of opencv.js dilate tutorial: 88 let dst = new cv.Mat(); 89 let M = cv.Mat.ones(5, 5, cv.CV_8U); 90 let anchor = new cv.Point(-1, -1); 91 cv.dilate(src, dst, M, anchor, 1, cv.BORDER_CONSTANT, cv.morphologyDefaultBorderValue()); 92 93 // Now that we are finish, we want to write `dst` to file `output.png`. For this we create a `Jimp` 94 // image which accepts the image data as a [`Buffer`](https://nodejs.org/docs/latest-v10.x/api/buffer.html). 95 // `write('output.png')` will write it to disk and Jimp infers the output format from given file name: 96 new Jimp({ 97 width: dst.cols, 98 height: dst.rows, 99 data: Buffer.from(dst.data) 100 }) 101 .write('output.png'); 102 103 src.delete(); 104 dst.delete(); 105} 106 107// Finally, load the open.js as before. The function `onRuntimeInitialized` contains our program. 108Module = { 109 onRuntimeInitialized 110}; 111cv = require('./opencv.js'); 112@endcode 113 114### Execute it ### 115 116- Save the file as `exampleNodeJimp.js`. 117- Make sure a sample image `lena.jpg` exists in the current directory. 118 119The following command should generate the file `output.png`: 120 121@code{.bash} 122node exampleNodeJimp.js 123@endcode 124 125 126Emulating HTML DOM and canvas 127----- 128 129As you might already seen, the rest of the examples use functions like `cv.imread()`, `cv.imshow()` to read and write images. Unfortunately as mentioned they won't work on Node.js since there is no HTML DOM. 130 131In this section, you will learn how to use [jsdom](https://www.npmjs.com/package/canvas) and [node-canvas](https://www.npmjs.com/package/canvas) to emulate the HTML DOM on Node.js so those functions work. 132 133### Example setup ### 134 135As before, we create a Node.js project and install the dependencies we need: 136 137@code{.bash} 138mkdir project2 139cd project2 140npm init -y 141npm install canvas jsdom 142@endcode 143 144### The example ### 145 146@code{.js} 147const { Canvas, createCanvas, Image, ImageData, loadImage } = require('canvas'); 148const { JSDOM } = require('jsdom'); 149const { writeFileSync, existsSync, mkdirSync } = require("fs"); 150 151// This is our program. This time we use JavaScript async / await and promises to handle asynchronicity. 152(async () => { 153 154 // before loading opencv.js we emulate a minimal HTML DOM. See the function declaration below. 155 installDOM(); 156 157 await loadOpenCV(); 158 159 // using node-canvas, we an image file to an object compatible with HTML DOM Image and therefore with cv.imread() 160 const image = await loadImage('./lena.jpg'); 161 162 const src = cv.imread(image); 163 const dst = new cv.Mat(); 164 const M = cv.Mat.ones(5, 5, cv.CV_8U); 165 const anchor = new cv.Point(-1, -1); 166 cv.dilate(src, dst, M, anchor, 1, cv.BORDER_CONSTANT, cv.morphologyDefaultBorderValue()); 167 168 // we create an object compatible HTMLCanvasElement 169 const canvas = createCanvas(300, 300); 170 cv.imshow(canvas, dst); 171 writeFileSync('output.jpg', canvas.toBuffer('image/jpeg')); 172 src.delete(); 173 dst.delete(); 174})(); 175 176// Load opencv.js just like before but using Promise instead of callbacks: 177function loadOpenCV() { 178 return new Promise(resolve => { 179 global.Module = { 180 onRuntimeInitialized: resolve 181 }; 182 global.cv = require('./opencv.js'); 183 }); 184} 185 186// Using jsdom and node-canvas we define some global variables to emulate HTML DOM. 187// Although a complete emulation can be archived, here we only define those globals used 188// by cv.imread() and cv.imshow(). 189function installDOM() { 190 const dom = new JSDOM(); 191 global.document = dom.window.document; 192 193 // The rest enables DOM image and canvas and is provided by node-canvas 194 global.Image = Image; 195 global.HTMLCanvasElement = Canvas; 196 global.ImageData = ImageData; 197 global.HTMLImageElement = Image; 198} 199@endcode 200 201### Execute it ### 202 203- Save the file as `exampleNodeCanvas.js`. 204- Make sure a sample image `lena.jpg` exists in the current directory. 205 206The following command should generate the file `output.jpg`: 207 208@code{.bash} 209node exampleNodeCanvas.js 210@endcode 211 212 213Dealing with files 214----- 215 216In this tutorial you will learn how to configure emscripten so it uses the local filesystem for file operations instead of using memory. Also it tries to describe how [files are supported by emscripten applications](https://emscripten.org/docs/api_reference/Filesystem-API.html) 217 218Accessing the emscripten filesystem is often needed in OpenCV applications for example to load machine learning models such as the ones used in @ref tutorial_dnn_googlenet and @ref tutorial_dnn_javascript. 219 220### Example setup ### 221 222Before the example, is worth consider first how files are handled in emscripten applications such as OpenCV.js. Remember that OpenCV library is written in C++ and the file opencv.js is just that C++ code being translated to JavaScript or WebAssembly by emscripten C++ compiler. 223 224These C++ sources use standard APIs to access the filesystem and the implementation often ends up in system calls that read a file in the hard drive. Since JavaScript applications in the browser don't have access to the local filesystem, [emscripten emulates a standard filesystem](https://emscripten.org/docs/api_reference/Filesystem-API.html) so compiled C++ code works out of the box. 225 226In the browser, this filesystem is emulated in memory while in Node.js there's also the possibility of using the local filesystem directly. This is often preferable since there's no need of copy file's content in memory. This section is explains how to do do just that, this is, configuring emscripten so files are accessed directly from our local filesystem and relative paths match files relative to the current local directory as expected. 227 228### The example ### 229 230The following is an adaptation of @ref tutorial_js_face_detection. 231 232@code{.js} 233const { Canvas, createCanvas, Image, ImageData, loadImage } = require('canvas'); 234const { JSDOM } = require('jsdom'); 235const { writeFileSync, readFileSync } = require('fs'); 236 237(async () => { 238 await loadOpenCV(); 239 240 const image = await loadImage('lena.jpg'); 241 const src = cv.imread(image); 242 let gray = new cv.Mat(); 243 cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY, 0); 244 let faces = new cv.RectVector(); 245 let eyes = new cv.RectVector(); 246 let faceCascade = new cv.CascadeClassifier(); 247 let eyeCascade = new cv.CascadeClassifier(); 248 249 // Load pre-trained classifier files. Notice how we reference local files using relative paths just 250 // like we normally would do 251 faceCascade.load('./haarcascade_frontalface_default.xml'); 252 eyeCascade.load('./haarcascade_eye.xml'); 253 254 let mSize = new cv.Size(0, 0); 255 faceCascade.detectMultiScale(gray, faces, 1.1, 3, 0, mSize, mSize); 256 for (let i = 0; i < faces.size(); ++i) { 257 let roiGray = gray.roi(faces.get(i)); 258 let roiSrc = src.roi(faces.get(i)); 259 let point1 = new cv.Point(faces.get(i).x, faces.get(i).y); 260 let point2 = new cv.Point(faces.get(i).x + faces.get(i).width, faces.get(i).y + faces.get(i).height); 261 cv.rectangle(src, point1, point2, [255, 0, 0, 255]); 262 eyeCascade.detectMultiScale(roiGray, eyes); 263 for (let j = 0; j < eyes.size(); ++j) { 264 let point1 = new cv.Point(eyes.get(j).x, eyes.get(j).y); 265 let point2 = new cv.Point(eyes.get(j).x + eyes.get(j).width, eyes.get(j).y + eyes.get(j).height); 266 cv.rectangle(roiSrc, point1, point2, [0, 0, 255, 255]); 267 } 268 roiGray.delete(); 269 roiSrc.delete(); 270 } 271 272 const canvas = createCanvas(image.width, image.height); 273 cv.imshow(canvas, src); 274 writeFileSync('output3.jpg', canvas.toBuffer('image/jpeg')); 275 src.delete(); gray.delete(); faceCascade.delete(); eyeCascade.delete(); faces.delete(); eyes.delete() 276})(); 277 278/** 279 * Loads opencv.js. 280 * 281 * Installs HTML Canvas emulation to support `cv.imread()` and `cv.imshow` 282 * 283 * 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` 284 * @param {string} rootDir The directory in emscripten filesystem in which the local filesystem will be mount. 285 * @param {string} localRootDir The local directory to mount in emscripten filesystem. 286 * @returns {Promise} resolved when the library is ready to use. 287 */ 288function loadOpenCV(rootDir = '/work', localRootDir = process.cwd()) { 289 if(global.Module && global.Module.onRuntimeInitialized && global.cv && global.cv.imread) { 290 return Promise.resolve() 291 } 292 return new Promise(resolve => { 293 installDOM() 294 global.Module = { 295 onRuntimeInitialized() { 296 // We change emscripten current work directory to 'rootDir' so relative paths are resolved 297 // relative to the current local folder, as expected 298 cv.FS.chdir(rootDir) 299 resolve() 300 }, 301 preRun() { 302 // preRun() is another callback like onRuntimeInitialized() but is called just before the 303 // library code runs. Here we mount a local folder in emscripten filesystem and we want to 304 // do this before the library is executed so the filesystem is accessible from the start 305 const FS = global.Module.FS 306 // create rootDir if it doesn't exists 307 if(!FS.analyzePath(rootDir).exists) { 308 FS.mkdir(rootDir); 309 } 310 // create localRootFolder if it doesn't exists 311 if(!existsSync(localRootDir)) { 312 mkdirSync(localRootDir, { recursive: true}); 313 } 314 // FS.mount() is similar to Linux/POSIX mount operation. It basically mounts an external 315 // filesystem with given format, in given current filesystem directory. 316 FS.mount(FS.filesystems.NODEFS, { root: localRootDir}, rootDir); 317 } 318 }; 319 global.cv = require('./opencv.js') 320 }); 321} 322 323function installDOM(){ 324 const dom = new JSDOM(); 325 global.document = dom.window.document; 326 global.Image = Image; 327 global.HTMLCanvasElement = Canvas; 328 global.ImageData = ImageData; 329 global.HTMLImageElement = Image; 330} 331@endcode 332 333### Execute it ### 334 335- Save the file as `exampleNodeCanvasData.js`. 336- Make sure the files `aarcascade_frontalface_default.xml` and `haarcascade_eye.xml` are present in project's directory. They can be obtained from [OpenCV sources](https://github.com/opencv/opencv/tree/master/data/haarcascades). 337- Make sure a sample image file `lena.jpg` exists in project's directory. It should display people's faces for this example to make sense. The following image is known to work: 338 339![image](lena.jpg) 340 341The following command should generate the file `output3.jpg`: 342 343@code{.bash} 344node exampleNodeCanvasData.js 345@endcode 346