一、原理講解
實現這種功能的關鍵技術叫做"感知哈希算法"(Perceptual Hash Algorithm), 意思是為圖片生成一個指紋(字符串格式), 兩張圖片的指紋越相似, 說明兩張圖片就越相似. 但關鍵是如何根據圖片計算出"指紋"呢? 下面用最簡單的步驟來說明一下原理:
《1》、第一步 縮小圖片尺寸
將圖片縮小到8x8的尺寸, 總共64個像素. 這一步的作用是去除各種圖片尺寸和圖片比例的差異, 只保留結構、明暗等基本信息.
《2》、第二步 轉為灰度圖片
將縮小后的圖片, 轉為64級灰度圖片.
《3》、第三步 計算灰度平均值
計算圖片中所有像素的灰度平均值
《4》、第四步 比較像素的灰度
將每個像素的灰度與平均值進行比較, 如果大於或等於平均值記為1, 小於平均值記為0.
《5》、第五步 計算哈希值
將上一步的比較結果, 組合在一起, 就構成了一個64位的二進制整數, 這就是這張圖片的指紋.
《6》、第六步 對比圖片指紋
得到圖片的指紋后, 就可以對比不同的圖片的指紋, 計算出64位中有多少位是不一樣的. 如果不相同的數據位數不超過5, 就說明兩張圖片很相似, 如果大於10, 說明它們是兩張不同的圖片.

import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import java.awt.Image; import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.WritableRaster; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import javax.imageio.ImageIO; import com.sun.image.codec.jpeg.ImageFormatException; import com.sun.image.codec.jpeg.JPEGCodec; import com.sun.image.codec.jpeg.JPEGImageDecoder; import com.sun.image.codec.jpeg.JPEGImageEncoder; public class ImageHelper { // 項目根目錄路徑 public static final String path = System.getProperty("user.dir"); /** * 生成縮略圖 <br/> * 保存:ImageIO.write(BufferedImage, imgType[jpg/png/...], File); * * @param source * 原圖片 * @param width * 縮略圖寬 * @param height * 縮略圖高 * @param b * 是否等比縮放 * */ public static BufferedImage thumb(BufferedImage source, int width, int height, boolean b) { // targetW,targetH分別表示目標長和寬 int type = source.getType(); BufferedImage target = null; double sx = (double) width / source.getWidth(); double sy = (double) height / source.getHeight(); if (b) { if (sx > sy) { sx = sy; width = (int) (sx * source.getWidth()); } else { sy = sx; height = (int) (sy * source.getHeight()); } } if (type == BufferedImage.TYPE_CUSTOM) { // handmade ColorModel cm = source.getColorModel(); WritableRaster raster = cm.createCompatibleWritableRaster(width, height); boolean alphaPremultiplied = cm.isAlphaPremultiplied(); target = new BufferedImage(cm, raster, alphaPremultiplied, null); } else target = new BufferedImage(width, height, type); Graphics2D g = target.createGraphics(); // smoother than exlax: g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g.drawRenderedImage(source, AffineTransform.getScaleInstance(sx, sy)); g.dispose(); return target; } /** * 圖片水印 * * @param imgPath * 待處理圖片 * @param markPath * 水印圖片 * @param x * 水印位於圖片左上角的 x 坐標值 * @param y * 水印位於圖片左上角的 y 坐標值 * @param alpha * 水印透明度 0.1f ~ 1.0f * */ public static void waterMark(String imgPath, String markPath, int x, int y, float alpha) { try { // 加載待處理圖片文件 Image img = ImageIO.read(new File(imgPath)); BufferedImage image = new BufferedImage(img.getWidth(null), img.getHeight(null), BufferedImage.TYPE_INT_RGB); Graphics2D g = image.createGraphics(); g.drawImage(img, 0, 0, null); // 加載水印圖片文件 Image src_biao = ImageIO.read(new File(markPath)); g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha)); g.drawImage(src_biao, x, y, null); g.dispose(); // 保存處理后的文件 FileOutputStream out = new FileOutputStream(imgPath); JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out); encoder.encode(image); out.close(); } catch (Exception e) { e.printStackTrace(); } } /** * 文字水印 * * @param imgPath * 待處理圖片 * @param text * 水印文字 * @param font * 水印字體信息 * @param color * 水印字體顏色 * @param x * 水印位於圖片左上角的 x 坐標值 * @param y * 水印位於圖片左上角的 y 坐標值 * @param alpha * 水印透明度 0.1f ~ 1.0f */ public static void textMark(String imgPath, String text, Font font, Color color, int x, int y, float alpha) { try { Font Dfont = (font == null) ? new Font("宋體", 20, 13) : font; Image img = ImageIO.read(new File(imgPath)); BufferedImage image = new BufferedImage(img.getWidth(null), img.getHeight(null), BufferedImage.TYPE_INT_RGB); Graphics2D g = image.createGraphics(); g.drawImage(img, 0, 0, null); g.setColor(color); g.setFont(Dfont); g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha)); g.drawString(text, x, y); g.dispose(); FileOutputStream out = new FileOutputStream(imgPath); JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out); encoder.encode(image); out.close(); } catch (Exception e) { System.out.println(e); } } /** * 讀取JPEG圖片 * @param filename 文件名 * @return BufferedImage 圖片對象 */ public static BufferedImage readJPEGImage(String filename) { try { InputStream imageIn = new FileInputStream(new File(filename)); // 得到輸入的編碼器,將文件流進行jpg格式編碼 JPEGImageDecoder decoder = JPEGCodec.createJPEGDecoder(imageIn); // 得到編碼后的圖片對象 BufferedImage sourceImage = decoder.decodeAsBufferedImage(); return sourceImage; } catch (FileNotFoundException e) { e.printStackTrace(); } catch (ImageFormatException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } /** * 讀取JPEG圖片 * @param filename 文件名 * @return BufferedImage 圖片對象 */ public static BufferedImage readPNGImage(String filename) { try { File inputFile = new File(filename); BufferedImage sourceImage = ImageIO.read(inputFile); return sourceImage; } catch (FileNotFoundException e) { e.printStackTrace(); } catch (ImageFormatException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null; } /** * 灰度值計算 * @param pixels 像素 * @return int 灰度值 */ public static int rgbToGray(int pixels) { // int _alpha = (pixels >> 24) & 0xFF; int _red = (pixels >> 16) & 0xFF; int _green = (pixels >> 8) & 0xFF; int _blue = (pixels) & 0xFF; return (int) (0.3 * _red + 0.59 * _green + 0.11 * _blue); } /** * 計算數組的平均值 * @param pixels 數組 * @return int 平均值 */ public static int average(int[] pixels) { float m = 0; for (int i = 0; i < pixels.length; ++i) { m += pixels[i]; } m = m / pixels.length; return (int) m; } }

import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.List; public class SimilarImageSearch { /** * @param args */ public static void main(String[] args) { List<String> hashCodes = new ArrayList<String>(); String filename = ImageHelper.path + "\\images\\"; String hashCode = null; for (int i = 0; i < 7; i++) { hashCode = produceFingerPrint(filename + "example" + (i + 1) + ".jpg"); hashCodes.add(hashCode); } System.out.println("Resources: "); System.out.println(hashCodes); System.out.println(); String sourceHashCode = produceFingerPrint(filename + "source.jpg"); System.out.println("Source: "); System.out.println(sourceHashCode); System.out.println(); for (int i = 0; i < hashCodes.size(); i++) { int difference = hammingDistance(sourceHashCode, hashCodes.get(i)); if(difference==0){ System.out.println("source.jpg圖片跟example"+(i+1)+".jpg一樣"); }else if(difference<=5){ System.out.println("source.jpg圖片跟example"+(i+1)+".jpg非常相似"); }else if(difference<=10){ System.out.println("source.jpg圖片跟example"+(i+1)+".jpg有點相似"); }else if(difference>10){ System.out.println("source.jpg圖片跟example"+(i+1)+".jpg完全不一樣"); } System.out.println(difference); } } /** * 計算"漢明距離"(Hamming distance)。 * 如果不相同的數據位不超過5,就說明兩張圖片很相似;如果大於10,就說明這是兩張不同的圖片。 * @param sourceHashCode 源hashCode * @param hashCode 與之比較的hashCode */ public static int hammingDistance(String sourceHashCode, String hashCode) { int difference = 0; int len = sourceHashCode.length(); for (int i = 0; i < len; i++) { if (sourceHashCode.charAt(i) != hashCode.charAt(i)) { difference ++; } } return difference; } /** * 生成圖片指紋 * @param filename 文件名 * @return 圖片指紋 */ public static String produceFingerPrint(String filename) { BufferedImage source = ImageHelper.readPNGImage(filename);// 讀取文件 int width = 8; int height = 8; // 第一步,縮小尺寸。 // 將圖片縮小到8x8的尺寸,總共64個像素。這一步的作用是去除圖片的細節,只保留結構、明暗等基本信息,摒棄不同尺寸、比例帶來的圖片差異。 BufferedImage thumb = ImageHelper.thumb(source, width, height, false); // 第二步,簡化色彩。 // 將縮小后的圖片,轉為64級灰度。也就是說,所有像素點總共只有64種顏色。 int[] pixels = new int[width * height]; for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { pixels[i * height + j] = ImageHelper.rgbToGray(thumb.getRGB(i, j)); } } // 第三步,計算平均值。 // 計算所有64個像素的灰度平均值。 int avgPixel = ImageHelper.average(pixels); // 第四步,比較像素的灰度。 // 將每個像素的灰度,與平均值進行比較。大於或等於平均值,記為1;小於平均值,記為0。 int[] comps = new int[width * height]; for (int i = 0; i < comps.length; i++) { if (pixels[i] >= avgPixel) { comps[i] = 1; } else { comps[i] = 0; } } // 第五步,計算哈希值。 // 將上一步的比較結果,組合在一起,就構成了一個64位的整數,這就是這張圖片的指紋。組合的次序並不重要,只要保證所有圖片都采用同樣次序就行了。 StringBuffer hashCode = new StringBuffer(); for (int i = 0; i < comps.length; i+= 4) { int result = comps[i] * (int) Math.pow(2, 3) + comps[i + 1] * (int) Math.pow(2, 2) + comps[i + 2] * (int) Math.pow(2, 1) + comps[i + 3]; hashCode.append(binaryToHex(result)); } // 得到指紋以后,就可以對比不同的圖片,看看64位中有多少位是不一樣的。 return hashCode.toString(); } /** * 二進制轉為十六進制 * @param int binary * @return char hex */ private static char binaryToHex(int binary) { char ch = ' '; switch (binary) { case 0: ch = '0'; break; case 1: ch = '1'; break; case 2: ch = '2'; break; case 3: ch = '3'; break; case 4: ch = '4'; break; case 5: ch = '5'; break; case 6: ch = '6'; break; case 7: ch = '7'; break; case 8: ch = '8'; break; case 9: ch = '9'; break; case 10: ch = 'a'; break; case 11: ch = 'b'; break; case 12: ch = 'c'; break; case 13: ch = 'd'; break; case 14: ch = 'e'; break; case 15: ch = 'f'; break; default: ch = ' '; } return ch; } }