OpenCVでドロップ認識 -課題1-

#前書き
OpenCVの開発環境は作ったので、実際になにか動かしてみます。
今回はOpenCV-Pythonのチュートリアル、[OpenCV-Python Tutorials](https://opencv-python-tutroals.readthedocs.io/en/latest/)の**[日本語版](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/index.html)**をやっていきます。
多少アレンジすると思う。

物体認識をやっていきたいので、とりあえず簡単なところから練習していきます。

# シリーズ
* [pythonでOpenCV使うための環境づくり](https://qiita.com/Ryo87/items/939b370c06c60c9d63c8)
* [OpenCVのチュートリアルをやる](https://qiita.com/Ryo87/items/c20be948d261f1be2f22)
* [OpenCVのチュートリアルをやる -その2 変換系-](https://qiita.com/Ryo87/items/6f367bf1eb69c4f3c80e)
* [OpenCVのチュートリアルをやる -その3 -](https://qiita.com/Ryo87/items/0ffc633fb918cb704566)

#開発環境
機種名 : MacBook Pro
OS : Mojave 10.14.2
プロセッサ名 : Intel Core i5 2.6 GHz
メモリ : 16 GB
グラフィック : Intel Iris 1536MB

## python系
```
% pip freeze
numpy==1.15.4
opencv-python==4.0.0.21
```

## 画像データ
今回はこの三枚の画像を使っていきます。

<img width="375" alt="IMG_0451.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/35860/44c1a394-7fa2-dc06-0885-d9c3ee9ad7c6.png">

<img width="375" alt="IMG_0452.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/35860/7249cb58-eb2a-f2c4-cbdf-93e76a75b35e.png">

<img width="375" alt="IMG_0453.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/35860/e27a3fd2-2bd8-4812-9af3-81067518de3d.png">

#本題
画面下部のそれぞれのドロップを認識することを目標とします。

## 課題
1. 丸ドロップと四角ドロップを別物として認識する(2パターン:6×5盤面)
2. 丸ドロップが別物として認識する(5パターン:6×5盤面)
3. 5×4盤面、6×5盤面、7×6盤面で認識する

## 丸ドロップと四角ドロップを別物として認識する(2パターン:6×5盤面)
### とりあえず2値化
```sample.py
import numpy as np
import cv2

# 画面下部のボードだけ取得
def getBoard(img):
    x1 = 0
    x2 = 750
    y1 = 650
    y2 = 1334
    return img[y1:y2, x1:x2]

# B:0,G:1,R:2
def getChanel(img, flag):
    b, g, r = cv2.split(img)
    if flag == 0:
        return b
    elif flag == 1:
        return g
    elif flag == 2:
        return r
    else:
        raise ValueError("No Flag")

# 2値化
def getBinary(img):
    _, thresh = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
    return thresh

def main():
    # 画像入力
    img = cv2.imread('6_5.PNG')
    # 盤面情報取得
    board = getBoard(img)
    # グレースケール画像取得
    blue = getChanel(board, 0)
    # 2値化
    binary = getBinary(blue)

    # 表示
    # cv2.imshow('img', img)
    # cv2.imshow('board', board)
    # cv2.imshow('blue', blue)
    cv2.imshow('binary', binary)

    cv2.waitKey(0)
    cv2.destroyAllWindows()

if __name__ == '__main__':
    try:
        main()
    except ValueError as e:
        print("type:{0}".format(type(e)))
        print("args:{0}".format(e.args))
        print("message:{0}".format(e.message))
        print("{0}".format(e))
```
これ、かなり手順踏んでやってるけど、`cv2.imread('6_5.PNG',0)`でグレースケール画像取れるの忘れてた。

* cv2.IMREAD_COLOR : カラー画像として読み込む．画像の透明度は無視される．デフォルト値
* cv2.IMREAD_GRAYSCALE : グレースケール画像として読み込む
* cv2.IMREAD_UNCHANGED : アルファチャンネルも含めた画像として読み込む

**グレースケール画像**
![blue.png](https://qiita-image-store.s3.amazonaws.com/0/35860/6379a748-2c28-94a2-23e0-3154c5f040e4.png)

**2値化画像**
![binary.png](https://qiita-image-store.s3.amazonaws.com/0/35860/08e2c7c9-001f-9b99-8895-7e76725492b1.png)

これが使える画像なのかはわからないけど、、、四角か丸かぐらいは判断できるでしょう。

### ハフ変換による円形抽出
[前回の記事](https://qiita.com/Ryo87/items/0ffc633fb918cb704566)で「ハフ変換による円検出」というのをやったので、今回のドロップ検出にも適応できるかなと。
※先に言っておくと、検出数やパラメータを適切にしてないとものすごい時間かかるから気をつけてね。

<img width="1011" alt="board_と_boardBlur_と_cimg.png" src="https://qiita-image-store.s3.amazonaws.com/0/35860/f40695a6-58ea-a39f-6ed0-a8a535c8a5c5.png">

[ハフ変換による円検出](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_houghcircles/py_houghcircles.html#hough-circles)こちらの記事をそのまま適応した画像がこちら。

![cimg.png](https://qiita-image-store.s3.amazonaws.com/0/35860/5ab70ba9-9c94-dd32-1a0a-c6782bb9b892.png)

全然わからんｗ
中心点だけピックアップしても600個近く。

![cimg.png](https://qiita-image-store.s3.amazonaws.com/0/35860/23bc07f9-6ed6-e8a6-cd8c-2219d2a8d8f0.png)

### ハフ変換のパラメータを調整してみる。
[ハフ変換(円)](http://opencv.jp/opencv-2svn/cpp/feature_detection.html#cv-houghcircles)

* image – 8ビット，シングルチャンネル，グレースケールの入力画像．
* circles – 検出された円を出力するベクトル．各ベクトルは，3要素の浮動小数点型ベクトル  (x, y, radius) としてエンコードされます．
* method – 現在のところ， CV_HOUGH_GRADIENT メソッドのみが実装されています．基本的には 2段階ハフ変換 で，これについては Yuen90 で述べられています．
* dp – 画像分解能に対する投票分解能の比率の逆数．例えば， dp=1 の場合は，投票空間は入力画像と同じ分解能をもちます．また dp=2 の場合は，投票空間の幅と高さは半分になります．
* minDist – 検出される円の中心同士の最小距離．このパラメータが小さすぎると，正しい円の周辺に別の円が複数誤って検出されることになります．逆に大きすぎると，検出できない円がでてくる可能性があります．
* param1 – 手法依存の 1 番目のパラメータ． CV_HOUGH_GRADIENT の場合は， Canny() エッジ検出器に渡される2つの閾値の内，大きい方の閾値を表します（小さい閾値は，この値の半分になります）．
* param2 – 手法依存の 2 番目のパラメータ． CV_HOUGH_GRADIENT の場合は，円の中心を検出する際の投票数の閾値を表します．これが小さくなるほど，より多くの誤検出が起こる可能性があります．より多くの投票を獲得した円が，最初に出力されます．
* minRadius – 円の半径の最小値．
* maxRadius – 円の半径の最大値．

```sample.py
circles = cv2.HoughCircles(img, cv2.HOUGH_GRADIENT, 1, 100,
                               param1=50, param2=30, minRadius=60, maxRadius=70)
```

とりあえず、minDistを100、minRadiusを60、maxRadiusを70にして画像を再出力してみる。
![cimg.png](https://qiita-image-store.s3.amazonaws.com/0/35860/f48b57ad-9e91-245a-6585-8576c948068b.png)

思ったよりきっちり取れててびっくり。

### 回復ドロップのマッチング
```sample.py
    # 盤面取得
    img_rgb = cv2.imread('6_5.PNG')
    img_rgb = getBoard(img_rgb)
    img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)

    # ドロップ取得
    template = img_gray[85:170, 635:730]

    w, h = template.shape[::-1]

    res = cv2.matchTemplate(img_gray, template, cv2.TM_CCOEFF_NORMED)
    threshold = 0.8
    loc = np.where(res >= threshold)
    for pt in zip(*loc[::-1]):
        cv2.rectangle(img_gray, pt, (pt[0] + w, pt[1] + h), (0, 0, 255), 2)

    cv2.imshow('img_rgb', img_rgb)
    cv2.imwrite('res.png', img_rgb)

    cv2.waitKey(0)
    cv2.destroyAllWindows()
```

**盤面**
![img_gray.png](https://qiita-image-store.s3.amazonaws.com/0/35860/3d80ade5-bc61-7417-a2a5-412372270c57.png)

**回復ドロップ**
![template.png](https://qiita-image-store.s3.amazonaws.com/0/35860/865a97e0-1c09-1c07-775c-52d6363f3945.png)

**結果画像**
![res.png](https://qiita-image-store.s3.amazonaws.com/0/35860/cd3acf29-316c-c65b-4578-9e07d69d2210.png)

### 攻撃色と回復色の認識がとりあえずできた。
```sample.py
    # 盤面取得
    img_rgb = cv2.imread('6_5.PNG')
    img_rgb_board = getBoard(img_rgb)
    img_gray = cv2.cvtColor(img_rgb_board, cv2.COLOR_BGR2GRAY)

    # 攻撃色
    img = cv2.medianBlur(img_gray, 5)
    circles = cv2.HoughCircles(img, cv2.HOUGH_GRADIENT, 1, 100,
                               param1=50, param2=30, minRadius=60, maxRadius=70)

    # 攻撃色に縁取り
    circles = np.uint16(np.around(circles))
    index = 1
    for i in circles[0, :]:
        index = index + 1
        cv2.circle(img_rgb_board, (i[0], i[1]), i[2], (0, 255, 0), 2)
        cv2.circle(img_rgb_board, (i[0], i[1]), 2, (0, 0, 255), 3)

    # 回復ドロップ取得
    template = img_gray[80:170, 635:730]
    cv2.imwrite('template.png', template)

    w, h = template.shape[::-1]

    res = cv2.matchTemplate(img_gray, template, cv2.TM_CCOEFF_NORMED)
    threshold = 0.8
    loc = np.where(res >= threshold)
    # 回復ドロップに縁取り
    for pt in zip(*loc[::-1]):
        cv2.rectangle(img_rgb_board, pt, (pt[0] + w, pt[1] + h), (0, 0, 255), 2)

    cv2.imshow('img_rgb', img_rgb_board)
    cv2.imwrite('res.png', img_rgb_board)

    cv2.waitKey(0)
    cv2.destroyAllWindows()
```

![res.png](https://qiita-image-store.s3.amazonaws.com/0/35860/27d6fd35-a505-40f4-b205-0c652389399c.png)

攻撃色がいまいち綺麗に抜けてないけど、ひとまず課題１を達成することができた。
(右下の青ドロップとか。)

#感想
とりあえず課題１を達成することができました。
複雑なコード書かずにこの程度のことはできることがわかった。