相似图片搜索的原理

本文介绍了一种基于感知哈希算法的相似图片搜索技术。该技术通过生成图片的指纹并比较指纹来判断图片间的相似度。文章详细展示了算法的具体步骤,并提供了一个Java实现示例。

上个月,Google把"相似图片搜索"正式放上了首页。

你可以用一张图片,搜索互联网上所有与它相似的图片。点击搜索框中照相机的图标。

一个对话框会出现。

你输入网片的网址,或者直接上传图片,Google就会找出与其相似的图片。下面这张图片是美国女演员Alyson Hannigan。

上传后,Google返回如下结果:

类似的"相似图片搜索引擎"还有不少,TinEye甚至可以找出照片的拍摄背景。

==========================================================

这种技术的原理是什么?计算机怎么知道两张图片相似呢?

根据Neal Krawetz博士的解释,原理非常简单易懂。我们可以用一个快速算法,就达到基本的效果。

这里的关键技术叫做"感知哈希算法"(Perceptual hash algorithm),它的作用是对每张图片生成一个"指纹"(fingerprint)字符串,然后比较不同图片的指纹。结果越接近,就说明图片越相似。

下面是一个最简单的实现:

第一步,缩小尺寸。

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

 

第二步,简化色彩。

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

第三步,计算平均值。

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

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

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

第五步,计算哈希值。

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

 =  = 8f373714acfcf4d0

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

具体的代码实现,可以参见Wote用python语言写的imgHash.py。代码很短,只有53行。使用的时候,第一个参数是基准图片,第二个参数是用来比较的其他图片所在的目录,返回结果是两张图片之间不相同的数据位数量(汉明距离)。

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

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


Java 相似图片搜索算法之感知哈希算法实例:


import java.awt.image.BufferedImage;
import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class SimilarImageSearch {
   
   public static final int SAMEVALUE = 5; //相同图片阀值
   public static final int SIMILARVALUE = 10; //相似图片阀值
 
   public static void main(String[] args) {
      List hashCodes = new ArrayList();
      
      String searchImgUrl = "C:\\Users\\admin\\Desktop\\image\\example.jpg";
      List urlList = collectionImgUrl();
      String hashCode = null;
      long startMillis = System.currentTimeMillis();
      
      for (String url : urlList) {
         hashCode = produceFingerPrint(url);
         hashCodes.add(hashCode);
      }
      
      System.out.println("Resources: ");
      System.out.println(hashCodes);
      System.out.println();

      String sourceHashCode = produceFingerPrint(searchImgUrl);
      System.out.println("Source: ");
      System.out.println(sourceHashCode);
      System.out.println();

      List resultList = new ArrayList();
      List similarResultList = new ArrayList();
      List differences = new ArrayList();
      
      for (int i = 0; i < hashCodes.size(); i++) {
         int difference = hammingDistance(sourceHashCode, hashCodes.get(i));
         if(difference <= SAMEVALUE){
           resultList.add(urlList.get(i).substring(urlList.get(i).lastIndexOf("\")+1,urlList.get(i).length()));
         }else if(difference <= SIMILARVALUE){
           similarResultList.add(urlList.get(i).substring(urlList.get(i).lastIndexOf("\")+1,urlList.get(i).length()));
         }
         differences.add(difference+"->"+urlList.get(i).substring(urlList.get(i).lastIndexOf("\")+1,urlList.get(i).length()));
      }
      
      System.out.println("curMillis:"+(System.currentTimeMillis()-startMillis));
      System.out.println("搜索图片:"+searchImgUrl.substring(searchImgUrl.lastIndexOf("\")+1,searchImgUrl.length()));
      System.out.println("相同图片:"+resultList);
      System.out.println("相似图片:"+similarResultList);
      System.out.println("图片对比:"+differences);
   }
   
 
   public static List collectionImgUrl(){
      String imgPath = "C:\\Users\\admin\\Desktop\\image";
      
      List list = new ArrayList();
      File file = new File(imgPath);
      
      if(file.isDirectory()){
         String[] fileNames = file.list();
         for(String name : fileNames){
            list.add(imgPath.concat("\")+name);
         }
      }
      
      return list;
   }

 
   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;
   }

 
   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();
   }

 
   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;
   }
}

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");

 
   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.createCompatibleWritable Raster(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;
   }

 
   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();
      }
   }

 

   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);
      }
   }

 
   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;
   }

 
   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;
   }

 
   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);
   }

 
   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;
   }
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值