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 WindowsPillow、numpy 、pyfirmataopencv-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、numpypip 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形”路径中误差会逐渐累积。这个问题在落点方块较小时有一定的发生概率。我尝试过添加一些纠正算法,但效果不明显。这个误差会在Z形路径中断时(出现连续3个落点在一条直线上)自动修正。如果误差较大棋子即将掉落,可以终止程序,手动修改时间系数纠正。舵机的摆动角度和时间系数没有绝对的数值,需要慢慢尝试,当前使用的时间系数是2.43。可以使用arduino + pyfirmata组合控制舵机,成本比较低,已经可以用arduino啦~。这个游戏在跳了200+次之后方块会变的非常小(如题图),已经不是普通人类所能做到的。研究了外挂之后才知道手玩高分有多难,大家还是不要刷分了,会没朋友的。来自一只正在艰难地转CS的通信狗,并没有二维码。第一次发文章,有很多小问题,欢迎各路大佬指教,给大佬倒茶。
1、总有一些池州麻将玩家喜欢贪图眼前的利益,他们为了做清一色宁可拆舍对子,到头来博彩问答博彩问答,落个“鸡飞蛋打”。