圖形學筆記(六)——單應性變換

python圖像處理筆記-六——單應性變換

齊次座標

單應性變化是將一個平面內的店映射到另一個平面內的二維投影變換。本質上,單應性變換
H,按照下方的方程映射到二維的點:

[xyw]=[h1h2h3h4h5h6h7h8h9][xyw] \left[\begin{matrix} x' \\ y' \\ w'\end{matrix}\right]=\left[\begin{matrix} h_1 & h_2 & h_3 \\ h_4 & h_5 & h_6 \\ h_7 & h_8 & h_9\end{matrix}\right]\left[\begin{matrix} x \\ y \\ w\end{matrix}\right]
我們也可以表示爲:
x=Hx x' = Hx
對於圖像平面內的點,齊次座標是一個非常有用的表示方式。點的齊次座標是依賴於其尺度定義的,所以以下三種表述實質上都在表示同一個二維點。
KaTeX parse error: No such environment: align at position 8: \begin{̲a̲l̲i̲g̲n̲}̲ x &= [\begin{m…

因此單應性矩陣H也僅依賴尺度定義,所以單應性矩陣具有八個獨立的自由度。我們常用w=1w = 1來歸一化點,這樣點就會擁有唯一的座標:x和y。如果你看過我寫的組成原理筆記的話,你會發現這裏的目的和浮點數的左歸、右歸那裏的目的非常相似。這個額外的座標使得我們可以簡單地使用一個矩陣來表示變換。

仿射變換

在投影變換中,有一些特別重要的變換,如:仿射變換:
[xy1]=[a1a2txa3a4ty001][xyz] \left[\begin{matrix} x' \\ y' \\ 1\end{matrix}\right]= \left[\begin{matrix} a_1 & a_2 & t_x \\ a_3 & a_4 & t_y \\ 0 & 0 & 1\end{matrix}\right] \left[\begin{matrix} x \\ y \\ z\end{matrix}\right]
或者我們也可以寫作:
x=[At01]x x' = \left[\begin{matrix}A & t \\ 0 & 1\end{matrix}\right] x
我們來實現一下這個仿射變換的代碼:

from PIL import Image
from numpy import *
from pylab import *

def normalize(points):
	"""
	在齊次座標意義下:
	對點進行歸一化,使得最後一行爲1
	"""
	for row in points:
		row = points[-1]
	return points

def makeHomog(points):
	"""
	將點集(dim * n)的數組轉化爲齊次座標表示
	"""
	return vstack((points, ones((1, points.shape[1]))))


# 仿射變換
def flact(points, rtMatrix):
	return np.dot(rtMatrix, points)


def plotPoints(points, color = None, line = True):
	"""
	給出一堆點,把他們畫出來,並且顯示
	"""
	for i in range(points.shape[1]):
		if(color == None):
			scatter(points[0][i], points[1][i])
		else :
			scatter(points[0][i], points[1][i], c= color[i])
	if(line == False ):
		return 
	for i in range(points.shape[1]):
		nextPoint = (i + 1)%points.shape[1]
		if(color == None):
			plot([points[0][i],points[1][i]],[points[0][nextPoint],points[1][nextPoint]])
		else:
			plot([points[0][i],points[1][i]], [points[0][nextPoint],points[1][nextPoint]], c= color[i])

if __name__ == '__main__':
	pointX = [0, 3, 3, 0]
	pointY = [0, 0, 3, 3]
	color = ['r','g','b','m']
	Points = array([pointX, pointY])
	Points = makeHomog(Points)

	# 繪製變換之前的點
	plotPoints(Points, color)

	# 仿射變換矩陣
	rtMatrix = array(
		[[0.5,0,0],
		[0,0.5,0],
		[0,0,1]]
		)
	changedPoints = flact(Points, rtMatrix)
	plotPoints(changedPoints, color)
	show()
上面的圖像中我們對左邊的圖像使用矩陣: $$ \left[ \begin{matrix} 0.5 &0 &0\\ 0 &0.5 & 0\\ 0 & 0 & 1 \end{matrix} \right] $$ 進行仿射變換,我們發現圖像變小了,於是我們知道左邊的A可以控制圖像的大小。隨後,我們將矩陣修改爲: $$ \left[\begin{matrix}0.5 &0 &1\\0 &0.5 & 1\\0 & 0 & 1\end{matrix}\right] $$ 我們發現之前說的T能夠控制圖像的平移,實際上它還可以控制圖像的旋轉等,我在網上看到一張圖,和大家分享一下: ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9zYWl5dXdhbmctYmxvZy5vc3MtY24tYmVpamluZy5hbGl5dW5jcy5jb20vJUU1JTlCJUJFJUU1JTgzJThGJUU1JUE0JTg0JUU3JTkwJTg2Y2g2L1Z1RWc1bi5wbmc?x-oss-process=image/format,png)

除此之外還有相似變換,相似變換是一種特殊的仿射變換,他描述了圖像的旋轉、平移、縮放,可以表示爲:

[xy1]=[scos(θ)ssin(θ)txssin(θ)scos(θ)tx001][xy1] \left[ \begin{matrix} x'\\ y'\\ 1 \end{matrix} \right] = \left[ \begin{matrix} s *cos(\theta) & -s *sin(\theta) & t_x\\ s *sin(\theta) & -s *cos(\theta) & t_x\\ 0&0 & 1 \end{matrix} \right] \left[ \begin{matrix} x\\ y\\ 1 \end{matrix} \right]

直接線性變換算法

單應性矩陣可以由兩幅圖像中對應點計算出來。一個完全攝影變換有8個自由度。根據對應點約束,每個對應點可以寫出兩個方程,分別對應於x,yx,y座標。因此計算單應性矩陣H需要四個對應點對。

DLT(Diret Linear Transformation, 直接線性相似變化)是給定4個或者更多對應點對矩陣,來計算單應性矩陣H的方法。將單應性矩陣H作用在對應點上,重新寫出該方程,我們可以得到下方方程:

我們可以看到,實際上用一組也可以解出結果,但是爲了解的穩定,我們最好使用四組以上的特徵匹配。方程的最小二乘解有一個既定的結論,即對A進行SVD分解,A的最小的奇異值對應的右奇異向量即是h的解。對h做reshape得到H。

直接線性變換算法在普通情況下,有八個自由度,而在解決仿射變換時,只有六個自由度,我們分別對兩種情況寫了代碼:

from PIL import Image
from numpy import *
from pylab import *


def normalize(points):
    """
    在齊次座標意義下:
    對點進行歸一化,使得最後一行爲1
    """
    for row in points:
        row = points[-1]
    return points


def makeHomog(points):
    """
    將點集(dim * n)的數組轉化爲齊次座標表示
    """
    return vstack((points, ones((1, points.shape[1]))))


# 仿射變換
def flact(points, rtMatrix):
    return np.dot(rtMatrix, points)


def plotPoints(ax, points, color=None):
    """
    給出一堆點,把他們畫出來,並且顯示
    """
    xlim(-1, 5)
    ylim(-1, 5)
    for i in range(points.shape[1]):
        if (color == None):
            ax.scatter(points[0][i], points[1][i])
        else:
            ax.scatter(points[0][i], points[1][i], c=color[i])


def plotTwoPoints(points1, points2):
    fig = figure()
    ax1 = fig.add_subplot(1, 2, 1)
    plotPoints(ax1, points1)
    ax2 = fig.add_subplot(1, 2, 2)
    plotPoints(ax2, points2)
    show()


def hFromPoints(fp, tp):
    """
    使用線性DLT,計算H,點自動進行歸一化

    輸入:
        fp : 起始點
        tp : 經過H映射到的點

    """
    # 形狀不同
    if fp.shape != tp.shape:
        return RuntimeError('number of points do not match')

    # 對點進行歸一化處理
    # 映射起始點
    m = mean(fp[:2], axis=1)
    maxstd = max(std(fp[:2], axis=1)) + 1e-9
    C1 = diag([1 / maxstd, 1 / maxstd, 1])
    C1[0][2] = -m[0] / maxstd
    C1[1][2] = -m[1] / maxstd
    fp = dot(C1, fp)

    # 映射對應點
    m = mean(tp[:2], axis=1)
    maxstd = max(std(tp[:2], axis=1)) + 1e-9
    C2 = diag([1 / maxstd, 1 / maxstd, 1])
    C2[0][2] = -m[0] / maxstd
    C2[1][2] = -m[1] / maxstd
    tp = dot(C2, tp)

    # 創建用於線性方法的矩陣,對於每個對應對,在矩陣中會出現兩行數值
    nbrCorreSponDences = fp.shape[1]
    A = zeros((2 * nbrCorreSponDences, 9))
    for i in range(nbrCorreSponDences):
        A[2*i] = [-fp[0][i], -fp[1][i], -1, 0, 0, 0,
                  tp[0][i]*fp[0][i],tp[0][i]*fp[1][i], tp[0][i]]
        A[2 * i + 1] = [0, 0, 0, -fp[0][i], -fp[1][i], -1,
                    tp[1][i] * fp[0][i], tp[1][i] * fp[1][i], tp[1][i]]

    U, S, V = linalg.svd(A)
    H = V[8].reshape((3,3))
    # 反歸一化
    H = dot(linalg.inv(C2), dot(H,C1))

    # 歸一化,返回
    return H / H[2,2]


def HaffineFromPoints(fp, tp):
    """
    計算H,仿射變換,使得 tp, fp 是經過仿射變換H 得到的,
    與上面不同的是,仿射變換是隻有六個自由度的,也就是說,h7=h8=0
    """
    # 形狀不同
    if fp.shape != tp.shape:
        return RuntimeError('number of points do not match')

    # 對點進行歸一化處理
    # 映射起始點
    m = mean(fp[:2], axis=1)
    maxstd = max(std(fp[:2], axis=1)) + 1e-9
    C1 = diag([1 / maxstd, 1 / maxstd, 1])
    C1[0][2] = -m[0] / maxstd
    C1[1][2] = -m[1] / maxstd
    fp_cond = dot(C1, fp)

    # 映射對應點
    m = mean(tp[:2], axis=1)
    C2 = C1.copy()
    C2[0][2] = -m[0] / maxstd
    C2[1][2] = -m[1] / maxstd
    tp_cond = dot(C2, tp)

    A = concatenate((fp_cond[:2],tp_cond[:2]), axis = 0)
    U, S, V = linalg.svd(A.T)

    # 創建B、C
    tmp = V[:2].T
    B   = tmp[:2]
    C   = tmp[2:4]

    tmp2 = concatenate((dot(C, linalg.pinv(B)),zeros((2,1))), axis = 1)
    H = vstack((tmp2, [0, 0, 1]))

    # 反歸一化
    H = dot(linalg.inv(C2), dot(H, C1))

    return H / H[2,2]

if __name__ == '__main__':
    pointX = []
    pointY = []
    for i in range(4):
        for j in range(4):
            pointX.append(i)
            pointY.append(j)
    Points = array([pointX, pointY])
    Points = makeHomog(Points)

    # 仿射變換矩陣
    rtMatrix = array(
        [[0.5, 0, 1],
         [0, 0.5, 1],
         [0, 0, 1]]
    )
    changedPoints = flact(Points, rtMatrix)
    plotTwoPoints(Points, changedPoints)
    print(hFromPoints(Points, changedPoints))
    print(HaffineFromPoints(Points, changedPoints))
    show()

結果

在剛纔的代碼中,我們使用仿射變換矩陣:
[0.50100.51001] \left[\begin{matrix}0.5 &0 &1\\0 &0.5 & 1\\0 & 0 & 1\end{matrix}\right]
進行變化,我們利用一組點變換前後的狀態,求仿射變換矩陣。得到的結果如下:

[[ 5.00000000e-01 1.20777603e-16 1.00000000e+00]
[-1.13154271e-16 5.00000000e-01 1.00000000e+00]
[ 2.45515188e-17 1.40739819e-17 1.00000000e+00]]

[[ 5.00000000e-01 6.07529133e-16 1.00000000e+00]
[-1.11022302e-16 5.00000000e-01 1.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 1.00000000e+00]]

其中,上面的結果是8自由度下的解,而下面的結果是6自由度下的解。我們可以發現結果非常完美。但是這是否是由於我門取了4*4=16個點的原因呢?於是我降低了採樣數量,從原來的採樣間隔爲1降低到採樣間隔爲2,並進行觀察。在此情況下,我們只取4個點,結果如下:

[[ 5.00000000e-01 -3.12481436e-16 1.00000000e+00]
[-5.50986389e-16 5.00000000e-01 1.00000000e+00]
[-3.84592537e-16 -9.61481342e-17 1.00000000e+00]]

[[ 5.00000000e-01 -1.79637859e-16 1.00000000e+00]
[ 1.11022302e-16 5.00000000e-01 1.00000000e+00]
[ 0.00000000e+00 0.00000000e+00 1.00000000e+00]]

我們發現,雖然結果與上一組相比較下,偏差相對較大,但是誤差仍然在可以忽略的範圍內,總體來說結果非常理想。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章