相似图像搜索

来源:互联网 发布:北京linux培训哪家好 编辑:程序博客网 时间:2024/06/05 00:40

1. 起因

前段时间面试刷经验的时候,和面试官聊到一个相似图像检索的问题,由于以前做过一个很小的demo,所以就 bulalala 的介绍了一番。某天中午吃饭的时候,第二次听到有人在探讨一个以图搜图的问题;这几天无意中又在网上第三次看到了这样一个类似的问题,利用图片指纹检测高相似度图片,看着好像很高端的样子,但是仔细一想,这不就是那个很简单的demo就能解决的问题嘛。所以我决定再次尝试一下。

2. 图像指纹

人的指纹可以作为人的标识,来区分你是你,不是他,主要是因为这个指纹独一无二,如果两个指纹相同,那么可以认为对应同一个人。就像如果人的血液 DNA 极其相近,那么也可以认为是一个人,或者是亲人。同样,如果几个图像的指纹相同或相似,那么可以认为它们对应相同或相似的图像。那么什么是图像的指纹呢?这是一个问题。

图像指纹可以理解为对图像进行的某种变换的结果,当然,这种变换要满足一下几个准则:

  1. 不同的图像得到的变换应该不同,或者说只有相同或相似图像变换后的结果才相同或相似。
  2. 这种变换不能过于复杂,不然建立指纹库或者查询的时间会过于复杂。
  3. 输出结果不能过于复杂,方便指纹库的存储

在本文中我们使用三种指纹,指纹的大小都是 8x8=64 ,以 Lena 图为例:

这里写图片描述

1. 第一种指纹

称为 hash 法。

  1. 输入图像,如果是 RGB 图像则转换为灰度图像
  2. 缩放,将图像缩放为 8x8 大小的图像
  3. 求均值,计算此 8x8 图像所有像素的均值
  4. 量化,比较 8x8 图像中每一个像素像素与均值大小关系,如果大于均值,则指纹上该位对应的值为1;否则,为0。
  5. 组合,将这64bit的0和1组成这幅图像的指纹。

输出如下所示:

1101000100000000101000001110000011111111111000111001000000000000

当然,为了方便,可以把上述64bit写成16进制数:

D100A0E0FFE39000

matlab 代码如下:

function imageHashValue = ImageHash(image)% 计算图形的哈希值,返回一个64位字符串% 输入:image 矩阵 输入图像% 输出:imageHashValue 字符串 图像对应的哈希值hashFingerSize = 8; % 决定哈希值的长度if(size(image, 3) == 3)    image = rgb2gray(image); % rgb转为灰度endimage = double(imresize(image, [hashFingerSize, hashFingerSize])); % 调整大小imageHashValue = image(:) > mean(image(:)); % 计算哈希值

opencv 代码如下:

/** 功能:利用 hash 法计算图像指纹* 输入:image cv::Mat 输入图像* 输出:hash uint64_t 计算得到的指纹*/uint64_t ImageHash(cv::Mat image){    uint64_t hash = 0; // 用于保存hash值    cv::Mat imageGray; // 转换后的灰度图像    cv::Mat imageFinger; // 缩放后的8x8的指纹图像    int fingerSize = 8; // 指纹图像的大小    if (3 == image.channels()) // rgb -> gray    {        cv::cvtColor(image, imageGray, CV_RGB2GRAY);    }    else    {        imageGray = image.clone();    }    cv::resize(imageGray, imageFinger, cv::Size(fingerSize, fingerSize)); // 图像缩放    imageFinger.convertTo(imageFinger, CV_32F); // 转换为浮点型    cv::Scalar imageMean = cv::mean(imageFinger); // 求均值    /* 计算图像哈希指纹,小于等于均值为0,大于为1 */    for (int i = 0; i < fingerSize; i++)    {        float* data = imageFinger.ptr<float>(i);        for (int j = 0; j < fingerSize; j++)        {            if (data[j] > imageMean[0])            {                hash = (hash << 1) + 1;            }            else            {                hash = hash << 1;            }        }    }    return hash;}

2. 第二种指纹

称为 phash 法。

  1. 输入图像,如果是 RGB 图像则转换为灰度图像
  2. 缩放,将图像缩放为 32x32 大小的图像,注意此时不是 8x8
  3. DCT变换,对上述 32x32 的图像进行dct变换
  4. 取低频,对图像进行变换后的大小为 32x32,如果以此作为图像的指纹会浪费太多空间,所以此处只取低频部份,也就是变换后的左上 8x8 的块
  5. 求均值,计算此 8x8 块的所有像素的均值
  6. 量化,比较 8x8 块中每一个像素与均值大小关系,如果大于均值,则指纹上该位对应的值为1;否则,为0。
  7. 组合,将这64bit的0和1组成这幅图像的指纹。

输出如下所示:

1100101111100001011101101111000010100100111100000001011010010111

当然,为了方便,可以把上述64bit写成16进制数:

F445821C820CAF3

matlab 代码如下:

function imageHashValue = ImagePHash(image)% 计算图形的哈希值,返回一个64位字符串% 输入:image 矩阵 输入图像% 输出:imageHashValue 字符串 图像对应的哈希值hashFingerSize = 8; % 决定哈希值的长度dctSize = 32; % DCT 变换矩阵大小if(size(image, 3) == 3)    image = rgb2gray(image); % rgb转为灰度endimage = dct2(double(imresize(image, [dctSize, dctSize]))); % 调整大小并进行DCT变换image = image(1:hashFingerSize, 1:hashFingerSize); % 取左上的低频部分image = log(abs(image)); % 取对数imageHashValue = image(:) > mean(image(:)); % 计算哈希值

matlab 代码如下:

/** 功能:利用 phash 法计算图像指纹* 输入:image cv::Mat 输入图像* 输出:hash uint64_t 计算得到的指纹*/uint64_t ImagePHash(cv::Mat image){    uint64_t hash = 0; // 用于保存hash值    cv::Mat imageGray; // 转换后的灰度图像    cv::Mat imageFinger; // 缩放后的8x8的指纹图像    int fingerSize = 8; // 指纹图像的大小    int dctSize = 32; // dct变换的尺寸大小    if (3 == image.channels()) // rgb -> gray    {        cv::cvtColor(image, imageGray, CV_RGB2GRAY);    }    else    {        imageGray = image.clone();    }    cv::resize(imageGray, imageFinger, cv::Size(dctSize, dctSize)); // 图像缩放    imageFinger.convertTo(imageFinger, CV_32F); // 转换为浮点型    cv::dct(imageFinger, imageFinger); // 对缩放后的图像进行dct变换    imageFinger = imageFinger(cv::Rect(0, 0, fingerSize, fingerSize)); // 取低频区域    /* 对dct变换后的系数取对数 */    for (int i = 0; i < fingerSize; i++)    {        float* data = imageFinger.ptr<float>(i);        for (int j = 0; j < fingerSize; j++)        {            data[j] = logf(abs(data[j]));        }    }    cv::Scalar imageMean = cv::mean(imageFinger); // 求均值    /* 计算图像哈希指纹,小于等于均值为0,大于为1 */    for (int i = 0; i < fingerSize; i++)    {        float* data = imageFinger.ptr<float>(i);        for (int j = 0; j < fingerSize; j++)        {            if (data[j] > imageMean[0])            {                hash = (hash << 1) + 1;            }            else            {                hash = hash << 1;            }        }    }    return hash;}

3. 第三种指纹

称为 dhash 法。

  1. 输入图像,如果是 RGB 图像则转换为灰度图像
  2. 缩放,将图像缩放为 8x9 大小的图像,注意,此时不是 8x8
  3. 量化,比较 8x9 图像中的前 8 列 像素中每一个像素与它右侧像素的大小,如果大于右侧像素,则指纹上该位对应的值为1;否则,为0。
  4. 组合,将这64bit的0和1组成这幅图像的指纹。

输出如下所示:

1101110010011111100110110110010000101101101111001110000110010111

当然,为了方便,可以把上述64bit写成16进制数:

DC9F9B642DBCE197

matlab 代码如下:

function imageHashValue = ImageDHash(image)% 计算图形的哈希值,返回一个64位字符串% 输入:image 矩阵 输入图像% 输出:imageHashValue 字符串 图像对应的哈希值hashFingerSize = 8; % 决定哈希值的长度if(size(image, 3) == 3)    image = rgb2gray(image); % rgb转为灰度endimage = double(imresize(image, [hashFingerSize+1, hashFingerSize])); % 调整大小imageHashValue = image(1:hashFingerSize, :) > image(2:hashFingerSize+1, :); % 计算哈希值imageHashValue = imageHashValue(:);

matlab 代码如下:

/** 功能:利用 dhash 法计算图像指纹* 输入:image cv::Mat 输入图像* 输出:hash uint64_t 计算得到的指纹*/uint64_t ImageDHash(cv::Mat image){    uint64_t hash = 0; // 用于保存hash值    cv::Mat imageGray; // 转换后的灰度图像    cv::Mat imageFinger; // 缩放后的8x8的指纹图像    int fingerSize = 8; // 指纹图像的大小    if (3 == image.channels()) // rgb -> gray    {        cv::cvtColor(image, imageGray, CV_RGB2GRAY);    }    else    {        imageGray = image.clone();    }    cv::resize(imageGray, imageFinger, cv::Size(fingerSize+1, fingerSize)); // 图像缩放    /* 计算图像哈希指纹,对于前八列的点,如果某个点大于它右侧的点,则为1,否则为0 */    for (int i = 0; i < fingerSize; i++)    {        float* data = imageFinger.ptr<float>(i);        for (int j = 0; j < fingerSize; j++)        {            if (data[j] > data[j+1])            {                hash = (hash << 1) + 1;            }            else            {                hash = hash << 1;            }        }    }    return hash;}

3. 建立指纹库

要找相似图像,首先我们需要有一个现成的数据集,作为后面的查找库。

假设已有数据集中有数以千计、万计的图像。分别计算每一个图像的指纹(或者称为哈希),然后建立该指纹与图像的一一对应关系,也就是说:知道指纹,就可以找到该图像。

4. 识别

要判断两幅图像是否相似,就是要判断它们的指纹是否相似,如果指纹相似则认为对应的两幅图像像素。那么怎么样的指纹才叫相似呢,这里用到了汉明距离,0与0的汉明距离为0,0与1的汉明距离为1,010与100的汉明距离为2,也就是说,针对二进制数,汉明距离可以理解为不同bit位的个数。如果两个指纹的所有bit位完全相同,可以认为这两个指纹相同,或者放宽松点,如果两个指纹只有0或1或2个bit位不同,则认为这两个指纹相似。

具体的识别过程如下所示:

  1. 输入待搜索图像
  2. 计算该图像的指纹
  3. 将该图像的指纹与数据库中的指纹进行对比
  4. 如果在数据库中找到与输入图像的指纹相似的指纹,则认为找到匹配的相似图像
  5. 输出找到的相似图像

注意:上述方式需要用输入指纹与数据库中的所有指纹进行对比,如果数据库较大,那么时间复杂度会比较高,为 O(n),n 为指纹库的大小,此时需要进行一些改进。

  • 使用哈希表
    将指纹组成十六进制数,作为查找的键值,这样就可以在 O(1) 的时间复杂度内找到是否存在匹配的指纹。此种方法依旧存在问题,也就是只能查找完全相同的指纹,而不能查找相似的指纹。
  • 使用 kd 树
    此时虽然复杂度降为 O(lg(n)),但是可以查找相似指纹,且不用遍历所有指纹,效果较好。
  • 使用并行查找
    要把输入图像的指纹与所有指纹库中的指纹库进行对比,可以使用并行的思路,每个线程复杂处理一部分的匹配。

5. 实验效果

本文采用CALTECH-101数据集进行测试,此数据集包含101个场景的共9144幅图像。

首先根据 hash 法、phash法、dhash法进行对比实验,试验组随机选择了CALTECH-101数据中的953个图像(随机产生1000个数,去重之后剩余953)。实验结果如下表所示:

方法 一个匹配 多个匹配 精度 hash 87 866 90.87% phash 5 948 99.48% dhash 5 948 99.48%

说明:


  1. 在匹配指纹时选择的阈值为1,也就是说汉明距离小于等于1则认为指纹匹配。
  2. 实际上 phash 组的结果里面并不是只有5个一一匹配的结果,还有22个如下图中的第三列所示,观察后发现这22组结果有明显的规律性,间距均为100,于是找到了其对应的分组,也就是CALTECH-101数据中的Leopards分组,该分组的前100个图像与后100个图像一一对应,呈左右镜像的形式,也就是说,下图中的第1348个图像左右反转之后就是第1448个图像,也就是说,phash法可以正确搜索出镜像的图像,而hash法和dhash法不能。

这是因为DCT变换具有一定的对称性,而phash法中计算指纹就是基于dct方法。

这里写图片描述

这里写图片描述
这里写图片描述

补充:

  1. 上述方法都不具有旋转不变性,也就是对于旋转的图像都无法识别。
  2. 第三种指纹相当去先将图像缩放,然后求水平差分,然后量化为0和1。

6. 代码

GitHub

0 0
原创粉丝点击