Skip to content

Instantly share code, notes, and snippets.

@sshongru
Last active December 17, 2015 21:29
Show Gist options
  • Save sshongru/5674574 to your computer and use it in GitHub Desktop.
Save sshongru/5674574 to your computer and use it in GitHub Desktop.
Examines two PNG files and outputs a diff PNG if they are different.
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
public class ImageDiff {
private static BufferedImage expectedImage;
private static BufferedImage actualImage;
private static int numDifferentPixels = 0;
public static void main(String[] args) throws IOException {
if (args.length != 3) {
System.out.println("Usage: java ImageDiff /path/to/expected.png /path/to/actual.png /path/to/diff.png");
return;
}
File expectedFile = new File(args[0]);
File actualFile = new File(args[1]);
File diffFile = new File(args[2]);
if (!expectedFile.exists()) {
System.out.println("Error: Expected file not found at " + expectedFile.getAbsolutePath());
return;
}
if (!actualFile.exists()) {
System.out.println("Error: Actual file not found at " + actualFile.getAbsolutePath());
return;
}
expectedImage = ImageIO.read(expectedFile);
actualImage = ImageIO.read(actualFile);
int width = expectedImage.getWidth();
int height = expectedImage.getHeight();
// ensure the images have the same dimensions
if (width != actualImage.getWidth()){
System.out.println("Error: Expected image width (" + width + ") does not match actual image width (" + actualImage.getWidth() + ").");
return;
}
if (height != actualImage.getHeight()){
System.out.println("Error: Expected image height (" + height + ") does not match actual image height (" + actualImage.getHeight() + ").");
return;
}
int[][] expectedPixels = getPixels(expectedImage);
int[][] actualPixels = getPixels(actualImage);
// create diff
BufferedImage diffImage = getDiff(expectedPixels, actualPixels);
// show the results
if (numDifferentPixels != 0){
// write the diff to disk
ImageIO.write(diffImage, "png", diffFile); // TODO: Support more than just PNG
System.out.println(numDifferentPixels + " pixels different, diff created at: " + diffFile);
} else {
System.out.println("0 pixels different");
}
}
private static int[][] getPixels(BufferedImage image) {
int type = image.getType();
// indexed images will use the standard slow implementation
if (type == BufferedImage.TYPE_BYTE_BINARY ||
type == BufferedImage.TYPE_BYTE_INDEXED ||
type == BufferedImage.TYPE_BYTE_GRAY){
return getPixelsSlow(image);
}
// non-indexed images we can easily optimize by getting a byte[] of pixels directly
byte[] bytes = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
int width = image.getWidth();
int height = image.getHeight();
int[][] result = new int[height][width];
int y = 0;
int x = 0;
int pixelLength; // number of bytes that represent a single pixel
boolean hasAlphaChannel = image.getAlphaRaster() != null;
// TODO: Clean this up into one chunk
if (hasAlphaChannel) {
pixelLength = 4;
for (int bytesIndex = 0; bytesIndex < bytes.length; bytesIndex += pixelLength) {
int argb = 0;
argb += (((int) bytes[bytesIndex] & 0xFF) << 24); // alpha
argb += (((int) bytes[bytesIndex + 3] & 0xFF) << 16); // red
argb += (((int) bytes[bytesIndex + 2] & 0xFF) << 8); // green
argb += ((int) bytes[bytesIndex + 1] & 0xFF); // blue
result[y][x] = argb;
x++;
if (x == width) {
x = 0;
y++;
}
}
} else {
pixelLength = 3;
for (int bytesIndex = 0; bytesIndex < bytes.length; bytesIndex += pixelLength) {
int argb = 0;
argb += -16777216; // alpha
argb += (((int) bytes[bytesIndex + 2] & 0xFF) << 16); // red
argb += (((int) bytes[bytesIndex + 1] & 0xFF) << 8); // green
argb += ((int) bytes[bytesIndex] & 0xFF); // blue
result[y][x] = argb;
x++;
if (x == width) {
x = 0;
y++;
}
}
}
return result;
}
/**
* A slow, but reliable way of getting RGB values. This works with
* all kinds of PNG types including indexed color.
*/
private static int[][] getPixelsSlow(BufferedImage image) {
int width = image.getWidth();
int height = image.getHeight();
int[][] result = new int[height][width];
for (int row = 0; row < height; row++)
for (int col = 0; col < width; col++)
result[row][col] = image.getRGB(col, row);
return result;
}
private static BufferedImage getDiff(int[][] expectedPixels, int[][] actualPixels) {
int height = expectedPixels.length;
int width = expectedPixels[0].length;
BufferedImage diffImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < height; y++){
for (int x = 0; x < width; x++){
// expected pixel
int e = expectedPixels[y][x];
int eA = (e >> 24) & 0xFF; // alpha
int eR = (e >> 16) & 0xFF; // red
int eG = (e >> 8) & 0xFF; // green
int eB = (e >> 0) & 0xFF; // blue
// actual pixel
int a = actualPixels[y][x];
int aA = (a >> 24) & 0xFF; // alpha
int aR = (a >> 16) & 0xFF; // red
int aG = (a >> 8) & 0xFF; // green
int aB = (a >> 0) & 0xFF; // blue
// difference
int dA = Math.abs(eA - aA);
int dR = Math.abs(eR - aR);
int dG = Math.abs(eG - aG);
int dB = Math.abs(eB - aB);
// keep track of failed pixels
boolean colorFailure = (dR != 0 || dG != 0 || dB != 0) ? true : false;
if (dA != 0 || colorFailure)
numDifferentPixels++;
// construct the difference pixel
int d = 0;
d += dR << 16;
d += dG << 8;
d += dB;
// add alpha:
// - perfectly matching pixels are transparent
// - color channel differences show up with full opacity
// - alpha channel differences show up with partial opacity
if (dA == 0 && colorFailure){
d += 255 << 24;
} else {
d += dA << 24;
}
diffImage.setRGB(x, y, d);
}
}
return diffImage;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment