图片处理GUI界面
在写过五子棋和围棋这两个小项目之后,我写了图片处理这一GUI界面,并现在把教程分享出来
先上几张成品效果图:
写在前面的:
这个项目我十分重视代码的结构和规范性,全程不用静态方法,体现Java语言面向对象的封装性、继承性、多态性,所以我写的很认真,也很上头。
在分析需求时我认为需要三个类来实现这个图片处理界面,分别是photoUI类,主要用于UI界面的绘制和main方法执行,photoMouse类,主要为事件监听类,存放监听器和处理图片的方法,Threadop类,主要用于创建摄像机线程。
在一开始,由于我将所有的处理图片的方法都写在了photoMouse类中,而摄像机也会用到其中大部分方法,为了不让摄像机线程和监听器线程竞争,我将处理方法都写为了静态方法,但是这样不仅耗费资源多,而且也不够“封装”。甚至在我调试程序时,出现了摄像头严重卡顿的情况。所以后来在老师的指导下,我将处理图片的方法全部写在了一个新类ImageMethod中,作为“工具类”,需要用时用对象调用方法。
1.总体分析
首先,我们要获取图片,才能进行后续的操作。
private BufferedImage buffImage = null;//用于存储当前界面的信息 File file = new File(path);//path为文件绝对路径 try { buffImage = ImageIO.read(file); } catch (IOException e1) { e1.printStackTrace(); }
BufferImage类是用于存储图片信息的一个类,ImageIO拥有一个read(File file)的方法,可以把图片读取成BufferImage的形式,所以我们新建一个对象,进行存储
这里的buffImage我设置为了全局变量,用于储存当前界面的信息,即:这个对象拥有我们需要处理的图片信息
所以,思路清晰点,图片处理即为处理这个我们导入的buffImage,再具体一点说就是我们稍后所有对图片处理的方法都引入buffImage进行处理,然后返回一个处理后的BufferedImage就行
现在,图片获取好了,我们可以对这张图片进行处理了
2.图片的处理方法
1.获取图片像素值
图片本质上是由一个个像素点构成的,我们对图片进行处理其实就是对图片的每个像素的值进行处理,而一个像素点又是由RGB加上透明度A这四个数字控制,而BufferImage类拥有一个getRGB()的方法,可以获取某个位置的像素点的RBG值。这里的RGB值为int型,拥有4个字节,而ARGB这四个数的取值范围都为【0,255),即用一个字节便可以表示,这里的RGB值便是A+R+G+B拼接而成。
在了解了图片的像素值后,我们可以用一个二维数组将图片的数据全部存储起来,便于后续的进一步操作。
代码如下:
//获取图片的像素值 public int[][] getImage(BufferedImage buffImage){ int w = buffImage.getWidth(); int h = buffImage.getHeight(); int[][] Imagearr = new int[w][h]; //定义整型的二位数组保存像数值 for(int i=0;i<w;i++) { for(int j=0;j<h;j++) { //获取对应的像数值 int pixel = buffImage.getRGB(i,j); Imagearr[i][j] = pixel; } } return Imagearr; }
2.全局马赛克算法
马赛克作为最常见的处理手段之一,写起来也是较为简单的,其大致思路是在确定马赛克边框长度的情况下将图片划分为若干个马赛克像素格,每个格子都取中心颜色填满整个格子
当然,马赛克的实现思路肯定不止这一种,你可以根据自己的想法进行方法的撰写。
实现代码如下:
//全局马赛克 public BufferedImage masaikeall(BufferedImage bufferedImage,int size){ //记录图片的宽高 int width = bufferedImage.getWidth(); int height = bufferedImage.getHeight(); int xnum = 0;//x方向绘制个数 int ynum = 0;//y方向绘制个数 //计算xnum,ynum if(width % size == 0){ xnum = width/size; }else { xnum = width/size + 1; } if(height % size == 0){ ynum = height/size; }else { ynum = height/size + 1; } BufferedImage bf = new BufferedImage(width,height,3); for (int i = 0; i < xnum; i++) { for (int j = 0; j < ynum; j++) { //马赛克矩形格子的大小 int mwidth = size; int mheight = size; //考虑最后边界情况 if(i == xnum - 1){ mwidth = width - i*size; } if(j == ynum - 1){ mheight = height - j*size; } int n = bufferedImage.getRGB(i*size+mwidth/2,j*size+mheight/2); for (int k = i*size; k < i*size+mwidth; k++) { for (int l = j*size; l < j*size+mheight; l++) { bf.setRGB(k,l,n); } } } } return bf; }
这里的size即为确定的马赛克边框长度,要注意的是考虑边界情况,并不复杂。
3.局部马赛克
局部马赛克是用画板画出来的马赛克效果,要注意画笔和图片的坐标(所在的组件)要保持一致,这里我将我最后写好的局部马赛克算法给出,由于后面实现了图片居中的效果,所以我这里形参较多,刚开始写时不必添加
//局部马赛克 public BufferedImage masaike(BufferedImage bufferedImage,int x,int y,int size,int Jpanelwidth,int Jpanelhight,Graphics g){ //记录图片的宽高 int width = bufferedImage.getWidth(); int height = bufferedImage.getHeight(); x += size/2; y += size/2; if(x % size > size/2){ x = x + size - x % size; }else { x = x - x % size; } if(y % size > size/2){ y = y + size - y % size; }else { y = y - y % size; } x -= size/2; y -= size/2; int w = 0,h = 0;//绘图偏移点 if(Jpanelwidth > width){ w = (Jpanelwidth - width)/2; x -= (Jpanelwidth - width)/2; }else { w = 0; } if(Jpanelhight > height){ h = (Jpanelhight - height)/2; y -= (Jpanelhight - height)/2; } if(x - size / 2 >= 0 && x + size / 2 <= width && y - size/2 >= 0 && y + size/2 <= height){ Color color = new Color(bufferedImage.getRGB(x,y)); for (int i = x - size / 2; i < x - size / 2 + size; i++) { for (int j = y - size / 2; j < y - size / 2 + size; j++) { g.setColor(color); g.drawLine(i+w,j+h,i+w,j+h); bufferedImage.setRGB(i,j,color.getRGB()); } } } return bufferedImage; }
3.获取灰度
灰度是图形处理的一个很重要的值,其具体的实现为gray=(r+g+b)/3,当然也有更好的比例
代码实现如下:
//获取灰度 public int getgray(BufferedImage bufferedImage,int x,int y){ int color = bufferedImage.getRGB(x,y); int r = (color >> 16) & 0xff; int g = (color >> 8 ) & 0xff; int b = color & 0xff; int gray = (int)(0.2989*r + 0.5870*g + 0.1140*b); return gray; }
这里的位移运算和&运算便是将RGB值分别转换为RGB三个颜色的值的方法,例如R为RBG值中从左往右数的第二个,所以往右移16位(2个字节),然后取后四位(&0xff),即可获取其具体值。
对像素点进行灰度处理就是将这个像素点的RBG值全部设置为计算出来的灰度即可
4.灰度化
灰度化即我们常说的黑白图片,即为将一张图片所有的像素点全部都进行灰度处理
代码实现如下:
//灰度化 public BufferedImage grayImage(BufferedImage bufferedImage){ int width = bufferedImage.getWidth(); int hight = bufferedImage.getHeight(); BufferedImage bf = new BufferedImage(width,hight,BufferedImage.TYPE_INT_ARGB); for (int i = 0; i < width; i++) { for (int k = 0; k < hight; k++) { int gray = getgray(bufferedImage,i,k); int newPixel = colorToRGB(255,gray,gray,gray); bf.setRGB(i,k,newPixel); } } return bf; }
colorToRGB()方法见下文RBG转换
5.获取轮廓
将图片的某一像素点和相邻像素点进行灰度比对,其差值大于某个数的设置为白色,小于则设置为黑色,这样就能获得到一张图片的大致轮廓
代码实现如下:
//轮廓 public BufferedImage lunkuo(BufferedImage bufferedImage,int lun){ int width = bufferedImage.getWidth(); int hight = bufferedImage.getHeight(); BufferedImage bf = new BufferedImage(width,hight,BufferedImage.TYPE_INT_ARGB); for (int i = 0; i < width-1; i++) { for (int k = 0; k < hight-1; k++) { int gray1 = getgray(bufferedImage,i,k); int gray2 = getgray(bufferedImage,i+1,k+1); if(Math.abs(gray1-gray2) > lun){ bf.setRGB(i,k,Color.WHITE.getRGB()); }else { bf.setRGB(i,k,Color.BLACK.getRGB()); } } } return bf; }
这里要注意范围限定和相邻距离,可以试一试,效果好就行
6.二值化
二值化是遍历像素点,将像素点灰度大于某个数的和小于的分别设置为黑白色
代码实现如下:
//二值化 public BufferedImage erzhi(BufferedImage bufferedImage,int k){ int width = bufferedImage.getWidth(); int hight = bufferedImage.getHeight(); BufferedImage bf = new BufferedImage(width,hight,BufferedImage.TYPE_INT_ARGB); for (int i = 0; i < width; i++) { for (int j = 0; j < hight; j++) { if(getgray(bufferedImage,i,j) > k){ bf.setRGB(i,j,Color.WHITE.getRGB()); }else { bf.setRGB(i,j,Color.BLACK.getRGB()); } } } return bf; }
7.油画
图像油画原理是:每隔固定距离遍历图片像素点,颜色取该像素点的颜色,取随机大小,如[5,25),以该点为圆心,随机的大小为半径画圆。代码实现如下:
//油画 public BufferedImage youhua(BufferedImage bufferedImage){ int width = bufferedImage.getWidth(); int hight = bufferedImage.getHeight(); BufferedImage bf = new BufferedImage(width,hight,BufferedImage.TYPE_INT_ARGB); Graphics gg = bf.getGraphics(); for (int i = 0; i < width; i+=5) { for (int j = 0; j < hight; j+=5) { Color c = new Color(bufferedImage.getRGB(i,j)); gg.setColor(c); Random ran = new Random(); int r = ran.nextInt(20) + 5; gg.fillOval(i,j,r,r); } } return bf; }
8.卷积
卷积的原理网上有好多,讲的比我好,我这里就不详说了,代码实现如下
//卷积 public BufferedImage sharpening(BufferedImage bufferedImage,int sharpe){ int width = bufferedImage.getWidth(); int hight = bufferedImage.getHeight(); BufferedImage bf = new BufferedImage(width,hight,BufferedImage.TYPE_INT_ARGB); //卷积核选择 float[][] filter = new float[0][0]; if(sharpe == 1){ filter = new float[][]{ { -1, -1, -1, -1, -1 }, { -1, -1, -1, -1, -1 }, { -1, -1, 25, -1, -1 }, { -1, -1, -1, -1, -1 }, { -1, -1, -1, -1, -1 } }; }else if(sharpe == 2){ filter = new float[][]{{-1,-1,-1},{-1,9,-1},{-1,-1,-1}}; }else if(sharpe == 3){ filter = new float[][]{{0,-1,0},{-1,4,-1},{0,-1,0}}; }else if(sharpe == 4){ filter = new float[][]{{0.8f,0,0.8f},{-1,1.8f,-1},{-1,0.8f,-1}}; } //原数组 int src[][] = getImage(bufferedImage); //过渡数组 int[][]tem = new int[filter.length][filter[0].length]; //遍历宽度和遍历高度 int valideWidth = src[0].length - filter[0].length + 1; int valideHight = src.length - filter.length + 1; for (int i = 0; i < valideHight; i++) { for (int j = 0; j < valideWidth; j++) { for (int y = 0; y < filter.length; y++) { for (int z = 0; z < filter[0].length; z++) { tem[y][z] = (int)(src[i+y][j+z]*filter[y][z]); } } int kk = 0; for (int y = 0; y < filter.length; y++) { for (int z = 0; z < filter[0].length; z++) { kk += tem[y][z]; } } if(kk<0)kk=0; if(kk>255)kk=255; bf.setRGB(i,j,(byte)kk); } } return bf; }
9.顺时针旋转
顺时针旋转是图片的一种坐标转换,用xy坐标轴可以很简单的计算出来
如一张两条边和xy轴贴合的矩形图片,新建一个宽高相反的矩形,则原坐标(i,j)会转换为(hight-j,i)
代码实现如下:
//顺时针旋转 public BufferedImage rotate(BufferedImage bufferedImage){ int width = bufferedImage.getWidth(); int hight = bufferedImage.getHeight(); BufferedImage bf = new BufferedImage(hight,width,BufferedImage.TYPE_INT_ARGB); for (int i = 1; i < width; i++) { for (int j = 1; j < hight; j++) { int rgb = bufferedImage.getRGB(i,j); bf.setRGB(hight-j,i,rgb); } } return bf; }
这里注意遍历的取值,上面的是我调试后没有问题的
10.RGB转换
上文已经讲过了如何将一个int型的数值拆分为ARGB四个数,那么当你拥有ARGB四个数时,也可以反向转换成一个值
//RGB转换 public int colorToRGB(int alpha, int red, int green, int blue) { int newPixel = 0; newPixel += alpha; newPixel = newPixel << 8; newPixel += red; newPixel = newPixel << 8; newPixel += green; newPixel = newPixel << 8; newPixel += blue; return newPixel; }
11.绘图方法
在拥有了一个存储我们对图片进行各种处理后的buffImage之后,我们要将图片显示到界面上,这就需要绘图方法
代码实现如下:
//通过BuffImage绘图 public void drawImage(BufferedImage bufferedImage, Graphics g, int width, int hight){ int w = 0,h = 0;//绘图偏移点 int widthbuf = bufferedImage.getWidth(); int heightbuf = bufferedImage.getHeight(); if(width > widthbuf){ w = (width - widthbuf)/2; } if(hight > heightbuf){ h = (hight - heightbuf)/2; } int[][] pixelArr = getImage(bufferedImage); for (int i = 0; i < pixelArr.length; i++) { for (int j = 0; j < pixelArr[i].length; j++) { Color color = new Color(pixelArr[i][j]); g.setColor(color); g.drawLine(i+w,j+h,i+w,j+h); } } }
这里的wight和hight指的是当前容器的宽高,经过一番简单的计算就可以在任意宽高下都能让图片居中显示了
3.UI界面的绘制
1.窗体创建
这没有什么好讲的,如果有不熟悉的可以看我之前的教程
这里我要额外讲几个swing组件
先把代码呈上:
JFrame jf = new JFrame(); jf.setSize(1715,857); jf.setExtendedState(JFrame.MAXIMIZED_BOTH);//窗口最大化 jf.setLocationRelativeTo(null); jf.setDefaultCloseOperation(3); jf.setTitle("图片处理"); //布局管理器 BorderLayout bl = new BorderLayout(); jf.setLayout(bl); JPanel east = new JPanel(); east.setOpaque(false); east.setPreferredSize(new Dimension(430,0)); east.setLayout(new FlowLayout(FlowLayout.CENTER,10,10)); JPanel eastorder = new JPanel();//指令集界面 eastorder.setOpaque(false); eastorder.setPreferredSize(new Dimension(430,0)); eastorder.setLayout(new FlowLayout(FlowLayout.CENTER,10,10)); //选项卡 JTabbedPane jTabbedPane = new JTabbedPane(); jTabbedPane.addTab("图片&视频",east); jTabbedPane.addTab("指令集",eastorder); jf.add(jTabbedPane,BorderLayout.EAST); JPanel south = new JPanel(); south.setOpaque(false); south.setPreferredSize(new Dimension(0,50)); south.setLayout(new FlowLayout(FlowLayout.CENTER,300,0)); jf.add(south,BorderLayout.SOUTH); this.setOpaque(false); jf.add(this,BorderLayout.CENTER); photoMouse = new photoMouse(); photoMouse.setUi(this); //可见 jf.setVisible(true); //添加鼠标监听器 this.addMouseListener(photoMouse); this.addMouseMotionListener(photoMouse); //获取画笔,传递 Graphics g = this.getGraphics(); photoMouse.setG(g);
这里的setOpaque()方法是背景透明的方法,当设置为false时该组件的背景色会被设置为透明。
这里我运用了选项卡组件JTabbedPane
其主要方法是jTabbedPane.addTab(名称,组件);
界面的整体思路是将整体界面划分为中,东,南三个部分,分别用this,jTabbedPane,north来承装
南边的没什么好说,我就用了一个Jpanel组件。中间的用this是因为中间的界面是用来绘制图片的,所以我让这个类继承了Jpanel类,方便后续重写paint()方法
东面用了一个选项卡组件,然后我有创建了east和eastorder这两个Jpanel组件放到上面,当然如果感觉空间不够还可以继续添加。这样就有跟多的空间来显示图片了。
2.lookandfeel和统一设置字体
窗体界面的风格是系统的默认风格,当然我们可以重写属于自己的UI界面(下文会讲我自己写了一个ui界面,但是失败了,使用后十分的卡顿),也可以改变窗体的风格。在java给出的几种风格中,我感觉NimbusLookAndFeel是最棒的
//lookandfell设置 try { UIManager.setLookAndFeel(new NimbusLookAndFeel()); } catch (UnsupportedLookAndFeelException e) { e.printStackTrace(); }
统一设置字体纯粹是为了界面好看和方便布局,其具体方法是我网上找来的
//统一设置字体,父界面设置之后,所有由父界面进入的子界面都不需要再次设置字体 private void InitGlobalFont(Font font) { FontUIResource fontRes = new FontUIResource(font); for (Enumeration<Object> keys = UIManager.getDefaults().keys(); keys.hasMoreElements();) { Object key = keys.nextElement(); Object value = UIManager.get(key); if (value instanceof FontUIResource) { UIManager.put(key, fontRes); } } }
将这个方法写在ui类中然后在创建窗体时加一句话
InitGlobalFont(new Font("楷体", Font.BOLD, 20)); //统一设置字体
这样你以后加入界面的所有文字的字体,只要你不单独设置,都会统一成你设置的格式。
3.顶部菜单
//****************************顶部菜单界面********************************// //添加顶部菜单 //菜单条 //JMenuBar --> JMenu --> JMenuItem JMenuBar jMenuBar = new JMenuBar(); JMenu jMenu = new JMenu("文件"); JMenuItem t1 = new JMenuItem("打开"); JMenuItem t2 = new JMenuItem("退出"); JMenuItem t3 = new JMenuItem("撤回", KeyEvent.VK_Z); t3.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, ActionEvent.CTRL_MASK)); jMenuBar.add(jMenu); jMenu.add(t1); jMenu.addSeparator();//添加分割线 jMenu.add(t2); jMenu.add(t3); t1.addMouseListener(photoMouse); t2.addMouseListener(photoMouse); t3.addMouseListener(photoMouse); jf.setJMenuBar(jMenuBar);
JMenuBar组件,可以直接添加到窗体上,也可以添加快捷键和扩展
4.滑道JSlider和east界面
具体的实现详见代码,我都有注释
/** * * @param name 名称 * @param min 最小值 * @param max 最大值 * @param value 显示值 * @param MajorTickSpacing 大标度间隔 * @param MinorTickSpacing 小标度间隔 * @param PaintLables 确定是否在滑块上绘制标签 * @param PaintTicks 确定是否在滑块上绘制刻度标记 * @param PaintTrack 确定是否在滑块上绘制滑道 * @param SnapToTicks 指定为 true,则滑块(及其所表示的值)解析为最靠近用户放置滑块处的刻度标记的值 * @param changeListener 监听器 * @param Opaque false为背景透明 * @param jpanel 在哪个组件上添加 *///新建滑条 public void creatJSlider(String name, int min, int max, int value, int MajorTickSpacing, int MinorTickSpacing, boolean PaintLables, boolean PaintTicks, boolean PaintTrack, boolean SnapToTicks, ChangeListener changeListener,boolean Opaque,JPanel jpanel){ jpanel.add(new JLabel(name)); JSlider jSlider = new JSlider(min,max,value); jSlider.setMajorTickSpacing(MajorTickSpacing); jSlider.setMinorTickSpacing(MinorTickSpacing); jSlider.setPaintLabels(PaintLables);//确定是否在滑块上绘制标签 jSlider.setPaintTicks(PaintTicks);//确定是否在滑块上绘制刻度标记 jSlider.setPaintTrack(PaintTrack);//确定是否在滑块上绘制滑道 jSlider.setSnapToTicks(SnapToTicks);//指定为 true,则滑块(及其所表示的值)解析为最靠近用户放置滑块处的刻度标记的值 jSlider.addChangeListener(changeListener); jSlider.setOpaque(Opaque); jpanel.add(jSlider); }
这里我创建了一个方法用于方便创建滑条,这样我们在对图片进行处理的时候,就不需要手动修改类似于马赛克边框像素值、轮廓临界值选取的值了,直接在界面中通过滑条选取就变得十分方便。
然后是我写的east界面
//****************************图片&视频界面********************************// //右侧添加按钮 ArrayList<String> functionlist = new ArrayList(14); functionlist.add("加载图片"); functionlist.add("全局马赛克"); functionlist.add("局部马赛克"); functionlist.add("灰度化"); functionlist.add("轮廓"); functionlist.add("二值化"); functionlist.add("油画"); functionlist.add("卷积"); functionlist.add("撤回"); functionlist.add("刷新"); functionlist.add("打开摄像头"); functionlist.add("关闭摄像头"); functionlist.add("顺时针旋转"); functionlist.add("恢复"); for(String s : functionlist){ JButton b = new JButton(s); b.setPreferredSize(new Dimension(150,50)); east.add(b); b.addActionListener(photoMouse); } //创建右侧滑道 creatJSlider("马赛克边框像素值:",5,25,15,5,5, true,true,true,true,photoMouse,false,east); creatJSlider("马赛克范围选取:",3,7,5,1,1, true,true,true,true,photoMouse,false,east); creatJSlider("二值化临界值选取:",0,255,127,50,25, true,true,true,false,photoMouse,false,east); creatJSlider("轮廓临界值选取:",0,30,15,6,3, true,true,true,false,photoMouse,false,east); creatJSlider("卷积核选择:",1,4,1,1,1, true,true,true,false,photoMouse,false,east); east.add(new JLabel("当前加载状态:")); JLabel j = new JLabel("未加载"); east.add(j); photoMouse.setJ(j); JPanel jPanele = new JPanel(); jPanele.setOpaque(false); jPanele.add(new JLabel("当前路径:")); JTextField jtf=new JTextField(20); photoMouse.setJtf(jtf); jPanele.add(jtf); JButton button=new JButton("选择"); button.addActionListener(photoMouse); jPanele.add(button); east.add(jPanele);
5.下侧界面和指令集界面
//*****************************下侧界面************************************// //下侧翻页按钮 ArrayList<String> functionlistdown = new ArrayList<>(2); functionlistdown.add("上一张"); functionlistdown.add("下一张"); for(String s : functionlistdown){ JButton b = new JButton(s); b.setPreferredSize(new Dimension(150,40)); south.add(b); b.addActionListener(photoMouse); } JPanel jPanel = new JPanel(); jPanel.setOpaque(false); jPanel.add(new JLabel("当前页数:")); JLabel i = new JLabel(""); jPanel.add(i); photoMouse.setPage(i); south.add(jPanel);
这里我新建了两个按钮,目的是在后面处理图片的时候可以导入一个图片包,方便翻页
指令集界面代码如下:
//***************************指令集界面**********************************// //按钮 ArrayList<String> orderslist = new ArrayList<>(); orderslist.add("新建指令集"); orderslist.add("结束创建"); orderslist.add("删除指令集"); orderslist.add("查询/修改"); orderslist.add("使用指令集"); orderslist.add("使用指令集"); for(String s : orderslist){ JButton b = new JButton(s); b.setPreferredSize(new Dimension(150,50)); b.addActionListener(photoMouse); eastorder.add(b); } //右侧下拉列表组件 eastorder.add(new JLabel("指令集选择:")); jComboBox = new JComboBox(); jComboBox.setPreferredSize(new Dimension(250,30)); jComboBox.addItem("--请选择--"); eastorder.add(jComboBox); eastorder.add(new JLabel("修改:")); jTextField = new JTextField(20);//文本框 eastorder.add(jTextField); JButton jButton = new JButton("确定"); jButton.setPreferredSize(new Dimension(70,30)); jButton.addActionListener(photoMouse); eastorder.add(jButton); eastorder.add(new JLabel("注:操作的目标是当前选择的指令集")); eastorder.add(new JLabel(" 对指令集的操作需在图片中进行 ")); eastorder.add(new JLabel(" 指令集不支持撤回操作 ")); eastorder.add(new JLabel("当前状态:")); JLabel orderLable = new JLabel(); eastorder.add(orderLable); photoMouse.setOrderLable(orderLable);
6.重写paint()方法
由于我们需要重写的界面是绘图界面,所以我在UI类中继承了Jpanel方法,且this这个容器是加入到center界面中的
分析一下需求:paint()方法的调用有三种情况,一个是第一次界面的绘制,第二个是当窗体被人为改变时,第三个是主动调用。而图片绘制界面没有什么需要固定显示的刚需,所以第一种情况不需要重写paint;第二个,当窗体改变时,窗体的宽和高也会发生改变,所以我们要在paint()中将窗体的宽高传递给需要的类。第三个,人为改变,就是当我们需要主动调用这个方法。分析一下,当我们把一张图片进行过一次处理之后(例如局部马赛克了一点,灰度化了),我们需要图片的显示反馈,可以调用之前写过的drawImage()方法,但是如果每次调用不同方法处理都调用一次draw方法不便于管理,所以我将draw方法写在了重写的paint中,在每次对图片进行处理之后都刷新一下界面,就是调用 paint()方法。就可以实现
延伸一下,既然所有的图片绘制都在paint中进行,我们便可以新建一个BufferImage类的集合用于存储我们每一步绘制的变化,这样就方便后续写撤回操作的方法了
代码实现如下,现在请暂时忽略视频部分:
@Override public void paint(Graphics g){ if(photoMouse.isPhotoflag()){//视频 if(threadop != null){ threadop.setJpanelhight(getHeight()); threadop.setJpanelwidth(getWidth()); } }else {//图片 bufferedImage = photoMouse.getBuffImage(); if(bufferedImage != null){ if(!isreduce){//如果当前操作不是撤回操作 int width = bufferedImage.getWidth(); int hight = bufferedImage.getHeight(); BufferedImage bf = new BufferedImage(width,hight,BufferedImage.TYPE_INT_ARGB); for (int i = 0; i < width; i++) { for (int j = 0; j < hight; j++) { bf.setRGB(i,j,bufferedImage.getRGB(i,j)); } } bufferlist.add(bf); } isreduce = false; imageMethod.drawImage(bufferedImage,g,getWidth(),getHeight()); } photoMouse.setJpanelhight(getHeight()); photoMouse.setJpanelwidth(getWidth()); photoMouse.setG(this.getGraphics()); } super.paint(g); }
注意!这里的将bufferImag添加到集合我没有选择直接赋值,因为那样是赋值地址,如果图片经过修改之后地址没变,那么存入进去的图片也会随之修改,就没有了存储效果,所以我们新建了一张图片,对每个像素点进行复制,这样集合中每个步骤的图片地址都各不相同,达成储存效果。
4.监听器类
1.按钮事件
@Override//点击按钮 public void actionPerformed(ActionEvent e){ if(e.getActionCommand() == "加载图片"){ j.setText("未加载"); //创建文件对象 File file = new File(path); num = 0; files = file.listFiles(new FilenameFilter() {//文件过滤器 @Override public boolean accept(File dir, String name) { if(name.endsWith(".jpg")||name.endsWith(".png")){ num++; return true; } return false; } }); //读取图片文件数据,返回对应的缓冲图片 try { buffImage = ImageIO.read(files[i]); } catch (IOException e1) { e1.printStackTrace(); } origal = files[i]; page.setText(i+1+""); jtf.setText(files[i].getAbsolutePath()); j.setText("图片加载完成"); }else if(e.getActionCommand() == "打开摄像头"){ op = new Threadop(); ui.setThreadop(op); op.setG(g); j.setText("摄像头已打开"); op.start(); photoflag = true; }else if(e.getActionCommand() == "关闭摄像头"){ op.setOpenflag(true); photoflag = false; //让正在进程中的bufferImage先draw完,再repaint(),所以先等一会 try { Thread.sleep(100); } catch (InterruptedException e1) { e1.printStackTrace(); } j.setText("摄像头已关闭"); }else if(e.getActionCommand() == "上一张"){ if(i-1>=0){ i--; try { buffImage = ImageIO.read(files[i]); } catch (IOException e1) { e1.printStackTrace(); } origal = files[i]; page.setText(i+1+""); jtf.setText(files[i].getAbsolutePath()); } }else if(e.getActionCommand() == "下一张"){ if(files != null){ if(i+1<=num){ i++; try { buffImage = ImageIO.read(files[i]); } catch (IOException e1) { e1.printStackTrace(); } origal = files[i]; page.setText(i+1+""); jtf.setText(files[i].getAbsolutePath()); } } }else if(e.getActionCommand() == "选择"){ openfile("F:\\LOL美术资源"); }else if(buffImage != null || photoflag){ //*************************下面是对buffImage的操作*******************************// if(e.getActionCommand() == "全局马赛克"){ if(photoflag){//视频 op.setNamee("全局马赛克"); op.setSize(size); }else {//图片 buffImage = imageMethod.masaikeall(buffImage,size); j.setText("全局马赛克加载完成"); } }else if(e.getActionCommand() == "灰度化"){ if(photoflag){ op.setNamee("灰度化"); }else { buffImage = imageMethod.grayImage(buffImage); j.setText("灰度化加载完成"); } }else if(e.getActionCommand() == "卷积"){ if(photoflag){ op.setNamee("卷积"); op.setShrape(sharpe); }else { buffImage = imageMethod.sharpening(buffImage,sharpe); j.setText("锐化加载完成"); } }else if(e.getActionCommand() == "轮廓"){ if(photoflag){//视频 op.setNamee("轮廓"); }else {//图片 buffImage = imageMethod.lunkuo(buffImage,lun); j.setText("轮廓加载完成"); } }else if(e.getActionCommand() == "二值化"){ if(photoflag){//视频 op.setNamee("二值化"); op.setErzhi(erzhi); }else {//图片 buffImage = imageMethod.erzhi(buffImage,erzhi); j.setText("二值化加载完成"); } }else if(e.getActionCommand() == "油画"){ if(photoflag){//视频 op.setNamee("油画"); }else {//图片 buffImage = imageMethod.youhua(buffImage); j.setText("油画加载完成"); } }else if(e.getActionCommand() == "恢复"){ if(photoflag){//视频 op.setNamee("恢复"); }else {//图片 try { buffImage = ImageIO.read(origal); } catch (IOException e1) { e1.printStackTrace(); } j.setText("恢复完成"); } }else if(e.getActionCommand() == "顺时针旋转"){ if(photoflag){//视频 op.setNamee("顺时针旋转"); }else {//图片 buffImage = imageMethod.rotate(buffImage); j.setText("顺时针旋转完成"); } }else if(e.getActionCommand() == "撤回"){ if(photoflag){ j.setText("视频撤回操作正在开发ing..."); }else { ui.arrreduce(); ui.setIsreduce(true); j.setText("撤回ing"); } }else if(e.getActionCommand() == "刷新"){ } if(e.getActionCommand() != "局部马赛克"){ ui.repaint(); } }
按钮事件中包括图片的打开和对bufferImage的操作,这里打开图片我除了运用ImageIO.read()的基本方法,还添加了文件过滤器,这样可以筛选我们需要的文件格式,也可以将path写为一个存放图片的文件夹,在界面内部就可以通过翻页读取文件夹内的所有图片。
2.JFileChooser
上面我写按钮事件中用到了openfile()这个方法,具体实现包括了JfileChooser类的实现和内部文件过滤器,具体代码实现如下
//打开文件 public void openfile(String path){ JFileChooser fc = new JFileChooser(path){ private static final long serialVersionUID = 1; @Override public JDialog createDialog(Component parent) { JDialog dialog = super.createDialog(parent); dialog.setMinimumSize(new Dimension(600, 600)); return dialog; } }; //文件过滤器 String [][] filenames = {{".jpg","jpg文件"},{".png","png文件"}}; for(final String[] filename : filenames){ fc.setFileFilter(new javax.swing.filechooser.FileFilter() { @Override public boolean accept(File f) { if(f.getName().endsWith(filename[0]) || f.isDirectory()){ return true; } return false; } @Override public String getDescription() { return filename[1]; } }); } //选择文件 int val = fc.showOpenDialog(null); if(val==fc.APPROVE_OPTION) { //正常选择文件 try { buffImage = ImageIO.read(fc.getSelectedFile()); } catch (IOException e1) { e1.printStackTrace(); } imageMethod.drawImage(buffImage,g,Jpanelwidth,Jpanelhight); origal = fc.getSelectedFile(); page.setText(""); jtf.setText(fc.getSelectedFile().toString()); } else { //未正常选择文件,如选择取消按钮 jtf.setText("未选择文件"); } }
当然,你也可以不设置文件过滤器,这样就可以读取所有类型的文件了,只是如果文件类型不对你读取之后使用会报错哦
3.局部马赛克的撤回
细心的同学应该会发现,我在图片处理方法中,只有局部马赛克是直接通过画笔绘制上容器,在改变bufferImage输出改变后的bufferimage,而其他的方法是先改变bufferimage中的像素点,再输出后通过按钮方法中的repaint()调用paint()方法去绘制bufferimage。这是因为我发现如果局部马赛克也跟其他方法一样的话,绘制效果太慢了,严重影响体验,所以才这样做,而且我在按钮事件中加入了这段代码
if(e.getActionCommand() != "局部马赛克"){ ui.repaint(); }
这样局部马赛克就不会调用paint()方法,也就是说局部马赛克并不会存储进我们创建的用于撤回操作的集合。所以我们要在其他地方单独写局部马赛克的撤回方法。
分析一下,绘制马赛克的撤回应该是一笔一笔的撤回,即为按下撤回按钮后,应该撤回上一笔绘制的马赛克。所以我们需要在鼠标案件释放时,把当前图片存储进入集合。
代码如下:
@Override//鼠标释放 public void mouseReleased(MouseEvent e){ if(e.getSource() instanceof JMenuItem){ JMenuItem jm = (JMenuItem)e.getSource(); String n = jm.getText(); if("打开".equals(n)){ openfile("F:\\LOL美术资源"); }else if("退出".equals(n)){ System.exit(0); }else if("撤回".equals(n)){ ui.arrreduce(); ui.setIsreduce(true); j.setText("快捷撤回ing"); ui.repaint(); } }else if(name.equals("局部马赛克")){ int width = buffImage.getWidth(); int hight = buffImage.getHeight(); BufferedImage bf = new BufferedImage(width,hight,BufferedImage.TYPE_INT_ARGB); for (int i = 0; i < width; i++) { for (int j = 0; j < hight; j++) { bf.setRGB(i,j,buffImage.getRGB(i,j)); } } ui.getBufferlist().add(bf); } }
上面的部分是菜单栏按钮的监听,下面则是马赛克绘制加入存储集合的方法,同样,新建一个bufferimage,对每一个像素点都进行复制。
4.滑块监听
这个很简单,没什么好说的
@Override//滑块变化 public void stateChanged(ChangeEvent e){ JSlider jSlider = (JSlider) e.getSource(); if(jSlider.getMaximum() == 25){ size = jSlider.getValue(); op.setSize(size); }else if(jSlider.getMaximum() == 7){ count = jSlider.getValue(); }else if(jSlider.getMaximum() == 4){ sharpe = jSlider.getValue(); op.setShrape(sharpe); }else if(jSlider.getMaximum() == 255){ erzhi = jSlider.getValue(); op.setErzhi(erzhi); }else if(jSlider.getMaximum() == 30){ lun = jSlider.getValue(); op.setLun(lun); } }
5.指令集的实现
1.指令集功能分析
指令集是指,你将你对当前图片的一系列操作全部存储起来,变成一道拥有一系列指令的集合,你可以对任意图片去使用这个指令集来达到批量绘制的效果。
指令集功能应该包括常见的增,删,改,查,还有对其他图片使用。
由于指令中并不只包含文字指令,有的指令例如全局马赛克还要记录马赛克边框的大小,局部马赛克更要记录坐标,马赛克边框大小,马赛克选定范围等数字,所以我决定将这些数字以我自己定义的方式存储进一个字符串,这样这个字符串就作为指令集可以对其他图片使用,在使用的时候在通过我定义的方式解析指令,然后对图片进行操作,就完成了指令集的效果。
2.按钮事件
先上代码,根据代码我来进行讲解:
//********************************************指令集*********************************************// else if(e.getActionCommand() == "新建指令集"){ orderlist = new String(""); orderflag = true; orderLable.setText("创建指令集ing..."); }else if(e.getActionCommand() == "结束创建"){ jcbnum++; ui.getjComboBox().addItem("指令集"+(jcbnum+1));//显示 orderflag = false; orderlist = orderlist.substring(6);//删除前六个字符 orderlistlist.add(orderlist);//集合 orderLable.setText("成功创建指令集"+(jcbnum+1)); }else if(e.getActionCommand() == "删除指令集"){ if(ui.getjComboBox().getSelectedIndex() != 0){ orderlistlist.remove(ui.getjComboBox().getSelectedIndex()-1);//集合 orderLable.setText("成功删除"+ui.getjComboBox().getSelectedItem()); ui.getjComboBox().removeItemAt(ui.getjComboBox().getSelectedIndex());//显示 } }else if(e.getActionCommand() == "查询/修改"){ if(ui.getjComboBox().getSelectedIndex() != 0){ ui.getjTextField().setText(orderlistlist.get(ui.getjComboBox().getSelectedIndex()-1)); searchflag = true; orderLable.setText("正在查询/修改指令集,按“确定”结束"); } }else if(e.getActionCommand() == "确定"){ if(searchflag){ orderlistlist.set(ui.getjComboBox().getSelectedIndex()-1,ui.getjTextField().getText()); searchflag = false; orderLable.setText("成功修改/查询"); }else { orderLable.setText("等待操作ing..."); } }else if(e.getActionCommand() == "使用指令集"){ if(ui.getjComboBox().getSelectedIndex() != 0){ orderLable.setText("正在计算中..."); buffImage = uesOrderList(buffImage,orderlistlist.get(ui.getjComboBox().getSelectedIndex()-1)); orderLable.setText("成功使用"+ui.getjComboBox().getSelectedItem()); } }else { name = e.getActionCommand(); } } //如果当前正在创建指令集 if(orderflag){ if(e.getActionCommand() == "全局马赛克"){ orderlist = orderlist + e.getActionCommand() + "," + size + ","; }else if (e.getActionCommand() == "二值化"){ orderlist = orderlist + e.getActionCommand() + "," + erzhi + ","; }else if(e.getActionCommand() == "轮廓"){ orderlist = orderlist + e.getActionCommand() + "," + lun + ","; }else if(e.getActionCommand() == "卷积"){ orderlist = orderlist + e.getActionCommand() + "," + sharpe + ","; }else { orderlist = orderlist + e.getActionCommand() + ","; } }
新建指令集和结束创建,新建时定义一个新的字符串用于指令集的存储,并将orderflag定义为true,表示当前正在创建指令集。这里的指令集我把它放在了UI界面的下拉栏中,一旦结束创建orderflag = false;,显示新增“指令集x”,并且会删除指令集的前六个字符,因为这六个字符为“新建指令集,”,当然不删除也可以,因为后面也不会写对应的解析代码,最后将处理好的字符串放入用于存储的集合,实现可以创建多个指令集的效果。
后面的删除和查询/修改都是对当前下拉栏中选定的指令集进行操作,而我下拉栏的第一个是放置的“--请选择--”,所以排除第一个,对其他项目进行操作,当然是在有选择的前提下。删除和修改都包括显示的删除修改,即为下拉栏中我们能看见的,还有实际存储的储存字符串的集合的删除,修改。
而指令集实际指令的添加原理是:由于所有的指令都会经过按钮,除了局部马赛克事件,所有指令在按钮事件就被我加入了字符串,并且以“,”隔开。
3.局部马赛克指令的添加
由于局部马赛克在按钮事件中并不会repaint(),所以我们单独写
@Override//鼠标拖动 public void mouseDragged(MouseEvent e){ if(name.equals("局部马赛克")){ j.setText("绘制马赛克ing"); int x = e.getX(); int y = e.getY(); int c = (count-1)/2; for (int i = 0; i < count; i++) { for (int k = 0; k < count; k++) { buffImage = imageMethod.masaike(buffImage,x-c*size+i*size,y-c*size+k*size,size,Jpanelwidth,Jpanelhight,g); } } if(orderflag){ orderlist = orderlist + "," + x + "," + y + "," + size + "," + Jpanelwidth + "," + Jpanelhight + "," + count + ","; } } }
这里的指令排序是我自定义的,只要后续使用的时候解析也按照排序的解析即可。
4.指令集的使用
现在我们已经拥有了一串存储以逗号隔开的指令的字符串,相当于给了我们一张图片,一个字符串,要我们输出处理之后的图片
首先要将字符串以“,”为界限分割开,存储进一个集合
//使用指令集方法 public BufferedImage uesOrderList(BufferedImage bufferedImage,String s){ ArrayList<String> temlist = new ArrayList<>(); int fromIndex = 0; int o = 0; //将字符串s中的指令存储到集合中 while (true){ o = s.indexOf(",",fromIndex); if(o == -1)break; String tem = s.substring(fromIndex,o); temlist.add(tem); fromIndex = o+1; }
这里看不懂的小伙伴可以去复习一下字符串String的方法
然后将解析好的字符串对图片使用
for (int i = 0; i < temlist.size(); i+=num) { if(temlist.get(i).equals("全局马赛克")){ bufferedImage = imageMethod.masaikeall(bufferedImage,Integer.parseInt(temlist.get(i+1))); num = 2; }else if(temlist.get(i).equals("二值化")){ bufferedImage = imageMethod.erzhi(bufferedImage,Integer.parseInt(temlist.get(i+1))); num = 2; }else if(temlist.get(i).equals("轮廓")){ bufferedImage = imageMethod.lunkuo(bufferedImage,Integer.parseInt(temlist.get(i+1))); num = 2; }else if(temlist.get(i).equals("卷积")){ bufferedImage = imageMethod.sharpening(bufferedImage,Integer.parseInt(temlist.get(i+1))); num = 2; }else if(temlist.get(i).equals("局部马赛克")){ int ma; for(ma = i;"".equals(temlist.get(ma+1));ma +=7){ int x = Integer.parseInt(temlist.get(ma+2)); int y = Integer.parseInt(temlist.get(ma+3)); int size = Integer.parseInt(temlist.get(ma+4)); int Jpanelwidth = Integer.parseInt(temlist.get(ma+5)); int Jpanelhight = Integer.parseInt(temlist.get(ma+6)); int count = Integer.parseInt(temlist.get(ma+7)); int c = (count-1)/2; for (int j = 0; j < count; j++) { for (int k = 0; k < count; k++) { bufferedImage = imageMethod.masaike(bufferedImage,x-c*size+j*size,y-c*size+k*size,size,Jpanelwidth,Jpanelhight,g); } } if(ma+9>temlist.size())break; } num = ma-i+1; }else if(temlist.get(i).equals("灰度化")){ bufferedImage = imageMethod.grayImage(bufferedImage); num = 1; }else if(temlist.get(i).equals("油画")){ bufferedImage = imageMethod.youhua(bufferedImage); num = 1; }else if(temlist.get(i).equals("顺时针旋转")){ bufferedImage = imageMethod.rotate(bufferedImage); num = 1; }else { num = 1; } } return bufferedImage; }
注意局部马赛克那一块的逻辑,需要多次调试的,那一块花了我好久0.0
现在,指令集就做好啦!