概述
在 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 算法的实现步骤
-
转换为灰度图:将图片转换为灰度图像,以便降低复杂度,并去除颜色信息的影响。
-
缩小尺寸:将图像缩小到一个固定的尺寸(例如 32x32),目的是去除高频细节,保留图片的整体特征。这一步骤在后续的离散余弦变换(DCT)中很重要。
-
离散余弦变换(DCT):对缩小后的图像执行离散余弦变换,将图像从空间域转换到频率域。这种转换能提取图像的低频信息,忽略高频噪声。
-
截取低频部分:只保留 DCT 结果的左上角部分(例如 8x8 的矩阵),因为这部分包含图像的主要信息。
-
计算均值:计算截取的低频部分的均值。
-
生成哈希值:将 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亮度调节、灰度化、二值化、相似距离实现