调整图片大小而不失去质量

15

我编写了这段代码来按两个因素重设图像大小。它可以工作,但是调整大小后的图像质量非常糟糕!你能帮我吗?

这是代码:

public class ImageTest {

private static final int factor1 = 3;
private static final int factor2 = 4;
public static void main(String [] args){

    JFileChooser cs = new JFileChooser();
    cs.setFileSelectionMode(cs.DIRECTORIES_ONLY);
    int i = cs.showOpenDialog(null);
    if(i==cs.APPROVE_OPTION){
        File f = cs.getSelectedFile();
        File[] ff = f.listFiles();
        for(int j=0;j<ff.length;j++){
            String end = ff[j].getName().substring(ff[j].getName().indexOf(".")+1);
            System.out.println(end);
            try{
                BufferedImage originalImage = ImageIO.read(ff[j]);
                int type = originalImage.getType() == 0? BufferedImage.TYPE_INT_ARGB : originalImage.getType();
                BufferedImage resizeImageJpg = resizeImageWithHint(originalImage, type);
                ImageIO.write(resizeImageJpg, end, new File("pr/"+ff[j].getName()));
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }


}
private static BufferedImage resizeImageWithHint(BufferedImage originalImage, int type){
    int IMG_WIDTH = (originalImage.getWidth()*factor1)/factor2;
    int IMG_HEIGHT = (originalImage.getHeight()*factor1)/factor2;
    BufferedImage resizedImage = new BufferedImage(IMG_WIDTH, IMG_HEIGHT, type);
    Graphics2D g = resizedImage.createGraphics();
    g.drawImage(originalImage, 0, 0, IMG_WIDTH, IMG_HEIGHT, null);
    g.dispose();    
    g.setComposite(AlphaComposite.Src);

    g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
            RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    g.setRenderingHint(RenderingHints.KEY_RENDERING,
            RenderingHints.VALUE_RENDER_QUALITY);
    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);

    return resizedImage;
}   
   }

我在网上看到resizeImageWithHint是在范围内完成的,以避免失去质量.. 但它确实会失去,为什么?你能帮我解决这个问题吗?

9个回答

31

我曾经读过的关于这个主题最好的文章是The Perils of Image.getScaledInstance()(网络档案馆)。

简而言之:你需要使用多个缩放步骤才能得到一个好的图像。以下是文章中给出的帮助方法:

public 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;
}

是的,那篇文章很好,imgscalr和java-image-scaling都引用了它,但是结果还是不同的,我认为java-image-scaling可以应用的滤镜会有所不同。 - stivlo
1
好的文章,真正帮助提高了缩放图像的质量。将'w /= 2'更改为'w /= 1.2'给我带来了更好的质量(执行更多循环,因此质量更好)。 - Jacob van Lingen
我尝试了 w /= 1.2 的建议,但我发现它会使缩小的图像变得更模糊,特别是在更小的尺寸下。我尝试了 1.5,但没有帮助,所以我还是坚持使用 w /= 2。 - MiguelMunoz

11

以下代码可以帮我实现保持宽高比的最高质量调整大小。尝试过几种方法并阅读了其他答案中呈现的几篇文章。花费了两天时间,最终用普通的Java方法得到了最好的结果(也尝试过ImageMagick和java-image-scaling库):

public static boolean resizeUsingJavaAlgo(String source, File dest, int width, int height) throws IOException {
  BufferedImage sourceImage = ImageIO.read(new FileInputStream(source));
  double ratio = (double) sourceImage.getWidth()/sourceImage.getHeight();
  if (width < 1) {
    width = (int) (height * ratio + 0.4);
  } else if (height < 1) {
    height = (int) (width /ratio + 0.4);
  }

  Image scaled = sourceImage.getScaledInstance(width, height, Image.SCALE_AREA_AVERAGING);
  BufferedImage bufferedScaled = new BufferedImage(scaled.getWidth(null), scaled.getHeight(null), BufferedImage.TYPE_INT_RGB);
  Graphics2D g2d = bufferedScaled.createGraphics();
  g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
  g2d.drawImage(scaled, 0, 0, width, height, null);
  dest.createNewFile();
  writeJpeg(bufferedScaled, dest.getCanonicalPath(), 1.0f);
  return true;
}


/**
* Write a JPEG file setting the compression quality.
*
* @param image a BufferedImage to be saved
* @param destFile destination file (absolute or relative path)
* @param quality a float between 0 and 1, where 1 means uncompressed.
* @throws IOException in case of problems writing the file
*/
private static void writeJpeg(BufferedImage image, String destFile, float quality)
      throws IOException {
  ImageWriter writer = null;
  FileImageOutputStream output = null;
  try {
    writer = ImageIO.getImageWritersByFormatName("jpeg").next();
    ImageWriteParam param = writer.getDefaultWriteParam();
    param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
    param.setCompressionQuality(quality);
    output = new FileImageOutputStream(new File(destFile));
    writer.setOutput(output);
    IIOImage iioImage = new IIOImage(image, null, null);
    writer.write(null, iioImage, param);
  } catch (IOException ex) {
    throw ex;
  } finally {
    if (writer != null) {
      writer.dispose();
    }
    if (output != null) {
      output.close();
    }
  }
}

@nfechner发布的代码是对你的一个改进。如果图片很大,质量可能不如你所期望的那样好。正如所述文章所说:“这种质量差异的原因在于所使用的不同滤波算法。如果缩小超过两倍,BILINEAR和BICUBIC算法往往会由于像素从源图像采样的方式而丢失信息。” 通过使用多步渲染方法,调整大小后的图片将更好。 - Jacob van Lingen
我使用了我的眼睛和将图像缩小到约320像素,这在我看来产生了更好的质量。我理解你的意思,然而,我对 @nfechner 算法得到的结果不满意,我也尝试过那个... - Mladen Adamovic

7

了解这个问题是老问题...我尝试了不同的解决方案搜索网络,最终发现使用getScaledInstance()并提供Image.SCALE_SMOOTH作为参数得到了最好的结果。实际上,生成的图像质量确实更好。 我的代码如下:

final int THUMB_SIDE = 140;
try {
    BufferedImage masterImage = ImageIO.read(startingImage);
    BufferedImage thumbImage = new BufferedImage(THUMB_SIDE, THUMB_SIDE, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g2d = thumbImage.createGraphics();
    g2d.drawImage(masterImage.getScaledInstance(THUMB_SIDE, THUMB_SIDE, Image.SCALE_SMOOTH), 0, 0, THUMB_SIDE, THUMB_SIDE, null);
    g2d.dispose();
    String thumb_path = path.substring(0, path.indexOf(".png")) + "_thumb.png";
    ImageIO.write(thumbImage, "png", new File(thumb_path));
} catch (IOException e) {
    e.printStackTrace();
}

它只推荐使用“png”格式吗? - Tiny

3

它只推荐使用“png”格式吗? - Tiny
我不记得了,很抱歉。 - Felipe

2

如果您希望获得真正的高质量,以下答案都无法满足您的需求。请在您的项目中添加thumbailator.jar(从此处下载):

https://code.google.com/p/thumbnailator/

https://code.google.com/p/thumbnailator/wiki/Downloads?tm=2

然后先上传图像(作为文件,不使用Thumbnailator - 它用于创建缩略图,但您当然可以使用它创建大图像),并将其调整为所需的每个尺寸(例如使用Thumbnailator 800x600)。质量会非常好。我花了很长时间搜索这个,这个.jar帮助我实现了我想要的。


嗨,谢谢你的分享!这是一篇旧文章,但我会尝试并记住它以备将来之需。谢谢! - Jayyrus

1

是的,我遇到了同样的问题并解决了它们,请阅读我的问题(答案嵌入在问题中)。我尝试过imgscalrjava-image-scaling库,并发现第二个质量更好。靠近显示器欣赏缩略图示例之间的差异。

尽管我最初认为调整图像大小似乎是一件非常复杂的事情,但你不希望自己去做。例如,我告诉java-image-scaling使用ResampleFilters.getLanczos3Filter()以获得更好的结果。

它还解决了如何以比标准的75更高的质量保存JPG图片的问题,尤其是对于缩略图而言,标准的75会产生糟糕的结果。

我还编写了一个小类,名为MyImage,以帮助完成常见任务,例如从字节数组或文件中读取图像,通过仅指定宽度或高度进行缩放,通过指定边界框进行缩放,通过指定宽度和高度进行缩放并添加白色带以使图像不失真,并将其写入JPG文件。

0

答案:移除提示VALUE_INTERPOLATION_BILINEARVALUE_RENDERING_QUALITY

示例:

public static BufferedImage resizeImage(BufferedImage image, int width, int height) {

    // Temporary image

    BufferedImage tmp = image;

    // Result image

    BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

    // Graphics object

    Graphics2D graphics = (Graphics2D)result.createGraphics();

    // Add rendering hints

    graphics.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
    graphics.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);

    // Draw tmp

    graphics.drawImage(tmp, 0, 0, width, height, null);

    // Dispose of graphics object

    graphics.dispose();

    // Return image result

    return result;
}

注意:由于某些原因,提示VALUE_INTERPOLATION_BILINEARVALUE_RENDERING_QUALITY会使调整大小的图像模糊。


0

所有发布的方法对我都不起作用,我必须缩小二维码的尺寸,但使用上述方法会导致质量较差且扫描器无法工作,如果我在画图中将原始图片调整大小,则扫描器可以正常工作。


0

Image.getScaledInstance()的危险之处中使用的do while循环将会陷入无限循环,如果给定以下值,w = 606; h = 505, targetWidth = 677, targetHeight = 505

这里是一个简化的测试代码,你可以试一下。

public class LoopTest {

    public static void main(String[] args) {
        new LoopTest(true, 606, 505, 677, 505);
    }

    public LoopTest(boolean higherQuality, int w, int h, int targetWidth, int targetHeight) {
        do {
            if (higherQuality && w > targetWidth) {
                w /= 2;
                if (w < targetWidth) {
                    w = targetWidth;
                }
            }

            if (higherQuality && h > targetHeight) {
                h /= 2;
                if (h < targetHeight) {
                    h = targetHeight;
                }
            }
        } while (w != targetWidth || h != targetHeight);        // TODO Auto-generated constructor stub
    }
}

一个快速的解决方法:为循环计数定义一个索引。如果索引大于等于10,则跳出循环。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接