Java-在不损失质量的情况下调整图像大小

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

我正在使用的图像是这样的:Firwork - original - large

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

resize - using Paint - small

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

resize - using java program - small

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

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

enter image description here

无论如何,可以提高程序输出的质量,因此我不必手动调整所有照片的大小?

先感谢您!

7个解决方案
65 votes

不幸的是,在Java中,没有建议的即用即用的缩放方式可以提供视觉上良好的结果。 除其他外,以下是我推荐的缩放方法:

  • Lanczos3重采样(通常在视觉上更好,但速度较慢)
  • 渐进式降级(通常在视觉上不错,可能很快)
  • 一步扩展以进行扩展(具有RenderHints双三次快速和良好的结果,通常不如Lanczos3更好)

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

视觉比较

这是您使用不同方法/库缩放到RenderHints的图像。 单击图像以获取完整大小:

comparison

comparison zoom

  1. 莫滕·诺贝尔的lib Lanczos3
  2. Thumbnailator双线性渐进缩放
  3. Imgscalr ULTRA_QUALTY(1/7步双三次渐进缩放)
  4. Imgscalr质量(1/2步双三次渐进式缩放)
  5. Morten Nobel的lib双线性渐进缩放
  6. RenderHints双三次插值
  7. RenderHints最近邻插值
  8. Photoshop CS5双三次参考

不幸的是,单个图像不足以判断缩放算法,您应该测试带有锐利边缘的图标,带有文本的照片等。

Lanczos重采样

据说对扩大规模特别是缩小规模很有好处。 不幸的是,当前的JDK中没有本机实现,因此您可以自己实现并使用Morten Nobel的lib之类的lib。 一个使用上述lib的简单示例:

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

该库发布在maven-central上,不幸的是它没有被提及。 不利之处在于,如果没有我所知道的任何高度优化或硬件加速的实现,它通常会非常缓慢。 Nobel的实现比使用RenderHints的1/2步渐进缩放算法慢大约8倍。请在他的博客上详细了解此库。

逐步缩放

在Chris Campbell的博客中提到有关Java缩放的内容时,渐进缩放基本上就是以较小的步骤逐步缩放图像,直到达到最终尺寸为止。 坎贝尔将其描述为将宽度/高度减半直至达到目标。 这样可以产生良好的结果,并且可以与RenderHints一起使用,后者可以进行硬件加速,因此通常在大多数情况下都具有非常好的性能,并且结果可以接受。 主要的缺点是,如果使用Image.getScaledInstance()缩小比例不到一半,则会产生相同的中等结果,因为它只能缩放一次。

这是一个有关其工作原理的简单示例:

progressive scaling

以下库结合了基于RenderHints的渐进式缩放形式:

Thumbnailator v0.4.8

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

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

它与RenderHints的单步缩放速度一样快或稍快,在我的基准测试中平均得分为6.9秒。

imgscalr v4.2

使用渐进式双三次缩放。 在RenderHints设置中,它使用Campbell样式算法将每一步的尺寸减半,而Image.getScaledInstance()则具有更精细的步长,将每个增量的大小减小1/7,这通常会生成较柔和的图像,但最小化了仅使用1次迭代的情况。

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

主要缺点是性能。 RenderHints比其他库慢得多。 甚至Image.getScaledInstance()也比Thumbnailator的实现慢一些。 我的简单基准测试分别得出平均26.2秒和11.1秒的结果。

Morten Nobel的lib v0.8.6

还具有用于所有基本RenderHints(双线性,双三次和最近邻)的逐步缩放的实现

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,但不鼓励您使用它-请在此处阅读完整说明。

patrickf answered 2019-11-19T03:10:43Z
37 votes

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

执行批量调整大小

例如,为了使用Thumbnailator修改示例,您应该能够通过以下代码获得相似的结果:

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

它将继续进行,并获取BufferedImage目录中的所有文件,并一步一步地处理它们,尝试调整它们的大小以适合112 x 75的尺寸,并且它将尝试保留原始图像的长宽比以防止 图像的“扭曲”。

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

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

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

生成高质量的缩略图

就图像质量而言,正如Marco13的回答中所述,克里斯坎贝尔(Chris Campbell)在他的《 Perils of Image.getScaledInstance()》中描述的技术是在Thumbnailator中实现的,从而可以生成高质量的缩略图,而无需进行任何复杂的处理。

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

Thumbnail of image in original question

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

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库的维护者。

coobird answered 2019-11-19T03:12:27Z
16 votes

给定您的输入图像,注释中第一个链接的答案中的方法(对Chris Campbell表示敬意)将产生以下缩略图之一:

enter image description here enter image description here

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

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

为了完整起见,MVCE

(编辑:我提到了Chris Campbell,并通过注释引用了源代码,但在此要更清楚:以下内容基于《 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();
    }    
}
Marco13 answered 2019-11-19T03:13:20Z
6 votes

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

使用Theanother example类具有如下构造函数:

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

所以你可以做(another example):

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

这是输出:

enter image description here

MaMaRo N0 answered 2019-11-19T03:14:05Z
5 votes

我们不应忘记一个十二猴子图书馆

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

用法示例:

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);
Quark answered 2019-11-19T03:14:40Z
2 votes

下面是我自己的渐进扩展实现,没有使用任何外部库。 希望能有所帮助。

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;
}
Maxwell Cheng answered 2019-11-19T03:15:06Z
1 votes

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

这是我用ij-1.49d.jar得到的结果:

Thumbnail when bluring first(sigma=15.0)

使用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。

fabian answered 2019-11-19T03:15:53Z
translate from https://stackoverflow.com:/questions/24745147/java-resize-image-without-losing-quality