webとモバイルアプリの逆転

webアプリとモバイルアプリの開発に関する話や本・ゲームなどの趣味の話を雑多にしていきたい

Chrome拡張+WebWorkerでTensorflow.jsを動かそう!

この記事はJavaScript Advent Calender2の11日目の記事です。

qiita.com

初めてのAdvent Calender参加でワクワクしています・・・!
この記事ではTensorflow.jsをChrome拡張+Web Workerで動かすためにやったこととTensorFlow.jsを使ってみての感想を書きたいと思います。

対象としては、Chrome拡張を少し触ったことがある人・Tensorflow.jsを少し触ったことある人が両方を組み合わせるためにはどうするかがわかるようになるということを考えてます。

はじめに

深層学習を使うことで高度な機能も簡単にできるようになったことで、webアプリに組み込んで便利な機能を作りたいという衝動が抑えきれなくなってくることはないですか? 私はあります。
抑えきれない衝動を何にむけようかな?と考えた際に、最近の悩みを機械学習で解決しようともくろみました。Twitterでいいイラストが流れてきたのでその人のメディア欄を見に行ったら飯テロ画像をたくさん見て苦しむという悩みを思い出しました。 そこで、機械学習の力で画像の種類を特定し見たい画像だけを表示するchrome拡張を作るためにTensorFlow.jsを組み込んで画像の種類を特定して飯テロ画像を見ないようになるアプリを作って遊んでみました。

動作させるとこんな感じになりました。最初は飯画像だけ消そうと思ったのですが、ついでにソシャゲのスクショも消せるようにしてみました。猫ちゃんのイラストだけが残っています。

f:id:s-haya:20191210005509g:plain
動作画面

ストアに公開しようかと思ったけども、TwitterのウェブアプリがVirtualScrollになっているため、スクロールすると消した画像が復活するということが起きてしまい、あまり実用的でないなと思ってまだ公開はしてないです。 自分で使うぶんにはTimerで監視して定期的に削除するって処理を加えて使ってます。ただ割と重たくなってしまってるのでうーん・・・って感じです。

作ってみて学んだことやはまったポイントをまとめて共有できたらなと思っています。

Tensorflow.jsって?

深層学習といえばPythonというイメージが強いと思うのですが、Tensorflow.jsを使えばなんとブラウザ上で深層学習ができます!

特に良いと思っている点としてKerasのモデルを変換する仕組みが公式で説明されているので、Kerasで公開されているor自作のモデルをwebに組み込んて使うことができ応用がきくところが素晴らしいなと思っています。最近はAutoMLでTensorflow.jsで使うようにモデル出力、インポートまでできるようになっているみたいで更に敷居が下がってほんとに良いなと思います。

TensorFlow.jsどう?

Web Workerでもwebgl backend で高速に動作するようになってメインスレッドを専有せずに使うことができるようになってるのでかなり実用的になってるなと思います。(まだchromeだけみたいですが)

実用的になってるなとは思う一方で使い所も難しいなと思っていて、画像解析のような一回だけ演算して結果を得ればいいようなユースケースではあまりメリットを享受できないなと感じています。そのような場合はGPUましましのサーバーで処理するようなシステムを構築したほうがマシンパワーゴリ推しで高速演算できてよい気がします。

ビデオなどリアルタイムな結果が欲しいアプリではネットワークを経由せずに結果を取得できるので活用できると思っていて、そういったユースケースで使われていくと面白いなあって思います。
あと個人開発みたいなできるだけサーバー代を節約してアプリを開発したい場合でも活用できると思っています。

より実用的にするためにはモデルの軽量化や高速化の知識が必要そうな気配がぷんぷんするので機械学習の知識もつけなきゃな・・・と思っています。

chrome拡張でTensorflow.jsを動かすうえではまったところ

チュートリアルを試してみてすんなり実装できそうだなと思ってたけども意外とつまってしまうことがありました。 大きく詰まったのは以下の3点です。

  • backgroundでGPUが使えなかった
  • chrome拡張では画像のDomを直接Tensorflow.jsのモデルに入力できない
  • WebWorkerをファイルから読み込みを指定して動かせない

それぞれについて、少しだけ内容に触れていきたいと思います。

backgroundでGPUが使えない

Chrome拡張ではbackgroundというタブを閉じる、作成などのイベントを検知するための環境が提供されています。( https://developer.chrome.com/extensions/background_pages

backgroundに各ページがどのような通信をしたのかを検知できるAPIがあったので、APIで画像データの通信を監視して画像を取得するたびにモデルを動かして画像の種類を予測するというやり方を最初行おうとしました。 ところが、backgroundでモデルを動かしたところGPUを使えずcpuでの演算になってしまい低速になってしまいました。

そのためbackgroundではなくwebWorkerで動かすようにして、メイン処理を止めることなく予測するようにしました。

chrome拡張ではHTMLImageElementを直接Tensorflow.jsのモデルに入力できない

Tensorflow.jsでは画像データをTensorに変換するAPIとしてtf.browser.fromPixelsが提供されており、この入力データの形式の一つにHTMLImageElementがあるので、例えば以下のようなコードで画像データをモデルに入れる形式に変換することができます。

const img = document.getElementById('hoge');
tf.browser.fromPixels(img).print();

これと同じようなことをChrome拡張でやろうとしたところ、以下のようなエラーが出てしまいます。

Uncaught (in promise) DOMException: Failed to execute 'texImage2D' on 'WebGL2RenderingContext': Tainted canvases may not be loaded.

これはWebGLで異なるオリジンを読み込む際にCORSが設定されていないため起きてしまう問題のようです。

そのため、CORSの設定を行うかHTMLImageElementを作成して変換して使うという処理が必要みたいでした。 今回はWebWorkerを使おうとしていてどちらにせよ画像データを作成する必要があったので後者の方法を採用しました。(WebWorkerではElementにアクセスできないため)
自分はWebWorker内でOffscreenCanvasに描画してtensorに変換する処理を行いました。下記のコードでcanvasをtf.browser.fromPixelsに指定すればTensorに変換できます

const img = '' // 画像のURL
const imgBlob = await (await fetch(img)).blob();
const bitmap = imgBlob.type.match(/svg/) ? undefined : await createImageBitmap(imgBlob);
if(bitmap) {
  const canvas = new OffscreenCanvas(bitmap.width,bitmap.height);
  const c = canvas.getContext('2d');
  c && c.drawImage(bitmap,0,0);
}

WebWorkerをファイルから指定して動かせない

これがなかなかうまく動かせなくてかなり詰まったポイントでした・・・

WebWorkerを作るためには通常以下のようにファイルを指定して作成すると思います。

const worker = new Worker('hoge.js', {type: 'module'});
・・・

chrome拡張で他のファイルを読み込むためには通常のパスではなく、独自のパスを指定する必要があります。 手順としては以下のようになっています。

  1. "web_accessible_resources"に対象のファイルを指定する
  2. chrome拡張のファイル拡張子を付与するAPIであるchrome.extension.getURLを使ってファイルURLを指定する

これで取得したURLを使えば通常の画像やファイルを扱うことができます。 なので、以下のコードで動くはず・・・だったのですが、、、現実は非常、動きませんでした。

const worker = new Worker(chrome.extension.getURL('hoge.js'), {type: 'module'});
・・・

以下のようなエラーが出されてしまいました。

bundle.js:1 Uncaught (in promise) DOMException: Failed to construct 'Worker': Script at 'ワーカーのパス' cannot be accessed from origin 'URL'.

どうもChrome側のバグみたいで、アクセスできないリソースを指定しているというふうにみなされてしまっているみたいです。 159303 - chromium - An open-source project to help move the web forward. - Monorail

しょうがないのでjsのファイルをダウンロードしてURL形式に変換するようにしたらWebWorkerが動くようになりました。

const worker = await fetch(chrome.extension.getURL('worker.js'));
const js = await worker.text();
const blob = new Blob([js], {type: "text/javascript"});
const url = URL.createObjectURL(blob)
const workerClass: any = comlink.wrap(new Worker(url));

これで、あとは普通のWebWorkerを扱うようにできました。

このやり方でいいのか悩ましいのでよりよい方法があったらぜひとも教えていただきたいです。

まとめ

一応作ったコードを貼っておきます。 https://github.com/s-haya-123/showIllustOnlyExtention/tree/master/src

ただ、整理は全くしてないので可読性は保証できないですが・・・
記事では触れてなかったのですがWebWorkerを使う方法としてComlinkを使ってみたところかなり使い勝手がよかったです。 WebWorkerはMessagePassingが扱いづらいからな・・・って思ってたけども、これだったら今後も積極的に採用してもよいなと思いました。

Tensorflow.js、ブラウザで高度なことが次々試せそうなので今後もどう使っていくかを考えていきたいと思ってます。