有没有一种简单的方法来比较BufferedImage实例?

21

我正在处理 Java 应用程序的一部分,该应用程序将图像作为字节数组处理,将其读入 java.awt.image.BufferedImage 实例中并将其传递给第三方库进行处理。

为了进行单元测试,我想要获取一张图片(从磁盘上的文件),并断言它与代码处理后的相同的图片是相等的。

  • 我的期望 BufferedImage 从磁盘上的 PNG 文件使用 ImageIO.read(URL) 读取。
  • 我的测试代码将相同的文件读入到 BufferedImage 中,并将其作为 PNG 写入字节数组,以提供给待测试系统。

当待测试系统将字节数组写入新的 BufferedImage 时,我希望断言这两个图像在有意义的方式上是相等的。使用 equals() 方法(从 Object 继承而来)不能很好地工作。比较 BufferedImage.toString() 值也不行,因为输出的字符串包括对象引用信息。

有人知道任何快捷方法吗?我不想在一个大型应用程序的小部分中为单个单元测试带来第三方库。


你能解释一下为什么 .equals() 不能起作用吗? - Alexis King
1
@JakeKing:如果它是从Object继承的,那么它不起作用,因为它只执行对象标识。 - Thilo
2
你能否直接比较包含PNG的字节数组? - Thilo
3
BufferedImage 没有重写 Object#equals() 方法。http://www.docjar.com/html/api/java/awt/image/BufferedImage.java.html - Matt Ball
我突然想到一个方法。如果你可以强制程序使用自定义方法来加载每个图像,那么你可以使用一个包装器,它也有一个字符串字段。使方法将图像的名称保存到字符串字段中。这样,如果字符串相同,则图像相同。但是,如果不同的图像可能具有相同的名称,则此方法将无法正常工作。 - WVrock
显示剩余2条评论
8个回答

20

这是最佳的方法。不需要保留一个变量来告诉图像是否仍然相等。当条件为假时,立即返回false即可。短路评估有助于在比较失败后节省时间循环遍历像素,就像在trumpetlick的答案中所述。

/**
 * Compares two images pixel by pixel.
 *
 * @param imgA the first image.
 * @param imgB the second image.
 * @return whether the images are both the same or not.
 */
public static boolean compareImages(BufferedImage imgA, BufferedImage imgB) {
  // The images must be the same size.
  if (imgA.getWidth() != imgB.getWidth() || imgA.getHeight() != imgB.getHeight()) {
    return false;
  }

  int width  = imgA.getWidth();
  int height = imgA.getHeight();

  // Loop over every pixel.
  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      // Compare the pixels for equality.
      if (imgA.getRGB(x, y) != imgB.getRGB(x, y)) {
        return false;
      }
    }
  }

  return true;
}

好的,简单明了的解决方案!正是我所需要的! - Waylander

8
如果速度是一个问题,并且两个BufferedImages的位深度、排列等相同(这似乎在这里必须成立),则可以这样做:
DataBuffer dbActual = myBufferedImage.getRaster().getDataBuffer();
DataBuffer dbExpected = bufferImageReadFromAFile.getRaster().getDataBuffer();

找出它属于哪种类型,例如一个 DataBufferInt
DataBufferInt actualDBAsDBInt = (DataBufferInt) dbActual ;
DataBufferInt expectedDBAsDBInt = (DataBufferInt) dbExpected ;

首先对DataBuffers的大小和bank进行一些"合理性检查",然后进入循环。

for (int bank = 0; bank < actualDBAsDBInt.getNumBanks(); bank++) {
   int[] actual = actualDBAsDBInt.getData(bank);
   int[] expected = expectedDBAsDBInt.getData(bank);

   // this line may vary depending on your test framework
   assertTrue(Arrays.equals(actual, expected));
}

这是尽可能快的方式,因为您一次获取一块数据而不是逐个获取。


2
相同的位深度、排列等等是一个非常重要的因素。昨天我回答了一个人的问题,他基本上在比较图像,结果发现其中一个是ARGB_8888格式,另一个是RGB_565格式。这些都是很大的假设。如果这些参数是正确的,你确实是正确的,这将是最快的方法 :-) - trumpetlicks
既然这是一个单元测试,那么他的测试图像似乎应该具有相同的位深度等特性。但我同意你的观点,在一般情况下,这是一个有点儿假设的前提。 - user949300
你的代码出了问题:java.lang.ClassCastException: java.awt.image.DataBufferByte 无法转换为 java.awt.image.DataBufferInt。 - cyril

3
您可以编写自己的比较程序!
int width;
int height;
boolean imagesEqual = true;

if( image1.getWidth()  == ( width  = image2.getWidth() ) && 
    image1.getHeight() == ( height = image2.getHeight() ) ){

    for(int x = 0;imagesEqual == true && x < width; x++){
        for(int y = 0;imagesEqual == true && y < height; y++){
            if( image1.getRGB(x, y) != image2.getRGB(x, y) ){
                imagesEqual = false;
            }
        }
    }
}else{
    imagesEqual = false;
}

这是一种方法!!!

2
当大小不匹配时,也需要将其设置为false。仅在if块内将布尔值设置为true。 - Thilo
1
@trumpetlicks 另外,那个 break 语句不会有太大作用,因为你有嵌套的 for 循环。 - Jeffrey
@Jeffrey - 也是个好观点,现在已经正确了,哈哈。我一定是太累了,有了新宝宝和所有的事情 :-) 感谢你的帮助。 - trumpetlicks
1
@trumpetlicks 现在情况更糟了:在你已经知道它们不相等之后,你仍然继续迭代图像。使用标记的break - Jeffrey
1
@trumpetlicks,你不需要 imagesEqual == true,只需要 imagesEqual 就可以了。那个新宝宝肯定会让你很辛苦的。 - Jeffrey
显示剩余2条评论

1

我在Groovy中更改了按像素相等的函数,可能会有帮助:

boolean imagesAreEqual(BufferedImage image1, BufferedImage image2) {
    if (image1.width != image2.width || image1.height != image2.height) {
         return false
    }
    for (int x = 1; x < image2.width; x++) {
        for (int y = 1; y < image2.height; y++) {
             if (image1.getRGB(x, y) != image2.getRGB(x, y)) {
                 return false
             }
        }
    }
    return true
}

1
如果你想使用Mockito,那么你可以编写一个Hamcrest Matcher。
import org.mockito.ArgumentMatcher;

public class BufferedImageMatcher extends ArgumentMatcher<BufferedImage> {

  private final BufferedImage expected;

  public BufferedImageMatcher(BufferedImage expected) {
    this.expected = expected;
  }

  @Override
  public boolean matches(Object argument) {
    BufferedImage actual = (BufferedImage) argument;

    assertEquals(expected.getWidth(), actual.getWidth());
    assertEquals(expected.getHeight(), actual.getHeight());

    for (int x = 0; x < actual.getWidth(); x++) {
      for (int y = 0; y < actual.getHeight(); y++) {
        assertEquals(expected.getRGB(x, y), actual.getRGB(x, y));
      }
    }

    return true;
  }
}

并像这样使用它
assertThat(actual, new BufferedImageMatcher(expected));

0

功能正常但效率不高

public static boolean compareImage(File fileA, File fileB) {        
    try {
        // take buffer data from botm image files //
        BufferedImage biA = ImageIO.read(fileA);
        DataBuffer dbA = biA.getData().getDataBuffer();
        int sizeA = dbA.getSize();                      
        BufferedImage biB = ImageIO.read(fileB);
        DataBuffer dbB = biB.getData().getDataBuffer();
        int sizeB = dbB.getSize();
        // compare data-buffer objects //
        if(sizeA == sizeB) {
            for(int i=0; i<sizeA; i++) { 
                if(dbA.getElem(i) != dbB.getElem(i)) {
                    return false;
                }
            }
            return true;
        }
        else {
            return false;
        }
    } 
    catch (Exception e) { 
        e.printStackTrace();
        return  false;
    }
}

0

我除了使用暴力循环之外,想不到其他的方法:

  BufferedImage bi1, bi2, ...
   ...
  Raster r1 = bi1.getData();
  DataBuffer db1 = r1.getDataBuffer();
  if (db1.getSize() != db2.getSize ())
     ...
  for (int i = 0; i < db1.getSize(); i++) {  
    int px = db1.getElem(i);
  }

回答不错,但是它使用了两倍的内存,因为它将整个图像复制到另一个缓冲区中!可能会更快,因为实际例程调用较少 :-) 没有测试就没有办法知道!!! - trumpetlicks
@trumpetlicks +1 对于“没有测试就无法知道!!!” 我怀疑这将比每个像素的getRGB(),但只有测试才能确定。 - Andrew Thompson
@AndrewThompson - 实际上不确定,他在每次迭代中都调用db1.getSize,以及可能进行4个数据复制。一个是为了创建r1,另一个是为了db1,还有相同的2个是为了r2和db2。然后他还有db1.getElem(i)。实际上这确实会更慢,因为他还要调用一个例程来获取数组中的两个元素。db1.getSize()将返回图像的(宽度*高度)。因此,他不仅复制数据,而且在循环内调用相同数量的例程。他甚至根本没有执行比较操作!!! - trumpetlicks
@trumpetlicks 看看我的答案,采用类似的方法应该更快(但更耗费内存)。 - user949300

0

你可以通过OutputStream将图像写入byte[],并使用imageio进行操作。

在我的代码中,大致如下:

byte[] encodeJpegLossless(BufferedImage img){...using imageio...}
...
Assert.assertTrue(Arrays.equals(encodeJpegLossless(img1)
                               ,encodeJpegLossless(img2)
                               )
                 );

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