WebカメラからJSで画像処理するひな形

# 概要

最近のブラウザだとWebカメラからの映像取得が簡単にできるようになっている。

この映像を毎フレームごとに画像処理したい場合、以下のように処理用の配列に変換する流れが一般的っぽい。

<img src="https://qiita-image-store.s3.amazonaws.com/0/28988/e6d8de32-0692-b709-d945-7ce608a3211c.png" width="33%">

MDNに以下2つの良いサンプルがあったので組み合わせると良さそう。

### Webカメラ → `<video>`

以下はWebカメラからMediaStreamを取得してvideo要素に読ませているサンプル

* https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/getUserMedia

### `<video>` → `<canvas>`→`ImageData`

以下はvideo要素からクロマキー合成とかやってるデモプログラム

* https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Manipulating_video_using_canvas


# ソースコード

以下のソースコードはMDNにあったコードを繋げてアレンジしたもの。
せっかく動画を使うので、数行追加する程度の簡単なデモとして、1フレーム前との差分を取ってみた。
動かす場合、ローカルかHTTPS環境でないとWebカメラにアクセスできないので注意。

<details><summary>HTML部分(クリックで展開)</summary><div>

```html:index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Webカメラから画像処理</title>
</head>
<body>
    <video width="160" height="120" autoplay></video>
    <div>
        <canvas id="c1" width="80" height="60"></canvas>
        <canvas id="c2" width="80" height="60"></canvas>
        <canvas id="c3" width="80" height="60"></canvas>
    </div>
      <script src="webcam.js"></script>
</body>
</html>
```

</div></details>


<details><summary>JavaScript部分(クリックで展開)</summary><div>

```javascript:webcam.js

const constraints = { audio: false, video: { width: 160, height: 120 } }; 

navigator.mediaDevices.getUserMedia(constraints)
.then(function(mediaStream) {
  const video = document.querySelector('video');
  video.srcObject = mediaStream;
  video.onloadedmetadata = function(e) {
    video.play();
  };
})
.catch(function(err) { console.log(err.name + ": " + err.message); }); // always check for errors at the end.


// https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/getUserMedia



let processor = {
    timerCallback: function() {
      if (this.video.paused || this.video.ended) {
        return;
      }
      this.computeFrame();
      let self = this;
      setTimeout(function () {
          self.timerCallback();
        }, 0);
    },
  
    doLoad: function() {
      this.video = document.querySelector("video");
      this.c1 = document.getElementById("c1");
      this.ctx1 = this.c1.getContext("2d");
      this.c2 = document.getElementById("c2");
      this.ctx2 = this.c2.getContext("2d");
      this.c3 = document.getElementById("c3");
      this.ctx3 = this.c3.getContext("2d");
      let self = this;
      this.video.addEventListener("play", function() {
          self.width = self.video.videoWidth / 2;
          self.height = self.video.videoHeight / 2;
          self.timerCallback();
        }, false);
    },
  
    computeFrame: function() {
      let frame1 = this.ctx1.getImageData(0, 0, this.width, this.height);
      this.ctx2.putImageData(frame1, 0, 0);
      this.ctx1.drawImage(this.video, 0, 0, this.width, this.height);
      
      
      frame1 = this.ctx1.getImageData(0, 0, this.width, this.height);
      let frame2 = this.ctx2.getImageData(0, 0, this.width, this.height);

      let l = frame1.data.length / 4;
  
      diffFrame = this.ctx3.createImageData(this.width, this.height);

      for (let i = 0; i < l; i++) {
        let r = frame1.data[i * 4 + 0] - frame2.data[i * 4 + 0];
        let g = frame1.data[i * 4 + 1] - frame2.data[i * 4 + 1];
        let b = frame1.data[i * 4 + 2] - frame2.data[i * 4 + 2];
        diffFrame.data[i * 4 + 0] = Math.abs(r);
        diffFrame.data[i * 4 + 1] = Math.abs(g);
        diffFrame.data[i * 4 + 2] = Math.abs(b);
        diffFrame.data[i * 4 + 3] = 255;
      }

      this.ctx3.putImageData(diffFrame, 0, 0);

      return;
    }
  };

document.addEventListener("DOMContentLoaded", () => {
  processor.doLoad();
});
  // https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Manipulating_video_using_canvas

```

</div></details>


# 実用

Webカメラを動かすと3つ目(一番右)のCanvasにフレーム間差分が見える。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/28988/663051b4-eb1e-de12-1212-e042cb9dd845.png)


Canvasにしてしまえば、各フレームに対する画像処理したり、適当な瞬間のフレームを保存したりとかができるはず。`cv.js`とかにも使えそうなので、とにかく煮たり焼いたりなんでもできそう。

何か面白いことをやってみたいけど現状考え中。