OpenCVのカメラ読み込みを高速化し、遅延時間も短くする

# この記事について
Raspberry PiにUSBカメラを接続してOpenCVで読み込むと、速度(FPS)が非常に遅いことがあります。また、PiCameraを使っても、解像度が高いと速度が出ないことがあります。
これを高速化します。対策は、単に圧縮フォーマットを指定するだけです。

- 速度が必要な場合は、非圧縮フォーマットじゃなくて、H264フォーマットなどを指定しましょう
    - `cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('H', '2', '6', '4'));`
    - [追記(2020/7/15)] 最近のOpenCV or Raspbianだと、H264フォーマットが指定できなくなってるっぽい ??
- H264非サポートなWebカメラの場合には、MJPGが使える可能性があります。だけど、MJPGだとCPUパワーを結構使います
- `CAP_PROP_BUFFERSIZE` の設定値を小さくすることで、遅延時間を短くすることも可能です

# 環境
- Raspberry Pi 3 B+
	- 2019-07-10-raspbian-buster
	- Python3 + OpenCV 3.2.0 (今はaptでインストール可能になってた `sudo apt install python3-opencv` )
	- Pi Camera v2.1
- USBカメラ
	- Logicool C270m

　* Jetson Nanoでもほぼ同じ結果でした。

## 測定方法
カメラはカバーせずに、明るいシーンを撮影(←重要)。
これをやらないと、シャッタースピードの影響を受けるためか、キャプチャ時間(`cap.read()` )が遅くなることがあります。

# フォーマット指定と速度
## フォーマットの指定方法
`VideoCapture` を開いた後に、`CAP_PROP_FOURCC` プロパティにfourcc形式でフォーマットを指定できます。
Pythonだと以下のようになります。C++でも同様です。

```python
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('H', '2', '6', '4'));
```

## 処理時間の比較
各カメラで、フォーマットと処理時間(`cap.read()` )の関係を比較しました。

### PiCamera V2.1 (1920x1080)
| フォーマット | BGR3 | MJPG | H264 |
|----------------------|------|------|------|
| Capture時間 [msec]   | 249  | 72   | 34   |
| CPU使用率 [%]        | 5    | 27   | 15   |

PiCamera V2.1の場合、デフォルトはBGR3フォーマットになります。1920x1080で読み込もうとするとめちゃくちゃ遅くなります。
MJPGだと少し速くなり、H264にすると、ほぼリアルタイムな30fpsが出ます。
CPU使用率はFPSが異なるので、あまり意味はない数字です。

### PiCamera V2.1 (1280x720)
| フォーマット | BGR3 | MJPG | H264 |
|---------------------|------|------|------|
| Capture時間 [msec]  | 33   | 33   | 33   |
| CPU使用率 [%]       | 13   | 27   | 8    |

1280x720だとどのフォーマットでもほぼリアルタイムな30fpsが出ます。
1920x1080に比べて速くなったのは、おそらくイメージセンサの動作モードが違うためです (https://picamera.readthedocs.io/en/release-1.12/fov.html )

CPU使用率を見ると、H264が最も少ないという面白い結果になりました。おそらくエンコード処理はハードウェアで行い、データ転送量もBGRに比べて少ないためだと思われます。


### USB Camera (1280x720)
| フォーマット | YUYV | MJPG | H264 |
|-----------------------|------|------|------|
| Capture時間 [msec]    | 135  | 33   | -    |
| CPU使用率 [%]         | 10   | 23   | -    |

今回使ったLogicool C270mだと、デフォルトフォーマットはMJPGでした。なので、意識しないと遅いことには気づかないのですが、CPU使用率が高いです。あえてYUYVフォーマットを使うと当然遅くなります。H264は非サポートでした。

確か、上位機種のC920rはデフォルトがYUYVだったので、デフォルトのままだと遅いので注意が必要です。
C270mでも、他のアプリやコマンドからフォーマットを変えたら、その設定は残るので、コード内でフォーマット指定した方が無難です。
(実は本記事は、これに気付かずに遅いな～とハマったのが発端です。。。)

ちなみに、対応フォーマットは`v4l2-ctl -d /dev/video1 --list-formats-ext` で確認可能です。

## 注意
圧縮フォーマットを利用したら速度は向上しますが、当然画質は下がります。

# テストコード
```python

# -*- coding: utf-8 -*-
import sys
import time
import cv2

def decode_fourcc(v):
	# https://amdkkj.blogspot.com/2017/06/opencv-python-for-windows-playing-videos_17.html
	v = int(v)
	return "".join([chr((v >> 8 * i) & 0xFF) for i in range(4)])

cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
# cap = cv2.VideoCapture(1)
# cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
# cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)

# cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('B', 'G', 'R', '3'));
# cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G'));
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('H', '2', '6', '4'));
# cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('Y', 'U', 'Y', 'V'));
print(decode_fourcc(cap.get(cv2.CAP_PROP_FOURCC)))

if cap.isOpened() == False:
	print("cannot open")
	sys.exit(1)
_, img = cap.read()	# dummy

cnt = 0
time_start = time.time()

try:
	while True:
		cnt += 1
		print(cnt)
		_, img = cap.read()
		# cv2.imshow('image', img)
		# key = cv2.waitKey(1)
		# if key == 27:	# ESC
			# break
except KeyboardInterrupt:
	print("Exit loop by ctrl-c")

cap.release()
cv2.destroyAllWindows()
time_end = time.time()
print ("Capture time: {0}".format((time_end - time_start) * 1000 / cnt) + "[msec]")

```

# [おまけ]シャッタースピードとキャプチャ処理時間
おそらくOpenCVの仕様として、露光時間(≒ 1/シャッタースピード)だけ`cap.read()` で待たされます。
例えば、上記コードで、

```python
_, img = cap.read()
```

のかわりに

```python
_, img = cap.read()
time.sleep(0.01)
```

として、`cap.read()` の処理時間(キャプチャ処理時間)だけを測定したら20msecになりました。

さらに、`time.sleep(0.1)` として待ち時間をさらに増やしたら、キャプチャ処理時間は10～20msecにバラついたので、`cap.read()` を呼んだタイミングの露光が完了するのを待っているのだと思います。


待たされるのが嫌だという場合には、キャプチャ処理は別スレッドでやった方がよさそうですね。
↓の記事のような感じ。
https://qiita.com/PINTO/items/dd6ba67643bdd3a0e595

# [おまけ]遅延時間も短くしたい
通常、「①カメラ画像キャプチャ(入力) ⇒ ②メイン処理 ⇒ ③出力」というループになると思います。
この時、ここまでの内容で、①キャプチャ処理時間は高速にすることが出来ました。
しかし、②メイン処理の処理時間が長くなると、遅延(Delay)が発生することがあります。

`CAP_PROP_BUFFERSIZE` の設定値を小さくすることで、この遅延を短くすることが出来ます。
(例. `cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)` )

CAP_PROP_BUFFERSIZE = 4(初期値)のとき、遅延 = 460msec
CAP_PROP_BUFFERSIZE = 1のとき、遅延 =  190msec
※ Raspberry Pi4, PiCamera v2.1, 1280x720, BGR3

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214268/8023eb1f-3d06-6693-dd27-67813d15bea5.png)
CAP_PROP_BUFFERSIZE = 4の結果 (ストップウォッチを撮影し、HDMI出力)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214268/820aa317-bfc8-064a-e36f-83aac32f0b6a.png)
CAP_PROP_BUFFERSIZE = 1の結果 (ストップウォッチを撮影し、HDMI出力)


# [おまけ]GStreamerでも遅延時間をなくしたい
Jetson Nano等では、Raspberry Pi Cameraモジュールを使用するためにはGStreamerを使用する必要があります。この場合、上記のプロパティ設定が効きません。GStreamerのパイプライン上で設定する必要があります。

具体的には、`appsink` に　`max-buffers=1 drop=True` を付けることで実現可能です。設定全体は以下のようになります。


```python
# https://github.com/JetsonHacksNano/CSI-Camera
# https://forums.developer.nvidia.com/t/how-to-eliminate-gstreamer-camera-buffer/75608/9
def gstreamer_pipeline (capture_width=1280, capture_height=720, display_width=1280, display_height=720, framerate=30, flip_method=2) :
    return ('nvarguscamerasrc ! ' 
    'video/x-raw(memory:NVMM), '
    'width=(int)%d, height=(int)%d, '
    'format=(string)NV12, framerate=(fraction)%d/1 ! '
    'nvvidconv flip-method=%d ! '
    'video/x-raw, width=(int)%d, height=(int)%d, format=(string)BGRx ! '
    'videoconvert ! '
    'video/x-raw, format=(string)BGR ! appsink max-buffers=1 drop=True' % (capture_width,capture_height,framerate,flip_method,display_width,display_height))

cap = cv2.VideoCapture(gstreamer_pipeline(), cv2.CAP_GSTREAMER)
```