OpenCV-Python (官方)中文教程(部分二)

[部分一]見:https://mp.csdn.net/postedit/103956799

第五章.特徵提取與

29.理解圖像特徵

大多數人都玩過拼圖遊戲。首先你們拿到一張圖片的一堆碎片,要做的就是把這些碎片以正確的方式排列起來從而重建這幅圖像。問題是怎樣做到呢?如果把做遊戲的原理寫成計算機程序,那計算機就也會玩拼圖遊戲了。如果計算機可以玩拼圖,我們就可以給計算機一大堆自然圖片, 然後就可以讓計算機把它拼成一張大圖了。如果計算機可以自動拼接自然圖片, 那我們是不是可以給計算機關於一個建築的的大量圖片,然後讓計算機給我們創建一個3D 的的模型呢?問題和聯想可以無邊無際。但是所有的這些問題都是建立在一個基礎問題之上的。這個問題就是:我們是如何玩拼圖的?我們是如何把一堆碎片拼在一 起的?我們有時如何把一個個自然場景拼接成一個單獨圖像的?答案就是:我們要尋找一些唯一的特徵,這些特徵要適於被跟蹤,容易被比較。如果我們要定義這樣一種特徵,雖然我們知道它是什麼但很難用語言來描述。如果讓你找出一個可以在不同圖片之間相互比較的好的特徵,你肯定能搞定。這就是爲什麼小孩子也會玩拼圖的原因。我們在一副圖像中搜索這樣的特徵,我們能找到它們,而且也能在其它圖像中找到這些特徵,然後再把它們拼接到一塊(在拼圖遊戲中,我們更注重的是圖片之間的連續性)。我們的這些能力都是天生的。

所以我們的一個問題現在擴展成了幾個,但是更加確切了。這些特徵是什麼呢?(我們的答案必須也能被計算機理解)。很難說人是怎樣找出這些特徵的。如果我們深入的觀察一些圖像並搜索不同的 pattern,我們會發現一 些有趣的事。以下圖爲例:

圖像很簡單。在圖像的上方給出了六個小圖。你要做的就是找到這些小圖 在原始圖像中的位置。你能找到多少正確結果呢?

A 和 B 是平面,而且它們的圖像中很多地方都存在。很難找到這些小圖的 準確位置。C 和 D 更簡單。它們是建築的邊緣。你可以找到它們的近似位置,但是準確位置還是很難找到。這是因爲:沿着邊緣,所有的地方都一樣。所以邊緣是比平面更好的特徵,但是還不夠好(在拼圖遊戲中要找連續的邊緣)。最後 E 和 F 是建築的一些角點。它們能很容易的被找到。因爲在角點的地方,無論你向哪個方向移動小圖,結果都會有很大的不同。所以可以把它們當 成一個好的特徵。爲了更好的理解這個概念我們舉個更簡單的例子。

如上圖所示,藍色框中的區域是一個平面很難被找到和跟蹤。無論你向哪個方向移動藍色框,長的都一樣。對於黑色框中的區域,它是一個邊緣。如果你沿垂直方向移動,它會改變。但是如果沿水平方向移動就不會改變。而紅色框中的角點,無論你向那個方向移動,得到的結果都不同,這說明它是唯一的。 所以,基本上來說角點是一個好的圖像特徵。(不僅僅是角點,有些情況斑點也 是好的圖像特徵)。

現在我們終於回答了前面的問題了,“這些特徵是什麼?”。但是下一個問題又來了。我們怎樣找到它們?或者說我們怎樣找到角點?我們也已經用一種直觀的方式做了回答,比如在圖像中找一些區域,無論你想那個方向移動這些區域變化都很大。在下一節中我們會用計算機語言來實現這個想法。所以找到圖像特徵的技術被稱爲特徵檢測

現在我們找到了圖像特徵(假設你已經搞定)。在找到這些之後,你應該在其它圖像中也找到同樣的特徵。我們應該怎麼做呢?我們選擇特徵周圍的一個區域,然後用我們自己的語言來描述它,比如“上邊是藍天,下邊是建築,在建築上有很多玻璃等”,你就可以在其它圖片中搜索相同的區域了。基本上看來, 你是在描述特徵。同樣,計算機也要對特徵周圍的區域進行描述,這樣它才能 在其它圖像中找到相同的特徵。我們把這種描述稱爲特徵描述。當你有了特徵 很它們的描述後,你就可以在所有的圖像中找這個相同的特徵了,找到之後你就可以做任何你想做的了。

本章我們就是要使用 OpenCV 中的各種算法來查找圖像的特徵,然後描 述它們,對它們進行匹配等。

30.Harris 角點檢測

在上一章中,我們看到角是圖像中各個方向上強度變化較大的區域。

Chris_Harris 和 Mike_Stephens 早在 1988 年的文章《A Combined Corner and Edge Detector》 中就已經提出了角點檢測的方法, 被稱爲 Harris 角點檢測。它把這個簡單的想法轉換成了數學形式。將窗口向各個方向移動(u,v)然後計算所有差異的總和。表達式如下:

窗口函數可以是正常的矩形窗口也可以是對每一個像素給予不同權重的高斯窗口。

角點檢測中要使 E (µ, ν) 的值最大。這就是說必須使方程右側的第二項的 取值最大。對上面的等式進行泰勒級數展開然後再通過幾步數學換算(可以參 考其它標準教材),我們得到下面的等式:

其中

這裏 Ix 和 Iy 是圖像在 x 和 y 方向的導數。(可以使用函數 cv2.Sobel()計算得到)。

在這之後,它們創建了一個分數,基本上是一個等式,它將決定一個窗口是否可以包含一個角:

其中

  • det (M ) = λ1λ2
  • trace (M ) = λ1 + λ2

• λ1 和 λ2 是矩陣 M 的特徵值

所以根據這些特徵中我們可以判斷一個區域是否是角點,邊界或者是平面。

• 當 λ1 和 λ2 都小時,|R| 也小,這個區域就是一個平坦區域。

• 當 λ1≫ λ2 或者 λ1≪ λ2,時 R 小於 0,這個區域是邊緣

• 當 λ1 和 λ2 都很大,並且 λ1~λ2 中的時,R 也很大,(λ1 和 λ2 中的最 小值都大於閾值)說明這個區域是角點。

可以用下圖來表示我們的結論:

所以 Harris 角點檢測的結果是一個由角點分數構成的灰度圖像。選取適 當的閾值對結果圖像進行二值化我們就檢測到了圖像中的角點。我們將用一個簡單的圖片來演示一下。

30.1 OpenCV 中的 Harris 角點檢測

OpenCV  中的函數cv2.cornerHarris() 可以用來進行角點檢測。參數如下:

img - 數據類型爲 float32 的輸入圖像。

•  blockSize  - 角點檢測中要考慮的領域大小。

ksize - Sobel 求導中使用的窗口大小

• k - Harris 角點檢測方程中的自由參數,取值參數爲 [0,04,0.06].

例子如下:

import cv2
import numpy as np

filename = 'chessboard.jpg' 
img = cv2.imread(filename)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) 
gray = np.float32(gray)
# 輸入圖像必須是  float32,最後一個參數在  0.04 到  0.05 之間
dst = cv2.cornerHarris(gray,2,3,0.04)

#result is dilated for marking the corners, not important
dst = cv2.dilate(dst,None)

# Threshold for an optimal value, it may vary depending on the image.
img[dst>0.01*dst.max()]=[0,0,255]

cv2.imshow('dst',img)
if cv2.waitKey(0) & 0xff == 27: 
    cv2.destroyAllWindows()

結果如下:

30.2 亞像素級精確度的角點

有時我們需要最大精度的角點檢測。OpenCV 爲我們提供了函數 cv2.cornerSubPix(), 它可以提供亞像素級別的角點檢測。下面是一個例子。首先我們要找到 Harris 角點,然後將角點的重心傳給這個函數進行修正。Harris 角點用紅色像素標 出,綠色像素是修正後的像素。在使用這個函數是我們要定義一個迭代停止條 件。當迭代次數達到或者精度條件滿足後迭代就會停止。我們同樣需要定義進 行角點搜索的鄰域大小。

import cv2
import numpy as np

filename = 'chessboard2.jpg' 
img = cv2.imread(filename)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
# 查找Harris 角點
gray = np.float32(gray)
dst = cv2.cornerHarris(gray,2,3,0.04) 
dst = cv2.dilate(dst,None)
ret, dst = cv2.threshold(dst,0.01*dst.max(),255,0) 
dst = np.uint8(dst)
# 查找重心
#connectedComponentsWithStats(InputArray image, OutputArray labels, OutputArray stats,OutputArray centroids, int connectivity=8, int ltype=CV_32S)
ret, labels, stats, centroids = cv2.connectedComponentsWithStats(dst)
# 定義停止和細化角點的標準
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 0.001)
#Python: cv2.cornerSubPix(image, corners, winSize, zeroZone, criteria)
#zeroZone – 搜索區域中間的死區大小的一半,在下面的公式中求和沒有完成。它有時用來避免自相關矩陣可能的奇異性。(-1,-1)的值表示沒有這樣的大小.
# 返回值由角點座標組成的一個數組(而非圖像)
corners = cv2.cornerSubPix(gray,np.float32(centroids),(5,5),(-1,-1),criteria)
# Now draw them
res = np.hstack((centroids,corners))
#np.int0 可以用來省略小數點後面的數字(非四捨五入)。 res = np.int0(res) img[res[:,1],res[:,0]]=[0,0,255]
img[res[:,3],res[:,2]] = [0,255,0]

cv2.imwrite('subpixel5.png',img)

結果如下,爲了方便查看我們對角點的部分進行了放大:

 

31.Shi-Tomasi角點檢測 & 適合於跟蹤的圖像特徵

上一節我們學習了 Harris 角點檢測,後來 1994 年,J.Shi 和 C.Tomasi 在它們的文章《Good_Features_to_Track》中對這個算法做了一個小小的修改,並得到了更好的結果。我們知道  Harris  角點檢測的打分公式爲:

但 Shi-Tomasi 使用的打分函數爲:

    R = min (λ1, λ2)

如果打分超過閾值,我們就認爲它是一個角點。我們可以把它繪製到 λ1 ~λ2  空間中,就會得到下圖:

從這幅圖中,我們可以看出來只有當 λ1 和 λ2 都大於最小值時,才被認爲是角點(綠色區域)。

OpenCV 提供了函數:cv2.goodFeaturesToTrack()。這個函數可以幫我們使用 Shi-Tomasi 方法獲取圖像中 N 個最好的角點(也可以通過改變參數來使用 Harris 角點檢測算法)。通常情況下,輸入的應該 是灰度圖像。然後確定你想要檢測到的角點數目。再設置角點的質量水平在0 到 1 之間。它代表了角點的最低質量,低於這個數的所有角點都會被忽略。最後在設置兩個角點之間的最短歐式距離。

根據這些信息,函數就能在圖像上找到角點。所有低於質量水平的角點都會被忽略。然後再把合格角點按角點質量進行降序排列。函數會採用角點質量 最高的那個角點(排序後的第一個),然後將它附近(最小距離之內)的角點都 刪掉。按着這樣的方式最後返回 N  個最佳角點。在下面的例子中,我們試着找出 25個最佳角點:

import numpy as np 
import cv2
from matplotlib import pyplot as plt

img = cv2.imread('simple.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
corners = cv2.goodFeaturesToTrack(gray,25,0.01,10)
# 返回的結果是  [[ 311.,  250.]] 兩層括號的數組。
corners = np.int0(corners)
for i in corners: x,y = i.ravel()
cv2.circle(img,(x,y),3,255,-1) 
plt.imshow(img),plt.show()

結果如下:

我們以後會發現這個函數很適合在目標跟蹤中使用。

32.SIFT(尺度不變特徵變換)介紹

32.1原理

在前面兩節我們學習了一些角點檢測技術,比如 Harris 等。它們具有旋 轉不變特性,即使圖片發生了旋轉,我們也能找到同樣的角點。很明顯即使圖 像發生旋轉之後角點還是角點。那如果我們對圖像進行縮放呢?角點可能就不 再是角點了。以下圖爲例,在一副小圖中使用一個小的窗口可以檢測到一個角 點,但是如果圖像被放大,再使用同樣的窗口就檢測不到角點了。

所以在 2004 年,D.Lowe 提出了一個新的算法: 尺度不變特徵變換(SIFT,Scale-Invariant Feature Transform,這個算法可以幫助我們提取圖像中的關鍵點並計算它們的描述符。SIFT  算法主要由四步構成。我們來逐步進行學習。

(1).尺度空間極值檢測

從上圖我們可以很明顯的看出來在不同的尺度空間不能使用相同的窗口檢測極值點。對小的角點要用小的窗口,對大的角點只能使用大的窗口。爲了達到這個目的,我們要使用尺度空間濾波器(尺度空間濾波器可以使用一些列具有不同方差 σ  的高斯卷積核構成)。使用具有不同方差值  σ 的高斯拉普拉斯算子(LoG)對圖像進行卷積,LoG 由於具有不同的方差值 σ 所以可以用來檢測不同大小的斑點(當 LoG 的方差 σ 與斑點直徑相等時能夠使斑點完全平滑)。簡單來說方差 σ 就是一個尺度變換因子。例如,上圖中使用一個小方差 σ 的高斯卷積核是可以很好的檢測出小的角點,而使用大方差 σ 的高斯卷積核時可以很 好的檢測除大的角點。所以我們可以在尺度空間和二維平面中檢測到局部最大值,如(x,y,σ), 這表示在 σ 尺度中(x,y)點可能是一個關鍵點。(高斯方差的大小與窗口的大小存在一個倍數關係:窗口大小等於 6 倍方差加 1,所以方差的大小也決定了窗口大小)

但是這個 LoG 的計算量非常大, 所以 SIFT 算法使用高斯差分算子(DoG)來對  LoG   做近似。這裏需要再解釋一下圖像金字塔,我們可以通過減少採樣(如只取奇數行或奇數列)來構成一組圖像尺寸(1,0.5,0.25 等)不同的金字塔,然後對這一組圖像中的每一張圖像使用具有不同方差 σ 的高斯卷 積核構建出具有不同分辨率的圖像金字塔(不同的尺度空間)。DoG 就是這組具有不同分辨率的圖像金字塔中相鄰的兩層之間的差值。如下圖所示:

在 DoG 搞定之後,就可以在不同的尺度空間和 2D 平面中搜索局部最大 值了。對於圖像中的一個像素點而言,它需要與自己周圍的 8 鄰域,以及尺度 空間中上下兩層中的相鄰的 18(2x9)個點相比。如果是局部最大值,它就可 能是一個關鍵點。基本上來說關鍵點是圖像在相應尺度空間中的最好代表。如 下圖所示:

該算法的作者在文章中給出了 SIFT 參數的經驗值:octaves=4(通過 降低採樣從而減小圖像尺寸,構成尺寸減小的圖像金字塔(4 層)?),尺度空間 爲 5,也就是每個尺寸使用 5 個不同方差的高斯核進行卷積,初始方差是 1.6, k  等於  √2 等。

(2).關鍵點(極值點)定位

一旦找到關鍵點, 我們就要對它們進行修正從而得到更準確的結果。 作者使用尺度空間的泰勒級數展開來獲得極值的準確位置,如果極值點的 灰度值小於閾(0.03) 就會被忽略掉。 在 OpenCV 中這種閾值被稱爲 contrastThreshold

DoG   算法對邊界非常敏感,所以我們必須要把邊界去除。前面我們講的Harris 算法除了可以用於角點檢測之外還可以用於檢測邊界。作者就是使用了同樣的思路。作者使用 2x2 的 Hessian 矩陣計算主曲率。從 Harris 角點檢測的算法中,我們知道當一個特徵值遠遠大於另外一個特徵值時檢測到的是邊界。所以它們使用了一個簡單的函數,如果比例高於閾值(OpenCV 中稱爲邊界閾值),這個關鍵點就會被忽略。文章中給出的邊界閾值爲 10。

所以低對比度的關鍵點和邊界關鍵點都會被去除掉,剩下的就是關鍵點了。

(3).爲關鍵點(極值點)指定方向參數

現在我們要爲每個關鍵點賦予一個反向參數,這樣它纔會具有旋轉不變性。獲取關鍵點(所在尺度空間)的鄰域,然後計算這個區域的梯度級和方向。 根據計算得到的結果創建一個含有 36 個 bins(每 10 度一個 bin)的方向直方圖。(使用當前尺度空間 σ 值的 1.5 倍爲方差的圓形高斯窗口和梯度級做權重)。直方圖中的峯值爲主方向參數,如果其它的任何柱子的高度高於峯值的 80% 被認爲是輔方向。這就會在相同尺度空間的相同位置構建除具有不同方 向的關鍵點。這對於匹配的穩定性會有所幫助。

(4).關鍵點描述符

新的關鍵點描述符被創建了。選取與關鍵點周圍一個 16x16 的鄰域,把 它分成 16 個 4x4 的小方塊,爲每個小方塊創建一個具有 8 個 bin 的方向直 方圖。總共加起來有 128 個 bin。由此組成長爲 128 的向量就構成了關鍵點 描述符。除此之外還要進行幾個測量以達到對光照變化,旋轉等的穩定性。

(5).關鍵點匹配

下一步就可以採用關鍵點特徵向量的歐式距離來作爲兩幅圖像中關鍵點的 相似性判定度量。取第一個圖的某個關鍵點,通過遍歷找到第二幅圖像中的距 離最近的那個關鍵點。但有些情況下,第二個距離最近的關鍵點與第一個距離 最近的關鍵點靠的太近。這可能是由於噪聲等引起的。此時要計算最近距離與 第二近距離的比值。如果比值大於 0.8,就忽略掉。這會去除 90% 的錯誤匹 配,同時只去除 5% 的正確匹配。如文章所說。

這就是 SIFT 算法的摘要。非常推薦你閱讀原始文獻,這會加深你對算法 的理解。請記住這個算法是受專利保護的。所以這個算法包含在 OpenCV 中的收費模塊中。

32.2 OpenCV 中的 SIFT

從關鍵點檢 測和繪製開始。首先我們要創建對象。我們可以使用不同的參數,這並不是必須的,關於參數的解釋可以查看文檔。

import cv2
import numpy as np

img = cv2.imread('home.jpg')
gray= cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

sift = cv2.SIFT()
kp = sift.detect(gray,None)
img=cv2.drawKeypoints(gray,kp) 
cv2.imwrite('sift_keypoints.jpg',img)

函數 sift.detect() 可以在圖像中找到關鍵點。如果你只想在圖像中的一個 區域搜索的話,也可以創建一個掩模圖像作爲參數使用。返回的關鍵點是一個 帶有很多不同屬性的特殊結構體,這些屬性中包含它的座標(x,y),有意義的 鄰域大小,確定其方向的角度等。

OpenCV 也提供了繪製關鍵點的函數:cv2.drawKeyPoints(),它可以 在關鍵點的部位繪製一個小圓圈。如果你設置參數爲 cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_ 就會繪製代表關鍵點大小的圓圈甚至可以繪製除關鍵點的方向。

img=cv2.drawKeypoints(gray,kp,flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS) 
cv2.imwrite('sift_keypoints.jpg',img)

結果如下:

現在來計算關鍵點描述符,OpenCV 提供了兩種方法:

(1).既然已經找到了關鍵字,那麼可以調用sift.compute(),它根據找到的關鍵字計算描述符。例如:kp,des = sift.compute(gray,kp)

(2).如果沒有找到關鍵字,可以使用函數sift.detectAndCompute()在單個步驟中直接找到關鍵字和描述符。

這裏我們來看看第二個方法:

sift = cv.xfeatures2d.SIFT_create()
kp, des = sift.detectAndCompute(gray,None)

這裏 kp 是一個關鍵點列表。des 是一個 Numpy 數組,其大小是關鍵點數目乘以 128。

所以我們得到了關鍵點和描述符等。現在我們想看看如何在不同圖像之間 進行關鍵點匹配,這就是我們在接下來的章節將要學習的內容。

33.SURF(Speeded-Up Robust Features)介紹

在上一節中我們學習了使用SIFT算法進行關鍵點檢測和描述。但是這種算法的執行速度比較慢,人們需要速度更快的算法。在2006年Bay,H.,Tuytelaars,T.和VanGool,L共同提出了SURF(加速穩健特徵)算法。跟它的名字一樣,這個算法是加速版的SIFT。

在SIFT中,Lowe在構建尺度空間時使用DoG對LoG進行近似。SURF使用盒子濾波器(box_filter)對LoG進行近似。下圖顯示了這種近似。在進行卷積計算時可以利用積分圖像(積分圖像的一大特點是:計算圖像中某個窗口內所有像素和時,計算量的大小與窗口大小無關),是盒子濾波器的一大優點。而且這種計算可以在不同尺度空間同時進行。同樣SURF算法計算關鍵點的尺度和位置是也是依賴與Hessian矩陣行列式的。

爲了保證特徵矢量具有選裝不變形,需要對於每一個特徵點分配一個主要方向。需要以特徵點爲中心,以6s(s爲特徵點的尺度)爲半徑的圓形區域內,對圖像進行Harr小波相應運算。這樣做實際就是對圖像進行梯度運算,但是利用積分圖像,可以提高計算圖像梯度的效率,爲了求取主方向值,需喲啊設計一個以方向爲中心,張角爲60度的扇形滑動窗口,以步長爲0.2弧度左右旋轉這個滑動窗口,並對窗口內的圖像Haar小波的響應值進行累加。主方向爲最大的Haar響應累加值對應的方向。在很多應用中根本就不需要旋轉不變性,所以沒有必要確定它們的方向,如果不計算方向的話,又可以使算法提速。

SURF提供了成爲U-SURF的功能,它具有更快的速度,同時保持了對+/-15度旋轉的穩定性。OpenCV對這兩種模式同樣支持,只需要對參數upright進行設置,當upright爲0時計算方向,爲1時不計算方向,同時速度更快。

生成特徵點的特徵矢量需要計算圖像的Haar小波響應。在一個矩形的區域內,以特徵點爲中心,沿主方向將20s*20s的圖像劃分成4*4個子塊,每個子塊利用尺寸2s的Haar小波模版進行響應計算,然後對響應值進行統計,組成向量v=(∑dx,∑dy,∑|dx|,∑|dy|)。這個描述符的長度爲64。降低的維度可以加速計算和匹配,但又能提供更容易區分的特徵。爲了增加特徵點的獨特性,SURF還提供了一個加強版128維的特徵描述符。當dy大於0和小於0時分別對dx和|dx|的和進行計算,計算dy和|dy|時也進行區分,這樣獲得特徵就會加倍,但又不會增加計算的複雜度。OpenCV同樣提供了這種功能,當參數extended設置爲1時爲128維,當參數爲0時爲64維,默認情況爲128維。

在檢測特徵點的過程中計算了Hessian矩陣的行列式,與此同時,計算得到了Hessian矩陣的跡,矩陣的跡爲對角元素之和。

按照亮度的不同,可以將特徵點分爲兩種,第一種爲特徵點跡其周圍小鄰域的亮度比背景區域要亮,Hessian矩陣的跡爲正;另外一種爲特徵點跡其周圍小鄰域的亮度比背景區域要暗,Hessian矩陣爲負值。根據這個特性,首先對兩個特徵點的Hessian的跡進行比較。如果同號,說明兩個特徵點具有相同的對比度;如果異號的話,說明兩個特徵點的對比度不同,放棄特徵點之間的後續的相似性度量。

對於兩個特徵點描述子的相似性度量,我們採用歐式距離進行計算。簡單來說SURF算法採用了很多方法來對每一步進行優化從而提高速度。

分析顯示在結果效果相當的情況下SURF的速度是SIFT的3倍。SURF善於處理具有模糊和旋轉的圖像,但是不善於處理視角變化和關照變化。

OpenCV中的SURF:

與SIFT相同OpenCV也提供了SURF的相關函數。首先我們要初始化一個SURF對象,同時設置好可選參數:64/128維描述符,Upright/Normal模式等。所有的細節都已經在文檔中解釋的很明白了。就像我們在SIFT中一樣,我們可以使用函數SURF.detect(),SURF.compute()等來進行關鍵點攙着和描述。

首先從查找描述繪製關鍵點開始。由於和SIFT一樣所以我們的示例都在Python終端中演示。

img = cv.imread('fly.png',0)
# 創建SURF對象。您可以在這裏或稍後指定參數。這裏將Hessian閾值設置爲400
surf = cv.xfeatures2d.SURF_create(400)
# 直接查找關鍵字和描述符
kp, des = surf.detectAndCompute(img,None)
print(len(kp))  # 699

在一幅圖像中顯示699個關鍵點太多了。我們把它縮減到50個再繪製到圖片上。在匹配時,我們可能需要所有的這些特徵,不過現在還不需要。所以我們現在提高Hessian的閾值。

# 檢查當前Hessian閥值
print( surf.getHessianThreshold() )    # 400.0

# 我們把它定在50000左右。記住,它只是用來表示圖像的。在實際情況中,值最好是300-500
surf.setHessianThreshold(50000)
# 再次計算關鍵點並檢查它的編號.
kp, des = surf.detectAndCompute(img,None)
print( len(kp) )    # 47

現在低於50了,把它們繪製到圖像中吧。

img2 = cv.drawKeypoints(img,kp,None,(255,0,0),4)
plt.imshow(img2),plt.show()

結果如下。你會發現SURF很像一個斑點檢測器。它可以檢測到蝴蝶翅膀上的白班。你可以在其它圖片中測試一下。

現在我們試一下U-SURF,它不會檢測關鍵點的方向。

# 檢查upright標誌,如果它是False,設置爲True
print( surf.getUpright() )    #False

surf.setUpright(True)
# 重新計算特徵點並繪製它
kp = surf.detect(img,None)
img2 = cv.drawKeypoints(img,kp,None,(255,0,0),4)
plt.imshow(img2),plt.show()

結果如下。所有的關鍵點的朝向都是一致的。它比前面的快很多。如果你的工作對關鍵點的朝向沒有特別的要求(如全景圖拼接)等,這種方法會更快。

最後我們再看看關鍵點描述符的大小,如果是64維的就改成128維。

# 查找描述符的大小
print( surf.descriptorSize() )    # 64
# 這意味着"extended"爲False.
surf.getExtended()    # False
# 所以我們將其設置成True得到128個模糊描述符.
surf.setExtended(True)
kp, des = surf.detectAndCompute(img,None)
print( surf.descriptorSize() )    # 128
print( des.shape )    # (47, 128)

接下來要做的就是匹配了,我們會在後面討論。

34.角點檢測的FAST算法

我們前面學習了幾個特徵檢測器,它們大多數效果都很好。但是從實時處理的角度來看,這些算法都不夠快。一個最好例子就是SLAM(同步定位與地圖構建),移動機器人,它們的計算資源非常有限。爲了解決這個問題,Edward_Rosten和Tom_Drummond在2006年提出裏FAST算法。我們下面將會對此算法進行一個簡單的介紹。你可以參考原始文獻獲得更多細節。

34.1使用FAST算法進行特徵提取

1.在圖像中選取一個像素點p,來判斷它是不是關鍵點。Ip等於像素點p的灰度值。

2.選擇適當的閾值t。

3.如下圖所示在像素點p的周圍選擇16個像素點進行測試。

4.如果在這16個像素點中存在n個連續像素點的灰度值都高於Ip+t,或者低於Ip−t,那麼像素點p就被認爲是一個角點。如上圖中的虛線所示,n選取的值爲12。

5.爲了獲得更快的效果,還採用了而外的加速辦法。首先對候選點的周圍每個90度的點:1,9,5,13進行測試(先測試1和19,如果它們符合閾值要求再測試5和13)。如果p是角點,那麼這四個點中至少有3個要符合閾值要求。如果不是的話肯定不是角點,就放棄。對通過這步測試的點再繼續進行測試(是否有12的點符合閾值要求)。這個檢測器的效率很高,但是它有如下幾條缺點:

•當n<12時它不會丟棄很多候選點(獲得的候選點比較多)。

•像素的選取不是最優的,因爲它的效果取決與要解決的問題和角點的分佈情況。

•高速測試的結果被拋棄

•檢測到的很多特徵點都是連在一起的。

前3個問題可以通過機器學習的方法解決,最後一個問題可以使用非最大值抑制的方法解決。

34.2機器學習的角點檢測器

1.選擇一組訓練圖片(最好是跟最後應用相關的圖片)

2.使用FAST算法找出每幅圖像的特徵點

3.對每一個特徵點,將其周圍的16個像素存儲構成一個向量。對所有圖像都這樣做構建一個特徵向量P

4.每一個特徵點的16像素點都屬於下列三類中的一種。

5.根據這些像素點的分類,特徵向量P也被分爲3個子集:Pd,Ps,Pb

6.定義一個新的布爾變量Kp,如果p是角點就設置爲Ture,如果不是就設置爲False.

7.使用ID3算法(決策樹分類器)查詢每個子集,使用變量Kp查詢關於真類的知識。它選擇產生關於候選像素是否是角的最多信息的x,通過Kp的熵來測量。

8.這是遞歸地應用於所有的子集,直到它的熵爲零。

9.將構建好的決策樹運用於其它圖像的快速的檢測。

34.3 非極大值抑制

使用極大值抑制的方法可以解決檢測到的特徵點相連的問題

1.對所有檢測到到特徵點構建一個打分函數V。V就是像素點p與周圍16個像素點差值的絕對值之和。

2.計算臨近兩個特徵點的打分函數V。

3.忽略V值最低的特徵點.

34.4 OpenCV中FAST特徵檢測器

和其它特徵點檢測一樣,我們可以在OpenCV中直接使用FAST特徵檢測器。如果你願意的話,你還可以設置閾值,是否進行非最大值抑制,要使用的鄰域大小等。

鄰域設置爲下列3種之一:

cv2.FAST_FEATURE_DETECTOR_TYPE_5_8;
cv2.FAST_FEATURE_DETECTOR_TYPE_7_12 ;
cv2.FAST_FEATURE_DETECTOR_TYPE_9_16。

下面是使用 FAST 算 法進行特徵點檢測的簡單代碼。

import numpy as np
import cv2
from matplotlib import pyplot as plt

img = cv2.imread('simple.jpg', 0)
# Initiate FAST object with default values
fast = cv2.FastFeatureDetector()
# find and draw the keypoints
kp = fast.detect(img, None)
img2 = cv2.drawKeypoints(img, kp, color=(255, 0, 0))
# Print all default params
print("Threshold: ", fast.getInt('threshold'))
print("nonmaxSuppression: ", fast.getBool('nonmaxSuppression'))
print("neighborhood: ", fast.getInt('type'))
print("Total Keypoints with nonmaxSuppression: ", len(kp))
cv2.imwrite('fast_true.png', img2)
# Disable nonmaxSuppression fast.setBool('nonmaxSuppression',0) 
kp = fast.detect(img,None)
print("Total Keypoints without nonmaxSuppression: ", len(kp))
img3 = cv2.drawKeypoints(img, kp, color=(255, 0, 0))
cv2.imwrite('fast_false.png', img3)

結果如下。第一幅圖是使用了非最大值抑制的結果,第二幅沒有使用非最大值抑制。

FAST 算法比其它角點檢測算法都快。 但是在噪聲很高時不夠穩定,這是由閾值決定。

35.BRIEF(二值穩健的獨立補充特徵)

我們知道 SIFT 算法使用的是 128 維的描述符。由於它是使用的浮點數, 所以要使用 512 個字節。同樣 SURF 算法最少使用 256 個字節(64 爲維描 述符)。創建一個包含上千個特徵的向量需要消耗大量的內存,在嵌入式等資源 有限的設備上這樣是合適的。匹配時還會消耗更多的內存和時間。

但是在實際的匹配過程中如此多的維度是沒有必要的。我們可以使用 PCA, LDA 等方法來進行降維。甚至可以使用 LSH(局部敏感哈希)將 SIFT 浮點 數的描述符轉換成二進制字符串。對這些字符串再使用漢明距離進行匹配。漢明距離的計算只需要進行 XOR 位運算以及位計數,這種計算很適合在現代的 CPU 上進行。但我們還是要先找到描述符才能使用哈希,這不能解決最初的內存消耗問題。

BRIEF (Binary Robust Independent El-ementary Features)應運而生。它不去計算描述符而是直接找到一個二進制字符串。這 種算法使用的是已經平滑後的圖像,它會按照一種特定的方式選取一組像素點 對 nd (x,y),然後在這些像素點對之間進行灰度值對比。例如,第一個點對的 灰度值分別爲 p 和 q。如果 p 小於 q,結果就是 1,否則就是 0。就這樣對 nd 個點對進行對比得到一個 nd  維的二進制字符串。

nd 可以是 128,256,512。OpenCV 對這些都提供了支持,但在默認 情況下是 256(OpenC 是使用字節表示它們的,所以這些值分別對應與 16, 32,64)。當我們獲得這些二進制字符串之後就可以使用漢明距離對它們進行 匹配了。

非常重要的一點是:BRIEF 是一種特徵描述符,它不提供查找特徵的方法。 所以我們不得不使用其它特徵檢測器,比如 SIFT 和 SURF 等。原始文獻推薦 使用 CenSurE 特徵檢測器,這種算法很快。而且 BRIEF 算法對 CenSurE 關鍵點的描述效果要比SURF關鍵點的描述更好。

簡單來說 BRIEF 是一種對特徵點描述符計算和匹配的快速方法。這種算 法可以實現很高的識別率,除非出現平面內的大旋轉。

OpenCV 中的 BRIEF:

下面的代碼使用了 CenSurE  特徵檢測器和 BRIEF  描述符。(在 OpenCV中 CenSurE 檢測器被叫做 STAR 檢測器)。

import numpy as np
import cv2
from matplotlib import pyplot as plt

img = cv2.imread('simple.jpg', 0)
# Initiate STAR detector
star = cv2.FeatureDetector_create("STAR")
# Initiate BRIEF extractor
brief = cv2.DescriptorExtractor_create("BRIEF")
# find the keypoints with STAR
kp = star.detect(img, None)
# compute the descriptors with BRIEF
kp, des = brief.compute(img, kp)
print(brief.getInt('bytes'))
print(des.shape)

函數 brief.getInt(′bytes′) 會以字節格式給出 nd 的大小,默認值爲 32。下面就是匹配了,我們會在其它章節中介紹。

 

36.ORB(定向快旋轉短BRIEF)

對於一個 OpenCV  的狂熱愛好者來說 ORB(Oriented FAST and Rotated BRIEF)  最重要的一點就是:它來自 “OpenCV_Labs''。這個算法是在 2011 年提出的。在計算開支,匹配效率以 及更主要的是專利問題方面 ORB 算法是 SIFT 和 SURF 算法的一個很好的 替代品。SIFT 和 SURF 算法是有專利保護的,如果你要使用它們,就可能要 花錢。但是 ORB 不需要!!

ORB 基本是 FAST 關鍵點檢測和 BRIEF 關鍵點描述器的結合體,並通 過很多修改增強了性能。首先它使用 FAST 找到關鍵點,然後再使用 Harris 角點檢測對這些關鍵點進行排序找到其中的前 N 個點。它也使用金字塔從而產 生尺度不變性特徵。但是有一個問題,FAST 算法步計算方向。那旋轉不變性 怎樣解決呢?作者進行了如下修改。

它使用灰度矩的算法計算出角點的方向。以角點到角點所在(小塊)區域 質心的方向爲向量的方向。爲了進一步提高旋轉不變性,要計算以角點爲中心 半徑爲 r 的圓形區域的矩,再根據矩計算除方向。

對於描述符,ORB 使用的是 BRIEF 描述符。但是我們已經知道 BRIEF 對與旋轉是不穩定的。所以我們在生成特徵前,要把關鍵點領域的這個 patch 的座標軸旋轉到關鍵點的方向。對於任意位置(xi, yi)的n個二進制測試的特徵集,定義一個2×n的矩陣,S包含這些像素的座標。然後使用補丁的取向,θ,發現其旋轉矩陣和旋轉S Sθ帶領(旋轉)版本。ORB離散化的角增量of2π/ 30(12度),並構造一個查找表的預先計算的簡單模式。只要關鍵點定位\θ是一致的觀點,正確的點集Sθ將用於計算它的描述符。

BRIEF 有一個重要的性質,那就是每個位元特徵都有一個大的變異數和一個接近0.5的均值。但是一旦它沿着關鍵點方向定向,它就會失去這種特性,變得更加分散。由於對輸入的響應不同,高方差使特徵更具有鑑別性。另一個可取的特性是使測試不相關,因爲這樣每個測試都有助於結果。爲了解決所有這些問題,ORB在所有可能的二進制測試中進行貪婪搜索,以找到方差和平均值都接近0.5且不相關的測試。其結果稱爲BRIEF。

在描述符匹配中,使用了改進傳統LSH的多探針LSH。文章說ORB比SURF和SIFT快得多,而ORB描述符比SURF工作得更好。在低功耗全景拼接等設備中,ORB是一個很好的選擇。

實驗證明,BRIEF  算法的每一位的均值接近 0.5,並且方差很大。steered_BRIEF

算法的每一位均值比較分散(均值爲 0.5,0.45,0.35... 等值的關鍵點數相當),這導致方差減小。數據的方差大的一個好處是:使得特徵更容易分辨。爲了對steered_BRIEF 算法使得特徵的方差減小的彌補和減小數據間的相關性, 用一個學習算法(learning      method)選擇二進制測試的一個子集。

在描述符匹配中使用了對傳統 LSH 改善後的多探針 LSH。文章中說 ORB 算法比 SURF 和 SIFT 算法快的多,ORB 描述符也比 SURF 好很多。ORB 是低功耗設備的最佳選擇。

OpenCV中的ORB算法:

和前面一樣我們首先要使用函數 cv3.ORB() 或者 feature2d 通用接口 創建一個 ORB 對象。它有幾個可選參數。最有用的應該是 nfeature,默認 值爲 500,它表示了要保留特徵的最大數目。scoreType 設置使用 Harris 打分還是使用 FAST 打分對特徵進行排序(默認是使用 Harris 打分)等。參 數 WTA_K 決定了產生每個oriented_BRIEF 描述符要使用的像素點的數目。默認值是 2,也就是一次選擇兩個點。在這種情況下進行匹配,要使用 NORM_HAMMING 距離。如果 WTA_K 被設置成 3 或 4,那匹配距離就要設置爲    NORM_HAMMING2

下面是一個使用 ORB 的簡單代碼。

import numpy as np
import cv2
from matplotlib import pyplot as plt

img = cv2.imread('simple.jpg', 0)
# Initiate STAR detector
orb = cv2.ORB()
# find the keypoints with ORB
kp = orb.detect(img, None)
# compute the descriptors with ORB
kp, des = orb.compute(img, kp)
# 只繪製關鍵點的位置,而不是大小和方向
img2 = cv2.drawKeypoints(img, kp, color=(0, 255, 0), flags=0)
plt.imshow(img2), plt.show()

結果如下:

我們將在其它章節介紹  ORB  特徵匹配。

37.特徵匹配

37.1 Brute-Force 匹配的基礎

蠻力匹配器是很簡單的。首先在第一幅圖像中選取一個關鍵點然後依次與 第二幅圖像的每個關鍵點進行(描述符)距離測試,最後返回距離最近的關鍵 點。

對於 BF 匹配器,我們首先要使用 cv2.BFMatcher() 創建一個 BFMatcher 對象。它有兩個可選參數。第一個是 normType。它是用來指定要 使用的距離測試類型。默認值cv2.Norm_L2。這很適合 SIFT SURF 等(cv2.NORM_L1 也可以)。對於使用二進制描述符的 ORB,BRIEF,BRISK 算法等,要使用 cv2.NORM_HAMMING,這樣就會返回兩個測試對象之 間的漢明距離。如果 ORB 算法的參數設置爲 WTA_K==3 或 4,normType 就應該設置成cv2.NORM_HAMMING2

第二個參數是布爾變量 crossCheck,默認值爲 False。如果設置爲 True,匹配條件就會更加嚴格,只有到 A 中的第 i 個特徵點與 B 中的第 j 個 特徵點距離最近,並且 B 中的第 j 個特徵點到 A 中的第 i 個特徵點也是最近(A 中沒有其它點到 j 的距離更近)時纔會返回最佳匹配(i,j)。也就是這兩個特徵點要互相匹配才行。這樣就能提供統一的結果,這可以用來替代 D.Lowe 在 SIFT 文章中提出的比值測試方法。

BFMatcher 對象具有兩個方法,BFMatcher.match() BFMatcher.knnMatch()。 第一個方法會返回最佳匹配。第二個方法爲每個關鍵點返回 k  個最佳匹配(降

序排列之後取前 k 個),其中 k 是由用戶設定的。如果除了匹配之外還要做其 它事情的話可能會用上(比如進行比值測試)。就像使用 cv2.drawKeypoints() 繪 制關 鍵點一樣, 我們可以使用 cv2.drawMatches() 來繪製匹配的點。它會將這兩幅圖像先水平排列,然後 在最佳匹配的點之間繪製直線(從原圖像到目標圖像)。如果前面使用的是 BF- Matcher.knnMatch(),現在我們可以使用函數 cv2.drawMatchsKnn 爲每個關鍵點和它的 k 個最佳匹配點繪製匹配線。如果 k 等於 2,就會爲每個 關鍵點繪製兩條最佳匹配直線。如果我們要選擇性繪製話就要給函數傳入一個 掩模。

讓我們分別看一個 ORB 和一個 SURF 的例子吧。(使用不同距離計算方法)。

(1).對 ORB 描述符進行蠻力匹配

現在我們看一個在兩幅圖像之間進行特徵匹配的簡單例子。在本例中我們 有一個查詢圖像和一個目標圖像。我們要使用特徵匹配的方法在目標圖像中尋 找查詢圖像的位置。(這兩幅圖像分別是/sample/c/box.png,和/sample/c/ box_in_scene.png)

我們使用  ORB  描述符來進行特徵匹配。首先我們需要加載圖像計算描述符。

import numpy as np
import cv2
from matplotlib import pyplot as plt

img1 = cv2.imread('box.png',0) # queryImage
img2 = cv2.imread('box_in_scene.png',0) # trainImage

下面我們要創建一個 BFMatcher 對象,並將距離計算設置爲 cv2.NORM_HAMMING(因爲我們使用的是 ORB),並將 crossCheck 設置爲 True。然後使用 Matcher.match() 方法獲得兩幅圖像的最佳匹配。然後將匹配結果按特徵點之間的距離進行降序排列,這樣最佳匹配就會排在前面了。最後我們只將前 10 個匹配繪製出來(太多了看不清,如果願意的話你可以多畫幾條)。

# Initiate SIFT detector
orb = cv2.ORB()
# find the keypoints and descriptors with SIFT 
kp1, des1 = orb.detectAndCompute(img1,None)
kp2, des2 = orb.detectAndCompute(img2,None)
# create BFMatcher object
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
# Match descriptors.
matches = bf.match(des1,des2)
# Sort them in the order of their distance.
matches = sorted(matches, key = lambda x:x.distance)

下面就是我得到的結果。

(2).匹配器對象

matches = bf.match(des1, des2) 返回值是一個 DMatch 對象列表。這個DMatch 對象具有下列屬性:

• DMatch.distance - 描述符之間的距離。越小越好。
• DMatch.trainIdx - 目標圖像中描述符的索引。
• DMatch.imgIdx - 目標圖像的索引。
• DMatch.queryIdx - 查詢圖像中描述符的索引。

(3).對 SIFT 描述符進行蠻力匹配和比值測試

現在我們使用 BFMatcher.knnMatch() 來獲得 k 對最佳匹配。在本例中 我們設置 k = 2,這樣我們就可以使用 D.Lowe 文章中的比值測試了。

import numpy as np
import cv2
from matplotlib import pyplot as plt

img1 = cv2.imread('box.png', 0)
img2 = cv2.imread('box_in_scene.png', 0)  # trainImage
# Initiate SIFT detector
sift = cv2.SIFT()
# find the keypoints and descriptors with SIFT 
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
# BFMatcher with default params
bf = cv2.BFMatcher()
matches = bf.knnMatch(des1, des2, k=2)
# Apply ratio test
# 比值測試,首先獲取與A 距離最近的點B(最近)和C(次近),只有當B/C小於閾值時(0.75)才被認爲是匹配,因爲假設匹配是一一對應的,真正的匹配的理想距離爲0
good = []
for m, n in matches:
    if m.distance < 0.75 * n.distance: good.append([m])

# cv2.drawMatchesKnn expects list of lists as matches.
img3 = cv2.drawMatchesKnn(img1, kp1, img2, kp2, good[:10], flags=2)
plt.imshow(img3), plt.show()

結果如下:

37.2 FLANN 匹配器

FLANN 是快速最近鄰搜索包(Fast_Library_for_Approximate_Nearest_Neighbors) 的簡稱。它是一個對大數據集和高維特徵進行最近鄰搜索的算法的集合,而且 這些算法都已經被優化過了。在面對大數據集時它的效果要好於 BFMatcher。 我們來對第二個例子使用   FLANN  匹配看看它的效果。

使用 FLANN 匹配,需要傳入兩個字典作爲參數。這兩個用來確定要使用的算法和其它相關參數等。第一個是 IndexParams。各種不同算法的信 息可以在 FLANN 文檔中找到。這裏我們總結一下,對於 SIFT 和 SURF 等, 我們可以傳入的參數是:

indexparams = dict(algorithm = FLANNI NDEXKDTREE, trees = 5)

但使用 ORB 時,我們要傳入的參數如下。註釋掉的值是文獻中推薦使用 的,但是它們並不適合所有情況,其它值的效果可能會更好。

import numpy as np
import cv2
from matplotlib import pyplot as plt

img1 = cv2.imread('box.png', 0)
img2 = cv2.imread('box_in_scene.png', 0)  # trainImage
# Initiate SIFT detector
sift = cv2.SIFT()
# find the keypoints and descriptors with SIFT 
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
# BFMatcher with default params
bf = cv2.BFMatcher()
matches = bf.knnMatch(des1, des2, k=2)
# Apply ratio test
good = []
for m, n in matches:
    if m.distance < 0.75 * n.distance: good.append([m])

# cv2.drawMatchesKnn expects list of lists as matches.
img3 = cv2.drawMatchesKnn(img1, kp1, img2, kp2, good, flags=2)
plt.imshow(img3), plt.show()

第二個字典是 SearchParams,用它來指定遞歸遍歷的次數,值越高結果越準確, 但是消耗的時間也越多。 如果想修改這個值, 傳入參數:searchparams = dict(checks = 100)。

有了這些信息我們就可以開始了。

import numpy as np
import cv2
from matplotlib import pyplot as plt

img1 = cv2.imread('box.png', 0)
img2 = cv2.imread('box_in_scene.png', 0)  # trainImage
# Initiate SIFT detector
sift = cv2.SIFT()
# find the keypoints and descriptors with SIFT 
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
# FLANN parameters
FLANN_INDEX_KDTREE = 0
index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5) 
search_params = dict(checks=50)    # or pass empty dictionary

flann = cv2.FlannBasedMatcher(index_params,search_params) 
matches = flann.knnMatch(des1,des2,k=2)
# Need to draw only good matches, so create a mask
matchesMask = [[0,0] for i in range(len(matches))]
# ratio test as per Lowe's paper
for i,(m,n) in enumerate(matches):
    if m.distance < 0.7*n.distance:
        matchesMask[i]=[1,0]

draw_params = dict(matchColor = (0,255,0),singlePointColor = (255,0,0), matchesMask = matchesMask, flags = 0)
img3 = cv2.drawMatchesKnn(img1,kp1,img2,kp2,matches,None,**draw_params) 
plt.imshow(img3,),plt.show()

結果如下:

38.使用特徵匹配和單應性查找對象

聯合使用特徵提取和 calib3d 模塊中的 findHomography 在複雜圖像 中查找已知對象。

上一節我們使用一個查詢圖像,在其中找到一些特徵點(關鍵點),我們又在另一幅圖像中也找到了一些特徵點,最後對這兩幅圖像之間的特徵點進行匹配。簡單來說就是:我們在一張雜亂的圖像中找到了 一個對象(的某些部分)的位置。這些信息足以幫助我們在目標圖像中準確找到(查詢圖像)對象。

爲了達到這個目的我們可以使用 calib3d 模塊中的 cv2.findHomography() 函數。如果將這兩幅圖像中的特徵點集傳給這個函數,它就會找到這個對象的 透視圖變換。然後我們就可以使用函數 cv2.perspectiveTransform() 找到這 個對象了。至少要 4 個正確的點才能找到這種變換。

我們已經知道在匹配過程可能會有一些錯誤,而這些錯誤會影響最終結果。爲了解決這個問題,算法使用 RANSAC 和 LEAST_MEDIAN(可以通過 參數來設定)。所以好的匹配提供的正確的估計被稱爲 inliers,剩下的被稱爲 outliers。cv2.findHomography() 返回一個掩模,這個掩模確定了 inlier 和 outlier  點。

和通常一樣我們先在圖像中來找到 SIFT 特徵點,然後再使用比值測試找 到最佳匹配。

import numpy as np
import cv2
from matplotlib import pyplot as plt

img1 = cv2.imread('box.png', 0)
img2 = cv2.imread('box_in_scene.png', 0)  # trainImage
# Initiate SIFT detector
sift = cv2.SIFT()
# find the keypoints and descriptors with SIFT 
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
FLANN_INDEX_KDTREE = 0
index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5) 
search_params = dict(checks = 50)
flann = cv2.FlannBasedMatcher(index_params, search_params) 
matches = flann.knnMatch(des1,des2,k=2)
# store all the good matches as per Lowe's ratio test.
good = []
for m,n in matches:
    if m.distance < 0.7*n.distance: 
        good.append(m)

現在設置只有存在 10 個以上匹配時纔去查找目標(MIN_MATCH_COUNT=10), 否則顯示警告消息:“現在匹配不足!”如果找到了足夠的匹配, 我們要提取兩幅圖像中匹配點的座標, 把它們傳入到函數中計算透視變換。一旦我們找到 3x3 的變換矩陣,就可以使用它將查 詢圖像的四個頂點(四個角)變換到目標圖像中去了。然後再繪製出來。

if len(good)>MIN_MATCH_COUNT:
    # 獲取關鍵點的座標
    src_pts = np.float32([ kp1[m.queryIdx].pt for m in good ]).reshape(-1,1,2) 
    dst_pts = np.float32([ kp2[m.trainIdx].pt for m in good ]).reshape(-1,1,2)
    # 第三個參數  用於計算單應矩陣的方法。以下是可能的方法:
    # 0 - 使用所有點的常規方法
    # CV_RANSAC - RANSAC-based robust method
    # CV_LMEDS - Least-Median robust method
    # 第四個參數取值範圍在  1 到 10, 拒絕一個點對的閾值。原圖像的點經過變換後點與目標圖像上對應點的誤差, 超過誤差就認爲是  outlier
    # 返回值中  M 爲變換矩陣。
    M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC,5.0) 
    matchesMask = mask.ravel().tolist()
    # 獲得原圖像的高和寬
    h,w = img1.shape
    # 使用得到的變換矩陣對原圖像的四個角進行變換,獲得在目標圖像上對應的座標。
    pts = np.float32([ [0,0],[0,h-1],[w-1,h-1],[w-1,0] ]).reshape(-1,1,2) 
    dst = cv2.perspectiveTransform(pts,M)
    # 原圖像爲灰度圖
    cv2.polylines(img2,[np.int32(dst)],True,255,10, cv2.LINE_AA)

else:
    print ("Not enough matches are found - %d/%d" % (len(good),MIN_MATCH_COUNT))
    matchesMask = None

最後再繪製 inliers(如果成功找到目標圖像)或者匹配關鍵點(如果失敗)。

draw_params = dict(matchColor = (0,255,0), # draw matches in green color
                   singlePointColor = None,
                   matchesMask = matchesMask, # draw only inliers
                   flags = 2)
img3 = cv2.drawMatches(img1,kp1,img2,kp2,good,None,**draw_params)
plt.imshow(img3, 'gray'),plt.show()

結果如下。複雜圖像中被找到的目標圖像被標記成白色。

 

第六章.視頻分析(運動目標檢測)

39.Meanshift 和 Camshift

39.1 Meanshift

Meanshift(均值偏移) 算法的基本原理很簡單。假設我們有一堆點(可以是像直方圖反投影那樣的像素分佈)和一個小窗口(可能是一個圓圈),我們要完成的任務就是將這個窗口移動到最大灰度密度處(或者是點最多的地方)。如下圖所示:

 

初始窗口是藍色“C1”,它的圓心爲藍色方框“C1_o”,而窗口中所有點的質心卻是“C1_r” (小的藍色圓圈),很明顯圓心和點的質心沒有重合。所以移動圓 心 C1_o  到質心 C1_r,這樣我們就得到了一個新的窗口。這時又可以找到新 窗口內所有點的質心,大多數情況下還是不重合的,所以重複上面的操作:將 新窗口的中心移動到新的質心。就這樣不停的迭代操作直到窗口的中心和其所 包含點的質心重合爲止(或者有一點小誤差)。按照這樣的操作我們的窗口最終 會落在像素值(和)最大的地方。如上圖所示“C2”是窗口的最後位置,可 以看出該窗口中的像素點最多。整個過程如下圖所示

通常情況下我們要使用直方圖方向投影得到的圖像和目標對象的起始位置。 當目標對象的移動會反映到直方圖反向投影圖中。就這樣,meanshift 算法把窗口移動到圖像中灰度密度最大的區域。

 

要在 OpenCV 中使用 Meanshift 算法首先要對目標對象進行設置, 計算目標對象的直方圖,這樣在執行 meanshift 算法時就可以將目標對象反向投影到每一幀中。另外我們還需要提供窗口的起始位置。在這裏我們值計算 H(Hue)通道的直方圖,同樣爲了避免低亮度造成的影響,我們使 用函數  cv2.inRange()  將低亮度的值忽略掉。

import numpy as np 
import cv2

cap = cv2.VideoCapture('slow.flv')
# take first frame of the video
ret,frame = cap.read()
# setup initial location of window
r,h,c,w = 250,90,400,125  # simply hardcoded the values
track_window = (c,r,w,h)
# set up the ROI for tracking
roi = frame[r:r+h, c:c+w]
hsv_roi =  cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv_roi, np.array((0., 60.,32.)), np.array((180.,255.,255.))) 
roi_hist = cv2.calcHist([hsv_roi],[0],mask,[180],[0,180]) 
cv2.normalize(roi_hist,roi_hist,0,255,cv2.NORM_MINMAX)
# Setup the termination criteria, either 10 iteration or move by atleast 1 pt
term_crit = ( cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 1 )

while(1):
    ret ,frame = cap.read()
    if ret == True:
        hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
        dst = cv2.calcBackProject([hsv],[0],roi_hist,[0,180],1)
        # apply meanshift to get the new location
        ret, track_window = cv2.meanShift(dst, track_window, term_crit)
        # Draw it on image
        x,y,w,h = track_window
        img2 = cv2.rectangle(frame, (x,y), (x+w,y+h), 255,2) 
        cv2.imshow('img2',img2)
        k = cv2.waitKey(60) & 0xff
        if k == 27:
            break 
        else:
            cv2.imwrite(chr(k)+".jpg",img2)
    else:
        break

cv2.destroyAllWindows() cap.release()

下面是使用  meanshift   算法對一個視頻前三幀分析的結果:

 

39.2 Camshift

上面的結果有一個問題。我們的窗口的大小是固定的,而汽車由遠及近(在視覺上)是一個逐漸變大的過程,固定的窗口是不合適的。所以我們需要根據目標的大小和角度來對窗口的大小和角度進行修訂。OpenCVLabs 爲我們帶來的解決方案(這個解決方案來自“OpenCV實驗室”,它被稱爲CAMshift(連續自適應Meanshift),由Gary Bradsky於1998年在它的論文“用於感知用戶界面的計算機視覺人臉跟蹤”中發表)。這個算法首先要使用 meanshift,meanshift 找到(並覆蓋)目標之後, 再去調整窗口的大小, 。它還會計算目標對象的最佳外接橢圓的角度,並以此調節窗口角度。然後使用更新後的窗口大小和角度來在原來的位 置繼續進行 meanshift。重複這個過程知道達到需要的精度。如下圖所示:

與 Meanshift 基本一樣,但是返回的結果是一個帶旋轉角度的矩形(這是 我們的結果),以及這個矩形的參數(被用到下一次迭代過程中)。下面是代碼:

import numpy as np 
import cv2

cap = cv2.VideoCapture('slow.flv')
# take first frame of the video
ret,frame = cap.read()
# setup initial location of window
r,h,c,w = 250,90,400,125  # simply hardcoded the values
track_window = (c,r,w,h)
# set up the ROI for tracking
roi = frame[r:r+h, c:c+w]
hsv_roi =  cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv_roi, np.array((0., 60.,32.)), np.array((180.,255.,255.))) 
roi_hist = cv2.calcHist([hsv_roi],[0],mask,[180],[0,180]) 
cv2.normalize(roi_hist,roi_hist,0,255,cv2.NORM_MINMAX)
# Setup the termination criteria, either 10 iteration or move by atleast 1 pt
term_crit = ( cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 1 )
while(1):
    ret ,frame = cap.read()
    if ret == True:
        hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
        dst = cv2.calcBackProject([hsv],[0],roi_hist,[0,180],1)
        # apply meanshift to get the new location
        ret, track_window = cv2.CamShift(dst, track_window, term_crit)
        # Draw it on image
        pts = cv2.boxPoints(ret) 
        pts = np.int0(pts)
        img2 = cv2.polylines(frame,[pts],True, 255,2) 
        cv2.imshow('img2',img2)
        k = cv2.waitKey(60) & 0xff
        if k == 27:
            break 
        else:
            cv2.imwrite(chr(k)+".jpg",img2)
    else:
        break

cv2.destroyAllWindows() 
cap.release()

對三幀圖像分析的結果如下:

OpenCV 的官方示例中有一個 camshift 的交互式演示:

'''
Camshift tracker
================
這個demo演示了基於均值漂移(Camshift )的跟蹤
你選擇一個顏色對象,比如你的臉,它會跟蹤它。這將從攝像機讀取數據(默認爲0,或用戶輸入的攝像機編號)

 http://www.robinhewitt.com/research/track/camshift.html
Usage:
------
camshift.py [<video source>]
To initialize tracking, select the object with mouse Keys:
-----
ESC    - exit
b  - toggle back-projected probability visualization
'''

import numpy as np 
import cv2
import video    # local module

class App(object):
    def   init  (self, video_src):
        self.cam = video.create_capture(video_src) 
        ret, self.frame = self.cam.read() 
        cv2.namedWindow('camshift') 
        cv2.setMouseCallback('camshift', self.onmouse)
    
        self.selection = None 
        self.drag_start = None 
        self.tracking_state = 0 
        self.show_backproj = False
        
    def onmouse(self, event, x, y, flags, param): 
        x, y = np.int16([x, y])
        if event == cv2.EVENT_LBUTTONDOWN: 
            self.drag_start = (x, y) 
            self.tracking_state = 0
        # 官方示例中下面一行判斷有問題,作如下修改就可以了
        if self.drag_start and  event == cv2.EVENT_MOUSEMOVE:
            #  print x,y
            if flags==cv2.EVENT_FLAG_LBUTTON:
                #  print 'ok'
                h, w = self.frame.shape[:2] 
                xo, yo = self.drag_start
                x0, y0 = np.maximum(0, np.minimum([xo, yo], [x, y]))
                x1, y1 = np.minimum([w, h], np.maximum([xo, yo], [x, y])) 
                self.selection = None
                if x1-x0 > 0 and y1-y0 > 0: 
                    self.selection = (x0, y0, x1, y1)
                    print (self.selection)
            else:
                self.drag_start = None
                if self.selection is not None: 
                    self.tracking_state = 1
    
    def show_hist(self):
        bin_count = self.hist.shape[0]

40.光流

由於目標對象或者攝像機的移動造成的圖像對象在連續兩幀圖像中的移動被稱爲光流。它是一個 2D 向量場,可以用來顯示一個點從第一幀圖像到第二 幀圖像之間的移動。如下圖所示(Image Courtesy: Wikipedia article on Optical  Flow):

上圖顯示了一個點在連續五幀圖像間的移動。箭頭表示光流場向量。光流在很多領域中都很有用,比如:

• 由運動重建結構

• 視頻壓縮

• 視頻穩定... 

光流是基於以下假設的:

1.  在連續的兩幀圖像之間(目標對象的)像素的灰度值不改變。

2.  相鄰的像素具有相同的運動

第一幀圖像中的像素 I (x,y,t) 在時間 dt 後移動到第二幀圖像的(x+dx,y+dy)處。根據第一條假設:灰度值不變。所以我們可以得到:

I (x, y, t) = I (x + dx, y + dy, t + dt)

對等號右側進行泰勒級數展開,消去相同項,兩邊都除以 dt,得到如下方程:

fxu + fyv + ft = 0

其中:

上邊的等式叫做光流方程。其中 fx 和 fy 是圖像梯度,同樣 ft 是時間方向 的梯度。但(u,v)是不知道的。我們不能在一個等式中求解兩個未知數。有 幾個方法可以幫我們解決這個問題,其中的一個是 Lucas-Kanade 法

40.1 OpenCV中的Lucas-Kanade 光流

現在我們要使用第二條假設,鄰域內的所有點都有相似的運動。Lucas- Kanade 法就是利用一個 3x3 鄰域中的 9 個點具有相同運動的這一點。這樣 我們就可以找到這 9 個點的光流方程,用它們組成一個具有兩個未知數 9 個等 式的方程組,這是一個約束條件過多的方程組。一個好的解決方法就是使用最 小二乘擬合。下面就是求解結果:

(有沒有發現上邊的逆矩陣與 Harris 角點檢測器非常相似,這說明角點很適合被用來做跟蹤)

從使用者的角度來看,想法很簡單,我們取跟蹤一些點,然後我們就會獲得 這些點的光流向量。但是還有一些問題。直到現在我們處理的都是很小的運動。 如果有大的運動怎麼辦呢?圖像金字塔。我們可以使用圖像金字塔的頂層,此 時小的運動被移除,大的運動裝換成了小的運動,現在再使用 Lucas-Kanade 算法,我們就會得到尺度空間上的光流。

上述所有過程都被 OpenCV 打包成了一個函數:cv2.calcOpticalFlowPyrLK()。 現在我們使用這個函數創建一個小程序來跟蹤視頻中的一些點。要跟蹤那些點 呢?我們使用函數 cv2.goodFeatureToTrack() 來確定要跟蹤的點。首先在視頻的第一幀圖像中檢測一些 Shi-Tomasi 角點,然後我們使用 Lucas-Kanade 算法迭代跟蹤這些角點。我們要給函數 cv2.calcOpticlaFlowPyrLK()傳入前一幀圖像和其中的點,以及下一幀圖像。函數將返回帶有狀態數的點, 如果狀態數是 1,那說明在下一幀圖像中找到了這個點(上一幀中角點),如果 狀態數是 0,就說明沒有在下一幀圖像中找到這個點。我們再把這些點作爲參 數傳給函數,如此迭代下去實現跟蹤。代碼如下:

import numpy as np
import cv2

cap = cv2.VideoCapture('slow.flv')
# params for ShiTomasi corner detection
feature_params = dict(maxCorners=100, qualityLevel=0.3, minDistance=7, blockSize=7)

# Parameters for lucas kanade optical flow
# maxLevel 爲使用的圖像金字塔層數
lk_params = dict(winSize=(15, 15), maxLevel=2,
                 criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))

# Create some random colors
color = np.random.randint(0, 255, (100, 3))
# Take first frame and find corners in it
ret, old_frame = cap.read()
old_gray = cv2.cvtColor(old_frame, cv2.COLOR_BGR2GRAY)
p0 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params)
# Create a mask image for drawing purposes
mask = np.zeros_like(old_frame)

while (1):
    ret, frame = cap.read()
    frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    # calculate optical flow 能夠獲取點的新位置
    p1, st, err = cv2.calcOpticalFlowPyrLK(old_gray, frame_gray, p0, None, **lk_params)
    # Select good points
    good_new = p1[st == 1]
    good_old = p0[st == 1]
    # draw the tracks
    for i, (new, old) in enumerate(zip(good_new, good_old)):
        a, b = new.ravel()
        c, d = old.ravel()
        mask = cv2.line(mask, (a, b), (c, d), color[i].tolist(), 2)
        frame = cv2.circle(frame, (a, b), 5, color[i].tolist(), -1)
    img = cv2.add(frame, mask)

    cv2.imshow('frame', img)
    k = cv2.waitKey(30) & 0xff
    if k == 27:
        break
    
    # Now update the previous frame and previous points
    old_gray = frame_gray.copy()
    p0 = good_new.reshape(-1, 1, 2)

cv2.destroyAllWindows()
cap.release()

下面是結果:

(上面的代碼沒有對返回角點的正確性進行檢查。圖像中的一些特徵點甚至 在丟失以後,光流還會找到一個預期相似的點。所以爲了實現穩定的跟蹤,我們應該每個一定間隔就要進行一次角點檢測。OpenCV 的官方示例中帶有這樣 一個例子,它是每 5 幀進行一個特徵點檢測。它還對光流點使用反向檢測來選取好的點進行跟蹤。示例爲/samples/python2/lk_track.py)

40.2 OpenCV中的稠密光流

Lucas-Kanade方法計算稀疏特徵集的光流(在我們的示例中,使用Shi-Tomasi算法檢測拐角)。OpenCV提供了另一種尋找稠密光流的算法。它計算幀中所有點的光流。它基於Gunner Farneback的算法,Gunner Farneback在2003年的“基於多項式展開的兩幀運動估計”中解釋了該算法。

下面的例子使用上面的算法計算稠密光流。結果是一個帶有光流向量(u,v)的雙通道數組。通過計算我們能得到光流的大小和方向。我們使用顏色對結果進行編碼以便於更好的觀察。方向對應於 H(Hue)通道,大小對應 於V(Value)通道。代碼如下:

import cv2
import numpy as np

cap = cv2.VideoCapture("vtest.avi")

ret, frame1 = cap.read()
prvs = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY)
hsv = np.zeros_like(frame1)
hsv[..., 1] = 255

while (1):
    ret, frame2 = cap.read()
    next = cv2.cvtColor(frame2, cv2.COLOR_BGR2GRAY)
    # cv2.calcOpticalFlowFarneback(prev, next, pyr_scale, levels, winsize, iterations, poly_n, poly_sigma, flags[)
    # pyr_scale – parameter, specifying the image scale (<1) to build pyramids for each image; pyr_scale=0.5 means a classical pyramid, where each next layer is twice smaller than the previous one.
    # poly_n – size of the pixel neighborhood used to find polynomial expansion in each pixel; typically poly_n =5 or 7.
    # poly_sigma – standard deviation of the Gaussian that is used to smooth derivatives used as a basis for the polynomial expansion; for poly_n=5, you can set poly_sigma=1.1, for poly_n=7, a good value would be poly_sigma=1.5.
    # flag(可選) - 0或1,0計算快,1慢但準確
    flow = cv2.calcOpticalFlowFarneback(prvs, next, None, 0.5, 3, 15, 3, 5, 1.2, 0)
    
    # cv2.cartToPolar Calculates the magnitude and angle of 2D vectors.
    mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1])
    hsv[..., 0] = ang * 180 / np.pi / 2
    hsv[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)
    rgb = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)

    cv2.imshow('frame2', rgb)
    k = cv2.waitKey(30) & 0xff
    if k == 27:
        break
    elif k == ord('s'):
        cv2.imwrite('opticalfb.png', frame2)
        cv2.imwrite('opticalhsv.png', rgb)
    prvs = next

cap.release()
cv2.destroyAllWindows()

結果如下:

OpenCV 官方示例中有一個更高級的稠密光流/samples/python2/opt_flow.py!

41.背景減除

背景減除(Background subtraction,BS)是一種使用靜態相機生成前景掩碼(即包含場景中移動物體像素的二值圖像)的常用技術。顧名思義,BS計算前景掩碼,執行當前幀和背景模型之間的減法,包含場景的靜態部分,或者更一般地說,考慮到所觀察場景的特徵,所有可以被視爲背景的部分。

後臺建模包括兩個主要步驟:

1.後臺初始化;

2.後臺更新。

第一步,計算背景的初始模型,而在第二步中,更新模型以適應場景中可能的變化。

在很多基礎應用中背景減除都是一個非常重要的步驟。例如顧客統計中使用一個靜態攝像頭來記錄進入和離開房間的人數,或者是交通攝像頭中需要提取交通工具的信息等。在所有的這些例子中,首先要將人或車單獨提取出來。 技術上來說,我們需要從靜止的背景中提取移動的前景。

如果你有一張背景(僅有背景不含前景)圖像,比如沒有顧客的房間,沒有交通工具的道路等,那就好辦了。我們只需要在新圖像中減去背景就可以得到前景對象了。但是在大多數情況下,我們沒有這樣的(背景)圖像,所以我們需要從現有圖像中提取背景。如果圖像中的交通工具有影子時這個工作就更難了,因爲影子也在移動,僅僅使用減法會把影子也當成前景。 真是一件很複雜的事情。爲了實現這個目的,科學家們已經提出了幾種算法。OpenCV 中已經包含了很多比較容易使用的方法。如下所示:

 

 

 

 

cv::Algorithm

 

 

 

 

cv::BackgroundSubtractor

cv::BackgroundSubtractorKNN

cv::BackgroundSubtractorMOG2

cv::bgsegm::BackgroundSubtractorCNT

cv::bgsegm::BackgroundSubtractorGMG

cv::bgsegm::BackgroundSubtractorGSOC

cv::bgsegm::BackgroundSubtractorLSBP

cv::bgsegm::BackgroundSubtractorMOG

cv::cuda::BackgroundSubtractorFGD

cv::cuda::BackgroundSubtractorGMG

cv::cuda::BackgroundSubtractorMOG

(1).BackgroundSubtractorMOG

這是一個以混合高斯模型爲基礎的前景/背景分割算法。它是   P.KadewTraKuPong 和 R.Bowden 在 2001 年提出的。它使用 K(K=3 或 5)個高斯分佈混合對 背景像素進行建模。使用這些顏色(在整個視頻中)存在時間的長短作爲混合 的權重。背景的顏色一般持續的時間最長,而且更加靜止。一個像素怎麼會有 分佈呢?在 x,y 平面上一個像素就是一個像素沒有分佈,但是我們現在講的 背景建模是基於時間序列的,因此每一個像素點所在的位置在整個時間序列中 就會有很多值,從而構成一個分佈。

在編寫代碼時,我們需要使用函數:cv2.createBackgroundSubtractorMOG() 創建一個背景對象。這個函數有些可選參數,比如要進行建模場景的時間長度, 高斯混合成分的數量,閾值等。將它們全部設置爲默認值。然後在整個視頻中 我們是需要使用   backgroundsubtractor.apply() 就可以得到前景的掩模了。

下面是一個簡單的例子:

import cv2

cap = cv2.VideoCapture(0)
fgbg = cv2.createBackgroundSubtractorMOG()
while(1):
    ret, frame = cap.read()
    fgmask = fgbg.apply(frame)
    cv2.imshow('frame',fgmask) 
    k = cv2.waitKey(30) & 0xff 
    if k == 27:
        break
cap.release() 
cv2.destroyAllWindows()

原始圖像

下圖顯示了一段視頻中的第  200 幀圖像

MOG的結果:

(2).BackgroundSubtractorMOG2

這個也是以高斯混合模型爲基礎的背景/前景分割算法。它是以 2004 年 和 2006 年 Z.Zivkovic 的兩篇文章爲基礎的。這個算法的一個特點是它爲每 一個像素選擇一個合適數目的高斯分佈。(上一個方法中我們使用是 K 高斯分 布)。這樣就會對由於亮度等發生變化引起的場景變化產生更好的適應。

和前面一樣我們需要創建一個背景對象。但在這裏我們我們可以選擇是否 檢測陰影。如果 detectShadows = True(默認值),它就會檢測並將影子標記出來,但是這樣做會降低處理速度。影子會被標記爲灰色。

import cv2

cap = cv2.VideoCapture('vtest.avi')
fgbg = cv2.createBackgroundSubtractorMOG2()
while(1):
    ret, frame = cap.read()
    fgmask = fgbg.apply(frame)
    cv2.imshow('frame',fgmask) 
    k = cv2.waitKey(30) & 0xff 
    if k == 27:
        break
cap.release() 
cv2.destroyAllWindows()

MOG2的結果(灰色區域代表陰影):

(3).BackgroundSubtractorGMG

此算法結合了靜態背景圖像估計和每個像素的貝葉斯分割。這是 2012 年 Andrew_B.Godbehere,Akihiro_Matsukawa 和 Ken_Goldberg 在文章中提出的。

它使用前面很少的圖像(默認爲前 120 幀)進行背景建模。使用了概率前景估計算法(使用貝葉斯估計鑑定前景)。這是一種自適應的估計,新觀察到的對象比舊的對象具有更高的權重,從而對光照變化產生適應。一些形態學操作如開運算閉運算等被用來除去不需要的噪音。在前幾幀圖像中你會得到一個黑色窗口。

對結果進行形態學開運算對與去除噪聲很有幫助。

import cv2

cap = cv2.VideoCapture('vtest.avi')
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(3,3)) 
fgbg = cv2.createBackgroundSubtractorGMG()
while(1):
    ret, frame = cap.read()
    fgmask = fgbg.apply(frame)
    fgmask = cv2.morphologyEx(fgmask, cv2.MORPH_OPEN, kernel)
    cv2.imshow('frame',fgmask) 
    k = cv2.waitKey(30) & 0xff 
    if k == 27:
        break
cap.release() 
cv2.destroyAllWindows()

GMG 的結果(使用形態學開運算將噪音去除):

(4).BackgroundSubtractorKNN

基於k近鄰的背景/前景分割算法。該類實現了[248]中描述的k近鄰背景減法。如果前景像素的數量比較低將非常有效。示例如下:

backgroundSubtractor = cv2.createBackgroundSubtractorKNN(detectShadows=True)
structuringElement = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))

# 利用幀差法獲取矩形框
def getBoxes(frame, backgroundSubtractor, structuringElement):
    boxes = []
    gray_L = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    fgmask = backgroundSubtractor.apply(gray_L)  # 背景分割器,該函數計算了前景掩碼
    # 二值化閾值處理,前景掩碼含有前景的白色值以及陰影的灰色值,在閾值化圖像中,將非純白色(244~255)的所有像素都設爲0,而不是255
    th = cv2.threshold(fgmask.copy(), 244, 255, cv2.THRESH_BINARY)[1]
    dilated = cv2.dilate(th, structuringElement, iterations=2)  # 形態學膨脹
    # 該函數計算一幅圖像中目標的輪廓
    contours, hierarchy = cv2.findContours(image=dilated, mode=cv2.RETR_EXTERNAL,method=cv2.CHAIN_APPROX_SIMPLE)
    for cnt in contours:
        if cv2.contourArea(cnt) > 2000:
            (x, y, w, h) = cv2.boundingRect(cnt)  # 外接矩形
            boxes.append((x, y, w, h))

    return frame, boxes

KNN的結果(灰色區域爲檢測到的陰影):

因篇幅過長,後續部分參見:OpenCV-Python (官方)中文教程(部分三)

 

發佈了43 篇原創文章 · 獲贊 99 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章