此篇文章是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)變換。
仿射變換
二維空間的仿射變換公式為:
此公式也可以表示為
其中
通常稱為仿射變換矩陣,由于最后一行為
,所以在之后的討論中會省略最后一行。常見的仿射變換類型有平移、縮放、旋轉(zhuǎn)。
平移
在圖像中,通常是取左上角為原點坐標(biāo),向右和向下為正方向。假設(shè)圖像中的任一坐標(biāo)為,假設(shè)圖像向右平移
個單位,向下平移
個單位,則平移后的坐標(biāo)
,用矩陣表示就是
若,則表示向右移動;若
,則表示向下移動。
縮放
這里的縮放與我們平常的認(rèn)知有所不同:以
為中心在水平方向上縮放
倍,在方向上垂直縮放上
倍,是指縮放后的坐標(biāo)距離縮放中心
的水平垂直距離分別變?yōu)榱嗽瓉淼?img class="math-inline" src="https://math.jianshu.com/math?formula=s_x" alt="s_x" mathimg="1">、
倍。若縮放中心為原點,則縮放公式為
,對應(yīng)的矩陣表示則為:
如果是以為中心進行縮放變換,相當(dāng)于先把原點平移到
,然后以原點為中心進行變換,最后將原點再移回去。對應(yīng)公式為
,用矩陣表示為:
以任意一點為中心的縮放變換矩陣是平移矩陣和以為中心的縮放矩陣組合相乘得到的。
旋轉(zhuǎn)
設(shè)坐標(biāo)以
順時針旋轉(zhuǎn)到
,角度從
變?yōu)?img class="math-inline" src="https://math.jianshu.com/math?formula=%5Calpha%2B%5Ctheta" alt="\alpha+\theta" mathimg="1">,
,
,其中
,則
化簡可得,
;相反,若左邊
逆時針旋轉(zhuǎn)到
,則取
為
即可。
矩陣表示為(順時針):
若以任意一點為中心旋轉(zhuǎn),相當(dāng)于先將原點移動到旋轉(zhuǎn)中心,然后繞原點旋轉(zhuǎn),最后移回坐標(biāo)原點,用矩陣表示為:
上面的運算順序是從右向左的。
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),想求出仿射變換矩陣,可以通過解方程法或矩陣法。
由于仿射變換矩陣
有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);
對于矩陣法求解,仿射變換矩陣是平移仿射矩陣乘以縮放仿射矩陣
即運算的順序是從右往左。對于矩陣的乘法,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表示。
若是縮放后以旋轉(zhuǎn),則通過以下公式求:
運算順序仍是從右向左,如還需平移,則左乘平移仿射矩陣。
對于等比例縮放的仿射變換,OpenCV提供函數(shù)getRotationMatrix2D(center, angle, scale)來計算矩陣,center是變換中心;angle是逆時針旋轉(zhuǎn)的角度,如果是負(fù)數(shù)就代表順時針了;scale是等比例縮放的系數(shù)。
插值算法
在運算中,我們可能會遇到目標(biāo)坐標(biāo)有小數(shù)的情況,比如將坐標(biāo)縮放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)的值來估算此位置的顏色,也就是插值。
最近鄰插值
最近鄰插值就是從的四個相鄰坐標(biāo)中找到最近的那個來當(dāng)作它的值,如
,它的相鄰坐標(biāo)分別為
、
、
、
,計算
與
的距離,最近的為
,則取
的值為
的值。
此種方法得到的圖像會出現(xiàn)鋸齒狀外觀,對于放大圖像則更明顯。
雙線性插值
用表示不大于
的最大整數(shù)
第一步:用表示點
到
的水平距離,
表示點
到
的水平距離,對于處于
的值
用以下公式計算:
第二步:用表示點
到
的水平距離,
表示點
到
的水平距離,對于處于
的值
用以下公式計算:
第三步:用表示點
到
的水平距離,
表示點
到
的水平距離,結(jié)合一二步中求得的
和
計算
的值
實現(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)陰影或者遮擋,所以投影變換很難修正。但是如果物體是平面的,那么很容易通過二維投影變換對此物體三維變換進行模型化,這就是專用的二維投影變換,可以通過以下公式表述:
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)
對于平面上的任意一點
,以
為中心的極坐標(biāo)轉(zhuǎn)換公式為
以變換中心為圓心的同一個圓上的點,在極坐標(biāo)系顯示為一條直線
可以用以下代碼實現(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)換公式為
OpenCV提供函數(shù)polarToCart(magnitude, angle[, x[, y[, angleInDegrees ]]]),返回的x,y是以原點為中心的笛卡爾坐標(biāo),即已知和
,計算出的是
;magnitude對應(yīng)
,angle對應(yīng)
;參數(shù)說明和
cartToPolar類似。
舉例已知坐標(biāo)系中的(30, 10),(31, 10), (30, 11), (31, 11),
以角度表示,問笛卡爾坐標(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è)要將與的距離范圍為
,角度范圍在
內(nèi)的點進行極坐標(biāo)向笛卡爾坐標(biāo)的轉(zhuǎn)換,輸出圖像
的值用以下公式計算:
其中,代表步長,
一般取值
,
,輸出圖像矩陣寬
,高
。
實現(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),在垂直方向上,
在水平方向上,和之前討論的相反,旋轉(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)的公式為:
反過來:
通過對M值的修改可以發(fā)現(xiàn)M值越大,在水平方向得到的信息越多。
參考
《OpenCV算法精解——基于Python和C++》(張平)第三章