NRI OpenStandia Advent Calendar 2020の 23日目は、WebAssembly版OpenCV(OpenCV.js)についてです。
WebAssemblyとは
本題に入る前に、軽くWebAssemblyについて説明です。
WebAssemblyとは、ブラウザから機械語を実行できるようにした技術であり、下記のような特徴があります。
- JavaScriptよりも高速
- C、C++、Rust、Go等幅広い言語に対応
- Chrome、Firefox、Edge、Safari等幅広いブラウザに対応
利便性が高いため、公式からも幅広い使い方が提案されています。
WebAssemblyは高速
JavaScriptは高度な処理を行おうとすると、挙動が遅くなります。理由は主に二つありますが、WebAssemblyはその二点を解消しています。
JavaScriptが遅い理由1:インタプリタ方式だから
インタプリタ方式は、ソースコードを実行する際、機械語に都度変換する作業が発生してしまいます。
しかし、コンパイラ方式はあらかじめ中間言語に変換されているため、実行速度が速くなります。
JavaScriptが遅い理由2:動的型付けだから
動的型付けは型が未決定のため、実行するたびに変数の型を決める作業が発生します。WebAssemblyは静的型付けであるため、この作業が存在せず、実行速度が速くなります。
WebAssemblyの仕組み
WebAssemblyではwasmファイルを作成し、それをブラウザにて実行します。
このwasmファイルには、人間が読みやすいように設計されているテキスト形式(.wat)と機械用のバイナリ形式(.wasm)の二つの側面があります。
テキスト形式(.wat)は、S式と呼ばれるツリー構造の形式で成り立っています。
(module
(func (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add))
上記のファイルをバイナリ形式(.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とは
OpenCVとは画像処理のためのオープンソースであり、画像編集や物体認識を実現することができます。
OpenCVはC++やPythonで書かれているオープンソースですが、WebAssemblyを利用することで、
JavaScriptでも利用可能です。
このWebAssembly版OpenCVのことをOpenCV.jsと呼びます。
OpenCV.jsのつくりかた
WebAssemblyではC++言語のプログラムをwasmファイルにコンパイルする際、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と同じように画像編集や物体認識が可能です。
ここでは公式ドキュメントで紹介されていた例を記載します。
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();
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();
もちろん動画像処理やDNNによる画像処理といった重い作業もOpenCV.jsで可能です。
ちなみに、codepenにもOpenCV.jsが存在していたので、WebAssemblyの環境構築を
行わなくてもOpenCV.js自体は試すことができます。興味ある方は、ぜひこちらでお試しください。