Java—缩放图像的正确方法
上下文 我正在回顾一些在服务器端应用程序中用于缩放图像的遗留Java代码。直到最近,它主要用于分辨率为1024x768或以下的输入图像,在这种情况下,它似乎工作得很好(或至少足够好) 然而,现在,许多正在处理的图像的分辨率在2592x1936(8MP)到6000x4000(24MP)之间。这导致服务器上频繁报告Java—缩放图像的正确方法,java,image,memory,garbage-collection,javax.imageio,Java,Image,Memory,Garbage Collection,Javax.imageio,上下文 我正在回顾一些在服务器端应用程序中用于缩放图像的遗留Java代码。直到最近,它主要用于分辨率为1024x768或以下的输入图像,在这种情况下,它似乎工作得很好(或至少足够好) 然而,现在,许多正在处理的图像的分辨率在2592x1936(8MP)到6000x4000(24MP)之间。这导致服务器上频繁报告OutOfMemoryError,我认为这是由图像处理代码引起的 测试用例 我编写了一个简单的测试应用程序,它以与服务器相同的方式调用图像处理代码(有一个警告;testcase是单线程的,
OutOfMemoryError
,我认为这是由图像处理代码引起的
测试用例
我编写了一个简单的测试应用程序,它以与服务器相同的方式调用图像处理代码(有一个警告;testcase是单线程的,而服务器环境显然不是):
此外,我发现,如果我开始注释代码,直到剩下的唯一一行是:
buffereImage image=ImageIO.read(上传文件)代码>
…就最终的记忆计数而言,结果基本上还是一样的
结果
下面是每次运行的最终输出(来自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本质上不适合这个用例)
首先,尽管你在这个问题上付出了很多努力,但我不确定它是否适合。也许代码审查是发布它的更好地方?第1项和第3项都是典型的“视情况而定”类问题,需要进一步的背景知识,并且可能会产生自以为是的答案。例如,我可以以牺牲图像质量为代价,以非常少的内存超快速缩放高分辨率图像。但我不知道你们的质量要求是什么。对于第3项,在我看来,对你来说“更好”的是,很大程度上取决于你团队的专业知识。@haraldK-即使我缩减了代码,只剩下BuffereImage=ImageIO.read(uploadedFile)代码>,结果基本相同。因此,我认为这个问题不是关于代码审查,而是关于“BuffereImage
/ImageIO因其内部如何管理内存而从根本上不可扩展吗?”。我倾向于回答“是的”。根据我所看到的,使用javax.image
包下的类,您无法以极快的速度和很少的内存完成任何事情。ImageIO
API使用ImageReader
子类有许多更“细粒度”的方法来控制图像的读取方式。您不需要使用ImageIO.read(..)
读取整个图像(顺便说一下,您的旧代码会执行两次)。您可以读取二次采样的图像,这将比读取整个图像更快,并且使用更少的内存。我知道人们正在用Java编写图像服务器,所以我认为“根本无法扩展”的说法是没有根据的。:-)PS:可能对于您的用例(以及您的经验/直觉),衍生到外部流程才是正确的做法。我知道人们也成功地做到了这一点(通常使用ImageMagick/GraphicsMagick)。它只是一个完全不同的野兽,有它自己的一套怪癖和问题。我终于找到时间运行你的测试应用程序,通过在末尾添加一个System.gc()
并再次打印内存统计数据,我得到了[完整的gc 741M->540K(7168K),0.1173000秒]1000次迭代后的Memstats:可用内存:6.4516754150390625 MB总内存:7.0 MB最大内存:4096.0 MB
。也就是说,所有内存都被回收并返回给主机操作系统。因此,除了遗留代码中一些明显的低效之外,我不认为有理由在这里抛弃Java。:-)
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;
}
#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