更新了基于OpenCV的算法,计算更准确。
还有Arduino实现,溜了溜了
——————————-
感谢来自 @神奇的战士 的跳跃距离算法
原项目地址:wangshub/wechat_jump_game
知乎专栏:教你用Python来玩微信跳一跳
1.3 再更: 悄咪咪地加上Arduino版
1.3 又更了: 竟然有1k赞还上了日报,一本满足。昨天研究了一晚上,加入了大家喜爱的OpenCV。现在计算准确率已经很好了,不会出现误差累积。感谢 @船D长 :用Python+Opencv让电脑帮你玩微信跳一跳 给我的启发,大神的代码简洁优雅,非常受用。
1.2 更:谢谢大家的400赞,非常开心。想说一下,我是因为手边只有树莓派才用树莓派控制舵机的,它毕竟是一台200+的小型计算机,肯定是大材小用了。想要自己动手做一个的知友们可以不用急着买树莓派,给我一两天的时间。我的arduino已经到啦,正在测试!
本项目源码: yangyiLTS/wechat_jump_game_iOS
认真写的一个简介
现在已有的跳一跳辅助原理有以下这些:
日天派:
- 直接抓取post请求包修改分数,服务器不对分数进行验证,想改多少改多少
平民方法:
基本步骤:1、获取游戏画面;2、图像分析计算跳跃距离;3、模拟触摸手机屏幕进行游戏。
其中针对不同平台也有不同的实现方案:
- Android平台:adb工具实现截图和触摸,PC或手机实现图像分析
- iOS平台+Mac:使用Mac的WDA工具,原理同adb工具。
- iOS但没有Mac:我的方法可能可以解决这个问题
先上效果:
基本思路是:
- 使用iOS自带Airplay服务将游戏画面投影到电脑上。
- 使用Pillow库截取电脑屏幕,获得游戏画面。
- 使用OpenCV分析图片,计算出跳跃距离,乘以时间系数获得按压时间。
- 将按压时间发送至树莓派/Arduino,树莓派/Arduino控制舵机点击手机屏幕。
运行环境&工具
- Python 3.6 in Windows
- Pillow、numpy 、pyfirmata
- opencv-python
- 局域网环境
- iToools Airplayer
- 树莓派 或 Arduino
- SG90 舵机
- 杜邦线、纸板
- 一小块海绵
- 橙子或其它多汁水果(可选)
原理&步骤
下载源码
- 下载wechat_autojump_iOS&Win_opencv.py到Windows。
- 如果使用树莓派,下载 servo_control.py到树莓派。
- 如果使用Arduino,下载servo_control_arduino.py到Windows,并且确保windows已经装好Arduino驱动和Arduino IDE。
舵机部分
- 拿一根杜邦线粘在舵机的摆臂上,并且用纸板固定舵机到合适高度,如图:
- 取一小块海绵,约10mm*10mm*5mm,不必太精确。海绵中间挖一个小洞。大概是这样:
- 海绵上滴水浸透,放在手机屏幕上“再来一次”的位置。杜邦线的另一头插进橙子。(触发电容屏需要在屏幕上形成一个电场,我尝试过连接干电池负极的方案,但是效果不理想,最后不得已拿了室友的一个橙子。当然,一直捏着或者含着导线也是可以的。)
如果使用树莓派
- 舵机连接上树莓派,电源使用5v(Pin #04,Pin #06),舵机控制线接在GPIO18(Pin #12)。
- 树莓派(OS:Raspbian Jessie)连接上局域网。
- 打开 servo_control.py,这里需要根据实际安装位置调整舵机高点和低点位置(范围: 2.5~12.5)
servo_down = 3.8
servo_up = 5
- 海绵放在“再来一次”的位置可以自动重新开始,然后就会一直自动刷分
- 在wechat_autojump_iOS&Win_opencv.py里
文档的开头需要注释掉这句,这是Arduino使用的
#from servo_control_arduino import arduino_servo_run
设置树莓派的ip地址
ip_addr = '192.168.199.181'
main()函数里面需要选择 send_time()
# #send_time() 为树莓派控制函数
send_time(t)
# #arduino_servo_run() 为arduino控制函数
##arduino_servo_run(t/1000)
- 最后树莓派上运行servo_control.py ,监听9999端口,等待Win的计算结果
如果使用Arduino
- Arduino请选择Arduino UNO或Arduino Mega,因为pyfrimata库不支持Arduino Nano。入门级的Arduino UNO成本在80RMB左右。
- Arduino需要烧入预置的StandardFirmata程序,在Arduino IDE的自带示例里面可以找到
- 在工具—端口可以看到当前Arduino连接的串行端口,记下来等下要用到。
- 安装pyfirmata,cmd运行
pip install pyfirmata
- 把舵机连接上Arduino,舵机有一个三线的接口。黑色(或棕色)的线是接地线,红线接+5V电压,黄线(或是白色或橙色)接控制信号端。舵机的电源线直接接在Arduino的+5V输出和GND上,控制信号端接在Digital 3输出口(程序设置是3号口,可以修改,但是必须是支持PWM输出的接口)
如果做完下面步骤,程序跑起来之后,发现舵机即使不动也会发出 “滋滋”的声音而且动作缓慢,是因为电脑USB口供电不足所致。这个时候需要对舵机使用外接5V电源,接上5V电源之后,还要把外接电源的地线(负极)跟Arduino的地线(板上的GND口)连在一起。
- 打开servo_control_arduino.py,这是Arduino的控制脚本
这里填入上一步看到的端口
# 修改串口编号 如果Arduino驱动正确,在Arduino IDE可以看到串口编号
serial_int = 'COM3'
这里根据Arduino的型号选择,不需要的那行注释或删掉
# 如果是Arduino UNO 使用这一行
board = pyfirmata.Arduino(serial_int)
# 如果是Arduino Mega 使用这一行 pyfirmata库暂不支持Nano
board = pyfirmata.ArduinoMega(serial_int)
然后调试一下舵机的最高点和最低点
# 设置舵机的高点和低点 单位:角度
# 范围 0-180°
servo_high = 45
servo_low = 37
舵机要根据实际的安装位置调试,运动幅度不宜太大,直接运行servo_control_arduino.py文件舵机会按设定位置来循环三次,如果舵机运动正常,则Arduino部分工作正常。
- 打开wechat_autojump_iOS&Win_opencv.py,也有一些地方需要配置
这行代码位于文件开头,确保没有被注释
from servo_control_arduino import arduino_servo_run
到文档的靠后的部分找到main()函数,其中
控制函数选择 arduino_servo_run(),需要把send_time(t)注释掉
# #send_time() 为树莓派控制函数
# send_time(t)
# #arduino_servo_run() 为arduino控制函数
arduino_servo_run(t/1000)
Windows 部分
- 下载Airplayer(免安装,暂无捆绑)
- 配置Airplayer,画质什么的统统调到最高。启动iPhone上的Airplay,然后可以在电脑上看到iPhone画面,游戏运行时需要Airplayer全屏显示。
- 安装opencv-python、numpy
pip install numpy
pip install opencv-python
- 下载wechat_autojump_iOS&Win_opencv.py,我的显示器分辨率是1920*1080,手机是iPhone7。如果使用不同的设备需要注意更改时间系数等参数。
- 安装Pillow库,本文使用Pillow库的ImageGrab截屏,截屏代码:
im = ImageGrab.grab((654, 0, 1264, 1080)) im.save('a.png', 'png')
其中(654, 0, 1264, 1080)是截屏的范围,我的显示器分辨率是1080p,截取屏幕中间的部分得到的图片大小是610*1080,但这个时候图片最左边的一列的像素是黑色的。
- 全部完成后,运行wechat_autojump_iOS&Win_opencv.py
是因为截图残留的黑边所致,这个黑边出现在截图的左边或者右边都会导致落点的计算偏差,打开screenshot_backups文件夹里面的图片会发现计算的轨迹像上图一样飘到整个图的上方。这时候的解决办法是:
找到pull_screenshot()函数:
# 使用PIL库截取Windows屏幕
def pull_screenshot():
im = ImageGrab.grab((654, 0, 1264, 1080))
im.save('a.png', 'png')
代码中(654, 0, 1264, 1080),表示截图的坐标。其中654,1264为截图的左边界和右边界,需要修改这两个边界使截图的尺寸变小。
举个例子,我发现默认参数的情况下出来的截图左边有3个像素的黑边,右边有1个像素的黑边,这个时候截图函数需要改成:
# 使用PIL库截取Windows屏幕
def pull_screenshot():
im = ImageGrab.grab((657, 0, 1263, 1080)) # 左边增加3个像素,右边减少一个
im.save('a.png', 'png')
修改完截图函数之后还需要修改默认图片的宽高,位置是在# Magic Number下面:
# Magic Number,不设置可能无法正常执行,请根据具体截图从上到下按需设置
under_game_score_y = 170 # 截图中刚好低于分数显示区域的 Y 坐标
press_coefficient = 2.38 # 长按的时间系数,
piece_base_height_1_2 = 10 # 二分之一的棋子底座高度,可能要调节
# 图片的宽和高
w,h = 610,1080
继续上面的例子,这个时候图片的宽和高需要改成
# 图片的宽和高
w,h = 606,1080
然后再次运行程序检查截图是否还有黑边。
OpenCV算法详解
- 本算法主要使用opencv和numpy两个库,首先要导入
import cv2
import numpy as np
- 使用OpenCV模板匹配,找到棋子
棋子是一个非常特殊的目标,用PS把它抠出来,保存为模板使用OpevCV的模板匹配函数,准确率几乎完美。
meth = eval('cv2.TM_CCORR_NORMED') piece_template = cv2.imread('piece.png',0) # 棋子模板 # 模板匹配 获取棋子坐标 def find_piece(img): res = cv2.matchTemplate(img, piece_template, meth) min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) piece_x, piece_y = max_loc # cv2.matchTemplate函数返回的是模板匹配最大值左上角的坐标 # 下面修正为棋子底盘中点坐标 piece_x = int(piece_x + piece_w / 2) piece_y = piece_y + piece_h - piece_base_height_1_2 return piece_x, piece_y
其中piece.png是棋子模板,长这样:
这个模板是必须的,但是它只适配610*1080的截图尺寸,如果分辨率跟我的有差异,需要另外扣一个模板,保存为png格式。如果对opencv没兴趣的话看到这里就可以跳过了。GitHub上还有其它目标的模板,但是不是一定要重新扣,原因下面讲。
- 对其它特殊目标尝试模板匹配
我最初的想是对有加分的特殊目标(徐记士多,魔方,下水道,播放器)使用模板匹配,通过函数返回值使主函数增加延时,让我们可以吃到特殊目标的加分。但是后来发现模板匹配的效果不理想,可能是选用的匹配算法问题?或是模板问题?稍后尝试修复。
现在wechat_autojump_iOS&Win_opencv.py文件里有一段代码是进行特殊目标模板匹配的,但是因为我把置信度阈值调得很高(低了又乱匹配),所以匹配成功率非常低。如果不成功,则采用下面的算法寻找目标。
- 对图片进行边缘检测
接下来继续寻找落点坐标,现在要把图像的边缘提取出来,游戏界面都是纯色,提取边缘非常容易:
代码是
img2 = cv2.GaussianBlur(img2, (3, 3), 0) # 先对图片高斯模糊
img_canny = cv2.Canny(img2, 1, 10) # 执行canny函数
输出的图像已经变成只有边缘的二值图像了
- 尝试模板匹配小圆点
提取边缘之后尝试对连击之后的小圆点进行模板匹配,但是效果一样不理想,大部分时候会跳过这一步。
- 找到目标落点
到了最后一步,就是找到棋子的落点,在代码中即board_,board_y。这个点有几个特点:
- 落点方块(圆柱)的最高点是整个图的最高点,先定义为board_y_top,这里我们已经排除了分数部分,背景和有可能高过方块的棋子部分。
- 落点平面的形状是对称的菱形或者椭圆形,确实有个别特殊的情况我们先不在意这些细节。然后这些形状在垂直方向是轴对称的,所以board_y_top一定在垂直的对称轴上。
- 同时落点平面也是水平方向轴对称的,并且从上往下遍历的第一个宽度最大的点是水平对称轴的位置。随便搞个图
然后我的思路是先找board_y_top:
# 遍历起点为分数下沿
board_y_top = under_game_score_y
for i in img_canny[under_game_score_y:]:
if max(i): # i是一整行像素的list,max(i)返回最大值,一旦最大值存在,则找到了board_y_top
break
board_y_top += 1
# board_y_top的像素可能有多个 对它们的坐标取平均值
board_x = int(np.mean(np.nonzero(img_canny[board_y_top])))
然后从board_y_top开始找图形的侧边缘,因为是对称图形只要找左右边缘之一就可以了。但是在两个落点非常近的时候,棋子会挡住其中一个边缘,造成影响。所以先根据棋子位置判断棋子在目标落点的左边还是右边,再选择与棋子不同的位置寻找侧边沿
x1 = board_x
fail_count = 0
if board_x > piece_x:
for i in img_canny[board_y_top:board_y_top+80]:
try:
x = max(np.nonzero(i)[0])
except:
pass
if x > x1:
x1 = x
board_y += 1
if fail_count < 5 and fail_count != 0:
fail_count -= 1
elif fail_count > 5 and board_y - board_y_bottom >10:
result = 1
board_y -= 3
break
elif fail_count > 5 and board_y - board_y_bottom <= 10:
result = 0
break
else:
fail_count += 1
else:
for i in img_canny[board_y_top:board_y_top+80]:
try:
x = min(np.nonzero(i)[0])
except:
pass
if x < x1:
x1 = x
board_y += 1
if fail_count < 5 and fail_count != 0:
fail_count -= 1
elif fail_count > 5 and board_y - board_y_bottom > 10:
board_y -= 3
result = 1
break
elif fail_count > 5 and board_y - board_y_bottom <= 10:
result = 0
break
else:
fail_count += 1
这段代码非常的不pythonic,得想办法优化,其中零零碎碎的整数是一些容差参数,因为在像素角度不是绝对的圆形和方形,在方型平面上的效果会比圆形平面好,但是总体效果都很不错。
- 最后,在上面那个算法抽风的情况下,采用原来的旧算法补救,可以说是十分之稳了
if result == 0:
board_y = piece_y - abs(board_x - piece_x) * math.sqrt(3) / 3
问题&其它
- 采用新版算法后,计算上的误差已经很小,可以查看screenshot_backups/文件夹,看是否得到正确的计算结果。但是仍然无法一直连续击中中心,这是由于舵机的物理误差引起的,需要调节好时间系数,舵机的高点和低点。如果采用海绵+水的接触方案,注意接触面的高度会因为水的蒸发而改变。评论区也有锡纸接触的方案,就看你们喜欢拉。
- (1.3 已解决)由于是物理点击屏幕,会产生一定的操作误差。操作误差由时间常数误差、舵机运动时间、杜邦线触点插进海绵的深度等等因素引起。而当前使用的算法在一种情况下会出现误差叠加的问题。
如图:在绿色方块跳至灰色方块的过程中,出现操作误差。连续“Z形”路径中误差会逐渐累积。这个问题在落点方块较小时有一定的发生概率。我尝试过添加一些纠正算法,但效果不明显。这个误差会在Z形路径中断时(出现连续3个落点在一条直线上)自动修正。如果误差较大棋子即将掉落,可以终止程序,手动修改时间系数纠正。
- 舵机的摆动角度和时间系数没有绝对的数值,需要慢慢尝试,当前使用的时间系数是2.43。
- 可以使用arduino + pyfirmata组合控制舵机,成本比较低,已经可以用arduino啦~。
- 这个游戏在跳了200+次之后方块会变的非常小(如题图),已经不是普通人类所能做到的。研究了外挂之后才知道手玩高分有多难,大家还是不要刷分了,会没朋友的。
来自一只正在艰难地转CS的通信狗,并没有二维码。第一次发文章,有很多小问题,欢迎各路大佬指教,给大佬倒茶。
转载自:https://zhuanlan.zhihu.com/p/32526110