感知散列检测重复图像

Duplicate image detection with perceptual hashing in Python

为了实现对大量图片的重复检测,我们用python实现了dHash感知散列算法和漂亮的BK树数据结构。

我们的网站有着近十五万的高清旅游照片,而且还在不断增加。这些照片来自各种来源,并以半自动的方式上传。所以经常有重复或几乎相同的照片,但是我们不希望我们的图片搜索页面充满重复。

所以我们决定使用感知散列算法(perceptual hashing)自动化过滤重复图片。感知散列就如普通哈希一样,它是一个比较大的数据片段的相对小的可比较的指纹。但它与典型的哈希算法不同在于,如果原始图像是相近的,感知散列值也"相近"(或相等)。

差分散列(dHash)

  • 我们使用的感知哈希名为dHash,是由Neal Krawetz在他的照片取证工作中开发的。它非常简单而惊人的有效。该算法包含以下步骤(生成一个128位的哈希值):

    • 将图片转为灰阶
    • 缩小为9x9平方的灰度值(或者大点为17x17,512位哈希值))
    • 计算"行哈希":对每一行从左到右移动,如果下一个像素点的灰度值大于或等于上一个,则输出1,否则输出0(每行9个像素点,产生8位的输出)
    • 计算"列哈希":就像行哈希,但对每列从上到下移动
    • 将上面计算出的两个64位值串联到一起,合成最终的128位哈希值
  • dHash很棒,因为它相当准确,而且非常易于理解和实现。它也非常快速(Python可能不擅长位操作,但是灰度化和缩小图像这些艰苦的工作都由C库完成了:ImageMagick+wand 或 PIL,封装好供python使用)

  • 下面是形象化的图像处理过程:



    灰度化并缩小到9x9(放大了查看):


    感知散列检测重复图像_第1张图片

    以及最后的8x8的行和列的哈希值,也放大了(黑色为0,白色为1):
    感知散列检测重复图像_第2张图片
  • dHash的核心代码只是简单的嵌套for循环:

def dhash_row_col(image, size=8):
    width = size + 1
    grays = get_grays(image, width, width)

    row_hash = 0
    col_hash = 0
    for y in range(size):
        for x in range(size):
            offset = y * width + x
            row_bit = grays[offset] < grays[offset + 1]
            row_hash = row_hash << 1 | row_bit

            col_bit = grays[offset] < grays[offset + width]
            col_hash = col_hash << 1 | col_bit

    return (row_hash, col_hash)
  • 它很简单,但也会有几个棘手的案例,所以我们很nice地将代码开源到GitHub以及Python Package Index——只需要 pip install dhash即可使用。

重复的阈值

  • 为了判断一个图片是否重复,可以比较它们的dHash值。如果值相同,那么图片几乎是一样的。如果它们的哈希值只有几位不同,那么图片是相似的——所以你需要计算两个值之间位不同的数量(汉明距离),然后判断它是否低于给定的阈值。
    -在我们的dhash库中有个辅助函数get_num_bits_different()来计算这个差值。奇怪的是,Python中最快的处理方式是异或两个值,将结果转换为二进制字符串,然后计算'1'的数量(这是因为用C编写的内建函数来做艰苦的工作和循环):
def get_num_bits_different(hash1, hash2):
    return bin(hash1 ^ hash2).count('1')
  • 对我们的图片集(总共超过20w),我们设置128位的dHash阈值为2。也就是说,如果哈希值相等或只有1-2位不同,就认为它们是重复的。在我们的测试中,这是一个足够大的差值来捕获大多数重复图片。当我们尝试设置为4或5时,它开始出现异常——看上去十分不同的图像会具有相似的指纹。
  • 例如,下面这对图片让我们将阈值定为2——它们只有4位的差异:


    感知散列检测重复图像_第3张图片

MySQL位计数

  • 尽管我是PostgreSQL的狂热粉,但是我们在这个项目使用了MySQL,它具有一个干净快捷的小功能,而PostgreSQL不具有, 那就是BIT_COUNT——计算64位整数的1的位数。因此,如果将128位哈希分解成两部分,你可以使用两个BIT_COUNT()调用来确定二进制哈希列是否在给定哈希的n位之内。
  • 这有点迂回,因为MySQL似乎没有办法将二进制列的一部分转换为64位整数。所以我们转换为16进制又转换回来(如果你知道有更好的办法希望能告诉我们)。SQL表中的dHash列名为dhash8,而dhash8_0dhash8_1分别是我们进行比较的新哈希值的高低64位字面值。
  • 所以这里是我们上传新图像时用来检测重复的查询(嗯,实际上我们用的是SQLAlchemy ORM,不过也没差了)(SQL语句参看):
SELECT asset_id, dhash8
FROM assets
WHERE
    BIT_COUNT(CAST(CONV(HEX(SUBSTRING(dhash8, 1, 8)), 16, 10)
        AS UNSIGNED) ^ :dhash8_0) +    -- high part
    BIT_COUNT(CAST(CONV(HEX(SUBSTRING(dhash8, 9, 8)), 16, 10)
        AS UNSIGNED) ^ :dhash8_1)      -- plus low part
    <= 2                               -- less than threshold?
  • 以上是一个相对较慢的查询,涉及全表扫描,但是我们只在上传时做一次,所以花费额外的一两秒不是什么大事。

BK树与更快的重复检测

  • 但是,当我们在整个图片集中搜索存在的重复时,它很快就变成了一个O(N^2)复杂度问题——对每一个图片要遍历所有其它图片来查找是否重复。对数十万的图片,这太慢了,所以我们需要改进——BK树。
  • 一个BK树是一个n元树数据结构,专门为快速找到相近的匹配而设计。例如,找到给定字符串的某个编辑距离内的其它字符串。或者在我们的案例中,找到给定dHash值的某个位距离内的其它dHash值。这将一个O(N^2)问题转化为近于O(logN)问题。(作者的幽默 - We discovered a truly marvelous proof of this, but the margin is too narrow to contain it.)
  • BK树在Nick Johnson的"非常酷的算法"系列博客中有阐述。(在国人Matrix67的博客中也有清晰的描述,很容易理解)
  • BK树的结构相当简单,特别是在python中,只需要一堆嵌套的字典(与元组)即可。下面是BKTree.add()函数的代码,用于为BK树添加一个子项:
def add(self, item):
    node = self.tree
    if node is None:
        self.tree = (item, {})
        return

    while True:
        parent, children = node
        distance = self.distance_func(item, parent)
        node = children.get(distance)
        if node is None:
            children[distance] = (item, {})
            break
  • 有一些现成的BK树的实现,不过这一个好像对我们的使用来说有些问题(ryanfox/bktree),另一个看上去不错但没放在PyPI上 (ahupp/bktree),所以我们自己弄了个——GitHub与Python Package Index - pip install pybktree安装。

讨论

  • Hacker News 和 reddit Python参与讨论。

你可能感兴趣的:(感知散列检测重复图像)