目錄
一、背景介紹
1.1 什麼是特徵
在數據挖掘、計算機視覺等領域經常會提起“特徵”一詞,那麼究竟什麼纔是特徵?數據挖掘中我們可以稱被分析數據集的某一屬性(一行爲一個樣本,一列爲一個屬性)爲特徵,因爲它反映了該數據集的某一種特性,是該數據集的一種固有標識。在計算機視覺中,一張圖像的特徵其實也有類似的概念,這種特徵也能夠描述這張圖像的全部(或局部)內容或含義。圖像特徵可以是①有意義的圖像區域,該區域具有獨特的特徵和良好的識別性;也可以是②對圖像信息的一種數據抽取和表示。
針對①可以是圖像中的角點、邊緣、斑點、直線、曲線及高密度區域等。大多數特徵檢測算法都會涉及圖像的角點、邊和斑點的識別,也有一些涉及脊向的概念,可以認爲脊向是細長物體的對稱軸,例如識別圖像中的一條路。角點和邊都好理解,那什麼是斑點呢?斑點通常是指與周圍有着顏色和灰度差別的區域。在實際地圖中,往往存在着大量這樣的斑點,如一顆樹是一個斑點,一塊草地是一個斑點,一棟房子也可以是一個斑點。由於斑點代表的是一個區域,相比單純的角點,它的穩定性要好,抗噪聲能力要強,所以它在圖像配準上扮演了很重要的角色。
針對上述的②,在文章CV筆記7:計算機視覺的通俗理解 第二節中較通俗的解釋了對圖像特徵的理解。
1.2 爲什麼做特徵檢測
圖像的特徵應用很廣泛,我們通過圖像特徵能過夠將實現圖像的分類、目標檢測、圖像拼接等。而特徵就需要特徵檢測來提出。我們直接使用下面的例子進行說明。
這裏有兩張同一山峯不同部位的圖像,我們怎麼能夠將上面的兩張圖像拼接成一張圖像呢?對於我們人來說,將兩張圖像相同的位置重合摺疊在一起就可以了,就像下圖,但是對於計算機要怎麼實現?
對於計算機來說,並不能像人一樣直接對圖像進行拼接,而是要根據一些關鍵點對圖像進行處理。我們可以通過一些角點來讓計算機認識並拼接圖像,就像下面圖片中畫出的一樣,我們找到圖像中的一些關鍵點(角點),並讓兩張圖像中相同的角點處進行拼接。這樣計算機也能夠像人一樣實現圖像的拼接了。
上面的例子只是用到了角點特徵,其實其他的類似特徵,如邊緣、線等都能用在不同的場景中。所以,最終達到的目標是:通過特徵檢測我們實現的就是能夠讓計算機“讀懂”圖像,將人類看到的視覺信息轉化成計算機能夠識別和處理的定量形式,這也就是一種圖像特徵提取方式。
1.3 角點特徵
1.3.1 角點定義
我們在上面圖像拼接的說明案例中提到了角點特徵,本文也重點介紹角點特徵的提取過程。角點一般多爲圖像輪廓之間的交點,對於同一場景,即使視角發生變化,也通常具備穩定性質的特徵,在角點附近區域的像素點無論在梯度方向上還是其梯度幅值上有着較大變化。可以看出,在對圖像求導後,極值點處往往就是角點的位置。
再通過圖像理解,就是在一幅圖像中,如上圖,我們考慮圖像上的一個很小的窗口。在平坦的區域時,當小窗口進行任意方向的移動,窗口內的像素都沒有太大灰度變化;在邊緣區域時,當小窗口沿着邊緣方向移動,窗口內的像素沒有太大灰度變化,當沿着垂直邊緣方向移動,窗口內的像素會發生跳變;在角點區域時,當小窗口沿任意方向移動,窗口內的像素都有明顯的灰度變化。下圖展示了不同類型的角點。
1.3.2 角點優勢
角點相對於其他的圖像特徵具有以下的優勢:
- 點特徵屬於局部特徵,對遮擋有一定的魯棒性
- 通常圖像中可以檢測到成百上千的點特徵,以量取勝
- 點特徵辨識度好,不同物體上的點容易區分
- 點特徵提取速度很快
二、Harris角點檢測原理
2.1 算法思想
我們已經對角點有了初步的認識,下一步開始進行更深的數理推導,也就是Harris角點檢測原理。算法的核心是利用局部窗口在圖像上進行移動,判斷灰度是否發生較大的變化。如果窗口內的灰度值(在梯度圖上)都有較大的變化,那麼這個窗口所在區域就存在角點。
這樣就可以將 Harris 角點檢測算法分爲以下三步:
- 當窗口(局部區域)同時向 (水平)和 (垂直) 兩個方向移動時,計算窗口內部的像素值變化量 ;
- 對於每個窗口,都計算其對應的一個角點響應函數;
- 然後對該函數進行閾值處理,如果 ,表示該窗口對應一個角點特徵。
2.2 算法推導
我們仍然考慮圖像中的一個小窗口,如下圖:
2.2.1 灰度變化描述
當窗口發生移動時,那麼滑動前與滑動後對應的窗口中的像素點灰度變化描述如下:
參數解釋:
- 是窗口的偏移量;
- 是窗口所對應的像素座標位置,窗口有多大,就有多少個位置;
- 是像素座標位置的圖像灰度值;
- 是像素座標位置的圖像灰度值;
- 是窗口函數,最簡單情形就是窗口內的所有像素所對應的權重係數均爲1.但有時候,我們會將函數設置爲以窗口中心爲原點的二元正太分佈。如果窗口W中心點是角點時,移動前與移動後,該點在灰度變化貢獻最大;而離窗口中心(角點)較遠的點,這些點的灰度變化幾近平緩,這些點的權重係數,可以設定小值,以示該點對灰度變化貢獻較小,那麼我們自然而然想到使用二元高斯函數來表示窗口函數;
根據上述表達式,當窗口在平坦區域上移動,可以想象得到,灰度不會發生太大變化。;如果窗口處在紋理比較豐富的區域上滑動,那麼灰度變化會很大。算法最終思想就是計算灰度發生較大變化時所對應的位置,當然這個較大是指任意方向上的滑動,並非單指某個方向。
同時,我們應該注意:計算的是窗口向這一個方向移動所產生的灰度變化。
2.2.2 公式化簡
爲了提高計算效率,利用泰勒級數展開對上述公式進行簡化。
對於二維的泰勒展開式公式爲:
則對於有:
其中和是的微分(偏導),在圖像中就是求 和 方向的梯度:
那麼有:
從而可以得到:
其中:
最後矩陣形式表達是把實對稱矩陣對角化處理後的結果,可以把看成旋轉因子,其不影響兩個正交方向的變化分量。經對角化處理後,將兩個正交方向的變化分量提取出來,就是 和 (特徵值)。
2.2.3 矩陣的理解
雖然我們已經得到了,但我們並不直接使用它來進行判斷當前窗口是否含有角點。Harris角點檢測而是通過對窗口內的每個像素的方向上的梯度與方向上的梯度進行統計分析,並結合了矩陣的性質進行角點判定的。
我們以和爲座標軸,每個像素的梯度座標可以表示成,在此基礎上針對平坦區域,邊緣區域以及角點區域和斜邊緣區域四種情形進行分析:
針對上圖中四種窗口區域,統計對應像素的梯度分佈情況,得到下圖,其中爲橫軸爲縱軸:
我們從上面能夠觀察到這幾種區域的特點:
- 平坦區域上的每個像素點所對應的座標分佈在原點附近,這是因爲平坦區域像素梯度方向雖然各異,但是其幅值都不是很大,所以均聚集在原點附近
- 邊緣區域沿一個方向分佈較散,至於是哪一個方向不能一概而論,這要視邊緣在圖像上的具體位置而定,如果邊緣是水平或者垂直方向,那麼軸方向或者方向上的數據分佈就比較散
- 角點區域的在、方向上的梯度分佈都比較散
我們再回頭看,根據上面的推導,可以表示爲如下的形式:所以,我們可以將近似爲二項函數:
其中:
二次項函數本質上就是一個橢圓函數。橢圓的扁率和尺寸是由其特徵值λ1、λ2決定的,方向是由其特徵向量決定的。以下圖爲例,設橢圓方程爲:
可以看出的特徵根決定了橢圓的長短軸的長度,對應的特徵向量決定了橢圓的方向(因爲橢圓的兩個軸指向特徵向量的方向)。所以,我們知道了求得的是可以通過橢圓的形式來表達的,我們對之前的數據集用橢圓形式表示,繪製的圖像如下圖所示:
之所以能夠使用橢圓描述上面的數據集,是因爲矩陣本身的形式和協方差矩陣就有着千絲萬縷的聯繫。我們將看成兩個字段,假設窗口內有個像素點,也就是等價於有個樣本,我們先計算每個字段的均值:
我們仍然使用表示樣本去均值後的值,則由這個樣本組成的矩陣爲:
則對應的協方差矩陣爲:
我們可以看到,上式中的中,我們先進行了各維的零均值化,這樣各維所對應的隨機變量的均值爲0,協方差矩陣就大大簡化,簡化的最終結果就是矩陣(注意:在這裏爲了簡化運算,我們先假設了矩陣中的權重係數,並且忽略了最後除樣本數的操作)。到這裏是否已經明白了我們的目的:我們是來分析圖像導數數據的主要成分。
先前我們已經對進行了對角化操作:
是否也讓你想起了PCA中的操作?不明白可以複習一下PCA(注:協方差矩陣需要大家深刻理解一下)。所以再結合上面數據集分佈圖像,可以知道: - 如果兩個字段所對應的特徵值都比較大,說明像素點的梯度分佈比較散,梯度變化程度比較大,符合角點在窗口區域的特點;
- 如果是平坦區域,那麼像素點的梯度所構成的點集比較集中在原點附近,因爲窗口區域內的像素點的梯度幅值非常小,此時矩陣M的對角化的兩個特徵值比較小;
- 如果是邊緣區域,在計算像素點的方向上的梯度時,邊緣上的像素點的某個方向的梯度幅值變化比較明顯,另一個方向上的梯度幅值變化較弱,其餘部分的點都還是集中原點附近,這樣對角化後的兩個特徵值理論應該是一個比較大,一個比較小,當然對於邊緣,某些情況下致使計算出的特徵值並不是都特別的大,但仍跟含有角點的窗口的分佈情況具有不同。
所以我們可以直接根據兩個特徵值的大小對圖像點進行分類,如上圖所示: - 特徵值都比較大時,即窗口中含有角點;
- 特徵值一個較大,一個較小,窗口中含有邊緣;
- 特徵值都比較小,窗口處在平坦區域;
2.2.4 角點響應函數
我們已經知道了什麼樣的窗口含有角點又或者是邊緣等。在實際應用中,爲了更好的應用到編程中,我們有定義了角點響應函數,通過判定大小來判斷窗口是否有角點,即時則有角點,是我們自定義的一個閾值。
最簡單的一種是,角點應該滿足基本性質:窗口最小的特徵值儘量的大。此時定義。
還有比上式更有效的角點相應函數:
上面新給出了三種角點響應函數,我們以(1)進行講解:
這裏是矩陣的2個特徵值,是一個指定值,這是一個經驗參數,需要實驗確定它的合適大小,通常它的值在0.04和0.06之間,它的存在只是調節函數的形狀而已。注:一般的,增大k的值,降低角點檢測的靈敏度,減少被檢測角點的數量;減少k值,增加角點檢測的靈敏度,增加被檢測角點的數量。
爲什麼可以使用這個函數進行判定呢?我們將其圖像畫出進行理解,圖像如下,可以看出這個函數圖形正好滿足角點、邊緣和平坦區域的特徵。
之後,我們根據需求設定的閾值,就可以進行角點的判斷了。後續還可以增加很多處理,比如如果需要可以在3×3或者5×5的鄰域進行非最大值抑制,則局部最大值點即爲圖像中的角點。
2.3 Harris角點的算法流程及性質
算法流程:
- 將原圖像使用進行卷積,並計算圖像的梯度和;
- 計算每一個圖像像素點的自相關矩陣;
- 計算角點相應;
- 選擇大於某一閾值的點作爲角點;
- 根據需要在圖像區域內進行角點的非極大值抑制;
Harris角點缺點:
Harris角點檢測獲取的角點在圖像中分佈不均勻(對比度高的區域角點多)
Harris角點性質:
- 旋轉不變性
Harris角點檢測算子使用的是角點附近的區域灰度二階矩矩陣。而二階矩矩陣可以表示成一個橢圓,橢圓的長短軸正是二階矩矩陣特徵值平方根的倒數。當特徵橢圓轉動時,特徵值並不發生變化,所以判斷角點響應值R也不發生變化,由此說明Harris角點檢測算子具有旋轉不變性。 - 光照不變性、比度變化部分不變性
這是因爲在進行Harris角點檢測時,使用了微分算子對圖像進行微分運算,而微分運算對圖像密度的拉昇或收縮和對亮度的擡高或下降不敏感。換言之,對亮度和對比度的仿射變換並不改變Harris響應的極值點出現的位置,但是,由於閾值的選擇,可能會影響角點檢測的數量。
- 不具有尺度不變性
如下圖所示,當圖像被縮小時,在檢測窗口尺寸不變的前提下,在窗口內所包含圖像的內容是完全不同的。左側的圖像可能被檢測爲邊緣或曲線,而右側的圖像則可能被檢測爲一個角點。
三、基於python-opencv實現Harris角點檢測
3.1 opencv API實現
python-opencv提供了Harris角點檢測的函數:
cornerHarris(src, blockSize, ksize, k[, dst[, borderType]]) -> dst
.
The function runs the Harris corner detector on the image. Similarly to cornerMinEigenVal and ornerEigenValsAndVecs , for each pixel it calculates a gradient covariance matrix over a neighborhood. Then, it computes the following characteristic:
.
. Corners in the image can be found as the local maxima of this response map.
.
. @param src Input single-channel 8-bit or floating-point image. 輸入單通道8位或者浮點型圖像
. @param dst Image to store the Harris detector responses. It has the type CV_32FC1 and the same size as src .輸出爲角點響應圖
. @param blockSize Neighborhood size (see the details on #cornerEigenValsAndVecs ). 掃描時窗口大小
. @param ksize Aperture parameter for the Sobel operator. 使用Sobel算子,該參數定義了Sobel算子的中孔。簡單來說,該函數定義了角點檢測的敏感度,其值必須介於3~31之間的奇數。
. @param k Harris detector free parameter. See the formula below. 響應函數中的k值,一般取0.04~0.06
. @param borderType Pixel extrapolation method. See #BorderTypes. 像素插值方法
import numpy as np
import cv2
# original image
image = cv2.imread('./timg.jpg')
h, w, c = image.shape
print('image shape --> h:%d w:%d c:%d' % (h, w, c))
cv2.imshow('image', image)
cv2.waitKey(2000)
cv2.destroyAllWindows()
# harris dst
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# gray = np.float32(gray)
dst = cv2.cornerHarris(gray, blockSize=3, ksize=5, k=0.05)
image_dst = image[:, :, :]
image_dst[dst > 0.01 * dst.max()] = [0, 0, 255]
cv2.imwrite('./dst.jpg', image_dst)
cv2.imshow('dst', image_dst)
cv2.waitKey(0)
cv2.destroyAllWindows()
處理圖像對比:
3.2 python實現
def harris_det(img, block_size=3, ksize=3, k=0.04, threshold = 0.01, WITH_NMS = False):
'''
params:
img:單通道灰度圖片
block_size:權重滑動窗口
ksize:Sobel算子窗口大小
k:響應函數參數k
threshold:設定閾值
WITH_NMS:非極大值抑制
return:
corner:角點位置圖,與源圖像一樣大小,角點處像素值設置爲255
'''
h, w = img.shape[:2]
# 1.高斯權重
gray = cv2.GaussianBlur(img, ksize=(ksize, ksize), sigmaX=2)
# 2.計算梯度
grad = np.zeros((h,w,2),dtype=np.float32)
grad[:,:,0] = cv2.Sobel(gray,cv2.CV_16S,1,0,ksize=3)
grad[:,:,1] = cv2.Sobel(gray,cv2.CV_16S,0,1,ksize=3)
# 3.計算協方差矩陣
m = np.zeros((h,w,3),dtype=np.float32)
m[:,:,0] = grad[:,:,0]**2
m[:,:,1] = grad[:,:,1]**2
m[:,:,2] = grad[:,:,0]*grad[:,:,1]
m = [np.array([[m[i,j,0],m[i,j,2]],[m[i,j,2],m[i,j,1]]]) for i in range(h) for j in range(w)]
# 4.計算局部特徵結果矩陣M的特徵值和響應函數R(i,j)=det(M)-k(trace(M))^2 0.04<=k<=0.06
D,T = list(map(np.linalg.det,m)),list(map(np.trace,m))
R = np.array([d-k*t**2 for d,t in zip(D,T)])
# 5.將計算出響應函數的值R進行非極大值抑制,濾除一些不是角點的點,同時要滿足大於設定的閾值
#獲取最大的R值
R_max = np.max(R)
#print(R_max)
#print(np.min(R))
R = R.reshape(h,w)
corner = np.zeros_like(R,dtype=np.uint8)
for i in range(h):
for j in range(w):
if WITH_NMS:
#除了進行進行閾值檢測 還對3x3鄰域內非極大值進行抑制(導致角點很小,會看不清)
if R[i,j] > R_max*threshold and R[i,j] == np.max(R[max(0,i-1):min(i+2,h-1),max(0,j-1):min(j+2,w-1)]):
corner[i,j] = 255
else:
#只進行閾值檢測
if R[i,j] > R_max*threshold :
corner[i,j] = 255
return corner
if __name__ == "__main__":
image = cv2.imread('./timg.jpg')
height, width, channel = image.shape
print('image shape --> h:%d w:%d c:%d' % (height, width, channel))
cv2.imshow('mount', image)
cv2.waitKey(2000)
cv2.destroyAllWindows()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# gray = np.float32(gray)
dst = harris_detect(gray)
image_dst = image[:, :, :]
image_dst[dst > 0.01 * dst.max()] = [0, 0, 255]
cv2.imwrite('./dsti8_1.jpg', image_dst)
cv2.imshow('dst', dst)
cv2.waitKey(0)
cv2.destroyAllWindows()
測試結果: