OpenCV特徵提取與影像檢索實現(附程式碼)

AI科技大本營發表於2018-03-03



翻譯 | AI科技大本營(ID:rgznai100)

參與 | 張蔚敏

審校 | reason_W


“拍立淘”“一鍵識花”“街景匹配”……不知道大家在使用這些神奇的功能的時候,有沒有好奇過它們背後的技術原理?其實這些技術都離不開最基本的影像檢索技術。本篇文章我們就將對這一技術的原理進行介紹,並通過一個簡單的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)的,所以該演算法在尋找相同(即使是被修改過的)影像方面表現更好,而不是在相似影像方面。如果是要尋找上下文相關的相似影像,那就要使用卷積神經網路了,我的下一篇文章會對這方面的知識進行詳細介紹。


作者:Andrey Nikishaev

原文連結:https://towardsdatascience.com/feature-extraction-and-similar-image-search-with-opencv-for-newbies-3c59796bf774  





相關文章