PythonにBrain Warsをプレイさせてみた１

# 概要
画像処理の練習がてら、PythonにBrain Warsをプレイさせてみた

# [Brain Wars](http://translimit.co.jp/services/brainwars/)とは?
シンプルな内容のミニゲームを通じて、世界中のユーザーとリアルタイムで対戦し、スコアを競う「リアルタイム対戦型脳トレ」。実際にプレイしたことがある方も多いのではないだろうか？
<img src="https://qiita-image-store.s3.amazonaws.com/0/185448/030eee96-9d26-dc87-159d-3690f9d73966.jpeg" width=50%>



# 今回プレイするゲーム
アプリ内には21種類のゲームが用意されているが、今回はその中から`Matching`を選択した。表示されたカードの中からマッチしているペアを見つけてタップするシンプルなゲーム
<img src="https://qiita-image-store.s3.amazonaws.com/0/185448/295311a6-92a9-804a-1002-acd44a9b2436.png" width=50%>


# プログラム動作
こんな感じで動作します。ちなみに私のベストスコアは23なので、少なくとも私よりは遥かに強い
<img src="https://qiita-image-store.s3.amazonaws.com/0/185448/bed03f17-977f-8d81-4c77-8d18e224c14a.gif" width=80%>

流れはざっくりとこんな感じ↓
1. カードが表示されている領域をキャプチャ
2. キャプチャした画像から、各カードの画像と座標を取得
3. 画像の類似度（SSIM）を算出して、マッチペアを特定
4. マッチペアをクリック
5. 1~4をゲームが終わるまで繰り返す

# 解説
どのようにプログラムを実装したか順番に解説していく

## 環境
- OS: Ubuntu 18.04.1 LTS
- Python version: 3.6.5
- スマホ: Huawei P10 Lite

Brain Warsはスマホでしかプレイできない。そこで、[Vysor](https://chrome.google.com/webstore/detail/vysor/gidgenkbbabolejbgbpnhbimgjbffefm)というChromeの拡張プラグインを使って、Ubuntuからスマホを操作してプレイした

## 使用した主なライブラリと役割
1. `mss`: スクリーンキャプチャ
2. `opencv`: カードの輪郭抽出や座標計算
3. `scikit-image`: 画像の類似度（SSIM）の計算
4. `pyautogui`: クリック自動化


## 1. カード表示領域のキャプチャ
最初は`PIL`の`ImageGrab`を使おうと思ったのだが、どうやら[MacとWindowsのみしかサポートしていない](https://pillow.readthedocs.io/en/3.0.x/reference/ImageGrab.html)とのことなので、[`mss`](https://python-mss.readthedocs.io/index.html)というライブラリを使用した。ゲームのレベルが上がると、カードの行数が3列に増えるので、キャプチャ領域は少し広めに設定してある

<img src="https://qiita-image-store.s3.amazonaws.com/0/185448/541188c2-6568-559d-4458-37d6bc9948ba.jpeg" width=50%>


## 2. 各カードの画像と座標の取得
1. 画像をGray-Scaleに変換
<img src="https://qiita-image-store.s3.amazonaws.com/0/185448/eef34ca1-9f90-0062-bf3f-aa5046afb211.jpeg" width=50%>


2. バイナリー化
<img src="https://qiita-image-store.s3.amazonaws.com/0/185448/9ccd8247-eadd-11b8-5fbc-08cea7b783b4.jpeg" width=50%>

3. Bounding Box検出
<img src="https://qiita-image-store.s3.amazonaws.com/0/185448/b0174753-71ec-934d-8143-6d5207c2ba71.jpeg" width=50%>

4. Bounding Boxの座標から、各カードの画像と中心座標を取得


## 3. マッチペアの特定
画像の類似度（SSIM）の計算には、`scikit-image`の[`compare_ssim`](http://scikit-image.org/docs/dev/auto_examples/transform/plot_ssim.html)を使用した。各カードの組み合わせ（カード枚数が6枚の場合だと、15パターン）毎にSSIMを算出し、スコアが最大になるものをマッチペアとした

## 4. マッチペアのクリック
`2`で求めた各カードの中心座標と、`3`で求めたマッチペアのIndexから、クリック座標を決定した。クリックの自動化には、`pyautogui`を使用した。非常に使い勝手の良いライブラリで気に入っている
 

# コード
```python
import mss
import numpy as np
import time
import cv2
import pyautogui as pag
from skimage.measure import compare_ssim as ssim


def contour_center(contour):
	"""Compute the center coordinates of a contour"""
	M = cv2.moments(contour)
	cx = int(M['m10'] / (M['m00'] + 1))
	cy = int(M['m01'] / (M['m00'] + 1))
	return cx, cy


def process_img(img):
	"""Extract card images and compute their locations"""
	cards = []
	locs = []
	img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
	_, thresh = cv2.threshold(img_gray, 245, 255, cv2.THRESH_BINARY)
	_, cnts, _ = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

	# filter contours by area
	area_thresh = 6000
	cnts = list(filter(lambda x: cv2.contourArea(x) > area_thresh, cnts))

	for i, cnt in enumerate(cnts):
		x, y, w, h = cv2.boundingRect(cnt)
		if i == 0: resize_h, resize_w = h, w
		card = thresh[y:y + h, x:x + w]
		card = cv2.resize(card, (resize_w, resize_h))
		cards.append(card)

		cx, cy = contour_center(cnt)
		locs.append((cx, cy))

	# sort cards and locs from top left to bottom right
	cards = [card for _, card in sorted(zip(locs, cards), key=lambda x: [x[0][1], x[0][0]])]
	locs = sorted(locs, key=lambda x: [x[1], x[0]])
	return cards, locs


def find_match(cards):
	"""Find a matching pair"""
	num_card = len(cards)
	max_score = 0
	for i in range(num_card - 1):
		for j in range(i + 1, num_card):
			score = ssim(cards[i], cards[j])
			if score > max_score:
				match_pair = (i, j)
				max_score = score
	return match_pair


def grab_screen(bbox):
	"""Capture the specified area on the screen"""
	with mss.mss() as sct:
		left, top, width, height = bbox
		grab_area = {'left': left, 'top': top, 'width': width, 'height': height}
		img = sct.grab(grab_area)
		return np.array(img)[:, :, :3]


def main():
	bbox = (785, 300, 1215 - 785, 740 - 300)
	while True:
		img = grab_screen(bbox)
		cards, locs = process_img(img)
		if len(locs) < 6:
			print('Done')
			exit()
		match_pair = find_match(cards)
		print('Matching pair:', match_pair)
		for idx in match_pair:
			x, y = locs[idx]
			pag.click(bbox[0] + x, bbox[1] + y)
		time.sleep(0.25)


if __name__ == '__main__':
	main()

```

# 今後の展望
`Matching`以外のゲームも自動化してみる予定