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