OpenCVでの色検出方法の比較  inRange , numpy , cupy

#はじめに
OpenCVで遊ぶときに、色をベースに物体検出がしたくなるときがあります。
多くの場合、OpenCVで色検出をするときに

- cv2.cvtColorをつかって、RGB色空間からHSV色空間へ変換
- cv2.inRangeでHSV色空間の範囲を指定して2値化
- 出てきた画像からfindContoursして形でフィルタリング

という方法が紹介されています。
一方で同様に、Numpyを使って画素毎の条件で2値化する方法が考えられます。
ここでは、この2つの方法のメリット、デメリットとcupyでの実装も含め速度をXavierNX上で比較しました。

#やったこと
HSVに変換したあとinRangeで2値化する方法を含め以下の4条件を比較しました
1、inRangeを使って緑のボールを検出する方法
2、numpyで条件を指定して2値化する方法
3、numpyでinRangeだとできない条件で検出する方法
4、3をCUPYで高速化した場合
1と23で結果の画像を　1234で速度をそれぞれ比較してみました。
python3で試したソースは後半に4通りつけています。

#ソース画像
![color.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119262/bb4ce5ce-3dba-c33c-732f-06c933516fd6.jpeg)

#inRangeでの問題点
通常inRangeを用いて色検出を行う場合、色相ベースで色検出がしたいため、まずRGB色空間からHSV色空間へ変換を行うかと思います。
この場合

- inRangeは、基本的に、色空間上で各軸に平行な定数での閾値しか取れず自由度が低い
- inRangeをHSV色空間で使用する場合、赤周辺の色がH=0とH=180の境界を跨ぐため、inRange一発でフィルタできない
- 白と黒の周辺では照明の条件によりHが変動してしまう。、
- inRangeではCUDAのアクセラレーションを効かせられない 

結果として、inRangeを用いた場合
![hsv_h.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119262/2b2ff720-e0ef-9162-8b41-b4aa1a29e744.jpeg)
のように、画像下部の黒いケーブルや、画像上部の白い壁部分を緑のボールと区別することに非常に苦労することになります。

#Numpyでの実装
NumpyでinRangeと同様のフィルタは、

```python3:numpy1.py
        hsv = cv2.cvtColor( frame , cv2.COLOR_BGR2HSV )

        h = frame[:,:,0]
        s = frame[:,:,1]
        v = frame[:,:,2]

        mask_g = np.zeros(h.shape, dtype=np.uint8)
        mask_g[ (h>20) & (h <100) & (s>200) & (s < 255) & (v>50) & (v<150 ) ] = 255
```
として実装が可能かと思います。
この場合

```
        mask_g[ (h>20) & (h <100) & (s>200) & (s < 255) & (v>50) & (v<150 ) ] = 255
```
の部分は

```
        mask_g[ ( g/r> 2.0) & (g/b>2.0) ] = 255
```
などと色空間の軸に平行な定数としての閾値だけでなく、要素毎の比率などを元に閾値が設定可能で、線形もしくはより複雑な閾値も設定が可能です。上記の例では、検出後の画像は以下のようになり、より期待した結果を容易に得られるかと思います。
上記の例では、緑の要素が赤、青の倍明るい場合に検出され、以下のように黒、白部分を容易に除外できます。
![rgb_g.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119262/4ce9a6b3-8820-53d6-b045-738a8431d90a.jpeg)

# 速度の比較
一方で、速度については、
1. inRange 
2. numpy( inRange同様のフィルタ条件 )
3. numpy( R/B > 2 & R/G>2 )
4. cupy( R/B >2 & R/G > 2 )
の4条件で以下のようになりました。

| 1.inRange       |   2.numpy(inRange同様)| 3.numpy(RGB相関)| 4.CUPY|
|:----------------:|:------------------:|:-----------:|:---|
| 0.009[s] |0.047[s]|0.055[s]   |0.021[s]|
inRangeが一番はやいとはおもわなかった。。。。

#試したソースコード
##1, inRangeで緑のボールを検出

```python3:inrange.py
import cv2
import numpy as np
import time

src = 'v4l2src device= /dev/video0 ! image/jpeg,width=1920,height=1080 !jpegdec !videoconvert ! appsink'

cap=cv2.VideoCapture(src)


W = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
H = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
fps = cap.get(cv2.CAP_PROP_FPS)
while(cap.isOpened()):
    sum=0
    for i in range( 0,100 ):
        ret, frame = cap.read()
        start=time.time()
        r = frame[:,:,0]
        g = frame[:,:,1]
        b = frame[:,:,2]

        mask_g = np.zeros(r.shape, dtype=np.uint8)
        mask_g[ ( g/r> 2.0) & (g/b>2.0) ] = 255
        end = time.time()
        sum+= (end- start)
        cv2.imshow('ReadM',mask_g )
        cv2.waitKey(1)
        if i % 10 == 0 :
            print( i )
    print( sum/100)
cap.release()
```

##2、numpyで条件を指定して2値化する方法

```python3:numpy1.py
import cv2
import numpy as np
import time

src = 'v4l2src device= /dev/video0 ! image/jpeg,width=1920,height=1080 !jpegdec !videoconvert ! appsink'

cap=cv2.VideoCapture(src)


W = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
H = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
fps = cap.get(cv2.CAP_PROP_FPS)
while(cap.isOpened()):
    sum=0
    for i in range( 0,100 ):
        ret, frame = cap.read()
        start=time.time()
        hsv = cv2.cvtColor( frame , cv2.COLOR_BGR2HSV )

        h = frame[:,:,0]
        s = frame[:,:,1]
        v = frame[:,:,2]

        mask_g = np.zeros(h.shape, dtype=np.uint8)
        mask_g[ (h>20) & (h <100) & (s>200) & (s < 255) & (v>50) & (v<150 ) ] = 255
        end = time.time()
        sum+= (end- start)
        cv2.imshow('ReadM',mask_g )
        cv2.waitKey(1)
        if i % 10 == 0 :
            print( i )
    print( sum/100)
cap.release()
```
##3、numpyでinRangeだとできない条件で検出する方法
```python3:numpy2.py
import cv2
import numpy as np
import time

src = 'v4l2src device= /dev/video0 ! image/jpeg,width=1920,height=1080 !jpegdec !videoconvert ! appsink'

cap=cv2.VideoCapture(src)


W = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
H = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
fps = cap.get(cv2.CAP_PROP_FPS)
while(cap.isOpened()):
    sum=0
    for i in range( 0,100 ):
        ret, frame = cap.read()
        start=time.time()
        r = frame[:,:,0]
        g = frame[:,:,1]
        b = frame[:,:,2]

        mask_g = np.zeros(r.shape, dtype=np.uint8)
        mask_g[ ( g/r> 2.0) & (g/b>2.0) ] = 255
        end = time.time()
        sum+= (end- start)
        cv2.imshow('ReadM',mask_g )
        cv2.waitKey(1)
        if i % 10 == 0 :
            print( i )
    print( sum/100)
cap.release()
```

##4、3をCUPYで高速化した場合

```python3:cupy.py
import cv2
import cupy as cp
import time

src = 'v4l2src device= /dev/video0 ! image/jpeg,width=1920,height=1080 !jpegdec !videoconvert ! appsink'

cap=cv2.VideoCapture(src)


W = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
H = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
fps = cap.get(cv2.CAP_PROP_FPS)
while(cap.isOpened()):
    sum=0
    for i in range( 0,100 ):
        ret, frame = cap.read()
        start=time.time()
        frame_cupy = cp.asarray( frame )
        r = frame_cupy[:,:,0]
        g = frame_cupy[:,:,1]
        b = frame_cupy[:,:,2]

        mask_g = cp.zeros(r.shape, dtype=cp.uint8)
        mask_g[ ( g/r> 2.0) & (g/b>2.0) ] = 255
        mask_gn = cp.asnumpy( mask_g )
        end = time.time()
        sum+= (end- start)
        cv2.imshow('ReadM',mask_gn )
        cv2.waitKey(1)
        if i % 10 == 0 :
            print( i )
    print( sum/100)
cap.release()

```