みかづきブログ・カスタム

基本的にはちょちょいのほいです。

OpenCV.jsを使ってカメラで撮影した映像のフレーム差分を算出し、動いているもののみを表示する 📷

まだ、完璧ではないのですが、カメラの映像の中から動いているものだけを表示するスクリプトを書きました。

全部で9個canvasがありますが、役割は下記の通りです。

9個の小さなCanvasを左上からA、B、C、つまり、

A B C
D E F
G H I

と呼んだとき、

A. 現在(カラー)
B. 1フレーム前(カラー)
C. 2フレーム前(カラー)
D. 現在(白黒)
E. 1フレーム前(白黒)
F. 2フレーム前(白黒)
G. DとEの差分
H. EとFの差分
I. GとHの論理積を白黒反転させたもの

をレンダリングしています。

そして、大きなCanvasには、

BとIを合成したものをレンダリングしています。

DEMO

kimizuka.github.io

※ スマホ向けにスタイルを書いているので、PCで観覧する際は画面幅をいい感じに狭めてください

ソースコード

index.html(抜粋)

<div>
  <video id="video" autoplay playsinline muted></video>
</div>
<div>
  <canvas id="c1"></canvas>
  <canvas id="c2"></canvas>
  <canvas id="c3"></canvas>
</div>
<div>
  <canvas id="m1"></canvas>
  <canvas id="m2"></canvas>
  <canvas id="m3"></canvas>
</div>
<div>
  <canvas id="diff-1"></canvas>
  <canvas id="diff-2"></canvas>
  <canvas id="diff"></canvas>
</div>
<div>
  <canvas id="dist"></canvas>
</div>

index.css(抜粋)

video {
  position: absolute;
  width: 100%;
  opacity: 0; // videoを非表示にするとiOSで動かなくなるので透明にしておく
  pointer-events: none;
}

canvas {
  box-sizing: border-box;
  border: solid red 1px;
  pointer-events: none;
}

#dist {
  width: 100%;
}

index.js

const Module = {
  onRuntimeInitialized() {
    const medias = {
      audio: false,
      video: {
        facingMode: 'user'
      }
    };
    const promise = navigator.mediaDevices.getUserMedia(medias);

    promise.then(successCallback).catch(errorCallback);
  }
};

function successCallback(stream) {
  const imgLength = 3;
  const videoMatList = [];
  const blackAndWhiteMatList = [];
  const diffMatList = [];
  const FPS = 4; // FPSを上げすぎると1フレームではあまり差分が発生しない
  const video = document.getElementById('video');
  const canvasList = [].slice.call(document.querySelectorAll('canvas'));
  const contextList = canvasList.map((canvas) => canvas.getContext('2d'));

  video.oncanplay = () => {
    const width = video.clientWidth / 4; // 適当にリサイズ
    const height = video.clientHeight / 4; // 適当にリサイズ
    const bitwiseMat = new cv.Mat(height, width, cv.CV_8UC1);
    const distCanvas = document.getElementById('dist');
    const distCtx = distCanvas.getContext('2d');

    for (let i = 0; i < imgLength; ++i) {
      videoMatList.push(new cv.Mat(height, width, cv.CV_8UC4));
      blackAndWhiteMatList.push(new cv.Mat(height, width, cv.CV_8UC1));
    }

    canvasList.forEach((canvas) => {
      canvas.width = width;
      canvas.height = height;
    });

    processVideo();

    function processVideo() {
      const begin = Date.now();

      contextList[0].drawImage(video, 0, 0, width, height);

      videoMatList[1].copyTo(videoMatList[2]); // 2フレーム前
      videoMatList[0].copyTo(videoMatList[1]); // 1フレーム前
      videoMatList[0].data.set(contextList[0].getImageData(0, 0, width, height).data); // 現在

      for (let i = 0; i < videoMatList.length; ++i) {
        // グレースケールにする
        cv.cvtColor(videoMatList[i], blackAndWhiteMatList[i], cv.COLOR_RGB2GRAY);
        cv.imshow(`c${ i + 1 }`, videoMatList[i]);
        cv.imshow(`m${ i + 1 }`, blackAndWhiteMatList[i]);
      }

      for (let i = 0; i < videoMatList.length - 1; ++i) {
        diffMatList.push(new cv.Mat(height, width, cv.CV_8UC1));

        // 差分を取る
        cv.absdiff(blackAndWhiteMatList[i], blackAndWhiteMatList[i + 1], diffMatList[i]);
        cv.imshow(`diff-${ i + 1 }`, diffMatList[i]);
      }

      const dilateSize = 8;

      cv.bitwise_and(diffMatList[0], diffMatList[1], bitwiseMat); // 論理積を取る
      cv.threshold(bitwiseMat, bitwiseMat, 127, 255, cv.THRESH_BINARY); // 白黒にする
      cv.dilate( // 範囲を広めに取る
        bitwiseMat,
        bitwiseMat,
        cv.Mat.ones(dilateSize, dilateSize, cv.CV_8U),
        new cv.Point(dilateSize / 2, dilateSize / 2),
        1,
        cv.BORDER_CONSTANT,
        cv.morphologyDefaultBorderValue()
      );
      cv.erode( // 範囲をやや狭くする
        bitwiseMat,
        bitwiseMat,
        cv.Mat.ones(dilateSize / 4, dilateSize / 4, cv.CV_8U),
        new cv.Point(dilateSize / 8, dilateSize / 8),
        1,
        cv.BORDER_CONSTANT,
        cv.morphologyDefaultBorderValue()
      );
      cv.bitwise_not(bitwiseMat, bitwiseMat); // 白黒反転させる
      cv.imshow('diff', bitwiseMat);

      distCanvas.width = width;
      distCanvas.height = height;

      distCtx.save();
        distCtx.drawImage(document.getElementById('c2'), 0, 0); // 現在(カラー)を描画
        distCtx.globalCompositeOperation = 'lighter'; // 合成方法を指定
        distCtx.drawImage(document.getElementById('diff'), 0, 0); // マスク画像を描画
      distCtx.restore();

      const delay = 1000 / FPS - (Date.now() - begin);

      setTimeout(processVideo, delay);
    }
  };

  video.srcObject = stream;
}

function errorCallback(err) {
  alert(err);
};