零、简介
本文主要为本人初次接触CSDN,尝试着自己去创作一些东西,虽然可能看来很简单,但这也是我学习的过程。
本次我想实现的主要是实现自动化win7扫雷的过程,我玩win7版本的扫雷也已经有上千局了,在游玩的过程中,我发现我的大部分操作和逻辑判断过程都是重复的,所以我产生了让电脑自己玩扫雷的想法,也是在此过程中来精进我的代码水平,向大神学习。
一、逻辑框架
1.1 与windows交互 包括获取截图 鼠标点击
1.2 扫雷内部算法实现
二、代码实现
2.1 windows下获取屏幕截图
在这里,我在网上首先搜索了C++的截图代码,太过繁长,并且经过我的测试,由于我的屏幕开启了文字大小缩放,所以其截图大小是有问题的,原先是1920*1080的屏幕,我的文字缩放是125%,也就是说其在系统中生成的原始图像是1536*864,所以C++代码截取了屏幕左上角开始1536*864的内容,不完整,所以弃用C++,改用Java,我发现Java的代码相当的简单,看来Java对这些接口做了相当好的封装,以下是实现屏幕获取的Java代码。
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.io.File;
import javax.imageio.ImageIO;
public class GetScreen {
public void getScreenShot() throws Exception
{
Robot robot = new Robot();
BufferedImage screenShot = robot.createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize()));
ImageIO.write(screenShot, "JPG", new File("screen_shot.jpg"));
}
public static void main(String[] args) throws Exception
{
GetScreen getScreen = new GetScreen();
getScreen.getScreenShot();
}
}
获取屏幕截图的主要实现原理是用java.awt包下的Robot类的createScreenCapture方法。
awt(Abstract Window Toolkit)抽象视窗工具组,是Java的平台独立的视窗系统, 图形和使用者界面器件工具包。AWT是Java基础类(JFC-Java Foundation Classes)的一部分,为Java程序提供图形使用者界面(GUI)的标准API。
Robot类用于生成本机系统输入事件,主要目的是促进Java平台的自动化测试。
createScreenCapture的参数为createScreenCapture(Rectangle screenRect),表明截取screenRect大小的屏幕,在这里我们用Toolkit.getDefaultToolkit().getScreenSize()来返回屏幕的大小。
BufferedImage是一个承载图像数据的数据流
ImageIO. write方法用来写入图片,其有三个参数,第一个是图像数据的数据流,第二个是文件格式,第三个是文件名。
2.2 对图像进行预处理
2.2.1 图像切分
图像切分的基本思路就是先把上一步得到的屏幕截图切分出雷区,再根据雷区的行数和列数来单独切分出每一小块,基本实现如下:
public static int[][] splitCells(int startX, int startY, int width, int height, int hori, int verti) throws IOException {
BufferedImage image = ImageIO.read(new File("screen_shot.jpg"));
BufferedImage croppedImage = image.getSubimage(startX, startY, width, height);
int[][] ans = new int[hori][verti];
for (int i = 0; i < hori; i++) {
for (int j = 0; j < verti; j++) {
int x0 = (int) Math.round(i * (width / (double) hori));
int x1 = (int) Math.round((i + 1) * (width / (double) hori));
int y0 = (int) Math.round(j * (height / (double) verti));
int y1 = (int) Math.round((j + 1) * (height / (double) verti));
BufferedImage cellImage = croppedImage.getSubimage(x0 + 5, y0 + 5, x1 - x0 - 10, y1 - y0 - 10);
ans[i][j] = recognize(cellImage);
//ImageIO.write(cellImage,"JPG","img_set\\crop_test_"+Integer.toString(i*16+j)+".jpg");
}
}
return ans;
}
splitCells函数接收六个参数,前两个分别是雷区左上角的坐标,中间两个是雷区的宽度和高度,后两个是雷区每一行的雷数和每一列的雷数,函数返回一个二维数组,表明这个雷区的状态,或者说每一个格子的数字,这就要看下一个小节,讲上面的代码中recognize函数的实现了。
在这里主要需要知道的就是BufferedImage的getSubimage方法了,该方法接收四个参数,分别是起始点的横纵坐标和图片的宽和高,根据这些参数返回原图像的一个子图像。
2.2.2 图像小块识别
下面就要讲这个程序比较难的一部分,也就是我们怎么把一个小块的图片转变成一个电脑可以识别的状态,众所周知,扫雷中的格子有这些状态,未点开的格子,点开的格子,其中有一个1~8的数字或者没有数字(这里因为是电脑玩游戏的原因,我们不需要标出雷),当然还有可能点开是雷,但这就不在我们考虑范围之内了(毕竟这就意味这游戏失败了)
一开始我会以为这很容易,毕竟我们面对的都是固定的图像,不需要像识别手写数字那样使用神经网络之类的方法,但我在写算法的时候发现了问题,首先,每个格子的颜色不尽相同,这里可以看到win7扫雷在实现的时候为图像添加了光效,这就使得格子的亮度从左上角向右下角递减,我本来的想法是对格子的每个状态截几张图求其平均值生成一张模板图然后进行对比,现在看来可能不大行,首先是有一个亮度的区别,然后因为这个图片比较小,所以如果有一两个像素的位移会对结果有较大的影响。
我的想法是首先把数字和非数字的区别出来,我们可以观察到,数字图像和非数字图像的一个最明显的区别是非数字图像的整体颜色较为平均,而数字图像因为有色块,颜色有较大差异,所以我考虑用方差的思路,这里不是把颜色求平均再求方差,而是求这个像素与上一个像素之间的差异,因为这样可以使得差异更明显,而且我们不用选取所有的点,这样可以减小计算量,代码如下:
public static int imageVarience(BufferedImage image){
int height=image.getHeight();
int width=image.getWidth();
int varience=0;
int color = image.getRGB(2, 2);
int preB = color & 0xff;
int preG = (color & 0xff00) >> 8;
int preR = (color & 0xff0000) >> 16;
for(int i=2;i<width-2;i++){
for (int j=2;j<height-2;j++){
color=image.getRGB(i,j);
int curB = color & 0xff;
int curG = (color & 0xff00) >> 8;
int curR = (color & 0xff0000) >> 16;
varience+=(curB-preB)*(curB-preB)+(curG-preG)*(curG-preG)+(curR-preR)*(curR-preR);
j+=2;
}
i+=2;
}
return varience;
}
在这里的代码中涉及到了BufferedImage的数据存储模式,我们可以将一个BufferedImage image数据其看作为是一个unsigned int image[width][height]数据,也就是无符号二维数组,每一个像素点BufferedImage用32bits来存储,分别为AAAAAAAA RRRRRRRR GGGGGGGG BBBBBBBB,其中A为Alpha,为不透明度,RGB为红绿蓝,BufferedImage的getRGB方法会返回这32bits中的低24位,然后稍加位运算即可获得RGB值。
这些图片的方差值如下:
我们可以看到,非数字图片和数字图片的值还是有很大差别的,所以我选取了600000作为了两者的分界线。
再然后就是区分未点开的格子和点开的空白格子了,观察不难发现,它们很明显的区别就是一个呈现明显的蓝色 而另一个呈现白色,也就是说一个的Blue值会明显偏高,而另一个会比较相同,另外经测试发现,再两者中Red的值都是三原色中最低的,所以我们将Blue-Red值作为区分其是白还是蓝的标准。经测试图片数据如下,每一行都是一张图片的数据,取其平均值。
左边是蓝色的,右边是白色的,可以看到区别还是不小,我们取60作为其的临界值。
计算其图片色差的代码如下:
public static int calColorDiff(BufferedImage image){
int height = image.getHeight();
int width = image.getWidth();
int diff = 0;
int cnt=0;
for (int i = 2; i < width - 2; i++) {
for (int j = 2; j < height - 2; j++) {
int color = image.getRGB(i, j);
int Blue = color & 0xff;
int Red = (color & 0xff0000) >> 16;
diff+=Blue-Red;
cnt++;
j += 2;
}
i += 2;
}
return diff/cnt;
}
接下来就是处理数字的部分了,我的想法和上面处理判断图像是否均匀的思路一致,因为每个数字都有自己的形状,我们可以对每一个数字设定一个其自己的点集,对于这个点集,只有正确的数字才能匹配上,判断是否匹配上的方法还是使用方差,如果匹配上,因为这个数字在它的点集上的颜色基本一致,所以其方差会很小,而别的数字方差会很大,经测试,效果还不错,唯一的缺陷就是我们需要自己去慢慢弄点集。在这里我也学习了java里ArrayList的使用,其使用方法类似于C++STL库中的vector,一样需要在尖括号中输入这个List里元素的类别,ArrayList里add方法类似于vector的push_back方法,其他方法暂时没有用到。
2.2.3 代码汇总
下面是2.2部分的代码汇总:
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
public class ImageProcess {
private static final ArrayList<ArrayList<Point>> AAP =initAAP();
public static int[][] splitCells(int startX, int startY, int width, int height, int hori, int verti) throws IOException {
BufferedImage image = ImageIO.read(new File("screen_shot.jpg"));
BufferedImage croppedImage = image.getSubimage(startX, startY, width, height);
int[][] ans = new int[verti][hori];
for (int i = 0; i < verti; i++) {
for (int j = 0; j < hori; j++) {
int x0 = (int) Math.round(j * (width / (double) hori));
int x1 = (int) Math.round((j + 1) * (width / (double) hori));
int y0 = (int) Math.round(i * (height / (double) verti));
int y1 = (int) Math.round((i + 1) * (height / (double) verti));
BufferedImage cellImage = croppedImage.getSubimage(x0 + 5, y0 + 5, x1 - x0 - 10, y1 - y0 - 10);
System.out.print(recognize(cellImage)+" ");
ans[i][j] = recognize(cellImage);
}
System.out.println();
}
return ans;
}
public static int imageVarience(BufferedImage image) {
int height = image.getHeight();
int width = image.getWidth();
int varience = 0;
int color = image.getRGB(2, 2);
int preB = color & 0xff;
int preG = (color & 0xff00) >> 8;
int preR = (color & 0xff0000) >> 16;
for (int i = 2; i < width - 2; i++) {
for (int j = 2; j < height - 2; j++) {
color = image.getRGB(i, j);
int curB = color & 0xff;
int curG = (color & 0xff00) >> 8;
int curR = (color & 0xff0000) >> 16;
varience += (curB - preB) * (curB - preB) + (curG - preG) * (curG - preG) + (curR - preR) * (curR - preR);
j += 2;
}
i += 2;
}
return varience;
}
public static int calColorDiff(BufferedImage image) {
int height = image.getHeight();
int width = image.getWidth();
int diff = 0;
int cnt = 0;
for (int i = 2; i < width - 2; i++) {
for (int j = 2; j < height - 2; j++) {
int color = image.getRGB(i, j);
int Blue = color & 0xff;
int Red = (c