Appium实现跳一跳
在小程序出来之初,其实心里就有一个想法,用APP自动化的方式实现自动跳一跳。当然不是为了刷分。 但由于是游戏,是无法直接取页面元素的,因此只能用到图像识别。本人对图像处理只是有一点点了解,因此一直没有动笔,偶然看到一篇文章,通过adb+python的方式实现了。原文在这里。
但是由于作者写作过程中有诸多语义不清的地方,于是在借鉴该作者的方式实现后决定重新写一篇,通过Appium+python的方式实现。
不过主要的图像识别技术依然参考该作者的内容。
需要应用到的Python库及环境:
- Appium环境: 点这里;
提供微信的基本操作,截图以及屏幕按压(点击); - opencv计算机视觉库:
pip install opencv-python
识别棋子中心和物件中心的坐标点; - math提供基本的数学运算,python内置库
实现开平方根,计算棋子与物件中心的距离; - numpy数值计算扩展,主要用于处理各种大型矩阵,图像的像素内容就是以矩阵的形式储存。numpy库在安装opencv-python时会同时安装。
由于opencv中的像素都是储存于numpy中,处理opencv识别到的各种坐标。
先说一下思路:
我们先看一下游戏界面;
跳一跳
玩过跳一跳的都知道,跳一跳游戏其实就是在前方(上方)出现各种物件,有各种形状。通过估计棋子(小人)与前方出现物件中心点的位置,估计距离,通过距离判断需要点击的时间。松开手指,完成跳跃。重复此过程,直到失败。
操作过程
那么要自动完成这一跳跃操作的关键点就是确定按压时间!
要想确定按压时间,就必须取得:
- 棋子与物件中心点的距离;
- 跳跃系数,也就是跳跃单位距离需要按压的时间,或者单位按压时间跳过的距离。
首先是计算棋子与物件中心点的距离:
本文以下内容均以小米Note3手机为测试机,分辨率为1080x1920!
其他机型需要根据实际分辨率调整。
上面说过,无法通过定位工具识别棋子与物件,因此就需要用到图像识别。在识别之前需要通过Appium提供的截图方法获取图像,并保存在电脑本地,然后通过opencv对图像进行处理和识别。
图片预处理
- 截图
用Appium WebDriver中提供的截图方法获取手机屏幕图片,并储存在本地文件夹:
# 截取当前图片
driver.driver.save_screenshot('E:/tyt/tyt_origina.png')
- 为减少其他图形的干扰,切出上图中红框标识的内容,具体切图的大小,需要根据实际屏幕确定。
# 读取图片
img = cv2.imread('E:/tyt/tyt_origina.png')
# 正常颜色的图片,以三维数组的形式保存每个像素点的颜色值
# array([[[247, 204, 199], [纵坐标[横坐标[像素点的BGR值]]]
# 切出图片中间位置
roi = img[600:1500, 0:1080]
切割后的图片
- 识别边缘
通过cv2提供的Canny算法识别图像边缘
# 边缘识别
edges = cv2.Canny(roi, 50, 100)
# 边缘识别后图片变为二维数组,仅储存0,255两种RGB值,也就是黑白两色,黑色0为背景白色255为轮廓
# array([[0, 0, 0, ..., 0, 0, 0], [纵坐标[横坐标 黑白两色0/255]]
边缘识别后的图片Canny算法:
Canny(image, threshold1, threshold2)
主要参数:
image,图片内容,这里使用的切出来的图像;
threshold1,threshold2 阈值范围,设置为50,100即可。
识别棋子
接下来就要在识别后的图像中确定棋子和新物件的位置:
棋子顶部是圆的,可以用到opencv中提供的霍夫圆变换算法识别圆,霍夫圆根据圆的半径在图像中寻找对应的圆,并返回该圆圆心所在的坐标点,也就是找到了头部中心;根据棋子头部中心点加上身体所在的高度就能得到底部中心点,即图中绿线部分。
-
识别棋子头部
通过霍夫圆变换算法获取圆心所在的位置,需要确定圆的半径,可通过Windows自带的画图板获取坐标,计算出半径即可。
画图板获取坐标位置
1080x1920的棋子头部半径大约是30像素左右。获得半径后就可以识别圆了。
# 通过霍夫圆变换识别棋子头部的圆
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)
主要参数:
image, 输入 8-比特、单通道灰度图像。传入我们边缘识别后的图片即可;
method,Hough 变换方式,目前只有HOUGH_GRADIENT这种方式;
dp, 累加器图像的分辨率,dp的值不能比1小。先设置为2吧;
min_dist, 该参数是让算法能明显区分的两个不同圆之间的最小距离。一般也就只会识别一个,这个参数随便设置;
param1, 用于Canny的边缘阀值上限,下限被置为上限的一半。设置100即可;
param2,累加器的阀值。设置为50;
min_radius, 最小圆半径,由于根据手机分辨率,圆的半径在30左右 ,因此设置为29,不能太小,否则有可能识别到其他的圆;
max_radius,最大圆半径,比实际半径大1像素即可。至于为何不设置为30,30,因为始终有一点误差;
返回值为检测到圆的序列,包括圆心坐标和半径。
array([[[443. , 289. , 29.8]]], dtype=float32)即为识别到的圆443.和289.为圆心坐标,29.8为半径。float32表示数据的类型。
接下来取出具体的圆心坐标值。
# 取出第一个圆
circles = circles1[0]
# array([[443. , 289. , 29.8]], dtype=float32)
# 转化为uint16类型
circles = np.uint16(np.around(circles))
# np.around 四舍六入五成双
ring = circles[0]
# array([443, 289, 29.8], dtype=uint16)
ring就是圆心的坐标,对于棋子,我们还有两件事要做。
计算棋子底部中心和消除棋子,计算底部中心大家都能理解,为什么要消除棋子,是因为如果新物件离得太近,棋子头部的轮廓会影响对物件的判断。
-
计算棋子底部中心点
这一步最简单,通过画图板工具,获取头部中心点到底部中心点的距离。
棋子
通过头部中心点加上图中绿色线标识出的距离既是底部中心点的中心。1080x1920像素的手机这段距离大约是159。
# 圆心的y坐标+159既是底部中心点的位置
ring[1] += 159
# array([443, 448, 29.8], dtype=uint16)
- 消除棋子
通过棋子头部中心点和底部中心点,计算出一个矩形位置,通过对这块位置切片,并全部赋值为0,0表示黑色,将该区域的全部像素设置为0,则可以消除这块区域,也就是棋子所在的区域。
# 根据圆心,计算圆周围的矩阵,然后全部设置为0,相当于把包括圆在内的所有轮廓消除
# 圆的半径30多加5个像素
# 圆上部
ryt = ring[1] - 35
# 圆下部
ryb = ring[1] + 35
# 圆左边
ryl = ring[0] - 35
# 圆右边
ryr = ring[0] + 35
edges[ryt:ryb, ryl:ryr] = 0
以上,关于棋子的图像处理部分结束。
识别新物件
识别图示根据上图,可以看出新物件的的y坐标是整个图最高(y坐标最小),x坐标在最右边,也就是x坐标是整个图的最大值。但是有时候新物件在左边,因此只能先取出y坐标最小的,然后右移一定的像素位置来计算新物件的x坐标,因为此时x坐标不是在最右边。
# edges == 255 取edges数组中值为255(白色)的所有下标
# edges中排列的[纵坐标,横坐标]的顺序
y, x = np.where(edges == 255)
通过np.where(edges == 255)获取所有白色的像素点。由于像素点在数组中的排列是纵坐标在前,横坐标在后。因此y,x倒过来取。
# 找到y轴最小值,y轴最小值的时候就是x轴的中心点
index = np.argmin(y)
xmin = x[index]
# 找到x轴最大的点,即最右端的点,此时的y坐标就是y的中心点
# 但是因为中间识别有凸起部分,因此要设定查找位置
ymax = y[np.argmax(x[index:index + 600])]
xmin和ymax就是新物件中心的坐标点。
计算距离
棋子中心点ring[0], ring[1]和新物件中心点xmin, ymax的距离,相当于抛物线上两点的距离。因此需要用到抛物线计算公式。
抛物线两点距离公式:点A(a,b)点B(c,d)距离 = 根号[(a-c)平方+(b-d)平方]
# 抛物线上两点之间的距离为"根号((x2-x1) ** 2 + (y2-y1) ** 2)"
dt = math.sqrt((xmin - ring[0]) ** 2 + (ymax - ring[1]) ** 2)
估算跳跃系数
单位像素距离需要按压的时间,大致在1-2.5之间。
需要多尝试一下,不同的分辨率不一样。
1080*1920的屏幕大致在1.365。
ds = int(dt * 1.365)
以上,核心内容都已实现。接下来只需要按压屏幕即可。
driver.tap([[300, 1000]], ds)
最后跳动大约60-80次左右,物件越来越小,因此为了增加容错率,需要将物件中心坐标略微上移10个像素左右。
ymax = y[np.argmax(x[index:index + 600])] - 10
分数
当然这个分数不会在排行榜显示。不过新技能又get了。