Is there a simple way to compare BufferedImage instances?
Asked Answered
I

8

22

I am working on part of a Java application that takes an image as a byte array, reads it into a java.awt.image.BufferedImage instance and passes it to a third-party library for processing.

For a unit test, I want to take an image (from a file on disk) and assert that it is equal to the same image that has been processed by the code.

  • My expected BufferedImage is read from a PNG file on disk using ImageIO.read(URL).
  • My test code reads the same file into a BufferedImage and writes that to a byte array as PNG to provide to the system under test.

When the system under test writes the byte array to a new BufferedImage I want to assert that the two images are equal in a meaningful way. Using equals() (inherited from Object) doesn’t work (of course). Comparing BufferedImage.toString() values also doesn’t work because the output string includes object reference information.

Does anybody know any shortcuts? I would prefer not to bring in a third-party library for a single unit test in a small part of a large application.

Innocuous answered 12/6, 2012 at 23:45 Comment(7)
Could you explain why exactly .equals() won't work?Inamorato
@JakeKing: if it is inherited from Object, it won't work because that only does object identity.Combustor
cannot you just compare the byte arrays (that contain the PNG)?Combustor
@JakeKing BufferedImage does not override Object#equals(). docjar.com/html/api/java/awt/image/BufferedImage.java.htmlConvenance
There are a few things I am not clear on. Maybe I can short-cut asking 20 questions by checking. 1) Assuming you had loaded BufferedImage instances, would the fastest check of comparison between those images answer your question? 2) Is memory a huge problem? (Are they 4000x3000px images?)Blockish
These are small images in general, to used as signatures in generated correspondence.Innocuous
It just occurred to me. If you can force the program to use a custom method to load every images then you can use a wrapper that also have a string field. Make the method save the name of the image into the string field. This way if the strings are the same then the images are the same. Though if it is possible for different images to have the same name, this wouldn't work.Belshin
H
20

This is the best approach. No need to keep a variable to tell whether the image is still equal. Simply return false immediately when the condition if false. Short-circuit evaluation helps save time looping over pixels after the comparison fails as is the case in trumpetlick's answer.

/**
 * 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;
}
Halfback answered 27/4, 2015 at 3:40 Comment(1)
nice and simple solution! Exactly what I was looking for!Orthography
R
8

If speed is an issue, and both BufferedImages are of the same bit-depth, arrangement, etc. (which seems like it must be true here) you can do this:

DataBuffer dbActual = myBufferedImage.getRaster().getDataBuffer();
DataBuffer dbExpected = bufferImageReadFromAFile.getRaster().getDataBuffer();

figure out which type it is, e.g. a DataBufferInt

DataBufferInt actualDBAsDBInt = (DataBufferInt) dbActual ;
DataBufferInt expectedDBAsDBInt = (DataBufferInt) dbExpected ;

do a few "sanity checks" for equals on the sizes and banks of the DataBuffers, then loop

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

This is close to as fast as you can get cause you are grabbing a chunk of the data at a time, not one at a time.

Ridotto answered 13/6, 2012 at 1:12 Comment(3)
The same bit-depth, arrangement, etc... is a pretty big one. Just answered a guys question yesterday where he was doing essentially a compare of images, and he found out that one was of ARGB_8888 and another was RGB_565. These are big assumptions. You are indeed correct, if those parameters are true, this would be the fastest method though :-)Reasoned
Since it is a unit test it seems like his test image should be the same bit-depth etc. But agree with you that, in general, that is a bit of an assumption.Ridotto
your code is not working java.lang.ClassCastException: java.awt.image.DataBufferByte cannot be cast to java.awt.image.DataBufferIntConidiophore
R
3

You could write your own routine for comparison!

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

This would be one way!!!

Reasoned answered 12/6, 2012 at 23:54 Comment(7)
needs to also be false when the sizes don't match. Set the boolean to true only inside of the if block.Combustor
@Reasoned Also, that break statement won't do much, you have nested for loops.Sik
@Sik - also a good point, this is now correct, LOL. I must be extremely tired. New baby and all :-) Thanks for the aid.Reasoned
@Reasoned Now it's even worse: you're continuing to iterate over the images after you already know they're not equal. Use a labelled break.Sik
@Sik - New baby is really making my coding BAD BAD BAD LOL. Again, thanks for the help. From a coding style perspective, I have always been taught not to have labels in my code, this is why I have avoided you labelled break idea, but that would definitely be an answer also!!!Reasoned
@Reasoned You don't need imagesEqual == true, just imagesEqual should suffice. That new baby must be taking a large toll.Sik
Apparently so, Ill go ahead and leave the explicit == true, but again you are correct LOL. If it makes you feel any better, I keep up-voting your comments :-)Reasoned
H
1

I changed function that equals by pixels in Groovy, may be helpful:

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
}
Haggadah answered 9/4, 2014 at 9:14 Comment(0)
I
1

If you want to use Mockito, then you could write a Hamcrest Matcher

import org.mockito.ArgumentMatcher;

public class BufferedImageMatcher implements ArgumentMatcher<BufferedImage> {

  private final BufferedImage expected;

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

  @Override
  public boolean matches(BufferedImage actual) {
    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;
  }
}

and use it like this

assertThat(actual, new BufferedImageMatcher(expected));
Ician answered 3/4, 2019 at 12:55 Comment(0)
M
0

I can't think of anything besides a brute force "do loop":

  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);
  }
Malang answered 12/6, 2012 at 23:57 Comment(4)
good answer, but this does use twice the memory as it copies the whole image into another buffer! May have a chance of being faster do to less actual routine calls :-) No way of knowing without testing!!!Reasoned
@Reasoned +1 for "No way of knowing without testing!!!" I suspect this will be faster than getRGB() per pixel, but testing will sort it.Blockish
@AndrewThompson - Not sure actually, he is calling db1.getSize each iteration, as well as potentially 4 data copies. 1 for creating r1, another for db1, and the same 2 for r2 and db2. Then he also has db1.getElem(i). Actually this will indeed be slower, because he is also calling a routine to get both elements within the arrays. db1.getSize() is going to return (width*height) of the image. So he is not only copying the data, but calling the same amount of routines within the loop. He also isn't even doing the compare operation at all!!!Reasoned
@Reasoned see my answer for a similar approach that should be faster (though more memory intensive).Ridotto
H
0

You can write that image via imageio through an OutputStream to a byte[]. In my code, it looks more or less like this:

byte[] encodeJpegLossless(BufferedImage img){...using imageio...}
...
Assert.assertTrue(Arrays.equals(encodeJpegLossless(img1)
                               ,encodeJpegLossless(img2)
                               )
                 );
Hayman answered 20/11, 2013 at 15:50 Comment(0)
C
0

working well but not efficient

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;
    }
}
Conidiophore answered 24/7, 2018 at 11:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.