系列文章

透過前面兩篇(安裝篇、測試篇)文章,我們建立了 Python 的環境、使用了 adb 控制模擬器,以及透過 OpenCV 來抓出需要點擊的物件,接下來我們需要直接實際操作模擬器,並且做出一個可模組化的程式。

展示頁面

首先我們可以先讓透過 adb 自動擷取模擬器截圖的動作抽離出來,做成一個 screenshot() 的方法,只要執行這個方法,就會自動執行 adb 指令將模擬器截圖並輸出:

1
2
3
def screenshot():
subprocess.check_output('adb shell /system/bin/screencap -p /sdcard/screencap.png', shell=True)
subprocess.check_output('adb pull /sdcard/screencap.png ./screencap.png', shell=True)

再來我們可以把透過 OpenCV 程式判斷這件事情抽離出來,做成一個 scan_screenshot(prepared) 的方法,每次只要丟目標物件進去,程式就自動抓取截圖,並且將判斷回傳:

1
2
3
4
5
def scan_screenshot(prepared):
screenshot = cv2.imread('./cache/screencap.png')
result = cv2.matchTemplate(screenshot, prepared, cv2.TM_CCORR_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
return {'screenshot': screenshot, 'min_val': min_val, 'max_val': max_val, 'min_loc': min_loc, 'max_loc': max_loc}

最後我們只需要再寫一個自動計算目標物件方位的方法,把 OpenCV 的計算結果丟進去,然後自動算出 x, y 座標:

物件與截圖範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def calculated(result, shape):
mat_top, mat_left = result['max_loc']
prepared_height, prepared_width, prepared_channels = shape

x = {
'left': int(mat_top),
'center': int((mat_top + mat_top + prepared_width) / 2),
'right': int(mat_top + prepared_width),
}

y = {
'top': int(mat_left),
'center': int((mat_left + mat_left + prepared_height) / 2),
'bottom': int(mat_left + prepared_height),
}

return {
'x': x,
'y': y,
}

接著就是將上面抽離出來的方法並實際應用即可,再這邊舉兩個例子,分別是判斷目標物件並執行點擊,以及判斷目標物件,再進行二次判斷才執行點擊:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
if __name__ == '__main__':
while True:
# 不斷刷新模擬器截圖
screenshot()


# 範例一、判斷目標物件並執行點擊
# 先從圖庫當中,找出你想偵測的圖片
target = cv2.imread('./images/XXX.png')
# 丟去跟畫面做比對
result = scan_screenshot(target, screen)
# 判斷畫面是否有跟圖片相符
if result['max_val'] > 0.9999:
# 對模擬器按圖片的中心點位置
points = util.calculated(result, target.shape)
subprocess.check_output('adb shell input tap %d %d' % (x, y), shell=True)


# 範例二、判斷目標物件,再進行二次判斷才執行點擊
# 先從圖庫當中,找出你想偵測的圖片(1)
target = cv2.imread('./images/XXX(1).png')
# 把圖片(1)丟去跟畫面做比對
result = scan_screenshot(target, screen)
# 判斷畫面是否有跟圖片相符
if result['max_val'] > 0.9999:
print('[偵測] 有正在進行的任務,要繼續進行該任務嗎?')
# 先從圖庫當中,找出你想偵測的圖片(2)
target = cv2.imread('./images/XXX(2).png')
# 把圖片(2)丟去跟畫面做比對
result = scan_screenshot(target, screen)
# 對模擬器按圖片的中心點位置
points = calculated(result, target.shape)
subprocess.check_output('adb shell input tap %d %d' % (x, y), shell=True)

以目前這樣的程式來講,仍然有一些問題所在,舉例來說透過 adb 對模擬器進行截圖並儲存起來,再透過 OpenCV 去讀取截圖的這個動作,會對硬碟造成大量讀寫的問題發生,這可能會導致硬碟壽命快速凋零,因此針對這樣的問題也有一個解決方案,就是不要直接將截圖儲存,而是讓它直接在程式之間傳送,走的是記憶體而不是硬碟。

1
2
3
4
5
def screenshot():
pipe = subprocess.Popen("adb shell screencap -p", stdin=subprocess.PIPE, stdout=subprocess.PIPE, shell=True)
image_bytes = pipe.stdout.read().replace(b'\r\n', b'\n')
image = cv2.imdecode(numpy.fromstring(image_bytes, numpy.uint8), cv2.IMREAD_COLOR)
return image

這樣呼叫 screenshot() 方法就可以直接得到當前模擬器的截圖畫面,就可以直接在 scan_screenshot() 判斷目標物件的環節當中,直接去抓取當前模擬器截圖去取代讀取模擬器截圖的動作。

1
2
3
4
5
def scan_screenshot(prepared):
screenshot = screenshot()
result = cv2.matchTemplate(screenshot, prepared, cv2.TM_CCORR_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
return {'screenshot': screenshot, 'min_val': min_val, 'max_val': max_val, 'min_loc': min_loc, 'max_loc': max_loc}

到這邊利用 Python + OpenCV + ADB 實現 Android 模擬器控制就差不多告一段落,如果更伸下去著墨的話,還可以有很多花樣可以探索,就留給大家去摸索了。