使用哈希函数查找重复图像

177 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第16天,点击查看活动详情

前言

在本节中,我们将讨论图像搜索相关问题,即使用基于哈希函数的方法来解决问题图像搜索问题。

哈希函数

哈希函数可以将任意长度的数据字符串映射到固定长度的输出,哈希函数本身具有确定性和公开性,但是映射结果应该看起来似乎是“随机”的,函数表达式为 y=hash(key)。在实践中,哈希函通常用于压缩输入数据。哈希函数应当具有抗碰撞性,也就是说,对于两个输入 M1M2,设计良好的哈希函数应当满足 h(M1)h(M2)h(M1)≠h(M2)

MD5 (Message Digest 5) 是一种广泛使用的哈希函数,该函数可以生成 128 位哈希值。即该算法可以接受任意长度的输入,并得到长度为 128 位的输入结果。尽管 MD5 最初设计用作加密哈希函数,现在已证明它具有脆弱性,但该算法仍被用于验证文件的完整性和真实性。MD5 相较于其他加密哈希函数的优势在于,它的执行更为高效。在本节中,我们将介绍如何使用 hashlib 检测重复图像。

我们使用图像内容作为 MD5 哈希函数的键 (key),然后计算图像集合中所有图像的十六进制哈希值( 128 位)。如果集合中存在彼此相同的图像,它们将具有相同的十六进制哈希值。通过比较哈希值,我们可以判断两张图像的内容是否相同。

但是,MD5 容易受到碰撞性的影响,这意味着有可能即使两个图像并不相同,它仍然有可能会产生相同的哈希值,但这种情况在哈希空间较大时极为罕见。

查找重复图像

(1) 首先,导入所需的库:

import hashlib, os
from glob import glob
import matplotlib.pylab as plt
from skimage.io import imread

hashlib 模块实现了 RSAMD5 哈希算法接口,其使用方法如下,创建以图像内容作为 MD5 对象的键,并计算哈希值。给定一个任意长度(即,对于任何大小的图像)的键,MD5 哈希函数将返回具有固定长度( 128 位)的哈希值。

(2) 计算图像的十六进制哈希值,然后计算位长度:

hex_digest = hashlib.md5(open('1.png', 'rb').read()).hexdigest()
bin_digest = format(int(str(hex_digest), 16), "040b")
print('MD5 digest = {} ({})'.format(hex_digest, bin_digest) )
print('length of hex digest = {} bytes'.format(len(hex_digest)))
print('length of bin digest = {} bits'.format(len(bin_digest)))
# MD5 digest = 284e09c26262ba709b9c54016c3ee197 (101000010011100000100111000010011000100110001010111010011100001001101110011100010101000000000101101100001111101110000110010111)
# length of hex digest = 32 bytes
# length of bin digest = 126 bits

从以上输出可以看出,哈希值的长度为 128 位。

(3) 接下来,我们实现函数 find_duplicates,该函数以目录名称作为输入,获取目录下的所有图像(带有 .jpg.png 扩展名),然后返回找到的重复图像列表,每个列表包含两个或多个内容相同的图像名。在函数中,计算所有图像的十六进制哈希值,使用 Python 词典将重复的项目插入其中,字典的键是计算出的十六进制摘要,其值是具有给定的十六进制哈希值的文件名列表。

如果字典中已经存在一个具有相同的十六进制键,则表示它是一个重复的图像,我们将文件名添加到该键的相应列表中。最后,返回具有多个文件名的键值对列表作为重复图像:

def find_duplicates(dir_name):
    def is_image(file_name):
        f = file_name.lower()
        return f.endswith(".png") or f.endswith(".jpg")

    hash_keys = dict()
    for file_name in glob(dir_name):
        if os.path.isfile(file_name) and is_image(file_name):
            with open(file_name, 'rb') as f:
                file_hash = hashlib.md5(f.read()).hexdigest()
            if file_hash not in hash_keys:
                hash_keys[file_hash] = [file_name]
            else:
                hash_keys[file_hash].append(file_name)
    return [hash_keys[file_hash] for file_hash in hash_keys if len(hash_keys[file_hash]) > 1]

(4) 定义函数 show_duplicates 显示所有重复的图像,函数在 find_duplicates 返回的列表上迭代,在同一行上打印重复图像,并计算重复的次数:

def show_duplicates(duplicates):
    for duplicated in duplicates:
        try:
            plt.figure(figsize=(20,10))
            plt.subplots_adjust(0,0,1,0.9,0.05,0.05)
            for (i, file_name) in enumerate(duplicated):
                plt.subplot(1, len(duplicated), i+1)
                plt.imshow(imread(file_name))
                plt.title(file_name, size=10)
                plt.axis('off')
            plt.suptitle('{} duplicate images found with MD5 hash'.format(len(duplicated)), size=15)
            plt.show()
        except OSError as e:
            continue

(5) 最后,调用以上函数查找重复图像,并进行显示:

duplicates = find_duplicates('images/*.*')
print(duplicates)
show_duplicates(duplicates)

Figure_8.png

Figure_9.png