見出し画像

画像解析して現実世界から3Dモデルを検索してみる(tensorflow.js編)

こんにちは、SRETKSです。専門はBlenderとWebGL、物理演算などを使ったインタラクティブコンテンツの開発です。現在はMESON, inc社が開発・運営している3Dモデルの検索エンジン、「heymesh.com」の開発をお手伝いしています。

heymesh.com」は、CG TraderUnity Asset StoreTurbosquidなど様々なサイトを横断した検索ができ、普段モデリングをしないVRやARのプログラマや企画者の助けになることを目指しています。

テーマ「現実を元に3Dモデルを検索したい」

最初にMESON, incのメンバーから「3Dモデルの検索サービスを始める」と聞いた時から「スマホで写真を撮るだけで現実世界に応じた3Dモデルを検索してくれれば、シーンの構築が楽になるのでは?」と考えてまして、今回はそれを試したいと思います。

実装方法を考える

画像解析に使えるサービスやライブラリは数多くあるため、目的にあわせて検証し最適なものを選択する必要があります。今回は、解析のリアルタイム性が高そうなTensorflow.jsを対象にしてみました。(単に2018年4月に公開されたばかりで使いたかった)

*Tensorflowは初めて触るため、認識違いなどあればご指摘くださいませ。

動作手順を考える

1.スマホのブラウザで特定URLにアクセス ->2 .ブラウザ内でカメラを表示 -> 3.ボタンクリックで画面をキャプチャして解析、というものを想定します。

1.TensorFlow.jsを使ってみる

サンプルコードは(https://github.com/tensorflow/tfjs-examples)に配布されています。今回は、その中の”mobilenet”をベースにしました。

mobilenet/index.js

全体としては、”const mobilenetDemo”でGoogleが配布している学習済みモデル(mobilenet)を読み込み、”predict(imgElement)”で画像データを解析、”getTopKClasses(logits, TOPK_PREDICTIONS)”で./imagenet_classes.jsで定義された物体のリストと照合する流れとなっているようです。つまり、この3箇所だけ把握できればとりあえず動くし、カスタマイズできるのではないかと思います。

const mobilenetDemo

サンプルコードでは、”tf.loadModel(MOBILENET_MODEL_PATH)”でmobilenetを読み込み、解析対象としてid="cat"の画像を送っています。つまり、ここで送る内容をカメラのキャプチャにすれば良さそうです。

const mobilenetDemo = async () => {
  status('Loading model...');

  mobilenet = await tf.loadModel(MOBILENET_MODEL_PATH);

  // Warmup the model. This isn't necessary, but makes the first prediction
  // faster. Call `dispose` to release the WebGL memory allocated for the return
  // value of `predict`.
  mobilenet.predict(tf.zeros([1, IMAGE_SIZE, IMAGE_SIZE, 3])).dispose();

  status('');

  // Make a prediction through the locally hosted cat.jpg.
  const catElement = document.getElementById('cat');
  if (catElement.complete && catElement.naturalHeight !== 0) {
    predict(catElement);
    catElement.style.display = '';
  } else {
    catElement.onload = () => {
      predict(catElement);
      catElement.style.display = '';
    }
  }

  document.getElementById('file-container').style.display = '';
};

predict(imgElement)

ここでは"tf.fromPixels(imgElement).toFloat();"で画像データを配列化し、mobilenet用に各ピクセルのRGB数値変換と画像サイズを調整してから解析しているようです。

async function predict(imgElement) {
  status('Predicting...');

  const startTime = performance.now();
  const logits = tf.tidy(() => {
    // tf.fromPixels() returns a Tensor from an image element.
    const img = tf.fromPixels(imgElement).toFloat();

    const offset = tf.scalar(127.5);
    // Normalize the image from [0, 255] to [-1, 1].
    const normalized = img.sub(offset).div(offset);

    // Reshape to a single-element batch so we can pass it to predict.
    const batched = normalized.reshape([1, IMAGE_SIZE, IMAGE_SIZE, 3]);

    // Make a prediction through mobilenet.
    return mobilenet.predict(batched);
  });

  // Convert logits to probabilities and class names.
  const classes = await getTopKClasses(logits, TOPK_PREDICTIONS);
  const totalTime = performance.now() - startTime;
  status(`Done in ${Math.floor(totalTime)}ms`);

  // Show the classes in the DOM.
  showResults(imgElement, classes);
}

2.スマホのカメラを解析に使う

Tensorflowの流れをなんとなく把握した(つもり)ので、次はスマホのカメラをキャプチャして解析に使う準備をします。

1.HTMLのVideo Elementを作る

今回は、内容を目視確認できることもあって、一度Videoの内容をCanvasにdrawすることにしました。(直接Videoエレメントを使用しても解析できそうな気はする)

<div id="capture" >
  <div id="video_container">
    <video id="video" autoplay playsinline width="224"></video>
  </div>
  <canvas id="video_cvs" width="224" height="224"></canvas>
</div>

2.VideoをキャプチャしてCanvasにdrawする

カメラへの接続とキャプチャに関しては適当にクラスを作りました。

class VideoCapture{

  constructor()
  {
    this.IMAGE_SIZE = 224;

    this.medias = {
      audio :false, 
      video :{
        facingMode:{
          exact:"environment"
        }
      }
    };

    //Video element
    this.video = document.getElementById("video");

    //Videoのキャプチャ用Canvas
    this.video_cvs = document.getElementById("video_cvs");
    this.video_cvs_ctx = this.video_cvs.getContext("2d");

    //Videoのサイズ
    this.video_offset_width = 0;
    this.video_offset_height = 0;
  }

  init = () => {
    navigator.getUserMedia(this.medias, this.successCallback, this.errorCallback);
  }

    successCallback = (stream) => {
      this.video.srcObject = stream;

      this.video.onplaying = (e) => {
        this.video_offset_width = this.video.offsetWidth;
        this.video_offset_height = this.video.offsetHeight;
      }  
    }
    
    errorCallback = (err)=> {
      alert(err);
    }

    getData = () => {
      let x = (this.video_offset_width-this.IMAGE_SIZE)/2;
      let y = (this.video_offset_height-this.IMAGE_SIZE)/2;

      this.video_cvs_ctx.drawImage( this.video , -x , -y , this.video_offset_width , this.video_offset_height );
      return	this.video_cvs_ctx.getImageData( 0 , 0 , this.IMAGE_SIZE , this.IMAGE_SIZE );
    }
}
export default new VideoCapture()

まず、”navigator.getUserMedia(this.medias, this.successCallback, this.errorCallback);”でカメラを取得します。this.mediasの”facingMode”は、スマホのフロントとリアどちらのカメラを使用するかを指定するために使います。UIでボタンをクリックすると"getData"を呼び、canvasのImageDataを返します。

3.解析結果をもとにheymeshで検索する

Mobilenetによる解析結果は、候補となる物体の名称(imageset_classes.jsで定義されたclassName)と、信頼性(probability)のオブジェクトとして戻ってきます。ここでは、classNameそのものをheymeshでの検索条件に使用します。

4.動かしてみる

ビルドしたコードをサーバに設置して実際に試してみます。*iOSのブラウザからカメラにアクセスするには、https環境と、Safariがカメラとマイクへのアクセスを許可している(「設定 -> Safari」で設定可能)必要がありますのでご注意ください。

結果はこれ

5.所感

動画のように、「物体が中央にはっきり映っている時」は、なかなか精度が高いです。ただ、街中などで「複数の物体が混ざっている時」は、かなり謎な答えが返ってきてしまいました。(設定次第で改善するのだろうか・・・自分の理解度の問題もありそう)

あとは、モデルの容量が大きく(mobilenetの0.25で数MB、1.0だと数十MBあった。)最初の読み込みに時間がかかってしまうこともあり、今回の目的にはちょっと合わないかも。

てことで、引き続きもう少し勉強してみます。

参考


この記事が気に入ったらサポートをしてみませんか?