Skip to content

Instantly share code, notes, and snippets.

@kuFEAR
Created September 2, 2015 18:07
Show Gist options
  • Save kuFEAR/6e20342198d4040e0bb5 to your computer and use it in GitHub Desktop.
Save kuFEAR/6e20342198d4040e0bb5 to your computer and use it in GitHub Desktop.
Android image phash comparing
package com.example.app;
import android.graphics.*;
import android.util.Log;
/*
* pHash-like image hash.
* Author: Elliot Shepherd ([email protected]
* Based On: http://www.hackerfactor.com/blog/index.php?/archives/432-Looks-Like-It.html
*/
public class ImagePHash {
private static final String TAG = "ImagePHASH";
private int size = 32;
private int smallerSize = 8;
public ImagePHash() {
initCoefficients();
}
public ImagePHash(int size, int smallerSize) {
this.size = size;
this.smallerSize = smallerSize;
initCoefficients();
}
public int distance(String s1, String s2) {
if (s1 != null && s2 != null) {
if (s1.length() == s2.length() && s1.length() != 0 && s2.length() != 0) {
int counter = 0;
for (int k = 0; k < s1.length(); k++) {
if (s1.charAt(k) != s2.charAt(k)) {
counter++;
}
}
Log.d(TAG, "Distance: " + counter + " from " + s1.length());
return counter;
} else {
Log.d(TAG, "Length of strings not equal: s1 = " + s1.length() + " and s2 = " + s2.length() + " or smaller then 0");
return -1;
}
}
return -1;
}
// Returns a 'binary string' (like. 001010111011100010) which is easy to do a hamming distance on.
public String culcPHash(Bitmap img) {
/* 1. Reduce size.
* Like Average Hash, pHash starts with a small image.
* However, the image is larger than 8x8; 32x32 is a good size.
* This is really done to simplify the DCT computation and not
* because it is needed to reduce the high frequencies.
*/
img = resize(img, size, size);
/* 2. Reduce color.
* The image is reduced to a grayscale just to further simplify
* the number of computations.
*/
String hash = "";
if (img != null) {
img = grayscale(img);
double[][] vals = new double[size][size];
for (int x = 0; x < img.getWidth(); x++) {
for (int y = 0; y < img.getHeight(); y++) {
vals[x][y] = getBlue(img, x, y);
}
}
/* 3. Compute the DCT.
* The DCT separates the image into a collection of frequencies
* and scalars. While JPEG uses an 8x8 DCT, this algorithm uses
* a 32x32 DCT.
*/
long start = System.currentTimeMillis();
double[][] dctVals = applyDCT(vals);
Log.d(TAG, String.valueOf((System.currentTimeMillis() - start)));
/* 4. Reduce the DCT.
* This is the magic step. While the DCT is 32x32, just keep the
* top-left 8x8. Those represent the lowest frequencies in the
* picture.
*/
/* 5. Compute the average value.
* Like the Average Hash, compute the mean DCT value (using only
* the 8x8 DCT low-frequency values and excluding the first term
* since the DC coefficient can be significantly different from
* the other values and will throw off the average).
*/
double total = 0;
for (int x = 0; x < smallerSize; x++) {
for (int y = 0; y < smallerSize; y++) {
total += dctVals[x][y];
}
}
total -= dctVals[0][0];
double avg = total / (double) ((smallerSize * smallerSize) - 1);
/* 6. Further reduce the DCT.
* This is the magic step. Set the 64 hash bits to 0 or 1
* depending on whether each of the 64 DCT values is above or
* below the average value. The result doesn't tell us the
* actual low frequencies; it just tells us the very-rough
* relative scale of the frequencies to the mean. The result
* will not vary as long as the overall structure of the image
* remains the same; this can survive gamma and color histogram
* adjustments without a problem.
*/
for (int x = 0; x < smallerSize; x++) {
for (int y = 0; y < smallerSize; y++) {
if (x != 0 && y != 0) {
hash += (dctVals[x][y] > avg ? "1" : "0");
}
}
}
Log.d(TAG, "HASH result: " + hash);
} else {
return null;
}
return hash;
}
public Bitmap resize(Bitmap bm, int newHeight, int newWidth) {
// "RECREATE" THE NEW BITMAP
Bitmap resizedBitmap = null;
try {
resizedBitmap = Bitmap.createScaledBitmap(bm, newWidth, newHeight, false);
} catch (NullPointerException e) {
e.printStackTrace();
}
return resizedBitmap;
}
private Bitmap grayscale(Bitmap orginalBitmap) {
ColorMatrix colorMatrix = new ColorMatrix();
colorMatrix.setSaturation(0);
ColorMatrixColorFilter colorMatrixFilter = new ColorMatrixColorFilter(colorMatrix);
Bitmap blackAndWhiteBitmap = orginalBitmap.copy(Bitmap.Config.ARGB_8888, true);
Paint paint = new Paint();
paint.setColorFilter(colorMatrixFilter);
Canvas canvas = new Canvas(blackAndWhiteBitmap);
canvas.drawBitmap(blackAndWhiteBitmap, 0, 0, paint);
return blackAndWhiteBitmap;
}
private static int getBlue(Bitmap img, int x, int y) {
return (img.getPixel(x, y)) & 0xff;
}
// DCT function stolen from http://stackoverflow.com/questions/4240490/problems-with-dct-and-idct-algorithm-in-java
private double[] c;
private void initCoefficients() {
c = new double[size];
for (int i = 1; i < size; i++) {
c[i] = 1;
}
c[0] = 1 / Math.sqrt(2.0);
}
private double[][] applyDCT(double[][] f) {
int N = size;
double[][] F = new double[N][N];
for (int u = 0; u < N; u++) {
for (int v = 0; v < N; v++) {
double sum = 0.0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
sum += Math.cos(((2 * i + 1) / (2.0 * N)) * u * Math.PI) * Math.cos(((2 * j + 1) / (2.0 * N)) * v * Math.PI) * (f[i][j]);
}
}
sum *= ((c[u] * c[v]) / 4.0);
F[u][v] = sum;
}
}
return F;
}
}
@aisnote
Copy link

aisnote commented Nov 10, 2020

not work on Kotlin, always get hash = 0. Any ideas?

@RobbWatershed
Copy link

@aisnote I don't know if that helps, but I've been getting zeroes while trying to run BitmapFactory.decodeStream from a Robolectric unit test.

Turns out that BitmapFactory stuff is mocked to the minimum and never actually loads the picture to memory. Once I ran my code in an actual app context, the bitmaps were loaded and the hashes were working.

@Jinter410
Copy link

Jinter410 commented Oct 29, 2021

why the length of phash with using this code is 49?

Because there is an error in the string computation the condition should not be x!=0 and y!=0
Here is a for that does the trick correctly :

for (int x = 0; x < smallerSize; x++) {
                for (int y = 0; y < smallerSize; y++) {
                        hash += (dctVals[x][y] > avg ? "1" : "0");
                }
            }

or with shifted y :

for (int x = 0; x < smallerSize; x++) {
                for (int y = 1; y < smallerSize+1; y++) {
                        hash += (dctVals[x][y] > avg ? "1" : "0");
                }
            }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment