圖像處理作業5——SIFT算法與全景圖像生成

在這裏插入圖片描述


前前後後花了差不多兩週的時間,終於完成了最後一個圖像處理大作業,由於自己太菜,這個作業屬實有點難頂哦,不過還好成功實現並按時提交,爲自己乾杯,哈哈!本篇博客記錄自己的學習筆記及過程,以備以後回味和複習。

IDE:Jupyter notebook

題目:用手機或者相機拍攝不同角度圖像(彼此之間有一定的重疊),用SIFT算子提取特徵,通過特徵匹配、圖像旋轉和圖像融合等操作,將圖像拼接在一起,形成大場景圖像。

1. 實驗思路

(1)嘗試採用SIFT特徵描述子提取特徵;

(2)嘗試特徵匹配;

(3)找到變換矩陣,變換圖像;

(4)拼接融合圖像。

2. 實驗原理

SIFT算法簡介

SIFT (Scale-invariant feature transform)尺度不變特徵轉換即是一種計算機視覺的算法。它用來偵測與描述影像中的局部性特徵,它在空間尺度中尋找極值點,並提取出其位置、尺度、旋轉不變量,此算法由 David Lowe在1999年所發表,2004年完善總結。

SIFT算法的實質是在不同的尺度空間上查找關鍵點(特徵點),並計算出關鍵點的方向。SIFT所查找到的關鍵點是一些十分突出,不會因光照,仿射變換和噪音等因素而變化的點,如角點、邊緣點、暗區的亮點及亮區的暗點等。

SIFT算法的特點有:

  1. SIFT特徵是圖像的局部特徵,其對旋轉、尺度縮放、亮度變化保持不變性,對視角變化、仿射變換、噪聲也保持一定程度的穩定性;

  2. 獨特性(Distinctiveness)好,信息量豐富,適用於在海量特徵數據庫中進行快速、準確的匹配;

  3. 多量性,即使少數的幾個物體也可以產生大量的SIFT特徵向量;

  4. 高速性,經優化的SIFT匹配算法甚至可以達到實時的要求;

  5. 可擴展性,可以很方便的與其他形式的特徵向量進行聯合。

算法流程

在這裏插入圖片描述

SIFT算法實現物體識別主要有三大工序,1、提取關鍵點;2、對關鍵點附加詳細的信息(局部特徵)也就是所謂的描述器;3、通過兩方特徵點(附帶上特徵向量的關鍵點)的兩兩比較找出相互匹配的若干對特徵點,也就建立了景物間的對應關係。

SIFT算法操作步驟

1. 關鍵點檢測

1.1 哪些是SIFT中要查找的關鍵點(特徵點)?

所爲關鍵點,就是在不同尺度空間的圖像下檢測出的具有方向信息的局部極值點。可以得出關鍵點具有的三個特徵:尺度、方向、大小。

1.2 什麼是尺度空間(scale space)?

尺度空間理論最早是在1962年提出,其主要思想是通過對原始圖像進行尺度變換,獲得圖像多尺度下的尺度空間表示序列,對這些序列進行尺度空間主輪廓的提取,並以該主輪廓作爲一種特徵向量,實現邊緣、角點檢測和不同分辨率上的特徵提取等。

尺度空間中各尺度圖像的模糊程度逐漸變大,能夠模擬人在距離目標由近到遠時目標在視網膜上的形成過程。尺度越大,圖像越模糊。

高斯核是唯一可以產生多尺度空間的核,一個圖像的尺度空間,L(x,y,σ)L(x,y,\sigma),定義爲原始圖像I(x,y)I(x,y)與一個可變尺度的2維高斯函數G(x,y,σ)G(x,y,\sigma)卷積運算。高斯函數定義爲:

G(xi,yi,σ)=12πσ2exp((xxi)2=(yyi)22σ2)G(x_i,y_i,\sigma)=\frac{1}{2\pi\sigma^2}exp(-\frac{(x-x_i)^2=(y-y_i)^2}{2\sigma^2})

L(x,y,σ)=G(x,y,σ)I(x,y)L(x,y,\sigma)=G(x,y,\sigma)*I(x,y)

尺度是自然存在的,不是人爲創造的!高斯卷積只是表現尺度空間的一種形式……

1.3 高斯模糊

高斯模糊通常用來減小圖像噪聲以及降低細節層次,這種模糊技術生成的圖像的視覺效果是好像經過一個半透明的屏幕觀察圖像。

G(r)=12πσ2exp(r22σ2)G(r)=\frac{1}{2\pi\sigma^2}exp(-\frac{r^2}{2\sigma^2})

rr爲模糊半徑,r=x2+y2r=\sqrt{x^2+y^2}

在實際應用中,在計算高斯函數的離散近似值時,在大概3σ3\sigma距離之外的像素都可以看作不起作用,這些像素的計算就可以忽略。

對一幅圖像進行多次連續高斯模糊的效果與一次更大的高斯模糊可以產生同樣的效果。例如,使用半徑爲6和8的兩次高斯模糊變換得到的效果等同於一次半徑爲10的高斯模糊效果,62+82=10\sqrt{6^2+8^2}=10

1.4 高斯金字塔

高斯金字塔的構建過程可分爲兩步:(1)對圖像做高斯平滑;(2)對圖像做降採樣。爲了讓尺度體現連續性,在簡單下采樣的基礎上加上了高斯濾波。一幅圖像可以產生幾組(octave)圖像,一組圖像包括幾層(interval)圖像。

在這裏插入圖片描述

高斯金字塔共o組、s層,則有:σ(s)=σ02sS\sigma(s)=\sigma_02^\frac{s}{S}

σ\sigma——尺度空間座標;s——sub-level層座標;σ0\sigma_0——初始尺度;SS——每組層數(一般爲3~5層)。

高斯金字塔的組內尺度與組間尺度:組內尺度是指同一組(octave)內的尺度關係,σs+1=σs21S\sigma_{s+1}=\sigma_s2^\frac{1}{S},組間尺度是指不同組直接的尺度關係,相鄰組的尺度可化爲:σo+1(s)=σo2s+SS\sigma_{o+1}(s)=\sigma_o2^\frac{s+S}{S}σo2s+SS=2σo2sS\sigma_o2^\frac{s+S}{S}=2\sigma_o2^\frac{s}{S}。由此可見,相鄰兩組的同一層尺度爲2倍關係。

1.5 差分高斯金字塔

差分金字塔的是在高斯金字塔的基礎上操作的,其建立過程是:在高斯金子塔中的每組中相鄰兩層相減(下一層減上一層)就生成高斯差分金字塔.高斯差分金字塔其操作如下圖:

在這裏插入圖片描述

我們可以通過高斯差分金字塔圖像看出圖像上的像素值變化情況。(如果沒有變化,也就沒有特徵。特徵必須是變化儘可能多的點。)DOG圖像描繪的是目標的輪廓。

在Lowe的論文中,將第0層的初始尺度定爲1.6,圖片的初始尺度定位0.5,則圖像金字塔第0層的實際尺度爲1.61.60.50.5=1.52\sqrt{1.6*1.6-0.5*0.5}=1.52,在檢測極值點前對原始圖像的高斯平滑以致圖像丟失高頻信息,所以Lowe建議在建立尺度空間前首先對原始圖像長寬擴展一倍,以保留原始圖像信息,增加特徵點數量。當對圖像長寬擴展一倍時,便構建了-1層,該層尺度爲1.61.6(20.5)(20.5)=1.25\sqrt{1.6*1.6-(2*0.5)*(2*0.5)}=1.25

1.6 極值點檢測

關鍵點是由DOG空間的局部極值點組成的,關鍵點的初步探查是通過同一組內各DOG相鄰兩層圖像之間比較完成的。爲了尋找DOG函數的極值點,每一個像素點要和它所有的相鄰點比較,看其是否比它的圖像域和尺度域的相鄰點大或者小。如圖下圖所示,中間的檢測點和它同尺度的8個相鄰點和上下相鄰尺度對應的9×2個點共26個點比較,以確保在尺度空間和二維圖像空間都檢測到極值點。

在這裏插入圖片描述

1.7 關鍵點精確定位

由於DOG值對噪聲和邊緣較敏感,因此,在上面DOG尺度空間中檢測到局部極值點還要經過進一步的檢驗才能精確定位特徵點。爲了提高關鍵點的穩定性,需要對尺度空間DOG函數進行曲線擬合。利用DOG函數在尺度空間的Taylor展開式(插值函數)爲:

任意一極值點在其 X0=(x0,y0,σ0)X 0=(x 0, \quad y 0, \quad \sigma 0) 處泰勒展開並舍掉 2 階以後的項結果如下:f([xyσ])f([x0y0σ0])+[fxfyfσ]([xyσ][x0y0σ0])f\left(\left[\begin{array}{l}x \\ y \\ \sigma\end{array}\right]\right) \approx f\left(\left[\begin{array}{l}x_{0} \\ y_{0} \\ \sigma_{0}\end{array}\right]\right)+\left[\begin{array}{lll}\frac{\partial f}{\partial x} & \frac{\partial f}{\partial y} & \frac{\partial f}{\partial \sigma}\end{array}\right]\left(\left[\begin{array}{l}x \\ y \\ \sigma\end{array}\right]-\left[\begin{array}{l}x_{0} \\ y_{0} \\ \sigma_{0}\end{array}\right]\right)

12([xyσ][x0y0σ0])[2fxx2fxy2fxσ2fxy2fyy2πyσ2fxσ2fyσ2fσσ]([xyσ][x0y0σ0])\left.\frac{1}{2}\left([\begin{array}{ccccccc}x & y & \sigma\end{array}\right]-\left[\begin{array}{ccc}x_{0} & y_{0} & \sigma_{0}\end{array}\right]\right)\left[\begin{array}{ccc}\frac{\partial^{2} f}{\partial x \partial x} & \frac{\partial^{2} f}{\partial x \partial y} & \frac{\partial^{2} f}{\partial x \partial \sigma} \\ \frac{\partial^{2} f}{\partial x \partial y} & \frac{\partial^{2} f}{\partial y \partial y} & \frac{\partial^{2} \pi}{\partial y \partial \sigma} \\ \frac{\partial^{2} f}{\partial x \partial \sigma} & \frac{\partial^{2} f}{\partial y \partial \sigma} & \frac{\partial^{2} f}{\partial \sigma \partial \sigma}\end{array}\right]\left(\left[\begin{array}{l} x \\ y \\ \sigma \end{array}\right]-\left[\begin{array}{l} x_{0} \\ y_{0} \\ \sigma_{0} \end{array}\right]\right)

其中 f 的一階偏導數,二階偏導數,以及二階混合偏導數由下面幾個公式求(h=1) 得:

fx=f(i,j+1)f(i,j1)2h,fy=f(i+1,j)f(i1,j)2h\frac{\partial f}{\partial x}=\frac{f(i, j+1)-f(i, j-1)}{2 h}, \quad \frac{\partial f}{\partial y}=\frac{f(i+1, j)-f(i-1, j)}{2 h}

2fx2=f(i,j+1)+f(i,j1)2f(i,j)h2,2fy2=f(i+1,j)+f(i1,j)2jh2\frac{\partial^{2} f}{\partial x^{2}}=\frac{f(i, j+1)+f(i, j-1)-2 f(i, j)}{h^{2}}, \quad \frac{\partial^{2} f}{\partial y^{2}}=\frac{f(i+1, j)+f(i-1, j)-2 j}{h^{2}}

2fxy=f(i1,j1)+f(i+1,j+1)f(i1,j+1)f(i+1,j1)4h2\frac{\partial^{2} f}{\partial x \partial y}=\frac{f(i-1, j-1)+f(i+1, j+1)-f(i-1, j+1)-f(i+1, j-1)}{4 h^{2}}

上面算式的矩陣表示如下:

D(X)=D+DTXX+12XT2DX2XD(X)=D+\frac{\partial D^{T}}{\partial X} X+\frac{1}{2} X^{T} \frac{\partial^{2} D}{\partial X^{2}} X,其中,X求導並讓方程等於0,可得極值點的偏移量爲X^=2D1X2DX\hat{X}=-\frac{\partial^{2} D^{-1}}{\partial X^{2}} \frac{\partial D}{\partial X},對應極值點,方程的值爲D(X^)=D+12DTXX^D(\hat{X})=D+\frac{1}{2} \frac{\partial D^{T}}{\partial X} \hat{X}

其中, X^\hat{X}代表相對插值中心的偏移量, 當它在任 一維度上的偏移量大於0.5時 (即xxyyσ\sigma),意味着插值中心已經偏移到它的鄰近點上, 所以必須改變當前關鍵點的位置。同時在新的位置上反覆插值直到收斂;也有可能超出所設定的迭代次數或者超出圖像邊界的範圍, 此時這樣的點應該刪除, 在Lowe中進行了5次迭代。另外, 過小的點易受噪聲的於擾而變得不穩定, 所以將 小於某個經驗值(Lowe論文中使用0.030.03,Rob Hess等人實現時使用0.04/S0.04/S)的極值點刪除。同時, 在此過程中獲取特徵點的精確位置(原位置加上擬合的偏移量以及尺度(σ\sigma)。

2. 關鍵點描述

2.1 關鍵點方向匹配

爲了使描述符具有旋轉不變性,需要利用圖像的局部特徵爲給每一個關鍵點分配一個基準方向。使用圖像梯度的方法求取局部結構的穩定方向。

(1)梯度計算

對於在DOG金字塔中檢測出的關鍵點,採集其所在高斯金字塔圖像3σ領域窗口內像素的梯度和方向分佈特徵。梯度的模值和方向如下:

在這裏插入圖片描述

(2)梯度直方圖

  1. 直方圖以每10度方向爲一個柱,共36個柱,柱所代表的的方向爲像素點梯度方向,柱的長短代表了梯度幅值。
  2. 根據Lowe的建議,直方圖1統計半徑採用31.5σ3*1.5*\sigma
  3. 在直方圖統計時每相鄰三個像素點採用高斯加權,模板採用[0.25,0.5,0.25][0.25,0.5,0.25],並連續加權兩次。

在這裏插入圖片描述

(3)特徵點主方向的確定

方向直方圖的峯值則代表了該特徵點處鄰域梯度的方向,以直方圖中最大值作爲該關鍵點的主方向。爲了增強匹配的魯棒性,只保留峯值大於主方向峯值80%的方向作爲該關鍵點的輔方向。因此,對於同一梯度值的多個峯值的關鍵點位置,在相同位置和尺度將會有多個關鍵點被創建但方向不同。僅有15%的關鍵點被賦予多個方向,但可以明顯的提高關鍵點匹配的穩定性。實際編程實現中,就是把該關鍵點複製成多份關鍵點,並將方向值分別賦給這些複製後的關鍵點,並且,離散的梯度方向直方圖要進行插值擬合處理,來求得更精確的方向角度值。

爲了防止某個梯度方向角度因受到噪聲的干擾而突變,我們還需要對梯度方向直方圖進行平滑處理,平滑公式爲:

H(i)=h(i2)+h(i+2)16+4×(h(i1)+h(i+1))16+6×h(i)16H(i)=\frac{h(i-2)+h(i+2)}{16}+\frac{4 \times(h(i-1)+h(i+1))}{16}+\frac{6 \times h(i)}{16}

其中i∈[0,35],hhHH分別表示平滑前和平滑後的直方圖。由於角度是循環的,即0=3600^{\circ}=360^{\circ},如果出現h(j)h(j)j超出了(0,…,35)的範圍,那麼可以通過圓周循環的方法找到它所對應的、在0=3600^{\circ}=360^{\circ}之間的值,如h(-1) = h(35)。

(4)梯度直方圖拋物線插值

在這裏插入圖片描述

假設我們在第i個小柱子要找一個精確的方向,那麼由上面分析知道:設插值拋物線方程爲h(t)=at2bt+ch(t)=at^2-bt+c,其中abca、b、c爲執物線的係數,tt自變量, t[1,1]t\in[-1,1],此拋物線求導並令它等於0。
h(t)=0h(t)'=0tmax=b/(2a)t_max=-b/(2a)。現在把這三個插值點代入方程可得:

h(1)=ab+ch(0)=ch(1)=a+b+c}\left.\begin{array}{l}\mathrm{h}(-1)=\mathrm{a}-\mathrm{b}+\mathrm{c} \\ \mathrm{h}(0)=\mathrm{c} \\ \mathrm{h}(1)=\mathrm{a}+\mathrm{b}+\mathrm{c}\end{array}\right\}——>{a=[h(1)+h(1)]/2h(0)b=[h(1)h(1)]/2c=h(0)\left\{\begin{array}{l}-\mathrm{a}=[\mathrm{h}(1)+\mathrm{h}(-1)] / 2-\mathrm{h}(0) \\ \mathrm{b}=[\mathrm{h}(1)-\mathrm{h}(-1)] / 2 \\ \mathrm{c}=\mathrm{h}(0)\end{array}\right.

由上式知:tmax=b/(2a)=h(1)h(1)2[h(1)+h(1)2h(0)]\mathrm{t}_{\mathrm{max}}=-\mathrm{b} /(2 \mathrm{a})=\frac{h(-1)-h(1)}{2[h(-1)+h(1)-2 h(0)]}(局部座標系中的取值)

i=i+h(i1)h(i+1)2[h(i1)+h(i+1)2h(i)]\mathbf{i}^{\prime}=\mathbf{i}+\frac{h(i-1)-h(i+1)}{2[h(i-1)+h(i+1)-2 h(i)]}(小柱子在直方圖中的索引號)。

圖像的關鍵點已檢測完畢,每個關鍵點有三個信息:位置、尺度、方向;同時也就使關鍵點具備平移、縮放、旋轉不變性。

2.2 生成描述符

(1)確定計算描述子所需的區域

描述子梯度方向直方圖由關鍵點所在尺度的模糊圖像計算產生。圖像區域的半徑通過下式計算:

radius=3σoct×2×(d+1)+12=\frac{3 \sigma_{\text {oct}} \times \sqrt{2} \times(d+1)+1}{2}σoct\sigma_{oct}是關鍵點所在組(octave)的組內尺度,d=4d=4

在這裏插入圖片描述

(2)將座標移至關鍵點主方向

在這裏插入圖片描述

旋轉角度後新座標:(x^y^)=(cosθsinθsinθcosθ)×(xy)\left(\begin{array}{c}\hat{x} \\ \hat{y}\end{array}\right)=\left(\begin{array}{cc}\cos \theta & -\sin \theta \\ \sin \theta & \cos \theta\end{array}\right) \times\left(\begin{array}{l}x \\ y\end{array}\right)

(3)梯度直方圖的生成

在窗口寬度爲2X2的區域內計算8個方向的梯度方向直方圖,繪製每個梯度方向的累加值,即可形成一個種子點。然後再在下一個2X2的區域內進行直方圖統計,形成下一個種子點,共生成16個種子點。

在這裏插入圖片描述

(4)三線性插值

插值計算每個種子點八個方向的梯度。

在這裏插入圖片描述

採樣點在子區域中的下標(x,y)(x'',y'')(圖中藍色窗口內紅色點)線性插值,計算其對每個種子點的貢獻。如圖中的紅色點,落在第0行和第1行之間,對這兩行都有貢獻。對第0行第3列種子點的貢獻因子爲drdr,對第1行第3列的貢獻因子爲1dr1-dr,同理,對鄰近兩列的貢獻因子爲dcdc1dc1-dc,對鄰近兩個方向的貢獻因子爲dodo1do1-do。則最終累加在每個方向上的梯度大小爲:weight=wdrk(1dr)1kdcm(1dc)1mdon(1dO)1nweight=w*d r^{k}*(1-d r)^{1-k} * d c^{m}*(1-d c)^{1-m} * d o^{n} *(1-d O)^{1-n}。其中k,m,n爲0(像素點超出了對要插值區間的四個鄰近子區間所在範圍)或爲1(像素點處在對要插值區間的四個鄰近子區間之一所在範圍)。

(5)描述子生成過程

在這裏插入圖片描述

2.3 關鍵點匹配

分別對模板圖和實時圖建立關鍵點描述子集合。目標的識別是通過兩點集內關鍵點描述子的對比來完成。具有128維的關鍵點描述子的相似性度量採樣歐氏距離。

模板圖中關鍵點描述子:Ri=(ri1,ri2,,ri128)R_{i}=\left(r_{i 1}, r_{i 2}, \cdots, r_{i 128}\right)

實時圖中關鍵點描述子:Si=(si1,si2,,si128)S_{i}=\left(s_{i 1}, s_{i 2}, \cdots, s_{i 128}\right)

任意兩描述子相似性度量:d(Ri,Si)=j=1128(rijsij)2d(R_i,S_i)=\sqrt{\sum\limits_{j=1}^{128}(r_{ij}-s_{ij})^2}

要得到配對的關鍵點描述子需滿足:RiSjRiSp<Threshold\frac{實時圖中距離R_i最近的點S_j}{實時圖中距離R_i的次最近點S_p}<Threshold

單應矩陣(Homography)

有了兩組相關點,接下來就需要建立兩組點的轉換關係,也就是圖像變換關係。單應性是兩個空間之間的映射,常用於表示同一場景的兩個圖像之間的對應關係,可以匹配大部分相關的特徵點,並且能實現圖像投影,使一張圖通過投影和另一張圖實現大面積的重合。

用RANSAC方法估算H:

  1. 首先檢測兩邊圖像的角點
  2. 在角點之間應用方差歸一化相關,收集相關性足夠高的對,形成一組候選匹配。
  3. 選擇四個點,計算H
  4. 選擇與單應性一致的配對。如果對於某些閾值:Dist(Hp、q) <ε,則點對(p, q)被認爲與單應性H一致
  5. 重複34步,直到足夠多的點對滿足H
  6. 使用所有滿足條件的點對,通過公式重新計算H

RANSAC(Random Sample Consensus,隨機抽樣一致)是一種魯棒性的參數估計方法。它的實質就是一個反覆測試、不斷迭代的過程。

基本思想:首先根據具體問題設計出某個目標函數,然後通過反覆提取最小點集估計該函數中參數的初始值,利用這些初始值把所有的數據分爲“內點”和“外點”,最後用所有的內點重新計算和估計函數的參數。

在這裏插入圖片描述

圖像變形和融合

(1)圖像變形

  1. 首先計算每個輸入圖像的變形圖像座標範圍,得到輸出圖像大小,可以很容易地通過映射每個源圖像的四個角並且計算座標(x,y)的最小值和最大值確定輸出圖像的大小。最後,需要計算指定參考圖像原點相對於輸出全景圖的偏移量的偏移量x_offset和偏移量y_offset。
  2. 下一步是使用上面所述的反向變形,將每個輸入圖像的像素映射到參考圖像定義的平面上,分別執行點的正向變形和反向變形。

(2)圖像融合

最後一步是在重疊區域融合像素顏色,以避免接縫。最簡單的可用形式是使用羽化(feathering),它使用加權平均顏色值融合重疊的像素。我們通常使用alpha因子,通常稱爲alpha通道,它在中心像素處的值爲1,在與邊界像素線性遞減後變爲0。當輸出拼接圖像中至少有兩幅重疊圖像時,我們將使用如下的alpha值來計算其中一個像素處的顏色:假設兩個圖像I1,I2I_1,I_2在輸出圖像中重疊;每個像素點(x,y)(x,y)在圖像Ii(x,y)=(αiR,αiG,αiB,αj)I_i(x,y)=(\alpha_iR,\alpha_iG,\alpha_iB,\alpha_j),其中(R,G,B)(R,G,B)是每個通道像素值,我們將在縫合後的輸出圖像中計算(x,y)(x,y)的像素值:

[(α1R,α1G,α1B,α1)+(α2R,α2G,α2B,α2)]/(α1+α2)[(\alpha_1R,\alpha_1G,\alpha_1B,\alpha_1)+(\alpha_2R,\alpha_2G,\alpha_2B,\alpha_2)]/(\alpha_1+\alpha_2)

3 代碼實現

#################
#Author:Tian YJ#
#圖像拼接實現全景圖#
#################

# 導入基本庫文件
import numpy as np 
from numpy import *
from numpy.linalg import det, lstsq, norm # 線性代數模塊
import cv2
import matplotlib.pyplot as plt
from functools import cmp_to_key # 接受兩個參數,將兩個參數做處理

# 加上這兩行可以一次性輸出多個變量而不用print
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
# 設置容忍度
float_tolerance = 1e-7

%matplotlib inline 
##################
#設置路徑、讀取圖片#
##################
# 設置路徑
path = 'C:\\Users\\86187\\Desktop\\image\\'
# 讀取待拼接圖片(灰度圖)
# img1爲左圖,img2位右圖
img1 = cv2.imread(path + 'left.jpg', 0)
img2 = cv2.imread(path + 'right.jpg', 0)

# 原始圖片展示
plt.figure(figsize=(25,10)) 
plt.subplot(1,2,1)
plt.imshow(img1.astype(np.uint8), cmap="gray")
plt.subplot(1,2,2)
plt.imshow(img2.astype(np.uint8), cmap="gray")
plt.show()

在這裏插入圖片描述

#############################
#對圖像進行倍數放大(雙線性插值)#
#############################
def resize(img, ratio=2.):
    """
    img: 待處理圖片
    ratio: 放大倍數
    """
    # 目標圖像尺寸
    new_shape = [int(img.shape[0] * ratio), int(img.shape[1] * ratio)]
    result = np.zeros((new_shape))  # 目標圖像初始化
    # 遍歷新的圖像座標
    for h in range(new_shape[0]):
        for w in range(new_shape[1]):
            # 對應的原圖像上的點(向下取整,也就是左上點的位置)
            h0 = int(np.floor(h / ratio))
            w0 = int(np.floor(w / ratio))
            # 新圖像的座標/放縮比例 - 原圖像座標點 = 距離
            dx = h / ratio - h0
            dy = w / ratio - w0
            # 防止溢出
            h1 = h0 + 1 if h0 < img.shape[0] - 1 else h0
            w1 = w0 + 1 if w0 < img.shape[1] - 1 else w0
            # 進行插值計算
            result[h, w] = (1 - dx) * (1 - dy) * img[h0, w0] + dx * (
                1 - dy) * img[h1, w0] + (
                    1 - dx) * dy * img[h0, w1] + dx * dy * img[h1, w1]
    result = result.astype(np.uint8)
    return result
##################
#對圖像進行邊緣填充#
##################
def padding(img):
    """
    img: 待處理圖片
    """
   
    # 獲取圖片尺寸
    H, W = img.shape
    pad = 2 # 填充尺寸

    # 先填充行
    rows = np.zeros((pad, W), dtype=np.uint8)
    # 再填充列
    cols = np.zeros((H + 2 * pad, pad), dtype=np.uint8)
    # 進行拼接
    img = np.vstack((rows, img, rows))  # 上下拼接
    img = np.hstack((cols, img, cols))  # 左右拼接

    # 進行鏡像padding,我第一次padding零,出現黑邊,邊緣失真嚴重
    # 第一步,上下邊框對稱取值
    img[0, :] = img[2, :]
    img[-1, :] = img[-3, :]
    # 第二步,左右邊框對稱取值
    img[:, 0] = img[:, 2]
    img[:, -1] = img[:, -3]
    # 第三步,四個頂點對稱
    img[0, 0] = img[0, 2]
    img[-1, 0] = img[-1, 2]
    img[0, -1] = img[0, -3]
    img[-1, -1] = img[-1, -3]

    return img
##############
#設置濾波器係數#
##############
def Kernel(K_sigma, K_size):
    """
    K_sigma: 模糊度
    K_size: 濾波器即卷積核尺寸
    """
   
    # 對濾波器進行初始化0
    pad = K_size // 2
    K = np.zeros((K_size, K_size), dtype=np.float)

    # 代入公式求高斯濾波器係數,並填入矩陣
    for x in range(-pad, -pad + K_size):
        for y in range(-pad, -pad + K_size):
            K[y + pad, x + pad] = np.exp(-(x**2 + y**2) / (2 * (K_sigma**2)))

    K /= K.sum()  # 進行歸一化

    return K
#############
#進行高斯濾波#
#############
def gaussFilter(img, K_size=5, K_sigma=1.6):
    """
    img: 需要處理圖像
    K_size: 濾波器尺寸
    K_sigma: 模糊度
    """

    # 獲取圖片尺寸
    pad = K_size // 2
    H, W = img.shape

    ## 對圖片進行padding
    img = padding(img)

    # 濾波器矩陣
    K = Kernel(K_sigma, K_size)

    ## 進行濾波
    out = img.copy()
    for h in range(H):
        for w in range(W):
            out[pad + h, pad + w] = np.sum(K * out[h:h + K_size, w:w + K_size])
    # 截取像素合理值
    out = out / out.max() * 255
    out = out[pad:pad + H, pad:pad + W].astype(np.uint8)
    return out
##################
#生成金字塔基礎圖像#
##################
def generateBaseImage(image, sigma, assumed_blur):
    """
    將輸入圖像放大一倍並應用高斯模糊,以生成圖像金字塔的基礎圖像
    image: 待處理圖片
    sigma: 目標模糊度
    assumed_blur: 假設模糊度
    """
    # 進行2倍放大
    image = resize(image, ratio=2.0)
    # 對圖像應用多個連續的高斯模糊效果與應用單個較大的高斯模糊效果相同
    sigma_diff = np.sqrt(max((sigma**2) - ((2 * assumed_blur)**2), 0.01))

    return gaussFilter(image, K_size=5, K_sigma=sigma_diff)
# 嘗試一下
base_image = generateBaseImage(img1, 1.6, 0.5)
# cv2.imshow('result', base_image)
# cv2.imshow('begin', img1)
# cv2.waitKey(0)
####################################
#計算可以將圖像重複減半直到變得很小的次數#
####################################
def computeNumberOfOctaves(image_shape):
    """
    image_shape: 圖像尺寸
    """
    return int(round(np.log(min(image_shape)) / np.log(2) - 1))
####################################
#爲特定圖層中的每個圖像創建一個模糊度列表#
####################################
def generateGaussianKernels(sigma, num_intervals):
    """
    sigma: 模糊度
    num_intervals: 能進行極值點檢測的圖層數
    高斯金字塔每組有num_intervals+1+2層
    """
    # 高斯金字塔每組層數
    num_images_per_octave = num_intervals + 3
    # 高斯模糊度係數
    k = 2**(1. / num_intervals)
    # 高斯模糊度列表初始化爲0
    gaussian_kernels = np.zeros(num_images_per_octave)
    # 第一個高斯模糊度
    gaussian_kernels[0] = sigma

    # 第0層在升採樣時已進行高斯模糊,故從第1層開始
    for image_index in range(1, num_images_per_octave):
        sigma_previous = (k**(image_index - 1)) * sigma
        sigma_total = k * sigma_previous
        gaussian_kernels[image_index] = np.sqrt(sigma_total**2 -
                                             sigma_previous**2)
    return gaussian_kernels
#####################
#生成尺度空間高斯金字塔#
#####################
def generateGaussianImages(image, num_octaves, gaussian_kernels):
    """
    image: 輸入基圖像
    num_octaves: 尺度金字塔組數
    gaussian_kernels: 每一組的高斯模糊度列表
    """
    # 總的高斯金字塔列表
    gaussian_images = []

    for octave_index in range(num_octaves):
        # 每一組的高斯金字塔列表
        gaussian_images_in_octave = []
        gaussian_images_in_octave.append(image)  # 第一個圖像已經濾波
        for gaussian_kernel in gaussian_kernels[1:]:
            # 進行高斯濾波
            image = gaussFilter(image, K_size=5, K_sigma=gaussian_kernel)
            gaussian_images_in_octave.append(image)
        gaussian_images.append(gaussian_images_in_octave)
        # 將上一組的倒數第三層作爲下一組的基圖像
        octave_base = gaussian_images_in_octave[-3]  # 倒數第三層
        image = octave_base[::2, ::2]  # 下采樣
    return array(gaussian_images)
# 打印高斯模糊度列表
gaussian_kernels = generateGaussianKernels(1.6, 3)
print(gaussian_kernels)
# 顯示高斯金字塔圖像
gaussian_images = generateGaussianImages(base_image, 8, gaussian_kernels)

for k in range(len(gaussian_images)):
    plt.figure(figsize=(25, 10))
    for i in range(len(gaussian_images[k])):
        plt.subplot(1, len(gaussian_images[k]), i + 1)
        plt.imshow(gaussian_images[k][i].astype(np.uint8), cmap="gray")
plt.show()

[1.6 1.2262735 1.54500779 1.94658784 2.452547 3.09001559]
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

#################
#生成高斯差分金字塔#
#################
def generateDoGImages(gaussian_images):
    """
    gaussian_images: 傳入高斯金字塔組
    """
    # 總的差分金字塔列表
    dog_images = []

    for gaussian_images_in_octave in gaussian_images:
        # 每一組高斯差分金字塔列表
        dog_images_in_octave = []
        # 兩兩進行差分運算
        for first_image, second_image in zip(gaussian_images_in_octave,
                                             gaussian_images_in_octave[1:]):
            dog_images_in_octave.append(cv2.subtract(
                second_image, first_image))  # 普通的減法不行,因爲圖像是無符號整數
        dog_images.append(dog_images_in_octave)
    return array(dog_images)
# 顯示差分金字塔圖像
dog_images = generateDoGImages(gaussian_images)
for k in range(len(dog_images)):
    plt.figure(figsize=(25, 10))
    for i in range(len(dog_images[k])):
        plt.subplot(1, len(dog_images[k]), i + 1)
        plt.imshow(dog_images[k][i].astype(np.uint8), cmap="gray")
plt.show()

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

####################
#查找極值點的像素位置#
####################
def findScaleSpaceExtrema(gaussian_images,
                          dog_images,
                          num_intervals,
                          sigma,
                          image_border_width,
                          contrast_threshold=0.04):
    """
    gaussian_images: 高斯金字塔組
    dog_images: 差分金字塔組
    num_intervals:每一組極值點檢測層數
    sigma:模糊度
    image_border_width:靠近圖像邊緣5個像素的區域不做檢測
    contrast_threshold:對比度閾值
    """
    # 閾值化,不保留低於閾值的不穩定點
    # abs(val)  > 0.5*T/n
    threshold = np.floor(0.5 * contrast_threshold / num_intervals * 255)
    # 關鍵點列表
    keypoints = []
    # 遍歷DoG金字塔
    for octave_index, dog_images_in_octave in enumerate(dog_images):
        # dog_images_in_octave是一個列表,每一個包含5張圖片
        # dog_images_in_octave[1:],包含4張圖片
        # dog_images_in_octave[2:],包含3張圖片
        for image_index, (first_image, second_image, third_image) in enumerate(
                zip(dog_images_in_octave, dog_images_in_octave[1:],
                    dog_images_in_octave[2:])):
            # 這裏(0,1,2)、(1,2,3)、(2,3,4) 每3張圖片分別是一組
            # (i, j) 是3x3矩陣的中心
            # 靠近圖像邊緣5個像素的區域不做檢測,image_border_width=5
            for i in range(image_border_width,
                           first_image.shape[0] - image_border_width):
                for j in range(image_border_width,
                               first_image.shape[1] - image_border_width):
                    ## 調用函數判別極值
                    if isPixelAnExtremum(
                            first_image[i - 1:i + 2, j - 1:j + 2],
                            second_image[i - 1:i + 2, j - 1:j + 2],
                            third_image[i - 1:i + 2, j - 1:j + 2], threshold):
                        ## 調用函數定位極值點(精確定位)
                        localization_result = localizeExtremumViaQuadraticFit(
                            i, j, image_index + 1, octave_index, num_intervals,
                            dog_images_in_octave, sigma, contrast_threshold,
                            image_border_width)
                        if localization_result is not None:
                            keypoint, localized_image_index = localization_result
                            # 計算關鍵點方向
                            keypoints_with_orientations = computeKeypointsWithOrientations(
                                keypoint, octave_index,
                                gaussian_images[octave_index]
                                [localized_image_index])
                            for keypoint_with_orientation in keypoints_with_orientations:
                                keypoints.append(keypoint_with_orientation)
    return keypoints
#############
#進行極值判別#
#############
def isPixelAnExtremum(first_subimage, second_subimage, third_subimage,
                      threshold):
    """
    first_subimage:第一張圖片
    second_subimage:第二張圖片
    third_subimage:第三張圖片
    threshold:閾值
    滿足條件返回True,不滿足條件返回False
    """
    center_pixel_value = second_subimage[1, 1]  # 中心像素爲第二層中間者
    # 小於閾值的極值點刪除
    if abs(center_pixel_value) > threshold:
        if center_pixel_value > 0:
            # 正值情況
            # 分別與上一層9個、下一層9個和本層8個像素進行比較
            return all(center_pixel_value >= first_subimage) and \
                   all(center_pixel_value >= third_subimage) and \
                   all(center_pixel_value >= second_subimage[0, :]) and \
                   all(center_pixel_value >= second_subimage[2, :]) and \
                   center_pixel_value >= second_subimage[1, 0] and \
                   center_pixel_value >= second_subimage[1, 2]
        elif center_pixel_value < 0:
            # 負值情況
            # 分別於上一層9個、一層9個和本層8個像素進行比較
            return all(center_pixel_value <= first_subimage) and \
                   all(center_pixel_value <= third_subimage) and \
                   all(center_pixel_value <= second_subimage[0, :]) and \
                   all(center_pixel_value <= second_subimage[2, :]) and \
                   center_pixel_value <= second_subimage[1, 0] and \
                   center_pixel_value <= second_subimage[1, 2]
    return False
#####################
#二次擬合精確定位極值點#
#####################
def localizeExtremumViaQuadraticFit(i,
                                    j,
                                    image_index,
                                    octave_index,
                                    num_intervals,
                                    dog_images_in_octave,
                                    sigma,
                                    contrast_threshold,
                                    image_border_width,
                                    eigenvalue_ratio=10,
                                    num_attempts_until_convergence=5):
    """
    i,j:中心像素點原座標
    image_index:每一octave種的圖像索引
    octave_index:差分金字塔octave索引
    num_intervals:每一組極值點檢測層數
    dog_images_in_octave:高斯差分金字塔組,每一組4張圖片
    sigma:高斯模糊度
    contrast_threshold:對比度閾值
    image_border_width:圖像邊界5像素不檢測
    eigenvalue_ratio:主曲率閾值
    num_attempts_until_convergence:最大嘗試次數
    """
    extremum_is_outside_image = False
    # 獲取每一octave第一層圖像尺寸
    image_shape = dog_images_in_octave[0].shape
    # 最大嘗試次數設爲5
    for attempt_index in range(num_attempts_until_convergence):
        first_image, second_image, third_image = dog_images_in_octave[
            image_index - 1:image_index + 2]
        # 縱向拼接形成三維數組
        pixel_cube = np.stack([
            first_image[i - 1:i + 2, j - 1:j + 2],
            second_image[i - 1:i + 2, j - 1:j + 2], third_image[i - 1:i + 2,
                                                                j - 1:j + 2]
        ]).astype('float32') / 255.
        # 需要從uint8轉換爲float32以計算導數,並且需要將像素值重新縮放爲[0,1]以應用閾值

        # 計算梯度
        gradient = computeGradientAtCenterPixel(pixel_cube)
        # 計算海森陣
        hessian = computeHessianAtCenterPixel(pixel_cube)
        # 最小二乘擬合
        extremum_update = -lstsq(hessian, gradient, rcond=None)[0]
        # 如果當前偏移量絕對值中的每個值均小於0.5,放棄迭代
        if abs(extremum_update[0]) < 0.5 and abs(
                extremum_update[1]) < 0.5 and abs(extremum_update[2]) < 0.5:
            break
        # 更新中心點座標,即極值點重定位
        j += int(round(extremum_update[0]))
        i += int(round(extremum_update[1]))
        image_index += int(round(extremum_update[2]))
        # 確保新的pixel_cube將完全位於圖像中
        if i < image_border_width or i >= image_shape[
                0] - image_border_width or j < image_border_width or j >= image_shape[
                    1] - image_border_width or image_index < 1 or image_index > num_intervals:
            extremum_is_outside_image = True
            break
    if extremum_is_outside_image:
        # 更新的極值在達到收斂之前移出圖像
        return None
    if attempt_index >= num_attempts_until_convergence - 1:
        # 超過最大嘗試次數,但未達到此極值的收斂。
        return None
    functionValueAtUpdatedExtremum = pixel_cube[1, 1, 1] + 0.5 * np.dot(
        gradient, extremum_update)
    if abs(functionValueAtUpdatedExtremum
           ) * num_intervals >= contrast_threshold:
        xy_hessian = hessian[:2, :2]
        # trace求取xy_hessian的對角元素和
        xy_hessian_trace = trace(xy_hessian)
        # det爲求xy_hessian的行列式值
        xy_hessian_det = det(xy_hessian)
        # 檢測主曲率是否在域值eigenvalue_ratio下
        if xy_hessian_det > 0 and eigenvalue_ratio * (xy_hessian_trace**2) < (
            (eigenvalue_ratio + 1)**2) * xy_hessian_det:
            # 返回KeyPoint對象,
            keypoint = cv2.KeyPoint()
            # 關鍵點的點座標
            keypoint.pt = ((j + extremum_update[0]) * (2**octave_index),
                           (i + extremum_update[1]) * (2**octave_index))
            # 從哪一層金字塔得到的此關鍵點
            keypoint.octave = octave_index + image_index * (2**8) + int(
                round((extremum_update[2] + 0.5) * 255)) * (2**16)
            # 關鍵點鄰域直徑大小
            keypoint.size = sigma * (2**(
                (image_index + extremum_update[2]) / np.float32(num_intervals)
            )) * (2**(octave_index + 1))  # octave_index + 1,因爲輸入的圖像是原來的兩倍
            # 響應程度,代表該點的強壯程度,也就是該點角點程度
            keypoint.response = abs(functionValueAtUpdatedExtremum)
            return keypoint, image_index
    return None
##############
#近似求離散梯度#
##############
def computeGradientAtCenterPixel(pixel_array):
    """
    pixel_array:3層3x3的像素區域,進行極值比較
    """
    # 對於步長h,f'(x)的中心差分公式爲(f(x + h)-f(x-h))/(2 * h)
    # 此處h = 1,因此公式簡化爲f'(x)=(f(x + 1)-f(x-1))/ 2

    # x對應於第二個數組軸,y對應於第一個數組軸,s(尺度)對應於第三個數組軸

    dx = 0.5 * (pixel_array[1, 1, 2] - pixel_array[1, 1, 0])
    dy = 0.5 * (pixel_array[1, 2, 1] - pixel_array[1, 0, 1])
    ds = 0.5 * (pixel_array[2, 1, 1] - pixel_array[0, 1, 1])  # 跨層差分
    return np.array([dx, dy, ds])
#############
#近似求海森陣#
#############
def computeHessianAtCenterPixel(pixel_array):
    """
    """
    # 步長爲h時,f"(x)的中心差分公式爲(f(x+h)-2*f(x)+f(x-h))/(h^2)
    # 這裏h= 1,公式化簡爲f"(x)=f(x+1)-2*f(x)+f(x-1)
    
    # 步長爲h時,(d^2)f(x,y)/(dxdy)的中心差分公式爲:
    # (f(x+h,y+h)-f(x+h,y-h)-f(x-h,y+h)+ f(x-h,y-h))/(4*h^2)
    # 在這裏h = 1,因此公式簡化爲:
    # (d^2)f(x,y)/(dx dy)=(f(x+1,y+1)-f(x+1,y-1)-f(x-1,y+1)+f(x-1,y-1))/4
    
    # x對應於第二個數組軸,y對應於第一個數組軸,s(尺度)對應於第三個數組軸
    center_pixel_value = pixel_array[1, 1, 1] # 中心像素值
    dxx = pixel_array[1, 1, 2] - 2 * center_pixel_value + pixel_array[1, 1, 0]
    dyy = pixel_array[1, 2, 1] - 2 * center_pixel_value + pixel_array[1, 0, 1]
    dss = pixel_array[2, 1, 1] - 2 * center_pixel_value + pixel_array[0, 1, 1]
    dxy = 0.25 * (pixel_array[1, 2, 2] - pixel_array[1, 2, 0] -
                  pixel_array[1, 0, 2] + pixel_array[1, 0, 0])
    dxs = 0.25 * (pixel_array[2, 1, 2] - pixel_array[2, 1, 0] -
                  pixel_array[0, 1, 2] + pixel_array[0, 1, 0])
    dys = 0.25 * (pixel_array[2, 2, 1] - pixel_array[2, 0, 1] -
                  pixel_array[0, 2, 1] + pixel_array[0, 0, 1])
    return np.array([[dxx, dxy, dxs], [dxy, dyy, dys], [dxs, dys, dss]])
###############################
##########計算關鍵點方向#########
#爲關鍵點附近的像素創建漸變的直方圖#
###############################
def computeKeypointsWithOrientations(keypoint,
                                     octave_index,
                                     gaussian_image,
                                     radius_factor=3,
                                     num_bins=36,
                                     peak_ratio=0.8,
                                     scale_factor=1.5):
    """
    keypoint:檢測到精確並定位的關鍵點
    octave_index:差分金字塔octave索引
    gaussian_image:高斯金字塔組
    radius_factor:半徑因子
    num_bins:直方圖柱數,沒0度一柱
    peak_ratio:只保留峯值大於主方向峯值80%的方向作爲該關鍵點的輔方向
    scale_factor:尺度因子
    """
    keypoints_with_orientations = []
    image_shape = gaussian_image.shape

    # scale = 1.5*sigma
    scale = scale_factor * keypoint.size / np.float32(2**(octave_index + 1))
    # 直方圖統計半徑採用3*1.5*sigma
    radius = int(round(radius_factor * scale))
    # 權重因子
    weight_factor = -0.5 / (scale**2)
    # 梯度直方圖將0~360度的方向範圍分爲36個柱(bins),其中每柱10度
    # num_bins=36
    raw_histogram = np.zeros(num_bins)
    # 高斯平滑直方圖
    smooth_histogram = np.zeros(num_bins)

    # 採集其所在高斯金字塔圖像3σ領域窗口內像素的梯度和方向分佈特徵
    for i in range(-radius, radius + 1):
        region_y = int(round(keypoint.pt[1] / np.float32(2**octave_index))) + i
        if region_y > 0 and region_y < image_shape[0] - 1:
            for j in range(-radius, radius + 1):
                region_x = int(
                    round(keypoint.pt[0] / np.float32(2**octave_index))) + j
                if region_x > 0 and region_x < image_shape[1] - 1:
                    # 差分求偏導,這裏省略了1/2的係數
                    dx = gaussian_image[region_y, region_x +
                                        1] - gaussian_image[region_y,
                                                            region_x - 1]
                    dy = gaussian_image[region_y - 1,
                                        region_x] - gaussian_image[region_y +
                                                                   1, region_x]
                    # 梯度模值
                    gradient_magnitude = np.sqrt(dx * dx + dy * dy)
                    # 梯度方向
                    gradient_orientation = np.rad2deg(np.arctan2(dy, dx))
                    weight = np.exp(weight_factor * (i**2 + j**2))
                    # 梯度幅值需先乘以高斯權重再累加到直方圖中去
                    histogram_index = int(
                        round(gradient_orientation * num_bins / 360.))
                    raw_histogram[histogram_index %
                                  num_bins] += weight * gradient_magnitude

    for n in range(num_bins):
        # 使用平滑公式
        smooth_histogram[n] = (
            6 * raw_histogram[n] + 4 *
            (raw_histogram[n - 1] + raw_histogram[(n + 1) % num_bins]) +
            raw_histogram[n - 2] + raw_histogram[(n + 2) % num_bins]) / 16.
    orientation_max = max(smooth_histogram)
    # 找出主方向
    orientation_peaks = where(
        np.logical_and(smooth_histogram > roll(smooth_histogram, 1),
                       smooth_histogram > roll(smooth_histogram, -1)))[0]
    for peak_index in orientation_peaks:
        peak_value = smooth_histogram[peak_index]
        # 輔方向,閾值爲80%
        if peak_value >= peak_ratio * orientation_max:
            left_value = smooth_histogram[(peak_index - 1) % num_bins]
            right_value = smooth_histogram[(peak_index + 1) % num_bins]
            # 梯度直方圖拋物線插值
            interpolated_peak_index = (
                peak_index + 0.5 * (left_value - right_value) /
                (left_value - 2 * peak_value + right_value)) % num_bins
            orientation = 360. - interpolated_peak_index * 360. / num_bins
            if abs(orientation - 360.) < float_tolerance:
                orientation = 0
            new_keypoint = cv2.KeyPoint(*keypoint.pt, keypoint.size,
                                        orientation, keypoint.response,
                                        keypoint.octave)
            keypoints_with_orientations.append(new_keypoint)
    return keypoints_with_orientations
################
#對關鍵點進行比較#
################
def compareKeypoints(keypoint1, keypoint2):
    """
    keypoint1、keypoint2:需要比較的兩個關鍵點
    """
    # 關鍵點的點座標
    if keypoint1.pt[0] != keypoint2.pt[0]:
        return keypoint1.pt[0] - keypoint2.pt[0]
    if keypoint1.pt[1] != keypoint2.pt[1]:
        return keypoint1.pt[1] - keypoint2.pt[1]
    # 關鍵點鄰域直徑大小
    if keypoint1.size != keypoint2.size:
        return keypoint2.size - keypoint1.size
    # 角度,表示關鍵點的方向,值爲[零,三百六十),負值表示不使用
    if keypoint1.angle != keypoint2.angle:
        return keypoint1.angle - keypoint2.angle
    # 響應強度
    if keypoint1.response != keypoint2.response:
        return keypoint2.response - keypoint1.response
    # 從哪一層金字塔得到的此關鍵點
    if keypoint1.octave != keypoint2.octave:
        return keypoint2.octave - keypoint1.octave
    return keypoint2.class_id - keypoint1.class_id
################
#排序並刪除重複項#
################
def removeDuplicateKeypoints(keypoints):
    """
    keypoints:關鍵點
    """
    if len(keypoints) < 2:
        return keypoints
    # 進行排序
    keypoints.sort(key=cmp_to_key(compareKeypoints))
    unique_keypoints = [keypoints[0]]
    # 刪除重複值
    for next_keypoint in keypoints[1:]:
        last_unique_keypoint = unique_keypoints[-1]
        if last_unique_keypoint.pt[0] != next_keypoint.pt[0] or \
           last_unique_keypoint.pt[1] != next_keypoint.pt[1] or \
           last_unique_keypoint.size != next_keypoint.size or \
           last_unique_keypoint.angle != next_keypoint.angle:
            unique_keypoints.append(next_keypoint)
    return unique_keypoints
####################################
#將關鍵點從基本圖像座標轉換爲輸入圖像座標#
####################################
def convertKeypointsToInputImageSize(keypoints):
    """
    keypoints:關鍵點
    """
    converted_keypoints = []
    for keypoint in keypoints:
        keypoint.pt = tuple(0.5 * np.array(keypoint.pt))
        keypoint.size *= 0.5
        keypoint.octave = (keypoint.octave & ~255) | (
            (keypoint.octave - 1) & 255)
        converted_keypoints.append(keypoint)
    return converted_keypoints
#############
#“解壓”關鍵點#
############
def unpackOctave(keypoint):
    """
    計算每一個關鍵點的octave、layer和scale
    """
    octave = keypoint.octave & 255
    layer = (keypoint.octave >> 8) & 255
    if octave >= 128:
        octave = octave | -128
    scale = 1 / np.float32(1 << octave) if octave >= 0 else np.float32(
        1 << -octave)
    return octave, layer, scale
####################
#爲每個關鍵點生成描述符#
####################
def generateDescriptors(keypoints,
                        gaussian_images,
                        window_width=4,
                        num_bins=8,
                        scale_multiplier=3,
                        descriptor_max_value=0.2):
    """
    keypoints:關鍵點
    gaussian_images:高斯金字塔圖像
    window_width:關鍵點附近的區域長爲4,4X4個子區域
    num_bins:8個方向的梯度直方圖
    scale_multiplier:
    descriptor_max_value:
    """
    descriptors = []

    for keypoint in keypoints:
        # 進行“解壓縮”
        octave, layer, scale = unpackOctave(keypoint)
        # 關鍵點所對應的高斯金字塔圖像
        gaussian_image = gaussian_images[octave + 1, layer]
        # 該圖像的尺寸
        num_rows, num_cols = gaussian_image.shape
        # 定位
        point = np.round(scale * np.array(keypoint.pt)).astype('int')
        # 爲方便後面計算的變量
        bins_per_degree = num_bins / 360.
        # 爲方便後面旋轉
        angle = 360. - keypoint.angle
        cos_angle = np.cos(deg2rad(angle))  # 角度轉弧度
        sin_angle = np.sin(deg2rad(angle))  # 角度轉弧度
        # Lowe 建議子區域的像素的梯度大小按0.5d的高斯加權計算
        weight_multiplier = -0.5 / ((0.5 * window_width)**2)
        row_bin_list = []
        col_bin_list = []
        magnitude_list = []
        orientation_bin_list = []
        histogram_tensor = np.zeros(
            (window_width + 2, window_width + 2, num_bins))  # 前兩個維度增加2
        # 把3sigma長度作爲一個單元長度
        hist_width = scale_multiplier * 0.5 * scale * keypoint.size
        # 實際計算所需的圖像區域半徑(根據公式)
        # 說明一下,這裏就是一個大圓外套一個正方形
        half_width = int(
            np.round(hist_width * np.sqrt(2) * (window_width + 1) *
                     0.5))  # sqrt(2)對應於像素的對角線長度
        # 最終區域長度
        half_width = int(min(half_width, sqrt(num_rows**2 + num_cols**2)))

        # 座標軸旋轉至主方向
        for row in range(-half_width, half_width + 1):
            for col in range(-half_width, half_width + 1):
                row_rot = col * sin_angle + row * cos_angle  # 旋轉後的特徵點座標
                col_rot = col * cos_angle - row * sin_angle  # 旋轉後的特徵點座標
                # 計算旋轉後的特徵點落在子區域的下標
                # 座標歸一化
                # +(d/2)是把座標系由特徵點處平移至左上角的邊界點
                # -0.5則是回移座標系至描述子區間中的第一個子區間的中心處
                row_bin = (row_rot / hist_width) + 0.5 * window_width - 0.5
                col_bin = (col_rot / hist_width) + 0.5 * window_width - 0.5
               
                if row_bin > -1 and row_bin < window_width and col_bin > -1 and col_bin < window_width:
                    window_row = int(np.round(point[1] + row))
                    window_col = int(np.round(point[0] + col))
                    if window_row > 0 and window_row < num_rows - 1 and window_col > 0 and window_col < num_cols - 1:
                        # 求偏導
                        dx = gaussian_image[window_row, window_col +
                                            1] - gaussian_image[window_row,
                                                                window_col - 1]
                        dy = gaussian_image[window_row - 1,
                                            window_col] - gaussian_image[
                                                window_row + 1, window_col]
                        # 模值
                        gradient_magnitude = np.sqrt(dx * dx + dy * dy)
                        # 方向
                        gradient_orientation = np.rad2deg(arctan2(dy,
                                                                  dx)) % 360
                        # 高斯加權值
                        weight = np.exp(weight_multiplier *
                                        ((row_rot / hist_width)**2 +
                                         (col_rot / hist_width)**2))
                        
                        row_bin_list.append(row_bin)
                        col_bin_list.append(col_bin)
                        magnitude_list.append(weight * gradient_magnitude)
                        orientation_bin_list.append(
                            (gradient_orientation - angle) * bins_per_degree)

        for row_bin, col_bin, magnitude, orientation_bin in zip(
                row_bin_list, col_bin_list, magnitude_list,
                orientation_bin_list):
            # 通過三線性插值平滑
            # 實際上是在做三線性插值的逆(取立方體的中心值,並將其分配給它的八個鄰域)
            row_bin_floor, col_bin_floor, orientation_bin_floor = floor(
                [row_bin, col_bin, orientation_bin]).astype(int)
            # 計算差值部分,小數餘項
            row_fraction, col_fraction, orientation_fraction = row_bin - row_bin_floor, col_bin - col_bin_floor, orientation_bin - orientation_bin_floor
            if orientation_bin_floor < 0:
                orientation_bin_floor += num_bins
            if orientation_bin_floor >= num_bins:
                orientation_bin_floor -= num_bins

            c1 = magnitude * row_fraction
            c0 = magnitude * (1 - row_fraction)
            
            c11 = c1 * col_fraction
            c10 = c1 * (1 - col_fraction)
            c01 = c0 * col_fraction
            c00 = c0 * (1 - col_fraction)
            # 最終累加在每個方向上的梯度大小爲
            c111 = c11 * orientation_fraction
            c110 = c11 * (1 - orientation_fraction)
            c101 = c10 * orientation_fraction
            c100 = c10 * (1 - orientation_fraction)
            c011 = c01 * orientation_fraction
            c010 = c01 * (1 - orientation_fraction)
            c001 = c00 * orientation_fraction
            c000 = c00 * (1 - orientation_fraction)

            histogram_tensor[row_bin_floor + 1, col_bin_floor + 1,
                             orientation_bin_floor] += c000
            histogram_tensor[row_bin_floor + 1, col_bin_floor + 1,
                             (orientation_bin_floor + 1) % num_bins] += c001
            histogram_tensor[row_bin_floor + 1, col_bin_floor + 2,
                             orientation_bin_floor] += c010
            histogram_tensor[row_bin_floor + 1, col_bin_floor + 2,
                             (orientation_bin_floor + 1) % num_bins] += c011
            histogram_tensor[row_bin_floor + 2, col_bin_floor + 1,
                             orientation_bin_floor] += c100
            histogram_tensor[row_bin_floor + 2, col_bin_floor + 1,
                             (orientation_bin_floor + 1) % num_bins] += c101
            histogram_tensor[row_bin_floor + 2, col_bin_floor + 2,
                             orientation_bin_floor] += c110
            histogram_tensor[row_bin_floor + 2, col_bin_floor + 2,
                             (orientation_bin_floor + 1) % num_bins] += c111

        descriptor_vector = histogram_tensor[1:-1,
                                             1:-1, :].flatten()  # 刪除直方圖邊界
        # 設定閾值,並歸一化描述符
        threshold = norm(descriptor_vector) * descriptor_max_value
        descriptor_vector[descriptor_vector > threshold] = threshold
        descriptor_vector /= max(norm(descriptor_vector), float_tolerance)

        descriptor_vector = np.round(512 * descriptor_vector)
        descriptor_vector[descriptor_vector < 0] = 0
        descriptor_vector[descriptor_vector > 255] = 255
        descriptors.append(descriptor_vector)

    return array(descriptors, dtype='float32')
##########主函數###############
##############################
#計算輸入圖像的SIFT關鍵點和描述符#
##############################
def computeKeypointsAndDescriptors(image,
                                   sigma=1.6,
                                   num_intervals=3,
                                   assumed_blur=0.5,
                                   image_border_width=5):
    """
    image:輸入圖像
    sigma:目標高斯模糊度
    num_intervals:能進行極值點檢測的圖層數
    assumed_blur:假設模糊度
    image_border_width:圖像邊緣5個像素不檢測
    """
    image = image.astype(np.float32)
    # 升採樣生成基圖像(爲了儘可能多地保留原始圖像信息,對原始圖像進行擴大兩倍採樣)
    base_image = generateBaseImage(image, sigma, assumed_blur)
    # 計算可以將圖像重複減半直到變得很小的次數
    num_octaves = computeNumberOfOctaves(base_image.shape)
    # 生成高斯模糊度列表,以產生尺度金字塔
    gaussian_kernels = generateGaussianKernels(sigma, num_intervals)
    # 生成高斯金字塔
    gaussian_images = generateGaussianImages(base_image, num_octaves,
                                             gaussian_kernels)
    # 生成高斯差分金字塔
    dog_images = generateDoGImages(gaussian_images)
    # 尋找關鍵點
    keypoints = findScaleSpaceExtrema(gaussian_images, dog_images,
                                      num_intervals, sigma, image_border_width)
    # 對關鍵點進行去重處理
    keypoints = removeDuplicateKeypoints(keypoints)
    # 將關鍵點從基本圖像座標轉換爲輸入圖像座標
    keypoints = convertKeypointsToInputImageSize(keypoints)
    # 爲關鍵點生成描述符
    descriptors = generateDescriptors(keypoints, gaussian_images)
    return keypoints, descriptors
kp1, des1 = computeKeypointsAndDescriptors(img1)
# 左圖特徵點可視化
fig = plt.figure()
ax =fig.add_subplot(111)
plt.imshow(img1, cmap='gray')
for i in range(len(kp1)):
   ax.plot(kp1[i].pt[0], kp1[i].pt[1], '.', color = 'red')
plt.show()

在這裏插入圖片描述

kp2, des2 = computeKeypointsAndDescriptors(img2)
# 右圖特徵點可視化
fig = plt.figure()
ax =fig.add_subplot(111)
plt.imshow(img2, cmap='gray')
for i in range(len(kp2)):
   ax.plot(kp2[i].pt[0], kp2[i].pt[1], '.', color = 'blue')

在這裏插入圖片描述

imageA = img2 # 右圖
imageB = img1 # 左圖
kpsA = kp2
kpsB = kp1 # 特徵點
featuresA = des2
featuresB = des1 # 特徵向量
kpsA = np.float32([kp.pt for kp in kpsA]) # 類型轉換
kpsB = np.float32([kp.pt for kp in kpsB])
###########
#全景圖生成#
##########
class Stitcher:
    # 拼接函數
    def stitch(self, images, ratio=0.75, reprojThresh=4.0, showMatches=False):
        # 獲取輸入圖片
        (imageA, imageB) = images

        # 匹配兩張圖片的所有特徵點,返回匹配結果
        M = self.matchKeypoints(kpsA, kpsB, featuresA, featuresB, ratio,
                                reprojThresh)

        # 如果返回結果爲空,沒有匹配成功的特徵點,退出算法
        if M is None:
            return None

        # 否則,提取匹配結果
        # H是3x3視角變換矩陣
        (matches, H, status) = M
        # 將圖片A進行視角變換,result是變換後圖片
        result = cv2.warpPerspective(
            imageA, H, (imageA.shape[1] + imageB.shape[1], imageA.shape[0]))
        self.cv_show('result', result)
        # 將圖片B傳入result圖片最左端
        result[0:imageB.shape[0], 0:imageB.shape[1]] = imageB
        self.cv_show('result', result)
        # 檢測是否需要顯示圖片匹配
        if showMatches:
            # 生成匹配圖片
            vis = self.drawMatches(imageA, imageB, kpsA, kpsB, matches, status)
            # 返回結果
            return (result, vis)

        # 返回匹配結果
        return result

    def cv_show(self, name, img):
        cv2.imshow(name, img)
        cv2.waitKey(0)
        cv2.destroyAllWindows()

    def matchKeypoints(self, kpsA, kpsB, featuresA, featuresB, ratio,
                       reprojThresh):
        # 建立暴力匹配器
        matcher = cv2.BFMatcher()
        
        # 使用KNN檢測來自A、B圖的SIFT特徵匹配對,K=2
        rawMatches = matcher.knnMatch(featuresA, featuresB, 2)  # 檢測出每個點,匹配的2個點
        # 返回的M結果爲[(1, 6), ..,(112, 113)]等等,裏面的數字爲第幾個特徵點
        matches = []
        for m in rawMatches:
            # 當最近距離跟次近距離的比值小於ratio值時,保留此匹配對
            if len(m) == 2 and m[0].distance < m[1].distance * ratio:
                # 存儲兩個點在featuresA, featuresB中的索引值
                matches.append((m[0].trainIdx, m[0].queryIdx))

        # 當篩選後的匹配對大於4時,計算視角變換矩陣
        if len(matches) > 4:
            # 獲取匹配對的點座標(float32型)
            ptsA = np.float32([kpsA[i] for (_, i) in matches])
            print(ptsA.shape)  # (148, 2)
            ptsB = np.float32([kpsB[i] for (i, _) in matches])

            # 計算視角變換矩陣(把RANSAC和計算H矩陣合併到了一起)
            (H, status) = cv2.findHomography(ptsA, ptsB, cv2.RANSAC,
                                             reprojThresh)
            # 該函數的作用就是先用RANSAC選擇最優的四組配對點,再計算H矩陣。H爲3*3矩陣
            print(status.shape)
            # 返回結果
            return (matches, H, status)

        # 如果匹配對小於4時,返回None
        return None

    def drawMatches(self, imageA, imageB, kpsA, kpsB, matches, status):
        # 初始化可視化圖片,將A、B圖左右連接到一起
        (hA, wA) = imageA.shape
        (hB, wB) = imageB.shape
        vis = np.zeros((max(hA, hB), wA + wB), dtype="uint8")
        vis[0:hA, 0:wA] = imageA
        vis[0:hB, wA:] = imageB

        # 聯合遍歷,畫出匹配對
        for ((trainIdx, queryIdx), s) in zip(matches, status):
            # 當點對匹配成功時,畫到可視化圖上
            if s == 1:
                # 畫出匹配對
                ptA = (int(kpsA[queryIdx][0]), int(kpsA[queryIdx][1]))
                ptB = (int(kpsB[trainIdx][0]) + wA, int(kpsB[trainIdx][1]))
                cv2.circle(vis, ptA, 5, (0, 0, 255), 1)
                cv2.circle(vis, ptB, 5, (0, 0, 255), 1)
                cv2.line(vis, ptA, ptB, (0, 0, 255), 1)

        # 返回可視化結果
        return vis
# 對右邊的圖形做變換
# 把圖片拼接成全景圖
stitcher = Stitcher()
(result, vis) = stitcher.stitch([imageA, imageB], showMatches=True)

# 顯示所有圖片
cv2.imshow("Image A", imageA)
cv2.imshow("Image B", imageB)
cv2.imshow("Keypoint Matches", vis)
cv2.imshow("Result", result)
cv2.waitKey(0)
cv2.destroyAllWindows()

完整代碼我已放到我的資源下載中心,田納爾多,可以在上面下載。

4 實驗結果與分析

1、原始圖像

左圖 右圖
在這裏插入圖片描述 在這裏插入圖片描述

2、兩張圖像的特徵點匹配

在這裏插入圖片描述

3、右圖作了變形的結果

在這裏插入圖片描述

4、拼接結果

在這裏插入圖片描述

可以看出兩張圖片已經被連接在了一起,圖片間沒有明顯的分割與錯位,整體上也沒有照片之間的獨立感。連續的拼接需要右側的圖像不斷被仿射變化來與左側圖像連接,而導致了最右側的圖像在最終的全景圖中有些扭曲。整體來說,還算成功!


5 參考內容

  1. SIFT特徵點提取
  2. sift算法詳解及應用課件
  3. 翻譯:圖像拼接
  4. Lowe原文
  5. 線性插值與拋物線插值
  6. SIFT特徵分析與源碼解讀
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章