21

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.

pharsicle
  • 1,209
  • 1
  • 14
  • 18
  • Could you explain why exactly `.equals()` won't work? – Alexis King Jun 12 '12 at 23:47
  • 1
    @JakeKing: if it is inherited from Object, it won't work because that only does object identity. – Thilo Jun 12 '12 at 23:49
  • 2
    cannot you just compare the byte arrays (that contain the PNG)? – Thilo Jun 12 '12 at 23:50
  • 3
    @JakeKing `BufferedImage` does not override `Object#equals()`. http://www.docjar.com/html/api/java/awt/image/BufferedImage.java.html – Matt Ball Jun 12 '12 at 23:50
  • 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?) – Andrew Thompson Jun 13 '12 at 00:17
  • These are small images in general, to used as signatures in generated correspondence. – pharsicle Jun 13 '12 at 23:28
  • 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. – WVrock Feb 21 '15 at 21:05

8 Answers8

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;
}
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
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.

user949300
  • 15,364
  • 7
  • 35
  • 66
  • 2
    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 :-) – trumpetlicks Jun 13 '12 at 01:45
  • 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. – user949300 Jun 13 '12 at 02:44
  • your code is not working java.lang.ClassCastException: java.awt.image.DataBufferByte cannot be cast to java.awt.image.DataBufferInt – cyril Jul 24 '18 at 11:23
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!!!

Freek de Bruijn
  • 3,552
  • 2
  • 22
  • 28
trumpetlicks
  • 7,033
  • 2
  • 19
  • 33
  • 2
    needs to also be false when the sizes don't match. Set the boolean to true only inside of the `if` block. – Thilo Jun 12 '12 at 23:56
  • 1
    @trumpetlicks Also, that `break` statement won't do much, you have nested `for` loops. – Jeffrey Jun 13 '12 at 00:00
  • @Jeffrey - also a good point, this is now correct, LOL. I must be extremely tired. New baby and all :-) Thanks for the aid. – trumpetlicks Jun 13 '12 at 00:11
  • 1
    @trumpetlicks 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](http://docs.oracle.com/javase/tutorial/java/nutsandbolts/branch.html). – Jeffrey Jun 13 '12 at 00:11
  • @Jeffrey - 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!!! – trumpetlicks Jun 13 '12 at 00:17
  • 1
    @trumpetlicks You don't need `imagesEqual == true`, just `imagesEqual` should suffice. That new baby must be taking a large toll. – Jeffrey Jun 13 '12 at 00:19
  • 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 :-) – trumpetlicks Jun 13 '12 at 00:20
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
}
Community
  • 1
  • 1
Sergey Ponomarev
  • 2,947
  • 1
  • 33
  • 43
1

If you want to use Mockito, then you could write a 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;
  }
}

and use it like this

assertThat(actual, new BufferedImageMatcher(expected));
G. Fiedler
  • 664
  • 4
  • 12
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);
  }
  • 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!!! – trumpetlicks Jun 12 '12 at 23:59
  • @trumpetlicks +1 for *"No way of knowing without testing!!!"* I suspect this will be *faster* than `getRGB()` per pixel, but testing will sort it. – Andrew Thompson Jun 13 '12 at 00:05
  • @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!!! – trumpetlicks Jun 13 '12 at 00:10
  • @trumpetlicks see my answer for a similar approach that should be faster (though more memory intensive). – user949300 Jun 13 '12 at 01:17
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)
                               )
                 );
comonad
  • 5,134
  • 2
  • 33
  • 31
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;
    }
}
cyril
  • 872
  • 6
  • 29