リモートの環境のPythonからOpenCVを使い、手元のWebカメラの映像を読みたい


概要
-------

手元のマシンについてるカメラを使って、リモートマシンのパワーで色々やりたい感じの時に使えると思います。

Windows同士ならUSBデバイスを共有すればできそうとか、1フレームずつソケットで送るとか、多分他にうまいやり方があると思うのですが、今回はWebカメラを動画として配信することを選びました。あくまで参考まで。

以下、環境と簡単にやることの概要です。

- 手元のマシン：Windows系(Windows 10)
    - （SSHのポートフォワーディングを行う）
    - ffmpegでWebカメラから動画を配信
- リモートのマシン: Linux系(ubuntu 18.04)
    - （LXDE+XRDPを入れてリモートデスクトップに繋ぐ）
    - Python+OpenCVで、手元のマシンの配信を受け取って表示
    - （後は煮るなり焼くなりする）

カッコの中はオプショナルで深くは解説しません。


手順
-------------

### SSHトンネルでリモートポートフォワードを行う

オプショナルだけど書きます。

※手元のマシンがファイアウォールで外からのアクセスを受け付けない制限がある場合に使います。信頼できるネットワークの上でお互いのマシンがあればこの手順は不要です。

リモートから接続させる性質から、SSHトンネルを使っています。

リモートから12345に接続したら、ローカルのlocalhost:12345ポートに繋がるようにします。

SSHコマンドなら以下のような感じです。

```
# 例
ssh -R 12345:127.0.0.1:12345 リモートホスト 
```

私はPoderosa 4のポートフォワーディングツールを使いました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/52527/60227d13-645e-b1fd-81b1-def2c1b66a5f.png)

なお、転送先ホストはIP指定がよいでしょう。localhostだと`::1`(IPv6)になってめんどくさかった記憶があります。


### カメラをキャプチャして配信する

配信と言っても全世界に公開するわけではなく、自分のマシンにアクセスした人に配信する形です。

[ffmpeg](https://www.ffmpeg.org/)がお手軽です。Windows端末ならDirectShowでストリームを取れるので取ります。

```
ffmpeg -list_devices true -f dshow -i dummy
```

以下のようなvideo devicesという形でデバイスの一覧が出ます。 `<=`で示した場所を使います。

```
> [dshow @ 000001b3ea02b0c0] DirectShow video devices (some may be both video and audio devices)
> [dshow @ 000001b3ea02b0c0]  "Microsoft Camera Front"        <= この値を使う
> [dshow @ 000001b3ea02b0c0]     Alternative name "@device_pnp_\\?\display#xxxxxx#xxxxxx&xxxxxx&xxxxxx&xxxxxx#{xxxxxx}\{xxxxxx}"
```

video deviceの名前が分かったら、配信するコマンドを打ちます。
また、ffmpegのオプションはいっぱいで順序性もそこそこあるので、頑張っていきましょう。

https://trac.ffmpeg.org/wiki/StreamingGuide が詳しいです。

```
ffmpeg -s 640x480 -framerate 15 -f dshow -i video="Microsoft Camera Front" -vf scale=320:240 -b:v 2000k -f mpegts "tcp://0.0.0.0:12345?listen=1&tcp_nodelay=1"
```

- SSHポートフォワードが不要な場合、プロトコルにRTPかUDPが使えます。しかし、IP指定に0.0.0.0は使えませんでした。調査不足。。
- `-framerate 15`や`-s 640x480`はdeviceによって対応していない場合があります。エラーが出たら根気強く調整してください
- `-b:v 2000k`は2000kbpsを目安に配信しますよ、という設定です
- `-vf scale=320:240`はvideo filter で縮小してます。多分。
- `-f mpegts "tcp://0.0.0.0:12345?listen=1&tcp_nodelay=1"`の部分で配信してます
    - TCPにしているのはSSHトンネルを使う都合上です。SSHトンネルを使わない場合は他のプロトコルの方が無駄がないでしょう
    - `listen=1`にすると、相手の接続を待ちます。ただ、私の環境ではこの値を入れておかないとエラーで落ちちゃうので設定してます。
    - `tcp_nodelay=1`はデフォルト無効と書いてあった気がするので有効にしています
    - `-f mpegts`だけだとMPEG-2でエンコードされます

RTPの場合、`-f rtp_mpegts rtp://127.0.0.1:1234`という感じにしても動きます。なおMPEG-4になります。

MPEG-2とかMPEG-4なんて嫌だ！俺はx264を使いたいんだ！という場合、以下のようにするとできます。

```
ffmpeg -s 640x480 -f dshow -i "video=Microsoft Camera Front" -vcodec libx264 -preset ultrafast -tune zerolatency -b:v 2000k -g 30 -f mpegts "tcp://127.0.0.1:12345?listen=1&tcp_nodelay=1"
```

- `-g 30`はキーフレームを30フレーム毎に入れます。この場合約1秒毎に映像がリセットされる感じになります。デフォルトは250フレームなので30fpsなら8秒ちょい毎です。同期(?)を取るとき、キーフレームが来るまで待たされるので、これを短くする意味で設定しています。圧縮効率は落ちると思います。
- 他のオプションはなるべく低レイテンシにするものです

ちょっと熱くなりました。こんな感じで頑張ってオプションを読み解いたり組み合わせてみてください。




### PythonからOpenCVを使って配信を取るコードを書く

OpenCVはカメラではなく動画URLや配信URLも受け付けます。rtp,udpもいけます。対応コーデック周りはよくわからない。

とりあえず出すだけなら以下のような感じでイケます。簡単ですね。

``` python
import cv2.cv2 as cv2
cap = cv2.VideoCapture("tcp://127.0.0.1:12345") # 配信のURLを指定

while True:
    k = cv2.waitKey(1)
    if k == 27: # ESC key
        break

    ret, frame = cap.read()
    if not ret:
        continue

    cv2.imshow('Raw Frame', frame)

cap.release()
cv2.destroyAllWindows()
```



### 実行する

以下の順序で実行します。多少なら前後しても問題ないと思います。

- 手元でffmpegのコマンドを打ち、動画配信を始める
- リモートマシンでpythonスクリプトを実行し、見れることを確認

ただ、動画がバッファリングしているようで、最初は若干タイムラグがあるみたいです。
また、リモート側の処理が追い付かないと徐々に遅延します。要改善。