如何使用Java获取图像的高度和宽度?

131

除了使用ImageIO.read来获取图像的高度和宽度,还有其他方法吗?因为我遇到了一个会锁住线程的问题。

at com.sun.medialib.codec.jpeg.Decoder.njpeg_decode(Native Method)      
at com.sun.medialib.codec.jpeg.Decoder.decode(Decoder.java:87)      
at com.sun.media.imageioimpl.plugins.jpeg.CLibJPEGImageReader.decode(CLibJPEGImageReader.java:73)     
 - locked <0xd96fb668> (a com.sun.media.imageioimpl.plugins.jpeg.CLibJPEGImageReader)      
at com.sun.media.imageioimpl.plugins.clib.CLibImageReader.getImage(CLibImageReader.java:320)    
 - locked <0xd96fb668> (a com.sun.media.imageioimpl.plugins.jpeg.CLibJPEGImageReader)     
 at com.sun.media.imageioimpl.plugins.clib.CLibImageReader.read(CLibImageReader.java:384)   
 - locked <0xd96fb668> (a com.sun.media.imageioimpl.plugins.jpeg.CLibJPEGImageReader)      
at javax.imageio.ImageIO.read(ImageIO.java:1400)      
at javax.imageio.ImageIO.read(ImageIO.java:1322)

这个错误只会在Sun应用服务器上出现,因此我怀疑这是一个Sun的bug。


什么错误?你只展示了部分堆栈跟踪信息(看起来是来自jstack)。 - Joachim Sauer
你找到这个问题的原因或解决方法了吗?我正在遇到相同的问题,它在那个同样的方法上锁定线程。 - Timothy Chen
可能与此相关的一个漏洞是:http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6791502 - Adam Schmideg
14个回答

323

这是非常简单又方便的东西。

BufferedImage bimg = ImageIO.read(new File(filename));
int width          = bimg.getWidth();
int height         = bimg.getHeight();

8
这是目前为止最好的回答,你被另一个人 17 天后发布的相同回答欺骗了投票。这应该成为顶部的答案而不是底部。 - Oversteer
60
根据我所阅读的,这会将整个图像读入内存中。这样做仅仅是为了获取宽度和高度,有些过度了。 - Marc
10
不好的方法:你需要将整个图像光栅加载到内存中,这会导致在处理非常大的图像时出现内存不足(OOM)的问题。 - yetanothercoder
14
这个问题明确要求提供一种不使用ImageIO.read的方法,而你的回答是“使用ImageIO.read”。为什么这被认为是一个好答案? - ssimm
9
这是最糟糕的方法,我不明白为什么它得到了如此高的赞成票。它会将整个图像加载到堆中,这很慢并且浪费大量不必要的内存空间。 - Patrick
显示剩余11条评论

98

这是对@Kay的优秀帖子进行的重写,它抛出IOException并提供了早期退出:

/**
 * Gets image dimensions for given file 
 * @param imgFile image file
 * @return dimensions of image
 * @throws IOException if the file is not a known image
 */
public static Dimension getImageDimension(File imgFile) throws IOException {
  int pos = imgFile.getName().lastIndexOf(".");
  if (pos == -1)
    throw new IOException("No extension for file: " + imgFile.getAbsolutePath());
  String suffix = imgFile.getName().substring(pos + 1);
  Iterator<ImageReader> iter = ImageIO.getImageReadersBySuffix(suffix);
  while(iter.hasNext()) {
    ImageReader reader = iter.next();
    try {
      ImageInputStream stream = new FileImageInputStream(imgFile);
      reader.setInput(stream);
      int width = reader.getWidth(reader.getMinIndex());
      int height = reader.getHeight(reader.getMinIndex());
      return new Dimension(width, height);
    } catch (IOException e) {
      log.warn("Error reading: " + imgFile.getAbsolutePath(), e);
    } finally {
      reader.dispose();
    }
  }

  throw new IOException("Not a known image file: " + imgFile.getAbsolutePath());
}

我想我的声望还不够高,无法使我的回答被认为是有价值的。


谢谢你!考虑到用户194715进行的性能比较,我会采纳你对性能和PNG的建议!谢谢! - ah-shiang han
5
在返回之前,你应该调用 .close() 关闭流,否则你会让流处于打开状态。 - EdgeCaseBerg
1
@php_coder_3809625 日志在迭代器中,因此可能出现一个ImageReader失败的情况,但是后续可能会成功。如果它们全部失败,则会引发IOException异常。 - Andrew Taylor
我在使用这个方法处理一些jpeg文件时遇到了错误:“javax.imageio.IIOException: Not a JPEG file: starts with 0x89 0x50”。采用@apurv的解决方案可以解决这个错误,即使它需要更多的内存。 - Paul
2
您可能考虑使用org.apache.commons.io.FilenameUtils#getExtension来检测文件名扩展名。 - Patrick Bergner
显示剩余3条评论

64

我尝试使用一些列出的不同方法来测试性能。由于许多因素影响结果,很难进行严格的测试。我准备了两个文件夹,一个包含330个jpg文件,另一个包含330个png文件。在这两种情况下,平均文件大小均为4Mb。然后我对每个文件调用getDimension方法。每个getDimension方法实现和每种图像类型都单独测试(单独运行)。以下是我得到的执行时间(第一个数字是jpg,第二个数字是png):

1(Apurv) - 101454ms, 84611ms
2(joinJpegs) - 471ms, N/A
3(Andrew Taylor) - 707ms, 68ms
4(Karussell, ImageIcon) - 106655ms, 100898ms
5(user350756) - 2649ms, 68ms

显而易见,有些方法加载整个文件以获取尺寸,而其他方法仅通过从图像中读取一些标头信息来获取尺寸。 我认为当应用程序性能至关重要时,这些数字可能非常有用。

感谢大家对本帖的贡献-非常有帮助。


7
那是个好回答,做得很好! - ssimm
3
上传图片时,你是否也分析了堆空间的使用情况?此外,在运行这些测试期间,你是否在任何方法中遇到了OOM错误? - saibharath
谢谢你的回答,对我帮助很大。我有(50k高清图片)。 - SüniÚr

55

我找到了另一种读取图像大小的方式(更通用)。 您可以使用ImageIO类与ImageReaders协作。以下是示例代码:

private Dimension getImageDim(final String path) {
    Dimension result = null;
    String suffix = this.getFileSuffix(path);
    Iterator<ImageReader> iter = ImageIO.getImageReadersBySuffix(suffix);
    if (iter.hasNext()) {
        ImageReader reader = iter.next();
        try {
            ImageInputStream stream = new FileImageInputStream(new File(path));
            reader.setInput(stream);
            int width = reader.getWidth(reader.getMinIndex());
            int height = reader.getHeight(reader.getMinIndex());
            result = new Dimension(width, height);
        } catch (IOException e) {
            log(e.getMessage());
        } finally {
            reader.dispose();
        }
    } else {
        log("No reader found for given format: " + suffix));
    }
    return result;
}

请注意,getFileSuffix是一个方法,它返回路径的扩展名(不带“.”),例如:png,jpg等。 示例实现如下:

private String getFileSuffix(final String path) {
    String result = null;
    if (path != null) {
        result = "";
        if (path.lastIndexOf('.') != -1) {
            result = path.substring(path.lastIndexOf('.'));
            if (result.startsWith(".")) {
                result = result.substring(1);
            }
        }
    }
    return result;
}

这个解决方案非常快,因为它仅从文件中读取图像大小,而不是整个图像。我进行了测试,与ImageIO.read相比,性能无可比拟。希望有人会发现这很有用。


getFileSuffix() 包含不必要的 if 语句,并且在这种情况下使用 null 进行初始化不是一个好主意。 - Jimmy T.
2
这真是“基本上非常快”!我认为你用那个词获得了“年度轻描淡写奖”。相比于ImageIO.read(),它在CPU时间和内存使用方面都完全超越了它。 - aroth
1
public static String getFileSuffix(final String path) { if (path != null && path.lastIndexOf('.') != -1) { return path.substring(path.lastIndexOf('.')).substring(1); } return null; } - Nilanchala

20

您可以将JPEG二进制数据作为文件加载并自行解析JPEG头。您要查找的是0xFFC0或帧开始标头:

Start of frame marker (FFC0)

* the first two bytes, the length, after the marker indicate the number of bytes, including the two length bytes, that this header contains
* P -- one byte: sample precision in bits (usually 8, for baseline JPEG)
* Y -- two bytes
* X -- two bytes
* Nf -- one byte: the number of components in the image
      o 3 for color baseline JPEG images
      o 1 for grayscale baseline JPEG images

* Nf times:
      o Component ID -- one byte
      o H and V sampling factors -- one byte: H is first four bits and V is second four bits
      o Quantization table number-- one byte

The H and V sampling factors dictate the final size of the component they are associated with. For instance, the color space defaults to YCbCr and the H and V sampling factors for each component, Y, Cb, and Cr, default to 2, 1, and 1, respectively (2 for both H and V of the Y component, etc.) in the Jpeg-6a library by the Independent Jpeg Group. While this does mean that the Y component will be twice the size of the other two components--giving it a higher resolution, the lower resolution components are quartered in size during compression in order to achieve this difference. Thus, the Cb and Cr components must be quadrupled in size during decompression.

关于头部信息的更多信息,请查看维基百科的jpeg词条或在这里获取上述信息。

我使用了类似下面代码的方法,该代码来自 sun 论坛的这篇帖子

import java.awt.Dimension;
import java.io.*;

public class JPEGDim {

public static Dimension getJPEGDimension(File f) throws IOException {
    FileInputStream fis = new FileInputStream(f);

    // check for SOI marker
    if (fis.read() != 255 || fis.read() != 216)
        throw new RuntimeException("SOI (Start Of Image) marker 0xff 0xd8 missing");

    Dimension d = null;

    while (fis.read() == 255) {
        int marker = fis.read();
        int len = fis.read() << 8 | fis.read();

        if (marker == 192) {
            fis.skip(1);

            int height = fis.read() << 8 | fis.read();
            int width = fis.read() << 8 | fis.read();

            d = new Dimension(width, height);
            break;
        }

        fis.skip(len - 2);
    }

    fis.close();

    return d;
}

public static void main(String[] args) throws IOException {
    System.out.println(getJPEGDimension(new File(args[0])));
}

}


很好。但我认为它应该检查数字192-207,除了196、200和204之外。而不是==192 - vortexwolf
2
或者您可以使用 com.drewnoakes.metadata-extractor 库来轻松提取这些头信息。 - Victor Petit

10

简单的方法:

BufferedImage readImage = null;

try {
    readImage = ImageIO.read(new File(your path);
    int h = readImage.getHeight();
    int w = readImage.getWidth();
} catch (Exception e) {
    readImage = null;
}

3
需要读取整个图像到内存中以获得其宽度和高度。是的,这很简单,但对于许多图像或巨大的图像来说性能表现不佳... - Clint Eastwood

7

在过去的几年中,我一直苦于处理ImageIO,我认为Andrew Taylor提出的解决方案是迄今为止最好的妥协(快:不使用ImageIO#read,且多用途)。谢谢你啊!

但是,强制我使用本地文件(File/String)有些令人沮丧,特别是在你想要检查来自multipart/form-data请求的图像大小时,你通常会检索InputPart/InputStream。因此,我很快就做了一个变种,它接受FileInputStreamRandomAccessFile,基于ImageIO#createImageInputStream的能力而产生。

当然,这样使用Object input的方法可能只保留为私有方法,并创建尽可能多的多态方法,调用此方法。您还可以接受PathPath#toFile()URLURL#openStream(),然后传递给此方法:

  private static Dimension getImageDimensions(Object input) throws IOException {

    try (ImageInputStream stream = ImageIO.createImageInputStream(input)) { // accepts File, InputStream, RandomAccessFile
      if(stream != null) {
        IIORegistry iioRegistry = IIORegistry.getDefaultInstance();
        Iterator<ImageReaderSpi> iter = iioRegistry.getServiceProviders(ImageReaderSpi.class, true);
        while (iter.hasNext()) {
          ImageReaderSpi readerSpi = iter.next();
          if (readerSpi.canDecodeInput(stream)) {
            ImageReader reader = readerSpi.createReaderInstance();
            try {
              reader.setInput(stream);
              int width = reader.getWidth(reader.getMinIndex());
              int height = reader.getHeight(reader.getMinIndex());
              return new Dimension(width, height);
            } finally {
              reader.dispose();
            }
          }
        }
        throw new IllegalArgumentException("Can't find decoder for this image");
      } else {
        throw new IllegalArgumentException("Can't open stream for this image");
      }
    }
  }

5

您可以使用工具包,无需使用ImageIO。

Image image = Toolkit.getDefaultToolkit().getImage(file.getAbsolutePath());
int width = image.getWidth(null);
int height = image.getHeight(null);

如果您不想处理图片的加载,请执行以下操作:

ImageIcon imageIcon = new ImageIcon(file.getAbsolutePath());
int height = imageIcon.getIconHeight();
int width = imageIcon.getIconWidth();

2
不需要ImageIO,但需要Toolkit。这两者有什么区别? - dieter
ImageIO是一个外部依赖项。工具包未包含在内。 - Karussell
1
ImageIO自Java 1.4版本开始就是Java的一部分。https://docs.oracle.com/javase/7/docs/api/javax/imageio/package-summary.html - dieter
是的,也许这会起作用,如果有些地方令人困惑,我很抱歉。当我尝试时,我需要使用JAI(也许是为了读取其他格式?):https://dev59.com/snM_5IYBdhLWcg3w1G2N - Karussell

4

ImageIO.read的问题在于它非常慢。你只需要读取图像头来获取大小即可。ImageIO.getImageReader是完美的选择。

这里是Groovy示例,但同样适用于Java。

def stream = ImageIO.createImageInputStream(newByteArrayInputStream(inputStream))
def formatReader = ImageIO.getImageWritersByFormatName(format).next() 
def reader = ImageIO.getImageReader(formatReader)
reader.setInput(stream, true)

println "width:reader.getWidth(0) -> height: reader.getHeight(0)"

这个性能与使用SimpleImageInfo java库相同。

https://github.com/cbeust/personal/blob/master/src/main/java/com/beust/SimpleImageInfo.java


getReaderByFormat 是什么? - Koray Tugay
这无法编译。 - riddle_me_this

3
使用Java可以通过BufferedImage对象获取图像的宽度和高度。
public void setWidthAndHeightImage(FileUploadEvent event) {
    byte[] imageTest = event.getFile().getContents();
    baiStream = new ByteArrayInputStream(imageTest);
    BufferedImage bi = ImageIO.read(baiStream);
    //get width and height of image
    int imageWidth = bi.getWidth();
    int imageHeight = bi.getHeight();
}

2
这将在内存中加载整个图像,非常浪费且非常缓慢。 - Aleksander Rezen

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