Deformable ConvNet簡介
關於Deformable Convolutional Networks的論文解讀,我們先講和Deformable ConvNets原理相似的Spatial Transformer Networks,而講STN之前,需要講解圖片處理中兩個重要的基本概念:仿射變換和雙線性插值。
故將這些內容分爲5個部分,本章是第一部分:
- [x] Part1: 快速學習實現仿射變換
- [ ] Part2: Spatial Transfomer Networks論文解讀
- [ ] Part3: TenosorFlow實現STN
- [ ] Part4: Deformable Convolutional Networks論文解讀
- [ ] Part5: TensorFlow實現Deformable ConvNets
主要包括仿射變換原理、論文STN解讀和實現、Deformable ConvNets論文解讀和實現。
本章講解線性變換到仿射變換,線性插值到雙線性插值,使用雙線性插值實現仿射變換,並給出代碼測試。
圖像變換
無論是STN還是Deformable ConvNets的原理都和仿射變換和雙線性插值相關。我們先講圖像變換中常見的線性變換和我們關注的仿射變換。後面再講雙線性插值。
線性變換(linear transformations)
考慮到如下定義:
點 的座標爲 代表一個 的列向量
矩陣 代表shape 的矩陣
我們通過 定義線性變換 。其中 內的 是可變參數。
恆等變換:
令 ,即 ,則:
放縮:
示意圖如下:
令 ,即 ,則:
isotropic scaling(同向異性)
,表示關於 的放縮方向是相同的;也有anisotropic(各向異性)
,放縮方向相反。
旋轉:
示意圖如下:
考慮到想旋轉 ,令 ,則 :
shear:
示意圖如下:
圖片需要對 做關於 的偏移,對 做關於 的偏移。例如:字體做斜體操作。令 ,即 則:
總結一下,這裏講了3個基本的線性變換:
- 放縮
- shear
- 旋轉
我們可將這三個變換矩陣表示爲 ,則變換可寫成:
其中 ,即用一個矩陣來表示各種線性變換。
仿射變換(Affine Transformation)
常見的圖像變換如下:
對於M爲 矩陣,可完成線性變換,將圖形扭曲成其他形狀。但這樣的變換存在一個缺點:不能做平移,故需要進一調整。
在計算圖像學中做平移操作過程如下:
可以看到是添加一個軸,再變換。對此將參數矩陣由2D換成3D:
- 點 變成了 的列向量
- 爲了表示變換,添加了兩個新參數,矩陣 變成了shape 的矩陣
注意到,我們需要2D的輸出,可將M改爲 卷積形式。
例如,做平移操作:
使用這樣一個技巧,可通過一個新的變換表示所有變換,這即是仿射變換,我們可以一般化結果,這4中變換使用放射矩陣表示:
總結來講就是:仿射變換=線性變換+平移功能
雙線性插值(Bilinear Interpolation)
考慮到當我們做仿射變換時:例如旋轉或放縮,圖片中的像素會移動到其他地方。這會暴露出一個問題,輸出中的像素位置可能沒有對應的輸入圖片中的位置。 下面的旋轉示例,可以清晰的看到輸出中有些點沒有在對應棋盤網格中央,這意味着輸入中沒有對應的像素點:
爲了支持這樣輸出是分數座標點的,可使用雙線性插值去尋找合適的顏色值。
線性插值
要說雙線性插值,先看看線性插值。 已知座標 和 ,需要在 之間 插值,如下:
兩點之間的線性方程爲:
變換一下上述公式即:
雙線性插值
雙線性插值是線性插值的拓展~
4個像素點座標爲 ,像素值爲 :
先是線性插值獲得 :
再使用 縱向插值得到 :
在像素計算中,通常是以4個相鄰的像素點做插值,故所有分母項都爲1,聯立 可得:
可以將公式化爲:
仿射變換代碼測試
前面講完了仿射變換和雙線性插值,我們整理一下:仿射變換是我們的目的,雙線性插值是幫助我們在圖像上實現仿射變換。
在寫代碼前,理一下在圖片上做仿射變換的思路,通常分爲3個步驟:
- 首先,創建採樣網格(sampling grid)。網格和輸入特徵映射相同空間大小的棋盤網格(標註各個座標點,用於存儲仿射變換後的座標點)
- 其次,將仿射轉換應用於上步生成的sampling grid,得到實際的採樣座標
- 最後,使用雙線性插值技術從原始圖片上按照實際採樣座標採樣得到最終結果
所有的代碼可從我的github上下載,使用Jupyter環境~
準備資源
從Github上把圖片clone下來,加載圖片並組成4D的張量,
- 導入包,注意
utils
是github上clone下來 - 圖片放在data目錄下,使用
img_to_array()
加載圖片爲numpy arrays - 將圖片按batch組合,同時處理多張圖片,擴充程序的擴展性
import numpy as np
from PIL import Image
from utils import img_to_array,array_to_img,visualize_grid,view_images
# params
DIMS = (400, 400)
CAT1 = 'cat1.jpg'
CAT2 = 'cat2.jpg'
data_path = './data/'
# 加載兩張小貓圖片
img1 = img_to_array(data_path + CAT1, DIMS)
img2 = img_to_array(data_path + CAT2, DIMS, view=True)
# 聯合兩張圖片到一個batch,shape爲(2, 400, 400, 3)
input_img = np.concatenate([img1, img2], axis=0)
print(img2.shape)
print("Input Img Shape: {}".format(input_img.shape))
輸出:
(1, 400, 400, 3)
Input Img Shape: (2, 400, 400, 3)
原圖:
構建仿射變換生成採樣矩陣
affine_grid_generator(height, width, M)
爲仿射變換函數:
height,width
:爲圖片長和寬M
: 仿射矩陣。 shape爲(num_batch, 2, 3).Return
: 返回sampling grid。shape爲(num_batch, H, W, 2)
def affine_grid_generator(height, width, M):
num_batch = M.shape[0] # 獲取batchsize
# 創建棋盤grid,平分整個圖片
x = np.linspace(-1, 1, width)
y = np.linspace(-1, 1, height)
x_t, y_t = np.meshgrid(x, y)
# 製作原始座標點的列向量(xt, yt, 1)
ones = np.ones(np.prod(x_t.shape))
sampling_grid = np.vstack([x_t.flatten(), y_t.flatten(), ones])
# 把所有像素整到一起 sampling_grid的shape爲(batch, 3, H*W)
sampling_grid = np.resize(sampling_grid, (num_batch, 3, height*width)) # 列向量擴展batch
# 做仿射矩陣運算 M*K
batch_grids = np.matmul(M, sampling_grid)
# batch grid 的 shape (num_batch, 2, H*W)
# 將仿射結果中H,W拆開
batch_grids = batch_grids.reshape(num_batch, 2, height, width)
batch_grids = np.moveaxis(batch_grids, 1, -1) #調整爲(num_batch, H, W, 2)
# sanity check
print("Transformation Matrices: {}".format(M.shape))
print("Sampling Grid: {}".format(sampling_grid.shape))
print("Batch Grids: {}".format(batch_grids.shape))
return batch_grids
雙線性插值函數
bilinear_sampler(input_img, x, y)
爲雙線性採樣:
input_img
:採樣的原始圖片,(B, H, W, C)x,y
: ` 仿射變換後對應的採樣點的x,y座標Return
:採樣後的值
def bilinear_sampler(input_img, x, y):
"""
如果想要測試是否正常,將仿射變換改爲恆等變換,查看是輸入和輸出
"""
# grab dimensions
B, H, W, C = input_img.shape
# 原本x,y在[-1,1]之間,現在放縮到 [0, W/H]
x = ((x + 1.) * W) * 0.5
y = ((y + 1.) * H) * 0.5
# 獲取到每個(x_i, y_i)相鄰的4個座標
x0 = np.floor(x).astype(np.int64)
x1 = x0 + 1
y0 = np.floor(y).astype(np.int64)
y1 = y0 + 1
# 確保不會超界 make sure it's inside img range [0, H] or [0, W]
x0 = np.clip(x0, 0, W-1)
x1 = np.clip(x1, 0, W-1)
y0 = np.clip(y0, 0, H-1)
y1 = np.clip(y1, 0, H-1)
# 取出對應的像素值
Ia = input_img[np.arange(B)[:,None,None], y0, x0]
Ib = input_img[np.arange(B)[:,None,None], y1, x0]
Ic = input_img[np.arange(B)[:,None,None], y0, x1]
Id = input_img[np.arange(B)[:,None,None], y1, x1]
# 依據雙線性插值公式,計算deltas
wa = (x1-x) * (y1-y)
wb = (x1-x) * (y-y0)
wc = (x-x0) * (y1-y)
wd = (x-x0) * (y-y0)
# add dimension for addition
wa = np.expand_dims(wa, axis=3)
wb = np.expand_dims(wb, axis=3)
wc = np.expand_dims(wc, axis=3)
wd = np.expand_dims(wd, axis=3)
# compute output
out = wa*Ia + wb*Ib + wc*Ic + wd*Id
return out
測試
設置仿射矩陣:
- 設置仿射矩陣的值,對應不同的仿射變換
- 調節仿射矩陣的batch維度
B, H, W, C = input_img.shape
# 設置仿射矩陣,依據上面的理論可自由調整
#M = np.array([[1., 0., 0.], [0., 1., 0.]]) 恆等變換
M = np.array([[1., 0., 0.5], [0., 1., 0.]]) #平移
#M = np.array([[0.707, -0.707, 0.], [0.707, 0.707, 0.]]) #旋轉
# 調整維度
M = np.resize(M, (B, 2, 3))
# print (M.shape) # (2, 2, 3)
獲取仿射矩陣採樣點,採樣:
- 獲取仿射矩陣生成的採樣點
- 將x,y維度拆分
- 使用雙線性插值採樣
batch_grids = affine_grid_generator(H, W, M)
x_s = batch_grids[:, :, :, 0:1].squeeze()
y_s = batch_grids[:, :, :, 1:2].squeeze()
out = bilinear_sampler(input_img, x_s, y_s)
out = array_to_img(out[-1])
out.show()
輸出:
Transformation Matrices: (2, 2, 3)
Sampling Grid: (2, 3, 160000)
Batch Grids: (2, 400, 400, 2)
原圖:
平移操作:
旋轉操作:
參考資料
PS:雙線性插值放縮圖片時參考座標
在實際的計算過程中,圖像的左上角是座標原點,我們記原始圖像大小爲 ,目標圖像大小爲 ,對應的長寬比例爲
原圖和目標圖之間的對齊方式有兩種:
- 原點對齊方式:
此時對於目標圖中的點 ,對應的原圖的爲
- 中心對齊方式:
此時對於目標圖中的點 ,對應的原圖的爲
對於中心對齊,相當於增加了一個 的偏移量,無論是放大還是縮小圖片,都會讓變換儘量的以中心對齊。
實際計算中由對齊方式計算出對應的座標點,然後套用前面的雙線性插值公式得到結果。