圖像預處理之warpaffine與雙線性插值及其高性能實現
視頻講解:https://www.bilibili.com/video/BV1ZU4y1A7EG
代碼Repo:https://github.com/shouxieai/tensorRT_Pro
本文為視頻講解的個人筆記。
warpaffine矩陣變換
對于坐標點的變換,我們通常考慮的是旋轉、縮放、平移這三種變換。例如將點 P(x,y)P(x,y)P(x,y) 旋轉 θ\thetaθ 度,縮放 scalescalescale 倍,平移 ox,oyox,oyox,oy 。warpaffine 將坐標點的旋轉、縮放、平移三種操作集成為一個矩陣乘法運算。
旋轉變換
我們先來看旋轉,如圖所示,我們要將點 P(x,y)P(x,y)P(x,y) 旋轉到點 P′(x′,y′)P'(x',y')P′(x′,y′) ,推導的過程很簡單,我們要求的就是 x′,y′x',y'x′,y′ 兩點的坐標,將其轉換為 m×cos(θ+α)m\times cos(\theta+\alpha)m×cos(θ+α) 和 m×sin(θ+α)m\times sin(\theta+\alpha)m×sin(θ+α) ,再用公式展開,即得結果(詳見圖中公式):
{x′y′}={cos(θ)?sin(θ)sin(θ)cos(θ)}{xy}\left\{ \begin{array}{rc} x' \\ y' \end{array} \right\}= \left\{ \begin{array}{rc} cos(\theta) & -sin(\theta) \\ sin(\theta) & cos(\theta) \end{array} \right\} \left\{ \begin{array}{rc} x \\ y \end{array} \right\} {x′y′?}={cos(θ)sin(θ)??sin(θ)cos(θ)?}{xy?}
再考慮到我們在圖像處理時的坐標系(如在 OpenCV 中的坐標系、常見目標檢測的坐標系等)通常是原點在左上角,因此應該為:
{x′y′}={cos(θ)sin(θ)?sin(θ)cos(θ)}{xy}\left\{ \begin{array}{rc} x' \\ y' \end{array} \right\}= \left\{ \begin{array}{rc} cos(\theta) & sin(\theta) \\ -sin(\theta) & cos(\theta) \end{array} \right\} \left\{ \begin{array}{rc} x \\ y \end{array} \right\} {x′y′?}={cos(θ)?sin(θ)?sin(θ)cos(θ)?}{xy?}
將旋轉變換的矩陣記為 RRR ,則 P′=RPP'=RPP′=RP
縮放變換
縮放變換比較簡單,兩坐標直接乘以縮放系數 scalescalescale 即可:
x′=x×scaley′=y×scalex'=x\times scale \\ y'=y\times scale x′=x×scaley′=y×scale
寫成矩陣形式即:
{x′y′}={scale00scale}{xy}\left\{ \begin{array}{rc} x' \\ y' \end{array} \right\}= \left\{ \begin{array}{rc} scale & 0 \\ 0 & scale \end{array} \right\} \left\{ \begin{array}{rc} x \\ y \end{array} \right\} {x′y′?}={scale0?0scale?}{xy?}
將縮放變換的變換矩陣記為 SSS,則:
P′=SPP'=SP P′=SP
則旋轉+縮放可以通過矩陣相乘寫到同一個矩陣中:
{x′y′}={cos(θ)×scalesin(θ)×scale?sin(θ)×scalecos(θ)×scale}{xy}\left\{ \begin{array}{rc} x' \\ y' \end{array} \right\}= \left\{ \begin{array}{rc} cos(\theta) \times scale & sin(\theta) \times scale \\ -sin(\theta) \times scale & cos(\theta) \times scale \end{array} \right\} \left\{ \begin{array}{rc} x \\ y \end{array} \right\} {x′y′?}={cos(θ)×scale?sin(θ)×scale?sin(θ)×scalecos(θ)×scale?}{xy?}
即:P′=SRPP'=SRPP′=SRP
注意旋轉和縮放順序是隨意的,不影響結果,這也可以通過代碼來驗證:
import numpy as nptheta = 0.8
scale = 2
rot = np.array([[np.cos(theta), np.sin(theta)],[-np.sin(theta), np.cos(theta)]
])sca = np.array([[scale, 0],[0, scale]
])print(np.allclose(rot @ sca, sca @ rot))
# 輸出:True
平移變換
平移變換可以表示為:
x′=x+oxy′=y+oyx'=x+ox\\ y'=y+oy x′=x+oxy′=y+oy
矩陣形式:
{x′y′}={1001}{xy}+{oxoy}\left\{ \begin{array}{rc} x' \\ y' \end{array} \right\}= \left\{ \begin{array}{rc} 1 & 0 \\ 0 & 1 \end{array} \right\} \left\{ \begin{array}{rc} x \\ y \end{array} \right\} + \left\{ \begin{array}{rc} ox \\ oy \end{array} \right\} {x′y′?}={10?01?}{xy?}+{oxoy?}
可以發現,平移變換直接寫成矩陣形式,已經不是單純的矩陣相乘了,而是多了一個很麻煩的相加的操作。這就很難與我們之前的縮放+旋轉的操作合并到一起,該怎么辦呢?
我們可以增加一個維度,將二維的非齊次的形式轉換為三維的齊次的形式,即這個知乎回答中所提到的:增加一個維度之后,就可以在高維度通過線性變換來完成低維度的放射變換。(該回答將放射變換講的很形象,推薦閱讀)。
那么我們增加一維 (x,y,w)(x,y,w)(x,y,w),從而將點 PPP 表示為 P(xw,yw,1)P(\frac{x}{w},\frac{y}{w},1)P(wx?,wy?,1) ,這樣平移變換就也可以表示為齊次矩陣乘的形式:
{x′y′w}={10ox01oy001}{xy1}\left\{ \begin{array}{rc} x' \\ y' \\ w \\ \end{array} \right\}= \left\{ \begin{array}{rc} 1 & 0 & ox \\ 0 & 1 & oy \\ 0 & 0 & 1 \end{array} \right\} \left\{ \begin{array}{rc} x \\ y \\ 1 \end{array} \right\} ????x′y′w?????=????100?010?oxoy1?????????xy1?????
最后我們得到縮放+旋轉+平移變換的矩陣表示(注意平移與縮放、旋轉的順序是不能隨意調換的):
{x′y′w}={cos(θ)×scalesin(θ)×scaleox?sin(θ)×scalecos(θ)×scaleoy001}{xy1}\left\{ \begin{array}{rc} x' \\ y' \\ w \end{array} \right\}= \left\{ \begin{array}{rc} cos(\theta) \times scale & sin(\theta) \times scale & ox \\ -sin(\theta) \times scale & cos(\theta) \times scale & oy \\ 0 & 0 & 1 \end{array} \right\} \left\{ \begin{array}{rc} x \\ y \\ 1 \end{array} \right\} ????x′y′w?????=????cos(θ)×scale?sin(θ)×scale0?sin(θ)×scalecos(θ)×scale0?oxoy1?????????xy1?????
將平移變換的變換矩陣記為 RRR ,則:P′=TSRPP'=TSRPP′=TSRP ,可以將整個 warpaffine 三個變換操作的矩陣記為 MMM ,即:M=TSR,P′=MPM=TSR,\ \ P'=MPM=TSR,??P′=MP 。
warpaffine矩陣變換的反變換
- 旋轉矩陣的逆矩陣,即是其轉置:R?1=RTR^{-1}=R^TR?1=RT
- 整個 warp affine 的三個變換求反變換,對整個變換矩陣求逆即可:P′=MP,P=M?1P′P'=MP,\ \ P=M^{-1}P'P′=MP,??P=M?1P′
目標檢測中的常用預處理
在目標檢測中,我們的預處理通常是先對圖像進行等比縮放,然后居中,多余部分填充,就類似下圖所展示的。
我們將這個過程分為三個步驟:
- 等比縮放,矩陣 SSS 實現
- 將圖片中心平移到左上坐標原點,矩陣 OOO 實現
- 將圖片平移到目標位置的重心,矩陣 TTT 實現
三步拆分法,看似麻煩了一點,實際上可以方便我們后續可能會需要到的更復雜的變換(比如在 OOO 平移后加入旋轉變換),并且便于記憶。
三步拆分法的矩陣表達:P′=TOSPP'=TOSPP′=TOSP 。
我們直接寫出具體的矩陣:
scale=min(Dst.widthOrigin.width,Dst.heightOrigin.height)M={scale0?scale×Origin.width2+Dst.width20scale?scale×Origin.height2+Dst.height2}scale = min(\frac{Dst.width}{Origin.width}, \frac{Dst.height}{Origin.height}) \\ \\ M = \left\{ \begin{array}{ll} scale & 0 & -\frac{scale \times Origin.width}{2} + \frac{Dst.width}{2} \\ 0 & scale & -\frac{scale \times Origin.height}{2} + \frac{Dst.height}{2} \\ \end{array} \right\} scale=min(Origin.widthDst.width?,Origin.heightDst.height?)M={scale0?0scale??2scale×Origin.width?+2Dst.width??2scale×Origin.height?+2Dst.height??}
{x′y′}={scale0?scale×Origin.width2+Dst.width20scale?scale×Origin.height2+Dst.height2}{xy1}\left\{ \begin{array}{ll} x' \\ y' \\ \end{array} \right\}= \left\{ \begin{array}{ll} scale & 0 & -\frac{scale \times Origin.width}{2} + \frac{Dst.width}{2} \\ 0 & scale & -\frac{scale \times Origin.height}{2} + \frac{Dst.height}{2} \\ \end{array} \right\} \left\{ \begin{array}{ll} x \\ y \\ 1 \end{array} \right\} {x′y′?}={scale0?0scale??2scale×Origin.width?+2Dst.width??2scale×Origin.height?+2Dst.height??}????xy1?????
逆變換:
k=scaleb1=?scale×Origin.width2+Dst.width2b2=?scale×Origin.height2+Dst.height2x′=kx+b1y′=ky+b2x=x′?b1k=x′×1k+(?b1k)y=y′?b2k=y′×1k+(?b2k)M?1={1k0?b1k01k?b2k}k = scale \\ b1 = -\frac{scale \times Origin.width}{2} + \frac{Dst.width}{2} \\ b2 = -\frac{scale \times Origin.height}{2} + \frac{Dst.height}{2} \\ x' = kx + b1 \\ y' = ky + b2 \\ x = \frac{x' - b1}{k} = x'\times \frac{1}{k} + (-\frac{b1}{k}) \\ y = \frac{y' - b2}{k} = y'\times \frac{1}{k} + (-\frac{b2}{k}) \\ M^{-1} = \left\{ \begin{array}{ll} \frac{1}{k} & 0 & -\frac{b1}{k} \\ 0 & \frac{1}{k} & -\frac{b2}{k} \\ \end{array} \right\} k=scaleb1=?2scale×Origin.width?+2Dst.width?b2=?2scale×Origin.height?+2Dst.height?x′=kx+b1y′=ky+b2x=kx′?b1?=x′×k1?+(?kb1?)y=ky′?b2?=y′×k1?+(?kb2?)M?1={k1?0?0k1???kb1??kb2??}
warpaffine正逆變換代碼實驗
TODO
雙線性插值
線性插值
距離目標點越遠,影響就越小,因此權重是對面的距離占比。
如目標點距離冷水 0.6,距離熱水 0.4,則冷水權重為 0.4 ,熱水權重為 0.6 。
p0 = 20 # 冷水
p1 = 100 # 熱水
pos = 0.6 # 應該多少度value = (1 - pos) * p0 + pos * p1
print(value)
雙線性插值
線性插值的二維版本,原理一直,只是權重從計算長度占比改為計算面積占比。
調色板,紅點對目標點(紫點)的影響權重即為對面的面積(紅框面積)占總面積的比例。
高性能實現
為什么高性能?
- 我們在操作每個像素的過程中,可以將模型需要的像素級預處理(如減均值除標準差、除以255、BGR通道轉換等)一并做了,避免多個操作分開來反復對每個像素進行循環訪問這種低效行為。
- warpaffine 極其適合通過 cuda 核函數進行 GPU 加速。可以參考 repo 中的 preprocess_kernel.cu 。完整代碼比較長這里就不放了。
- 以下是 warpaffine 雙線性插值的 Python 實現,供參考:
def pyWarpAffine(image, M, dst_size, constant=(0, 0, 0)):# 注意輸入的M矩陣格式,是Origin->Dst# 而這里需要的是Dst->Origin,所以要取逆矩陣M = cv2.invertAffineTransform(M)constant = np.array(constant)ih, iw = image.shape[:2]dw, dh = dst_sizedst = np.full((dh, dw, 3), constant, dtype=np.uint8)irange = lambda p: p[0] >= 0 and p[0] < iw and p[1] >= 0 and p[1] < ihfor y in range(dh):for x in range(dw):homogeneous = np.array([[x, y, 1]]).Tox, oy = M @ homogeneouslow_ox = int(np.floor(ox))low_oy = int(np.floor(oy))high_ox = low_ox + 1high_oy = low_oy + 1# p0 p1# o# p2 p3pos = ox - low_ox, oy - low_oyp0_area = (1 - pos[0]) * (1 - pos[1])p1_area = pos[0] * (1 - pos[1])p2_area = (1 - pos[0]) * pos[1]p3_area = pos[0] * pos[1]p0 = low_ox, low_oyp1 = high_ox, low_oyp2 = low_ox, high_oyp3 = high_ox, high_oyp0_value = image[p0[1], p0[0]] if irange(p0) else constantp1_value = image[p1[1], p1[0]] if irange(p1) else constantp2_value = image[p2[1], p2[0]] if irange(p2) else constantp3_value = image[p3[1], p3[0]] if irange(p3) else constantdst[y, x] = p0_area * p0_value + p1_area * p1_value + p2_area * p2_value + p3_area * p3_value# 交換bgr rgb# normalize -> -mean /std# 1行代碼實現normalize , /255.0# bgr bgr bgr -> bbb ggg rrr# focus# focus offset, 1行代碼實現focusreturn dstcat1 = cv2.imread("cat1.png")
#acat1_cv, M, inv = align(cat1, (100, 100))
M = cv2.getRotationMatrix2D((0, 0), 30, 0.5)
acat1_cv = cv2.warpAffine(cat1, M, (100, 100))
acat1_py = pyWarpAffine(cat1, M, (100, 100))plt.figure(figsize=(10, 10))
plt.subplot(1, 2, 1)
plt.title("OpenCV")
plt.imshow(acat1_cv[..., ::-1])plt.subplot(1, 2, 2)
plt.title("PyWarpAffine")
plt.imshow(acat1_py[..., ::-1])