用Python实现OpenCV特征提取与图像检索demo

鹤之淇水 2019-04-22

“拍立淘”“一键识花”“街景匹配”……不知道大家在使用这些神奇的功能的时候,有没有好奇过它们背后的技术原理?其实这些技术都离不开最基本的图像检索技术。本篇文章我们就将对这一技术的原理进行介绍,并通过一个简单的Python脚本来实现一个最基本的图像检索demo。

图像特征

首先我们需要明白图像特征是什么以及它的使用方法。

图像特征是一种简单的图像模式,基于这种模式我们可以描述我们在图像上所看到的内容。 例如,在一张跟猫有关的图片中,猫咪的眼睛就可以作为这幅图像的特征。特征在(包括但不限于)计算机视觉中的主要作用是将视觉信息转换为向量空间表示。这种向量空间表示让我们可以利用数学运算对其进行处理,例如通过计算寻找相似向量(这可以用来寻找相似图像或图像中的相似目标)。

如何从图像中获取特征?

从图像中获取特征的方法有两种,第一种是通过提取图像描述符实现(白盒算法);第二种通过基于神经网络的方法实现(黑盒算法)。本文主要介绍第一种方法。

特征提取的算法有很多,最常用的有:SURF、ORB、SIFT、BRIEF等。这些算法大多是基于图像梯度的。为了简化安装需求,本教程使用的是KAZE描述符,因为其他描述符在python的基础OpenCV库中没有提供。

下面是特征提取器的实现代码:

import cv2
import numpy as np
import scipy
from scipy.misc import imread
import cPickle as pickle
import random
import os
import matplotlib.pyplot as plt
# Feature extractor 
# 特征提取器
def extract_features(image_path, vector_size=32):
 image = imread(image_path, mode="RGB")
 try:
 # Using KAZE, cause SIFT, ORB and other was moved to additional module
 # which is adding addtional pain during install
 #此处为了简化安装步骤,使用KAZE,因为SIFT/ORB以及其他特征算子需要安
#装额外的模块
 alg = cv2.KAZE_create()
 # Finding image keypoints
 #寻找图像关键点
 kps = alg.detect(image)
 # Getting first 32 of them. 
 #计算前32个
 # Number of keypoints is varies depend on image size and color pallet
 #关键点的数量取决于图像大小以及彩色调色板
 # Sorting them based on keypoint response value(bigger is better)
 #根据关键点的返回值进行排序(越大越好)
 kps = sorted(kps, key=lambda x: -x.response)[:vector_size]
 # computing descriptors vector
 #计算描述符向量
 kps, dsc = alg.compute(image, kps)
 # Flatten all of them in one big vector - our feature vector
 # 将其放在一个大的向量中,作为我们的特征向量
 dsc = dsc.flatten()
 # Making descriptor of same size
 # 使描述符的大小一致
 # Descriptor vector size is 64
 #描述符向量的大小为64
 needed_size = (vector_size * 64)
 if dsc.size < needed_size:
 # if we have less the 32 descriptors then just adding zeros 
 # at the end of our feature vector
#如果少于32个描述符,则在特征向量后面补零
 dsc = np.concatenate([dsc, np.zeros(needed_size - dsc.size)])
 except cv2.error as e:
 print 'Error: ', e
 return None
 return dsc
 
def batch_extractor(images_path, pickled_db_path="features.pck"):
 files = [os.path.join(images_path, p) for p in sorted(os.listdir(images_path))]
 result = {}
 for f in files:
 print 'Extracting features from image %s' % f
 name = f.split('/')[-1].lower()
 result[name] = extract_features(f)
 
# saving all our feature vectors in pickled file
# 将特征向量存于pickled 文件
 with open(pickled_db_path, 'w') as fp:
 pickle.dump(result, fp)

OpenCV中的大多数特征提取算法的python接口都相同,所以如果你想要使用SIFT特征,只需要用SIFT_create替换KAZE_create就行。

首先,程序会用extract_features检测图像上的关键点(局部模式的中心点)。 因为关键点数量随图像的不同有所不同,因此我们需要添加一些规则,以确保所得到的特征向量大小始终相同(这是因为在计算时,我们无法对维度不同的向量进行比较,所以必须保证相同的大小)。

然后是根据关键点构建向量描述符,每个描述符的大小为64,我们有32个这样的描述符,所以我们的特征向量是2048维。

batch_extractor是在所有的图像中批量运行特征提取器,并将特征向量保存在pickled文件中以供后续使用。

现在我们来建立类Matcher,它会将待搜索图像和数据库中的图像进行匹配。

class Matcher(object):
 def __init__(self, pickled_db_path="features.pck"):
 with open(pickled_db_path) as fp:
 self.data = pickle.load(fp)
 self.names = []
 self.matrix = []
 for k, v in self.data.iteritems():
 self.names.append(k)
 self.matrix.append(v)
 self.matrix = np.array(self.matrix)
 self.names = np.array(self.names)
 
 def cos_cdist(self, vector):
 # getting cosine distance between search image and images database 
 #计算待搜索图像与数据库图像的余弦距离
 v = vector.reshape(1, -1)
 return scipy.spatial.distance.cdist(self.matrix, v, 'cosine').reshape(-1)
 def match(self, image_path, topn=5):
 features = extract_features(image_path)
 img_distances = self.cos_cdist(features)
 # getting top 5 records
 # 获得前5个记录
 nearest_ids = np.argsort(img_distances)[:topn].tolist()
 
 nearest_img_paths = self.names[nearest_ids].tolist()
 return nearest_img_paths, img_distances[nearest_ids].tolist()

这里要加载前一步得到的特征向量,并从它们中创建一个大矩阵,然后计算待搜索图像的特征向量和特征向量数据库之间的余弦距离,然后输出最近的前N个结果。

当然,这仅仅是一个demo,在实际计算中,还可以用一些算法来快速计算数百万图像间的余弦距离。你可以使用简单且运行速度相当快的Annoy Index(在1M图像中搜索约需2ms)。

现在把它们放在一起运行一下:

def show_img(path):
 img = imread(path, mode="RGB")
 plt.imshow(img)
 plt.show()
 
 def run():
 images_path = 'resources/images/'
 files = [os.path.join(images_path, p) for p in sorted(os.listdir(images_path))]
 # getting 3 random images 
 # 随机获取3张图
 sample = random.sample(files, 3)
 
 batch_extractor(images_path)
 
 ma = Matcher('features.pck')
 
 for s in sample:
 print 'Query image =========================================='
 show_img(s)
 names, match = ma.match(s, topn=3)
 print 'Result images ========================================'
 for i in range(3):
 # we got cosine distance, less cosine distance between vectors
 # more they similar, thus we subtruct it from 1 to get match value
#我们得到了余弦距离,向量之间的余弦距离越小表示它们越相似,因此我们从1中减去它以得到匹配值
 print 'Match %s' % (1-match[i])
 show_img(os.path.join(images_path, names[i]))
 run()

大家可以在我的 github上下载源码,或者在Google Colab上运行(Google Colab是一种提供GPU在线计算的免费服务):

https://colab.research.google.com/drive/1BwdSConGugBlGzPLLkXHTz2ahkdzEhQ9

总结

在运行上述代码的过程中,你可能会发现搜索到的相似图像并不总能达到我们想象中的那种相似程度。这是因为我们所用的这种算法是上下文无关(context-unaware)的,所以该算法在寻找相同(即使是被修改过的)图像方面表现更好,而不是在相似图像方面。如果是要寻找上下文相关的相似图像,那就要使用卷积神经网络了,我的下一篇文章会对这方面的知识进行详细介绍。

用Python实现OpenCV特征提取与图像检索demo

相关推荐