Java图象处理

2016-09-29  本文已影响0人  MarkOut

近接触了一下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类

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;
    }

代码还是很简单的。其实就是从位图信息里面,把图的长宽读出来,然后再把信息后面的值与像素一一对应。这里要解决的是两个问题:

当然,最后我们用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;
}

本教程就到这里,学到新东西之后再更新。

上一篇下一篇

猜你喜欢

热点阅读