感知散列检测重复图像

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+wandPIL,封装好供python使用)

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



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



    以及最后的8x8的行和列的哈希值,也放大了(黑色为0,白色为1):
  • 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位的差异:


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),所以我们自己弄了个——GitHubPython Package Index - pip install pybktree安装。

讨论

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容