【问题标题】:Java - resize image without losing qualityJava - 在不损失质量的情况下调整图像大小
【发布时间】:2014-07-14 20:15:22
【问题描述】:

我有 10,000 张照片需要调整大小,所以我有一个 Java 程序可以做到这一点。不幸的是,图像质量很差,我无法访问未压缩的图像。

import java.awt.Graphics;
import java.awt.AlphaComposite;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;


import javax.imageio.ImageIO;
/**
 * This class will resize all the images in a given folder
 * @author 
 *
 */
public class JavaImageResizer {

    public static void main(String[] args) throws IOException {

        File folder = new File("/Users/me/Desktop/images/");
        File[] listOfFiles = folder.listFiles();
        System.out.println("Total No of Files:"+listOfFiles.length);
        BufferedImage img = null;
        BufferedImage tempPNG = null;
        BufferedImage tempJPG = null;
        File newFilePNG = null;
        File newFileJPG = null;
        for (int i = 0; i < listOfFiles.length; i++) {
              if (listOfFiles[i].isFile()) {
                System.out.println("File " + listOfFiles[i].getName());
                img = ImageIO.read(new File("/Users/me/Desktop/images/"+listOfFiles[i].getName()));
                tempJPG = resizeImage(img, img.getWidth(), img.getHeight());
                newFileJPG = new File("/Users/me/Desktop/images/"+listOfFiles[i].getName()+"_New");
                ImageIO.write(tempJPG, "jpg", newFileJPG);
              }
        }
        System.out.println("DONE");
    }

    /**
     * This function resize the image file and returns the BufferedImage object that can be saved to file system.
     */
        public static BufferedImage resizeImage(final Image image, int width, int height) {
    int targetw = 0;
    int targeth = 75;

    if (width > height)targetw = 112;
    else targetw = 50;

    do {
        if (width > targetw) {
            width /= 2;
            if (width < targetw) width = targetw;
        }

        if (height > targeth) {
            height /= 2;
            if (height < targeth) height = targeth;
        }
    } while (width != targetw || height != targeth);

    final BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
    final Graphics2D graphics2D = bufferedImage.createGraphics();
    graphics2D.setComposite(AlphaComposite.Src);
    graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY);
    graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
    graphics2D.drawImage(image, 0, 0, width, height, null);
    graphics2D.dispose();

    return bufferedImage;
}

我正在使用的图像是这样的:

这是我在 Microsoft Paint 中手动调整大小:

这是我的程序 [双线性] 的输出:

更新:使用BICUBIC没有显着差异

这是我的程序 [bicubic] 的输出:

有没有办法提高程序输出的质量,这样我就不必手动调整所有照片的大小?

提前谢谢你!

【问题讨论】:

标签: java image image-processing bufferedimage


【解决方案1】:

不幸的是,Java 中没有推荐的开箱即用的缩放可以提供良好的视觉效果。其中,以下是我推荐的缩放方法:

  • Lanczos3 重采样(通常视觉效果更好,但速度较慢)
  • 渐进式缩小(通常视觉效果很好,可能很快)
  • 单步缩放以实现向上缩放(Graphics2d bicubic 快速且效果好,通常不如 Lanczos3)

可以在此答案中找到每种方法的示例。

视觉对比

这是使用不同方法/库缩放到 96x140 的图像。点击图片查看完整尺寸:

  1. Morten Nobel 的 lib Lanczos3
  2. 缩略图双线性渐进式缩放
  3. Imgscalr ULTRA_QUALTY(1/7 步双三次渐进缩放)
  4. Imgscalr 质量(1/2 步双三次渐进缩放)
  5. Morten Nobel 的 lib 双线性渐进式缩放
  6. Graphics2d双三次插值
  7. Graphics2d最近邻插值
  8. Photoshop CS5 双三次作为参考

很遗憾,单张图片不足以判断缩放算法,你应该测试边缘锐利的图标、带文字的照片等。

Lanczos 重采样

据说有利于扩大规模,尤其是缩小规模。不幸的是there is no native implementation in current JDK,所以你要么自己实现它,要么使用像Morten Nobel's lib 这样的库。一个使用上述库的简单示例:

ResampleOp resizeOp = new ResampleOp(dWidth, dHeight);
resizeOp.setFilter(ResampleFilters.getLanczos3Filter());
BufferedImage scaledImage = resizeOp.filter(imageToScale, null);

这个库是published on maven-central,不幸的是没有提到。缺点是它通常很慢,没有我知道的任何高度优化或硬件加速的实现。 Nobel 的实现比 Graphics2d 的 1/2 步渐进式缩放算法慢了大约 8 倍。 Read more about this lib on his blog.

渐进式缩放

在 Java 中的 Chris Campbell's blog about scaling 中提到,渐进式缩放基本上是以较小的步长递增地缩放图像,直到达到最终尺寸。 Campbell 将其描述为在达到目标之前将宽度/高度减半。这会产生很好的结果,并且可以与可以硬件加速的Graphics2D 一起使用,因此在大多数情况下通常具有非常好的性能和可接受的结果。这样做的主要缺点是,如果使用Graphics2D 缩小不到一半,则会提供同样平庸的结果,因为它只缩小了一次。

下面是一个简单的例子说明它是如何工作的:

以下库包含基于Graphics2d 的渐进式缩放形式:

Thumbnailator v0.4.8

如果目标至少是每个维度的一半,则使用渐进式双线性算法,否则使用简单的Graphics2d 双线性缩放和双三次进行放大。

Resizer resizer = DefaultResizerFactory.getInstance().getResizer(
  new Dimension(imageToScale.getWidth(), imageToScale.getHeight()), 
  new Dimension(dWidth, dHeight))
BufferedImage scaledImage = new FixedSizeThumbnailMaker(
  dWidth, dHeight, false, true).resizer(resizer).make(imageToScale);

它与一步缩放一样快或略快,Graphics2d 在我的benchmark 中平均得分为 6.9 秒。

Imgscalr v4.2

使用渐进式双三次缩放。在QUALITY 设置中,它使用坎贝尔风格算法,每一步将尺寸减半,而ULTRA_QUALITY 具有更精细的步长,将每个增量的尺寸减小 1/7,这通常会生成更柔和的图像,但会最小化仅使用 1 次迭代的实例.

BufferedImage scaledImage = Scalr.resize(imageToScale, Scalr.Method.ULTRA_QUALITY, Scalr.Mode.FIT_EXACT, dWidth, dHeight, bufferedImageOpArray);

主要缺点是性能。 ULTRA_QUALITY 比其他库慢得多。甚至QUALITY 比Thumbnailator 的实现要慢一点。我的简单benchmark 分别导致平均 26.2 秒和 11.1 秒。

Morten Nobel's lib v0.8.6

还实现了所有基本Graphics2d(双线性、双三次和最近邻)的渐进式缩放

BufferedImage scaledImage = new MultiStepRescaleOp(dWidth, dHeight, RenderingHints.VALUE_INTERPOLATION_BILINEAR).filter(imageToScale, null);

关于 JDK 扩展方法的一句话

当前缩放图像的 jdk 方法是这样的

scaledImage = new BufferedImage(dWidth, dHeight, imageType);
Graphics2D graphics2D = scaledImage.createGraphics();
graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
graphics2D.drawImage(imageToScale, 0, 0, dWidth, dHeight, null);
graphics2D.dispose();

但无论使用什么插值或其他RenderHints,大多数人都对缩小的结果感到非常失望。另一方面,放大似乎会产生可接受的图像(最好是双三次的)。在以前的 JDK 版本(我们谈论 90 年代 v1.1)中引入了 Image.getScaledInstance(),它通过参数 SCALE_AREA_AVERAGING 提供了良好的视觉效果,但不鼓励使用它 - read the full explanation here

【讨论】:

    【解决方案2】:

    Thumbnailator 是一个库,旨在以简单的方式创建高质量的缩略图,对现有图像进行批量转换是其用例之一。

    执行批量调整大小

    例如,要使用 Thumbnailator 调整您的示例,您应该能够使用以下代码实现类似的结果:

    File folder = new File("/Users/me/Desktop/images/");
    Thumbnails.of(folder.listFiles())
        .size(112, 75)
        .outputFormat("jpg")
        .toFiles(Rename.PREFIX_DOT_THUMBNAIL);
    

    这将继续并获取 images 目录中的所有文件并继续处理它们,尝试调整它们以适应 112 x 75 的尺寸,它会尝试保持宽高比防止图像“变形”的原始图像。

    Thumbnailator 将继续读取所有文件,无论图像类型如何(只要 Java Image IO 支持该格式,Thumbnailator 就会处理它),执行调整大小操作并将缩略图输出为 JPEG 文件,同时添加一个thumbnail. 到文件名的开头。

    以下是在执行上述代码时,如何在缩略图的文件名中使用原始文件名的说明。

    images/fireworks.jpg     ->  images/thumbnail.fireworks.jpg
    images/illustration.png  ->  images/thumbnail.illustration.png
    images/mountains.jpg     ->  images/thumbnail.mountains.jpg
    

    生成高质量的缩略图

    在图像质量方面,如Marco13's answer 所述,Chris Campbell 在其The Perils of Image.getScaledInstance() 中描述的技术在 Thumbnailator 中实现,无需任何复杂处理即可生成高质量的缩略图。

    以下是使用Thumbnailator调整原始问题中显示的烟花图像大小时生成的缩略图:

    上面的图片是用以下代码创建的:

    BufferedImage thumbnail = 
        Thumbnails.of(new URL("http://i.stack.imgur.com/X0aPT.jpg"))
            .height(75)
            .asBufferedImage();
    
    ImageIO.write(thumbnail, "png", new File("24745147.png"));
    

    代码显示它也可以接受 URL 作为输入,并且 Thumbnailator 也能够创建 BufferedImages。


    免责声明:我是Thumbnailator 库的维护者。

    【讨论】:

    • 支持 gif 和动画 webp 了吗?
    【解决方案3】:

    根据您的输入图像,来自 cmets 第一个链接中答案的方法(感谢 Chris Campbell)会生成以下缩略图之一:

    (另一个是您使用 MS Paint 创建的缩略图。很难说其中一个比另一个“更好”...)

    编辑:也指出这一点:原始代码的主要问题是您并没有真正缩放多个步骤中的图像。您只是使用了一个奇怪的循环来“计算”目标大小。关键是您实际上是在多个步骤中执行缩放

    为了完整起见,MVCE

    (编辑:我提到了 Chris Campbell 并通过 cmets 引用了来源,但在这里更清楚一点:以下内容基于文章 The Perils of Image.getScaledInstance()

    import java.awt.Graphics2D;
    import java.awt.RenderingHints;
    import java.awt.Transparency;
    import java.awt.image.BufferedImage;
    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.OutputStream;
    import java.util.Iterator;
    
    import javax.imageio.IIOImage;
    import javax.imageio.ImageIO;
    import javax.imageio.ImageWriteParam;
    import javax.imageio.ImageWriter;
    import javax.imageio.stream.ImageOutputStream;
    import javax.imageio.stream.MemoryCacheImageOutputStream;
    
    public class ResizeQuality
    {
        public static void main(String[] args) throws IOException
        {
            BufferedImage image = ImageIO.read(new File("X0aPT.jpg"));
            BufferedImage scaled = getScaledInstance(
                image, 51, 75, RenderingHints.VALUE_INTERPOLATION_BILINEAR, true);
            writeJPG(scaled, new FileOutputStream("X0aPT_tn.jpg"), 0.85f);
        }
    
        public static BufferedImage getScaledInstance(
            BufferedImage img, int targetWidth,
            int targetHeight, Object hint, 
            boolean higherQuality)
        {
            int type =
                (img.getTransparency() == Transparency.OPAQUE)
                ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
            BufferedImage ret = (BufferedImage) img;
            int w, h;
            if (higherQuality)
            {
                // Use multi-step technique: start with original size, then
                // scale down in multiple passes with drawImage()
                // until the target size is reached
                w = img.getWidth();
                h = img.getHeight();
            }
            else
            {
                // Use one-step technique: scale directly from original
                // size to target size with a single drawImage() call
                w = targetWidth;
                h = targetHeight;
            }
    
            do
            {
                if (higherQuality && w > targetWidth)
                {
                    w /= 2;
                    if (w < targetWidth)
                    {
                        w = targetWidth;
                    }
                }
    
                if (higherQuality && h > targetHeight)
                {
                    h /= 2;
                    if (h < targetHeight)
                    {
                        h = targetHeight;
                    }
                }
    
                BufferedImage tmp = new BufferedImage(w, h, type);
                Graphics2D g2 = tmp.createGraphics();
                g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);
                g2.drawImage(ret, 0, 0, w, h, null);
                g2.dispose();
    
                ret = tmp;
            } while (w != targetWidth || h != targetHeight);
    
            return ret;
        }
    
        public static void writeJPG(
            BufferedImage bufferedImage,
            OutputStream outputStream,
            float quality) throws IOException
        {
            Iterator<ImageWriter> iterator =
                ImageIO.getImageWritersByFormatName("jpg");
            ImageWriter imageWriter = iterator.next();
            ImageWriteParam imageWriteParam = imageWriter.getDefaultWriteParam();
            imageWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
            imageWriteParam.setCompressionQuality(quality);
            ImageOutputStream imageOutputStream =
                new MemoryCacheImageOutputStream(outputStream);
            imageWriter.setOutput(imageOutputStream);
            IIOImage iioimage = new IIOImage(bufferedImage, null, null);
            imageWriter.write(null, iioimage, imageWriteParam);
            imageOutputStream.flush();
        }    
    }
    

    【讨论】:

      【解决方案4】:

      经过几天的研究,我更喜欢 javaxt。

      使用javaxt.io.Image 类具有如下构造函数:

      public Image(java.awt.image.BufferedImage bufferedImage)
      

      所以你可以这样做(another example):

      javaxt.io.Image image = new javaxt.io.Image(bufferedImage);
      image.setWidth(50);
      image.setOutputQuality(1);
      

      这是输出:

      【讨论】:

      • 为什么这被否决了? JavaXT 使用双线性插值,调整大小后的输出在视觉上与您从 Photoshop 中得到的相同。我还建议在调整图像大小后致电Image.sharpen()
      • 更正,JavaXT 在缩放图像时使用区域平均,在倾斜图像时使用双线性插值。
      【解决方案5】:

      我们不应该忘记TwelveMonkeys Library

      它包含一个非常令人印象深刻的过滤器集合。

      使用示例:

      BufferedImage input = ...; // Image to resample
      int width, height = ...; // new width/height
      
      BufferedImageOp resampler = new ResampleOp(width, height, ResampleOp.FILTER_LANCZOS);
      BufferedImage output = resampler.filter(input, null);
      

      【讨论】:

        【解决方案6】:

        以下是我自己的渐进式缩放实现,没有使用任何外部库。希望对您有所帮助。

        private static BufferedImage progressiveScaling(BufferedImage before, Integer longestSideLength) {
            if (before != null) {
                Integer w = before.getWidth();
                Integer h = before.getHeight();
        
                Double ratio = h > w ? longestSideLength.doubleValue() / h : longestSideLength.doubleValue() / w;
        
                //Multi Step Rescale operation
                //This technique is describen in Chris Campbell’s blog The Perils of Image.getScaledInstance(). As Chris mentions, when downscaling to something less than factor 0.5, you get the best result by doing multiple downscaling with a minimum factor of 0.5 (in other words: each scaling operation should scale to maximum half the size).
                while (ratio < 0.5) {
                    BufferedImage tmp = scale(before, 0.5);
                    before = tmp;
                    w = before.getWidth();
                    h = before.getHeight();
                    ratio = h > w ? longestSideLength.doubleValue() / h : longestSideLength.doubleValue() / w;
                }
                BufferedImage after = scale(before, ratio);
                return after;
            }
            return null;
        }
        
        private static BufferedImage scale(BufferedImage imageToScale, Double ratio) {
            Integer dWidth = ((Double) (imageToScale.getWidth() * ratio)).intValue();
            Integer dHeight = ((Double) (imageToScale.getHeight() * ratio)).intValue();
            BufferedImage scaledImage = new BufferedImage(dWidth, dHeight, BufferedImage.TYPE_INT_RGB);
            Graphics2D graphics2D = scaledImage.createGraphics();
            graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            graphics2D.drawImage(imageToScale, 0, 0, dWidth, dHeight, null);
            graphics2D.dispose();
            return scaledImage;
        }
        

        【讨论】:

        • 哇!通过使用您的代码,我从缩放图像中难以辨认的文本变成了相当不错的结果。一千个谢谢。
        【解决方案7】:

        如果您在调整大小之前应用高斯模糊,结果似乎更好(比您的程序的结果):

        这是我得到的结果,sigma * (scale factor) = 0.3:

        使用ImageJ 执行此操作的代码非常短:

        import ij.IJ;
        import ij.ImagePlus;
        import ij.io.Opener;
        import ij.process.ImageProcessor;
        
        public class Resizer {
        
            public static void main(String[] args) {
                processPicture("X0aPT.jpg", "output.jpg", 0.0198, ImageProcessor.NONE, 0.3);
            }
        
            public static void processPicture(String inputFile, String outputFilePath, double scaleFactor, int interpolationMethod, double sigmaFactor) {
                Opener opener = new Opener();
                ImageProcessor ip = opener.openImage(inputFile).getProcessor();
                ip.blurGaussian(sigmaFactor / scaleFactor);
                ip.setInterpolationMethod(interpolationMethod);
                ImageProcessor outputProcessor = ip.resize((int)(ip.getWidth() * scaleFactor), (int)(ip.getHeight()*scaleFactor));
                IJ.saveAs(new ImagePlus("", outputProcessor), outputFilePath.substring(outputFilePath.lastIndexOf('.')+1), outputFilePath);
            }
        
        }
        

        顺便说一句:您只需要ij-1.49d.jar(或其他版本的等效项);无需安装 ImageJ。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-12-18
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多