淘宝小游戏"玩个球"自动执行
本文记录了为实现本游戏的自动执行而做的探索过程
第一阶段: 通过截屏进行判断
1.1 基本步骤
1) 通过adb shell截屏
2) 判断特定行的蓝色和红色像素数量
3) 通过adb shell发送指令
1.1.1 截屏
首先获得用su获得root权限(后面的步骤需要) 然后用screencap命令截屏, 图片放到放到手机SD卡里, 然后通过pull命令将图片复制到电脑上(d:\ss.png)
su
adb shell screencap sdcard/#swap/ss.png
adb pull /sdcard/#swap/ss.png d:\ss.png
1.1.2 加载图片, 判断颜色
最开始使用C语言编写, 使用altimage.h提供的库.
CImage类是ATL和MFC共用的一个类,其头文件为atlimage.h,主要用于图片文件的打开,显示与保存。这里需要注意的是,在VS2010和VS2012的MFC编程中,不需要将头文件包含进来。MFC中要使用CImage类,必须先将头文件包含进来,可以包含在当前代码的CPP文件中,也可以包含在所属类的头文件中,不过最好还是包含在工程的stdafx.h文件中。CImage总共有39个成员函数。
(百度百科)
首先执行上面的命令, 然后进行图片的判断
system("D:\\input.bat");
image.Load(_T("D:\\ss.png"));
bool result = check(895, image);
其中check函数定义如下, 判断第row行红色像素和蓝色像素哪个多一些.
// 返回 false代表蓝色, true代表红色
bool check(int row, CImage& image) {
int blue = 0;
int red = 0;
for (int i = 0; i < 1080; ++i) {
COLORREF color = image.GetPixel(i, row);
BYTE r = GetRValue(color);
BYTE g = GetGValue(color);
BYTE b = GetBValue(color);
// 当时考虑到方块表面可以有一些轻微的渐变效果 所以设置了RGB的范围 后来发现是纯色
if (r >= 250 && g >= 94 && g <= 103 && b >= 97 && b <= 103) {
red++;
}
if (r >= 50 && r <= 56 && g >= 250 && b >= 250) {
blue++;
}
}
return red > blue;
}
1.1.3 命令发送
使用adb提供的input命令可以模拟触摸操作(需要root权限)
没用root权限直接使用input tap只会显示一个killed, 手机上没有任何反应. 获得root权限之后手机就有反应了, 电脑上没有任何报错.
代码如下, 首先打开一个文件 向里面写入root授权命令和input命令, 然后将adb shell命令的输入定向到该文件
ofstream f("D:\\.input");
bool result = check(895, image);
f << "su" << endl;
if (result[i])
f << "input tap " << 284 << " " << 1606 << endl;
else
f << "input tap " << 797 << " " << 1608 << endl;
system("adb shell < D:\\.input");
1.2 出现的问题及优化
1.2.1 出现的问题
程序根本无法使用! 因为太慢了. root授权需要1s左右, 截屏需要1s左右, tap命令从发出到执行也至少需要1秒左右~
所以我从针对上面的问题进行了如下优化
1.2.2 改用java语言
C++似乎无法获取到adb命令的输入流,所以只能讲命令写到文件里,adb执行完这几条命令就退出了;要执行新的命令必须重启adb,重启就意味着要重新进行root授权,极其浪费时间。
Java语言的优势是不仅可以执行外部程序,还能获得输入流输出流,可以在其它程序执行时向其动态写入命令(代码的参考资料)
try {
Process mainProcess = Runtime.getRuntime().exec("adb shell");
DataOutputStream os = new DataOutputStream(mainProcess.getOutputStream());
os.writeBytes("su" + "\n");
os.flush();
//处理错误输出流
final BufferedReader brError = new BufferedReader(new InputStreamReader(mainProcess.getErrorStream()));
ReaderThread t2 = new ReaderThread(brError, "error");
t2.start();
//处理标准输出流
final BufferedReader br = new BufferedReader(new InputStreamReader(mainProcess.getInputStream()));
ReaderThread t1 = new ReaderThread(br, "std");
t1.start();
os.writeBytes("input tap " + (797 + random.nextInt(30) - 15) + " " + (1608 + random.nextInt(30) - 15) + "\n");
os.flush();
} catch (IOException e) {
e.printStackTrace();
}
改用Java语言之后, 原来的图像处理库就不能用了。经过搜索发现java提供图片读取的处理的功能。
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
Process captureProcess = Runtime.getRuntime().exec(captureCommand);
// TRY
captureProcess.waitFor(); // 等待截图完成
File f = new File("D:\\ss.png");
BufferedImage image = ImageIO.read(f);
result = handle(image, 815);
// CATCH
// 省略
判断函数如下, 读取一行像素缓存到数组中, 然后判断这一行有多少个红色, 多少个蓝色
static int[] colors = new int[1080];
// 处理图片 返回true代表红色
public static boolean handle(BufferedImage image, int row) throws Exception {
int blue = 0;
int red = 0;
image.getRGB(0, row, 1080, 1, colors, 0, image.getWidth()); // 获得第row行像素
for (int i = 0; i < 1080; ++i) {
Color color = new Color(colors[i]);
int r = color.getRed();
int g = color.getGreen();
int b = color.getBlue();
if (r >= 250 && g >= 94 && g <= 103 && b >= 97 && b <= 103) red++;
if (r >= 50 && r <= 56 && g >= 250 && b >= 250) blue++;
}
if (red < 10 && blue < 10)
throw new Exception("异常状况! blue=" + blue + " red=" + red);
return red > blue;
}
1.2.3 一次判断多行
从每一张截图都可以得到4个方块的颜色, 所以首先想到的是一次输出4个命令.
bool result[4];
result[0] = check(895, image);
result[1] = check(815, image);
result[2] = check(737, image);
result[3] = check(658, image);
while (i < 4) {
if (result[i])
// f << "input swipe 615 1600 615 500" << endl;
f << "input tap " << 284 + rand() % 30 - 15 << " " << 1606 + rand() % 30 - 15 << endl;
else
f << "input tap " << 797 + rand() % 30 - 15 << " " << 1608 + rand() % 30 - 15 << endl;
i++;
}
这样做的结果还是失败. 设4个方块为一组, 组内的问题解决了,组之间仍然需要root授权、截屏等漫长的操作。
解决方案是3个方块为一组. 在刚跳到方块2, 还没开始到方块3的起跳时马上进行截图, 并发出命令(要过一会才会真正执行)
if (firstTime)
result[0] = handle(image, 895); // 判断第一行
result[1] = handle(image, 815); // 判断第二行
result[2] = handle(image, 737); // 判断第三行
result[3] = handle(image, 658); // 判断第四行
1.3 本阶段总结
步数越多,小球下落的速度就越快。受限于截图速度和发送命令的速度,做到这里程序可以实现跳140步。
第二阶段: 经过拍照进行判断
2.1 基本步骤
由于截屏速度太慢, 所以我想对手机屏幕拍照, 然后用照片来判断, 这样获得照片的延迟就很小了.
基本步骤如下
1) 拍照
2) 判断颜色
3) 发送命令
2.1.1 拍照
JavaCV是一款开源的视觉处理库,基于GPLv2协议,对各种常用计算机视觉库封装后的一组jar包,封装了OpenCV、libdc1394、OpenKinect、videoInput和ARToolKitPlus等计算机视觉编程人员常用库的接口。
(百度百科)
OpenCVFrameGrabber grabber = new OpenCVFrameGrabber(0);
grabber.start(); //开始获取摄像头数据
CanvasFrame canvas = new CanvasFrame("摄像头");//新建一个窗口
canvas.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
canvas.setAlwaysOnTop(true);
Frame f = grabber.grab(); // 获得一帧图像
canvas.showImage(f); // 显示到窗口中
2.1.2 判断颜色
经过摄像头拍照, 方块的颜色已经不是纯色, 外加摄像头有自动调节色温和亮度和功能, 游戏背景的变化让摄像头不断进行调节, 导致直接判断某一块像素的颜色是否在某个区间已经很不准确了.
我的方案是将两个红色矩形圈住的像素颜色的平均值作为参数(共6个,R1 G1 B1 R2 G2 B2),进行线性分类。
从摄像头采集大量数据(共8000帧)进行训练,4种情况 (左蓝 右蓝 左红 右红)各2000帧。使用某人写的一个fisher线性判别法的分类器(链接)求出线性分类所需的参数(6个系数+1个常数), 然后6个系数分别与6个参数相乘减常数得到结果, 根据结果的正负就是颜色的红蓝.
经过这样处理, 本反应速度可以满足跳到140步.
2.2 并发处理
上面只能140步的瓶颈是拍照+命令执行串行执行的速度仍然不够快.
所以我的方案是将拍照识别和命令执行并发执行(在代码上仍然是串行的),即在第i步起跳之前抓紧判断出第i+1步的颜色。
这种方法的重点就在判断过程。上面的Fisher判断法只能判断两类颜色,这里好像还是只需要判断两类颜色。
但是有个因素需要考虑。在2.1.2节中矩形2圈住的部分是偏右的,为什么不能偏左呢?因为下一层的颜色会对结果产生影响。在下图中,红色圈住的部分是深蓝色,但我们希望这一块是背景色,这样才能让分类结果更准确,所以把方块向右挪了一些。
由于这个原因,第三层中间的方块的采样位置要随着第二层的位置的变化而变化。如下图所示。所以需要用线性分类器求出来第二层的方块所在位置,然后再用线性分类器求出第三层方块的颜色。
在实际操作时发现,用上面的方法好像无法判断出第二层的方块在左边还是在右边。所以就先判断了一下第二层的颜色,再判断第二层的方块位置。代码如下:
// 处理图片 返回true代表红色
public static boolean handle(opencv_core.Mat mat) {
int area = 2 * radius + 1;
area *= area;
int[] sumr = new int[]{0, 0};
int[] sumg = new int[]{0, 0};
int[] sumb = new int[]{0, 0};
// 第一列
for (int location = 0; location < 2; location++)
for (int i = xs[location] - radius; i <= xs[location] + radius; ++i) {
for (int j = ys[location] - radius; j <= ys[location] + radius; ++j) {
int r = mat.ptr(i, j).get(2);
if (r < 0) r += 256;
int g = mat.ptr(i, j).get(1);
if (g < 0) g += 256;
int b = mat.ptr(i, j).get(0);
if (b < 0) b += 256;
mat.ptr(i, j).put(2, (byte) 0);
mat.ptr(i, j).put(1, (byte) 0);
mat.ptr(i, j).put(0, (byte) 0);
sumr[location] += r;
sumg[location] += g;
sumb[location] += b;
}
}
boolean top;
double v1 = sumr[0] / area, v2 = sumg[0] / area, v3 = sumb[0] / area;
double v4 = sumr[1] / area, v5 = sumg[1] / area, v6 = sumb[1] / area;
if (-3.071493634915069e-08 * v1 + -8.965083557914888e-09 * v2 + 5.008942301758934e-08 * v3 + -4.538183589991674e-08 * v4 + 6.484576383927891e-09 * v5 + 1.338740636500615e-08 * v6 - -7.921691543220516e-07 > 0.0) {
top = -3.496725019873052e-07 * v1 + -3.051296334266514e-07 * v2 + 9.296019536868188e-07 * v3 + 4.172851262673971e-07 * v4 + 3.456453947975248e-08 * v5 + -5.255057496652702e-07 * v6 - 3.837558916574416e-05 > 0.0;
} else {
// 判断是红色左还是红色右
top = 1.504201326901337e-07 * v1 + 2.349572163952280e-07 * v2 + -1.274891509349603e-06 * v3 + -9.486338303509874e-08 * v4 + -1.371281460535185e-07 * v5 + 1.015599357102451e-06 * v6 - -1.401218220219478e-05 > 0.0;
}
System.out.print(top ? "左 " : "右 ");
sumr[0] = 0; // 重置颜色累计器
sumr[1] = 0;
sumg[0] = 0;
sumg[1] = 0;
sumb[0] = 0;
sumb[1] = 0;
if (top) {
// 第二列 左中
for (int location = 0; location < 2; location++)
for (int i = xss[0][location] - radius; i <= xss[0][location] + radius; ++i) {
for (int j = yss[0][location] - radius; j <= yss[0][location] + radius; ++j) {
int r = mat.ptr(i, j).get(2);
if (r < 0) r += 256;
int g = mat.ptr(i, j).get(1);
if (g < 0) g += 256;
int b = mat.ptr(i, j).get(0);
if (b < 0) b += 256;
mat.ptr(i, j).put(2, (byte) -128);
mat.ptr(i, j).put(1, (byte) 0);
mat.ptr(i, j).put(0, (byte) 0);
sumr[location] += r;
sumg[location] += g;
sumb[location] += b;
}
}
v1 = sumr[0] / area;
v2 = sumg[0] / area;
v3 = sumb[0] / area;
v4 = sumr[1] / area;
v5 = sumg[1] / area;
v6 = sumb[1] / area;
return 5.418415219721577e-08 * v1 + 2.153937329720428e-08 * v2 + -5.16423473866430e-08 * v3 + 6.26519705171625e-08 * v4 + 6.40218352025572e-09 * v5 + -2.00462022639291e-08 * v6 - 6.79348987631247e-06 > 0;
} else {
// 第二列 中右
for (int location = 0; location < 2; location++)
for (int i = xss[1][location] - radius; i <= xss[1][location] + radius; ++i) {
for (int j = yss[1][location] - radius; j <= yss[1][location] + radius; ++j) {
int r = mat.ptr(i, j).get(2);
if (r < 0) r += 256;
int g = mat.ptr(i, j).get(1);
if (g < 0) g += 256;
int b = mat.ptr(i, j).get(0);
if (b < 0) b += 256;
mat.ptr(i, j).put(2, (byte) 0);
mat.ptr(i, j).put(1, (byte) -128);
mat.ptr(i, j).put(0, (byte) 0);
sumr[location] += r;
sumg[location] += g;
sumb[location] += b;
}
}
v1 = sumr[0] / area;
v2 = sumg[0] / area;
v3 = sumb[0] / area;
v4 = sumr[1] / area;
v5 = sumg[1] / area;
v6 = sumb[1] / area;
return 3.07920338699231e-08 * v1 + -5.73074515948778e-09 * v2 + -2.83755827904865e-08 * v3 + 3.80993052348830e-08 * v4 + -1.78177491190312e-08 * v5 + -9.56819783128036e-09 * v6 - -1.649897238658026e-07 > 0;
}
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int i = 0;
try {
// 打开adb shell
System.out.println("打开adb shell");
Process mainProcess = Runtime.getRuntime().exec("adb shell");
DataOutputStream os = new DataOutputStream(mainProcess.getOutputStream());
os.writeBytes("su" + "\n");
os.flush();
//处理错误输出流
final BufferedReader brError = new BufferedReader(new InputStreamReader(mainProcess.getErrorStream()));
ReaderThread t2 = new ReaderThread(brError, "error");
t2.start();
//处理标准输出流
final BufferedReader br = new BufferedReader(new InputStreamReader(mainProcess.getInputStream()));
ReaderThread t1 = new ReaderThread(br, "std");
t1.start();
// 摄像头预备
OpenCVFrameGrabber grabber = new OpenCVFrameGrabber(0);
grabber.start(); //开始获取摄像头数据
CanvasFrame canvas = new CanvasFrame("摄像头");//新建一个窗口
canvas.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
canvas.setAlwaysOnTop(true);
// 首次检测
Frame f;
if (!canvas.isDisplayable()) {//窗口是否关闭
grabber.stop();//停止抓取
System.exit(2);//退出
}
f = grabber.grab();
canvas.showImage(f);
String cmd = input.nextLine();
if (cmd.charAt(0) == 'b') {
os.writeBytes("input tap " + (797) + " " + (1558) + "\n");
} else if (cmd.charAt(0) == 'r') {
os.writeBytes("input tap " + (284) + " " + (1556) + "\n");
}
os.flush();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("GO!");
// 循环检测
while (true) {
// 拍照 获得图像
if (!canvas.isDisplayable()) {//窗口是否关闭
grabber.stop();//停止抓取
System.exit(2);//退出
}
f = grabber.grab();
opencv_core.Mat mat = converter.convertToMat(f);
try {
boolean r = handle(mat);
if (i == 1) {
if (r)
os.writeBytes("input tap " + (284) + " " + (1556) + "\n");
else
os.writeBytes("input tap " + (797) + " " + (1558) + "\n");
os.flush();
System.out.println("\n发出命令:" + (r ? "红" : "蓝"));
i++;
i %= 19;
} else {
i++;
i %= 19;
System.out.print(r ? "红" : "蓝");
}
} catch (Exception e) {
}
canvas.showImage(converter.convert(mat));
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
2.3 总结
一切完成之后,看着小球逐渐超越了140步,万分高兴,然而最后等来的是大大的失望(T▽T)只能跳到200步!
速度首先的原因是通过adb执行命令的速度太慢了。
我好想没有优化办法了,靠摄像头采集图像的方法就告一段落了。
第三阶段: 使用杀手锏——按键精灵
3.1 前言
以前用过按键精灵电脑版,而且了解过按键精灵手机版,知道它一定能解决这个问题。
但是我不想用,我觉得这个方法太简单了,跟作弊似的!
但是我没有办法
3.2 步骤
手机上安装按键精灵,电脑上安装按键精灵手机助手。
然后新建脚本,复制粘贴以下代码就OK了。代码很简单,判断特定像素点的颜色是蓝的还是红的,然后按某个键
Dim c1,c2
While True
c1 = GetPixelColor(377, 897)
c2 = GetPixelColor(699, 896)
If c1 = "6563FF" or c2 = "6563FF" Then
Tap 285 + Rnd() * 30, 1610 + Rnd() * 30
ElseIf c1 = "FFFF35" or c2 = "FFFF35" Then
Tap 796 + Rnd() * 30, 1593 + Rnd() * 30
End If
Delay 175
Wend
3.3 结论
只要手机不卡,刷几万分不成问题。速度贼快。