OpenCVを使った色検出で物体の描く軌道をプロットしてみた

## はじめに
昔研究に使用した画像処理のプログラムが出てきたので，復習がてら記事にまとめることにしました．
内容はPython3 + OpenCVで赤色のマーカーを検出し，軌道をプロットするといったものです．
## 環境
Python 3.8.5
OpenCV 4.5.3
## 目次
以下のような流れで進めていきたいと思います．
* マスキング処理で色抽出(画像)
* オブジェクトの中心座標を取得(画像)
* マーカーの描く軌道をプロット(動画)

## マスキング処理で色抽出(画像) 
### マスキング処理とは
画像の中から特定の領域のみを抽出する処理
マスキング処理には抽出する領域の基準となる二値画像が必要であり，これをマスク画像といいます．

### 指定した色のマスク画像を生成し，マスキング処理
```python
import cv2
import numpy as np

def red_detect(img):
    #HSVでの色抽出
    RED_MIN = np.array([0, 150, 20]) #しきい値の下限
    RED_MAX = np.array([10, 255, 255]) #しきい値の上限
    hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) #HSVに変換
    mask = cv2.inRange(hsv_img, RED_MIN, RED_MAX) #マスク画像生成
    return mask

def main():
    img = cv2.imread("sample.png") #画像の読み込み
    mask = red_detect(img)　#色抽出用のマスク画像生成
    output = cv2.bitwise_and(img, img, mask=mask) #マスキング処理
    #各画像をを表示
    cv2.imshow("sample", img)
    cv2.imshow("sample_mask", mask)
    cv2.imshow("sample_output", output)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

if __name__ == "__main__":
    main()
```
inRange(入力画像,しきい値の下限,しきい値の上限)でマスク画像を生成しています．
上のコードではHSV空間上で赤色を検出するようにしきい値を設定しました．
そのため，cvtColor()メソッドを使って入力画像をRGBからHSV空間へ色変換しています．
しきい値の設定は[こちら](https://codehero.jp/opencv/10948589/choosing-the-correct-upper-and-lower-hsv-boundaries-for-color-detection-withcv)を参考にしました．

結果はこんな感じです
![スクリーンショット 2021-09-04 18.43.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1477548/37e9c9cf-ad8e-6387-d5ec-dd53cd248f4f.png)

しっかりと赤色を検出して抽出できました！

## オブジェクトの中心座標を取得(画像)
今回はラベリング処理をした後ブロブ解析で目的の物体の中心座標を取得していきたいと思います．
その前に，用語の説明を載せておきます．
### ラベリングとは
>ラベリングとは、二値化した画像内の連続した点の集まりごとに、個別の番号を割り当てる処理です。

引用元：[Python+OpenCVを利用したラベリング処理](https://dronebiz.net/tech/opencv/labeling)
とのことです．この処理を行うことで，画像内のオブジェクトの個数，位置や面積などの情報を得ることができます．

### ブロブ解析とは
ラベリングされた領域の特徴を解析することを言います．
ブロブという言葉は調べてみると英語における擬声語だそうで日本語では「ブヨブヨ」などといった表現になるらしいです
不定形の塊の形状，すなわちラベリングされたオブジェクトの形状を解析するといったイメージですね
### いざ実装
ではやっていこうと思います．
OpenCVでは以下のような記述で簡単にラベリング処理，ブロブ解析ができるようです↓

```python
nlabels, labels, stats, center = cv2.connectedComponentsWithStats(binary_img)
```
入力： 二値化画像
出力: 
nlabels: ラベル数，ただし背景も含まれるため実際のラベル数は1引いたものになる
labels: ラベル番号が入った配列データ
stats: オブジェクトの詳細が入った配列データ 各ラベル番号ごとにx,y,width,height,sizeが格納されている
center: オブジェクトの中心座標の配列データ(浮動小数点型)

```python
def main():
    #画像読み込み，表示
    img = cv2.imread("sample2.png")
    cv2.imshow("sample2", img)
    #色検出用マスク画像生成
    mask = red_detect(img)

    #ラベリング処理
    nlabels, labels, stats, center = cv2.connectedComponentsWithStats(mask)

    #円を描画
    for i in range(nlabels-1):
        cv2.circle(img, (int(center[i+1][0]), int(center[i+1][1])), 10, (0, 255, 0), thickness=10)

    #描画後の画像とマスク画像を表示
    cv2.imshow("sample2_marked", img)
    cv2.imshow("sample2_mask", mask)
    
    cv2.waitKey(0)
    cv2.destroyAllWindows()

if __name__ == "__main__":
    main()
```
ラベリング処理を行い，ラベリングされた各オブジェクトの中心座標の位置に円をプロットしています．
結果がこちら
![スクリーンショット 2021-09-04 23.35.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1477548/65fd3c36-153e-6a95-c3fd-3f8799eec476.png)
小さいものはわかりにくいですが一番大きい怒った顔のオブジェクトを見れば中心座標が取れていることがわかると思います．
このままでも十分なのですが，目標とする物体のみを検出したい場合，その他の小さいオブジェクトは邪魔になってしまいます．
そこで，オブジェクトの中でも面積が最大のものだけに対してマーキングするようにしたいと思います．

```python
def main():
    #画像読み込み，表示
    img = cv2.imread("sample2.png")
    cv2.imshow("sample2", img)
    #色検出用マスク画像生成
    mask = red_detect(img)

    #ラベリング処理
    nlabels, labels, stats, center = cv2.connectedComponentsWithStats(mask)

    #背景のオブジェクト情報の削除
    nlabels = nlabels - 1
    stats = np.delete(stats, 0, 0)
    center = np.delete(center, 0, 0)

    #面積が最大のオブジェクトのラベル番号を取得
    max_index = np.argmax(stats[:,4])

    #円を描画
    cv2.circle(img, (int(center[max_index][0]), int(center[max_index][1])), 10, (0, 255, 0), thickness=10)

    #描画後の画像とマスク画像を表示
    cv2.imshow("sample2_marked", img)
    cv2.imshow("sample2_mask", mask)

    cv2.waitKey(0)
    cv2.destroyAllWindows()

if __name__ == "__main__":
    main()
```
<img width=400px src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1477548/51a01e41-e03f-eb75-5856-9de8ffef2afe.png">
しっかりと一番大きいオブジェクトのみにマーカーがプロットされました！

## マーカーの描く軌道をプロット(動画)
さていよいよ本題に入っていきたいと思います．
とは言ってもやることは単純で，動画の1フレームごとに先程の画像処理を適用するだけですね
### 完成したコード
```python
import cv2
import numpy as np

# マスク画像を生成する関数 (input: 画像) -> (output: マスク画像)
def red_detect(img):
    #HSVでの色抽出
    RED_MIN = np.array([150, 150, 20])
    RED_MAX = np.array([180, 255, 255])
    hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    mask = cv2.inRange(hsv_img, RED_MIN, RED_MAX)
    return mask

# 中心座標を取得する関数 (input: 二値化画像) -> (output: 中心座標)
def get_center(binary_img):
    #ラベリング処理
    nlabels, labels, stats, center = cv2.connectedComponentsWithStats(binary_img)

    #背景のオブジェクト情報の削除
    nlabels = nlabels - 1
    stats = np.delete(stats, 0, 0)
    center = np.delete(center, 0, 0)

    #面積が最大のオブジェクトのラベル番号を取得
    max_index = np.argmax(stats[:,4])

    center_x = int(center[max_index][0])
    center_y = int(center[max_index][1])

    return (center_x, center_y)

#軌跡を描く関数　(input：　過去の軌跡, 追加する点, フレーム)
def drawObit(tracks, new_point, frame):
    tracks.append(new_point)
    for track in tracks:
        cv2.circle(frame, track, 5, (0, 255, 0), thickness=-1)


def main():
    #動画の読み込み
    mov_file = "star.MOV"
    cap = cv2.VideoCapture(mov_file)

    #中心座標を格納するリスト
    center_tracks = []

    #メインループスタート
    while cap.isOpened():
        #フレームを取得
        ret, frame = cap.read()

        if ret:
            #色抽出用のマスク画像生成
            mask = red_detect(frame)

            #ラベリング処理を行い中心座標を取得
            center = get_center(mask)

            #軌道をプロット
            drawObit(center_tracks, center, frame)

            #ウィンドウに表示
            cv2.imshow("draw_star", frame)
            cv2.imshow("mask", mask)

            #Qキーを押すと止まる
            if cv2.waitKey(25) & 0xFF == ord("q"):
                break
        else:
            break   
    #いろいろ閉じる
    cap.release()
    cv2.destroyAllWindows()

if __name__ == "__main__":
    main()
```
### 元動画
![ezgif com-gif-maker (2)](https://user-images.githubusercontent.com/84612341/132117084-39ae8e26-6a77-4168-8b65-21f8247f1afc.gif)
### 実行結果
![ezgif com-gif-maker (1)](https://user-images.githubusercontent.com/84612341/132117108-bf1ecc5c-dd7c-48b0-874f-cf3d83dcd15c.gif)
完成！！
## 補足
### 動画ファイルの保存
今回実装したコードではウィンドウに表示されるだけですが，動画ファイルに保存したい場合があると思ったので補足しておきます．

```python
    #保存する動画の設定
    frame_rate = 30
    size = (int(cap.get(3)), int(cap.get(4)))
    fmt = cv2.VideoWriter_fourcc("m", "p", "4", "v")
    writer_video = cv2.VideoWriter("result_triangle_draw.mp4", fmt, frame_rate, size)
```
まず，メインループを回す前の段階で保存する動画の設定を行います．
上から，フレームレート，サイズ，フォーマットなどを指定してVideoWriterオブジェクトを作成します．

```python
    #動画ファイルに書き込み
    writer_video.write(frame)
```
あとはメインループ内で各フレームを動画ファイルに書き込みます.

```python
    #ファイルを閉じる
    writer_video.release()
```
メインループから抜けたときにファイルを閉じておしまいです．

## 最後に
最後までお付き合いいただきありがとうございました！
OpenCVは初めてで色々調べながらやりましたが結構面白そうなことが簡単にできて楽しかったです．
ソースコード:[https://github.com/GOTO-TSL/marker-recognition](https://github.com/GOTO-TSL/marker-recognition)

## 参考
[OpenCV 公式](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_colorspaces/py_colorspaces.html)
[OpenCVで画像の読み込みと表示，保存](https://rabbitfoot141.hatenablog.com/entry/2016/02/06/123859)
[【画像処理】PythonでOpenCVを使ったラベリング処理](https://qiita.com/spc_ehara/items/72e878fc131b151c01fa)
[【Python/OpenCV】最大面積のブロブ解析（座標・大きさなど）](https://algorithm.joho.info/programming/python/blob-max-moment/)