关于图像指纹的一些想法

2019-08-11

结论

哈希算法不论准确性还是实效性,均高于直接使用灰度直方图或者rgb直方图。但由于测试数据集需要手动处理,所以数据集较小。以至于第一次测试效果中,三种哈希算法差异性不大。

TODO

  1. 数据集过小,预期在本周内,将测试集的数量增加至200+。除了手动为图片增加水印、滤镜、截取部分外,寻找已有数据集补充。
  2. 命中判断标准过于简单,预期在本周内寻找到更加合理的判断标准,考虑与已有的机器学习模型结合
  3. 重点比较pHash与dHash的差异性

介绍

图像指纹与音频指纹的表现形式类似,都是将原始文件安装一定的哈希算法进行转换。最后得出一个或者一组能够代表图像特征的值。 针对于图像指纹的提取,常见的哈希算法有几种:平均哈希算法(aHash)、感知哈希算法(pHash)、差异值哈希算法(dHsh)以及最为简单的加密哈希MD5. 由于MD5算法的敏感度极高,内容稍微有所差异,哈希出来的结果都会有极大的差异。所以我们只能将其作为最表层的检查方式。在提取图像指纹中,我们只考虑aHash、pHash与dHash。 同时为了比较效果,引入普通的直方图判断作为参考

平均哈希算法(aHash)

aHash理论实现方法如下:

  1. 图像缩放: 将图像缩放到8*8大小。
  2. 灰度化: 对8*8大小的图像进行灰度化。
  3. 计算均值: 计算这8*8大小图片中64个像素的均值。
  4. 得到88图像的ahash: 88的像素值中大于均值的则用1表示,小于的用0表示,这样就得到一个64位二进制码作为该图像的ahash值。 计算两幅图像ahash值的汉明距离,如果不相同的数据位不超过5,就说明两张图片很相似,如果大于10,就说明这是两张不同的图片。

因为aHash最终的结果只保留了实际值对于自身均值的比较结果。在实际应用中过于严格与简单。唯一的优点在于实现简单、速度较快,不受图片大小缩放的影响,但是图片的内容不能变更。如果在图片上加几个文字,它就认不出来了。所以,它的最佳用途在于根据缩略图,找出原图,与我们视频判重的应用场景不是非常的符合。

感知哈希算法(pHash)

与aHash在一定程度上类似,也是与均值相比较得出最终的结果。但是为了弥补aHash精度上的不足,pHash使用离散余弦变换(DCT)来获取图片的低频成分。DCT能将图像从像素域变换到频率域,一般图像都存在很多冗余和相关性的,转换到频率域之后,只有很少的一部分频率分量的系数才不为0,大部分系数都为0(或者说接近于0)。所以我们只用保留左上角的8*8大小的部分。

  1. 图像缩放: 将图像缩放到3232大小。虽然我们只需要88的大小,但是3232便于DTC计算,最后只需要截取左上角的88。
  2. 灰度化: 对图像进行灰度化。
  3. 计算DCT: 计算图片的DCT变换,得到32*32的DCT系数矩阵。
  4. 缩小DCT: 虽然DCT的结果是3232大小的矩阵,但我们只要保留左上角的88的矩阵,这部分呈现了图片中的最低频率。
  5. 计算平均值: 如同均值哈希一样,计算DCT的均值。
  6. 计算hash值: 这是最主要的一步,根据8*8的DCT矩阵,设置0或1的64位的hash值,大于等于DCT均值的设为”1”,小于DCT均值的设为“0”。组合在一起,就构成了一个64位的整数,这就是这张图片的指纹。 最后,pHash也能像aHash一样,通过海明距离进行相似度比较。

差异值哈希算法(dHash)

  1. 图像缩放: 将图片收缩到9*8的大小,以便它有72的像素点。
  2. 灰度话: 转化为灰度图。
  3. 计算差异值: dHash算法工作在相邻像素之间,这样每行9个像素之间产生了8个不同的差异,一共8行,则产生了64个差异值。
  4. 计算hash值: 如果左边的像素比右边的更亮,则记录为1,否则为0。

算法比较

实验设计,将图片分为三类:基础图片、重复图片、不重复图片 基础图片:作为重复图片与不重复图片的依据,40张 重复图片:基础图片中出现过的图片,以及添加水印后的图片,10张,包括3张未变动,3张添加水印,2张添加滤镜,2张截取部分 不重复图片:没有在基础图片中出现过的图片

计分标准:不重复图片未命中基础图片误伤性+1,重复图片命中基础图片准确度+1,统计记录三种哈希算法的总耗时

单通道直方图: 在重合度>0.6时记为命中,误伤4,准确8,总耗时5.87s,单次比对耗时0.010s

三通道直方图: 在重合度>0.6时记为命中,误伤3,准确8,总耗时8.89s,单次比对耗时0.014s

aHash:汉明距离小于5记为命中,误伤0,准确7,总耗时4.39s,单次比对耗时0.0065s

pHash:汉明距离小于5记为命中,误伤0,准确7,总耗时4.48s,单次比对耗时0.0063s

dHash:汉明距离小于5记为命中,误伤0,准确7,总耗时4.11s,单次比对耗时0.0058s

测试代码:

import cv2
import numpy as np
import os
import time
#灰度直方图
def classify_gray_hist(image1, image2, size=(256, 256)):
    # 先调整大小
    image1 = cv2.resize(image1, size)
    image2 = cv2.resize(image2, size)
    hist1 = cv2.calcHist([image1], [0], None, [256], [0.0, 255.0])
    hist2 = cv2.calcHist([image2], [0], None, [256], [0.0, 255.0])
    # 计算直方图的重合度
    degree = 0
    for i in range(len(hist1)):
        if hist1[i] != hist2[i]:
            degree = degree + (1 - (abs(hist1[i] - hist2[i]) / max(hist1[i], hist2[i])))
        else:
            degree = degree + 1
    degree = degree / len(hist1)
    return degree


def classify_hist_with_split(image1, image2, size=(256, 256)):
    # 将图像resize后,分类为三个通道,再计算每个通道的相似值
    image1 = cv2.resize(image1, size)
    image2 = cv2.resize(image2, size)
    sub_image1 = cv2.split(image1)
    sub_image2 = cv2.split(image2)
    sub_data = 0
    for im1, im2 in zip(sub_image1, sub_image2):
        sub_data += calculate(im1, im2)
    sub_data = sub_data / 3
    return sub_data


def calculate(im1, im2):
    hist1 = cv2.calcHist([im1], [0], None, [256], [0.0, 255.0])
    hist2 = cv2.calcHist([im2], [0], None, [256], [0.0, 255.0])
    degree = 0
    for i in range(len(hist1)):
        if hist1[i] != hist2[i]:
            degree = degree + (1 - (abs(hist1[i] - hist2[i]) / max(hist1[i], hist2[i])))
        else:
            degree = degree + 1
    degree = degree / len(hist1)
    return degree


# 平均哈希算法计算
def classify_aHash(image1, image2):
    image1 = cv2.resize(image1, (8, 8))
    image2 = cv2.resize(image2, (8, 8))
    gray1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY)  # 切换至灰度图
    gray2 = cv2.cvtColor(image2, cv2.COLOR_BGR2GRAY)
    hash1 = getHash(gray1)
    hash2 = getHash(gray2)
    return Hamming_distance(hash1, hash2)


def classify_pHash(image1, image2):
    image1 = cv2.resize(image1, (32, 32))
    image2 = cv2.resize(image2, (32, 32))
    gray1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY)  # 切换至灰度图
    gray2 = cv2.cvtColor(image2, cv2.COLOR_BGR2GRAY)
    # 将灰度图转为浮点型,再进行dct变换
    dct1 = cv2.dct(np.float32(gray1))
    dct2 = cv2.dct(np.float32(gray2))
    # 取左上角的8*8,这些代表图片的最低频率
    # 这个操作等价于c++中利用opencv实现的掩码操作
    # 在python中进行掩码操作,可以直接这样取出图像矩阵的某一部分
    dct1_roi = dct1[0:8, 0:8]
    dct2_roi = dct2[0:8, 0:8]
    #print(dct1)
    hash1 = getHash(dct1_roi)
    hash2 = getHash(dct2_roi)
    return Hamming_distance(hash1, hash2)


# 输入灰度图,返回hash
def getHash(image):
    avreage = np.mean(image)  # 计算像素平均值
    hash = []
    for i in range(image.shape[0]):
        for j in range(image.shape[1]):
            if image[i, j] > avreage:
                hash.append(1)
            else:
                hash.append(0)
    return hash


# 计算汉明距离
def Hamming_distance(hash1, hash2):
    num = 0
    for index in range(len(hash1)):
        if hash1[index] != hash2[index]:
            num += 1
    return num


# 差异值哈希算法
def dhash(image1, image2):
    image1 = cv2.resize(image1, (9, 8))
    image2 = cv2.resize(image2, (9, 8))
    gray1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY)  # 切换至灰度图
    gray2 = cv2.cvtColor(image2, cv2.COLOR_BGR2GRAY)
    hash1 = dhashcaulate(gray1)
    hash2 = dhashcaulate(gray2)
    return Hamming_distance(hash1, hash2)


def dhashcaulate(gray):
    hash_str = ''
    for i in range(8):
        for j in range(8):
            if gray[i, j] > gray[i, j + 1]:
                hash_str = hash_str + '1'
            else:
                hash_str = hash_str + '0'
    return hash_str


if __name__ == '__main__':
    base_path = './base'
    repeat_path = './repeat'
    unrepeat_path = './unrepeat'
    A = 0
    cnt = 0
    B = 0
    T = time.time()
    for unrepeat in os.listdir(unrepeat_path):
        imgobj1 = cv2.imread('./unrepeat/' + unrepeat)
        for base in os.listdir(base_path):
            imgobj2 = cv2.imread('./base/' + base)
            cnt = cnt + 1
            #degree = classify_gray_hist(imgobj1,imgobj2)   #单通道直方图
            #degree = classify_hist_with_split(imgobj1,imgobj2)  #三通道直方图
            #degree = classify_aHash(imgobj1,imgobj2)          #平均哈希法
            #degree = classify_pHash(imgobj1,imgobj2)          #感知哈希法
            degree = dhash(imgobj1, imgobj2)  # 差异值哈希算法
            if degree < 5:
                A = A + 1
                break

    for repeat in os.listdir(repeat_path):
        imgobj1 = cv2.imread('./repeat/' + repeat)
        for base in os.listdir(base_path):
            imgobj2 = cv2.imread('./base/' + base)
            cnt = cnt + 1
            #degree = classify_gray_hist(imgobj1,imgobj2)   #单通道直方图
            #degree = classify_hist_with_split(imgobj1,imgobj2)  #三通道直方图
            #degree = classify_aHash(imgobj1,imgobj2)          #平均哈希法
            #degree = classify_pHash(imgobj1,imgobj2)          #感知哈希法
            degree = dhash(imgobj1, imgobj2)  # 差异值哈希算法
            if degree < 5 :
                #print (degree)
                B = B + 1
                break
    FT = time.time();
    print("unrepeat: " + str(A) +", repeat:"+  str(B) + ", time: " + str(FT - T)+", avg_time:" + str((FT-T)/cnt) )

所有文章