上一篇博客分享了如何一笔画完的关卡破解,这篇博客分享一下自己做的一个demo,先上个动图
动图略大,需要加载。
源码在文章最后
下面开始正题,说一下我做的想法和过程,其中对二值化的过程做了一定优化
//选择进入 未通过 关卡
//破解当前关
//1.adb截屏传到电脑
//2.打开图片,创建副本,对其进行二值化,阈值240
//3.识别以上二值化的图像获得行列数,方格大小,方格间隙,以及理论上第一个方格的屏幕坐标,最后生成初始矩阵
//4.对内存中的原图片副本进行二值化,阈值180,
//5.对以上图像识别,获得起点
//6.求出起点对应下标,初始化矩阵
//7.对由图片分析出的迷宫进行求解
//8.输出第一个解 + 另一个线程异步用adb进行破解
//点击确定
即主要流程为破解当前关,其中破解方式上一篇中已经提到,暴力破解即可,这一篇主要是怎么实现自动获取当前关卡的对应矩阵,以及自动点击实现全自动破解,这儿以java为主,不了解adb(Android Debug Brige)的小伙伴自行百度咯。
难度主要是在如何让计算机识别一张图片,并把这张图片转换成我想要的数据格式。
我们需要用到的参数包括:
上一篇设计的破解算法过程:声明 array[row][column]; → 初始化这个数组 → 破解。其中声明数组需要知道格子行row、列的个数column ,初始化数组需要知道每个格子的状态(空的、起点、障碍),初始化数组需要知道array每个元素对应的图的格子的像素坐标和状态。
模拟点击需要知道array[][]数组中每个变量对应的像素坐标。
求array[][]数组中每个变量对应的像素坐标不容易,但可以求array[0][0]对应的实际坐标和格子的边长L,格子间隔gap即可:
array[i][j]对应的实际屏幕坐标 = (array[0][0]的横坐标 + (L + gap)* j ,array[0][0]的纵坐标 + (L + gap)* i)
array[][]数组中每个变量对应的初始值可以看对应的实际像素位置是不是黑色确定,起点另算。
需求分析:获取共有多少行、列、起点位置、哪些位置不能走、array[0][0点对应的坐标、每个格子的边长,矩形的间隙
得到以下模型
public static class Result_Of_Analysis{
public static int L; //格子的边长
public static int Lg; //格子长+格子缝隙
public static int[][] array; //要求解的数组
public static MyPosition firstPosition; //第一个像素点的位置
public static int startI; //数组起点i坐标
public static int startJ; //起点数组j坐标,这两个变量确定一个点
}
(由于格子大小不固定,因此需要图片识别来获取)下图是我获取这些参数的方式(没有接触过图像识别,完全自己考虑的,可应该不是很正规,欢迎补充识别方式)
其中第一张图片是截图的原图
第二种图片是进行阈值240的二值化
第三张图片是二图通过检测矩形确定 行数、列数、以及Lg(格子边长L + 格子间隙gap),0,0点对应格子中心的像素坐标
第四张图片是原图进行阈值180的二值化,来确定起点的像素和对应下标
首先,截图方式
//截图
static void screenCap() throws Exception {
cmd("adb shell screencap -p /sdcard/screen.png");
cmd("adb pull /sdcard/screen.png");
}
static void cmd(String s) throws IOException {
Runtime.getRuntime().exec(s);
}
2.打开图片
BufferedImage screen = ImageIO.read(new File("screen.png"));
关于图片的二值化:
即对原图片:计算每个像素的灰度,与阈值作比较,如果大于阈值则为白,否则为黑
灰度计算:当前像素点的灰度 = (当前点的红色值+绿色值+蓝色值)/3
//这里的灰度最终只是作为一个参数与阈值比较,因此可以不必除3操作,让阈值*3即可代替这里做了200w次的除法运算
综合灰度:当前像素点点的综合灰度 =(当前点的灰度 + 附近8个节点的灰度) / 9
//这里的综合灰度最终只是作为一个参数与阈值比较,因此可以不必除9操作,让阈值*9即可代替这里做了200w次的除法运算
//在这里我用右移3位(除8)代替除9操作
二值化:当前像素点的综合灰度是否大于阈值,大于则白色ffffff,否则黑色000000
其中去处噪音是为了去处截图左上角那个 还有x关超越好友 的提示,顶部底部也可以去掉,不过不干扰后面的识别
public static BufferedImage convertGray(BufferedImage bi, String newFileName, int threshold) throws IOException {
{
//BufferedImage bi = ImageIO.read(new File(bi));// 通过imageio将图像载入
int h = bi.getHeight();// 获取图像的高
int w = bi.getWidth();// 获取图像的宽
int[][] gray = new int[w][h];
for (int x = 0; x < w; x++) {
for (int y = 0; y < h; y++) {
gray[x][y] = getGray(bi.getRGB(x, y));
}
}
BufferedImage nbi = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_BINARY);
int SW = threshold;//分析起点:200 智能分析图片:240
for (int x = 0; x < w; x++) {
for (int y = 0; y < h; y++) {
nbi.setRGB(x, y, isBlack(gray, x, y, SW)?0xffffff:0);
}
}
removeNoise(nbi);
if(newFileName != null)
ImageIO.write(nbi, "jpg", new File(newFileName + ".jpg")); //
return nbi;
}
}
public static void removeNoise(BufferedImage nbi) {
for (int y = 223; y < 315; y++) {
for (int x = 0; x < 240; x++) {
nbi.setRGB(x, y, 0xffffff);
}
}
}
public static int getGray(int rgb){
int r = (rgb & 0xff0000) >> 16;
int g = (rgb & 0xff00) >> 8;
int b = rgb & 0xff;
return (r+g+b)/3;
}
/**
* @author StarMonitor
* 由于边界点无所谓,边缘点周边为0(黑色)
由于阈值可调,这里直接位运算除8,threshold = (9*real_threshold) >> 3处理即可,
多进行1次除法运->用位运算代替了hight*wight(200w)次除法运算
*/
public static boolean isBlack(int[][] gray, int x, int y, int threshold)
{
int rs = gray[x][y]
+ (x == 0 ? 0 : gray[x - 1][y])
+ (x == 0 || y == 0 ? 0 : gray[x - 1][y - 1])
+ (x == 0 || y == gray[0].length - 1 ? 0 : gray[x - 1][y + 1])
+ (y == 0 ? 0 : gray[x][y - 1])
+ (y == gray[0].length - 1 ? 0 : gray[x][y + 1])
+ (x == gray.length - 1 ? 0 : gray[x + 1][ y])
+ (x == gray.length - 1 || y == 0 ? 0 : gray[x + 1][y - 1])
+ (x == gray.length - 1 || y == gray[0].length - 1 ? 0 : gray[x + 1][y + 1]);
return rs >> 3 > threshold;
}
行列数的确定:
行、列的识别有点类似,一个是横着识别,另一个是纵着识别,说一个好了,另一个一样的
观察图片可发现矩形的行数可以通过计算矩形间隙的个数+1获得,而且对于二值化之后的图片这个间隙可以理解为宽度为5-10个像素的水平白线,如下图绘画(请勿吐槽,手残)
·········!!【关于像素】:为了简单,直接写的具体数值,我的手机是720*1440的像素,如果涉及具体像素的地方请小伙伴们自行计算百分比然后乘自己手机屏幕大小!!········
间隙线即为图中圈的部分,很显然第一个想法是求高为5,宽为720的白色矩形的个数,如果找到,就直接往下跳过30个像素点,避免重复计算。
(矩形寻找:如果要找一个长为l,宽为w的矩形,就一个一个像素点两层遍历,向右遍历l个,向下遍历w个像素点,如果有黑点直接返回,如果全部是白点,则是一个矩形,下次再找下一个矩形时候直接跳过上面找过的像素点,这样虽然看起来是3层循环,其实只是对图像扫描一遍,时间复杂度还是O(n),n个像素点)
但是这样发现前两行由于起点(猫爪子和猫尾巴的阻挡)识别不出来,而且格子阵上面 下面的白色区域非常多,极容易误识别。
改进过程几经曲折,这里直接分享改进过后的算法:
我找的横线为格子的底线,即真正的横线一定满足,横着100个像素,竖着5个像素全为白色点,且紧上面有可不连续20个像素以上为黑色才行(即寻找格子的底部),这样虽然排除了上部分和下部分的连续白色区域,但是仍然无法满足前两行猫爪子和猫尾巴的影响,需要再加一个条件,如果当前像素点不满足,向右移动50个像素点继续尝试,多尝试几次,毕竟起点只能有1个,这样即使起点在中间也不能击溃咱们的算法。
上面就把行的个数找出来了,列的个数同理也可以找出来,此处不啰嗦,继续求需求。
下面求矩形长度和矩形间缝隙L+gap
先观察咱们上步骤用代码找出来的矩形(粗为5的线,我用红色找的column,用黄绿色找的row)如图:
观察图片,可以得出求column时,找到的最后一竖线的x坐标 - 第一条竖线的x坐标 = (column - 1) * (L + gap)(像素)
观察图片,可以得出求column时,找到的最后一横线的y坐标 - 第一条横线的y坐标 = (column - 1) * (L + gap)(像素)
用公式写一下关系,(有无数个解的2元一次方程组)
redline.last.x - redline.first.x = (column - 1) * (L + gap)
yellowline.last.y - yellowline.first.y = (column - 1) * (L + gap)
据观察矩形缝隙一般在10个像素左右,直接取10即可,我们允许的误差在max(column-1,row-1)*(realGap-10) < L/2,所以取10完全可以,这样就可以求出实际的L,当然可以直接用两条线的距离相减,但是这样误差更大,因此采用以下方式计算L
L = ((column_X_array.get(column_X_array.size()-1) - column_X_array.get(0))/(column - 1) +
(row_Y_array.get(row_Y_array.size()-1) - row_Y_array.get(0))/(row - 1) )/2 - gap;
求出平均的L,误差在0-3个像素之间,max(row, column)最大不过10,3*10<L/2 符合要求
继续求须知条件,array[][] 每个元素对应的实际像素坐标,我们可以通过上面提到的思想:
array[i][j]对应的实际屏幕坐标 = (array[0][0]的横坐标 + (L + gap)* j ,array[0][0]的纵坐标 + (L + gap)* i)
把问题进一步简化至,只需求a[0][0]的实际坐标。
a[0][0]在实际中有可能是个障碍物点,因此不能根据图像识别来获得,我采用以下计算方式计算第一个点的中心像素点坐标:
firstPosition = new MyPosition(column_X_array.get(0)-L/2, row_Y_array.get(0) - L/2);
即横坐标为第一道竖线的横坐标 - L/2,纵坐标为第一道横线的纵坐标 - L/2
现在只差起点实际坐标和起点对应数组下标,初始化矩阵了。
4.对内存中的原图片副本进行二值化,方式与第2步类似,只是阈值改为180,不详述了。
5.对以上图像识别,获得起点像素坐标,求出起点对应下标,我寻找起点的方式如下图
画圈为放大部分,可以看出起点所在大概位置特征为:右边一部分和下边一部分像素点全是黑色,如上图两个红色矩形标记部分,只要找出一个符合这样的像素即可
public static void analysisStart(BufferedImage image) throws Exception {
for(int y = 315; y < 1250; y += 5 ) {
for(int x = 10; x < 710; x+=5)
if(isStart(image,x,y) ){
//Result_Of_Analysis.startPosition = new MyPosition(x, y);
initStartIndex(x,y);
return;
}
}
}
public static boolean isStart(BufferedImage image,int x,int y ) throws Exception {
//横着找
for(int i = 0; i < 15; i++) {
for(int w = 0; w < 5; w++ ) {
if(getSumRGB(image.getRGB(x+i,y + w))>300)//有白色像素就返回
return false;
}
}
//竖着找
for(int i = 0; i < 15 ; i++) {
for(int w = 0; w < 5; w++ ) {
if(getSumRGB(image.getRGB(x+w,y + i))>100)//有白色像素就返回
return false;
}
}
return true;
}
上面只是找到了起点大概的像素位置,那如何确定该像素对应哪个方格呢?
另写initStartIndex(x,y);来完成后序操作
观察发现,我们找到了每个array元素的对应实际坐标,如上图红线交点位置(手动粗略画的)。而我们找到的起点的大约位置是在五角星标记的像素点,而标准化之后起点的位置应该是在这个格子中间,即五角星的右下角,直接上代码:
public static void initStartIndex(int x, int y) {
int lg = Result_Of_Analysis.Lg;
int firstX = Result_Of_Analysis.firstPosition.x;
int firstY = Result_Of_Analysis.firstPosition.y;
for(int i = 0; i < Result_Of_Analysis.array.length; i++) {
for(int j = 0; j < Result_Of_Analysis.array[0].length; j++) {
if((x < firstX + j * lg) && (y < firstY + i * lg)) {
Result_Of_Analysis.startI = i;
Result_Of_Analysis.startJ = j;
return;
}
}
}
}
6.,初始化矩阵
这一步很简单,只需要判断每个元素对应实际像素点的颜色是什么即可,上代码:
public static void initArray(BufferedImage image) {
int lg = Result_Of_Analysis.Lg;
int firstX = Result_Of_Analysis.firstPosition.x;
int firstY = Result_Of_Analysis.firstPosition.y;
for(int i = 0; i < Result_Of_Analysis.array.length; i++) {
for(int j = 0; j < Result_Of_Analysis.array[0].length; j++) {
Result_Of_Analysis.array[i][j] = getSumRGB(image.getRGB(firstX + j * lg, firstY + i * lg))<300 ? 0 : -1;
}
}
}
意思是一般的都为0(黑色像素),障碍为-1(白色像素)
getSumRGB()这个函数可以不加,直接判断这个点的像素值 是否等于 0x000000即可,由于是分几天写的,写的时候每一步都是读的文件,jpg格式出现了点问题,加这个函数是保证是深色块。
7.对由图片分析出的迷宫进行求解,直接使用上一篇博客中写的结果,
Checkpoint question = new Checkpoint(Result_Of_Analysis.array,Result_Of_Analysis.startI,Result_Of_Analysis.startJ);
question.caculate();
这样,我们的所有信息就都有了,就可以实现自动截手机屏,自动破解当前关卡了
8.输出解 + 另一个线程异步用adb进行破解 or adb进行破解
最后我们只需要模拟人点击屏幕即可完成自动通关当前关卡了,像第一篇中的输出一样即可,按照顺序点击屏幕,完成通关
public static void through(int[][] array) throws Exception {
TreeMap<Integer, MyPosition> map = new TreeMap<Integer,MyPosition>();
for(int i = 0; i < array.length; i++) {
for(int j = 0; j < array[0].length; j++) {
map.put(array[i][j], new MyPosition(i,j));
}
}
Iterator<Integer> iterator = map.keySet().iterator();
int lg = Result_Of_Analysis.Lg;
int firstX = Result_Of_Analysis.firstPosition.x;
int firstY = Result_Of_Analysis.firstPosition.y;
while (iterator.hasNext()) {
int key = (int) iterator.next();
if(key > 0) {
click(firstX + lg * map.get(key).y, firstY + lg * map.get(key).x);
}
Thread.sleep(400);
}
}
到这我们就实现了全自动通关当前关,只需点一下运行,不需要自己建立矩阵,也不需要自己设定起点和障碍物,甚至连触屏都不需要了。
一下贴出自动通关所有关卡和刷完美的思路
通关所有关卡:
while(还有未通关的关卡){
1.截图上传至电脑
2.代码分析哪一个大关卡没有过关
3.进入这个大关
while(有没过的小关){
4.截图上传至电脑
5.代码分析哪一个小关卡没有过关
6.进入这个小关
7.运行以上代码
8.进入下一关
}
返回上一级
}
这个游戏设计者脑洞非常奇葩,完美不完美不是看通关时间、尝试次数,而是特喵的看猫的颜色,用个while循环点击刷新即可,直到颜色正确,把这一步放在第6 7 步之间即可
运行完以上代码,不管开发者怎么更新关卡,我们都能够第一时间破解关卡了哟,而不会因为刚出而百度不到答案,也过不去而苦恼~
欢迎评论,附主函数:
public static void main(String[] args) throws Exception {
//选择下一个未通过的关卡
selectNext();
//破解当前关
//1.adb截屏传到电脑
screenCap();
Thread.sleep(2000);
//System.out.println("正在获取屏幕..");
//2.打开图片,创建副本(这里为代码编写简单,采用读两次的方式代替复制),对其进行二值化240
//System.out.print("读取分析中.");
BufferedImage region ,backups;
region = ImageIO.read(new File("screen.png"));
backups = ImageIO.read(new File("screen.png"));
//3.识别以上二值化的图像获得行列数,方格大小,方格间隙,以及理论上第一个方格的屏幕坐标,最后生成初始矩阵
region = convertGray(region,null,240);
analysisImage(region);
//4.对内存中的原图片副本进行二值化180,
backups = convertGray(backups,null,200);
//5.对以上图像识别,获得起点像素坐标,求出起点对应下标
analysisStart(backups);
//6.初始化矩阵
initArray(region);
//7.对由图片分析出的迷宫进行求解
//System.out.println("图片智能分析完毕,准备求解");
Checkpoint question = new Checkpoint(Result_Of_Analysis.array,Result_Of_Analysis.startI,Result_Of_Analysis.startJ);
question.caculate();
//8.输出解 + 另一个线程异步用adb进行破解
//System.out.println("求解完毕,正在自动通关");
//question.print_plus2();
(new Thread(new Runnable() {
public void run() {
try {
question.print_plus2();
} catch (Exception e) {
e.printStackTrace();
}
}
})).start();
through(question.getArray());
}
可以用以上代码,也可以从github上下载完整代码 https://github.com/ChinaLym/SingleStroke
不会git的也可以从我的csdn下载,内附上下所有代码,且有多张图片说明和例子https://download.csdn.net/download/qq_35425070/10762903