From 161ff0aed734e329bc806098639beaaa7d83d3a0 Mon Sep 17 00:00:00 2001 From: ArrowM Date: Thu, 10 Aug 2023 19:23:14 -0500 Subject: [PATCH 1/7] Add ColorMomentsHash --- .../hashAlgorithms/ColorMomentsHash.java | 314 ++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java diff --git a/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java b/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java new file mode 100644 index 0000000..32d7daf --- /dev/null +++ b/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java @@ -0,0 +1,314 @@ +package interpolator.utils.sorter; + +import dev.brachtendorf.graphics.FastPixel; +import dev.brachtendorf.jimagehash.hashAlgorithms.HashBuilder; +import dev.brachtendorf.jimagehash.hashAlgorithms.HashingAlgorithm; + +import java.awt.image.BufferedImage; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Objects; +import java.util.stream.IntStream; + +public class ColorMomentsHash extends HashingAlgorithm { + private static final long serialVersionUID = -5234612717498362659L; + + /** + * The height and width of the scaled instance used to compute the hash + */ + protected int height, width; + + public ColorMomentsHash(int bitResolution) { + super(bitResolution); + /* + * Figure out how big our resized image has to be in order to create a hash with + * approximately bit resolution bits while trying to stay as squared as possible + * to not introduce bias via stretching or shrinking the image asymmetrically. + */ + computeDimension(bitResolution); + } + + @Override + protected BigInteger hash(BufferedImage image, HashBuilder hash) { + FastPixel fp = createPixelAccessor(image, width, height); + + int[][] hue = getHue(fp); + double[][] sat = getSaturation(fp); + int[][] val = getValue(fp); + + double[] moments = new double[9]; + moments[0] = mean(hue); + moments[1] = mean(sat); + moments[2] = mean(val); + moments[3] = standardDeviation(hue); + moments[4] = standardDeviation(sat); + moments[5] = standardDeviation(val); + moments[6] = skewness(hue); + moments[7] = skewness(sat); + moments[8] = skewness(val); + + for (int i = 0; i < moments.length; i++) { + double moment = moments[i]; + // Weight mean 4x + if (i < 3) { + IntStream.range(0, 3).forEach(j -> computeHash(hash, moment)); + } + computeHash(hash, moment); + } + return hash.toBigInteger(); + } + + public void computeHash(HashBuilder hash, double moment) { + String binary = Long.toBinaryString(Double.doubleToRawLongBits(moment)); + for (char c : binary.toCharArray()) { + if (c == '0') { + hash.prependZero(); + } else { + hash.prependOne(); + } + } + } + + /** + * Compute the dimension for the resize operation. We want to get to close to a + * quadratic images as possible to counteract scaling bias. + * + * @param bitResolution the desired resolution + */ + private void computeDimension(int bitResolution) { + + // Allow for slightly non symmetry to get closer to the true bit resolution + int dimension = (int) Math.round(Math.sqrt(bitResolution)); + + // Lets allow for a +1 or -1 asymmetry and find the most fitting value + int normalBound = (dimension * dimension); + int higherBound = (dimension * (dimension + 1)); + + this.height = dimension; + this.width = dimension; + if (normalBound < bitResolution || (normalBound - bitResolution) > (higherBound - bitResolution)) { + this.width++; + } + } + + @Override + protected int precomputeAlgoId() { + /* + * String and int hashes stays consistent throughout different JVM invocations. + * Algorithm changed between version 1.x.x and 2.x.x ensure algorithms are + * flagged as incompatible. Dimension are what makes average hashes unique + * therefore, even + */ + return Objects.hash("com.github.kilianB.hashAlgorithms."+getClass().getSimpleName(), height, width); + } + + public int[][] getHue(FastPixel fp) { + int[][] blueArr = fp.getBlue(); + int[][] greenArr = fp.getGreen(); + int[][] redArr = fp.getRed(); + + int[][] hueArr = new int[width][height]; + + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + int blue = blueArr[x][y]; + int green = greenArr[x][y]; + int red = redArr[x][y]; + + int min = Math.min(blue, Math.min(green, red)); + int max = Math.max(blue, Math.max(green, red)); + + if (max == min) { + hueArr[x][y] = 0; + continue; + } + + double range = max - min; + + double h; + if (red == max) { + h = 60 * ((green - blue) / range); + } else if (green == max) { + h = 60 * (2 + (blue - red) / range); + } else { + h = 60 * (4 + (red - green) / range); + } + + int hue = (int) Math.round(h); + + if (hue < 0) + hue += 360; + + hueArr[x][y] = hue; + } + } + + return hueArr; + } + + public double[][] getSaturation(FastPixel fp) { + int[][] blueArr = fp.getBlue(); + int[][] greenArr = fp.getGreen(); + int[][] redArr = fp.getRed(); + + double[][] satArr = new double[width][height]; + + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + int blue = blueArr[x][y]; + int green = greenArr[x][y]; + int red = redArr[x][y]; + + int max = Math.max(blue, Math.max(green, red)); + if (max == 0) { + satArr[x][y] = 0; + continue; + } + int min = Math.min(blue, Math.min(green, red)); + + satArr[x][y] = ((max - min) / (double) max); + } + } + + return satArr; + } + + public int[][] getValue(FastPixel fp) { + int[][] blueArr = fp.getBlue(); + int[][] greenArr = fp.getGreen(); + int[][] redArr = fp.getRed(); + + int[][] valArr = new int[width][height]; + + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + int blue = blueArr[x][y]; + int green = greenArr[x][y]; + int red = redArr[x][y]; + + int max = Math.max(blue, Math.max(green, red)); + + valArr[x][y] = max; + } + } + + return valArr; + } + + public static double skewness(final double[][] arr) { + double[] flattened = Arrays.stream(arr) + .flatMapToDouble(Arrays::stream) + .toArray(); + int length = flattened.length; + + // Initialize the skewness + double skew = Double.NaN; + // Get the mean and the standard deviation + double m = Arrays.stream(flattened).average().getAsDouble(); + + // Calc the std, this is implemented here instead + // of using the standardDeviation method eliminate + // a duplicate pass to get the mean + double accum = 0.0; + double accum2 = 0.0; + for (int i = 0; i < length; i++) { + final double d = flattened[i] - m; + accum += d * d; + accum2 += d; + } + final double variance = (accum - (accum2 * accum2 / length)) / (length - 1); + + double accum3 = 0.0; + for (int i = 0; i < length; i++) { + final double d = flattened[i] - m; + accum3 += d * d * d; + } + accum3 /= variance * Math.sqrt(variance); + + // Get N + double n0 = length; + + // Calculate skewness + return (n0 / ((n0 - 1) * (n0 - 2))) * accum3; + } + + public static double skewness(final int[][] arr) { + int[] flattened = Arrays.stream(arr) + .flatMapToInt(Arrays::stream) + .toArray(); + int length = flattened.length; + + // Initialize the skewness + double skew = Double.NaN; + // Get the mean and the standard deviation + double m = Arrays.stream(flattened).average().getAsDouble(); + + // Calc the std, this is implemented here instead + // of using the standardDeviation method eliminate + // a duplicate pass to get the mean + double accum = 0.0; + double accum2 = 0.0; + for (int i = 0; i < length; i++) { + final double d = flattened[i] - m; + accum += d * d; + accum2 += d; + } + final double variance = (accum - (accum2 * accum2 / length)) / (length - 1); + + double accum3 = 0.0; + for (int i = 0; i < length; i++) { + final double d = flattened[i] - m; + accum3 += d * d * d; + } + accum3 /= variance * Math.sqrt(variance); + + // Get N + double n0 = length; + + // Calculate skewness + return (n0 / ((n0 - 1) * (n0 - 2))) * accum3; + } + + public double standardDeviation(double[][] arr) { + double sum = 0; + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + sum += arr[i][j]; + } + } + double mean = sum / (width * height); + double sd = 0; + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + sd += Math.pow(arr[i][j] - mean, 2); + } + } + return Math.sqrt(sd / (width * height)); + } + + public double standardDeviation(int[][] arr) { + int sum = 0; + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + sum += arr[i][j]; + } + } + double mean = sum / (width * height); + double sd = 0; + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + sd += Math.pow(arr[i][j] - mean, 2); + } + } + return Math.sqrt(sd / (width * height)); + } + + public double mean(double[][] arr) { + return Arrays.stream(arr).flatMapToDouble(Arrays::stream).average().getAsDouble(); + } + + public double mean(int[][] arr) { + return Arrays.stream(arr).flatMapToInt(Arrays::stream).average().getAsDouble(); + } +} From 7963a48b15cdaeade4a5e6857d93fc73f8a2a1b8 Mon Sep 17 00:00:00 2001 From: ArrowM Date: Thu, 10 Aug 2023 20:30:03 -0500 Subject: [PATCH 2/7] Improve performance --- .../hashAlgorithms/ColorMomentsHash.java | 68 ++++++++----------- 1 file changed, 28 insertions(+), 40 deletions(-) diff --git a/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java b/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java index 32d7daf..a478847 100644 --- a/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java +++ b/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java @@ -41,12 +41,12 @@ protected BigInteger hash(BufferedImage image, HashBuilder hash) { moments[0] = mean(hue); moments[1] = mean(sat); moments[2] = mean(val); - moments[3] = standardDeviation(hue); - moments[4] = standardDeviation(sat); - moments[5] = standardDeviation(val); - moments[6] = skewness(hue); - moments[7] = skewness(sat); - moments[8] = skewness(val); + moments[3] = standardDeviation(hue, moments[0]); + moments[4] = standardDeviation(sat, moments[1]); + moments[5] = standardDeviation(val, moments[2]); + moments[6] = skewness(hue, moments[0]); + moments[7] = skewness(sat, moments[1]); + moments[8] = skewness(val, moments[2]); for (int i = 0; i < moments.length; i++) { double moment = moments[i]; @@ -196,7 +196,7 @@ public int[][] getValue(FastPixel fp) { return valArr; } - public static double skewness(final double[][] arr) { + public double skewness(final double[][] arr, double mean) { double[] flattened = Arrays.stream(arr) .flatMapToDouble(Arrays::stream) .toArray(); @@ -204,16 +204,11 @@ public static double skewness(final double[][] arr) { // Initialize the skewness double skew = Double.NaN; - // Get the mean and the standard deviation - double m = Arrays.stream(flattened).average().getAsDouble(); - // Calc the std, this is implemented here instead - // of using the standardDeviation method eliminate - // a duplicate pass to get the mean double accum = 0.0; double accum2 = 0.0; for (int i = 0; i < length; i++) { - final double d = flattened[i] - m; + final double d = flattened[i] - mean; accum += d * d; accum2 += d; } @@ -221,7 +216,7 @@ public static double skewness(final double[][] arr) { double accum3 = 0.0; for (int i = 0; i < length; i++) { - final double d = flattened[i] - m; + final double d = flattened[i] - mean; accum3 += d * d * d; } accum3 /= variance * Math.sqrt(variance); @@ -233,7 +228,7 @@ public static double skewness(final double[][] arr) { return (n0 / ((n0 - 1) * (n0 - 2))) * accum3; } - public static double skewness(final int[][] arr) { + public double skewness(final int[][] arr, double mean) { int[] flattened = Arrays.stream(arr) .flatMapToInt(Arrays::stream) .toArray(); @@ -241,16 +236,11 @@ public static double skewness(final int[][] arr) { // Initialize the skewness double skew = Double.NaN; - // Get the mean and the standard deviation - double m = Arrays.stream(flattened).average().getAsDouble(); - // Calc the std, this is implemented here instead - // of using the standardDeviation method eliminate - // a duplicate pass to get the mean double accum = 0.0; double accum2 = 0.0; for (int i = 0; i < length; i++) { - final double d = flattened[i] - m; + final double d = flattened[i] - mean; accum += d * d; accum2 += d; } @@ -258,7 +248,7 @@ public static double skewness(final int[][] arr) { double accum3 = 0.0; for (int i = 0; i < length; i++) { - final double d = flattened[i] - m; + final double d = flattened[i] - mean; accum3 += d * d * d; } accum3 /= variance * Math.sqrt(variance); @@ -270,14 +260,7 @@ public static double skewness(final int[][] arr) { return (n0 / ((n0 - 1) * (n0 - 2))) * accum3; } - public double standardDeviation(double[][] arr) { - double sum = 0; - for (int i = 0; i < width; i++) { - for (int j = 0; j < height; j++) { - sum += arr[i][j]; - } - } - double mean = sum / (width * height); + public double standardDeviation(double[][] arr, double mean) { double sd = 0; for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { @@ -287,14 +270,7 @@ public double standardDeviation(double[][] arr) { return Math.sqrt(sd / (width * height)); } - public double standardDeviation(int[][] arr) { - int sum = 0; - for (int i = 0; i < width; i++) { - for (int j = 0; j < height; j++) { - sum += arr[i][j]; - } - } - double mean = sum / (width * height); + public double standardDeviation(int[][] arr, double mean) { double sd = 0; for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { @@ -305,10 +281,22 @@ public double standardDeviation(int[][] arr) { } public double mean(double[][] arr) { - return Arrays.stream(arr).flatMapToDouble(Arrays::stream).average().getAsDouble(); + double sum = 0; + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + sum += arr[i][j]; + } + } + return sum / (width * height); } public double mean(int[][] arr) { - return Arrays.stream(arr).flatMapToInt(Arrays::stream).average().getAsDouble(); + int sum = 0; + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + sum += arr[i][j]; + } + } + return sum / (width * height); } } From 17c63a561b44c802f917084b79dc3733c2dffd74 Mon Sep 17 00:00:00 2001 From: ArrowM Date: Thu, 10 Aug 2023 20:45:31 -0500 Subject: [PATCH 3/7] Improve performance --- .../hashAlgorithms/ColorMomentsHash.java | 54 ++++++++----------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java b/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java index a478847..57ffa75 100644 --- a/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java +++ b/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java @@ -52,7 +52,9 @@ protected BigInteger hash(BufferedImage image, HashBuilder hash) { double moment = moments[i]; // Weight mean 4x if (i < 3) { - IntStream.range(0, 3).forEach(j -> computeHash(hash, moment)); + for (int j = 0; j < 0; j++) { + computeHash(hash, moment); + } } computeHash(hash, moment); } @@ -197,28 +199,23 @@ public int[][] getValue(FastPixel fp) { } public double skewness(final double[][] arr, double mean) { - double[] flattened = Arrays.stream(arr) - .flatMapToDouble(Arrays::stream) - .toArray(); - int length = flattened.length; + int length = width * height; // Initialize the skewness double skew = Double.NaN; - double accum = 0.0; + double accum1 = 0.0; double accum2 = 0.0; - for (int i = 0; i < length; i++) { - final double d = flattened[i] - mean; - accum += d * d; - accum2 += d; - } - final double variance = (accum - (accum2 * accum2 / length)) / (length - 1); - double accum3 = 0.0; - for (int i = 0; i < length; i++) { - final double d = flattened[i] - mean; - accum3 += d * d * d; + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + final double d = arr[i][j] - mean; + accum1 += d; + accum2 += d * d; + accum3 += d * d * d; + } } + final double variance = (accum2 - (accum1 * accum1 / length)) / (length - 1); accum3 /= variance * Math.sqrt(variance); // Get N @@ -229,28 +226,23 @@ public double skewness(final double[][] arr, double mean) { } public double skewness(final int[][] arr, double mean) { - int[] flattened = Arrays.stream(arr) - .flatMapToInt(Arrays::stream) - .toArray(); - int length = flattened.length; + int length = width * height; // Initialize the skewness double skew = Double.NaN; - double accum = 0.0; + double accum1 = 0.0; double accum2 = 0.0; - for (int i = 0; i < length; i++) { - final double d = flattened[i] - mean; - accum += d * d; - accum2 += d; - } - final double variance = (accum - (accum2 * accum2 / length)) / (length - 1); - double accum3 = 0.0; - for (int i = 0; i < length; i++) { - final double d = flattened[i] - mean; - accum3 += d * d * d; + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + final double d = arr[i][j] - mean; + accum1 += d; + accum2 += d * d; + accum3 += d * d * d; + } } + final double variance = (accum2 - (accum1 * accum1 / length)) / (length - 1); accum3 /= variance * Math.sqrt(variance); // Get N From e8e1dcb67e7cc02022d16ec44b7b4e6d7073520d Mon Sep 17 00:00:00 2001 From: ArrowM Date: Thu, 10 Aug 2023 20:51:18 -0500 Subject: [PATCH 4/7] Fix previous commit --- .../jimagehash/hashAlgorithms/ColorMomentsHash.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java b/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java index 57ffa75..2a960a5 100644 --- a/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java +++ b/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java @@ -48,15 +48,15 @@ protected BigInteger hash(BufferedImage image, HashBuilder hash) { moments[7] = skewness(sat, moments[1]); moments[8] = skewness(val, moments[2]); + for (double moment : moments) { + computeHash(hash, moment); + } + // Weight mean 4x for (int i = 0; i < moments.length; i++) { double moment = moments[i]; - // Weight mean 4x - if (i < 3) { - for (int j = 0; j < 0; j++) { - computeHash(hash, moment); - } + for (int j = 0; j < 3; j++) { + computeHash(hash, moment); } - computeHash(hash, moment); } return hash.toBigInteger(); } From aea1581ffcd07b03d69df6c8c5a15c5d72dedd3d Mon Sep 17 00:00:00 2001 From: ArrowM Date: Thu, 10 Aug 2023 22:52:59 -0500 Subject: [PATCH 5/7] More tweaks --- .../hashAlgorithms/ColorMomentsHash.java | 554 +++++++++--------- 1 file changed, 272 insertions(+), 282 deletions(-) diff --git a/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java b/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java index 2a960a5..f4a9118 100644 --- a/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java +++ b/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java @@ -6,289 +6,279 @@ import java.awt.image.BufferedImage; import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.util.Arrays; import java.util.Objects; -import java.util.stream.IntStream; public class ColorMomentsHash extends HashingAlgorithm { - private static final long serialVersionUID = -5234612717498362659L; - - /** - * The height and width of the scaled instance used to compute the hash - */ - protected int height, width; - - public ColorMomentsHash(int bitResolution) { - super(bitResolution); - /* - * Figure out how big our resized image has to be in order to create a hash with - * approximately bit resolution bits while trying to stay as squared as possible - * to not introduce bias via stretching or shrinking the image asymmetrically. - */ - computeDimension(bitResolution); - } - - @Override - protected BigInteger hash(BufferedImage image, HashBuilder hash) { - FastPixel fp = createPixelAccessor(image, width, height); - - int[][] hue = getHue(fp); - double[][] sat = getSaturation(fp); - int[][] val = getValue(fp); - - double[] moments = new double[9]; - moments[0] = mean(hue); - moments[1] = mean(sat); - moments[2] = mean(val); - moments[3] = standardDeviation(hue, moments[0]); - moments[4] = standardDeviation(sat, moments[1]); - moments[5] = standardDeviation(val, moments[2]); - moments[6] = skewness(hue, moments[0]); - moments[7] = skewness(sat, moments[1]); - moments[8] = skewness(val, moments[2]); - - for (double moment : moments) { - computeHash(hash, moment); - } - // Weight mean 4x - for (int i = 0; i < moments.length; i++) { - double moment = moments[i]; - for (int j = 0; j < 3; j++) { - computeHash(hash, moment); - } - } - return hash.toBigInteger(); - } - - public void computeHash(HashBuilder hash, double moment) { - String binary = Long.toBinaryString(Double.doubleToRawLongBits(moment)); - for (char c : binary.toCharArray()) { - if (c == '0') { - hash.prependZero(); - } else { - hash.prependOne(); - } - } - } - - /** - * Compute the dimension for the resize operation. We want to get to close to a - * quadratic images as possible to counteract scaling bias. - * - * @param bitResolution the desired resolution - */ - private void computeDimension(int bitResolution) { - - // Allow for slightly non symmetry to get closer to the true bit resolution - int dimension = (int) Math.round(Math.sqrt(bitResolution)); - - // Lets allow for a +1 or -1 asymmetry and find the most fitting value - int normalBound = (dimension * dimension); - int higherBound = (dimension * (dimension + 1)); - - this.height = dimension; - this.width = dimension; - if (normalBound < bitResolution || (normalBound - bitResolution) > (higherBound - bitResolution)) { - this.width++; - } - } - - @Override - protected int precomputeAlgoId() { - /* - * String and int hashes stays consistent throughout different JVM invocations. - * Algorithm changed between version 1.x.x and 2.x.x ensure algorithms are - * flagged as incompatible. Dimension are what makes average hashes unique - * therefore, even - */ - return Objects.hash("com.github.kilianB.hashAlgorithms."+getClass().getSimpleName(), height, width); - } - - public int[][] getHue(FastPixel fp) { - int[][] blueArr = fp.getBlue(); - int[][] greenArr = fp.getGreen(); - int[][] redArr = fp.getRed(); - - int[][] hueArr = new int[width][height]; - - for (int x = 0; x < width; x++) { - for (int y = 0; y < height; y++) { - int blue = blueArr[x][y]; - int green = greenArr[x][y]; - int red = redArr[x][y]; - - int min = Math.min(blue, Math.min(green, red)); - int max = Math.max(blue, Math.max(green, red)); - - if (max == min) { - hueArr[x][y] = 0; - continue; - } - - double range = max - min; - - double h; - if (red == max) { - h = 60 * ((green - blue) / range); - } else if (green == max) { - h = 60 * (2 + (blue - red) / range); - } else { - h = 60 * (4 + (red - green) / range); - } - - int hue = (int) Math.round(h); - - if (hue < 0) - hue += 360; - - hueArr[x][y] = hue; - } - } - - return hueArr; - } - - public double[][] getSaturation(FastPixel fp) { - int[][] blueArr = fp.getBlue(); - int[][] greenArr = fp.getGreen(); - int[][] redArr = fp.getRed(); - - double[][] satArr = new double[width][height]; - - for (int x = 0; x < width; x++) { - for (int y = 0; y < height; y++) { - int blue = blueArr[x][y]; - int green = greenArr[x][y]; - int red = redArr[x][y]; - - int max = Math.max(blue, Math.max(green, red)); - if (max == 0) { - satArr[x][y] = 0; - continue; - } - int min = Math.min(blue, Math.min(green, red)); - - satArr[x][y] = ((max - min) / (double) max); - } - } - - return satArr; - } - - public int[][] getValue(FastPixel fp) { - int[][] blueArr = fp.getBlue(); - int[][] greenArr = fp.getGreen(); - int[][] redArr = fp.getRed(); - - int[][] valArr = new int[width][height]; - - for (int x = 0; x < width; x++) { - for (int y = 0; y < height; y++) { - int blue = blueArr[x][y]; - int green = greenArr[x][y]; - int red = redArr[x][y]; - - int max = Math.max(blue, Math.max(green, red)); - - valArr[x][y] = max; - } - } - - return valArr; - } - - public double skewness(final double[][] arr, double mean) { - int length = width * height; - - // Initialize the skewness - double skew = Double.NaN; - - double accum1 = 0.0; - double accum2 = 0.0; - double accum3 = 0.0; - for (int i = 0; i < width; i++) { - for (int j = 0; j < height; j++) { - final double d = arr[i][j] - mean; - accum1 += d; - accum2 += d * d; - accum3 += d * d * d; - } - } - final double variance = (accum2 - (accum1 * accum1 / length)) / (length - 1); - accum3 /= variance * Math.sqrt(variance); - - // Get N - double n0 = length; - - // Calculate skewness - return (n0 / ((n0 - 1) * (n0 - 2))) * accum3; - } - - public double skewness(final int[][] arr, double mean) { - int length = width * height; - - // Initialize the skewness - double skew = Double.NaN; - - double accum1 = 0.0; - double accum2 = 0.0; - double accum3 = 0.0; - for (int i = 0; i < width; i++) { - for (int j = 0; j < height; j++) { - final double d = arr[i][j] - mean; - accum1 += d; - accum2 += d * d; - accum3 += d * d * d; - } - } - final double variance = (accum2 - (accum1 * accum1 / length)) / (length - 1); - accum3 /= variance * Math.sqrt(variance); - - // Get N - double n0 = length; - - // Calculate skewness - return (n0 / ((n0 - 1) * (n0 - 2))) * accum3; - } - - public double standardDeviation(double[][] arr, double mean) { - double sd = 0; - for (int i = 0; i < width; i++) { - for (int j = 0; j < height; j++) { - sd += Math.pow(arr[i][j] - mean, 2); - } - } - return Math.sqrt(sd / (width * height)); - } - - public double standardDeviation(int[][] arr, double mean) { - double sd = 0; - for (int i = 0; i < width; i++) { - for (int j = 0; j < height; j++) { - sd += Math.pow(arr[i][j] - mean, 2); - } - } - return Math.sqrt(sd / (width * height)); - } - - public double mean(double[][] arr) { - double sum = 0; - for (int i = 0; i < width; i++) { - for (int j = 0; j < height; j++) { - sum += arr[i][j]; - } - } - return sum / (width * height); - } - - public double mean(int[][] arr) { - int sum = 0; - for (int i = 0; i < width; i++) { - for (int j = 0; j < height; j++) { - sum += arr[i][j]; - } - } - return sum / (width * height); - } + private static final long serialVersionUID = -5234612717498362659L; + + /** + * The height and width of the scaled instance used to compute the hash + */ + protected int height, width, area; + + public ColorMomentsHash(int bitResolution) { + super(bitResolution); + /* + * Figure out how big our resized image has to be in order to create a hash with + * approximately bit resolution bits while trying to stay as squared as possible + * to not introduce bias via stretching or shrinking the image asymmetrically. + */ + computeDimension(bitResolution); + } + + @Override + protected BigInteger hash(BufferedImage image, HashBuilder hash) { + FastPixel fp = createPixelAccessor(image, width, height); + + int[][] hue = getHue(fp); + double[][] sat = getSaturation(fp); + int[][] val = getValue(fp); + + double[] meanMoments = new double[]{ + mean(hue), + mean(sat), + mean(val) + }; + double[] stdDevMoments = new double[]{ + standardDeviation(hue, meanMoments[0]), + standardDeviation(sat, meanMoments[1]), + standardDeviation(val, meanMoments[2]) + }; + double[] skewnessMoments = new double[]{ + skewness(hue, meanMoments[0]), + skewness(sat, meanMoments[1]), + skewness(val, meanMoments[2]) + }; + + computeHash(hash, 4, meanMoments); + computeHash(hash, 2, stdDevMoments); + computeHash(hash, 1, skewnessMoments); + + return hash.toBigInteger(); + } + + public void computeHash(HashBuilder hash, int weight, double[] moments) { + for (double moment: moments) { + String binary = Long.toBinaryString(Double.doubleToRawLongBits(moment)) + .substring(24) + .repeat(weight); + for (char c : binary.toCharArray()) { + if (c == '0') + hash.prependZero(); + else + hash.prependOne(); + } + } + } + + /** + * Compute the dimension for the resize operation. We want to get to close to a + * quadratic images as possible to counteract scaling bias. + * + * @param bitResolution the desired resolution + */ + private void computeDimension(int bitResolution) { + + // Allow for slightly non symmetry to get closer to the true bit resolution + int dimension = (int) Math.round(Math.sqrt(bitResolution)); + + // Lets allow for a +1 or -1 asymmetry and find the most fitting value + int normalBound = (dimension * dimension); + int higherBound = (dimension * (dimension + 1)); + + this.height = dimension; + this.width = dimension; + if (normalBound < bitResolution || (normalBound - bitResolution) > (higherBound - bitResolution)) { + this.width++; + } + this.area = this.height * this.width; + } + + @Override + protected int precomputeAlgoId() { + /* + * String and int hashes stays consistent throughout different JVM invocations. + * Algorithm changed between version 1.x.x and 2.x.x ensure algorithms are + * flagged as incompatible. Dimension are what makes average hashes unique + * therefore, even + */ + return Objects.hash("com.github.kilianB.hashAlgorithms."+getClass().getSimpleName(), height, width); + } + + public int[][] getHue(FastPixel fp) { + int[][] blueArr = fp.getBlue(); + int[][] greenArr = fp.getGreen(); + int[][] redArr = fp.getRed(); + + int[][] hueArr = new int[width][height]; + + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + int blue = blueArr[x][y]; + int green = greenArr[x][y]; + int red = redArr[x][y]; + + int min = Math.min(blue, Math.min(green, red)); + int max = Math.max(blue, Math.max(green, red)); + + if (max == min) { + hueArr[x][y] = 0; + continue; + } + + double range = max - min; + + double h; + if (red == max) { + h = 60 * ((green - blue) / range); + } else if (green == max) { + h = 60 * (2 + (blue - red) / range); + } else { + h = 60 * (4 + (red - green) / range); + } + + int hue = (int) Math.round(h); + + if (hue < 0) + hue += 360; + + hueArr[x][y] = hue; + } + } + + return hueArr; + } + + public double[][] getSaturation(FastPixel fp) { + int[][] blueArr = fp.getBlue(); + int[][] greenArr = fp.getGreen(); + int[][] redArr = fp.getRed(); + + double[][] satArr = new double[width][height]; + + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + int blue = blueArr[x][y]; + int green = greenArr[x][y]; + int red = redArr[x][y]; + + int max = Math.max(blue, Math.max(green, red)); + if (max == 0) { + satArr[x][y] = 0; + continue; + } + int min = Math.min(blue, Math.min(green, red)); + + satArr[x][y] = ((max - min) / (double) max); + } + } + + return satArr; + } + + public int[][] getValue(FastPixel fp) { + int[][] blueArr = fp.getBlue(); + int[][] greenArr = fp.getGreen(); + int[][] redArr = fp.getRed(); + + int[][] valArr = new int[width][height]; + + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + int blue = blueArr[x][y]; + int green = greenArr[x][y]; + int red = redArr[x][y]; + + int max = Math.max(blue, Math.max(green, red)); + + valArr[x][y] = max; + } + } + + return valArr; + } + + public double skewness(final double[][] arr, final double mean) { + double accum1 = 0.0; + double accum2 = 0.0; + double accum3 = 0.0; + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + final double d = arr[i][j] - mean; + accum1 += d; + accum2 += d * d; + accum3 += d * d * d; + } + } + final double variance = (accum2 - (accum1 * accum1 / area)) / (area - 1); + accum3 /= variance * Math.sqrt(variance); + + // Get N + double n0 = area; + + // Calculate skewness + return (n0 / ((n0 - 1) * (n0 - 2))) * accum3; + } + + public double skewness(final int[][] arr, final double mean) { + double accum1 = 0.0; + double accum2 = 0.0; + double accum3 = 0.0; + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + final double d = arr[i][j] - mean; + accum1 += d; + accum2 += d * d; + accum3 += d * d * d; + } + } + final double variance = (accum2 - (accum1 * accum1 / area)) / (area - 1); + accum3 /= variance * Math.sqrt(variance); + + // Get N + double n0 = area; + + // Calculate skewness + return (n0 / ((n0 - 1) * (n0 - 2))) * accum3; + } + + public double standardDeviation(final double[][] arr, final double mean) { + double stdDev = 0; + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + stdDev += Math.pow(arr[i][j] - mean, 2); + } + } + return Math.sqrt(stdDev / area); + } + + public double standardDeviation(final int[][] arr, final double mean) { + double stdDev = 0; + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + stdDev += Math.pow(arr[i][j] - mean, 2); + } + } + return Math.sqrt(stdDev / area); + } + + public double mean(final double[][] arr) { + double sum = 0; + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + sum += arr[i][j]; + } + } + return sum / area; + } + + public double mean(final int[][] arr) { + int sum = 0; + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + sum += arr[i][j]; + } + } + return sum / area; + } } From df4b5caff788627d2c10cdf13d8dc98f5ba57102 Mon Sep 17 00:00:00 2001 From: ArrowM Date: Thu, 10 Aug 2023 23:25:21 -0500 Subject: [PATCH 6/7] More tweaks --- .../jimagehash/hashAlgorithms/ColorMomentsHash.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java b/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java index f4a9118..427ccd3 100644 --- a/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java +++ b/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java @@ -60,7 +60,7 @@ protected BigInteger hash(BufferedImage image, HashBuilder hash) { public void computeHash(HashBuilder hash, int weight, double[] moments) { for (double moment: moments) { String binary = Long.toBinaryString(Double.doubleToRawLongBits(moment)) - .substring(24) + .substring(0, 32) .repeat(weight); for (char c : binary.toCharArray()) { if (c == '0') From 9db52c88689ff5fced8e76e2d8607b7b75444220 Mon Sep 17 00:00:00 2001 From: ArrowM Date: Thu, 17 Aug 2023 15:46:48 -0500 Subject: [PATCH 7/7] More tweaks --- .../jimagehash/hash/ColorMoments.java | 68 +++++++++++++++++++ .../hashAlgorithms/ColorMomentsHash.java | 45 ++++++++++-- 2 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 src/main/java/dev/brachtendorf/jimagehash/hash/ColorMoments.java diff --git a/src/main/java/dev/brachtendorf/jimagehash/hash/ColorMoments.java b/src/main/java/dev/brachtendorf/jimagehash/hash/ColorMoments.java new file mode 100644 index 0000000..7559ece --- /dev/null +++ b/src/main/java/dev/brachtendorf/jimagehash/hash/ColorMoments.java @@ -0,0 +1,68 @@ +package dev.brachtendorf.jimagehash.hash; + +import java.util.Objects; + +/** + * A color moment is an aggregation of mean, standard deviation, and skewness moments. + */ +public final class ColorMoments { + private final double[] meanMoments; + private final double[] stdDevMoments; + private final double[] skewnessMoments; + + public ColorMoments( + double[] meanMoments, + double[] stdDevMoments, + double[] skewnessMoments + ) { + this.meanMoments = meanMoments; + this.stdDevMoments = stdDevMoments; + this.skewnessMoments = skewnessMoments; + } + + public double distance(ColorMoments other) { + double distance = 0; + for (int i = 0; i < 3; i++) { + double meanDiff = Math.abs(meanMoments[i] - other.meanMoments[i]); + double stdDevDiff = Math.abs(stdDevMoments[i] - other.stdDevMoments[i]); + double skewnessDiff = Math.abs(skewnessMoments[i] - other.skewnessMoments[i]); + distance += meanDiff + stdDevDiff + skewnessDiff; + } + return distance; + } + + public double[] meanMoments() { + return meanMoments; + } + + public double[] stdDevMoments() { + return stdDevMoments; + } + + public double[] skewnessMoments() { + return skewnessMoments; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (ColorMoments) obj; + return Objects.equals(this.meanMoments, that.meanMoments) && + Objects.equals(this.stdDevMoments, that.stdDevMoments) && + Objects.equals(this.skewnessMoments, that.skewnessMoments); + } + + @Override + public int hashCode() { + return Objects.hash(meanMoments, stdDevMoments, skewnessMoments); + } + + @Override + public String toString() { + return "MomentHash[" + + "meanMoments=" + meanMoments + ", " + + "stdDevMoments=" + stdDevMoments + ", " + + "skewnessMoments=" + skewnessMoments + ']'; + } +} \ No newline at end of file diff --git a/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java b/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java index 427ccd3..5c1fd71 100644 --- a/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java +++ b/src/main/java/dev/brachtendorf/jimagehash/hashAlgorithms/ColorMomentsHash.java @@ -1,14 +1,19 @@ package interpolator.utils.sorter; import dev.brachtendorf.graphics.FastPixel; +import dev.brachtendorf.jimagehash.hash.ColorMoments; import dev.brachtendorf.jimagehash.hashAlgorithms.HashBuilder; import dev.brachtendorf.jimagehash.hashAlgorithms.HashingAlgorithm; +import javax.imageio.ImageIO; import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; import java.math.BigInteger; import java.util.Objects; public class ColorMomentsHash extends HashingAlgorithm { + private static final long serialVersionUID = -5234612717498362659L; /** @@ -20,12 +25,44 @@ public ColorMomentsHash(int bitResolution) { super(bitResolution); /* * Figure out how big our resized image has to be in order to create a hash with - * approximately bit resolution bits while trying to stay as squared as possible + * approximately bitResolution bits while trying to stay as squared as possible * to not introduce bias via stretching or shrinking the image asymmetrically. */ computeDimension(bitResolution); } + + public ColorMoments hash2(File imageFile) throws IOException { + return hash2(imageFile, new int[]{1, 2, 3}); + } + + public ColorMoments hash2(File imageFile, int[] weights) throws IOException { + BufferedImage image = ImageIO.read(imageFile); + FastPixel fp = createPixelAccessor(image, width, height); + + int[][] hue = getHue(fp); + double[][] sat = getSaturation(fp); + int[][] val = getValue(fp); + + double[] meanMoments = new double[]{ + weights[0] * mean(hue), + weights[0] * mean(sat), + weights[0] * mean(val) + }; + double[] stdDevMoments = new double[]{ + weights[1] * standardDeviation(hue, meanMoments[0]), + weights[1] * standardDeviation(sat, meanMoments[1]), + weights[1] * standardDeviation(val, meanMoments[2]) + }; + double[] skewnessMoments = new double[]{ + weights[2] * skewness(hue, meanMoments[0]), + weights[2] * skewness(sat, meanMoments[1]), + weights[2] * skewness(val, meanMoments[2]) + }; + + return new ColorMoments(meanMoments, stdDevMoments, skewnessMoments); + } + @Override protected BigInteger hash(BufferedImage image, HashBuilder hash) { FastPixel fp = createPixelAccessor(image, width, height); @@ -50,9 +87,9 @@ protected BigInteger hash(BufferedImage image, HashBuilder hash) { skewness(val, meanMoments[2]) }; - computeHash(hash, 4, meanMoments); - computeHash(hash, 2, stdDevMoments); - computeHash(hash, 1, skewnessMoments); + computeHash(hash, 3, meanMoments); + computeHash(hash, 1, stdDevMoments); + computeHash(hash, 0, skewnessMoments); return hash.toBigInteger(); }