美文网首页
Java图象处理

Java图象处理

作者: MarkOut | 来源:发表于2016-09-29 21:46 被阅读0次

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

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

    • 像素是从下到上、从左到右保存的。
    • 每个像素使用一个或者多个字节表示。如果一个图像水平线的字节数不是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;
    }
    

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

    相关文章

      网友评论

          本文标题:Java图象处理

          本文链接:https://www.haomeiwen.com/subject/xpifyttx.html