Google Colaboratoryで外付けカメラを使う

## はじめに
今回は、Google Colaboratory(無料でGPUを提供してくれる神様)で外付けカメラを使う方法を紹介したいと思います.

CPUしか使えないPCの強き味方でお馴染みのColabですが、
Colab環境下で内蔵カメラ、外付けカメラを使うには一癖あります.
以前、少し苦闘したので、お役に立てたら幸いです.

え？PythonならOpenCVをインポートして
```
cap = cv2.VideoCapture(0)
```
「この１行で解決でしょ！」
そう思ったあなた！天才！！！！
でも実はColabではうまく実行してくれません, Oh...

Colabでカメラを使うには**JavaScript**が必要になります.

「JavaScriptわからないよ〜〜〜助けてどら○も〜〜〜〜ん」
大丈夫！
Colab君が、**内蔵カメラのフレームを取得するコードを提供**しているので
それをちょこっと書き換えれば、外付けカメラを使うことができます.

## 環境
始める前に・・・

:::note alert
SafariでColabを使わない！
:::

⇦Safariは、今回使うコードの一部、Navigator.getUserMedia()メソッドに
対応していないからです.
そうとは知らずにSafariを使い続け、時間を溶かしました.

|ブラウザ|互換性|
|------|-|
|Chrome|○|
|Edge|○|
|FireFox|○|
|Opera|○|
|Safari|×|


## 目次
・Colabが提供しているコードを見てみよう
・外付けカメラのIDを取得しよう
・実行してみよう
・おまけ
&ensp;-

## Colabが提供しているコードを見てみよう

#### コードの挿入方法

<details><summary>「そもそも、そのコードはどこにあるのさ！」という方向け👈</summary>
順を追って説明します！

![スクリーンショット 2023-10-01 2.38.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530344/cc67dde2-082e-b990-0da9-0b7bc1442e34.png)
    ① 画面左下の`＜＞`をクリック
    ② `Camera Capture`をクリック
    ③ `挿入`をクリック
これでOK！
</details>

#### コードの中身

```python
from IPython.display import display, Javascript
from google.colab.output import eval_js
from base64 import b64decode

def take_photo(filename='photo.jpg', quality=0.8):
  js = Javascript('''
    async function takePhoto(quality) {
      const div = document.createElement('div');
      const capture = document.createElement('button');
      capture.textContent = 'Capture';
      div.appendChild(capture);

      const video = document.createElement('video');
      video.style.display = 'block';
      const stream = await navigator.mediaDevices.getUserMedia({video: true});

      document.body.appendChild(div);
      div.appendChild(video);
      video.srcObject = stream;
      await video.play();

      // 画面を調整
      google.colab.output.setIframeHeight(document.documentElement.scrollHeight, true);

      // キャプチャボタンが押されるまで待機
      await new Promise((resolve) => capture.onclick = resolve);

      const canvas = document.createElement('canvas');
      canvas.width = video.videoWidth;
      canvas.height = video.videoHeight;
      canvas.getContext('2d').drawImage(video, 0, 0);
      stream.getVideoTracks()[0].stop();
      div.remove();
      return canvas.toDataURL('image/jpeg', quality);
    }
    ''')
  display(js)
  data = eval_js('takePhoto({})'.format(quality))
  binary = b64decode(data.split(',')[1])
  with open(filename, 'wb') as f:
    f.write(binary)
  return filename
```

```python
from IPython.display import Image
try:
  filename = take_photo()
  print('Saved to {}'.format(filename))
  
  # キャプチャしたフレームを表示
  display(Image(filename))
except Exception as err:

  # 下記に該当する場合、エラー表示：
  # ・ウェブカメラを持っていない
  # ・ページにアクセスの許可を承諾していない
  print(str(err))
```

JavaScript側とPython側の処理をざっくりとまとめると

js側：内蔵カメラの映像とcaptureボタンを表示
&emsp;&emsp;&ensp;&nbsp;captureボタンを押した時の画像を切り取ってPython側に渡す
py側：受け取ったフレームを表示

このコード内で、外付けカメラを使うために変更すべき箇所はJavaScript内のこの部分です.
```javascript
const stream = await navigator.mediaDevices.getUserMedia({video: true});
```
`{video: true}`
ここでは、使用するデバイスを引数で指定しています.
カメラを使いたいので`video`の部分はOKですが
`true`だと、デフォルトで内蔵カメラを指してしまいます.

今回は内蔵カメラではなく外付けカメラを使いたいので、
**外付けカメラのIDを取得して、`true`部分に置換する必要**があります.

「じゃあ、どうやってカメラのIDを調べるのさ！」↓

## 外付けカメラのIDを取得しよう
どうやるんだい！？
まずは、js内に下記のコードを追加します.
```javascript
if (!navigator.mediaDevices?.enumerateDevices) {
        console.log("enumerateDevices() not supported.");
      } else {
        // 利用できるカメラやマイクのID一覧を表示
        navigator.mediaDevices
          .enumerateDevices()
          .then((devices) => {
            devices.forEach((device) => {
              console.log(`${device.kind}: ${device.label} id = ${device.deviceId}`);
            });
          })
          .catch((err) => {
            console.error(`${err.name}: ${err.message}`);
          });
      }
```
<details><summary>コード追加後(どこに追加すればいいのかわからない方向け)👈</summary>

```python
from IPython.display import display, Javascript
from google.colab.output import eval_js
from base64 import b64decode

def take_photo(filename='photo.jpg', quality=0.8):
  js = Javascript('''
    async function takePhoto(quality) {
      const div = document.createElement('div');
      const capture = document.createElement('button');
      capture.textContent = 'Capture';
      div.appendChild(capture);

##########追記部分#############################################
    if (!navigator.mediaDevices?.enumerateDevices) {
        console.log("enumerateDevices() not supported.");
    } else {
        navigator.mediaDevices
          .enumerateDevices()
          .then((devices) => {
            devices.forEach((device) => {
              console.log(`${device.kind}: ${device.label} id = ${device.deviceId}`);
            });
          })
          .catch((err) => {
            console.error(`${err.name}: ${err.message}`);
          });
    }
##############################################################

      const video = document.createElement('video');
      video.style.display = 'block';
      const stream = await navigator.mediaDevices.getUserMedia({video: true});

      document.body.appendChild(div);
      div.appendChild(video);
      video.srcObject = stream;
      await video.play();

      // Resize the output to fit the video element.
      google.colab.output.setIframeHeight(document.documentElement.scrollHeight, true);

      // Wait for Capture to be clicked.
      await new Promise((resolve) => capture.onclick = resolve);

      const canvas = document.createElement('canvas');
      canvas.width = video.videoWidth;
      canvas.height = video.videoHeight;
      canvas.getContext('2d').drawImage(video, 0, 0);
      stream.getVideoTracks()[0].stop();
      div.remove();
      return canvas.toDataURL('image/jpeg', quality);
    }
    ''')
  display(js)
  data = eval_js('takePhoto({})'.format(quality))
  binary = b64decode(data.split(',')[1])
  with open(filename, 'wb') as f:
    f.write(binary)
  return filename
```
</details>

このコードを実行すると、デバイスIDがデベロッパーツールのコンソールに出力されます.実行して確認してみましょう！

<details><summary>どこにデベロッパーツールがあるかわからないという方向け👈</summary>

MacOS, Chromeの場合：
&emsp;①画面上部の`表示`をクリック
&emsp;②`開発/管理`にカーソルを当てる
&emsp;③`デベロッパーツール`をクリック

windowsや他のブラウザの方はご自身で調べていただけるとありがたいです！🙇
</details>

![スクリーンショット 2023-10-01 5.39.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530344/12f2812e-01df-2072-f524-e908e3d7ceb6.png)

矢印部分に外付けカメラのIDが出力されています！

デバイスIDを取得できたところで、
次は、**どうやってデバイスIDをコードに組み込むのか？** ですね.

#### デバイスIDを指定
コードはこのようになります.

変更前
```javascript
const stream = await navigator.mediaDevices.getUserMedia({video: true});
```
変更後
```javascript
const stream = await navigator.mediaDevices.getUserMedia(
        video: {
          deviceId: { 
            exact: "416d352c2e3efdf7c92b3ae3f777a7cc4936bd6e153ef35882236911daffb2f6"
          }
        }
      });
```
あるいは、下記のように、新たに変数を宣言して分けても大丈夫です.
```javascript
var constraints = {
        video: {
          deviceId: { 
            exact: "416d352c2e3efdf7c92b3ae3f777a7cc4936bd6e153ef35882236911daffb2f6"
          }
        }
      };
const stream = await navigator.mediaDevices.getUserMedia(constraints);
```
取得したデバイスIDを" "(ダブルクオーテーション)で囲み、
`exact:` の後に書くことで使用するデバイスを指定できます.

これで外付けカメラを使う準備OK！Good！

## 実行してみよう
いざ！！！！！！
[実行結果]
映像映し出し中↓
![スクリーンショット 2023-10-01 17.14.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530344/399938dd-4b8f-1138-8c1a-5c32ad5517cb.png)

Captureボタン押した後↓
![スクリーンショット 2023-10-01 17.14.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530344/e49cf713-254a-6dbb-1b63-6938ce582a1f.png)

OK!
最近ハマってるねり梅ちゃんに出演してもらいました.ありがとう

**エラーが出てしまったら**
```
OverconstrainedError
```
と怒られたら、デベロッパーツールを開いて、コードに書いたデバイスIDとコンソール上のデバイスIDを見比べてみよう！
実は、デバイスIDは時間経過で勝手にIDが違うものに変わる仕様になっています.
pythonに怒られてもあなたは何も悪くないです！

:::note warn
注意
デバイスIDは時間が経つと変わってしまうため、うまく動作しなかった時はデベロッパーツールでIDを確認してみよう.
:::

## おまけ
今回紹介したカメラの機能で、こんなこともできるよ〜というご紹介です.

```python
from IPython.display import display, Javascript
from google.colab.output import eval_js
from base64 import b64decode

def take_photo(filename1='photo1.jpg', filename2='photo2.jpg', quality=0.8):
  js = Javascript('''
    async function takePhoto(quality) {
      const div1 = document.createElement('div');
      const div2 = document.createElement('div');
      const capture = document.createElement('button');
      capture.textContent = 'Capture';
      div1.appendChild(capture);

      // デバイスIDを取得
      if (!navigator.mediaDevices?.enumerateDevices) {
        console.log("enumerateDevices() not supported.");
      } else {
        navigator.mediaDevices
          .enumerateDevices()
          .then((devices) => {
            devices.forEach((device) => {
              console.log(`${device.kind}: ${device.label} id = ${device.deviceId}`);
            });
          })
          .catch((err) => {
            console.error(`${err.name}: ${err.message}`);
          });
      }
      // Camera 1
      const video1 = document.createElement('video');
      video1.style.display = 'block';
      var constraints1 = {
        video: {
          deviceId: {
            exact: // ここにデバイスIDを書く
          }
        }
      };
      const stream1 = await navigator.mediaDevices.getUserMedia(constraints1);


      //Camera 2
      const video2 = document.createElement('video');
      video2.style.display = 'block';
      var constraints2 = {
        video:{
          deviceId: 
                exact: // ここにデバイスIDを書く
        }
      };
      const stream2 = await navigator.mediaDevices.getUserMedia(constraints2);


      document.body.appendChild(div1);
      document.body.appendChild(div2);
      div1.appendChild(video1);
      div2.appendChild(video2);
      video1.srcObject = stream1;
      video2.srcObject = stream2;
      await video1.play();
      await video2.play();

      // 画面調整
      google.colab.output.setIframeHeight(document.documentElement.scrollHeight, true);

    　// captureボタン押されるまで待機
      await new Promise((resolve) => capture.onclick = resolve);

      const canvas1 = document.createElement('canvas');
      canvas1.width = video1.videoWidth;
      canvas1.height = video1.videoHeight;
      canvas1.getContext('2d').drawImage(video1, 0, 0);
      stream1.getVideoTracks()[0].stop();
      div1.remove();

      const canvas2 = document.createElement('canvas');
      canvas2.width = video2.videoWidth;
      canvas2.height = video2.videoHeight;
      canvas2.getContext('2d').drawImage(video2, 0, 0);
      stream2.getVideoTracks()[0].stop();
      div2.remove();

      const canvas = [canvas1.toDataURL('image/jpeg', quality), canvas2.toDataURL('image/jpeg', quality)];
      console.log(canvas1.toDataURL('image/jpeg', quality));
      console.log('2dayo~~~');
      console.log(canvas2.toDataURL('image/jpeg', quality))

      return canvas;
    }
    ''')
  display(js)
  data = eval_js('takePhoto({})'.format(quality))
  print(data)
  binary1 = b64decode(data[0].split(',')[1])
  binary2 = b64decode(data[1].split(',')[1])

  with open(filename1, 'wb') as f:
    f.write(binary1)
  with open(filename2, 'wb') as f2:
    f2.write(binary2)
  return filename1, filename2
```
```python
from IPython.display import Image
try:
  filename1, filename2 = take_photo()
  display(Image(filename1))
  display(Image(filename2))
  
except Exception as err:
  print(str(err))
```
2つのカメラから同時にフレームを取得するコードです.
pythonでマルチスレッドを使わずに2台同時に動かすことができます.
![unnamed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3530344/8a57a840-b78d-380c-c5d9-aaee417dbafb.png)

## 最後に
ここまでお付き合いくださりありがとうございました！

今回の部分少し手こずったので（互換性のないSafariを使っていたのが悪い）、同じように悩む方が少しでも減ったら幸いです.

そういえば、
**colab上でYOLOv5のリアルタイム検出**を実装している記事は、探した限り見つかりませんでした.
要因として、Colab上でYOLOv5はコマンドラインでウェブカメを利用できないことが挙げられます.
ですが、pytorchと今回のフレーム取得を合わせてコーディングすればColab上でも実装できます！！！できました！！！！
またの機会に紹介できればと思います！！！

## 参考サイト
・Navigator.getUserMedia()
&ensp;&ensp;<https://developer.mozilla.org/ja/docs/Web/API/Navigator/getUserMedia>