OpenCV.jsとWebカメラ画像表示

OpenCV.jsでのWebカメラの取り扱いは[OpenCVのサイト](https://docs.opencv.org/3.4/dd/d00/tutorial_js_video_display.html)にも記載されているが、メモとしてこちらにも記載。OpenCV.jsの場合はWebカメラを直接扱ってくれるわけではなく、自分でgetUserMediaを扱う必要がある。
getUserMediaを使ったWebカメラ画像の表示については、Googleのサイト[Web Fundamentals](https://developers.google.com/web/fundamentals/media/capturing-images)がわかりやすい。
カメラのアクセスにはlocalhostもしくは、https接続が必要となる。

2021/09/10: iOS対応のためにはvideoタグに playsinline muted が必要だったため追記。

## サンプルコード１（単純にWebカメラ画像を表示）
Googleのサイト[Web Fundamentals](https://developers.google.com/web/fundamentals/media/capturing-images)のコードそのまま。

```html:getUserMedia.html
<video id="player" controls playsinline muted autoplay></video>
<script>
  const player = document.getElementById('player');

  const constraints = {
    video: true,
  };

  navigator.mediaDevices.getUserMedia(constraints)
    .then((stream) => {
      player.srcObject = stream;
    });
</script>
```
上記（とほぼ同じコード）は[ここ](https://mktshhr.github.io/webTest/getUserMedia.html)にホストしてある。

## サンプルコード２（canvasから画像を取得・canvasに表示）
`src = cv.imread('canvas')`でcanvasから画像を取得して、`cv.imshow("canvas2",dst)`でcanvasに画像を表示している部分がJavaScriptらしいところ。

ここではOpenCV.jsを持ってきて利用している。ロードは同期的に行った。

```html:grabSnapshot.html
<!DOCTYPE html>
<html>
    <body>
        <video id="player" controls playsinline muted autoplay></video>
        <button id="capture">Capture</button>
        <canvas id="canvas" width=320 height=240></canvas>
        <canvas id="canvas2" width=320 height=240></canvas>
        <script src=https://docs.opencv.org/4.5.1/opencv.js></script>
    </body>
</html>
<script>
  const player = document.getElementById('player');
  const canvas = document.getElementById('canvas');
  const context = canvas.getContext('2d');
  const captureButton = document.getElementById('capture');

  const constraints = {
    video: true,
  };

  captureButton.addEventListener('click', () => {
    // Draw the video frame to the canvas.
    context.drawImage(player, 0, 0, canvas.width, canvas.height);

    // OpenCVでグレーに変換、表示
    let src = cv.imread('canvas');
    let dst = new cv.Mat();
    cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY, 0);
    cv.imshow("canvas2",dst)
    src.delete()
    dst.delete()
  });

  navigator.mediaDevices.getUserMedia(constraints)
    .then((stream) => {
      player.srcObject = stream;
    });
</script>
```
上記（とほぼ同じコード）は[ここ](https://mktshhr.github.io/webTest/grabSnapshot.html)にホストしてある。

## サンプルコード３（cv.VideoCaptureの例）

cv.VideoCaptureを使うとvideo要素から画像を取得できるため、サンプルコード２で
`context.drawImage(player, 0, 0, canvas.width, canvas.height);`のようにいったんcanvasに書き込んで、`src = cv.imread('canvas')`で読み込んでいた部分が不要になる。
setTimeoutで定期的にprocessVideoを呼び出してOpenCVでフレームの処理を行っている。
ここではOpenCV.jsのロードは非同期的に行った。
OpenCV.jsの読み込みが完了したこと(isCvLoaded==true)と、getUserMediaのvideoの取り込みの準備が終わったこと(onVideoCanPlay呼び出し)を確認してから`cap = new cv.VideoCapture(player)`を行わないと、`cap.read(src)`で正しく画像が得られず、画像処理が行えない（時々canvasが黒画面になる問題に遭遇した）。

```html:videoCapture.html
<!DOCTYPE html>
<html>
    <body>
        <video id="videoInput" controls playsinline muted autoplay></video>
        <canvas id="canvasOutput"></canvas>
        <script async src=https://docs.opencv.org/4.5.1/opencv.js onload="onOpenCvReady()"></script>
    </body>
</html>
<script>
    const player = document.getElementById('videoInput');
    let src = null;
    let dst = null;
    let cap = null;
    let isCvLoaded = false

    function onOpenCvReady() {
        // OpenCV.jsのロードが終わったら呼ばれる
        if (cv.getBuildInformation)
        {
            console.log(cv.getBuildInformation());
            onloadCallback();
        }
        else
        {
            // WASM
            cv['onRuntimeInitialized']=()=>{
                console.log(cv.getBuildInformation());
                onloadCallback();
            }
        }
    };

    function onloadCallback() {
        // OpenCV.jsのロード完了
        isCvLoaded = true;
    };

    const constraints = {
        video: true,
    };

    // Attach the video stream to the video element and autoplay.
    navigator.mediaDevices.getUserMedia(constraints)
        .then((stream) => {
            player.srcObject = stream;
            player.addEventListener('canplay', onVideoCanPlay, false);
        });

    function onVideoCanPlay() {
        // ビデオ再生が可能になった
        player.width = player.videoWidth // width, heightを設定しないとcap.read(src)で失敗する。
        player.height = player.videoHeight
        setTimeout(processVideo, 100);
    };

    const FPS = 30;
    function processVideo() {
        try {
            if(!isCvLoaded){
                setTimeout(processVideo, 100);
                return;
            }else if(cap==null){
                // OpenCVのロードが終わり、Videoのフレームがくるのを待つ
                cap = new cv.VideoCapture(player);
            }
            let begin = Date.now();

            // start processing.
            src = new cv.Mat(player.height, player.width, cv.CV_8UC4);
            dst = new cv.Mat();
            cap.read(src);
            cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY);
            cv.imshow('canvasOutput', dst);
            src.delete();
            dst.delete();

            // schedule the next one.
            let delay = 1000/FPS - (Date.now() - begin);
            setTimeout(processVideo, delay);
        } catch (err) {
            console.error(err.message);
        }
        return;
    };
</script>
```
上記（とほぼ同じコード）は[ここ](https://mktshhr.github.io/webTest/videoCapture.html)にホストしてある。


nodeで動作させるには[opencv4nodejs](https://www.npmjs.com/package/opencv4nodejs)もあるが、手元の環境(macOS)ではうまくセットアップできず、OpenCV.jsを使って試した。