Upgrade to Pro — share decks privately, control downloads, hide ads and more …

やっぱりWasm は C++!!!~巨人の肩に乗ろう~

やっぱりWasm は C++!!!~巨人の肩に乗ろう~

この資料はWebAssembly Night #10の資料であり、同時にOpenCV Advent Calendar 2020の9日目の記事の素材でもあります。

リンクをクリックできる、PDF版はこちらのGoogle Driveからどうぞ。

4bfdd9b38e3f81f80e543004d810093d?s=128

Yuusuke KOUNOIKE

December 09, 2020
Tweet

Transcript

  1. はじめに質問 Wasmは何で書く︖ Go? Rust? AssembyScript? 1

  2. やっぱりWasm は C++!!! 〜巨⼈の肩に乗ろう〜 WebAssembly Night #10 kounoike (@ko_noike) &

    OpenCV Advent Calendar 2020 9⽇⽬ ※現状での個⼈の⾒解です 2
  3. はじめに ⾃⼰紹介 鴻池 祐輔(こうのいけ ゆうすけ) 株式会社スタジアム(Web⾯接サービス 「インタビューメーカー」を提供) 以前は産業⽤画像処理、IoT映像解析などの業務 ※本⽇の発表は個⼈としての発表で会社の意⾒表 明・開発した技術とは関係ありません。

    注釈 この資料はWebAssembly Night #10の資料であり、 同時にOpenCV Advent Calendar 2020の9⽇⽬の記事 の素材でもあります。 ※資料には各社の商標などが含まれます。 3
  4. ついでに 「Meet 最強 」記事を書いた⼈です (こうググれば出てくる)。 こういう記事を書けるくらいに画像処理の経 験と、ブラウザ上での画像処理への興味を持 ってます。 4

  5. 5

  6. 6

  7. 画像処理 on your Browserの時代 WebRTCインフラの充実 時⾬堂 WebRTC SFU Sora/NTT Com.

    SkyWay/Amazon Chime SDK/ Azure Communication Services/Zoom Customizable SDK... Web会議の実装がより簡単に →差別化要素は︖︓その⼀つとしてのブラウザ側での画像処理 Google Meet 背景ぼかし Meet 最強 記事に書いたように、Googleの技術を結集した成果 7
  8. 画像処理を⾃前で書く︖ まずは20年の歴史を持つOpenCVの⼒を借りよう GoやRustにも独⾃の画像処理ライブラリはあるが、 充実度で⾔えばやっぱりOpenCV Go/RustのOpenCVラッパー︖ 現状のwasmだとFFIは… 画像処理以外のマルチメディア・解析処理 動画: ffmpeg/gstreamer (話題のffmpeg.wasmはちょっと違う気がする…)

    ⾳声: Essentia(Essentia.jsあり) ⾃然⾔語処理: MeCab 推論処理: Tensorflow/Caffe/OpenVINO... 8
  9. 近い将来の変化 MediaStreamTrack Insertable Media Processing using Streams カメラからのストリームにフレーム単位で触れるようになる。 Wasm呼び出しとの相性も良いはず︖ →シンプルに画像処理に閉じたWasmの作成が求められる

    ※Web Audio APIの⽅が先⾏しててWasmで作成した処理ノード作るのが 流⾏ってるっぽい(Meetでも⾳声処理っぽいWasmを使ってる) 9
  10. 典型的ブラウザ画像処理 背景ぼかし TypeScript/tensorflow.jsで作られたBodyPixがよく使われる Webカメラ => getUserMedia() => videoタグ => BodyPix

    => canvasタグ => captureStream() => WebRTC 推論︓tensorflow.js(WebGLバックエンド/Wasmバックエンド) 後処理(ぼかし)︓BrowserJS/CSSでcanvasお絵描き頑張る WebRTCへの送信︓canvasの captureStream() バーチャル背景(背景差し替え)は︖ JSで頑張る︖ OpenCV使って楽をする︖ ⽬標処理時間(理想)︓30fps=33.3333ms 10
  11. 背景差し替え(ぼかし)処理 BodyPixの推論処理はマスク画像を得られる→マスクを使って合成(ぼかし)処理 元画像 →マスク画像 +背景画像 →合成画像 ※マスク画像だけはただのUint8Arrayで1チャンネル画像と看做せる。他はRGBA画像 「+」の処理(合成処理)はどうする︖ ※もうちょい複雑な話は「Meet 最強」記事にて解説

    11
  12. 合成処理(単純版) JSだと for(row=0; row<h; row++) for(col=0; col<w; col++) composed[(row*w+col)*4+3] =

    mask[row*w+col]; ctx.drawImage(bg); // 背景を先に描く ctx.putImageData(...); // アルファチャンネル使って合成 OpenCV.jsだと cv.split(original, imageRGBA); // RGBAチャンネルの分離 imageRGBA.set(3, mask); // 3:Alphaをmaskに差し替え cv.merge(imageRGBA, composed); // 結合して戻す ctx.drawImage(bg); // 背景を先に描く ctx.putImageData(...); // アルファチャンネル使って合成 ループはネイティブ側で処理できる 単純な合成でなく他の様々な画像処理もやりやすい 12
  13. OpenCV.jsの実態 C++ライブラリをEmscriptenビルドしたもの jsライブラリのドキュメントはそこまで充実してない C++のドキュメントを⾒ながら書く ラッパーの都合上、メモリ開放( img.delete() など)を⾃前で⾯倒みる必要がある 13

  14. 簡単な処理を書いて⽐較してみる 画像の明るさを-100〜+100でフレームごとに変化させてみる (ちょっとサボってRGBで-100〜+100にしておく) 配布されているopencv.js版 ⾃分でビルドしたopencv.js版 C++で書いたフルWasm版 GitHub kounoike/webassembly-night-sampleで公開 Docker/docker-compose があればOK

    14
  15. opencv.js版(配布/⾃分でビルドともにソースは同じ) let cnt = 0; const updateCanvas = () =>

    { const srcMat = new cv.Mat(video.height, video.width, cv.CV_8UC4); const rgbMat = new cv.Mat(); const rgbOutMat = new cv.Mat(); const outMat = new cv.Mat(); cap.read(srcMat); // videoタグから画像読み込み cv.cvtColor(srcMat, rgbMat, cv.COLOR_RGBA2RGB); // RGBA->RGB rgbMat.convertTo(rgbOutMat, -1, 1, cnt++ % 200 - 100); // -100〜+100 cv.cvtColor(rgbOutMat, outMat, cv.COLOR_RGB2RGBA); // RGBAに戻す cv.imshow("canvas", outMat); // canvasに描画 srcMat.delete(); // メモリ解放処理 rgbMat.delete(); rgbOutMat.delete(); outMat.delete(); requestAnimationFrame(updateCanvas); // rAFでループ }; updateCanvas(); ※実際は変数のスコープを持ち上げたりして解放処理サボったりする 15
  16. C++版(C++処理関数) void doOpenCvTask(size_t addr, int width, int height, int cnt)

    { auto data = reinterpret_cast<void *>(addr); cv::Mat rgbaMat(height, width, CV_8UC4, data); cv::Mat rgbMat; cv::Mat rgbOutMat; cv::Mat outMat; cv::cvtColor(rgbaMat, rgbMat, cv::COLOR_RGBA2RGB); // RGBA->RGB rgbMat.convertTo(rgbOutMat, -1, 1.0, cnt % 200 - 100.0); // -100〜+100 cv::cvtColor(rgbOutMat, outMat, cv::COLOR_RGB2RGBA); // RGBAに戻す // メモリ解放処理いらない if (SDL_MUSTLOCK(screen)) // 描画処理はちょっと⾯倒 SDL_LockSurface(screen); cv::Mat dstRGBAImage(height, width, CV_8UC4, screen->pixels); outMat.copyTo(dstRGBAImage); if (SDL_MUSTLOCK(screen)) SDL_UnlockSurface(screen); SDL_Flip(screen); } 16
  17. C++版呼び出しJS const updateCanvas = () => { context.drawImage(video, 0, 0);

    const data = context.getImageData(0, 0, width, height); const buffer = Module._malloc(data.data.length); Module.HEAPU8.set(data.data, buffer); Module.doOpenCvTask(buffer, width, height, cnt++); Module._free(buffer); requestAnimationFrame(updateCanvas); }; updateCanvas(); ※JS側でヒープメモリを管理している (確保・解放をループの外側にしてもいいけど) 17
  18. 以上のように・・・ OpenCV関数の呼び出し⽅はC++/JSで⼤して変わらない OpenCV.js専⽤ドキュメントはないのでC++版を⾒ながら書く JavaScriptはメモリ管理しなければならなくて⾯倒 C++はメモリ管理がいらなくて簡単 「C++はJavaScriptと違ってメモリ管理がいらなくて簡単」 18

  19. その他もろもろ 実⾏速度 画像サイズや処理内容(OpenCVの関数呼び出し回数)などにもよるが若⼲C++版の⽅ が速い、かな︖ というくらい サイズ OpenCV.jsは配布版・フルビルド版ともに8MBほど C++版は数百KB リンカで必要な関数だけになってる︖ 19

  20. もうちょっと複雑なことをやってみる DNNモジュールで顔検出の実装 C++での実装記事を参考にごにょごにょっと実装 結果 0.5fpsくらい ⾼速化 →SIMD対応でWasmビルド& chrome://flags で有効化 →8fpsくらい

    16倍⾼速化 ソースは先のリポジトリのfacedetect-full-wasm 20
  21. まとめ&補⾜ C++でWasm書くのは便利 各種マルチメディア・⾃然⾔語処理などのライブラリ活⽤ 「C++はJavaScriptと違ってメモリ管理がいらなくて簡単」 やりたいことと各⾔語の現状次第 単機能Wasmを組み合わせて使えばそれぞれで⾔語選択が可能 なぜ WebAssembly ⽣成を Go

    にしたのか(WebRTC E2EEの話) 「Pure Goで信頼できる暗号化ライブラリがある」など Apache TVM(Rust:推論ランタイム on Wasm)とかも頑張ってる Wasmそのものはまだまだ遅いのでSIMD/Thread/WebGPUなどの発展に期待 21