在小程序出來(lái)之初决帖,其實(shí)心里就有一個(gè)想法,用APP自動(dòng)化的方式實(shí)現(xiàn)自動(dòng)跳一跳蓖捶。當(dāng)然不是為了刷分地回。 但由于是游戲,是無(wú)法直接取頁(yè)面元素的俊鱼,因此只能用到圖像識(shí)別刻像。本人對(duì)圖像處理只是有一點(diǎn)點(diǎn)了解,因此一直沒有動(dòng)筆并闲,偶然看到一篇文章细睡,通過(guò)adb+python的方式實(shí)現(xiàn)了。原文在這里帝火。
但是由于作者寫作過(guò)程中有諸多語(yǔ)義不清的地方溜徙,于是在借鑒該作者的方式實(shí)現(xiàn)后決定重新寫一篇,通過(guò)Appium+python的方式實(shí)現(xiàn)犀填。
不過(guò)主要的圖像識(shí)別技術(shù)依然參考該作者的內(nèi)容蠢壹。
需要應(yīng)用到的Python庫(kù)及環(huán)境:
- Appium環(huán)境: 點(diǎn)這里;
提供微信的基本操作宏浩,截圖以及屏幕按壓(點(diǎn)擊)知残; - opencv計(jì)算機(jī)視覺庫(kù):
pip install opencv-python
識(shí)別棋子中心和物件中心的坐標(biāo)點(diǎn); - math提供基本的數(shù)學(xué)運(yùn)算,python內(nèi)置庫(kù)
實(shí)現(xiàn)開平方根求妹,計(jì)算棋子與物件中心的距離乏盐; - numpy數(shù)值計(jì)算擴(kuò)展,主要用于處理各種大型矩陣制恍,圖像的像素內(nèi)容就是以矩陣的形式儲(chǔ)存父能。numpy庫(kù)在安裝opencv-python時(shí)會(huì)同時(shí)安裝。
由于opencv中的像素都是儲(chǔ)存于numpy中净神,處理opencv識(shí)別到的各種坐標(biāo)何吝。
先說(shuō)一下思路:
我們先看一下游戲界面;
玩過(guò)跳一跳的都知道鹃唯,跳一跳游戲其實(shí)就是在前方(上方)出現(xiàn)各種物件爱榕,有各種形狀。通過(guò)估計(jì)棋子(小人)與前方出現(xiàn)物件中心點(diǎn)的位置坡慌,估計(jì)距離黔酥,通過(guò)距離判斷需要點(diǎn)擊的時(shí)間。松開手指洪橘,完成跳躍跪者。重復(fù)此過(guò)程,直到失敗熄求。
那么要自動(dòng)完成這一跳躍操作的關(guān)鍵點(diǎn)就是確定按壓時(shí)間渣玲!
要想確定按壓時(shí)間,就必須取得:
- 棋子與物件中心點(diǎn)的距離弟晚;
- 跳躍系數(shù)忘衍,也就是跳躍單位距離需要按壓的時(shí)間,或者單位按壓時(shí)間跳過(guò)的距離指巡。
首先是計(jì)算棋子與物件中心點(diǎn)的距離:
本文以下內(nèi)容均以小米Note3手機(jī)為測(cè)試機(jī)淑履,分辨率為1080x1920隶垮!
其他機(jī)型需要根據(jù)實(shí)際分辨率調(diào)整藻雪。
上面說(shuō)過(guò),無(wú)法通過(guò)定位工具識(shí)別棋子與物件狸吞,因此就需要用到圖像識(shí)別勉耀。在識(shí)別之前需要通過(guò)Appium提供的截圖方法獲取圖像,并保存在電腦本地蹋偏,然后通過(guò)opencv對(duì)圖像進(jìn)行處理和識(shí)別便斥。
圖片預(yù)處理
- 截圖
用Appium WebDriver中提供的截圖方法獲取手機(jī)屏幕圖片,并儲(chǔ)存在本地文件夾:
# 截取當(dāng)前圖片
driver.driver.save_screenshot('E:/tyt/tyt_origina.png')
- 為減少其他圖形的干擾威始,切出上圖中紅框標(biāo)識(shí)的內(nèi)容枢纠,具體切圖的大小,需要根據(jù)實(shí)際屏幕確定黎棠。
# 讀取圖片
img = cv2.imread('E:/tyt/tyt_origina.png')
# 正常顏色的圖片晋渺,以三維數(shù)組的形式保存每個(gè)像素點(diǎn)的顏色值
# array([[[247, 204, 199], [縱坐標(biāo)[橫坐標(biāo)[像素點(diǎn)的BGR值]]]
# 切出圖片中間位置
roi = img[600:1500, 0:1080]
- 識(shí)別邊緣
通過(guò)cv2提供的Canny算法識(shí)別圖像邊緣
# 邊緣識(shí)別
edges = cv2.Canny(roi, 50, 100)
# 邊緣識(shí)別后圖片變?yōu)槎S數(shù)組镰绎,僅儲(chǔ)存0,255兩種RGB值,也就是黑白兩色木西,黑色0為背景白色255為輪廓
# array([[0, 0, 0, ..., 0, 0, 0], [縱坐標(biāo)[橫坐標(biāo) 黑白兩色0/255]]
Canny算法:
Canny(image, threshold1, threshold2)
主要參數(shù):
image畴栖,圖片內(nèi)容,這里使用的切出來(lái)的圖像八千;
threshold1吗讶,threshold2 閾值范圍,設(shè)置為50,100即可恋捆。
識(shí)別棋子
接下來(lái)就要在識(shí)別后的圖像中確定棋子和新物件的位置:
棋子頂部是圓的照皆,可以用到opencv中提供的霍夫圓變換算法識(shí)別圓,霍夫圓根據(jù)圓的半徑在圖像中尋找對(duì)應(yīng)的圓沸停,并返回該圓圓心所在的坐標(biāo)點(diǎn)纵寝,也就是找到了頭部中心;根據(jù)棋子頭部中心點(diǎn)加上身體所在的高度就能得到底部中心點(diǎn)星立,即圖中綠線部分爽茴。
-
識(shí)別棋子頭部
通過(guò)霍夫圓變換算法獲取圓心所在的位置,需要確定圓的半徑绰垂,可通過(guò)Windows自帶的畫圖板獲取坐標(biāo)室奏,計(jì)算出半徑即可。
1080x1920的棋子頭部半徑大約是30像素左右劲装。獲得半徑后就可以識(shí)別圓了胧沫。
# 通過(guò)霍夫圓變換識(shí)別棋子頭部的圓
circles1 = cv2.HoughCircles(edges, cv2.HOUGH_GRADIENT, 2, 400,
param1=100, param2=50, minRadius=29, maxRadius=31)
# array([[[443. , 289. , 29.8]]], dtype=float32)
cv2.HoughCircles(image, method, dp, minDist, circles, param1, param2, minRadius, maxRadius)
主要參數(shù):
image, 輸入 8-比特占业、單通道灰度圖像绒怨。傳入我們邊緣識(shí)別后的圖片即可;
method谦疾,Hough 變換方式南蹂,目前只有HOUGH_GRADIENT這種方式;
dp念恍, 累加器圖像的分辨率六剥,dp的值不能比1小。先設(shè)置為2吧峰伙;
min_dist疗疟, 該參數(shù)是讓算法能明顯區(qū)分的兩個(gè)不同圓之間的最小距離。一般也就只會(huì)識(shí)別一個(gè)瞳氓,這個(gè)參數(shù)隨便設(shè)置策彤;
param1, 用于Canny的邊緣閥值上限,下限被置為上限的一半店诗。設(shè)置100即可叽赊;
param2,累加器的閥值必搞。設(shè)置為50必指;
min_radius, 最小圓半徑恕洲,由于根據(jù)手機(jī)分辨率塔橡,圓的半徑在30左右 ,因此設(shè)置為29霜第,不能太小葛家,否則有可能識(shí)別到其他的圓;
max_radius泌类,最大圓半徑癞谒,比實(shí)際半徑大1像素即可。至于為何不設(shè)置為30,30刃榨,因?yàn)槭冀K有一點(diǎn)誤差弹砚;
返回值為檢測(cè)到圓的序列,包括圓心坐標(biāo)和半徑枢希。
array([[[443. , 289. , 29.8]]], dtype=float32)即為識(shí)別到的圓443.和289.為圓心坐標(biāo)桌吃,29.8為半徑。float32表示數(shù)據(jù)的類型苞轿。
接下來(lái)取出具體的圓心坐標(biāo)值茅诱。
# 取出第一個(gè)圓
circles = circles1[0]
# array([[443. , 289. , 29.8]], dtype=float32)
# 轉(zhuǎn)化為uint16類型
circles = np.uint16(np.around(circles))
# np.around 四舍六入五成雙
ring = circles[0]
# array([443, 289, 29.8], dtype=uint16)
ring就是圓心的坐標(biāo),對(duì)于棋子搬卒,我們還有兩件事要做瑟俭。
計(jì)算棋子底部中心和消除棋子,計(jì)算底部中心大家都能理解契邀,為什么要消除棋子摆寄,是因?yàn)槿绻挛锛x得太近,棋子頭部的輪廓會(huì)影響對(duì)物件的判斷蹂安。
-
計(jì)算棋子底部中心點(diǎn)
這一步最簡(jiǎn)單椭迎,通過(guò)畫圖板工具,獲取頭部中心點(diǎn)到底部中心點(diǎn)的距離田盈。
通過(guò)頭部中心點(diǎn)加上圖中綠色線標(biāo)識(shí)出的距離既是底部中心點(diǎn)的中心。1080x1920像素的手機(jī)這段距離大約是159缴阎。
# 圓心的y坐標(biāo)+159既是底部中心點(diǎn)的位置
ring[1] += 159
# array([443, 448, 29.8], dtype=uint16)
- 消除棋子
通過(guò)棋子頭部中心點(diǎn)和底部中心點(diǎn)允瞧,計(jì)算出一個(gè)矩形位置,通過(guò)對(duì)這塊位置切片,并全部賦值為0述暂,0表示黑色痹升,將該區(qū)域的全部像素設(shè)置為0,則可以消除這塊區(qū)域畦韭,也就是棋子所在的區(qū)域疼蛾。
# 根據(jù)圓心,計(jì)算圓周圍的矩陣艺配,然后全部設(shè)置為0察郁,相當(dāng)于把包括圓在內(nèi)的所有輪廓消除
# 圓的半徑30多加5個(gè)像素
# 圓上部
ryt = ring[1] - 35
# 圓下部
ryb = ring[1] + 35
# 圓左邊
ryl = ring[0] - 35
# 圓右邊
ryr = ring[0] + 35
edges[ryt:ryb, ryl:ryr] = 0
以上,關(guān)于棋子的圖像處理部分結(jié)束转唉。
識(shí)別新物件
根據(jù)上圖皮钠,可以看出新物件的的y坐標(biāo)是整個(gè)圖最高(y坐標(biāo)最小)赠法,x坐標(biāo)在最右邊麦轰,也就是x坐標(biāo)是整個(gè)圖的最大值。但是有時(shí)候新物件在左邊砖织,因此只能先取出y坐標(biāo)最小的款侵,然后右移一定的像素位置來(lái)計(jì)算新物件的x坐標(biāo),因?yàn)榇藭r(shí)x坐標(biāo)不是在最右邊侧纯。
# edges == 255 取edges數(shù)組中值為255(白色)的所有下標(biāo)
# edges中排列的[縱坐標(biāo),橫坐標(biāo)]的順序
y, x = np.where(edges == 255)
通過(guò)np.where(edges == 255)獲取所有白色的像素點(diǎn)喳坠。由于像素點(diǎn)在數(shù)組中的排列是縱坐標(biāo)在前,橫坐標(biāo)在后茂蚓。因此y壕鹉,x倒過(guò)來(lái)取。
# 找到y(tǒng)軸最小值聋涨,y軸最小值的時(shí)候就是x軸的中心點(diǎn)
index = np.argmin(y)
xmin = x[index]
# 找到x軸最大的點(diǎn)晾浴,即最右端的點(diǎn),此時(shí)的y坐標(biāo)就是y的中心點(diǎn)
# 但是因?yàn)橹虚g識(shí)別有凸起部分牍白,因此要設(shè)定查找位置
ymax = y[np.argmax(x[index:index + 600])]
xmin和ymax就是新物件中心的坐標(biāo)點(diǎn)脊凰。
計(jì)算距離
棋子中心點(diǎn)ring[0], ring[1]和新物件中心點(diǎn)xmin, ymax的距離,相當(dāng)于拋物線上兩點(diǎn)的距離茂腥。因此需要用到拋物線計(jì)算公式狸涌。
拋物線兩點(diǎn)距離公式:點(diǎn)A(a,b)點(diǎn)B(c,d)距離 = 根號(hào)[(a-c)平方+(b-d)平方]
# 拋物線上兩點(diǎn)之間的距離為"根號(hào)((x2-x1) ** 2 + (y2-y1) ** 2)"
dt = math.sqrt((xmin - ring[0]) ** 2 + (ymax - ring[1]) ** 2)
估算跳躍系數(shù)
單位像素距離需要按壓的時(shí)間,大致在1-2.5之間最岗。
需要多嘗試一下帕胆,不同的分辨率不一樣。
1080*1920的屏幕大致在1.365般渡。
ds = int(dt * 1.365)
以上懒豹,核心內(nèi)容都已實(shí)現(xiàn)芙盘。接下來(lái)只需要按壓屏幕即可。
driver.tap([[300, 1000]], ds)
最后跳動(dòng)大約60-80次左右脸秽,物件越來(lái)越小儒老,因此為了增加容錯(cuò)率,需要將物件中心坐標(biāo)略微上移10個(gè)像素左右记餐。
ymax = y[np.argmax(x[index:index + 600])] - 10
當(dāng)然這個(gè)分?jǐn)?shù)不會(huì)在排行榜顯示驮樊。不過(guò)新技能又get了。