「Wavashare Jetbot AIカー」でカメラ使用時にエラー

# 現象  
「Wavashare Jetbot AIカー」において、以下のようにカメラのインスタンスを生成しようとすると、
```python
from jetbot import Camera

camera = Camera()
```
エラー : ``RuntimeError('Could not read image from camera.')``が表示される。

# 原因
1.他のプログラムでもカメラを使用している
→ Jupyterlab画面右の所から他のプログラムを終了しましょう。

2.前回使用時、カメラを正常に終了していない可能性がある
→ 以下で解説します。

# 解決法  
カメラを終了する際に``<Cameraのインスタンス>.stop()``を記述する。
```python
from jetbot import Camera

camera = Camera()
camera.stop()  # カメラの終了
```
## 実は...
[Wavashare Jetbot AIカーの公式ドキュメント](https://www.waveshare.com/wiki/JetBot_AI_Kit)では、NVIDIA公式ではなく[Wavashare独自のJetbotのgithub](https://github.com/waveshare/jetbot)を使用してセットアップが行われますが、その中にあるNotebookのバージョンが古いものになっています。(NVIDIAの方では何回かNotebookが修正されているのですが、Wavashareの方は特に何もしていないようです)

[NVIDIA公式github](https://github.com/NVIDIA-AI-IOT/jetbot/tree/master/notebooks)の方では、プログラムの最後に``camera.stop()``が追加されていたりとコードが一部修正されていますので、Notebookに関してはこちらを参照した方が良いかと思います。

# おまけ - ``camera.stop()``についての小話
[JetRacerを解説しているサイト](https://faboplatform.github.io/JetracerDocs/test/03.interactive_regression/)にて、カメラ終了時に``camera.cap.release()``を記述しないと次回読み込み時にエラーが起きるという記述を見ました。

:::note warn
```camera```はインスタンス名です。
:::

最初は特に気にせず、私も例に倣いプログラムの最後に付け加えていましたが、何故これで上手く行くのか気になったため、少し調べる事にしてみました。

差し当たっては、```Camera```モジュールが提供している関数について知りたいと思い、NVIDIA公式が提供している[Jetbotのgithub](https://github.com/NVIDIA-AI-IOT/jetbot/tree/636add19d602ebd63d9a2d1a64d458329357ac3c/jetbot)を覗いてみる事にしてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2357932/6e410c3f-f00a-c065-8731-1891565857a3.png)
上から2つ目に```camera```というディレクトリがあります。名前からしてここに書いてありそうです。  

[中身](https://github.com/NVIDIA-AI-IOT/jetbot/tree/636add19d602ebd63d9a2d1a64d458329357ac3c/jetbot/camera)はこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2357932/521149c5-d985-8a14-45f7-391d6c479e4a.png)

見たところ、各ファイルが実装している機能は以下のような感じでした。
| ファイル名 | 機能 |
|:-----------:|:------------:|
| [`__init__.py`](https://github.com/NVIDIA-AI-IOT/jetbot/blob/636add19d602ebd63d9a2d1a64d458329357ac3c/jetbot/camera/__init__.py) | カメラのセットアップ / importされる際の名前の指定 |
| [camera_base.py](https://github.com/NVIDIA-AI-IOT/jetbot/blob/636add19d602ebd63d9a2d1a64d458329357ac3c/jetbot/camera/camera_base.py) | カメラから受け取った画像の受け渡し |
| [opencv_gst_camera.py](https://github.com/NVIDIA-AI-IOT/jetbot/blob/636add19d602ebd63d9a2d1a64d458329357ac3c/jetbot/camera/opencv_gst_camera.py) | カメラの開始、終了などを行う関数 |
| [zmq_camera.py](https://github.com/NVIDIA-AI-IOT/jetbot/blob/636add19d602ebd63d9a2d1a64d458329357ac3c/jetbot/camera/zmq_camera.py) | 同上 |

:::note warn
Jetbotで使用可能なカメラはいくつかありますが、その種類によって使用する関数が変わるようです。  ```__init__.py```で接続されているカメラの種類を判別し、それに合わせて```opencv_gst_camera.py```か```zmq_camera.py```を適用する...というとこまでは読み取れましたが、それ以上は分かりませんでした。  
USBカメラにも対応しているとの事なので、```zmq_camera.py```はそれ用なのでしょうか。
:::

少し先回りした書き方になってしまいましたが、結論から言うと「```Camera```モジュールが提供している関数」は```opencv_gst_camera.py```に記述されていました。
  
実際に見てみましょう。
```opencv_gst_camera.py
import traitlets
import atexit
import cv2
import threading
import numpy as np
from .camera_base import CameraBase


class OpenCvGstCamera(CameraBase):
    
    value = traitlets.Any()
    
    # config
    width = traitlets.Integer(default_value=224).tag(config=True)
    height = traitlets.Integer(default_value=224).tag(config=True)
    fps = traitlets.Integer(default_value=30).tag(config=True)
    capture_width = traitlets.Integer(default_value=816).tag(config=True)
    capture_height = traitlets.Integer(default_value=616).tag(config=True)

    def __init__(self, *args, **kwargs):
        self.value = np.empty((self.height, self.width, 3), dtype=np.uint8)
        super().__init__(self, *args, **kwargs)

        try:
            self.cap = cv2.VideoCapture(self._gst_str(), cv2.CAP_GSTREAMER)

            re, image = self.cap.read()

            if not re:
                raise RuntimeError('Could not read image from camera.')

            self.value = image
            self.start()
        except:
            self.stop()
            raise RuntimeError(
                'Could not initialize camera.  Please see error trace.')

        atexit.register(self.stop)

    def _capture_frames(self):
        while True:
            re, image = self.cap.read()
            if re:
                self.value = image
            else:
                break
                
    def _gst_str(self):
        return 'nvarguscamerasrc sensor-mode=3 ! video/x-raw(memory:NVMM), width=%d, height=%d, format=(string)NV12, framerate=(fraction)%d/1 ! nvvidconv ! video/x-raw, width=(int)%d, height=(int)%d, format=(string)BGRx ! videoconvert ! appsink' % (
                self.capture_width, self.capture_height, self.fps, self.width, self.height)
    
    def start(self):
        if not self.cap.isOpened():
            self.cap.open(self._gst_str(), cv2.CAP_GSTREAMER)
        if not hasattr(self, 'thread') or not self.thread.isAlive():
            self.thread = threading.Thread(target=self._capture_frames)
            self.thread.start()

    def stop(self):
        if hasattr(self, 'cap'):
            self.cap.release()
        if hasattr(self, 'thread'):
            self.thread.join()
            
    def restart(self):
        self.stop()
        self.start()
        
    @staticmethod
    def instance(*args, **kwargs):
        return OpenCvGstCamera(*args, **kwargs)
```
下の方に```stop(self)```という関数があります。
更にその関数内には、```self.cap.release()```と記述されています。  

つまり、冒頭で紹介した[JetRacerを解説しているサイト](https://faboplatform.github.io/JetracerDocs/test/03.interactive_regression/)で見た```camera.cap.release()```の下りはこの部分から来ている、というオチでした。  

# 参考  
https://qiita.com/seigot/items/e969deb60db7e51674bd  

https://github.com/NVIDIA-AI-IOT/jetbot

https://faboplatform.github.io/JetracerDocs/test/03.interactive_regression/