2019/01/16

Canvasでカメラの映像に画像処理フィルターをかけて表示する方法

カメラの映像をリアルタイムに処理しドット絵っぽくしたくて、年末から画像処理学んでいき欲が再燃した。
今回はWebカメラやラップトップパソコンのカメラを使って映像を取得し、適当な画像処理フィルターをかけ、ブラウザに表示する方法をまとめる。(筆者環境はMBP)


基本: カメラの映像をブラウザに表示する


単純に表示するだけの場合は、video要素(HTMLMediaElement)MediaStreamを使う。
<video id="camera"></video>
async function main() {
  const video = document.querySelector("video#camera");
  // MediaStreamを取得する(Promiseが返る)
  const stream = await navigator.mediaDevices.getUserMedia({ video: true });

  video.srcObject = stream;
  // MediaStreamが取得できたら再生する
  video.onloadedmetadata = () => video.play();
}

main();
今回は映像だけを使いたいので、mediaDevices.getUserMedia{video: true}を指定したが、音声も使いたい場合は{video: true, audio: true}のように指定する。

詳細はMediaDevices.getUserMediaを参照してください。
※ 執筆時点では日本語ドキュメントが古いため、英語ドキュメントを読んだほうが良い。

カメラの映像にリアルタイムにフィルターをかける


画像処理フィルターをかけるために、video要素に直接表示するのではなくCanvasを使う。
詳しく説明すると以下のような流れになる。
  1. MediaStreamを使ってカメラから映像を取得する
  2. 一定間隔(60fpsなど)で映像をCanvasに描画する
    • このときに直接ブラウザに表示するCanvasに描画するのではなくオフスクリーンCanvasを使うと高速化が図れる
    • オフスクリーンを使わないと1フレームに2回描画されるため
  3. オフスクリーンCanvasに画像処理フィルターをかける
  4. ブラウザ表示用のCanvasに描画する

以降、具体的な実装方法を説明する。



Canvasを使ってカメラの映像を表示する


<canvas id="canvas"></canvas>
async function main() {
  // 表示用のCanvas
  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");
  // 画像処理用のオフスクリーンCanvas
  const offscreen = document.createElement("canvas");
  const offscreenCtx = offscreen.getContext("2d");
  // カメラから映像を取得するためのvideo要素
  const video = document.createElement("video");

  const stream = await navigator.mediaDevices.getUserMedia({
    video: true
  });

  video.srcObject = stream;
  // streamの読み込み完了
  video.onloadedmetadata = () => {
    video.play();

    // Canvasのサイズを映像に合わせる
    canvas.width = offscreen.width = video.videoWidth;
    canvas.height = offscreen.height = video.videoHeight;

    tick();
  };

  // 1フレームごとに呼び出される処理
  function tick() {
    // カメラの映像をCanvasに描画する
    offscreenCtx.drawImage(video, 0, 0);

    // イメージデータを取得する([r,g,b,a,r,g,b,a,...]のように1次元配列で取得できる)
    const imageData = offscreenCtx.getImageData(0, 0, offscreen.width, offscreen.height);
    // imageData.dataはreadonlyなのでfilterメソッドで直接書き換える
    filter(imageData.data);

    // オフスクリーンCanvasを更新する
    offscreenCtx.putImageData(imageData, 0, 0);

    // 表示用Canvasに描画する
    ctx.drawImage(offscreen, 0, 0);

    // 次フレームを処理する
    window.requestAnimationFrame(tick);
  }

  function filter(data) {
    // 画像処理を行う
  }
}

main();

動画や音声のメタデータの読み込みが終わったら、onloadedmetadataイベントでvideo.play()で再生する。このとき、Canvasのサイズを設定する。注意事項としてwidthheightはvideo要素のサイズなので、映像自体のサイズを取得するときは、videoWidthvideoHeightプロパティを見る必要がある。

Canvasのサイズ指定が終わったらtickメソッドを呼び出す。

tickメソッド内では、毎フレーム、オフスクリーンCanvasに映像を描画→画像処理フィルタ→表示用Canvasに描画をする。このときrequestAnimationFrameを使って約60fpsで実行する。

getImageDataメソッドを使って画像情報を取得する。中身は[r,g,b,a,r,g,b,a,....]と画像の左上から1ピクセルずつRGBA(Red, Green, Blue, Alpha:透過)がフラットに一次元配列に格納されている。この値を書き換えて、再度Canvasに描画し直すことで画像処理フィルターをかけることができる。

オフスクリーンCanvasの具体的な使い方は、以下の記事を参照してほしい。
filterメソッド内で画像処理を行うのだが、具体的な実装方法やアルゴリズムは以下の記事にまとめているので参照してほしい。


そんなこんなで実際にfilter部分をいい感じに実装すると以下のようなことができる。



以上

written by @bc_rikko

0 件のコメント :

コメントを投稿