如何扩展 BufferedImage

58

根据Javadocs的指导,我尝试缩放BufferedImage,但没有成功,这是我的代码:

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

我不明白为什么它不能正常工作,有帮助吗?


1
一个优秀的教程:http://www.glyphic.com/transform/applet/1intro.html - Marcus Becker
截至目前,最流行的答案是错误的答案。它将图像放大,但返回一个大小相同的图像,其中3/4的图像丢失了。这是trashgod给出的答案。它很接近,但有一个小错误。 - MiguelMunoz
谢谢,@MiguelMunoz。随着我们收到更多的反馈,我可以修改答案。 - Thiago Diniz
8个回答

80

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

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

这个示例展示的是重采样,而不是裁剪;这个答案解决了问题;这里还有一些相关的例子可以查看:点击此处


2
当你有像 after = ... 这样的语句时,是否真的需要为 after 分配所有内存? - Martijn Courteaux
3
这句话的意思是:取决于你在filter()中想要使用哪种ColorModel。它返回一个引用,所以没有额外的内存占用。 - trashgod
1
是的,比例参数只是 xy 的新旧比率;保持它们相等以保留纵横比。 - trashgod
1
我尚未尝试这段代码,但不确定它是否有效。你正在创建一个名为“after”的BufferedImage,它具有与原始图像相同的尺寸。你可能需要缩放新图像的尺寸。但是,我对此并不确定。scaleOp.filter()可能会为你处理这个问题。 - MiguelMunoz
9
我刚刚测试了一下。就像我预期的那样,after的尺寸没有变化,只是原图的左上角四分之一。解决方法是在创建after时将宽度和高度乘以比例尺。 - MiguelMunoz
显示剩余7条评论

39

不幸的是,如果不出问题的话,getScaledInstance()的性能非常差。

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

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的尺寸,需要正确计算。在进行因子缩放时:

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

编辑: 找到了一篇文章,介绍了性能问题: The Perils of Image.getScaledInstance()


1
我认为现在getScaledInstance()更快了,至少如果你有一个不错的显卡,这要归功于优化的Java2D渲染管道。 - ceklock
1
请参阅**此处**,了解RenderingHints.KEY_INTERPOLATION可能的其他值。 - rustyx
这对我很有帮助,而且没有改变库。 - Maia
这种方法的一个重要问题是:如果original.getType() == TYPE_CUSTOM,则无法使用该值构造BufferedImage,因此无法正常工作。 - Joel Aelwyn

14

使用imgscalr——Java图像缩放库:

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

https://github.com/rkalla/imgscalr


1
同意,这是最佳解决方案,避免了使用仿射变换和其他各种方法时的透明度、错误翻译、错误颜色等所有问题。 - zyamys
1
太棒了!这是我在这个帖子上看到的第一个解决方案,它满足了我的需求。 - Shadoninja
1
这个解决方案对我很有效,甚至都没去尝试其他的。 - Ali Farooq

13

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

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

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

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()快。双立方插值最慢,但在扩展图像时效果最好。(对于性能,它应该只与scaleBilinear()scaleNearest()进行比较。)双线性似乎更适合缩小图像,尽管这很难说。 NearestNeighbor是最快的,但效果最差。双线性似乎是速度和质量之间的最佳折衷方案。questionable()方法中调用的Image.getScaledInstance()表现非常差,并返回与NearestNeighbor相同的低质量。 (仅给出扩展图像的性能数字。)

enter image description here

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

你能否推荐一些在缩小时能够得到平滑结果的工具呢?getScaledInstanceImage.SCALE_SMOOTH可以实现,但众所周知它非常缓慢。我尝试过的其他所有方法(包括AffineTransformOp和应用了任何组合的RenderingHints进行变换绘制)都会给我带来锯齿边缘。 - user2543253
好的,我要建议一些东西,但我不知道它是否能很好地工作,或者是否会更快。尝试进行两阶段缩放,其中第一阶段是整数缩放。因此,如果您需要按1/3.4的比例缩放,请取倒数(3.4)并将其截断为整数。这给了我们3。因此,在第一阶段中按3的比例缩小。然后在第二阶段中完成剩余部分。(这只是一个有教养的猜测,但这是我要尝试的第一件事。)您还可以寻找一些具有良好缩放方法的第三方库。(本页上已经提到了一些。) - MiguelMunoz

10

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

然而,要理解grph.scale(2.0, 2.0)的工作原理,您可以查看这段代码:

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


1
我尝试了这段代码,但是得到的结果和展示的不同。我得到的结果有严重的走样。如果您在浏览器中放大第一张图像,直到与第二张大小相同,您就可以更好地了解此代码生成的内容。(我尝试将生成的图像放入该评论中,但失败了。我猜测评论中不允许图片。) - MiguelMunoz
你可以尝试使用grph.setRenderingHint(RenderingHints.KEY_INTERPOLATION, interpolation);,其中interpolationRenderingHints.VALUE_INTERPOLATION_...,例如VALUE_INTERPOLATION_BICUBIC吗? - aioobe

9
如果您不介意使用外部库,Thumbnailator 可以对 BufferedImage 进行缩放。
Thumbnailator 会处理 Java 2D 处理(例如使用 Graphics2D 并设置适当的 渲染提示),因此可以使用简单流畅的 API 调用来调整图像大小:
BufferedImage image = Thumbnails.of(originalImage).scale(2.0).asBufferedImage();

尽管Thumbnailator的名字暗示它主要用于缩小图像,但它也可以通过双线性插值在默认的调整大小实现中扩大图像。

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


1
这是一个非常优秀的库!与 Graphics2D 相比,缩略图仅仅是惊人的。 - Artem
1
很棒的库!与 Kotlin 很搭配。而且似乎比其他一些选项更为现代化。 - ATutorMe

3

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


我尝试了这种方法,但是getScaledInstance返回的是ToolkitImage,而对于我的目的来说它并不适合我。谢谢。 - Thiago Diniz
2
您可以通过将其光栅复制到新的 BufferedImage 中来将其转换为 BufferedImage。搜索“将图像转换为 BufferedImage”。 - Bozho

0
trashgod的最佳答案有一些小问题,所以我将发布一个改进版本(由于声望点数不足,我无法将其作为评论发表在他的解决方案下)。
BufferedImage before = getBufferedImage(encoded);
int w = before.getWidth();
int h = before.getHeight();
double scaleFactor = 2.0;
BufferedImage after = new BufferedImage((int)(w * scaleFactor), (int)(h * scaleFactor), before.getType());
AffineTransform at = new AffineTransform();
at.scale(scaleFactor, scaleFactor);
AffineTransformOp scaleOp = 
    new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
after = scaleOp.filter(before, after);

改进:
  • “after”图像具有适合缩放图像的正确尺寸
  • “after”图像与编码图像具有相同的图像类型(否则转换将失败)

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