OpenCV算法學(xué)習(xí)筆記之幾何變換

此篇文章是OpenCV算法學(xué)習(xí)筆記的第二篇文章,之前的文章請看:

OpenCV算法學(xué)習(xí)筆記之初識OpenCV
OpenCV算法學(xué)習(xí)筆記之對比度增強
OpenCV算法學(xué)習(xí)筆記之平滑算法
OpenCV算法學(xué)習(xí)筆記之閾值分割
OpenCV算法學(xué)習(xí)筆記之形態(tài)學(xué)處理
OpenCV算法學(xué)習(xí)筆記之邊緣檢測(一)
OpenCV算法學(xué)習(xí)筆記之邊緣處理(二)
OpenCV算法學(xué)習(xí)筆記之形狀檢測

更多文章可以訪問我的博客Aengus | Blog

對于一張圖片的放大、縮小、旋轉(zhuǎn)等操作我們統(tǒng)稱為幾何變換。幾何變換是圖像最基本也是最成用的操作,常見的幾何變換有仿射變換、投影變換、極坐標(biāo)變換。

仿射變換

二維空間的仿射變換公式為:

\left(\begin{matrix}\bar{x}\\\bar{y}\end{matrix}\right)=\left(\begin{matrix}a_{11}&a_{12}\\a_{21}&a_{22}\end{matrix}\right)\left(\begin{matrix}x\\y\end{matrix}\right)+\left(\begin{matrix}a_{13}\\a_{23}\end{matrix}\right)

此公式也可以表示為

\left(\begin{matrix}\bar{x}\\\bar{y}\\1\end{matrix}\right)=A\left(\begin{matrix}x\\y\\1\end{matrix}\right)
其中

A=\left(\begin{matrix}a_{11}&a_{12}&a_{13}\\a_{21}&a_{22}&a_{23}\\0&0&1\end{matrix}\right)

A通常稱為仿射變換矩陣,由于最后一行為(0, 0, 1),所以在之后的討論中會省略最后一行。常見的仿射變換類型有平移、縮放、旋轉(zhuǎn)。

平移

在圖像中,通常是取左上角為原點坐標(biāo),向右和向下為正方向。假設(shè)圖像中的任一坐標(biāo)為(x,y),假設(shè)圖像向右平移t_x個單位,向下平移t_y個單位,則平移后的坐標(biāo)(\bar{x},\bar{y})=(x+t_x,y+t_y),用矩陣表示就是

\left(\begin{matrix}\bar{x}\\\bar{y}\\\bar{1}\end{matrix}\right)=\left(\begin{matrix}1&0&t_x\\0&1&t_y\\0&0&1\end{matrix}\right)\left(\begin{matrix}x\\y\\1\end{matrix}\right)
t_x>0,則表示向右移動;若t_y>0,則表示向下移動。

縮放

這里的縮放與我們平常的認(rèn)知有所不同:(x,y)(0,0)為中心在水平方向上縮放s_x倍,在方向上垂直縮放上s_y倍,是指縮放后的坐標(biāo)距離縮放中心(0,0)的水平垂直距離分別變?yōu)榱嗽瓉淼?img class="math-inline" src="https://math.jianshu.com/math?formula=s_x" alt="s_x" mathimg="1">、s_y倍。若縮放中心為原點,則縮放公式為(\bar{x},\bar{y})=(x*s_x,y*s_y),對應(yīng)的矩陣表示則為:

\left(\begin{matrix}\bar{x}\\\bar{y}\\\bar{1}\end{matrix}\right)=\left(\begin{matrix}s_x&0&0\\0&s_y&0\\0&0&1\end{matrix}\right)\left(\begin{matrix}x\\y\\1\end{matrix}\right)

如果是以(x_0,y_0)為中心進行縮放變換,相當(dāng)于先把原點平移到(x_0,y_0),然后以原點為中心進行變換,最后將原點再移回去。對應(yīng)公式為 (\bar{x},\bar{y})=((x-x_0)*s_x+x_0,(y-y_0)*s_y+y_0),用矩陣表示為:

\left(\begin{matrix}\bar{x}\\\bar{y}\\\bar{1}\end{matrix}\right)=\left(\begin{matrix}1&0&x_0\\0&1&y_0\\0&0&1\end{matrix}\right)\left(\begin{matrix}s_x&0&0\\0&s_y&0\\0&0&1\end{matrix}\right)\left(\begin{matrix}1&0&-x_0\\0&1&-y_0\\0&0&1\end{matrix}\right)\left(\begin{matrix}x\\y\\1\end{matrix}\right)

以任意一點為中心的縮放變換矩陣是平移矩陣和以(0,0)為中心的縮放矩陣組合相乘得到的。

旋轉(zhuǎn)

設(shè)坐標(biāo)(x,y)(0,0)順時針旋轉(zhuǎn)到(\bar{x},\bar{y}),角度從\alpha變?yōu)?img class="math-inline" src="https://math.jianshu.com/math?formula=%5Calpha%2B%5Ctheta" alt="\alpha+\theta" mathimg="1">,cos\alpha=\frac{x}{p}sin\alpha=\frac{y}{p},其中p=\sqrt{x^2+y^2},則
cos(\alpha+\theta)=cos\alpha cos\theta-sin\alpha sin\theta=\frac{\bar{x}}{p}\\sin(\alpha+\theta)=sin\alpha cos\theta+sin\theta cos\alpha=\frac{\bar{y}}{p}
化簡可得\bar{x}=xcos\theta-ysin\theta,\bar{y}=xsin\theta+ycos\theta;相反,若左邊(x,y)逆時針旋轉(zhuǎn)到(\bar{x},\bar{y}),則取\theta-\theta即可。

矩陣表示為(順時針):

\left(\begin{matrix}\bar{x}\\ \bar{y}\\ \bar{1}\end{matrix}\right)=\left(\begin{matrix}cos\theta&-sin\theta&0\\sin\theta&cos\theta&0\\0&0&1\end{matrix}\right)\left(\begin{matrix}x\\y\\1\end{matrix}\right)

若以任意一點(x_0,y_0)為中心旋轉(zhuǎn),相當(dāng)于先將原點移動到旋轉(zhuǎn)中心,然后繞原點旋轉(zhuǎn),最后移回坐標(biāo)原點,用矩陣表示為:

\left(\begin{matrix}\bar{x}\\\bar{y}\\\bar{1}\end{matrix}\right)=\left(\begin{matrix}1&0&x_0 \\ 0&1 & y_0\\0&0&1\end{matrix}\right)\left(\begin{matrix}cos\theta & -sin\theta &0\\sin\theta&cos\theta&0\\0&0&1\end{matrix}\right)\left(\begin{matrix}1&0&-x_0\\0&1&-y_0\\0&0&1\end{matrix}\right)\left(\begin{matrix}x\\y\\1\end{matrix}\right)

上面的運算順序是從右向左的。

OpenCV提供函數(shù)rotate(InputArray src, Output dst, int rotateCode)實現(xiàn)順時針90°、180°、270°的旋轉(zhuǎn),rotateCode有以下取值:

ROTATE_90_CLOCKWISE    //順時針旋轉(zhuǎn)90度
ROTATE_180             //順時針旋轉(zhuǎn)180度
ROTATE_270_COUNTERCLOCKWISE  //順時針旋轉(zhuǎn)270度

需要注意的是OpenCV還有一個函數(shù)為flip(src, dst, int flipCode)實現(xiàn)了圖像的水平鏡像、垂直鏡像和逆時針旋轉(zhuǎn)180°,不過并不是通過仿射變換實現(xiàn)的,而是通過行列互換,它與rotate()、transpose()函數(shù)一樣都在core.hpp頭文件中。

求解仿射變換矩陣

以上都是知道變換前坐標(biāo)求變換后的坐標(biāo),如果我們已經(jīng)知道了變換前的坐標(biāo)和變換后的坐標(biāo),想求出仿射變換矩陣,可以通過解方程法或矩陣法。

由于仿射變換矩陣
A=\left(\begin{matrix}a_{11}&a_{12}&a_{13}\\a_{21}&a_{22}&a_{23}\\0&0&1\end{matrix}\right)
有6個未知數(shù),所以我們只需三組坐標(biāo)列出六個方程即可。

OpenCV提供函數(shù)getAffineTransform(src, dst)通過方程法求解,其中src和dst分別為前后坐標(biāo),函數(shù)聲明在imgproc.hpp頭文件,在Python中,可以用以下方式求解:

import cv2 as cv
import numpy as np
src = np.array([[0, 0], [200, 0], [0, 200]], np.float32)
dst = np.array([[0, 0], [100, 0], [0, 100]], np.float32)
A = cv.getAffineTransform(src, dst)

對于C++來說,一種方式是將坐標(biāo)存在Point2f數(shù)組中,另一種方法是保存在Mat中:

// 第一種方法
Point2f src1[] = {Pointy2f(0, 0), Point2f(200, 0), Point2f(0, 200)};
Point2f dst1[] = {Pointy2f(0, 0), Point2f(100, 0), Point2f(0, 100)};
// 第二種方法
Mat src2 = (Mat_<float>(3, 2) << 0, 0, 200, 0, 0, 200);
Mat dst2 = (Mat_<float>(3, 2) << 0, 0, 100, 0, 0, 100);

Mat A = getAffineTransform(src1, dst1);

對于矩陣法求解,仿射變換矩陣是平移仿射矩陣乘以縮放仿射矩陣

\left(\begin{matrix}\bar{x}\\\bar{y}\\\bar{1}\end{matrix}\right)=\left(\begin{matrix}1&0&t_x\\0&1&t_y\\0&0&1\end{matrix}\right)\left(\begin{matrix}s_x&0&0\\0&s_y&0\\0&0&1\end{matrix}\right)\left(\begin{matrix}x\\y\\1\end{matrix}\right)

即運算的順序是從右往左。對于矩陣的乘法,Numpy提供函數(shù)dot()實現(xiàn),假設(shè)某個圖像先等比例縮放2倍,然后水平向右移動100,垂直向下移動200,則

import numpy as np
# 縮放矩陣
s = np.array([[0.5, 0, 0],
              [0, 0.5, 0],
              [0, 0, 1.0]])
# 平移矩陣
t = np.array([[1, 0, 100],
              [0, 1, 200],
              [0, 0, 1]])
A = np.dot(s, t)

C++中OpenCV通過“*”或gemm()函數(shù)實現(xiàn)矩陣乘法,縮放矩陣和平移矩陣可以用Mat表示。

若是縮放后以(x_0,y_0)旋轉(zhuǎn),則通過以下公式求:

\left(\begin{matrix}1&0&x_0\\0&1&y_0\\0&0&1\end{matrix}\right)\left(\begin{matrix}cos\theta&-sin\theta&0\\sin\theta&cos\theta&0\\0&0&1\end{matrix}\right)\left(\begin{matrix}s_x&0&0\\0&s_y&0\\0&0&1\end{matrix}\right)\left(\begin{matrix}1&0&-x_0\\0&1&-y_0\\0&0&1\end{matrix}\right)

運算順序仍是從右向左,如還需平移,則左乘平移仿射矩陣。

對于等比例縮放的仿射變換,OpenCV提供函數(shù)getRotationMatrix2D(center, angle, scale)來計算矩陣,center是變換中心;angle是逆時針旋轉(zhuǎn)的角度,如果是負(fù)數(shù)就代表順時針了;scale是等比例縮放的系數(shù)。

插值算法

在運算中,我們可能會遇到目標(biāo)坐標(biāo)有小數(shù)的情況,比如將坐標(biāo)(3,3)縮放2倍變?yōu)榱?img class="math-inline" src="https://math.jianshu.com/math?formula=(1.5%2C1.5)" alt="(1.5,1.5)" mathimg="1">,但是對于圖像來說并沒有這個點,這時候我們就要用周圍坐標(biāo)的值來估算此位置的顏色,也就是插值。

最近鄰插值

最近鄰插值就是從(x,y)的四個相鄰坐標(biāo)中找到最近的那個來當(dāng)作它的值,如(2.3,4.7),它的相鄰坐標(biāo)分別為(2,4)、(3,4)(2,5)、(3,5),計算(x_i,y_i)(i=1,2,3,4)(x,y)的距離,最近的為(2,5),則取(2,5)的值為(2.3,4.7)的值。

此種方法得到的圖像會出現(xiàn)鋸齒狀外觀,對于放大圖像則更明顯。

雙線性插值

[x]表示不大于x的最大整數(shù)

第一步:|x-[x]|表示點(x,y)([x],[y])的水平距離,|[x]+1-x|表示點(x,y)([x]+1,[y])的水平距離,對于處于(x,[y])的值f_I(x,[y])用以下公式計算:

f_I(x,[y])=|x-[x]|*f_I([x+1],y)+(1-|x-[x]|)*f_I([x],[y])

第二步:|x-[x]|表示點(x,y)([x],[y]+1)的水平距離,|[x]+1-x|表示點(x,y)([x]+1,[y]+1)的水平距離,對于處于(x,[y]+1)的值f_I(x,[y]+1)用以下公式計算:

f_I(x,[y]+1)=|x-[x]|*f_I([x+1],[y]+1)+(1-|x-[x]|)*f_I([x],[y]+1)

第三步:|y-[y]|表示點(x,y)(x,[y])的水平距離,|[y]+1-y|表示點(x,y)(x,[y]+1)的水平距離,結(jié)合一二步中求得的f_I(x,[y]+1)f_I(x,[y])計算f_I(x,y)的值

f_I(x,y)=|y-[y]|*f_I(x,[y]+1)+(1-|y-[y]|)*f_I(x,[y])

實現(xiàn)

在已知仿射矩陣的基礎(chǔ)上,OpenCV提供函數(shù)warpAffine(src, M, dsize[, dst[, flags[, borderMode[, borderValue ]]]])實現(xiàn)圖像的仿射變換,其中,src是輸入圖像矩陣;M是2×3的仿射矩陣;dsize是輸出圖像的大小(二元元組);flags是插值法,有INTE_NEAREST、INTE_LINEAR(默認(rèn))等;borderMode是填充模式,有BORDER_CONSTANT等;borderValue是當(dāng)borderMode=BORDER_CONSTANT的時候的值。

為了使用方便,OpenCV還提供了另一個函數(shù)resize(InputArray src, OutputArray dst,Size dsize, double fx=0, double fy=0, int interpolation=INTER_LINEAR)實現(xiàn)縮放,其中dsize是輸出圖像的大小,二元元組;fx、fy分別是水平垂直方向上的縮放比例,默認(rèn)為0;interpolation是插值法。

Mat I = imread("test.png");
Mat dst;
resize(I, dst, Size(I.cols/2, I.rows/2), 0.5, 0.5);

//resize也可寫成下面這種形式
//resize(I, dst, Size(), 0.5, 0.5);

投影變換

如果物體在三維空間中發(fā)生旋轉(zhuǎn),這種變換通常成為投影變換。由于可能出現(xiàn)陰影或者遮擋,所以投影變換很難修正。但是如果物體是平面的,那么很容易通過二維投影變換對此物體三維變換進行模型化,這就是專用的二維投影變換,可以通過以下公式表述:

\left(\begin{matrix}\bar{x}\\\bar{y}\\\bar{z}\end{matrix}\right)=\left(\begin{matrix}a_{11}&a_{12}&a_{13}\\a_{21}&a_{22}&a_{23}\\a_{31}&a_{32}&a_{33}\end{matrix}\right)\left(\begin{matrix}x\\y\\z\end{matrix}\right)

OpenCV提供函數(shù)getPerspectiveTransform(src, dst)實現(xiàn)求解投影矩陣,需要輸入四組對應(yīng)的坐標(biāo)。該函數(shù)的Python的API,src和dst分別是4×2的二維ndarray,數(shù)據(jù)類型必須是float32,否則會報錯;返回的矩陣是float64類型的。

OpenCV提供函數(shù)warpPerspective(src, M, dsize[, dst[, flags[, borderMode[, borderValue ]]]])實現(xiàn)投影變換,參數(shù)說明和仿射變化類似。

極坐標(biāo)變換

通常通過極坐標(biāo)變化校正圖像中的圓形物體或包含在圓環(huán)中的物體。

笛卡爾坐標(biāo)轉(zhuǎn)化為極坐標(biāo)

對于xoy平面上的任意一點(x,y),以(x_0,y_0)為中心的極坐標(biāo)轉(zhuǎn)換公式為

r=\sqrt{(x-x_0)^2+(y-y_0)^2}\\ \theta=\left\{\begin{array}22\pi+arctan2(y-y_0,x-x_0),y-y_0<=0\\arctan2(y-y_0,x-x_0),y-y_0>0 \end{array}\right.

以變換中心為圓心的同一個圓上的點,在極坐標(biāo)系\theta or顯示為一條直線

可以用以下代碼實現(xiàn)

import math
r = math.sqrt(math.pow(x-x0)+math.pow(y-y0))
theta = math.atan2(y-y0, x-x0)/math.pi*180 # 轉(zhuǎn)化為角度

OpenCV提供函數(shù)cartToPolar(x, y[, magnitude[, angle[, angleInDegrees ]]])實現(xiàn)將原點移動到變換中心后的笛卡爾坐標(biāo)向極坐標(biāo)轉(zhuǎn)換,返回值magnitude和angle是和參數(shù)x,y相同尺寸和類型的ndarray,angleInDegrees為True時返回的angle是角度,否則為弧度;x是數(shù)組且數(shù)據(jù)類型必須為浮點型、float32或float64,y尺寸和類型和x一致,x、y分別代表x坐標(biāo)和y坐標(biāo)。

極坐標(biāo)轉(zhuǎn)化為笛卡爾坐標(biāo)

轉(zhuǎn)換公式為
x=x_0+rcos\theta \\ y=y_0+rsin\theta
OpenCV提供函數(shù)polarToCart(magnitude, angle[, x[, y[, angleInDegrees ]]]),返回的x,y是以原點為中心的笛卡爾坐標(biāo),即已知(\theta,r)(x_0,y_0),計算出的是(x-x_0,y-y_0);magnitude對應(yīng)r,angle對應(yīng)\theta;參數(shù)說明和cartToPolar類似。

舉例已知\theta or坐標(biāo)系中的(30, 10),(31, 10), (30, 11), (31, 11),\theta以角度表示,問笛卡爾坐標(biāo)系中的哪四個坐標(biāo)以(-12, 15)為中心經(jīng)過極坐標(biāo)變換后得到這四個坐標(biāo),實現(xiàn)代碼為:

import cv2 as cv
import numpy as np
# 也可以用np.array([30, 31, 30, 31]),只影響輸出下x,y的格式
angle = np.array([[30, 31], [30, 31]], np.float32) 
r = np.array([[10, 10], [11, 11]], np.float32)
x, y = cv.polarToCart(r, angle, angleInDegrees, True)
# 計算出的是(x-x0, y-y0)的坐標(biāo)
x += -12
y += 15

如果用C++實現(xiàn),可以將角度和距離放在Mat中。

極坐標(biāo)變換處理圖像

假設(shè)要將與(x_0,y_0)的距離范圍為[r_{min},r_{max}],角度范圍在[\theta_{min},\theta_{max}]內(nèi)的點進行極坐標(biāo)向笛卡爾坐標(biāo)的轉(zhuǎn)換,輸出圖像\textbf{O}(i,j)的值用以下公式計算:
\textbf{O}(i,j)=f_{\textbf{I}}(x_0+(r_{min}+r_{step}i)*cos(\theta_{min}+\theta_{step}j),y_0+(r_{min}+r_{step}i)*sin(\theta_{min}+\theta_{step}j))
其中,0<r_{step}<=1代表步長,\theta_{step}一般取值\frac{360}{180*N}N>=2,輸出圖像矩陣寬w\approx\frac{r_{max}-r_{min}}{r_{step}}+1,高h\approx\frac{\theta_{max}-\theta_{max}}{\theta_{step}}+1。

實現(xiàn)

首先了解以下Numpy的tile(a, (m, n))函數(shù),此函數(shù)返回一個m×n個a平鋪成的矩陣,垂直方向m個,水平方向n個,如:

a = np.array([[1, 2], [3, 4]])
print(np.tile(a, (2, 3)))

輸出

array([[1, 2, 1, 2, 1, 2],
       [3, 4, 3, 4, 3, 4],
       [1, 2, 1, 2, 1, 2],
       [1, 2, 1, 2, 1, 2]])

對C++來說,OpenCV提供函數(shù)repeat(const Mat& src, int ny, int nx)實現(xiàn)類似的功能。

下面的代碼是用Python實現(xiàn)極坐標(biāo)變換,使用的是最近鄰插值法,也可以使用其他插值方法:

def polar(I, center, r, theta=(0, 360), rstep=1.0, thetastep=360.0/(180*8)):
    """
    :param I: 輸入的圖像矩陣
    :param center: 極坐標(biāo)變換中心
    :param r: 二元元組,代表最小和最大距離
    :param theta: 二元元組,角度范圍
    :param rstep: r的變換步長
    :param thetastep: theta的變換步長
    :return 輸出圖像矩陣
    """
    # 得到距離的最小最大范圍
    r_min, r_max = r
    # 角度最小最大范圍
    theta_min, theta_max = theta
    # 輸出圖像的高、寬
    h = int((r_max - r_min)/rstep) + 1
    w = int((theta_max - theta_min)/thetastep) + 1
    O = 125 * np.ones((h, w), I.dtype)
    # 極坐標(biāo)變換
    # linspace(start, stop, num=50)產(chǎn)生從start到stop,數(shù)量為num的等差數(shù)列
    r = np.linspace(r_min, r_max, h)
    r = np.tile(r, (w, 1))
    # 轉(zhuǎn)置
    r = np.transpose(r)
    theta = np.linspace(theta_min, theta_max, w)
    theta = np.tile(theta, (h, 1))
    x, y = cv2.polarToCart(r, theta, angleInDegrees=True)
    # 最近鄰插值法
    for i in range(h):
        for j in range(w):
            # round()按照指定精度四舍五入
            px = int(round(x[i][j])+cx)
            py = int(round(y[i][j])+cy)
            if (px>=0 and py <= w-1) and (py >=0 and py <= h-1):
                O[i][j] = I[py][px]
    return O

OpenCV3.X以上提供線性極坐標(biāo)函數(shù)linearPolar(src, dst, Point2f center, double maxRadius, int flags);實現(xiàn)了我們上面的函數(shù)效果,其中center是變換中心,maxRadius是最大距離,flags是插值算法,值和函數(shù)resize、warpAffine一樣。需要注意的是,linearPolar函數(shù)生成的極坐標(biāo),\theta在垂直方向上,r在水平方向上,和之前討論的相反,旋轉(zhuǎn)90°后得到的結(jié)果類似。

對數(shù)極坐標(biāo)函數(shù)

OpenCV3.X中提供函數(shù)logPolar(src, dst, center, M, flags),其中M是系數(shù),值大一些效果好;flags等于WARP_FILL_OUTLIERS代表笛卡爾坐標(biāo)向?qū)?shù)坐標(biāo)變換,等于WARP_INVERSE_MAP代表對數(shù)坐標(biāo)向笛卡爾坐標(biāo)變換。

將笛卡爾坐標(biāo)轉(zhuǎn)換為對數(shù)坐標(biāo)的公式為:

\bar{r}=M * log \sqrt{(x-x_0)^2 + (y-y_0)^2}\\ \theta=\left\{\begin{array}22 \pi+ arctan2(y-y_0,x-x_0),y-y_0<=0\\arctan2(y-y_0,x-x_0),y-y_0>0 \end{array}\right.

反過來:

x=x_0-exp(\frac{\bar{r}}{M})cos\theta,y=y_0-exp(\frac{\bar{r}}{M}))sin\theta

通過對M值的修改可以發(fā)現(xiàn)M值越大,在水平方向得到的信息越多。

參考

《OpenCV算法精解——基于Python和C++》(張平)第三章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容