クラッシュオブクラウンと画像解析(1)

# やりたいこと
スマホゲームのクラッシュオブクラウン（以下クラクラ）において，対戦の配置画像からいろいろなこと（攻撃施設の密度の可視化や配置の弱点など）が分かりやすくなれば面白いかなと思ったので，pythonの勉強も兼ねて配置画像を解析したいと思います．今回はpythonのOpenCVを用いて入力画像の前処理までしたいと思います．

# 0．環境
MacOS
VScode 1.52.0
Python 2.7.16
numpy==1.18.1
opencv-python==4.4.0.46


# 1．対戦配置について
配置は以下の写真のようなものを想定しています．レイアウトエディタから撮影モードにしてスクリーンショトしたものです．
（この配置はQueen walkersのかたが配られたもので，それを拝借しています．）
![IMG_6807A4D68BD8-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/949600/66d286eb-53a9-e6de-afe6-22c3bc26201c.jpeg)

# 2.画像の加工
クラクラの配置のフォーマットは$48 \times 48$のマス目状になっているため，まずは菱形上の配置を正方形にする必要があります．そこで以下のコードを実行しました．

```
import numpy as np
import cv2

def resize_image():
  # 入力画像の読み込み
  pictrue = 'picture/GAKU2020.jpeg'
  img = cv2.imread(pictrue, cv2.IMREAD_COLOR)
  
  # 高さを定義
  height = img.shape[0]                         
  # 幅を定義
  width = img.shape[1]  
  # 回転の中心を指定                          
  center = (int(width/2), int(height/2))
  # 回転角度
  angle = 45.0
  # 拡大・縮小率
  scale = 1.0

  # 画像の横方向を縮小する
  img = cv2.resize(img , (int(width*0.75), height))
  # 画像を回転
  trans = cv2.getRotationMatrix2D(center, angle , scale)
  img2 = cv2.warpAffine(img, trans, (width,height))

　＃画像の保存
　cv2.imwrite('./picture/GAKU2020_resize.png',img2)

if __name__ == '__main__':
  img2 = resize_image()


```
すると以下のような画像を得ることができます．
![GAKU2020_resize.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/949600/8b2525e0-7768-8834-1115-1a33323a46ac.png)

# 3．画像のトリミング
画像を正方形にすることができました．次にいらない部分のトリミングを行いたいと思います．欲しい画像は下地が緑色の部分なので，緑の部分を抽出しようと考えました．実装したコードは以下の通りです．（ほぼ参考したコードですが）

```
def detect_green_color(img):
    # HSV色空間に変換
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

    # 緑色のHSVの値域1
    hsv_min = np.array([20, 64, 0])
    hsv_max = np.array([90,255,255])

    # 緑色領域のマスク（255：赤色、0：赤色以外）    
    mask = cv2.inRange(hsv, hsv_min, hsv_max)
    
    # マスキング処理
    masked_img = cv2.bitwise_and(img, img, mask=mask)

    return mask, masked_img

```
参考：https://algorithm.joho.info/programming/python/opencv-color-detection/

得られたmaskの画像がこちらです．欲しい部分の正方形がなんとなく分かるようになったと思います．
![green_mask.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/949600/9173b992-6361-8f71-ac60-21da07935ab5.png)

# 4．正方形の検出
最後に上の画像から配置の部分の正方形を検出したいと思います．正方形を検出するコードが検索すると出てきたので，とりあえずそれをそのまま実行してみました．すると，以下のような画像が得られました．

参考：https://www.366service.com/jp/qa/886f2ccd5feba07ec3b78b49744ceb4b
![GAKU2020_all.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/949600/c994e88a-1d0e-6ddb-1c4e-b7e377dc3812.png)

たくさん正方形が検出されました．しかも，若干ずれています．そこで複数検出した正方形のうち一番大きいものを選び，それを縦横の座標を整えて出力するようなものを実装しました．以下がそのコードです．
もっといい方法があると思いますが，思いつかなかったので頭が悪そうな書き方になってしまいました…

```
import numpy as np
import cv2


def resize_image():
　　　　．．．

def detect_green_color(img):
　　　　．．．

# resize_imageの続き
def trim_image(img2):
  # 緑の部分を抽出
  green_mask, green_masked_img = detect_green_color(img2)
  cv2.imwrite("./picture/green_mask.png", green_mask)
  #cv2.imwrite("./picture/green_masked_img.png", green_masked_img)

  square = find_squares(green_mask)
  print(square)
  cv2.drawContours(img2, square, -1, (0, 255, 0), 3)

  # 検知した領域を表示
  cv2.imwrite('./picture/GAKU2020_a.png', img2)


# cosの計算
def angle_cos(p0, p1, p2):
    d1, d2 = (p0-p1).astype('float'), (p2-p1).astype('float')
    return abs( np.dot(d1, d2) / np.sqrt( np.dot(d1, d1)*np.dot(d2, d2) ) )

# 正方形の検出
def find_squares(img):
    img = cv2.GaussianBlur(img, (5, 5), 0)
    squares = []
    for gray in cv2.split(img):
        for thrs in range(0, 255, 26):
            if thrs == 0:
                bin = cv2.Canny(gray, 0, 50, apertureSize=5)
                bin = cv2.dilate(bin, None)
            else:
                _retval, bin = cv2.threshold(gray, thrs, 255, cv2.THRESH_BINARY)
            contours, _hierarchy = cv2.findContours(bin, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
            for cnt in contours:
                cnt_len = cv2.arcLength(cnt, True)
                cnt = cv2.approxPolyDP(cnt, 0.02*cnt_len, True)
                if len(cnt) == 4 and cv2.contourArea(cnt) > 1000 and cv2.isContourConvex(cnt):
                    cnt = cnt.reshape(-1, 2)
                    max_cos = np.max([angle_cos( cnt[i], cnt[(i+1) % 4], cnt[(i+2) % 4] ) for i in range(4)])
                    if max_cos < 0.1:
                        squares.append(cnt)
    
    max_length = 0
    largest_square = None
    for square in squares:
      # 全ての座標の読み込み
      x0, y0 = square[0]
      x1, y1 = square[1]
      x2, y2 = square[2]
      x3, y3 = square[3]

      # 横と縦の長さの平均
      x_length = int(1/2 * (abs(x1 - x2) +  abs(x3 - x0)))
      y_length = int(1/2 * (abs(y0 - y1) +  abs(y2 - y3)))
      length = int(1/2*(x_length+y_length))

      # 新しい座標（仮）
      x0_new = int(1/2 * (x0 + x1)) 
      x1_new = int(1/2 * (x2 + x3))
      y0_new = int(1/2 * (y1 + y2))
      y1_new = int(1/2 * (y0 + y3))
      #print('*old*')
      #print(y0_new)
      #print(y1_new)

      # 横と縦の長さを揃えるための処理
      x_len = x1_new - x0_new
      y_len = y0_new - y1_new

      x_diff = length - x_len
      y_diff = length - y_len

      x0_new += int(x_diff/2)
      x1_new = x0_new + length
      y0_new += int(y_diff/2)
      y1_new = y0_new - length
      #print('*new*')
      #print(y0_new)
      #print(y1_new)

      x_len = x1_new - x0_new
      y_len = y0_new - y1_new

      # 新しい座標
      new_square = np.array([[x0_new, y1_new],
                            [x0_new, y0_new],
                            [x1_new, y0_new],
                            [x1_new, y1_new]])
　　　　# 一番長い辺を持つ正方形を記憶
      if length > max_length:
        max_length = length
        largest_square = new_square

    # データ形状の微調整
    largest = []
    largest.append(largest_square)

    return largest


if __name__ == '__main__':
  img2 = resize_image()
  trim_image(img2)


```
得られた画像がこちらです．

![GAKU2020_a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/949600/07f6f017-0e0a-2461-7a4e-5ca39b2cf5ac.png)

いい感じだけど，左と上が若干ずれているのが気になる．ズレた部分は10ピクセル程度で，配置の1マスが40ピクセル程度だからこれがどう響いてくるか…


# 5．最後に
今回は入力画像を整形して欲しい部分の抽出を行いました．
次は施設を検出できるかどうか試したいと思います．
$48 \times 48$マスのどこにあるか分かればいいから比較的簡単なはず（多分）

(訂正2020/12/29)マス目は$48 \times 48$でなく$44 \times 44$みたいでした．