pythonで赤い物体を認識しよう

##初めに
pythonで、特にOpenCVの支援によるところが大きいですが、ビデオカメラの映像から赤色の物体を認識してみようということで、記事にしてみました。

OpenCVを使う関係上、何もpythonじゃなくてもC/C++とかでも十分可能なのですが、pythonで作るとビックリするほど少ない作業量で書けるというか、やっぱり凄いな、と。

ということで記事にしてみるのでしたｗ

##赤い物体を検出するには？
アルゴリズムなんて言葉を使うとカッコイイのですが、そこまで高尚なものでもありません。赤い物体を検出する仕組みにちょっと触れておこうかと思います。

色の表現方法として定番ドコロとして、RGBがあると思います。
R, G, Bそれぞれの明るさを0〜255で表現することで、24bitカラーになるということです。では、このRGBを使って赤色を判定できるでしょうか？
実は結構難しいんですね。例えば、R=255,G=0,B=0は、誰が何と言おうと赤色でしょう。しかし、R=10,G=0,B=0はどうでしょうか？ これって、赤というより黒じゃない？なんて感じもしてきます。つまり、RGBでは色合いの判定が難しいのです。

そこで、HSV色空間というデータ構造を使います。\(詳しくは[Wikipedia](https://ja.wikipedia.org/wiki/HSV色空間)まで\)

では、関数を作って、HSV色空間への変換を行います。OpenCV, numpyのインポートは定番です。

```py
import cv2
import numpy as np

def find_rect_of_target_color(image):
  hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV_FULL)
  h = hsv[:, :, 0]
  s = hsv[:, :, 1]
```

ここまでで、画像のH成分、S成分が取り出せました。
H成分は、Hue(色相)です。実際は、360段階で赤→緑→青→赤と円のように一周します。
冒頭の出だしにも書きましたが、RGBでは色相の判定は難しいのですが、HSVに変換してしまえば、Hを見ることで色相による判定がとても簡単になります。

OpenCVの場合、H,S,Vともに256段階で保持する仕組みですので(`COLOR_BGR2HSV_FULL`を指定した場合)、本来のHは360段階であるべきところが256段階に丸められてます。その辺を計算に入れて進みます。
Hue(色相)で赤と言えば、紫っぽい範囲まで含めれば 280〜28°くらいでしょうか。これを256段階に計算し直すと、200〜20くらいになると思います。なので、Hの値が (H < 20 | H > 200) の範囲が赤色ということになります。

また、色の濃さにも着目しましょう。
Hue(色相)が赤でも、色の濃さが十分薄ければ、白や黒に近づいていきます。
S成分、Saturation(彩度)も同時に判定する必要があります。これは普通に0〜255の範囲で、数値が大きくなればなるほど「鮮やか」という事を示します。
ここでは、赤色の判定なので、Sの値が (S > 128) である事を条件として付け加えましょう。

これらを纏めて numpy 的に書くと、以下のような感じになります。

```py
  mask = np.zeros(h.shape, dtype=np.uint8)
  mask[((h < 20) | (h > 200)) & (s > 128)] = 255
```

これで、赤いところを示すマスクデータができました。(赤は255、非赤は0)
でも、これで終わりではありません。このマスクデータは、画像の「ドットが赤っぽいところ」を示しているに過ぎません。

##マスクデータを解析する
やっぱり、赤い物体を検出する以上、ある程度の大きさのドットの塊を認識したいところです。今のままではただの点群で、纏まりがありません。
この、ただの点に過ぎないデータを塊として認識するため、まずは「輪郭」というものを考えてみましょう。
輪郭ができれば、その輪郭に囲まれた塊がわかるということです。

```py
  contours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
```

OpenCVとpythonの組み合わせは偉大です。
たったの1行で書けてしまいます。
で、次は `contours` の内容を見ていきます。実は、すでに輪郭を作る過程で、塊ごとに配列化されてしまっています。

```py
  rects = []
  for contour in contours:
    approx = cv2.convexHull(contour)
    rect = cv2.boundingRect(approx)
    rects.append(rect)    
```

また同じこと書きますが、OpenCVとpythonの組み合わせは偉大です。実に簡潔に書けます。
`cv2.convexHull()`は、凹凸のある塊を内包する、凸状の形状を算出する関数です。`contour`には輪郭を成すドット情報が入っているのですが、あくまでも輪郭ですので非常に複雑な形状をしているのです、リアス式海岸のような。これを、マルッと包み込む袋のような2Dポリゴンにしてしまうのが、`cv2.convexHull()`です。復帰値である`approx`は、(X, Y)の配列です。
そして、袋状になったポリゴンがスッポリ入る矩形を計算するのが、`cv2.boundingRect()`です。これは、(x, y, width, height)という形式の矩形情報を返してくれます。
ここまでで、点群の情報が、塊ごとの矩形のリストにまとまりました。

さて、順を追ってバラバラに書いたものを一旦繋げてみます。

```py
import cv2
import numpy as np

def find_rect_of_target_color(image):
  hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV_FULL)
  h = hsv[:, :, 0]
  s = hsv[:, :, 1]
  mask = np.zeros(h.shape, dtype=np.uint8)
  mask[((h < 20) | (h > 200)) & (s > 128)] = 255
  contours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
  rects = []
  for contour in contours:
    approx = cv2.convexHull(contour)
    rect = cv2.boundingRect(approx)
    rects.append(np.array(rect))
  return rects
```

たったこれだけ。たったこれだけで、与えた画像から赤い箇所を特定する矩形の配列を返す関数ができました。

##ビデオカメラからの映像を解析する
ここまで作ったら、その動作を確かめたいですね。
ビデオカメラの映像でリアルタイムに処理をしてみましょう。

```py
if __name__ == "__main__":
  capture = cv2.VideoCapture()
  while cv2.waitKey(30) < 0:
    _, frame = capture.read()
    rects = find_rect_of_target_color(frame)
    for rect in rects:
      cv2.rectangle(frame, tuple(rect[0:2]), tuple(rect[0:2] + rect[2:4]), (0, 0, 255), thickness=2)
    cv2.imshow('red', frame)
  capture.release()
  cv2.destroyAllWindows()
```

どうですか？ 思った通りの結果になりましたか？
ワタクシの予想ですが、いまいちな結果だったんじゃないかと思います。
それは、大きな矩形も小さな矩形も、全部、「赤い物体」として認識してしまっているからです。ノイズのようなものまで「物体」と捉えるのはナンセンスです。ある程度の大きさということで間引きましょう。

ここで、ちょっとお勧めは、最大の大きさの矩形だけを「赤い物体」としてしまうことです。
赤い物体を検出するにあたって、手近に有った赤い物体をカメラの前にかざしましたね？ つまり、ソレを検出したかったわけです。往々にして、検出して欲しいものは一番大きく写すのです。

得られた矩形リストから一番面積の大きいものを探索するよう、以下のように変更します。

```py
if __name__ == "__main__":
  capture = cv2.VideoCapture()
  while cv2.waitKey(30) < 0:
    _, frame = capture.read()
    rects = find_rect_of_target_color(frame)
    if len(rects) > 0:
      rect = max(rects, key=(lambda x: x[2] * x[3]))
      cv2.rectangle(frame, tuple(rect[0:2]), tuple(rect[0:2] + rect[2:4]), (0, 0, 255), thickness=2)
    cv2.imshow('red', frame)
  capture.release()
  cv2.destroyAllWindows()
```

ということで、今回はここまでです。
python + OpenCVの組み合わせは、とても便利だなぁと感心します。