图像识别+指纹对比

今天在阮一峰的博客上看到了这篇《相似图片搜索原理》博客,于是就有了研究一下指纹识别和图像识别的想法。本篇博客大部分内容引用了它博客的内容。
根据Neal Krawetz博士的解释,实现相似图片搜素的关键技术叫做"感知哈希算法"(Perceptualhash algorithm),它的作用是对每张图片生成一个"指纹"(fingerprint)字符串,然后比较不同图片的指纹。结果越接近,就说明图片越相似。

以下是一个最简单的Java实现:

预处理:读取图片

第一步,缩小尺寸。

将图片缩小到8x8的尺寸,总共64个像素。这一步的作用是去除图片的细节,只保留结构、明暗等基本信息,摒弃不同尺寸、比例带来的图片差异。

第二步,简化色彩。

将缩小后的图片,转为64级灰度。也就是说,所有像素点总共只有64种颜色。

第三步,计算平均值。

计算所有64个像素的灰度平均值。

第四步,比较像素的灰度。

将每个像素的灰度,与平均值进行比较。大于或等于平均值,记为1;小于平均值,记为0。

第五步,计算哈希值。

将上一步的比较结果,组合在一起,就构成了一个64位的整数,这就是这张图片的指纹。组合的次序并不重要,只要保证所有图片都采用同样次序就行了。

得到指纹以后,就可以对比不同的图片,看看64位中有多少位是不一样的。在理论上,这等同于计算"汉明距离"(Hammingdistance)。如果不相同的数据位不超过5,就说明两张图片很相似;如果大于10,就说明这是两张不同的图片。

你可以将几张图片放在一起,也计算出他们的汉明距离对比,就可以看看两张图片是否相似。

这种算法的优点是简单快速,不受图片大小缩放的影响,缺点是图片的内容不能变更。如果在图片上加几个文字,它就认不出来了。所以,它的最佳用途是根据缩略图,找出原图。

实际应用中,往往采用更强大的pHash算法和SIFT算法,它们能够识别图片的变形。只要变形程度不超过25%,它们就能匹配原图。这些算法虽然更复杂,但是原理与上面的简便算法是一样的,就是先将图片转化成Hash字符串,然后再进行比较。

以上内容大部分直接从阮一峰的网站上复制过来,想看原著的童鞋可以去在最上面的链接点击进去看。
以下为阿里的开源实现,在实验此代码的情况下,大家要先先在工程中导下jdk 中的rt.jar 包,此外也有专门的jar包来实现图片预处理,有兴趣的可自行去百度,小编今天也是初次研究,其中也有很多不懂,希望大家共同学习。

package com.ldy.ly002;

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;

/**
 * 图片工具类,主要针对图片水印处理
 * 
 * @author 025079
 * @version [版本号, 2011-11-28]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
public class ImageHelper {

 // 项目根目录路径
 public static final String path = System.getProperty("user.dir");
 
 /**
 * 生成缩略图 
* 保存: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; } }
package com.ldy.ly002;

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 hashCodes = new ArrayList();
    
    String filename = ImageHelper.path + "\\images\\";
    String hashCode = null;
    
    for (int i = 0; i < 1; 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 + "source1.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));
      System.out.print("汉明距离:"+difference+"   ");
      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完全不一样");
      }
    }
    
  }

  /**
   * 计算"汉明距离"(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 + 2];
      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;
  }

}

你可能感兴趣的:(杂七杂八)