WebAssembly版OpenCV（OpenCV.js）について

[NRI OpenStandia Advent Calendar 2020](https://qiita.com/advent-calendar/2020/nri-openstandia)の 23日目は、WebAssembly版OpenCV（OpenCV.js）についてです。

# WebAssemblyとは

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534839/36c7e51c-0827-4d17-18f7-af418149402f.png" width="300" />

本題に入る前に、軽くWebAssemblyについて説明です。
WebAssemblyとは、ブラウザから機械語を実行できるようにした技術であり、下記のような特徴があります。

- JavaScriptよりも高速
- C、C++、Rust、Go等幅広い言語に対応
- Chrome、Firefox、Edge、Safari等幅広いブラウザに対応

利便性が高いため、公式からも幅広い使い方が提案されています。
- https://github.com/WebAssembly/design/blob/master/UseCases.md

# WebAssemblyは高速

JavaScriptは高度な処理を行おうとすると、挙動が遅くなります。理由は主に二つありますが、WebAssemblyはその二点を解消しています。

### JavaScriptが遅い理由１：インタプリタ方式だから

インタプリタ方式は、ソースコードを実行する際、機械語に都度変換する作業が発生してしまいます。
しかし、コンパイラ方式はあらかじめ中間言語に変換されているため、実行速度が速くなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534839/ee040a42-52eb-dce9-6979-4d08bf79dc09.png)

### JavaScriptが遅い理由２：動的型付けだから

動的型付けは型が未決定のため、実行するたびに変数の型を決める作業が発生します。WebAssemblyは静的型付けであるため、この作業が存在せず、実行速度が速くなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534839/e7f19733-e0d6-fcc0-c811-9024fcb531ff.png)

# WebAssemblyの仕組み

WebAssemblyではwasmファイルを作成し、それをブラウザにて実行します。
このwasmファイルには、人間が読みやすいように設計されているテキスト形式（.wat）と機械用のバイナリ形式（.wasm）の二つの側面があります。

テキスト形式（.wat）は、S式と呼ばれるツリー構造の形式で成り立っています。

```テキスト形式（.wat）例
(module
  (func (param $lhs i32) (param $rhs i32) (result i32)
    local.get $lhs
    local.get $rhs
    i32.add))
```

上記のファイルをバイナリ形式（.wasm）に変換すると、下記のようになります。

```バイナリ形式（.wasm）例
0000000: 0061 736d                                 ; WASM_BINARY_MAGIC
0000004: 0100 0000                                 ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01                                        ; section code
0000009: 00                                        ; section size (guess)
000000a: 01                                        ; num types
; func type 0
000000b: 60                                        ; func
000000c: 02                                        ; num params
000000d: 7f                                        ; i32
000000e: 7f                                        ; i32
000000f: 01                                        ; num results
0000010: 7f                                        ; i32
0000009: 07                                        ; FIXUP section size
; section "Function" (3)
0000011: 03                                        ; section code
0000012: 00                                        ; section size (guess)
0000013: 01                                        ; num functions
0000014: 00                                        ; function 0 signature index
0000012: 02                                        ; FIXUP section size
; section "Code" (10)
0000015: 0a                                        ; section code
0000016: 00                                        ; section size (guess)
0000017: 01                                        ; num functions
; function body 0
0000018: 00                                        ; func body size (guess)
0000019: 00                                        ; local decl count
000001a: 20                                        ; local.get
000001b: 00                                        ; local index
000001c: 20                                        ; local.get
000001d: 01                                        ; local index
000001e: 6a                                        ; i32.add
000001f: 0b                                        ; end
0000018: 07                                        ; FIXUP func body size
0000016: 09                                        ; FIXUP section size
; section "name"
0000020: 00                                        ; section code
0000021: 00                                        ; section size (guess)
0000022: 04                                        ; string length
0000023: 6e61 6d65                                name  ; custom section name
0000027: 02                                        ; local name type
0000028: 00                                        ; subsection size (guess)
0000029: 01                                        ; num functions
000002a: 00                                        ; function index
000002b: 02                                        ; num locals
000002c: 00                                        ; local index
000002d: 03                                        ; string length
000002e: 6c68 73                                  lhs  ; local name 0
0000031: 01                                        ; local index
0000032: 03                                        ; string length
0000033: 7268 73                                  rhs  ; local name 1
0000028: 0d                                        ; FIXUP subsection size
0000021: 14                                        ; FIXUP section size
```

ちなみにWebAssemblyは、正確にはアセンブリではございません。

アセンブリとは、機械語に近く、人間が読める文字列のことです。
このアセンブリというものは、実はプロセッサごとに微妙に異なります。

WebAssemblyはどのプロセッサにも対応できるよう、最小公倍数のような仕組みとなっております。
これにより、WebAssemblyはどのプロセッサやOS対しても同じwasmファイルを適用することができる（つまり、移植性が高い）という素晴らしい特徴があります。


# OpenCV.jsとは

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534839/fa67f3ee-9774-4c25-f15d-4597033beec7.png)

OpenCVとは画像処理のためのオープンソースであり、画像編集や物体認識を実現することができます。
OpenCVはC++やPythonで書かれているオープンソースですが、WebAssemblyを利用することで、
JavaScriptでも利用可能です。

このWebAssembly版OpenCVのことをOpenCV.jsと呼びます。

# OpenCV.jsのつくりかた

WebAssemblyではC++言語のプログラムをwasmファイルにコンパイルする際、[Emscripten](https://developer.mozilla.org/ja/docs/Mozilla/Projects/Emscripten)が主に用いられます。
そのため、本稿ではEmscriptenを用いてコンパイルを実行します。

まず、Windows10で実施する場合は、下記を事前に用意します。

- Ubuntu
- Git
- CMake
- Visual Studio
- Python

次に、Emscriptenをダウンロードします。EmscriptenはLLVMをベースとしたものです。

```
git clone https://github.com/juj/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
```

Emscriptenのダウンロードが完了したら、環境変数もセットします。

```
source ./emsdk_env.sh --build=Release
```

ここまで完了したら、コンパイル作業に移ります。
まずOpenCVをgitからcloneします。

```
git clone https://github.com/opencv/opencv.git
```

次に、OpenCVの中のスクリプトであるbuild_js.pyを実行します。
なおこのスクリプトを実行するためにはPythonとcmakeが必要です。

これはbuild_wasmディレクトリ内のソースをビルドするスクリプトとなっております。

```
cd opencv
python ./platforms/js/build_js.py build_wasm --build_wasm
```

この際、`--build_wasm`オプションがないとasm.jsでビルドされてしまうので注意してください。

# OpenCV.jsのつかいかた

OpenCV.jsを手に入れたら、通常のOpenCVと同じように画像編集や物体認識が可能です。
ここでは[公式ドキュメント](https://docs.opencv.org/3.4/d5/d10/tutorial_js_root.html)で紹介されていた例を記載します。

```javascript:画像の切り取り
let src = cv.imread('canvasInput');
let dst = new cv.Mat();
// You can try more different parameters
let rect = new cv.Rect(100, 100, 200, 200);
dst = src.roi(rect);
cv.imshow('canvasOutput', dst);
src.delete();
dst.delete();
```

切り取り結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534839/47cf6480-1a0b-0816-eafd-3c51948b376c.png)


```javascript:顔特徴量の検出

let src = cv.imread('canvasInput');
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 classifiers
faceCascade.load('haarcascade_frontalface_default.xml');
eyeCascade.load('haarcascade_eye.xml');
// detect faces
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]);
    // detect eyes in face ROI
    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();
}
cv.imshow('canvasOutput', src);
src.delete(); gray.delete(); faceCascade.delete();
eyeCascade.delete(); faces.delete(); eyes.delete();
```

検出結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534839/4b5024fb-b24d-1a6a-c7ac-05db21c374b4.png)

もちろん動画像処理やDNNによる画像処理といった重い作業もOpenCV.jsで可能です。

ちなみに、codepenにもOpenCV.jsが存在していたので、WebAssemblyの環境構築を
行わなくてもOpenCV.js自体は試すことができます。興味ある方は、ぜひこちらでお試しください。
- https://codepen.io/puku0x/pen/aKPgVE

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534839/1e2a3bb2-cc01-4a59-dfad-97dbc6ea8742.png)

# 参考

- https://webassembly.org/
- https://developer.mozilla.org/ja/docs/WebAssembly/Understanding_the_text_format
- https://developer.mozilla.org/ja/docs/WebAssembly/C_to_wasm
- https://docs.opencv.org/3.4/df/df7/tutorial_js_table_of_contents_setup.html