当前位置: 首页 > article >正文

Android 图片相识度比较(pHash)

概述

在 Android 中,要比对两张 Bitmap 图片的相似度,常见的方法有基于像素差异直方图比较、或者使用一些更高级的算法如 SSIM(结构相似性)感知哈希(pHash)

1. 基于像素的差异比较

可以逐像素比较两张 Bitmap,计算它们之间的差异。以下是一个简单的逐像素比较的例子:

public static double compareBitmaps(Bitmap bitmap1, Bitmap bitmap2) {
    if (bitmap1.getWidth() != bitmap2.getWidth() || bitmap1.getHeight() != bitmap2.getHeight()) {
        throw new IllegalArgumentException("Bitmap sizes are different!");
    }

    int width = bitmap1.getWidth();
    int height = bitmap1.getHeight();
    long diff = 0;

    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            int pixel1 = bitmap1.getPixel(x, y);
            int pixel2 = bitmap2.getPixel(x, y);

            int r1 = Color.red(pixel1);
            int g1 = Color.green(pixel1);
            int b1 = Color.blue(pixel1);

            int r2 = Color.red(pixel2);
            int g2 = Color.green(pixel2);
            int b2 = Color.blue(pixel2);

            // 计算 RGB 差异
            diff += Math.abs(r1 - r2);
            diff += Math.abs(g1 - g2);
            diff += Math.abs(b1 - b2);
        }
    }

    // 最大可能差异
    double maxDiff = 3L * 255 * width * height;

    // 返回 0 到 1 的值,越小表示相似度越高
    return (double) diff / maxDiff;
}

这段代码计算两张图片的 RGB 差异,返回的结果范围在 0-1 之间,数值越接近 0 表示图片越相似。

2. 基于直方图的比较

通过比较两张图片的颜色直方图来评估相似度。直方图可以捕捉图像的颜色分布,而不关心具体像素位置。

public static double compareHistograms(Bitmap bitmap1, Bitmap bitmap2) {
    int[] histogram1 = new int[256];
    int[] histogram2 = new int[256];

    // 计算两张图的灰度直方图
    for (int y = 0; y < bitmap1.getHeight(); y++) {
        for (int x = 0; x < bitmap1.getWidth(); x++) {
            int pixel1 = bitmap1.getPixel(x, y);
            int gray1 = (Color.red(pixel1) + Color.green(pixel1) + Color.blue(pixel1)) / 3;
            histogram1[gray1]++;
            
            int pixel2 = bitmap2.getPixel(x, y);
            int gray2 = (Color.red(pixel2) + Color.green(pixel2) + Color.blue(pixel2)) / 3;
            histogram2[gray2]++;
        }
    }

    // 计算直方图的差异
    double diff = 0;
    for (int i = 0; i < 256; i++) {
        diff += Math.abs(histogram1[i] - histogram2[i]);
    }

    return diff / (bitmap1.getWidth() * bitmap1.getHeight());
}

3. 使用 SSIM(结构相似性)

SSIM 是一种用来衡量两张图片结构相似性的算法,它比简单的像素差异或直方图比较更准确。Android SDK 没有内置的 SSIM 方法,但可以引入第三方库或者自己实现。SSIM 主要关注三方面:亮度、对比度和结构。

4. 感知哈希(pHash)

pHash 是一种图像哈希技术,它可以生成图片的“指纹”,然后比较两个哈希值的相似性。与传统哈希方法不同,pHash 对于图像的细微改变(例如缩放、旋转)不敏感。

可以通过第三方库实现 pHash,比如 ImageHash 库,或者自己实现基于 DCT(离散余弦变换)的算法。

// 引入第三方库 ImageHash 进行哈希比较
String hash1 = ImageHash.hash(bitmap1);
String hash2 = ImageHash.hash(bitmap2);

int similarity = ImageHash.compare(hash1, hash2);

一般来说:

  • 对于简单的图像比较,基于像素差异的方式即可。
  • 如果要忽略图片的细微变动,直方图或感知哈希是更合适的选择。
  • SSIM 适用于对图像结构有更高要求的场景。

实现

图像比较的算法应用相当广泛, 本文基于感知哈希算法, 用于识别视频帧图像的左右两部分的相似度, 从而判断视频是否是一个左右眼的VR视频格式, 本文采用 感知哈希(pHash) 算法, 它非常适合处理具有细微变化的图像,如裁剪、缩放、亮度变化等。

感知哈希(pHash)是一种用于衡量图像相似度的算法,它通过将图像转换为频域信息,提取其视觉特征来生成一个哈希值。pHash 具有鲁棒性,能够忽略图像的小幅度变动、旋转和缩放等影响。下面是 pHash 算法的实现步骤及其原理。

pHash 算法的实现步骤

  1. 转换为灰度图:将图片转换为灰度图像,以便降低复杂度,并去除颜色信息的影响。

  2. 缩小尺寸:将图像缩小到一个固定的尺寸(例如 32x32),目的是去除高频细节,保留图片的整体特征。这一步骤在后续的离散余弦变换(DCT)中很重要。

  3. 离散余弦变换(DCT):对缩小后的图像执行离散余弦变换,将图像从空间域转换到频率域。这种转换能提取图像的低频信息,忽略高频噪声。

  4. 截取低频部分:只保留 DCT 结果的左上角部分(例如 8x8 的矩阵),因为这部分包含图像的主要信息。

  5. 计算均值:计算截取的低频部分的均值。

  6. 生成哈希值:将 DCT 中每个像素值与均值进行比较,生成一个二进制序列。如果某个像素值大于均值,置 1,否则置 0。最终的哈希值是由这个二进制序列构成。

参考pHash 算法实现

import android.graphics.Bitmap;
import android.graphics.Color;
import java.util.Arrays;

public class ImagePHash {

    // 默认使用 32x32 大小
    private static final int SIZE = 32;
    // DCT 截取的大小(例如 8x8)
    private static final int SMALLER_SIZE = 8;

    public String getHash(Bitmap img) {
        // 1. 转换为灰度图像
        Bitmap grayImg = toGrayscale(img);

        // 2. 缩小图片
        Bitmap smallImg = Bitmap.createScaledBitmap(grayImg, SIZE, SIZE, false);

        // 3. 转换为二维数组
        double[][] vals = new double[SIZE][SIZE];
        for (int x = 0; x < SIZE; x++) {
            for (int y = 0; y < SIZE; y++) {
                vals[x][y] = Color.red(smallImg.getPixel(x, y));
            }
        }

        // 4. 对图像执行离散余弦变换(DCT)
        double[][] dctVals = applyDCT(vals);

        // 5. 截取 DCT 左上角的 8x8 部分
        double[] dctLowFreq = new double[SMALLER_SIZE * SMALLER_SIZE];
        for (int x = 0; x < SMALLER_SIZE; x++) {
            for (int y = 0; y < SMALLER_SIZE; y++) {
                dctLowFreq[x * SMALLER_SIZE + y] = dctVals[x][y];
            }
        }

        // 6. 计算均值
        double avg = Arrays.stream(dctLowFreq).average().orElse(0.0);

        // 7. 生成哈希值
        StringBuilder hash = new StringBuilder();
        for (double value : dctLowFreq) {
            hash.append(value > avg ? "1" : "0");
        }

        return hash.toString();
    }

    // 转换为灰度图像
    private Bitmap toGrayscale(Bitmap img) {
        int width = img.getWidth();
        int height = img.getHeight();
        Bitmap grayscaleImg = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);

        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                int pixel = img.getPixel(x, y);
                int red = Color.red(pixel);
                int green = Color.green(pixel);
                int blue = Color.blue(pixel);
                int gray = (red + green + blue) / 3;
                int newPixel = Color.rgb(gray, gray, gray);
                grayscaleImg.setPixel(x, y, newPixel);
            }
        }
        return grayscaleImg;
    }

    // 执行离散余弦变换(DCT)
    private double[][] applyDCT(double[][] f) {
        int N = f.length;
        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 += f[i][j] *
                               Math.cos((2 * i + 1) * u * Math.PI / (2.0 * N)) *
                               Math.cos((2 * j + 1) * v * Math.PI / (2.0 * N));
                    }
                }
                double alphaU = (u == 0) ? Math.sqrt(1.0 / N) : Math.sqrt(2.0 / N);
                double alphaV = (v == 0) ? Math.sqrt(1.0 / N) : Math.sqrt(2.0 / N);
                F[u][v] = alphaU * alphaV * sum;
            }
        }
        return F;
    }

    // 比较两个哈希值,返回汉明距离(不同位数的个数)
    public int hammingDistance(String hash1, String hash2) {
        int distance = 0;
        for (int i = 0; i < hash1.length(); i++) {
            if (hash1.charAt(i) != hash2.charAt(i)) {
                distance++;
            }
        }
        return distance;
    }
}

对比效果如下(使用ListView 显示多张图片对比结果, 一帧视频图像从中间切割左右两部分, 分别显示在列表项的左右两侧, 中间的文字输出比较结果的汉明值, 值越小图像差异越小):
在这里插入图片描述
在这里插入图片描述

原始测试图片(从一个VR视频中截取出的视频帧):
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

代码分享:

test_img_diff.xml 布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/rlRoot">
    <ListView android:id="@+id/lv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</RelativeLayout>

ListView 的item 布局: item_img_diff.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView android:id="@+id/ivLeft"
        android:layout_width="128dp"
        android:layout_height="72dp"/>

    <ImageView android:id="@+id/ivRight"
        android:layout_width="128dp"
        android:layout_height="72dp"
        android:layout_alignParentRight="true"/>

    <TextView android:id="@+id/tvRes"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:textSize="18sp"
        android:textColor="#FFFFFFFF"/>
</RelativeLayout>

主界面Activity: ImgDiffTester.java

public class ImgDiffTester extends Activity implements View.OnClickListener {
	final String TAG = "ImgDiffTester";
	ListView lv;
	ImgListAdapter adapter;
	@Override
	protected void onCreate(@Nullable Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.test_img_diff);
		findViewById(R.id.rlRoot).setOnClickListener(this);
		lv = (ListView) findViewById(R.id.lv);
		adapter = new ImgListAdapter();
		lv.setAdapter(adapter);
		startCompare();
	}

	void startCompare(){
		new Thread(){
			@Override
			public void run() {
				File[] fs = new File("/sdcard/Download/").listFiles(new FileFilter() {
					@Override
					public boolean accept(File file) {
						return file.getName().endsWith(".png");
					}
				});
				for(File f : fs){
					Bitmap bm = BitmapFactory.decodeFile(f.getAbsolutePath());
					compareBitmapAndShow(bm);
				}

				lv.post(new Runnable() {
					@Override
					public void run() {
						adapter.notifyDataSetChanged();
					}
				});
			}
		}.start();
	}
	void compareBitmapAndShow(Bitmap bm){
		if(bm != null && bm.getWidth() > 0 && bm.getHeight() > 0) {

			final Bitmap bm1 = BitmapUtils.clipBitmapWidthBounds(bm, new Rect(0, 0, bm.getWidth() / 2, bm.getHeight()));
			//bm1 = BitmapFactory.decodeFile("/sdcard/l.png");
			final Bitmap bm2 = BitmapUtils.clipBitmapWidthBounds(bm, new Rect(bm.getWidth() / 2, 0, bm.getWidth(), bm.getHeight()));
			//bm2 = BitmapFactory.decodeFile("/sdcard/r.png");
			try {
				Bitmap[] scaled = new Bitmap[2];
				//scaled[0] = Bitmap.createBitmap(pHash.DCT_LENGTH, pHash.DCT_LENGTH, Bitmap.Config.ARGB_8888);
				//scaled[1] = Bitmap.createBitmap(pHash.DCT_LENGTH, pHash.DCT_LENGTH, Bitmap.Config.ARGB_8888);
				//int cmp = pHash.compareBitmap(bm1, bm2, scaled, false);
				long st = SystemClock.uptimeMillis();
				final int cmp = ImagePHash.compareBitmap(bm1, bm2);
				long et = SystemClock.uptimeMillis();
				Log.d(TAG, "compare " + cmp + " spend " + (et - st) + " ms");
				Item item = new Item();
				item.l = bm1;
				item.r = bm2;
				item.res = "Result: " + cmp + ", spend " + (et - st) + " ms";
				adapter.items.add(item);
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}
	public static class ImagePHash {

		// 默认使用 32x32 大小
		private static final int SIZE = 32;
		// DCT 截取的大小(例如 8x8)
		private static final int SMALLER_SIZE = 8;

		public static int compareBitmap(Bitmap bm1, Bitmap bm2){
			String h1 = getHash(bm1);
			String h2 = getHash(bm2);
			return hammingDistance(h1, h2);
		}
		@SuppressLint("NewApi")
		public static String getHash(Bitmap img) {
			long st = SystemClock.uptimeMillis();
			// 1. 转换为灰度图像
			Bitmap grayImg = toGrayscale(img);

			// 2. 缩小图片
			Bitmap smallImg = Bitmap.createScaledBitmap(grayImg, SIZE, SIZE, false);

			// 3. 转换为二维数组
			double[][] vals = new double[SIZE][SIZE];
			for (int x = 0; x < SIZE; x++) {
				for (int y = 0; y < SIZE; y++) {
					vals[x][y] = Color.red(smallImg.getPixel(x, y));
				}
			}

			long ct1 = SystemClock.uptimeMillis();
			// 4. 对图像执行离散余弦变换(DCT)
			double[][] dctVals = applyDCT(vals);
			long ct2 = SystemClock.uptimeMillis();

			// 5. 截取 DCT 左上角的 8x8 部分
			double[] dctLowFreq = new double[SMALLER_SIZE * SMALLER_SIZE];
			for (int x = 0; x < SMALLER_SIZE; x++) {
				for (int y = 0; y < SMALLER_SIZE; y++) {
					dctLowFreq[x * SMALLER_SIZE + y] = dctVals[x][y];
				}
			}

			// 6. 计算均值
			double avg = Arrays.stream(dctLowFreq).average().orElse(0.0);


			long ct3 = SystemClock.uptimeMillis();

			// 7. 生成哈希值
			StringBuilder hash = new StringBuilder();
			for (double value : dctLowFreq) {
				hash.append(value > avg ? "1" : "0");
			}
			Log.d("ImgDiff", (ct1 - st) + ", " + (ct2 - ct1));
			return hash.toString();
		}

		// 转换为灰度图像
		private static Bitmap toGrayscale(Bitmap img) {
			int width = img.getWidth();
			int height = img.getHeight();
			Bitmap grayscaleImg = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);

			for (int y = 0; y < height; y++) {
				for (int x = 0; x < width; x++) {
					int pixel = img.getPixel(x, y);
					int red = Color.red(pixel);
					int green = Color.green(pixel);
					int blue = Color.blue(pixel);
					int gray = (red + green + blue) / 3;
					int newPixel = Color.rgb(gray, gray, gray);
					grayscaleImg.setPixel(x, y, newPixel);
				}
			}
			return grayscaleImg;
		}

		// 执行离散余弦变换(DCT)
		private static double[][] applyDCT(double[][] f) {
			int N = f.length;
			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 += f[i][j] *
									Math.cos((2 * i + 1) * u * Math.PI / (2.0 * N)) *
									Math.cos((2 * j + 1) * v * Math.PI / (2.0 * N));
						}
					}
					double alphaU = (u == 0) ? Math.sqrt(1.0 / N) : Math.sqrt(2.0 / N);
					double alphaV = (v == 0) ? Math.sqrt(1.0 / N) : Math.sqrt(2.0 / N);
					F[u][v] = alphaU * alphaV * sum;
				}
			}
			return F;
		}

		// 比较两个哈希值,返回汉明距离(不同位数的个数)
		public static int hammingDistance(String hash1, String hash2) {
			int distance = 0;
			for (int i = 0; i < hash1.length(); i++) {
				if (hash1.charAt(i) != hash2.charAt(i)) {
					distance++;
				}
			}
			return distance;
		}
	}

	class ImgListAdapter extends BaseAdapter{
		ArrayList<Item> items = new ArrayList<>();
		@Override
		public int getCount() {
			return items.size();
		}

		@Override
		public Object getItem(int i) {
			return items.get(i);
		}

		@Override
		public long getItemId(int i) {
			return i;
		}

		@Override
		public View getView(int pos, View view, ViewGroup viewGroup) {
			if(view == null){
				view = getLayoutInflater().inflate(R.layout.item_img_diff, null, false);
			}
			((ImageView)view.findViewById(R.id.ivLeft)).setImageBitmap(items.get(pos).l);
			((ImageView)view.findViewById(R.id.ivRight)).setImageBitmap(items.get(pos).r);
			((TextView)view.findViewById(R.id.tvRes)).setText(items.get(pos).res);
			return view;
		}
	}
	class Item{
		Bitmap l, r;
		String res;
	}
}

温馨提示
本文算法及用例仅供参考, 未经大量测试验证
请谨慎阅读参考

参考

Android Bitmap亮度调节、灰度化、二值化、相似距离实现


http://www.kler.cn/news/363678.html

相关文章:

  • 蚁剑连接本地木马文件报错
  • 【部署篇】RabbitMq-03集群模式部署
  • 微服务与多租户详解:架构设计与实现
  • Cheat Engine v7.1 修改百度网盘无限速下载(修改方法在网盘内)
  • 免费开源Odoo软件如何实现电商仓库高效发货
  • react18中在列表项中如何使用useRef来获取每项的dom对象
  • linux-牛刀小试
  • NAND FLASH 与 SPI FLASH
  • Python基于OpenCV的实时疲劳检测
  • AI网关对企业的意义及如何构建 AI 网关
  • [Windows] 很火的开源桌面美化工具 Seelen UI v2.0.2
  • Github 2024-10-18Java开源项目日报Top9
  • 使用 SSH 连接 GitLab 的常见问题及解决方案
  • 摄像机实时接入分析平台LiteAlServer视频智能分析软件抽烟检测算法的应用场景
  • a标签点击页面跳转是-403,回车后正常了
  • MySQL-28.事务-介绍与操作
  • 【每日一题】LeetCode - 反转整数问题
  • 多线程初阶(七):单例模式指令重排序
  • 【Docker技术详解】(一)Docker镜像文件系统的关系和交互
  • Spring Cache Caffeine 高性能缓存库
  • 学习AJAX请求(初步)24.10.21-10.23
  • JAVA应用测试,线上故障排查分析全套路!
  • 数据结构 —— 链式二叉树(C语言讲解)
  • main.ts中引入App.vue报错,提示“Cannot find module ‘./App.vue’ or its corresponding type
  • Webpack安装
  • python: Parent-child form operations