用图像哈希检测相似图像

了解哈希函数如何创建图像签名,并用于检测 AI 生成的图像的相似性。

用图像哈希检测相似图像

在所有相册中搜索(近似)相同的照片可能是一项繁琐的任务;您需要点击浏览成千上万张图片,然后逐一判断它们是否“相似”。检测重复项最直接的方法是按文件名或文件大小进行分类。然而,照片通常来源于移动设备和社交媒体应用等不同渠道,这导致文件大小、文件名、分辨率、缩放比例、压缩方式和亮度等方面存在差异。近年来,人工智能生成的图像数量也大幅增长。一个自然而然的问题是:图像哈希函数能否用于区分真实照片和合成照片?哈希函数对细微变化具有很强的鲁棒性,因此非常适合检测(近乎)相同的照片。在本博客中,您将学习哈希函数的概念,并通过一个实践教程学习如何在最大限度减少误报的情况下检测重复图像。所有结果均使用 Python 的 undouble 库获得。阅读完本博客后,您将能够从图像中创建哈希签名,并将其应用于您自己的相似性搜索用例!

1、图像在视觉上可能相似,但数值上却可能不同

两张图像在视觉上可能相似,但在数值上却可能不同。数值差异可能由多种原因造成,例如使用社交媒体应用程序,这些应用程序可能会改变图像的亮度、对比度、伽玛校正、压缩、分辨率和/或缩放比例。例如,使用 WhatsApp 发送图像会导致分辨率降低(图 1)。请注意,不同设备和用户自定义设置下的分辨率降低程度可能有所不同。

图 1. 视觉上相似但数值不同的照片。A. 最左侧为原始图像。B. 中间为使用 WhatsApp 发送后的图像。C. 右侧图像为两张图像的差值

从视觉角度来看,很难看出原始图像和 WhatsApp 发送后的图像之间的任何变化,但当我们将两张图像相减时,差异就变得清晰可见(图 1C)。如果您磁盘上只有几张需要去重的图片,那么选择分辨率最高的图片很容易。但是,如果您偶尔或每年都要将所有照片备份到磁盘,这将是一项耗时的任务,尤其当家人、朋友或同事也与您分享几乎相同的照片时,难度会更大。

问题不在于是否存在重复照片,而在于它们在哪里。

因此,诸如按文件大小排序或图像相减之类的方法将失效。不过,有解决方案:哈希函数!哈希函数对亮度、对比度、伽马校正、压缩、缩放和/或分辨率的微小变化具有鲁棒性,因此非常适合检测(几乎)相同的图像。哈希函数有很多应用,例如数字取证、版权保护,以及更普遍的磁盘空间压缩和去重。

2、Undouble 库:搜索重复图像

Undouble 库旨在检测整个系统或任何输入目录中(几乎)相同的图像。给定输入数据,系统会对图像进行预处理,计算哈希值,并根据图像哈希值对图像进行分组。在根据哈希相似度对图像进行分组后,您可以使用 move_to_dir 功能自动整理磁盘上的图像,无需手动操作。该功能会移动除分辨率最高的图像(将其复制)之外的所有相同图像。步骤示意图如下所示,安装方法如下:

pip install undouble
undouble 基于图像哈希值对图像进行分组的步骤示意图

在接下来的章节中,我将更详细地描述预处理步骤、哈希函数、分组结果以及绘图功能,例如图像哈希。

3、什么是图像哈希函数?

哈希函数将输入数据转换或映射到一个固定长度的字符串,该字符串可以被视为输入数据的“指纹”或“签名”;即图像哈希值。因此,一个好的哈希函数应该完全由输入数据决定,或者说,在我们的……中以图像为例。有多种哈希函数,例如平均哈希、感知哈希、差分哈希、Haar小波哈希、Daubechies小波哈希、HSV颜色哈希和抗裁剪哈希。每种哈希函数都有其特定的属性,使其能够应对某些变化,例如亮度、对比度、伽玛值、校正、水印、压缩、缩放和灰度化。

图像哈希函数将图像映射到一个称为图像哈希的短字符串,可用于图像认证或作为数字指纹。

然而,有时两个视觉上不同的图像可能会得到相同的图像哈希,这称为碰撞。在包含101个对象的数据集中,我们展示了多个示例,但首先让我们评估最常用的哈希函数的鲁棒性。我们通过改变单张图像的亮度(-50%、-20%、+20%、+50%)、对比度(-50%、-20%、+20%、+50%)、缩放和压缩(png、jpg)来评估其鲁棒性。总共创建了 10 种不同的“猫和狗”图像(图 2)的修改版本。请注意,这并非评估碰撞,而是评估不同哈希函数对亮度和对比度的影响。所有哈希函数均使用 Python 库 undouble 进行评估,而 undouble 又使用了图像哈希库 imagehash [3] 的功能。

图 2. 亮度和对比度变化的四个示例

4、计算哈希值前的预处理

在确定图像哈希值之前,需要进行以下预处理步骤:1. 去色,2. 归一化像素值,以及 3. 缩放图像。去色的原因是,识别图像所需的信息已存在于灰度通道中。此外,将 RGB 图像的每像素 24 位减少到每像素 8 位,在时间和内存方面都更具计算优势。下一步是将图像下采样/缩放到更小的尺寸。通常情况下,会选择 64 位哈希值,这意味着图像会被下采样到 8 x 8 像素。下面的代码块展示了如何使用不同的哈希函数计算图像哈希值。

pip install undouble
import cv2
from scipy.spatial import distance
import numpy as np
import matplotlib.pyplot as plt
from undouble import Undouble

methods = ['ahash', 'dhash', 'whash-haar']

for method in methods:
    # Initialize with Hash size of 8
    model = Undouble(method=method, hash_size=8)
    
    # Import example data
    targetdir = model.import_example(data='cat_and_dog')
    
    # Import with preprocessing: Grayscaling and scaling
    model.import_data(targetdir)
    
    # Compute image-hash for only the first image.
    hashs = model.compute_imghash(model.results['img'][0], to_array=False)
    
    # Print to screen
    image_hash = ''.join(hashs[0].astype(int).astype(str).ravel())
    print(f'{method } Hash:')
    print(f"Binary image hash: {image_hash}")
    print(f"Hex image hash: {(hex(int(image_hash, 2)))}")

    # Plot results
    img_g = cv2.imread(model.results['pathnames'][0], cv2.IMREAD_GRAYSCALE)
    img_r = cv2.resize(img_g, (8, 8), interpolation=cv2.INTER_AREA)

    # Make the figure
    fig, ax = plt.subplots(2, 2, figsize=(15, 10))
    ax[0][0].imshow(model.results['img'][0][..., ::-1])
    ax[0][0].axis('off')
    ax[0][0].set_title('Source image')
    ax[0][1].imshow(img_g, cmap='gray')
    ax[0][1].axis('off')
    ax[0][1].set_title('grayscale image')
    ax[1][0].imshow(img_r, cmap='gray')
    ax[1][0].axis('off')
    ax[1][0].set_title('grayscale image, size %.0dx%.0d' %(8, 8))
    ax[1][1].imshow(hashs[0], cmap='gray')
    ax[1][1].axis('off')
    ax[1][1].set_title(method + ' function')

5、计算平均哈希值

使用平均哈希函数创建图像签名的方法是将图像中每个像素的值与平均像素值进行比较。经过去色和缩放步骤后,每个像素的值都会被提取出来,并与所有像素值的平均值进行比较。在下面的示例中,我们将生成一个 64 位哈希值,这意味着图像被缩放到 8×8 像素。如果像素块中的值大于平均值,则其值为 1(白色),否则值为 0(黑色)。图像哈希值是通过将二进制数组展平为向量而创建的。

图 2. 图像哈希二进制值:111111111111110111111100111000000111000001000000011110000111111111
图 3. 10 张修改后图像的平均哈希值

如果我们对这 10 张修改后的图像运行平均哈希函数,我们可以看到图像哈希值相当稳定,只有细微的差异(图 3)。只有当亮度增加 50% 时,图像哈希值才会开始出现偏差。平均而言,所有组合的图像哈希值变化为 2.1。

6、计算感知哈希值

为了计算感知哈希值,我们首先需要对图像进行去色处理。之后,应用离散余弦变换 (DCT),先按行,再按列。像素将高频部分现在被裁剪为 8 x 8 像素。然后,将每个像素块与图像所有灰度值的中位数进行比较。如果像素块中的值大于中位数,则赋值为 1,否则赋值为 0。最终的图像哈希值是通过将二进制数组展平为向量得到的。

图 4. 感知哈希值:1110101011010001100101010011011011100100011011000000110111001010
图 5. 10 张修改后图像的感知哈希值

如果我们对这 10 张经过一些修改的图像运行感知哈希函数,则会检测到图像哈希值的各种细微差异(图 5)。亮度增加 50% 时,图像哈希值的偏差会更大。平均而言,所有组合的图像哈希值会有 4 个变化。

7、计算差值哈希值

计算差值哈希值首先需要对图像进行去色和缩放。然后,像素按顺序(每行从左到右)与其右侧相邻像素进行比较。如果位置 x 处的字节小于位置 (x+1) 处的字节,则其值为 1,否则值为 0。最后,将二进制数组展平为向量,得到最终的图像哈希值。

图 6. 图像哈希值:00011110000110010011001100010011000010010100001010000010110000001
图 7. 10 张修改后图像的差值哈希值

如果我们对经过一些修改的 10 张图像运行差分哈希函数,图像哈希值会产生一些细微的变化(图 7)。亮度增加 50% 时差异最大。平均而言,所有组合的图像哈希值变化为 3.8。

8、计算 Haar 小波哈希

计算 Haar 小波哈希也需要先对图像进行去色和缩放,就像差分哈希方法一样。然后对图像应用二维小波变换。将每个像素块与图像所有灰度值的中位数进行比较。如果像素块中的值大于中位数,则赋值为 1,否则赋值为 0。最后,将数组展平为向量,得到最终的图像哈希值。

图 8. 图像哈希值:1110011111101101110110011100000100000001000000011100000111111101
图 9. 10 张修改图像的 Haar 小波哈希值

比较这 10 张修改图像的哈希值,我们可以看到,尽管修改后的图像存在一些细微的变化,但 Haar 小波哈希值相当稳定。平均而言,所有组合的图像哈希值变化约为 2.5 次。

9、在真实数据集中查找(近乎)相同的图像

为了展示哈希函数在真实数据集上的性能,我们可以下载 Caltech 101 [2] 数据集并将其保存到本地磁盘。我们测试了 aHash、pHash、dHash 和小波哈希的重复图像检测性能。加州理工学院数据集包含 9144 张真实世界图像,分为 101 个类别,每个类别约有 40 到 800 张图像。每张图像的大小约为 300 x 200 像素。undouble 库的输入可以是所有图像的存储目录。所有子目录也将被递归分析(递归也可以关闭)。请注意,此数据集不包含相同图像的真实标签。因此,我们将对所有分组结果进行可视化检查,并描述每个哈希函数的结果。请参阅下面的代码块,了解如何在包含 101 个对象的数据集中检测具有相同图像哈希值的图像。

# Import library
from undouble import Undouble

# Initialize model
model = Undouble(method='phash', hash_size=8)

# Download dataset here: http://www.vision.caltech.edu/Image_Datasets/Caltech101/101_ObjectCategories.tar.gz
targetdir = 'C://101_ObjectCategories'
  
# Importing the files files from disk, cleaning and pre-processing
model.import_data(targetdir)

# Compute image-hash
model.compute_hash()

# [undouble] >INFO> Extracting images from: [C://101_ObjectCategories]
# [undouble] >INFO> [9144] files are collected recursively from path: [C://101_ObjectCategories]
# [undouble] >INFO> [9144] images are extracted.
# [undouble] >INFO> Reading and checking images.
# [undouble] >INFO> Reading and checking images.
# 100%|██████████| 9144/9144 [02:14<00:00, 67.95it/s] 
# 100%|██████████| 9144/9144 [00:02<00:00, 3514.21it/s]
# [undouble] >INFO> Compute adjacency matrix [9144x9144] with absolute differences based on the image-hash of [phash].

# Group images that are identical in image-hash, i.e. those with a hash difference of 0.
model.group(threshold=0)

# [undouble] >INFO> [38] groups with similar image-hash.
# [undouble] >INFO> [38] groups are detected for [79] images.

# print([*model.results.keys()])
#   * img: Preprocessed images
#   * pathnames: Absolute path location to image file
#   * filenames: Filename
#   * img_hash_bin: binary image hash
#   * img_hash_hex: hex image hash
#   * adjmat: adjacency matrix containing absolute hash differences between the images
#   * select_pathnames: Selected path locations that have image-hash score <= threshold
#   * select_scores: Image-hash scores of the selected images
#   * stats: Some simple statistics about the number of groups and imges detected.

# Plot the marked images
model.plot()

# Move the files
model.move_to_dir()

# -------------------------------------------------
# >You are at the point of physically moving files.
# -------------------------------------------------
# >[79] similar images are detected over [38] groups.
# >[41] images will be moved to the [undouble] directory.
# >[38] images will be copied to the [undouble] directory.

# >[C]ontinue moving all files.
# >[W]ait in each directory.
# >[Q]uit
# >Answer: W

# [undouble] >INFO> Working in dir: [C://101_ObjectCategories/BACKGROUND_Google]
# ><enter> to proceed to the next directory.
# >[C]ontinue to move all files.
# >[Q]uit.
# >Answer:

平均哈希函数检测到 135 个组,这些组可以链接到 335 张具有相同哈希值(阈值=0)的图像,输入哈希大小为 8(64 位)。尽管检测到了相同的图像,但大多数组都存在冲突,例如左上角和左下角的图像,和/或几乎相同的图像,例如摩托车(图 11)。将哈希大小增加到 16(256 位)后,检测到 28 个组,对应 64 张图像。没有出现冲突,但检测到了几乎相同的图像,例如摩托车。

图 11. 平均哈希值检测到 135 个组,共 335 张图像具有相同的哈希值。几乎所有组中都存在碰撞图像和近乎相同的图像

基于输入哈希值大小为 8(64 位),小波哈希函数检测到 141 个组,这些组可以关联到 513 张具有相同哈希值(阈值=0)的图像。目视检查显示,几乎所有组都包含碰撞图像或近乎相同的图像(图 12)。谁能想到草莓的图像哈希值竟然可以与摩托车的图像哈希值相似?将哈希值大小增加到 16(256 位),检测到 25 个组,共 51 张图像。虽然没有发现碰撞图像,但检测到了近乎相同的图像,例如摩托车。

图 12. 小波哈希值检测到 141 个组,共 513 张图像具有相同的哈希值。几乎每个组都包含碰撞图像或近乎相同的图像

差分哈希函数检测到 28 张图像可以与 31 张具有相同哈希值(阈值=0)的图像关联。目视检查未发现冲突,但检测到一些非常相似的图像(两辆摩托车)。将哈希值大小增加到 16(256 位)后,检测到 8 组图像,每组包含 16 张图像。未发现冲突,但存在一些非常相似的图像,例如摩托车。将哈希值大小增加到 16(256 位)后,检测到 8 组图像,每组包含 16 张图像。未发现冲突,也未发现非常相似的图像,所有图像在视觉上都很相似。

图 13. 差分哈希函数检测到 28 组图像,每组包含 31 张具有相同哈希值的图像。未观察到冲突,仅存在 1 张图像包含非常相似的图像(摩托车)

感知哈希函数检测到 38 组图像,每组图像可以与 41 张具有相同哈希值(阈值=0)的图像关联。目视检查未发现碰撞,但检测到了近乎相同的图像,例如摩托车,如图 14 所示。将哈希大小增加到 16(256 位)后,检测到 10 组共 20 张图像。未检测到碰撞,也未检测到近乎相同的图像,所有图像在视觉上都很相似。

图 14. 感知哈希检测到 38 组共 41 张图像,且哈希值相同。未观察到碰撞,但在 4 组中存在近乎相同的图像

10、在真实图像中使用哈希函数的要点

在“猫和狗图像”实验中,每种哈希函数的结果都相当稳定。然而,当我们使用真实数据集时,很明显,对于近乎相同的图像分组,平均哈希和小波哈希在哈希大小为 8(64 位)时都会导致大量碰撞。差分哈希的结果准确,但也最为保守。另一方面,感知哈希也展现出了准确的结果,但保守性稍差。当哈希大小增加到 16(256 位)时,所有哈希函数均未出现冲突,但平均哈希和小波哈希检测到了几乎相同的图像。感知哈希和差分哈希均未出现冲突或几乎相同的图像。在此情况下,感知哈希再次表现最佳。

感知哈希在检测重复图像方面最为准确。

此外,结果也可能取决于输入图像的类型。当图像具有纯色背景时,例如摩托车和交通标志,更容易发生冲突。原因之一是二值像素信息表征了图像并形成了图像。哈希值会变得不那么唯一。例如,背景为纯色的图像,如果中心包含任何较暗的物体,使用平均哈希值和小波哈希值都很容易得到相同的图像哈希值。

11、哈希函数还是无监督聚类?

哈希函数和聚类方法都旨在将相似的图像分组,但两者之间存在差异。哈希函数可以创建图像的“指纹”或“签名”,即图像哈希值。单个哈希值即可用于轻松检测相同的图像。在聚类中,首先提取特征,选择距离度量和连接类型,最后对图像进行聚类。由于不同的步骤和/或方法会隐式地对数据施加结构,从而影响样本的划分,因此逻辑上会产生不同的分组结果。或者换句话说,定义“相似”图像的自由度更高。

哈希函数旨在创建可用于检测相同图像的唯一签名,而聚类方法旨在检测相似图像组。

为了比较哈希函数和聚类方法得到的结果,我将使用相同的 101 个对象的数据集,并进行聚类分析。使用 clustimage 库 [5] 对图像进行聚类,输入仅包含图像路径。递归地收集子目录中的所有图像。使用默认设置和 PCA 方法,检测到 63 个最佳聚类(图 15),图中显示了每个聚类的质心图像。虽然某些聚类包含高度相似的图像(图 15D),但图像并不一定完全相同或近似相同。

请参阅下面的代码块,了解对 101 个对象数据集进行聚类的示例。

# Import library
from clustimage import Clustimage

# init
cl = Clustimage(method='pca')

# Note that you manually need to download the data from the caltech website and simply provide the directory where the images are stored.
# Download dataset
targetdir = 'C://101_ObjectCategories'

# Cluster  images in path location.
results = cl.fit_transform(targetdir, min_clust=60, max_clust=110)

# If you want to experiment with a different clustering and/or evaluation approach, use the cluster functionality.
# This will avoid pre-processing, and performing the feature extraction of all images again.
# You can also cluster on the 2-D embedded space by setting the cluster_space parameter 'low'
#
# cluster(cluster='agglomerative', evaluate='dbindex', metric='euclidean', linkage='ward', min_clust=15, max_clust=200, cluster_space='high')

# Cluster evaluation plots such as the Silhouette plot
cl.clusteval.plot()
cl.clusteval.scatter(cl.results['xycoord'])

# PCA explained variance plot
cl.pca.plot()

# Dendrogram
cl.dendrogram()

# Plot unique image per cluster
cl.plot_unique(img_mean=False)

# Scatterplot
cl.scatter(dotsize=8, zoom=0.2, img_mean=False)

# Plot images per cluster or all clusters
cl.plot(labels=8)
图 15. Caltech101 数据集 [2] 下载链接。A. 轮廓系数检测到 63 个聚类时达到最优。B. 使用检测到的聚类标签对样本进行着色的 tSNE 嵌入。C. 并非所有聚类都产生高度相似的图像。D. 在第 13 个聚类中检测到的图像大多是钢琴图像

12、AI生成图像 vs .真实图像:哈希函数有用吗?

近年来,人工智能生成的图像数量大幅增加。一个自然的问题是,图像哈希函数是否可以用来区分真实照片和伪造(合成)照片?乍一看,这似乎合情合理:如果伪造图像遵循某些视觉模式,从而具有特定的特征,那么它们的哈希特征是否与真实照片的哈希特征存在系统性差异?

图像哈希函数非常擅长检测视觉相似性,但无法描述语义真实性。

哈希函数可以帮助识别完全相同或几乎相同的图像,例如,当重复/伪造的图像被重新发布、调整大小或通过裁剪、压缩或亮度调整等方式修改时。这可以帮助您标记与已知合成内容极其相似的可疑图像。从这个意义上讲,哈希函数非常适合追踪和去重人工智能生成的图像。

然而,当您只有一张图像时,它无法判断图像是真实的还是人工智能生成的。因此,哈希函数本身并不是可靠的伪造检测工具,因为它们会刻意丢弃高频信息和语义信息,以保持对细微图像变换的鲁棒性。因此,真实照片和合成图像可能会产生非常相似的哈希值。换句话说,哈希函数回答的是“这些图像看起来相似吗?”,而不是“这张图像是由人工智能生成的吗?”。

13、结束语

本文探讨了利用哈希函数检测(近乎)相同图像的概念。感知哈希函数被推荐用于检测重复图像。从这个意义上讲,哈希还可以用于追踪和去重已知经人工智能修改的图像。借助 undouble 库,可以相对轻松地检测整个系统或目录中的(近乎)相同图像。该库将图像预处理(灰度化、归一化和缩放)、计算图像哈希值以及基于图像哈希值差异对图像进行分组的过程流水线化(图 10)。阈值为 0 会将具有相同图像哈希值的图像分组。然而,在对我的个人照片集进行去重处理时,阈值为 10 时效果最佳,因为这样也能将连拍等照片分组。可以使用绘图功能轻松查看结果,并使用 move_to_dir 功能将图像去重。对于动态图像,我们将复制组中分辨率最高的图像,并将所有其他图像移动到新创建的“undouble”子目录中。如果您需要更灵活地分组图像,我建议您阅读我关于图像聚类的博客[4],其中介绍了如何使用clustimage库[5]进行图像聚类。


原文链接:Detection of (Near) Identical Images Using Image Hash Functions.

汇智网翻译整理,转载请标明出处