package com.vitco.util.graphic;
import com.vitco.util.misc.ArrayUtil;
import com.vitco.util.misc.IntegerTools;
import gnu.trove.iterator.TIntIntIterator;
import gnu.trove.iterator.TIntIterator;
import gnu.trove.map.hash.TIntIntHashMap;
import gnu.trove.map.hash.TIntObjectHashMap;
import gnu.trove.set.hash.TIntHashSet;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collection;
/**
* Allows for fast comparison of images.
*
* - sub image detection
* - detection of best merging points for two images (slow)
*/
public class ImageComparator {
// holds the colors of this texture with count (color -> count)
private final TIntIntHashMap colors = new TIntIntHashMap();
// holds the colors of this texture row/column wise (row/column -> color -> count)
private final TIntObjectHashMap<TIntIntHashMap> colorsPerRow = new TIntObjectHashMap<TIntIntHashMap>();
private final TIntObjectHashMap<TIntIntHashMap> colorsPerCol = new TIntObjectHashMap<TIntIntHashMap>();
// stores all the pixels of the encapsulated image
private final TIntIntHashMap pixels = new TIntIntHashMap();
// dimension of this image
private final int width;
private final int height;
// dimension minus one (used for position inversion)
private final int widthM;
private final int heightM;
// amount of pixels in this image
// Note: this is <= width * height as some pixels might not be set
public final int pixelCount;
// amount of different colors in this image
public final int colorCount;
// helper class - array list that has an init action executed on initialization
private static abstract class InitArrayList extends ArrayList<int[]> {
protected abstract void init();
public InitArrayList() {
super();
init();
}
}
// constructor
public ImageComparator(final BufferedImage image) {
// read the content of the buffered image and pass on to other constructor
this(new InitArrayList() {
@Override
protected void init() {
for (int x = 0, width = image.getWidth(); x < width; x++) {
for (int y = 0, height = image.getHeight(); y < height; y++) {
int rgb = image.getRGB(x,y);
// check that this is not a fully transparent pixel
if (((rgb >> 24) & 0xff) != 0) {
add(new int[]{x, y, rgb});
}
}
}
}
});
}
// constructor
public ImageComparator(Collection<int[]> pixels) {
// image dimension (updated from the pixel data)
int width = 0;
int height = 0;
// extract colors and count
for (int[] pixel : pixels) {
// set global color count
int count = colors.get(pixel[2]);
colors.put(pixel[2], count+1);
// set row count
TIntIntHashMap row = colorsPerRow.get(pixel[1]);
if (row == null) {
row = new TIntIntHashMap();
colorsPerRow.put(pixel[1], row);
}
count = row.get(pixel[2]);
row.put(pixel[2], count+1);
// set col count
TIntIntHashMap col = colorsPerCol.get(pixel[0]);
if (col == null) {
col = new TIntIntHashMap();
colorsPerCol.put(pixel[0], col);
}
count = col.get(pixel[2]);
col.put(pixel[2], count+1);
// update pixel buffer (for fast access)
this.pixels.put(IntegerTools.makeInt(pixel[0], pixel[1]), pixel[2]);
// update width and height
width = Math.max(pixel[0], width);
height = Math.max(pixel[1], height);
}
// set pixel and color count
this.pixelCount = pixels.size();
this.colorCount = colors.size();
// finalize the size
this.widthM = width;
this.heightM = height;
this.width = width + 1;
this.height = height + 1;
}
// ===========================
// compute the Jaccard similarity coefficient (using the colors)
public final float jaccard(ImageComparator other) {
TIntHashSet uniqueColors = new TIntHashSet(this.colors.keySet());
uniqueColors.addAll(other.colors.keySet());
int intersection = 0;
int union = 0;
for (TIntIterator it = uniqueColors.iterator(); it.hasNext();) {
int color = it.next();
int count1 = this.colors.get(color);
int count2 = other.colors.get(color);
intersection += Math.min(count1, count2);
union += Math.max(count1, count2);
}
return intersection / (float)union;
}
// =================
// helper - check if a certain pixel is "ok" (matches
// or "not set") for a specific matching
private static void checkPixel(
int x, int y, ImageComparator one, ImageComparator two,
int i, int j, boolean[] matched, int[] pixelOverlapTmp,
boolean flip
) {
// compute point in static image ("one")
int p1 = IntegerTools.makeInt(i, j);
for (int k = 0; k < 4; k ++) {
// only proceed if this check has not already failed
if (matched[k]) {
// compute the corresponding pixel in image "two"
int p2;
if (flip) {
// -- check for overlap of corresponding pixel
// 0 : check for "rotation 1" (1)
// 1 : check for "rotation 3" (3)
// 2 : check for "flipped and rotation 1" (5)
// 3 : check for "flipped and rotation 3" (7)
switch (k) {
case 0: p2 = IntegerTools.makeInt(j - y, two.height - 1 - (i - x)); break;
case 1: p2 = IntegerTools.makeInt(two.width - 1 - (j - y), i - x); break;
case 2: p2 = IntegerTools.makeInt(two.width - 1 - (j - y), two.height - 1 - (i - x)); break;
default: p2 = IntegerTools.makeInt(j - y, i - x); break;
}
} else {
// -- check for overlap of corresponding pixel
// 0 : check for "default orientation" (0)
// 1 : check for "twice rotated" (2)
// 2 : check for "flipped" (4)
// 3 : check for "flipped and twice rotated" (6)
switch (k) {
case 0: p2 = IntegerTools.makeInt(i - x, j - y); break;
case 1: p2 = IntegerTools.makeInt(two.width - 1 - (i - x), two.height - 1 - (j - y)); break;
case 2: p2 = IntegerTools.makeInt(two.width - 1 - (i - x), j - y); break;
default: p2 = IntegerTools.makeInt(i - x, two.height - 1 - (j - y)); break;
}
}
// check for containment
if (one.pixels.containsKey(p1) && two.pixels.containsKey(p2)) {
if (one.pixels.get(p1) == two.pixels.get(p2)) {
pixelOverlapTmp[k]++;
} else {
matched[k] = false;
}
}
}
}
}
// helper - check if a certain offset allows placing
// the second image onto the first one
private static void checkPosition(
int x, int y, ImageComparator one, ImageComparator two,
int[] area, int[] size, int originalWidth, int originalHeight,
int[] pixelOverlap, int[] result,
boolean flip
) {
// compute intersection area
int minX = Math.max(0, x);
int minY = Math.max(0, y);
int maxX, maxY;
int widthTmp, heightTmp;
if (flip) {
maxX = Math.min(one.width, x + two.height);
maxY = Math.min(one.height, y + two.width);
// compute new width, height and pixel count
widthTmp = Math.max(one.width, x + two.height) - Math.min(0, x);
heightTmp = Math.max(one.height, y + two.width) - Math.min(0, y);
} else {
maxX = Math.min(one.width, x + two.width);
maxY = Math.min(one.height, y + two.height);
// compute new width, height and pixel count
widthTmp = Math.max(one.width, x + two.width) - Math.min(0, x);
heightTmp = Math.max(one.height, y + two.height) - Math.min(0, y);
}
int areaTmp = widthTmp * heightTmp;
// do some restriction checking
if ((area[0] >= areaTmp) &&
// ensure that the image can not only grow into one direction
(widthTmp < heightTmp * 3 || (originalWidth != size[0] && size[0] >= widthTmp)) &&
(heightTmp < widthTmp * 3 || (originalHeight != size[1] && size[1] >= heightTmp))) {
// initialize variables
boolean[] matched = new boolean[] {true, true, true, true};
int[] pixelOverlapTmp = new int[4];
// loop over all intersection points
loop: for (int i = minX; i < maxX; i++) {
for (int j = minY; j < maxY; j++) {
checkPixel(x, y, one, two, i, j, matched, pixelOverlapTmp, flip);
// if all overlap checks have already failed we can break the loop
if (!matched[0] && !matched[1] && !matched[2] && !matched[3]) {
break loop;
}
}
}
// check if matches are better
for (int k = 0; k < 4; k ++) {
if (matched[k]) {
if (area[0] > areaTmp || pixelOverlapTmp[k] > pixelOverlap[0]) {
result[0] = x;
result[1] = y;
result[2] = k * 2 + (flip ? 1 : 0);
area[0] = areaTmp;
size[0] = widthTmp;
size[1] = heightTmp;
pixelOverlap[0] = pixelOverlapTmp[k];
}
}
}
}
}
// find the best "merge" position with orientation
public static int[] getMergePoint(ImageComparator one, ImageComparator two) {
// default result if nothing better is found
int[] result = new int[]{one.width, 0, 0};
int[] size = new int[] {one.width + two.width, Math.max(one.height, two.height)};
// add to bottom if the first image is wide
if (size[0] > size[1] * 3) {
result[0] = 0;
result[1] = one.height;
result[2] = 0;
size[0] = Math.max(one.width, two.width);
size[1] = one.height + two.height;
}
int[] area = new int[] {size[0] * size[1]};
int[] pixelOverlap = new int[] {0};
int[] originalSize = size.clone();
// loop over all "non flipped" start positions
for (int x = -two.width + 1; x < one.width; x++) {
for (int y = -two.height + 1; y < one.height; y++) {
checkPosition(x,y,one,two,area,size,originalSize[0],originalSize[1],pixelOverlap,result, false);
}
}
// loop over all "flipped" start positions
// (i.e. the width and height of "two" are swapped)
for (int x = -two.height + 1; x <= one.width; x++) {
for (int y = -two.width + 1; y < one.height; y++) {
checkPosition(x,y,one,two,area,size,originalSize[0],originalSize[1],pixelOverlap,result, true);
}
}
return result;
}
// ===========================
// helper - check if child is contained in this image for a certain orientation given by "type" (explaination see below)
private int[] getPosition(ImageComparator child, ArrayList<Integer> one, ArrayList<Integer> two, int[] restriction, int type) {
if (restriction == null || ArrayUtil.contains(restriction, type)) {
for (int x : one) {
for (int y : two) {
// loop over all child pixels
boolean match = true;
for (TIntIntIterator pixel = child.pixels.iterator(); pixel.hasNext(); ) {
pixel.advance();
// check for containment in parent
short[] childPos = IntegerTools.getShorts(pixel.key());
int color;
switch (type) {
// 0 - original, 1 - rotated x 1, 2 - rotated x 2, 3 - rotated x 3,
// 4 - flipped, 5 - flipped & rotated x 1, 6 - flipped & rotated x 2, 7 - flipped & rotated x 3
case 0: color = this.pixels.get(IntegerTools.makeInt(x + childPos[0], y + childPos[1])); break;
case 4: color = this.pixels.get(IntegerTools.makeInt(x + (child.widthM - childPos[0]), y + childPos[1])); break;
case 2: color = this.pixels.get(IntegerTools.makeInt(x + (child.widthM - childPos[0]), y + (child.heightM - childPos[1]))); break;
case 6: color = this.pixels.get(IntegerTools.makeInt(x + childPos[0], y + (child.heightM - childPos[1]))); break;
case 7: color = this.pixels.get(IntegerTools.makeInt(x + childPos[1], y + childPos[0])); break;
case 1: color = this.pixels.get(IntegerTools.makeInt(x + (child.heightM - childPos[1]), y + childPos[0])); break;
case 3: color = this.pixels.get(IntegerTools.makeInt(x + childPos[1], y + (child.widthM - childPos[0]))); break;
default: color = this.pixels.get(IntegerTools.makeInt(x + (child.heightM - childPos[1]), y + (child.widthM - childPos[0]))); break; // case 5
}
// Note: "color" might be null
if (pixel.value() != color) {
match = false;
break;
}
}
if (match) {
return new int[]{x, y, type};
}
}
}
}
return null;
}
// return the first position of this sub image in this image
// or return null if no position is found
// ----
// Note: all orientations are analysed if not otherwise specified in the
// restriction array:
// 0 - original, 1 - rotated x 1, 2 - rotated x 2, 3 - rotated x 3,
// 4 - flipped, 5 - flipped & rotated x 1, 6 - flipped & rotated x 2, 7 - flipped & rotated x 3
public final int[] getPosition(ImageComparator child, int[] restriction) {
// -- do a quick return if child is one pixel in size
if (child.pixelCount == 1) {
int color = child.colors.keySet().iterator().next();
// ensure that the color is present
if (this.colors.containsKey(color)) {
if (this.pixelCount == 1) {
return new int[] {0,0,0};
} else {
// find location
for (TIntIntIterator it = this.pixels.iterator(); it.hasNext();) {
it.advance();
if (it.value() == color) {
short[] p = IntegerTools.getShorts(it.key());
return new int[]{p[0], p[1], 0};
}
}
// this should never be reached since the color is present
assert false;
}
}
// color is not present
return null;
}
// -- check if dimensions fit
if ((child.width > this.width || child.height > this.height) && (child.height > this.width || child.width > this.height)) {
return null;
}
// -- check if pixel count fits
if (this.pixelCount < child.pixelCount) {
return null;
}
// -- check if there are enough different colors
if (this.colorCount < child.colorCount) {
return null;
}
// -- check if contained colors are subset
if (!contained(child.colors, colors)) {
return null;
}
// ==============
// -- check for placement without "swap"
int[] result;
if (child.width <= this.width && child.height <= this.height) {
// -- check for containment (Orientation 1)
ArrayList<Integer> rowRow = getPossiblePositions(child.colorsPerRow, colorsPerRow, false);
ArrayList<Integer> colCol = getPossiblePositions(child.colorsPerCol, colorsPerCol, false);
result = getPosition(child, colCol, rowRow, restriction, 0);
if (result != null) {
return result;
}
// -- check for containment (Flip 1)
ArrayList<Integer> colColFlip = getPossiblePositions(child.colorsPerCol, colorsPerCol, true);
result = getPosition(child, colColFlip, rowRow, restriction, 4);
if (result != null) {
return result;
}
// -- check for containment (Rotation 2)
ArrayList<Integer> rowRowFlip = getPossiblePositions(child.colorsPerRow, colorsPerRow, true);
result = getPosition(child, colColFlip, rowRowFlip, restriction, 2);
if (result != null) {
return result;
}
// -- check for containment (Flip + Rotation 2)
result = getPosition(child, colCol, rowRowFlip, restriction, 6);
if (result != null) {
return result;
}
}
// ============
// -- check for placement with "swap"
if (child.height <= this.width && child.width <= this.height) {
// -- check for containment (Flip + Rotation 3)
ArrayList<Integer> colRow = getPossiblePositions(child.colorsPerCol, colorsPerRow, false);
ArrayList<Integer> rowCol = getPossiblePositions(child.colorsPerRow, colorsPerCol, false);
result = getPosition(child, rowCol, colRow, restriction, 7);
if (result != null) {
return result;
}
// -- check for containment (Rotation 1)
ArrayList<Integer> rowColFlip = getPossiblePositions(child.colorsPerRow, colorsPerCol, true);
result = getPosition(child, rowColFlip, colRow, restriction, 1);
if (result != null) {
return result;
}
// -- check for containment (Rotation 3)
ArrayList<Integer> colRowFlip = getPossiblePositions(child.colorsPerCol, colorsPerRow, true);
result = getPosition(child, rowCol, colRowFlip, restriction, 3);
if (result != null) {
return result;
}
// -- check for containment (Flip + Rotation 1)
result = getPosition(child, rowColFlip, colRowFlip, restriction, 5);
if (result != null) {
return result;
}
}
// -- nothing found
return null;
}
// check "containment" positions of child array in parent array
private ArrayList<Integer> getPossiblePositions(
TIntObjectHashMap<TIntIntHashMap> childArray,
TIntObjectHashMap<TIntIntHashMap> parentArray,
boolean flip) {
ArrayList<Integer> result = new ArrayList<Integer>();
if (flip) {
// -- check for presence of inverted child array
int lenChild = childArray.size();
// loop over all possible starting positions
for (int i = lenChild - 1, lenParent = parentArray.size(); i < lenParent; i++) {
// loop over child positions
boolean matched = true;
for (int j = 0; j < lenChild; j++) {
// check if contained
if (!contained(childArray.get(j), parentArray.get(i - j))) {
matched = false;
break;
}
}
if (matched) {
result.add(i - lenChild + 1);
}
}
} else {
// -- check for presence of normal child array
int lenChild = childArray.size();
// loop over all possible starting positions
for (int i = 0, len = parentArray.size() - lenChild + 1; i < len; i++) {
// loop over child positions
boolean matched = true;
for (int j = 0; j < lenChild; j++) {
// check if contained
if (!contained(childArray.get(j), parentArray.get(i + j))) {
matched = false;
break;
}
}
if (matched) {
result.add(i);
}
}
}
return result;
}
// check if color list is contained in other color list
private boolean contained(TIntIntHashMap child, TIntIntHashMap parent) {
if (child == null) {
System.err.println("ImageComparator Error 3941");
return true;
}
if (parent == null) {
System.err.println("ImageComparator Error 3942");
return false;
}
for (TIntIntIterator it = child.iterator(); it.hasNext();) {
it.advance();
// check that the colors are contained in the necessary amount
if (parent.get(it.key()) < it.value()) {
return false;
}
}
return true;
}
}