关于Java:如何缩放BufferedImage

How to scale a BufferedImage

按照javadocs,我尝试缩放BufferedImage失败,这是我的代码:

1
2
3
4
BufferedImage image = MatrixToImageWriter.getBufferedImage(encoded);
Graphics2D grph = image.createGraphics();
grph.scale(2.0, 2.0);
grph.dispose();

我不明白为什么它不起作用,有帮助吗?


AffineTransformOp提供了选择插值类型的额外灵活性。

1
2
3
4
5
6
7
8
9
BufferedImage before = getBufferedImage(encoded);
int w = before.getWidth();
int h = before.getHeight();
BufferedImage after = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
AffineTransform at = new AffineTransform();
at.scale(2.0, 2.0);
AffineTransformOp scaleOp =
   new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
after = scaleOp.filter(before, after);

显示的片段说明了重采样,而非裁剪;这个相关的答案解决了这个问题;这里检查一些相关的例子。


不幸的是,即使没有问题,getScaledInstance()的性能也很差。

另一种方法是创建一个新的BufferedImage,并在新图像上绘制原始图像的缩放版本。

1
2
3
4
5
6
7
BufferedImage resized = new BufferedImage(newWidth, newHeight, original.getType());
Graphics2D g = resized.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
    RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(original, 0, 0, newWidth, newHeight, 0, 0, original.getWidth(),
    original.getHeight(), null);
g.dispose();

newWidth,newHeight指示新的BufferedImage大小,必须正确计算。
在因子缩放的情况下:

1
2
int newWidth = new Double(original.getWidth() * widthFactor).intValue();
int newHeight = new Double(original.getHeight() * heightFactor).intValue();

编辑:找到了说明性能问题的文章:Image.getScaledInstance()的危险


正如@Bozho所说,您可能要使用getScaledInstance

要了解grph.scale(2.0, 2.0)的工作原理,您可以看一下以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;

import javax.imageio.ImageIO;
import javax.swing.ImageIcon;


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

        final int SCALE = 2;

        Image img = new ImageIcon("duke.png").getImage();

        BufferedImage bi = new BufferedImage(SCALE * img.getWidth(null),
                                             SCALE * img.getHeight(null),
                                             BufferedImage.TYPE_INT_ARGB);

        Graphics2D grph = (Graphics2D) bi.getGraphics();
        grph.scale(SCALE, SCALE);

        // everything drawn with grph from now on will get scaled.

        grph.drawImage(img, 0, 0, null);
        grph.dispose();

        ImageIO.write(bi,"png", new File("duke_double_size.png"));
    }
}

给定duke.png:
enter image description here

它产生duke_double_size.png:
enter image description here


使用imgscalr – Java图像缩放库:

1
2
BufferedImage image =
     Scalr.resize(originalImage, Scalr.Method.BALANCED, newWidth, newHeight);

这对我来说足够快。


如果您不介意使用外部库,则Thumbnailator可以缩放BufferedImage s。

Thumbnailator将负责处理Java 2D处理(例如使用Graphics2D并设置适当的呈现提示),以便可以使用简单的流利API调用来调整图像大小:

1
BufferedImage image = Thumbnails.of(originalImage).scale(2.0).asBufferedImage();

尽管顾名思义,Thumbnailator旨在缩小图像,但在默认缩放器实现中使用双线性插值,它也会做得不错,可以放大图像。

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


要缩放图像,您需要创建一个新图像并将其绘制。一种方法是使用AffineTransferOpfilter()方法,如此处建议的那样。这使您可以选择插值技术。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static BufferedImage scale1(BufferedImage before, double scale) {
    int w = before.getWidth();
    int h = before.getHeight();
    // Create a new image of the proper size
    int w2 = (int) (w * scale);
    int h2 = (int) (h * scale);
    BufferedImage after = new BufferedImage(w2, h2, BufferedImage.TYPE_INT_ARGB);
    AffineTransform scaleInstance = AffineTransform.getScaleInstance(scale, scale);
    AffineTransformOp scaleOp
        = new AffineTransformOp(scaleInstance, AffineTransformOp.TYPE_BILINEAR);

    scaleOp.filter(before, after);
    return after;
}

另一种方法是使用缩放操作进行缩放,将原始图像简单地绘制到新图像中。此方法非常相似,但是它也说明了如何在最终图像中绘制所需的任何内容。 (我在空白处放置了两种方法开始不同的地方。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static BufferedImage scale2(BufferedImage before, double scale) {
    int w = before.getWidth();
    int h = before.getHeight();
    // Create a new image of the proper size
    int w2 = (int) (w * scale);
    int h2 = (int) (h * scale);
    BufferedImage after = new BufferedImage(w2, h2, BufferedImage.TYPE_INT_ARGB);
    AffineTransform scaleInstance = AffineTransform.getScaleInstance(scale, scale);
    AffineTransformOp scaleOp
        = new AffineTransformOp(scaleInstance, AffineTransformOp.TYPE_BILINEAR);

    Graphics2D g2 = (Graphics2D) after.getGraphics();
    // Here, you may draw anything you want into the new image, but we're
    // drawing a scaled version of the original image.
    g2.drawImage(before, scaleOp, 0, 0);
    g2.dispose();
    return after;
}

附录:结果

为了说明差异,我比较了以下五种方法的结果。这是结果的样子,随性能数据一起向上和向下缩放。 (每次运行的性能各不相同,因此仅将这些数字作为大致指导。)顶部图像为原始图像。我将其缩放为两倍大小和一半大小。

如您所见,scaleBilinear()中使用的AffineTransformOp.filter()scale2()Graphics2D.drawImage()的标准绘图方法快。同样,BiCubic插值最慢,但是在扩展图像时会提供最佳结果。 (出于性能考虑,只能将其与scaleBilinear()scaleNearest().进行比较。)双线性似乎更适合缩小图像,尽管这是一个艰难的选择。而NearestNeighbor最快,结果最差。双线性似乎是速度和质量之间的最佳折衷。在questionable()方法中调用的Image.getScaledInstance()表现很差,并且返回的质量与NearestNeighbor相同。 (性能编号仅用于扩展图像。)

enter image description here

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public static BufferedImage scaleBilinear(BufferedImage before, double scale) {
    final int interpolation = AffineTransformOp.TYPE_BILINEAR;
    return scale(before, scale, interpolation);
}

public static BufferedImage scaleBicubic(BufferedImage before, double scale) {
    final int interpolation = AffineTransformOp.TYPE_BICUBIC;
    return scale(before, scale, interpolation);
}

public static BufferedImage scaleNearest(BufferedImage before, double scale) {
    final int interpolation = AffineTransformOp.TYPE_NEAREST_NEIGHBOR;
    return scale(before, scale, interpolation);
}

@NotNull
private static
BufferedImage scale(final BufferedImage before, final double scale, final int type) {
    int w = before.getWidth();
    int h = before.getHeight();
    int w2 = (int) (w * scale);
    int h2 = (int) (h * scale);
    BufferedImage after = new BufferedImage(w2, h2, before.getType());
    AffineTransform scaleInstance = AffineTransform.getScaleInstance(scale, scale);
    AffineTransformOp scaleOp = new AffineTransformOp(scaleInstance, type);
    scaleOp.filter(before, after);
    return after;
}

/**
 * This is a more generic solution. It produces the same result, but it shows how you
 * can draw anything you want into the newly created image. It's slower
 * than scaleBilinear().
 * @param before The original image
 * @param scale The scale factor
 * @return A scaled version of the original image
 */

private static BufferedImage scale2(BufferedImage before, double scale) {
    int w = before.getWidth();
    int h = before.getHeight();
    // Create a new image of the proper size
    int w2 = (int) (w * scale);
    int h2 = (int) (h * scale);
    BufferedImage after = new BufferedImage(w2, h2, before.getType());
    AffineTransform scaleInstance = AffineTransform.getScaleInstance(scale, scale);
    AffineTransformOp scaleOp
            = new AffineTransformOp(scaleInstance, AffineTransformOp.TYPE_BILINEAR);

    Graphics2D g2 = (Graphics2D) after.getGraphics();
    // Here, you may draw anything you want into the new image, but we're just drawing
    // a scaled version of the original image. This is slower than
    // calling scaleOp.filter().
    g2.drawImage(before, scaleOp, 0, 0);
    g2.dispose();
    return after;
}

/**
 * I call this one"questionable" because it uses the questionable getScaledImage()
 * method. This method is no longer favored because it's slow, as my tests confirm.
 * @param before The original image
 * @param scale The scale factor
 * @return The scaled image.
 */

private static Image questionable(final BufferedImage before, double scale) {
    int w2 = (int) (before.getWidth() * scale);
    int h2 = (int) (before.getHeight() * scale);
    return before.getScaledInstance(w2, h2, Image.SCALE_FAST);
}


scale(..)的工作方式略有不同。您可以使用bufferedImage.getScaledInstance(..)