Java像素图像处理
在学会了之前的画板功能之后,我们就更进一步进入图像处理部分,下面来学习一下纯像素级图像处理项目,不使用任何高级滤镜工具,全部通过遍历图片每一个像素、拆分 RGB、重新计算颜色值的方式实现图片特效。
让我们来看一下具体的内容:
实现功能包括:原图还原,图片灰度化,图片二值化(黑白效果),图片马赛克模糊,蓝调滤镜,黄昏暖色滤镜,反片颜色反转。
以上所有代码都应写在监听器中,通过点击按钮来实现对应功能,以下我们只看监听器内代码。
先实现监听器接口,并定义变量:
Public class ImageListener implements ActionListener(){ Graphics g; int w; int h; int[][] data;接下来,我们写对应的方法:
一:原图还原:
先了解一下几个基础概念:
IO = Input(输入) + Output(输出),也就是程序和外部设备读写数据的操作:
读文件、写文件、读取图片、网络传输、读取键盘输入都属于 IO 操作。
IOException全称 Input/Output Exception,中文:输入输出异常。
当程序读写文件、图片、网络等外部资源失败时,Java 就会抛出这个异常。
接着,在进行原图还原之前,我们首先要新建一个方法,把原图每个点的像素数据存到data数组里面,
我们要认识一个新的类BufferedImage类
BufferedImage img = ImageIO.read(new File(path));拆解一下这行代码:
① new File(path)
File 是 java.io 自带类,代表磁盘上的文件 / 文件夹路径;
path = "Image/1.png",就是项目里图片的相对路径;
作用:根据路径定位到硬盘里的 1.png 图片文件,生成文件对象。
② ImageIO.read(文件对象)
ImageIO:Java AWT 内置图像工具类,专门读写图片(png/jpg/bmp 等);
.read():静态方法,接收一个文件,从磁盘读取图片,加载到内存;
读取完成后,返回一个 BufferedImage 图像对象;
重点:这个方法读取文件会抛出 IOException,所以必须放在 try{} 里。
③ BufferedImage img
BufferedImage:内存图像缓冲区,就是存放在内存里的图片;
它封装了整张图片的所有信息:宽度、高度、每一个像素的 ARGB 颜色;
img 是变量名,用来接收加载好的内存图片;
后续写的 img.getWidth()、img.getRGB(i,j) 全是调用这个内存图片对象的方法。
总结这行代码就是:根据路径找到磁盘上的图片文件,把整张图片读取到内存,生成内存图像对象,用变量 img 接收这个图片。
接着,我们来了解一下try-catch用法:
Java 程序运行时可能出现异常(错误),比如文件找不到、读取失败、空指针。
如果不处理异常,程序会直接崩溃退出;
try-catch 用来捕获、处理异常,保证程序不会直接卡死,还能自定义报错逻辑。、
1. try 代码块
把有风险、会抛出受检异常的代码放这里:
比如上面的 ImageIO.read(new File(path)),读取文件时可能出现:
文件不存在
图片损坏无法解析
路径写错
没有文件读取权限
这类 IO 操作会抛出 IOException,必须放进 try,否则编译器直接报错。
2. catch 捕获块
IOException e:指定要捕获的异常类型,e 是异常对象,存储错误详情;
一旦 try 内部代码报错,程序立刻跳转到对应 catch 块,不会终止程序;
代码格式如下:
try { // 【可能出错、会抛出异常的代码】 } catch (异常类型 变量名) { // 【捕获到异常后执行的补救/报错代码】 } // 可选拓展:finally(无论是否报错都会执行) finally { // 释放资源、关闭文件等操作 }我们直接看catch部分:
catch (IOException e) { throw new RuntimeException(e); }IOException是专门用来封装程序读写外部资源失败的错误信息的类
这两行代码作用为:
一旦读取图片失败,程序跳到这个 catch 块;
把捕获到的 IO 异常包装成运行时异常重新抛出;
以上了解完之后,我们就可以用双层for循环,把每一个像素点RGB存入数组:
int w = img.getWidth(); int h = img.getHeight(); int[][] data = new int[w][h]; for(int i =0;i<W;i++){ for(int j = 0;j<h;j++){ data[w][h] = img.getRGB(i,j);再加上try-catch,完整代码如下:
public int[][] getImageData() { String path ="Image/1.png"; try { BufferedImage img = ImageIO.read(new File(path)); w = img.getWidth(); h = img.getHeight(); int[][] data = new int[w][h]; for (int i = 0; i < w; i++) { for (int j = 0; j < h; j++) { data[i][j] = img.getRGB(i, j); } } return data; } catch (IOException e) { throw new RuntimeException(e); } }data数组建立完成之后,我们就可以通过遍历data数组来进行原图还原了:
public void draw() { for (int i = 0; i < w; i++) { for (int j = 0; j < h; j++) { int pix = data[i][j]; Color c1 = new Color(pix); g.setColor(c1); g.drawRect(i + 50, j + 100, 1, 1); } } }效果:
二:灰度化
在进行灰度化之前,先来认识一下标准 ARGB32 位像素(getRGB()返回的 int)
总长度:32 位二进制整数,4 个通道各占 8 bit(1 字节)
分段分配(从高位到低位):
A 透明度:第 31~24 位 → 8 位
R 红色:第 23~16 位 → 8 位
G 绿色:第 15~8 位 → 8 位
B 蓝色:第 7~0 位 → 8 位
数值范围:
每个通道 8 位二进制,取值范围:0 ~ 255
R=0 无红色,R=255 红色拉满
G=0 无绿色,G=255 绿色拉满
B=0 无蓝色,B=255 蓝色拉满
A=0 完全透明,A=255 完全不透明
暗→亮(0→255)
最大数字11111111,十进制为255
灰度化的本质就是取平均,三色数值完全相等,画面无彩色,只有黑白灰
代码如下:
public void draw1() { for (int i = 0; i < w; i++) { for (int j = 0; j < h; j++) { int pix = data[i][j]; Color c2 = new Color(pix); int red = c2.getRed(); int green = c2.getGreen(); int blue = c2.getBlue(); int ave = (red + green + blue) / 3; Color c3 = new Color(ave, ave, ave); g.setColor(c3); g.drawRect(i + 400, j + 100, 1, 1); } } }效果:
三:二值化
图像里只有两种颜色:纯黑 (0)、纯白 (255),不存在中间灰色,只有 0 和 255 两个数值,所以叫 “二值”
完整两步流程
第一步:彩色图 → 灰度图
先计算 RGB 平均值得到gray(0 最黑,255 最亮),把彩色转成单通道亮度。(必须先进行灰度化:彩色有 3 个通道,无法直接判断明暗,必须合并成单通道亮度)
第二步:阈值分割
设置一个阈值(分界线),这里阈值 = 127(0~255 正中间):
规则 1:像素灰度 > 127 → 归为白色
规则 2:像素灰度 ≤ 127 → 归为黑色
所有中间灰度全部被强制抹除,整张图片只剩下黑白两种色块。
代码如下:
public void draw2() { for (int i = 0; i < w; i++) { for (int j = 0; j < h; j++) { int pix = data[i][j]; Color c2 = new Color(pix); int red = c2.getRed(); int green = c2.getGreen(); int blue = c2.getBlue(); int gray = (red + green + blue) / 3; if (gray > 127) { Color c3 = new Color(255, 255, 255); g.setColor(c3); g.drawRect(i + 400, j + 100, 1, 1); } else { Color c3 = new Color(0, 0, 0); g.setColor(c3); g.drawRect(i + 400, j + 100, 1, 1); } } } }效果:
四:马赛克
步骤如下:
- 分块:把原图切割成无数个大小相等的正方形小块(以 5×5 像素一块为例);
- 取代表色:只拿每一块左上角的像素颜色,作为这个方块唯一颜色;
- 整块同色填充:把方块里所有位置全部画成这一个颜色;
代码如下:
public void draw3() { int m = 5; for (int i = 0; i < w; i = i + m) { for (int j = 0; j < h; j = j + m) { int pix = data[i][j]; Color c = new Color(pix); for (int k = 0; k < m; k++) { for (int l = 0; l < m; l++) { g.setColor(c); g.drawRect(i + k + 400, j + l + 100, 1, 1); } } } } }效果:
五:蓝调
增加蓝色效果,削弱红绿,原理与上述基本相同
代码如下:
public void draw4() { for (int i = 0; i < w; i++) { for (int j = 0; j < h; j++) { int pix = data[i][j]; Color c1 = new Color(pix); int red = c1.getRed(); int green = c1.getGreen(); int blue = c1.getBlue(); red = red / 2; green = green / 2; blue = blue + 60; if (blue > 255) { blue = 255; } Color c2 = new Color(red, green, blue); g.setColor(c2); g.fillRect(i + 400, j + 100, 1, 1); } } }效果:
六:黄昏
增强红绿色效果,削弱蓝色,与蓝调类似
代码如下:
public void draw5() { for (int i = 0; i < w; i++) { for (int j = 0; j < h; j++) { int pix = data[i][j]; Color c = new Color(pix); int red = c.getRed(); int green = c.getGreen(); int blue = c.getBlue(); red = red + 60; if (red > 255) { red = 255; } green = green + 30; if (green > 255) { green = 255; } blue = blue / 3; Color c2 = new Color(red, green, blue); g.setColor(c2); g.fillRect(i + 400, j + 100, 1, 1); } } }效果:
六:反片
用通道最大值 255 减去原始亮度,得到互补色
例如:
原始红色 0(无红)→ 255 - 0 = 255(纯红)
原始红色 255(纯红)→ 255 - 255 = 0(无红)
亮变暗,暗变亮,每个颜色通道全部翻转。
代码如下:
public void draw6() { for (int i = 0; i < w; i++) { for (int j = 0; j < h; j++) { int pix = data[i][j]; Color c = new Color(pix); int red = c.getRed(); int green = c.getGreen(); int blue = c.getBlue(); red = 255 - red; green = 255 - green; blue = 255 - blue; Color c2 = new Color(red, green, blue); g.setColor(c2); g.fillRect(i + 400, j + 100, 1, 1); } } }效果:
最后,重写actionPerform方法:
public void actionPerformed(ActionEvent e) { String key = e.getActionCommand(); System.out.println(key); if (key.equals("原图")) { data = getImageData(); draw(); } else if (key.equals("灰度化")) { draw1(); } else if (key.equals("二值化")) { draw2(); } else if (key.equals("马赛克")) { draw3(); } else if (key.equals("蓝调")) { draw4(); } else if (key.equals("黄昏")) { draw5(); } else if (key.equals("反片")) { draw6(); } }注意:有返回值要加上data=getImageData();
把 getImageData() 读取到的全新原图像素数组,赋值给全局变量 data,覆盖旧数据:
从硬盘加载原始图片像素;
将返回的像素数组存入全局 data;
draw() 读取更新后的 data,才能画出原图。