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,才能画出原图。