忽然心血来潮,想写一个辅助工具,让朋友们在“大家来找茬”之类的游戏中可以少费一些眼睛。
在Java方面我是新手,在折腾了一段时间后,终于还是写出了一个基本可用的测试版程序。详细的使用方法和使用效果,可以参见这个博客/s/blog_9245c9e0010136by.html
这个小程序也可以通过这种方法进行下载——CSDN的资源中搜索“自己用Java写的图像比较器”
这里我想和大家分享一下编写过程中遇到的问题,同时也把源代码公布于此,朋友们可以批评指正。
思路:
类似于“大家来找茬”的游戏,可以用这样的思路来破解:
1.截取第一幅图A
2.截图第二幅图B
3.通过逐个像素比较,将差异部分显示出来
但是实际编程时,发现了问题——你很难确保两次截图时的大小位置完全一致!
开始我还想着通过算法来判断,后来想到了一个好解法——机器是死的,人是活的啊。所以步骤2可以调整为“将第一幅截图显示在窗口中,通过人工移动窗口,将两幅图叠加”
源码分享:
最终的程序由三个类构成
myScreenCapture:实现截图功能,并把截图的数据传给PicCheckFrame
PicCheckFrame:创建一个新窗口,用户可以手工移动该窗口,实现两幅图片的叠加
PicCheckPanel:实现步骤3中的算法,显示两幅图片的差异
/*** @author LiuCC* 本类实现了截屏功能* 即在屏幕上选择一块区域(如大家来找茬中的A图片),然后将选定的区域交给另一个类去处理比较**/public class myScreenCapture extends JFrame{private JButton screenCaptureButton, exitButton;/*** 程序入口* @param args*/public static void main(String[] args) {// TODO Auto-generated method stubnew myScreenCapture(); //非常简单,直接new即可}/** 构造函数* 一个截图按钮,一个退出按钮*/public myScreenCapture(){super("Picture Checker by LiuCC");initWindow(); //把这个窗体构建函数拆开拆开以便于修改和复用}private void initWindow(){setLayout(new FlowLayout()); //若无此句,只会显示最后一个加入的按钮screenCaptureButton = new JButton("截图");add(screenCaptureButton);//设置截图的处理函数screenCaptureButton.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {// 1.利用Toolkit获得全屏幕大小// 2.利用Robot将截屏结果放入BufferedImage// 3.建立一个全屏幕大小的Frame,里面放置一个自定义的Panel以做为鼠标截图的显示区域try{Toolkit tk = Toolkit.getDefaultToolkit();Dimension screenSize = tk.getScreenSize();Rectangle screenRectangle = new Rectangle(0,0,screenSize.width,screenSize.height);Robot myRobot = new Robot();BufferedImage screenBufferedImage = myRobot.createScreenCapture(screenRectangle);JFrame screenFrame = new JFrame();screenFrame.getContentPane().add(new scrCapturePanel(screenFrame, screenBufferedImage, screenSize.width, screenSize.height));screenFrame.setUndecorated(true); //如此设置,可以让用户感觉不到该Frame的存在screenFrame.setAlwaysOnTop(true);//要有如下两句Frame才会显示screenFrame.setVisible(true);screenFrame.setSize(screenSize);}catch(Exception robotException){robotException.printStackTrace();}}});exitButton = new JButton("退出");add(exitButton);exitButton.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {// TODO Auto-generated method stubSystem.exit(0);}});//有了以下两句,窗体才能显示//setSize(220,80);pack();setVisible(true);setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);}/*** 建立一个与屏幕一致的Panel,主要是为了显示个性化的鼠标,以及通过鼠标拉出选择框* @author LiuCC**/private class scrCapturePanel extends JPanel implements MouseListener, MouseMotionListener{private JFrame parentFrame;private int width, height;private BufferedImage screenImage;private BufferedImage selectImage;private int startX, startY, endX, endY; //在一次选择过程中涉及到的坐标private Rectangle selectRectangle = new Rectangle(0,0,0,0);private int tempX, tempY;private Cursor specialCursor;/*** 构造函数* @param* 参数依次为:父窗口; 在Panel中显示的画面; 宽,高*/public scrCapturePanel(JFrame parentFrame, BufferedImage showedImage, int width, int height){this.parentFrame = parentFrame;this.screenImage = showedImage;this.width = width;this.height = height;//设置一个特别的鼠标,以标识截图状态Image cursorImage = Toolkit.getDefaultToolkit().createImage(this.getClass().getResource("/image/icon.png"));specialCursor = Toolkit.getDefaultToolkit().createCustomCursor(cursorImage, new Point(0,0), "special icon");setCursor(specialCursor);//关联鼠标动作addMouseListener(this);addMouseMotionListener(this);}/*** JPanel绘制的关键函数*/public void paintComponent(Graphics g){g.drawImage(screenImage, 0, 0, width, height, 0, 0, width, height, this);//以红框标识鼠标圈定的范围g.setColor(Color.RED);g.drawLine(startX,startY,endX,startY);g.drawLine(startX,endY,endX,endY);g.drawLine(startX,startY,startX,endY);g.drawLine(endX,startY,endX,endY);//记录选定区域,考虑了反向拖拽画框的情况int x=startX<endX?startX:endX;int y=startY<endY?startY:endY;selectRectangle = new Rectangle(x,y,Math.abs(startX-endX),Math.abs(startY-endY));}@Overridepublic void mousePressed(MouseEvent e) {// TODO Auto-generated method stubtempX = e.getX();tempY = e.getY();}@Overridepublic void mouseReleased(MouseEvent e) {// TODO Auto-generated method stub//Do Nothing!}@Overridepublic void mouseDragged(MouseEvent e) {// TODO Auto-generated method stubstartX=tempX;startY=tempY;endX=e.getX();endY=e.getY();repaint(); //重绘图像,以显示动态效果}@Overridepublic void mouseMoved(MouseEvent e) {if(selectRectangle.contains(e.getPoint())){setCursor(new Cursor(Cursor.MOVE_CURSOR));//在选定区域内外,采用不同的鼠标样式以示区别}else{setCursor(specialCursor);}}@Overridepublic void mouseClicked(MouseEvent e) {if(2==e.getClickCount()){//在选定区域内双击表示选定该区域if(selectRectangle.contains(e.getPoint())){selectImage = screenImage.getSubimage(selectRectangle.x, selectRectangle.y, selectRectangle.width, selectRectangle.height);parentFrame.dispose();//供测试用,生成一个图片,考察效果try {ImageIO.write(selectImage, "jpg", new File("./test1.jpg"));} catch (IOException e1) {// TODO Auto-generated catch blocke1.printStackTrace();}//接下来启动对比图片所需的Framenew PicCheckFrame(selectImage);}else{//在选定区域外双击,则重新选择startX=0;startY=0;endX=0;endY=0;selectRectangle=new Rectangle(0,0,0,0);repaint();}}}@Overridepublic void mouseEntered(MouseEvent e) {// TODO Auto-generated method stub}@Overridepublic void mouseExited(MouseEvent e) {// TODO Auto-generated method stub}}}
第二个类
/*** 建立一个JFrame的子类,包含两个区域——图片显示区域和一个退出按钮* @author LiuCC* */public class PicCheckFrame extends JFrame{private int relativeX, relativeY; //表示鼠标按下时,该点与图像界面原点的相对位置private int absoluteX, absoluteY; //鼠标拖拽时,鼠标所在绝对位置private int setX, setY;//拖动时,鼠标相对于图像界面原点的位置private boolean mousePressedNow=false; //true表示鼠标左键按下private PicCheckPanel pCkPanel;private JButton exitButton;/*** 构造函数* @param get*/public PicCheckFrame(BufferedImage capturedImage){pCkPanel = new PicCheckPanel(capturedImage);setUndecorated(true);//这样设置可以免去调整标题栏的麻烦//我至今不知道如果加入了标题栏,在之后的函数中如何处理像素点相对位置//如果有朋友搞懂了,可以留言告诉我setLayout(new BorderLayout());add(pCkPanel, BorderLayout.CENTER);exitButton = new JButton("Exit");add(exitButton, BorderLayout.SOUTH);exitButton.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent arg0) {// TODO Auto-generated method stubSystem.exit(0);}});addMouseListener(new mouseFunction());addMouseMotionListener(new mouseMotionFunction());setSize(capturedImage.getWidth(), capturedImage.getHeight());setVisible(true);}/*** * @author LiuCC**/private class mouseFunction extends MouseAdapter{@Overridepublic void mousePressed(MouseEvent e){if(1==e.getClickCount()){mousePressedNow=true;relativeX = e.getX();relativeY = e.getY(); //e带来的是,鼠标对于当前Component的相对位置}}@Overridepublic void mouseReleased(MouseEvent e){mousePressedNow=false;}}private class mouseMotionFunction extends MouseMotionAdapter{@Overridepublic void mouseDragged(MouseEvent e){if(true==mousePressedNow){//说明此时在拖动窗口int tmpX = PicCheckFrame.this.getLocationOnScreen().x;int tmpY = PicCheckFrame.this.getLocationOnScreen().y;absoluteX = tmpX + e.getX();absoluteY = tmpY + e.getY();//如果SetLocation时用absoluteX,会让鼠标回到Component的原点,这样用户体验不好,所以还应该做如下修正setX = absoluteX - relativeX;setY = absoluteY - relativeY;pCkPanel.setPicCheckPanelLocation(tmpX, tmpY); //为什么要传tmpX,而不是setX?这是我试出来的//原理我也不是很确定,如果有朋友弄清楚了,请指出setLocation(setX, setY);}}}}
第三个类
/*** 这个类完成了图像的对比工作* Frame传给本类一个Image,即基准的Image* 本类同时做了一次截屏,在Frame移动的过程中,本类不断地比较下方的图片和基准图像* 两者不相同的地方以蓝色显示* * 由此可以大致判断出两幅图像的区别* @author LiuCC**/public class PicCheckPanel extends JPanel{private int width, height; private int positionX=0, positionY=0;private BufferedImage screenImage;private BufferedImage capturedImage;private BufferedImage showedImage;private Robot myRobot;private int scrR, scrG, scrB;private int capR, capG, capB;/*** 构造函数*/public PicCheckPanel(BufferedImage inImage){this.capturedImage = inImage;this.width = inImage.getWidth();this.height = inImage.getHeight();screenImage = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);//showedImage = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);try{myRobot = new Robot();}catch (Exception e) {// TODO: handle exception}screenImage=myRobot.createScreenCapture(new Rectangle(0, 0, Toolkit .getDefaultToolkit().getScreenSize().width, Toolkit .getDefaultToolkit().getScreenSize().height));}/*** 设置Panel的位置* @param posX* @param posY*/public void setPicCheckPanelLocation(int posX, int posY){this.positionX = posX;this.positionY = posY;validate();repaint();//这个是关键的一步,会调用paintComponent函数}/*** 设置Panel的大小* @param width* @param height*/public void setPicCheckPanelSize(int width, int height){this.width = width;this.height = height;repaint();//这个是关键的一步,会调用paintComponent函数}/*** 最关键的,绘图函数* 对传入的图片capturedImage 与 当前在JPanel下方的图片(ScreenImage的一部分)做对比* 逐个像素比较,若相同则显示,若不相同则显示出蓝色* * 似乎完全相同是不可能的,我调试了多次,设置了一个阈值VAL,目前总体效果不错* 但是图片叠加后,特别是边缘部分还是不怎么干净,如朋友们有更好的方法,也请告诉我*/@Overridepublic void paintComponent(Graphics g){super.paintComponent((Graphics2D)g);for(int i=0; i<width; i++){for(int j=0; j<height; j++){//之前试过把 getRGB 的值直接进行比较的,但效果有问题,只好拆成R/G/B分别比较了Object scrData = screenImage.getRaster().getDataElements(i+positionX, j+positionY, null);scrR=screenImage.getColorModel().getRed(scrData);scrG=screenImage.getColorModel().getGreen(scrData);scrB=screenImage.getColorModel().getBlue(scrData);Object capData = capturedImage.getRaster().getDataElements(i, j, null);capR=capturedImage.getColorModel().getRed(capData);capG=capturedImage.getColorModel().getGreen(capData);capB=capturedImage.getColorModel().getBlue(capData);int VAL = 22; //阈值为什么选这个?只能说,这是我试验出来的//下面不得不采用了“差别不大就算相同”的判断方法if(Math.abs(scrR-capR)<=VAL&&Math.abs(scrG-capG)<=VAL&&Math.abs(scrB-capB)<=VAL)showedImage.setRGB(i, j, capturedImage.getRGB(i, j));else {showedImage.setRGB(i, j, Color.blue.getRGB());}}}g.drawImage( showedImage, // 要画的图片 0, // 目标矩形的第一个角的x坐标 0, // 目标矩形的第一个角的y坐标 width, // 目标矩形的第二个角的x坐标 height, // 目标矩形的第二个角的y坐标 0, // 源矩形的第一个角的x坐标 0, // 源矩形的第一个角的y坐标 width, // 源矩形的第二个角的x坐标 height, // 源矩形的第二个角的y坐标 this );}}
功能基本实现,但是还是有一些地方存在疑点,请朋友们指正
如:在第二个类中
setUndecorated(true);
这样设置可以免去调整标题栏的麻烦,我不知道如果加入了标题栏,在之后的函数中如何处理像素点相对位置(即,我不知道如何获得标题栏的宽度),如果有朋友搞懂了,可以留言告诉我
还有一些问题就写在注释当中了,朋友们发现有问题,请留言提醒我噢。
如果觉得《“图片差异检查”辅助工具(即“大家来找茬”辅助工具)源码分享》对你有帮助,请点赞、收藏,并留下你的观点哦!