近接触了一下Java的图象处理的知识。看到网络上面的教程都比较乱,所以自己写一个大致的总结,把关键代码写下来,方便未来参考。
Java读取图象
Java读取文件其实特别简单。就一行代码:
import java.awt.image.BufferedImage;
import java.io.*;
public BufferedImage myRead(String path) throws IOException {
return ImageIO.read(new File(path));
}
这里用到了ImageIO.read(File)
函数。里面传入的是一个FIle对象。我们的new File(path)
就读入了path路径下的File。这样我们返回的就是一个BufferedImage。
BufferedImage类是继承自Image类的。它有很多优越的性质。以后我们的编程会用到它。
当然我们也可以自己写文件读入,然后将图象文件一个像素一个像素地读入。
之前实训的时候,就实现了Bitmap文件(.png)文件的读入。只需要知道下面Bitmap文件的详细内容,还是不难写的。
Bitmap文件的详细内容private int toIntForNums(byte[] src, int offset, int num) {
int temp = 0;
for (int i = 0; i < num; i++)
temp |= (src[offset + i] & 0xFF) << i * 8;
return temp;
}
public Image myRead(String var1) throws IOException {
try {
// read the Image
FileInputStream in = new FileInputStream(new File(var1));
DataInputStream dis = new DataInputStream(in);
byte[] buffer = new byte[54];
dis.read(buffer, 0, 54);
int width = toIntForNums(buffer, 18, 4);
int height = toIntForNums(buffer, 22, 4);
int size = toIntForNums(buffer, 34, 4);
int numEmptyByte = (size / height - 3 * width) % 4;
int[] pixelArray = new int[width * height];
byte[] pixelBytes = new byte[size];
dis.read(pixelBytes, 0, size);
int index = 0;
for (int i = height - 1; i >= 0; i--) {
for (int j = 0; j < width; j++) {
pixelArray[width * i + j] = toIntForNums(pixelBytes, index, 3) | (0x0FF << 24);
index += 3;
}
index += numEmptyByte;
}
dis.close();
in.close();
return Toolkit.getDefaultToolkit().createImage(new MemoryImageSource(width, height, pixelArray, 0, width));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return null;
}
代码还是很简单的。其实就是从位图信息里面,把图的长宽读出来,然后再把信息后面的值与像素一一对应。这里要解决的是两个问题:
- 像素是从下到上、从左到右保存的。
- 每个像素使用一个或者多个字节表示。如果一个图像水平线的字节数不是4的倍数,这行就使用空字节补齐,通常是ASCII码0。例如有一张5*5的图片,应该会有25个pixels,但是因为5不是4的倍数所以会显示成: xxxxx000 xxxxx000 xxxxx000 xxxxx000 xxxxx000
当然,最后我们用Toolkit.getDefaultToolkit().createImage(new MemoryImageSource(width, height, pixelArray, 0, width));
得出来的是一个Image类型,不是BufferedImage。
Image类型转BufferedImage
BufferedImage比Image类型不知道高到哪里去了,而且由于是继承的Image,转化为Image也很简单。那么怎么把Image类型转为BufferedImage呢?我查到的资料是这样。
private BufferedImage toBufferedImage(Image img) {
if (img instanceof BufferedImage) return (BufferedImage)img;
BufferedImage bImg = new BufferedImage(img.getWidth(null), img.getHeight(null), BufferedImage.TYPE_INT_ARGB);
Graphics2D bGr = bImg.createGraphics();
bGr.drawImage(img, 0, 0, null);
bGr.dispose();
return bImg;
}
处理BufferedImage的ARGB值
有了特别屌的BufferedImage类,我们就可以做一些简单的事情了。例如我们可以把原来的图片转为红色,绿色,蓝色和灰色。
我们知道,大部分图片是由ARGB构成的。A是Alpha,指的图象的透明度,R是Red,G是Green,B是Blue。RGB三种颜色决定了图片最后的颜色。我们用BufferedImage的话,直接用getRGB(x, y),就可以获得(x, y)点上面的ARGB值。返回的是一个int类型。
用下面公式就可以把这个值分成A,R,G,B了。
int pixel = bf.getRGB(x, y);
intalpha=(pixel &0xFF000000)>>24;
intred= (pixel & 0xff0000) >> 16;
intgreen=(pixel & 0xff00) >> 8;
intblue=(pixel & 0xff);
但是这样很麻烦,还得记住那个int类型的四个段分别对应什么,还得用位运算。我们当然有更加简单的方法,只要用到Color类就行了。
Color cl = new Color(bf.getRGB(x, y));
intalpha=cl.getAlpha();
intred= cl.getRed();
intgreen=cl.getGreen();
intblue=cl.getBlue();
这样就可以不去记那么多复杂的东西了。
简单图象处理
能够得到RGB值了,我们就可以对图象进行简单的处理了。
我们记得,图象的本质是一个二维的矩阵,一个数组,数组里面存着一个int值,代表了ARGB四个值。因此,我们只需要让RGB中其中两个变为0,就可以改变图象的颜色了。
// 改成红色
public BufferedImage showChanelR(BufferedImage img) {
for (int i = 0; i < img.getWidth(); i++) {
for (int j = 0; j < img.getHeight(); j++) {
Color pixel = new Color(img.getRGB(i, j));
img.setRGB(i, j, new Color(pixel.getRed(), 0, 0).getRGB());
}
}
return img;
}
// 改成绿色
public BufferedImage showChanelR(BufferedImage img) {
for (int i = 0; i < img.getWidth(); i++) {
for (int j = 0; j < img.getHeight(); j++) {
Color pixel = new Color(img.getRGB(i, j));
img.setRGB(i, j, new Color(0, pixel.getGreen(), 0).getRGB());
}
}
return img;
}
// 改成蓝色
public BufferedImage showChanelR(BufferedImage img) {
for (int i = 0; i < img.getWidth(); i++) {
for (int j = 0; j < img.getHeight(); j++) {
Color pixel = new Color(img.getRGB(i, j));
img.setRGB(i, j, new Color(0, 0, pixel.getBlue()).getRGB());
}
}
return img;
}
当然,我们可以调整这三种颜色的比例,从而得到其他颜色的图象。例如我们可以转为灰色图片。用到的公式是:I = 0.299 * R + 0.587 * G + 0.114 * B
,所以代码可以这样写。
public BufferedImage showGray(BufferedImage img) {
for (int i = 0; i < img.getWidth(); i++) {
for (int j = 0; j < img.getHeight(); j++) {
Color pixel = new Color(img.getRGB(i, j));
img.setRGB(i, j, (int)(pixel.getRed() * 0.299 + pixel.getGreen * 0.587 + pixel.getBlue * 0.114));
}
}
return img;
}
我用上一种方法得出的灰色图会偏暗。理论上这个比例是按人眼的感觉的比例来调的,但是事实就是有些失真了。当然,其实在我们创建BufferedImage对象的时候,是有Type选项的。通常我们创建的是彩色图。
BufferedImage bf= new BufferedImage(width(), height(), BufferedImage.TYPE_INT_ARGB);
我们其实也可以创建灰色图,只需要把BufferedImage.TYPE_INT_ARGB
改为BufferedImage.TYPE_BYTE_GRAY
所以可以这样把彩色图变为灰色图
public BufferedImage showGray(BufferedImage img) {
BufferedImage grayImag = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
for (int i = 0; i < grayImag.getWidth(); i++) {
for (int j = 0; j < grayImag.getHeight(); j++) {
grayImag.setRGB(i, j, img.getRGB(i, j));
}
}
return grayImag;
}
但是如果是这样,得出的BufferedImage的类型是BufferedImage.TYPE_BYTE_GRAY
类型。我们怎么得到完美的BufferedImage.TYPE_INT_ARGB
的灰度图呢?我们可以试试用Java的一些函数。
public BufferedImage getGrayPicture(BufferedImage originalPic) {
int imageWidth = originalPic.getWidth();
int imageHeight = originalPic.getHeight();
BufferedImage newPic = new BufferedImage(imageWidth, imageHeight,BufferedImage.TYPE_3BYTE_BGR);
ColorConvertOp cco = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null);
cco.filter(originalPic, newPic);
return newPic;
}
记得头文件
import java.awt.color.ColorSpace;
import java.awt.image.ColorConvertOp;
这样得出的图就是完美的灰度图了。
图象的放大缩小——双线性插值法
图象的放大和缩小就不是只是换一个颜色这么简单了。由于在缩小的时候会像素点会变少,放大的时候要多出很多像素点,因此就必须有某种算法可以让图象在放大缩小的过程中不失真。本文介绍的方法叫做双线性插值法。
双线性插值法其实并不难,总共分为两步。
第一步是找点。由于图象是放大缩小的,所以就不可能像之前图象处理一样,采用一对一的方式了。为了保证图象处理之后能够更加平滑,找对应点自然要以一对多的方式。双线性插值法是一对四的。
例如我们要创造的新图象是原图象的2倍,那么例如我们这个图象上面(5,7)这个像素点,这个点,缩小2倍对应的点是(2.5,3.5)。这是个小数,于是我们距离这里最近的四个点(2, 3),(2, 4),(3, 3),(3, 4)。
找点(2.5,3.5)是一个特殊情况,它刚刚好在中间。但是对应的任意一个点,我们向下取整就可以得到(x, y),然后就可以得到(x, y + 1),(x + 1, y),(x + 1, y + 1)这三个点。这样我们采样的四个点就出来了。
当然我们最后还是要考虑数组越界的问题,因此要加一个对边界的处理。代码如下:
int x = (int) Math.floor(oriX);
int y = (int) Math.floor(oriY);
x = x < 0 ? 0 : x;
y = y < 0 ? 0 : y;
int tempX = x + 1 >= imageWidth ? imageWidth - 1 : x + 1;
int tempY = y + 1 >= imageHeight ? imageHeight - 1 : y + 1;
另一方面,刚刚我们是直接除以对应的倍数。例如刚刚的放大两倍,我们就除以2了。这样做有一个问题,就是最后处理的时候,不能尽可能多的使用原图象的像素点。为了尽可能多地使用原像素点。我们把直接的除法变为(x + 0.5) / times - 0.5
这个运算。对应的点就从(x / times, y / times)变成了((x + 0.5) / times - 0.5
, (y + 0.5) / times - 0.5
)。
所谓双线性插值法,其实就是对这四个点做适当的权值,最后加起来。这个权值我们之前见过。我们在处理彩色图象转灰色图象的时候,就是给RGB三种颜色适当的权值,最后出来的图象就是灰色了。这里也是一样,我们要找到最科学的权值。
关于权值的计算,是十分复杂的。我们之间说结论:
假设对应的点的整数部分是x,y,小数部分是u,v,即对应的点是(x + u,y + v)的话。我们用f(x)表示点对应的像素值的函数。那么有公式:
f(x + u, y + v) = (1 - u)(1 - v)f(x,y) + (1 - u)vf(x,y+1) + u(1 - v)f(x+1,y) + uvf(x+1,y+1)
注意:千万不要直接把对应点的RGB值直接代入方程。因为ARGB分为了四个部分,如果我们直接对它进行运算,就会得到奇奇怪怪的结果(我就是找了很久这个bug)。应该把RGB分开来,分别代入这个式子,最后再合起来。
最后,我们上代码:
public BufferedImage changeImage(BufferedImage oriPic, double times) {
int imageWidth = oriPic.getWidth();
int imageHeight = oriPic.getHeight();
int targetWidth = (int)(imageWidth * times);
int targetHeight = (int)(imageHeight * times);
BufferedImage newPic = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_3BYTE_BGR);
for (int i = 0; i < targetWidth; i++) {
for (int j = 0; j < targetHeight; j++) {
double temp1 = (i + 0.5) / times - 0.5;
int x = (int) Math.floor(temp1);
double u = temp1 - x;
double temp2 = (j + 0.5) / times - 0.5;
int y = (int) Math.floor(temp2);
double v = temp2 - y;
x = x < 0 ? 0 : x;
y = y < 0 ? 0 : y;
int tempX = x + 1 >= imageWidth ? imageWidth - 1 : x + 1;
int tempY = y + 1 >= imageHeight ? imageHeight - 1 : y + 1;
Color p1 = new Color(oriPic.getRGB(x, y));
Color p2 = new Color(oriPic.getRGB(x, tempY));
Color p3 = new Color(oriPic.getRGB(tempX, y));
Color p4 = new Color(oriPic.getRGB(tempX, tempY));
// 得到目标的像素值
double red = (1 - u) * (1 - v) * p1.getRed() + (1 - u) * v * p2.getRed()
+ u * (1 - v) * p3.getRed() + u * v * p4.getRed();
double green = (1 - u) * (1 - v) * p1.getGreen() + (1 - u) * v * p2.getGreen()
+ u * (1 - v) * p3.getGreen() + u * v * p4.getGreen();
double blue = (1 - u) * (1 - v) * p1.getBlue() + (1 - u) * v * p2.getBlue()
+ u * (1 - v) * p3.getBlue() + u * v * p4.getBlue();
Color cl = new Color((int) red, (int) green, (int) blue);
// 将值放入目标图片
newPic.setRGB(i, j, cl.getRGB());
}
}
return newPic;
}
通过这个函数,我们就用双线性插值实现了图象的放大缩小。
写入图象文件
图象的写与读差不多。同样十分简单。
public void writeImage(BufferedImage oriImage, String path) throws IOException{
ImageIO.write(oriImage, "png", new File(path));
}
当然,同样可以自己造轮子。就是读取的逆过程罢了。
private static void wInt(DataOutputStream dos, int i) throws IOException {
dos.write(i);
dos.write(i >> 8);
dos.write(i >> 16);
dos.write(i >> 24);
}
private static void wShort(DataOutputStream dos, short i) throws IOException {
dos.write(i);
dos.write(i >> 8);
}
private BufferedImage toBufferedImage(Image img) {
if (img instanceof BufferedImage) return (BufferedImage)img;
BufferedImage bImg = new BufferedImage(img.getWidth(null), img.getHeight(null), BufferedImage.TYPE_INT_ARGB);
Graphics2D bGr = bImg.createGraphics();
bGr.drawImage(img, 0, 0, null);
bGr.dispose();
return bImg;
}
public Image myWrite(Image var1, String var2) throws IOException {
BufferedImage bImage = toBufferedImage(var1);
int width = bImage.getWidth();
int tripleWidth = width * 3;
int height = bImage.getHeight();
int fullTriWidth = tripleWidth % 4 == 0 ? tripleWidth : 4 * ((tripleWidth / 4) + 1);
int[] px = new int[width * height];
px = bImage.getRGB(0, 0, width, height, px, 0, width);
byte[] rgbs = new byte[tripleWidth * height];
int index = 0;
// r, g, b数组
for (int i = height - 1; i >= 0; i--) {
for (int j = 0; j < width; j++) {
int pixel = px[i * width + j];
rgbs[index++] = (byte) pixel;
rgbs[index++] = (byte) (pixel >>> 8);
rgbs[index++] = (byte) (pixel >>> 16);
}
}
// 补齐扫描行长度为4的倍数
byte[] realArray = new byte[fullTriWidth * height];
for (int i = 0; i < height; i++) {
for (int j = 0; j < fullTriWidth; j++) {
if (j < tripleWidth) {
realArray[fullTriWidth * i + j] = rgbs[i * tripleWidth + j];
} else {
realArray[fullTriWidth * i + j] = 0;
}
}
}
int header = 14;
int info = 40;
int offset = header + info;
int length = width * height * 3 + offset;
short frame = 1;
short deep = 24;
int fbl = 3800;
try {
FileOutputStream out = new FileOutputStream(var2);
DataOutputStream dir = new DataOutputStream(out);
dir.write('B');
dir.write('M'); // #0-1 BM
wInt(dir, length); // #2-5 The size of the BMP file
wInt(dir, 0); // #6-9 Don't need
wInt(dir, offset); // #10-13 the offset
wInt(dir, info); // #14-17 BitmapInfoHeader
wInt(dir, width); // #18-21 width
wInt(dir, height); // #22-25 height
wShort(dir, frame); // #26-27 number of colorful dimension
wShort(dir, deep); // #28-29 the deep of color
wInt(dir, 0); // #30-33 if zip
wInt(dir, 4); // #34-37 size of BMP
wInt(dir, fbl); // #38-41 the resolution ratio of row
wInt(dir, fbl); // #42-45 the resolution ratio of column
wInt(dir, 0); // #46-49 the number of colors
wInt(dir, 0); // #50-53
dir.write(realArray);
dir.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return var1;
}
本教程就到这里,学到新东西之后再更新。
网友评论