给定一张图片,如何从一组图片中找到与它最相近的图片呢?相信很多小伙伴跟我一样,第一想到的解决办法就是遍历比较每张图片的像素点,找到差异最小的那张图片。这种方法虽然可行,但时间复杂度高,只适用于像素点较少的图片,对于像素点较多的图片,我们需要另寻他路,即通过获取图片指纹,计算两张图片编码数的汉明距离,从而找出最相近的图片。
步骤一:将图片转为int类型的二维数组
public int[][] getpixelarray(String path){
BufferedImage buffimg=null;
try {
buffimg=ImageIO.read(new File(path));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
int w=buffimg.getWidth();
int h=buffimg.getHeight();
int[][] imgarr=new int[w][h];
for(int i=0;i<w;i++) {
for(int j=0;j<h;j++) {
imgarr[i][j]=buffimg.getRGB(i, j);
}
}
return imgarr;
}
步骤二:缩小图片,并将其转为灰度图,保存均值化灰度值
若我们将图片二维数组中的每个像素值都取相应的二进制码字符,则会得到很长的字符串,不利于计算汉明距离,所以我们需要将图片缩少,减小所得字符串长度。下面代码是将图片矩阵缩小为同等规模的8*8矩阵。将图片缩小后,我们还需要将图片转为灰度图。那如何转成灰度图呢?假设原先图片矩阵为128*128的矩阵,则我们需要将原先图片中每一个16*16的小矩阵中的256个像素点灰度值的平均值保存至8*8的矩阵中。所以当我们在原先图片矩阵中每隔16个长度取一个16*16的小矩阵时,我们都需将灰度值初始化为0,然后遍历16*16矩阵中的每个像素点的灰度值进行求和,最后取平均值并依次将其保存至8*8的矩阵中,从而得到8*8的灰度图。
//将图片缩小 获取8*8的灰度矩阵
int sw=imgarr.length/8;
int sh=imgarr[0].length/8;
int[][] grayarr=new int[8][8];//定义一个8*8的灰度矩阵
//计算均值化灰度值 保存至灰度矩阵中
for(int i=0;i<=imgarr.length-sw;i+=sw) {
for(int j=0;j<=imgarr[i].length-sh;j+=sh) {
int grayend=0;//在原先图片矩阵中每取一个sw*sh的矩阵都将灰度值初始化为0
//遍历计算每次所取的sw*sh矩阵中的每个像素点的灰度值 进行求和并最终取平均值
for(int k=0;k<sw;k++) {
for(int l=0;l<sh;l++) {
int rgb=imgarr[i+k][j+l];
Color color=new Color(rgb);
//计算灰度值
int red=color.getRed();
int green=color.getGreen();
int blue=color.getBlue();
int gray=(red+green+blue)/3;
grayend+=gray;
}
}
grayarr[i/sw][j/sh]=grayend/(sw*sh);//将均值化的灰度值保存至灰度矩阵中
}
}
步骤三:根据均值化灰度矩阵,获取图片相应的编码数
先遍历均值化灰度矩阵中的每个灰度值,进行求和并取平均值。然后再次遍历均值化灰度矩阵,若灰度值大于所求的平均值,则将其所对应的二进制码字符设置为‘0’,否则则将其所对应的二进制码字符设置为‘1’,从而获取图片相应的二进制码字符串。
int avergray=0;
//遍历灰度矩阵 计算灰度值的平均值
for(int i=0;i<grayarr.length;i++) {
for(int j=0;j<grayarr[i].length;j++) {
avergray+=grayarr[i][j];
}
}
avergray=avergray/(grayarr.length*grayarr[0].length);
//再次遍历灰度矩阵 大于平均值则设置为0
for(int k=0;k<grayarr.length;k++) {
for(int l=0;l<grayarr[k].length;l++) {
int gray=grayarr[k][l];
if(gray>avergray) {
qrcodestr+="0";
}else {
qrcodestr+="1";
}
}
}
步骤四:计算两个字符串的汉明距离
第一种方法:计算字符串a变成字符串b需要改变的最少字符数。该最少字符数即为两个字符串的汉明距离。下面提供具体实现的代码,详细的算法讲解可参考网址https://blog.csdn.net/xcxy2015/article/details/77164126?utm_source=app&app_version=5.3.0&code=app_1562916241&uLinkId=usr1mkqgl919blen
//创建一个方法 获取两个字符串差异操作数
public int Levenshtein(String str1,String str2) {
int len1=str1.length();
int len2=str2.length();
//创建一个比字符串长度大一的二维空间
int[][] dif=new int[len1+1][len2+1];
for(int a=0;a<=len1;a++) {
dif[a][0]=a;
}
for(int a=0;a<=len2;a++) {
dif[0][a]=a;
}
//设标志 若上面值和左边值不相等 则左上角值加1 相等则加0
int temp;
for(int i=1;i<=len1;i++) {
for(int j=1;j<=len2;j++) {
if(str1.charAt(i-1)==str2.charAt(j-1)) {//上面值与左边值相等
temp=0;
}else {
temp=1;
}
//上面值和左边值都加1后 取上面值、左边值、左上角值中的最小值
dif[i][j]=min(dif[i-1][j]+1,dif[i][j-1]+1,dif[i-1][j-1]+temp);//调用最小值方法
}
}
//最右下角的数值即为操作数
return dif[len1][len2];
}
//创建最小值方法 从一组数据中获取最小的数据值
private static int min(int...is) {
int min=Integer.MAX_VALUE;
for(int i:is) {
if(min>i) {
min=i;
}
}
return min;
}
第二种方法:先将汉明距离初始化为0,再遍历两个字符串,若两个字符串对应位置的字符不相等,则汉明距离加1,直至最小长度字符串遍历完得到最终的汉明距离。
int hashcodedistance=0;//初始化汉明距离
for(int i=0;i<Math.min(str1.length(),str2.length());i++){
if(str1.charAt(i)!=str2.charAt(i)) {
hashcodedistance++;
}
}
有时图片对应的二进码字符串很长,比较次数很多,所以可将二进制码字符串先转为对应的十进制整数,再将十进制整数转为相应的十六进制码字符串,从而比较两个十六进制码字符串中不相等的字符数,减少比较次数。
步骤五:找出与给定图片指纹的最小汉明距离所对应的图片
创建一个保存键值对的泛型数组1,键为一组N张图片的图片路径,值为该N张图片所对应的编码数。再创建一个保存键值对的泛型数组2,键为该N张图片的图片路径,值为该N张图片与给定图片的汉明距离。获取泛型数组2中所有键值对中的值,将其转为数组进行排序,从而获取最小值。然后根据最小值访问其所对应的键,从而获取与给定图片最相近图片的图片路径,最后将其在窗体上绘出。
//定义一个泛型数组1 保存图片路径以及该图片所代表的二进制码字符串的键值对
static HashMap<String, String> imghashcodeMap1= new HashMap<> ();
int[][] imgarr=getpixelarray(path);//获取图片二维数组
String addimgstr=getimagestr(imgarr);//获取该图片的二进制码字符串
imghashcodeMap1.put(path, addimgstr);//将该图片的路径和二进制码字符串保存至数组1中
//定义一个泛型数组2 保存每张添加的图片路径与待匹配图片的字符操作数的键值对
HashMap<String, Integer> imghashcodeMap2= new HashMap<> ();
//遍历泛型数组1 计算每张添加图片与待配对图片的字符操作数
int difnum;//声明字符串差异操作数
for(String key : imghashcodeMap1.keySet ()){
String addstr = imghashcodeMap1.get (key);
//调用字符串差异操作数方法
difnum=Levenshtein(addstr,imgstr);//addstr为添加图片路径 imgstr为给定图片路径
imghashcodeMap2.put(key,difnum);//将添加图片路径与其所对应的字符串差异操作数保存至数组2中
}
//获取所有值 转为数组进行排序 从而获取最小的(汉明距离)
Collection<Integer> c=imghashcodeMap2.values();
Object[] obj=c.toArray();
Arrays.sort(obj);//按升序排序
//通过最小值获取其对应图片路径
String simimgpath="";//初始化图片路径
for(Entry<String, Integer> entry:imghashcodeMap2.entrySet()){
if(entry.getValue().equals(obj[0])) {
simimgpath=entry.getKey();//最相近图片路径
}
}
项目实操:
从一组11张图片(douw0.jpg-douw10.jpg)中找到与douw13.jpg最相近的图片,并将douw13.jpg与最相近图片在窗体上绘出
完整代码:
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map.Entry;
import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.filechooser.FileNameExtensionFilter;
public class Imagecmp2 extends JFrame {
private static final long serialVersionUID = 1L;
//定义一个泛型数组1 保存图片路径以及该图片所代表的二进制码字符串的键值对
HashMap<String, String> imghashcodeMap1= new HashMap<> ();
int[][] simimgarr= {{}};//初始化最相近图片二维数组
int[][] matchimgarr= {{}};//初始化待匹配图片二维数组
Graphics g=null;
//创建窗体
public void showUI() {
setTitle("相似图片查询");
setSize(800,800);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new FlowLayout());
//添加窗体按钮 实现自动加图片操作
JButton btn1=new JButton("添加图库");
JButton btn2=new JButton("查询图片");
add(btn1);
add(btn2);
//打开添加图片监听器
btn1.addActionListener(e->{
for(int i=0;i<=10;i++) {
String path="D:\\图片\\Pictures\\douw"+i+".jpg";
System.out.println("添加的图片为:"+path);
int[][] imgarr=getpixelarray(path);//获取图片二维数组
String addimgstr=getimagestr(imgarr);//获取该图片的二进制码字符串
imghashcodeMap1.put(path, addimgstr);//将该图片的路径和二进制码字符串保存至数组中
System.out.println("添加的图片二进制码字符串为:"+addimgstr);
}
});
//打开待匹配图片查询按钮
btn2.addActionListener(e->{
//文件选择器
JFileChooser jfc=new JFileChooser();
FileNameExtensionFilter filefilter=new FileNameExtensionFilter("图片文件","jpg", "png", "gif");
jfc.setFileFilter(filefilter);
int returnvale = jfc.showOpenDialog (null);
if(returnvale == JFileChooser.APPROVE_OPTION){
String path = jfc.getSelectedFile ().getPath ();
System.out.println("待匹配图片为:"+path);
matchimgarr=getpixelarray(path);//获取待匹配图片二维数组
String imgstr=getimagestr(matchimgarr);//获取待匹配图片的二进制码字符串
//创建泛型数组2 保存每张添加的图片路径与待匹配图片的字符操作数的键值对
HashMap<String, Integer> imghashcodeMap2= new HashMap<> ();
//遍历泛型数组1 计算每张添加图片与待配对图片的字符操作数
int difnum;//声明字符串差异操作数
for(String key : imghashcodeMap1.keySet ()){
String addstr = imghashcodeMap1.get (key);
//调用字符串差异操作数方法
difnum=Levenshtein(addstr,imgstr);
System.out.println("待匹配图片与"+key+"的字符串操作差异数为"+difnum);
imghashcodeMap2.put(key,difnum);
}
//获取所有值 转为数组进行排序 从而获取最小的(汉明距离)
Collection<Integer> c=imghashcodeMap2.values();
Object[] obj=c.toArray();
Arrays.sort(obj);//按升序排序
//通过最小值获取其对应图片路径
String simimgpath="";//初始化图片路径
for(Entry<String, Integer> entry:imghashcodeMap2.entrySet()){
if(entry.getValue().equals(obj[0])) {
simimgpath=entry.getKey();//最相近图片路径
}
}
System.out.println("与待匹配图片最相近的图片为"+simimgpath);
simimgarr=getpixelarray(simimgpath);//最相近图片二维数组
}
//绘制待匹配图片
for(int k=0;k<matchimgarr.length;k++) {
for(int l=0;l<matchimgarr[0].length;l++) {
int rgb=matchimgarr[k][l];
Color color=new Color(rgb);
g.setColor(color);
g.fillRect(100+k, 100+l, 1, 1);
}
}
//绘制最相近图片
for(int k=0;k<simimgarr.length;k++) {
for(int l=0;l<simimgarr[0].length;l++) {
int rgb=simimgarr[k][l];
Color color=new Color(rgb);
g.setColor(color);
g.fillRect(400+k, 100+l, 1, 1);
}
}
});
setVisible(true);
//窗体获取画笔
g=getGraphics();
paint(g);
}
//将图片转为二维数组 保存其像素点
public int[][] getpixelarray(String path){
BufferedImage buffimg=null;
try {
buffimg=ImageIO.read(new File(path));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
int w=buffimg.getWidth();
int h=buffimg.getHeight();
int[][] imgarr=new int[w][h];
for(int i=0;i<w;i++) {
for(int j=0;j<h;j++) {
imgarr[i][j]=buffimg.getRGB(i, j);
}
}
return imgarr;
}
//获取8*8均值化灰度矩阵 遍历灰度矩阵获取二进制码字符串并将其返回
public String getimagestr(int[][] imgarr) {
String qrcodestr="";
//将图片缩小 获取8*8的灰度矩阵
int sw=imgarr.length/8;
int sh=imgarr[0].length/8;
int[][] grayarr=new int[8][8];//定义一个8*8的灰度矩阵
//计算均值化灰度值 保存至灰度矩阵中
for(int i=0;i<=imgarr.length-sw;i+=sw) {
for(int j=0;j<=imgarr[i].length-sh;j+=sh) {
int grayend=0;//在原先图片矩阵中每取一个sw*sh的矩阵都将灰度值初始化为0
//遍历计算每次所取的sw*sh矩阵中的每个像素点的灰度值 进行求和并最终取平均值
for(int k=0;k<sw;k++) {
for(int l=0;l<sh;l++) {
int rgb=imgarr[i+k][j+l];
Color color=new Color(rgb);
//计算灰度值
int red=color.getRed();
int green=color.getGreen();
int blue=color.getBlue();
int gray=(red+green+blue)/3;
grayend+=gray;
}
}
grayarr[i/sw][j/sh]=grayend/(sw*sh);//将均值化的灰度值保存至灰度矩阵中
}
}
int avergray=0;
//遍历灰度矩阵 计算灰度值的平均值
for(int i=0;i<grayarr.length;i++) {
for(int j=0;j<grayarr[i].length;j++) {
avergray+=grayarr[i][j];
}
}
avergray=avergray/(grayarr.length*grayarr[0].length);
//再次遍历灰度矩阵 大于平均值则设置为0
for(int k=0;k<grayarr.length;k++) {
for(int l=0;l<grayarr[k].length;l++) {
int gray=grayarr[k][l];
if(gray>avergray) {
qrcodestr+="0";
}else {
qrcodestr+="1";
}
}
}
return qrcodestr;
}
//创建一个方法 获取两个字符串差异操作数
public int Levenshtein(String str1,String str2) {
int len1=str1.length();
int len2=str2.length();
//创建一个比字符串长度大一的二维空间
int[][] dif=new int[len1+1][len2+1];
for(int a=0;a<=len1;a++) {
dif[a][0]=a;
}
for(int a=0;a<=len2;a++) {
dif[0][a]=a;
}
//设标志 若上面值和左边值不相等 则左上角值加1 相等则加0
int temp;
for(int i=1;i<=len1;i++) {
for(int j=1;j<=len2;j++) {
if(str1.charAt(i-1)==str2.charAt(j-1)) {//上面值与左边值相等
temp=0;
}else {
temp=1;
}
//上面值和左边值都加1后 取上面值、左边值、左上角值中的最小值
dif[i][j]=min(dif[i-1][j]+1,dif[i][j-1]+1,dif[i-1][j-1]+temp);//调用最小值方法
}
}
//最右下角的数值即为操作数
return dif[len1][len2];
}
//创建最小值方法 从一组数据中获取最小的数据值
private static int min(int...is) {
int min=Integer.MAX_VALUE;
for(int i:is) {
if(min>i) {
min=i;
}
}
return min;
}
public static void main(String[]args) {
Imagecmp2 imgcmp=new Imagecmp2();
imgcmp.showUI();
}
}
效果呈现: