（ラズパイで）USBカメラの映像をPythonのOpenCVで高速に表示する

非常に初歩的な話です…USBカメラではなくて、ラズパイ専用カメラでも [Raspberry Piと純正カメラモジュールで監視カメラを作る、おそらく正しい方法 jessie版 (motion + v4l2ドライバ)](https://qiita.com/rukihena/items/95da3860f9ca86c39f8d) に説明されているように `bcm2835-v4l2.ko` カーネルモジュールを読み込んでおけば以下の手順でできるはずですが、ラズパイ専用カメラは現物持ってないので確認していません。よく考えたらこの記事はラズパイに依存していませんでした…（タイトル更新しますた）。UbuntuかDebianなら下記の話は当てはまります。

**更新情報: OpenCV 4.5.1に合わせて更新(2021/05)**

# 準備
1. `sudo apt-get install usbutils python3-opencv libcanberra-gtk3-module v4l-utils qv4l2`
2. Ubuntu の場合は`video`グループに所属しないとカメラを操作できないから、 `sudo adduser ログイン名 video` とする
3. うまく行かない場合は `qv4l2` でカメラの映像を見られるか確認する
4. カメラが対応しているフレームレートと解像度は `v4l2-ctl --list-formats-ext` で確認できる

# 接続確認
`lsusb -t` の出力に `Class=Video, Driver=uvcvideo` があることを確認する

```shell-session
pi@raspberrypi:~ $ lsusb -t
/:  Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/4p, 5000M
/:  Bus 01.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/1p, 480M
    |__ Port 1: Dev 2, If 0, Class=Hub, Driver=hub/4p, 480M
        |__ Port 2: Dev 3, If 3, Class=Audio, Driver=snd-usb-audio, 480M
        |__ Port 2: Dev 3, If 1, Class=Video, Driver=uvcvideo, 480M
        |__ Port 2: Dev 3, If 2, Class=Audio, Driver=snd-usb-audio, 480M
        |__ Port 2: Dev 3, If 0, Class=Video, Driver=uvcvideo, 480M
        |__ Port 3: Dev 4, If 0, Class=Human Interface Device, Driver=usbhid, 1.5M
        |__ Port 4: Dev 5, If 0, Class=Human Interface Device, Driver=usbhid, 1.5M
        |__ Port 4: Dev 5, If 1, Class=Human Interface Device, Driver=usbhid, 1.5M
```

# Pythonプログラム
以下のプログラムを `python3 capture.py` として実行するとカメラからの映像が表示されます。なおUbuntu Mate ラズベリーパイだと、/dev/video0 にアクセスできないパーミッションになっているから `sudo chmod a+rw /dev/video0` とかして下さい。

```python:capture.py
import cv2

capture = cv2.VideoCapture(0)
if capture.isOpened() is False:
  raise IOError

while(True):
  try:
    ret, frame = capture.read()
    if ret is False:
      raise IOError
    cv2.imshow('frame',frame)
    cv2.waitKey(1)
  except KeyboardInterrupt:
    # 終わるときは CTRL + C を押す
    break

capture.release()
cv2.destroyAllWindows()
```

# Pythonでキャプチャするときの小技集

* カメラからのデータをBGR形式に変換せずに直接受け取る例が https://github.com/opencv/opencv/blob/master/samples/python/video_v4l2.py にある
* キャプチャ解像度やフレームレート、取り込みデータ形式は下記のように行う。これらの組み合わせにカメラが対応していないとエラーが起きることがあり、どの組み合わせに対応しているかは `v4l2-ctl --list-formats-ext` で調べられる

```python
capture.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('Y', 'U', 'Y', 'V'))
capture.set(cv2.CAP_PROP_FRAME_WIDTH, MY_CAMERA_WIDTH_ORIG)
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, MY_CAMERA_HEIGHT_ORIG)
capture.set(cv2.CAP_PROP_FPS, MY_FPS)
```

* キャプチャした後のプログラムの処理に時間が掛かったときにコマ落ちしないためのバッファがあるが、バッファが大きすぎると遅延（コマ遅れ）が気になることもある。その場合は `capture.set(cv2.CAP_PROP_BUFFERSIZE, 1)` でバッファサイズを小さくすると遅延が減る
* OpenCV 4以降ではキャプチャを直接V4L2で行われず GStreamer を経由されることがある。そうすると上記のテクニックの一部は使えなくなるが、V4L2から直接キャプチャした場合は、例えば以下のようにする

```python
try:
  capture = cv2.VideoCapture(0, cv2.CAP_V4L2)
except TypeError:
  capture = cv2.VideoCapture(0)
if capture.isOpened() is False:
  raise IOError
```

上記の小技を全部いれた例は以下です。しかし、ラズパイに[USB 2.0接続のElecom製ウェブカメラ](https://www.elecom.co.jp/products/UCAM-C520FEBK.html) をラズパイ4BのUSB 3ポートに接続すると、次の例ではキャプチャが25fps程度で出来るが画面更新が10fps以下でしかできなかった。**並列処理** を行い画面更新も25fpsで行う例を最後に示す。また、記事著者のウェブカメラではUSB 2.0接続なのでキャプチャ解像度を640x480よりも大きくすると、フレームレートが結構落ちる。キャプチャ自体のフレームレートを高くするためには [OpenCVのカメラ読み込みを高速化し、遅延時間も短くする](https://qiita.com/iwatake2222/items/b8c442a9ec0406883950) に説明されるように `capture.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('Y', 'U', 'Y', 'V'))` の行を変えて、カメラから取り込むデータを圧縮してUSBケーブルに流すようにするとよい。記事著者は画質劣化を嫌ったので敢えて圧縮を行っていない。

```python:capture2.py
import time
import cv2

try:
  capture = cv2.VideoCapture(0, cv2.CAP_V4L2)
except TypeError:
  capture = cv2.VideoCapture(0)

if capture.isOpened() is False:
  raise IOError

if isinstance(capture.get(cv2.CAP_PROP_CONVERT_RGB), float):
  capture.set(cv2.CAP_PROP_CONVERT_RGB, 0.0)
else:
  capture.set(cv2.CAP_PROP_CONVERT_RGB, False)

capture.set(cv2.CAP_PROP_BUFFERSIZE, 4)
capture.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('Y', 'U', 'Y', 'V'))
capture.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
capture.set(cv2.CAP_PROP_FPS, 30)

last_capture_start_time = time.time()
while(True):
  try:
    capture_start_time = time.time()
    ret, frame = capture.read()
    if ret is False:
      raise IOError
    capture_end_time = time.time()
    print("Capture FPS = ", 1.0 / (capture_end_time- capture_start_time))
    cv2.imshow('frame',cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_YUYV))
    cv2.waitKey(1)
    print("Real FPS = ", 1.0 / (time.time() - last_capture_start_time))
    last_capture_start_time = capture_start_time
  except KeyboardInterrupt:
    # 終わるときは CTRL + C を押す
    break

capture.release()
cv2.destroyAllWindows()
```


# 並列処理による画面更新の高速化

直前の例ではキャプチャが25fpsでできるのに、画面表示は12fps程度になってしまっていた。画面表示更新とキャプチャを並列に行うと両方を25fps程度で動作させることができる。[Python 3のmultiprocessingでプロセス間で大量のデータを受け渡しつつnumpyで処理する](https://qiita.com/kakinaguru_zo/items/f53e2485f15dd0d71f82)に基づいて、並列処理を行う例を次に示す

```python:capture4.py
import cv2
import multiprocessing
import multiprocessing.sharedctypes
import time
import numpy

WIDTH=640
HEIGHT=480

def camera_reader(out_buf, buf1_ready):
  try:
    capture = cv2.VideoCapture(0, cv2.CAP_V4L2)
  except TypeError:
    capture = cv2.VideoCapture(0)

  if capture.isOpened() is False:
    raise IOError

  if isinstance(capture.get(cv2.CAP_PROP_CONVERT_RGB), float):
    capture.set(cv2.CAP_PROP_CONVERT_RGB, 0.0)
  else:
    capture.set(cv2.CAP_PROP_CONVERT_RGB, False)

  capture.set(cv2.CAP_PROP_BUFFERSIZE, 4)
  capture.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('Y', 'U', 'Y', 'V'))
  capture.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH)
  capture.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT)
  capture.set(cv2.CAP_PROP_FPS, 30)

  while(True):
    try:
      capture_start_time = time.time()
      ret, frame = capture.read()
      if ret is False:
        raise IOError
      #print("Capture FPS = ", 1.0 / (time.time() - capture_start_time))
      bgr_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_YUYV)
      #cv2.imshow('frame2', bgr_frame)
      buf1_ready.clear()
      memoryview(out_buf).cast('B')[:] = memoryview(bgr_frame).cast('B')[:]
      buf1_ready.set()
      print("Capture+Conversion+Copy FPS = ", 1.0 / (time.time() - capture_start_time))
    except KeyboardInterrupt:
      # 終わるときは CTRL + C を押す
      break
  capture.release()

if __name__ == "__main__":
  buf1 = multiprocessing.sharedctypes.RawArray('B', HEIGHT*WIDTH*3)
  buf1_ready = multiprocessing.Event()
  buf1_ready.clear()
  p1=multiprocessing.Process(target=camera_reader, args=(buf1,buf1_ready), daemon=True)
  p1.start()
  
  captured_bgr_image = numpy.empty((HEIGHT, WIDTH, 3), dtype=numpy.uint8)
  while True:
    try:
      display_start_time = time.time()
      buf1_ready.wait()
      captured_bgr_image[:,:,:] = numpy.reshape(buf1, (HEIGHT, WIDTH, 3))
      buf1_ready.clear()
      cv2.imshow('frame', captured_bgr_image)
      cv2.waitKey(1)
      print("Display FPS = ", 1.0 / (time.time() - display_start_time))
    except KeyboardInterrupt:
      # 終わるときは CTRL + C を押す
      print("Waiting camera reader to finish.")
      p1.join(10)
      break

  cv2.destroyAllWindows()
```