UnitVで色検出をする

# はじめに
UnitVで色検出する際に役立つポイントやテクニックについてまとめました。

# デバイス & 開発環境
- [UnitV AI Camera(OV7740版)](https://docs.m5stack.com/en/unit/unitv_ov7740)
- [MaixPy](https://wiki.sipeed.com/soft/maixpy/en/index.html)

# 特定の色を検出する
```python
import sensor
import image

sensor.reset()                      # カメラの初期化
sensor.set_pixformat(sensor.RGB565) # RGB565フォーマットを使用
sensor.set_framesize(sensor.QVGA)   # QVGAフレームサイズを使用
sensor.skip_frames(time = 2000)     # 2秒間フレームをスキップして安定化

threshold = [(30, 100,  15, 127,  15, 127)] # 検出する色のしきい値を設定

while(True):
    img = sensor.snapshot() # 画像を取得
    blobs = img.find_blobs(threshold) # しきい値内の色を検出
    if blobs:
        for b in blobs:
            img.draw_rectangle(b[0:4]) # 検出した色を矩形で囲む
            img.draw_cross(b[5], b[6]) # 検出した色の中心に十字を描く
```
カメラから画像を取得し、指定されたしきい値内の色を ```find_blobs()``` で検出しています。もし検出された色があれば、それらの領域を矩形で囲み、中心に十字を描画します。

しきい値はLab色空間で指定します。MaixPyの「ツール」>「マシンビジョン」>「しきい値エディタ」より、「フレームバッファ」を選択することで現在カメラに写っている画像に対してしきい値調整ができます。

![tool.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3526610/dcf52bbb-8fd5-2643-c328-34d0ffbb83fb.png)

# 複数の色を検出する
```python
import sensor
import image

sensor.reset()                      # カメラの初期化
sensor.set_pixformat(sensor.RGB565) # RGB565フォーマットを使用
sensor.set_framesize(sensor.QVGA)   # QVGAフレームサイズを使用
sensor.skip_frames(time = 2000)     # 2秒間フレームをスキップして安定化

thresholds = [(30, 100,  15, 127,  15, 127),  # Red
              (30, 100, -64,  -8, -32,  32),  # Green
              (0,   15,   0,  40, -80, -20)]  # Blue

while(True):
    img = sensor.snapshot() # 画像を取得
    blobs = img.find_blobs(thresholds) # しきい値内の色を検出
    if blobs:
        for b in blobs:
            if b.code() == 1:   # Red
                img.draw_rectangle(b[0:4], color=(255, 0, 0))
                img.draw_cross(b[5], b[6], color=(255, 0, 0))
            if b.code() == 2:   # Green
                img.draw_rectangle(b[0:4], color=(0, 255, 0))
                img.draw_cross(b[5], b[6], color=(0, 255, 0))
            if b.code() == 4:   # Blue
                img.draw_rectangle(b[0:4], color=(0, 0, 255))
                img.draw_cross(b[5], b[6], color=(0, 0, 255))
```
複数の色を検出したい場合は、しきい値をリストにして ```find_blob()``` に渡すことで、一度に検出できます。最大32個まで指定できるようです。どの色が検出されたかは、 ```blob``` の ```code()``` を呼び出すことで確認できます。しきい値の順番に応じてビットが立つため、この場合Redは ```1``` 、Greenは ```2``` 、Blueは ```4``` となります。

# 検出を安定化させる
### オートゲイン・オートホワイトバランスのオフ
```python
import sensor
import image

sensor.reset()                      # カメラの初期化
sensor.set_pixformat(sensor.RGB565) # RGB565フォーマットを使用
sensor.set_framesize(sensor.QVGA)   # QVGAフレームサイズを使用

sensor.set_auto_gain(False)         # オートゲインをオフ
# sensor.set_auto_whitebal(False)   # オートホワイトバランスをオフ 画像全体が黄色っぽくなってしまう
sensor.set_auto_whitebal(False, rgb_gain_db = (25, 20, 40)) # ゲインを設定しておく

sensor.skip_frames(time = 2000)     # 2秒間フレームをスキップして安定化
```
[OpenMVのドキュメント](https://docs.openmv.io/library/omv.sensor.html#sensor.set_auto_gain)や[MaixPyのドキュメント](https://wiki.sipeed.com/soft/maixpy/en/api_reference/machine_vision/sensor.html#Set-automatic-gain)を見てみると、どうやら色の検出をする際は、 ```sensor.set_auto_gain()``` と ```sensor.set_auto_whitebal()``` を ```False``` に設定することが望ましいようです。
しかし、```sensor.set_auto_whitebal()``` を ```False``` に設定すると画像全体が黄色っぽくなることがあります。~~その場合、 ```sensor.__write_reg(0x80, 0xEF)``` のように手動でレジスタを書き換えると良いかもしれません。~~ rgb_gain_db = (25, 20, 40)のようにゲインを指定すると良いみたいです。ゲインの値は周囲の環境に合わせて調整してください。

### 明度・彩度・コントラストの調整
```python
import sensor
import image

sensor.reset()                      # カメラの初期化
sensor.set_pixformat(sensor.RGB565) # RGB565フォーマットを使用
sensor.set_framesize(sensor.QVGA)   # QVGAフレームサイズを使用

sensor.set_auto_gain(False)         # オートゲインをオフ
sensor.set_auto_whitebal(False, rgb_gain_db = (25, 20, 40)) # ゲインを設定しておく

sensor.set_brightness(0)            # 明度の調整
sensor.set_saturation(0)            # 彩度の調整
sensor.set_contrast(2)              # コントラストの調整

sensor.skip_frames(time = 2000)     # 2秒間フレームをスキップして安定化
```
明度・彩度・コントラストを `-3～+3` の範囲で調整することが出来ます。色検出ではコントラストを少し高めに設定しておくと良いでしょう。

# 面積が最大となる領域を求める
```python
import sensor
import image

sensor.reset()                      # カメラの初期化
sensor.set_pixformat(sensor.RGB565) # RGB565フォーマットを使用
sensor.set_framesize(sensor.QVGA)   # QVGAフレームサイズを使用
sensor.set_auto_gain(False)         # オートゲインをオフ
sensor.set_auto_whitebal(False, rgb_gain_db = (25, 20, 40)) # ゲインを設定しておく
sensor.set_brightness(0)            # 明度の調整
sensor.set_saturation(0)            # 彩度の調整
sensor.set_contrast(2)              # コントラストの調整
sensor.skip_frames(time = 2000)     # 2秒間フレームをスキップして安定化

threshold = [(30, 100,  15, 127,  15, 127)] # 検出する色のしきい値を設定

while(True):
    img = sensor.snapshot()           # 画像を取得
    blobs = img.find_blobs(threshold) # しきい値内の色を検出
    if blobs:
        max_blob = max(blobs, key=lambda b: b.area()) # 面積が最大の領域を取得
        img.draw_rectangle(max_blob[0:4])             # 検出した色を矩形で囲む
        img.draw_cross(max_blob[5], max_blob[6])      # 検出した色の中心に十字を描く
```
```blob.area()``` や ```blob.pixels()``` を使うことで、領域の面積やピクセル数を求めることができます。``` max_blob = max(blobs, key=lambda b: b.area()) ```の部分で面積が最大となる領域を求めています。

# 参考資料
MaixPyのドキュメントには抜けている情報が多いため、OpenMVのドキュメントも参考にした方が良いです。逆に、OpenMVのドキュメントにはMaixPyではまだ実装されていない関数も含まれているので注意が必要です。
- [MaixPy公式ドキュメント](https://wiki.sipeed.com/soft/maixpy/en/api_reference/machine_vision/image/image.html)
- [OpenMV公式ドキュメント](https://docs.openmv.io/library/omv.image.html)