Java - 缩放图像的正确方法

时间:2016-02-04 04:13:00

标签: java image memory garbage-collection javax.imageio

上下文

我正在审查服务器端应用程序中用于扩展图像的一些遗留Java代码。直到最近它主要用于具有1024x768或更低分辨率的输入图像,并且在这种情况下它似乎运行良好(或者至少足够好)。

然而,现在,许多被操作的图像的分辨率范围从2592x1936(8MP)到6000x4000(24MP)。这导致在服务器上频繁OutOfMemoryError报告,我认为这是由图像处理代码引起的。

测试用例

我已经整理了一个简单的测试应用程序,它以与服务器相同的方式调用图像处理代码(有一点需要注意;测试用例是单线程的,而服务器环境显然不是这样):

public static void main(String[] args) throws Exception {
    File file = new File("test_fullres.png");     //8MP test image, PNG format
    for (int index = 0; index < 1000; index++) {
        File output = new File("target/images/img_" + index + ".jpg");
        verifyImageIsValidAndScale(file, output, 1024, 768, true, 80);

        if (index % 10 == 0) {
            String memStats = "Memstats after " + index + " iterations:";
            memStats += "\tFree Memory:  " + (Runtime.getRuntime().freeMemory() / 1024.0 / 1024.0) + " MBytes\n";
            memStats += "\tTotal Memory:  " + (Runtime.getRuntime().totalMemory() / 1024.0 / 1024.0) + " MBytes\n";
            memStats += "\tMax Memory:  " + (Runtime.getRuntime().maxMemory() / 1024.0 / 1024.0) + " MBytes\n";

            System.out.println(memStats);
        }
    }
}

非常基本,只需循环1000次并在每次迭代时缩放输入图像,每10次迭代输出一些内存统计信息。我还设置了以下JVM标志:

-verbose:gc -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:GCPauseIntervalMillis=3000

对于控件,我使用1024x768 JPEG作为源输入。对于实际测试我输入的是8MP PNG图像。

代码

其他测试代码是:

public static boolean verifyImageIsValidAndScale(File in, File out, int width, int height, boolean aspectFit, int quality){
    boolean valid = verifyImageIsValid(in);
    aspectFitImage(in, out, width, height, quality);

    return valid;
}

public static boolean verifyImageIsValid(File file){
    InputStream stream = null;
    try {
        String path = file.getAbsolutePath();
        stream = new FileInputStream(path);

        byte[] buffer = new byte[11];

        int numRead = stream.read(buffer);

        if(numRead > 3 && buffer[1] == 'P' && buffer[2] == 'N' && buffer[3] == 'G') {
            stream.close();
            buffer = null;
            LOG.debug("Valid PNG");
            return ImageIO.read(new File(path)) != null;    //PNG
        } else if (numRead > 10 && ((buffer[6] == 'J' && buffer[7] == 'F' && buffer[8] == 'I' && buffer[9] == 'F' && buffer[10] == 0) || 
                (buffer[6] == 'E' && buffer[7] == 'x' && buffer[8] == 'i' && buffer[9] == 'f' && buffer[10] == 0))){
            stream.close();
            buffer = null;
            LOG.debug("Valid JPEG");
            return ImageIO.read(new File(path)) != null;    //JPEG
        }
        buffer = null;
    } catch (Exception e) {
        return false;
    }
    finally {
        try {
            stream.close();
        }
        catch (Exception ignored) {}
    }
    LOG.debug("Invalid Image");
    return false;
}

public static Image aspectFitImage(File uploadedFile, File outputFile, int maxWidth, int maxHeight, int quality) {
    if (quality < 0) {
        quality = 0;
    }

    Image scaledImage = null;
    try {
        BufferedImage image = ImageIO.read(uploadedFile);
        double scaleX = image.getWidth() > maxWidth ? maxWidth / (double)image.getWidth() : 1.0;
        double scaleY = image.getHeight() > maxHeight ? maxHeight / (double)image.getHeight() : 1.0;
        double useScale = scaleX < scaleY ? scaleX : scaleY;

        scaledImage = image.getScaledInstance((int)(image.getWidth() * useScale), (int)(image.getHeight() * useScale), Image.SCALE_SMOOTH);

        //XXX:  getting bizarre results where overwriting a pre-existing output file doesn't properly overwrite the existing file; explicitly deleting the output file when it already exists seems to solve the issue
        if (outputFile.exists()) {
            outputFile.delete();
        }

        ImageOutputStream imageOut = ImageIO.createImageOutputStream(outputFile);
        ImageWriter writer = ImageIO.getImageWritersByFormatName("jpeg").next();

        ImageWriteParam options = writer.getDefaultWriteParam();
        options.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
        options.setCompressionQuality(quality / 100.0f);

        PixelGrabber pg = new PixelGrabber(scaledImage, 0, 0, -1, -1, true);
        pg.grabPixels();

        DataBuffer buffer = new DataBufferInt((int[]) pg.getPixels(), pg.getWidth() * pg.getHeight());
        WritableRaster raster = Raster.createPackedRaster(buffer, pg.getWidth(), pg.getHeight(), pg.getWidth(), RGB_MASKS, null);
        BufferedImage bi = new BufferedImage(RGB_OPAQUE, raster, false, null);

        writer.setOutput(imageOut);
        writer.write(null, new IIOImage(bi, null, null), options);
        imageOut.close();
        writer.dispose();
    }
    catch (Exception e) {
        uploadedFile.delete();  
        return null;
    }

    return scaledImage;
}

此外,我发现如果我开始评论代码,直到剩下的唯一非常重要的行是:

BufferedImage image = ImageIO.read(uploadedFile);

......就最终记忆结果而言,结果仍然基本相同。

结果

下面是每次运行的最终输出,来自GC日志和内置内存日志记录语句:

#Source @ 1024x768 (control)
[GC pause (young) 1333M->874M(1612M), 0.0016051 secs]
Memstats after 990 iterations:  Free Memory:  505.5521469116211 MBytes
    Total Memory:  1612.0 MBytes
    Max Memory:  6144.0 MBytes


#Source @ 2592x1936 (test):
[GC pause (young) 2161M->1610M(2632M), 0.0025038 secs]
Memstats after 990 iterations:  Free Memory:  425.37239837646484 MBytes
    Total Memory:  2632.0 MBytes
    Max Memory:  6144.0 MBytes

两者似乎都应该高于。而这里没有捕获的是,在测试运行期间,垃圾收集器通常会一次回收高达1GB的内存。

也许值得注意的是,随着运行的进行,“总内存”值逐渐变得越来越高,好像垃圾收集器无法回收所有正在搅拌的内存。那么也许这意味着内存泄漏?

平均而言,垃圾收集器似乎每2-3次循环迭代触发一次。在任何情况下,由于我在单个线程上连续缩放图像,因此流失量似乎相当不合理。这不应该是烧掉千兆字节和千兆字节的内存。即使在幕后,Java正在将每个图像解压缩到32位位图,我也不希望内存使用率能够像观察到的那样快速攀升。

问题

  • 在Java中缩放高分辨率图像的正确/最具内存效率的方法是什么?
  • 遗留代码中是否存在明显的错误,或者我应该将其废弃并重写?
  • 我是否会更好地产生一个外部进程来执行图像处理(例如,如果Java由于某种原因本身不适合这个用例)?

0 个答案:

没有答案