空间变换网络STN
出自論文Spatial Transformer Networks?
Insight:
文章提出的STN的作用類似于傳統的矯正的作用。比如人臉識別中,需要先對檢測的圖片進行關鍵點檢測,然后使用關鍵點來進行對齊操作。但是這樣的一個過程是需要額外進行處理的。但是有了STN后,檢測完的人臉,直接就可以做對齊操作。關鍵的一點就是這個矯正過程是可以進行梯度傳導的。想象一下,人臉檢測完了,直接使用ROI pooling取出人臉的feature map,輸入STN就可以進行矯正,輸出矯正后的人臉。后面還可以再接點卷積操作,直接就可以進行分類,人臉識別的訓練。整個流程從理論上來說,都有梯度傳導,理論上可以將檢測+對齊+識別使用一個網絡實現。當然實際操作中可能會有各種trick。
空間變換基礎:
圖像的幾何變換包括透視變換和仿射變換,透視變換又稱為投影變換、投射變換、投影映射,透視變換是將圖片投影到一個新的視平面,它是二維(x,y)到三維(X,Y,Z)、再到另一個二維(x’,y’)空間的映射。
仿射變換又稱為圖像仿射映射,可以認為是透視變換的一種特殊情況,是透視變換的子集,仿射變換是從二維空間到自身的映射,是指在幾何中,一個向量空間進行一次線性變換并接上一個平移,變換為另一個向量空間,也就是圖像仿射變換等于圖像線性變換和平移的組合。
仿射變換包括平移(translation)、旋轉(rotation)、縮放(scaling)、錯切(shear )四種類型:
平移和旋轉兩者的組合不改變圖像的大小和形狀,只有圖像的位置(平移變換)和朝向(旋轉變換)發生改變,稱之為歐式變換(Euclidean transformation)或剛體變換(rigid transformation),剛性變換是最一般的變換
縮放又分為等比例縮放(uniform scaling)和非等比例縮放(non-uniform scaling),如果縮放系數為負數,則會疊加翻轉(reflection,又翻譯為反射、鏡像),因此翻轉可以看成是特殊的縮放
歐式變換和等比例縮放保持了圖像外觀沒有變形,因此二者的組合稱為相似變換(similarity transformation)
錯切是保持圖形上各點的某一坐標值不變,而另一坐標值關于該保持不變坐標值進行線性變換。坐標不變的軸稱為依賴軸,其余坐標軸稱為方向軸。錯切分為水平錯切和垂直錯切。
2D仿射變換(affine):
平移:
旋轉:
縮放:
整體:
3D透視變換(projection):
平移:
旋轉:
縮放:
Opencv函數:
1、getAffineTransform函數
getAffineTransform通過確認源圖像中不在同一直線的三個點對應的目標圖像的位置,來獲取對應仿射變換矩陣,從而用該仿射變換矩陣對圖像進行統一的仿射變換。
特別注意,getAffineTransform只支持非等比例縮放。也就是說x,y的縮放是不一樣的。但是有些場合可能需要等比例的縮放,比如換臉任務,需要保證長寬比不變。比如人臉識別任務,如果保證長寬比,效果會更佳。
調用語法
retval = cv.getAffineTransform(src, dst)
語法說明
src:源圖像中三角形頂點的坐標,也就是在源圖像中任找不在同一直線上的三個點,將三個點的坐標作為三個元素放到src對應列表中
dst:目標圖像中相應三角形頂點的坐標,也就是三個點在變換后圖像中的坐標列表,要求與源圖像三個點一一對應
retval返回值:從三對對應的點計算出來的仿射變換矩陣
2、warpAffine函數
在OpenCV中,仿射變換可以通過函數warpAffine來支持,當然部分單獨的函數也可以進行某個特定的變換,如縮放和旋轉就有單獨的變換函數。
調用語法
warpAffine(src, M, dsize, dst=None, flags=None, borderMode=None, borderValue=None)
語法說明
src:輸入圖像矩陣
M:2*3的仿射變換矩陣,可以自己構建,也可以通過getAffineTransform、getRotationMatrix2D等函數獲取
dsize:結果圖像大小,為寬和高的二元組
dst:輸出結果圖像,可以省略,結果圖像會作為函數處理結果輸出
flags:可選參數,插值方法的組合(int 類型),默認值 INTER_LINEAR
borderMode:可選參數,邊界像素模式(int 類型),默認值 BORDER_CONSTANT
borderValue:可選參數,邊界填充值,當borderMode為cv2.BORDER_CONSTANT時使用,默認值為None。另外當borderMode取值為cv2.BORDER_TRANSPARENT時,目標圖像中與源圖像中的離群值相對應的像素不被函數修改(關于離群值老猿暫還未完全弄明白,暫且存疑)
返回值:為仿射變換后的結果圖像矩陣,最后的結果矩陣每個像素與原圖像像素的對應關系為:
如果flags標記設置了WARP_INVERSE_MAP標記,首先使用invertAffineTransform對變換矩陣進行反轉即求其逆矩陣,然后將其放入上面的公式中,而不是將M直接放入。例如如果變換矩陣是順時針旋轉30°,則設置WARP_INVERSE_MAP標記的情況下實際變換效果是逆時針旋轉30°。這樣做的目的是為了已知目標圖像和變換方法的情況下,可以求出原圖像。所以這個標記很重要
返回值:仿射變換后的結果圖像
3、getRotationMatrix2D
getRotationMatrix2D用于獲取一個旋轉二維圖像的仿射變換矩陣。
調用語法
getRotationMatrix2D(center, angle, scale)
參數語法說明
center:圖像旋轉的旋轉參考中心點坐標二元組
angle:旋轉角度,坐標原點為左上角情況下正值表示逆時針旋轉
scale:等比例縮放因子
返回值
getRotationMatrix2D返回值為一2*3復合旋轉變換仿射矩陣。按照OpenCV官方介紹,getRotationMatrix2D得到的矩陣為:
?
繞指定點旋轉進行組合變換時,參考點p(m,n)順時針旋轉θ的組合變換的齊次坐標表示公式為:
上述公式中θ為正表示是順時針旋轉,與getRotationMatrix2D中的angle參數取值方式相反,由于cos(-θ)=cosθ,sin(-θ)=-sinθ,因此實際上getRotationMatrix2D中旋轉正值的角度時對應的上述矩陣計算公式中sin函數前面的符號需要取反(正號變副號、副號變正號)。
而縮放的齊次坐標表示公式為:
?
用縮放矩陣左乘平移矩陣則可以得到順時針旋轉同時進行縮放的齊次坐標表示公式:?
?
當等比例縮放且縮放因子等于s時,上述公式中的kx、ky使用s替換。則最后的組合變換矩陣為:?
?可以看到,將getRotationMatrix2D的參數angle(逆時針旋轉為正)的角度變為上述組合矩陣變換公式中的-θ(順時針旋轉為正)、getRotationMatrix2D中的center.x、center.y分別使用m、n替換,取組合變換矩陣的前2行,則二者結果等價。因此getRotationMatrix2D函數獲得的變換矩陣和上述組合變換矩陣連乘的結果相同。
代碼演示:
方法1,(基于opencv,非剛性變換):
#src_pts,np.array,shape[-1,2] #ref_pts,np.array,shape[-1,2] tfm = cv2.getAffineTransform(src_pts, ref_pts) #變換矩陣 tfm_inv = np.linalg.inv(np.vstack([tfm, [0, 0, 1]]))[0:2] #變換矩陣的逆矩陣 warped_image = cv2.warpAffine(image, tfm , (width, height)) #圖片變換five_points_tfmed = [] for value in five_points: #點變換five_points[[x1,y1],[x2,y2],[x3,y3],[x4,y4],[x5,y5]]tmp = np.ones((3,1),np.float32)tmp[0,0] = value[0]tmp[1,0] = value[1] aff_tmp = np.dot(tfm , tmp) five_points_tfmed.append([aff_tmp[0,0], aff_tmp[1,0]])方法2,(基于最小二乘法,非剛性變換):
def get_affine_transform_matrix(src_pts, dst_pts):tfm = np.float32([[1, 0, 0], [0, 1, 0]])n_pts = src_pts.shape[0]ones = np.ones((n_pts, 1), src_pts.dtype)src_pts_ = np.hstack([src_pts, ones])dst_pts_ = np.hstack([dst_pts, ones])A, res, rank, s = np.linalg.lstsq(src_pts_, dst_pts_)if rank == 3:tfm = np.float32([[A[0, 0], A[1, 0], A[2, 0]],[A[0, 1], A[1, 1], A[2, 1]]])elif rank == 2:tfm = np.float32([[A[0, 0], A[1, 0], 0],[A[0, 1], A[1, 1], 0]])tfm_inv = np.linalg.inv(np.vstack([tfm, [0, 0, 1]]))[0:2]return tfm, tfm_inv#調用 tfm, tfm_inv = get_affine_transform_matrix(src_pts, ref_pts)方法3,(剛性變換):
import numpy as np from numpy.linalg import inv, norm, lstsq from numpy.linalg import matrix_rank as rankclass MatlabCp2tormException(Exception):def __str__(self):return 'In File {}:{}'.format(__file__, super.__str__(self))def tformfwd(trans, uv):"""Function:----------apply affine transform 'trans' to uvParameters:----------@trans: 3x3 np.arraytransform matrix@uv: Kx2 np.arrayeach row is a pair of coordinates (x, y)Returns:----------@xy: Kx2 np.arrayeach row is a pair of transformed coordinates (x, y)"""uv = np.hstack((uv, np.ones((uv.shape[0], 1))))xy = np.dot(uv, trans)xy = xy[:, 0:-1]return xydef tforminv(trans, uv):"""Function:----------apply the inverse of affine transform 'trans' to uvParameters:----------@trans: 3x3 np.arraytransform matrix@uv: Kx2 np.arrayeach row is a pair of coordinates (x, y)Returns:----------@xy: Kx2 np.arrayeach row is a pair of inverse-transformed coordinates (x, y)"""Tinv = inv(trans)xy = tformfwd(Tinv, uv)return xydef findNonreflectiveSimilarity(uv, xy, options=None):options = {'K': 2}K = options['K']M = xy.shape[0]x = xy[:, 0].reshape((-1, 1)) # use reshape to keep a column vectory = xy[:, 1].reshape((-1, 1)) # use reshape to keep a column vector# print('--->x, y:\n', x, ytmp1 = np.hstack((x, y, np.ones((M, 1)), np.zeros((M, 1))))tmp2 = np.hstack((y, -x, np.zeros((M, 1)), np.ones((M, 1))))X = np.vstack((tmp1, tmp2))# print('--->X.shape: ', X.shape# print('X:\n', Xu = uv[:, 0].reshape((-1, 1)) # use reshape to keep a column vectorv = uv[:, 1].reshape((-1, 1)) # use reshape to keep a column vectorU = np.vstack((u, v))# print('--->U.shape: ', U.shape# print('U:\n', U# We know that X * r = Uif rank(X) >= 2 * K:r, _, _, _ = lstsq(X, U)r = np.squeeze(r)else:raise Exception('cp2tform:twoUniquePointsReq')# print('--->r:\n', rsc = r[0]ss = r[1]tx = r[2]ty = r[3]Tinv = np.array([[sc, -ss, 0],[ss, sc, 0],[tx, ty, 1]])# print('--->Tinv:\n', TinvT = inv(Tinv)# print('--->T:\n', TT[:, 2] = np.array([0, 0, 1])return T, Tinvdef findSimilarity(uv, xy, options=None):options = {'K': 2}# uv = np.array(uv) # xy = np.array(xy)# Solve for trans1trans1, trans1_inv = findNonreflectiveSimilarity(uv, xy, options)# Solve for trans2# manually reflect the xy data across the Y-axisxyR = xyxyR[:, 0] = -1 * xyR[:, 0]trans2r, trans2r_inv = findNonreflectiveSimilarity(uv, xyR, options)# manually reflect the tform to undo the reflection done on xyRTreflectY = np.array([[-1, 0, 0],[0, 1, 0],[0, 0, 1]])trans2 = np.dot(trans2r, TreflectY)# Figure out if trans1 or trans2 is betterxy1 = tformfwd(trans1, uv)norm1 = norm(xy1 - xy)xy2 = tformfwd(trans2, uv)norm2 = norm(xy2 - xy)if norm1 <= norm2:return trans1, trans1_invelse:trans2_inv = inv(trans2)return trans2, trans2_invdef get_similarity_transform(src_pts, dst_pts, reflective=True):"""Function:----------Find Similarity Transform Matrix 'trans':u = src_pts[:, 0]v = src_pts[:, 1]x = dst_pts[:, 0]y = dst_pts[:, 1][x, y, 1] = [u, v, 1] * transParameters:----------@src_pts: Kx2 np.arraysource points, each row is a pair of coordinates (x, y)@dst_pts: Kx2 np.arraydestination points, each row is a pair of transformedcoordinates (x, y)@reflective: True or Falseif True:use reflective similarity transformelse:use non-reflective similarity transformReturns:----------@trans: 3x3 np.arraytransform matrix from uv to xytrans_inv: 3x3 np.arrayinverse of trans, transform matrix from xy to uv"""if reflective:trans, trans_inv = findSimilarity(src_pts, dst_pts)else:trans, trans_inv = findNonreflectiveSimilarity(src_pts, dst_pts)return trans, trans_invdef cvt_tform_mat_for_cv2(trans):"""Function:----------Convert Transform Matrix 'trans' into 'cv2_trans' which could bedirectly used by cv2.warpAffine():u = src_pts[:, 0]v = src_pts[:, 1]x = dst_pts[:, 0]y = dst_pts[:, 1][x, y].T = cv_trans * [u, v, 1].TParameters:----------@trans: 3x3 np.arraytransform matrix from uv to xyReturns:----------@cv2_trans: 2x3 np.arraytransform matrix from src_pts to dst_pts, could be directly usedfor cv2.warpAffine()"""cv2_trans = trans[:, 0:2].Treturn cv2_transdef get_similarity_transform_for_cv2(src_pts, dst_pts, reflective=True):"""Function:----------Find Similarity Transform Matrix 'cv2_trans' which could bedirectly used by cv2.warpAffine():u = src_pts[:, 0]v = src_pts[:, 1]x = dst_pts[:, 0]y = dst_pts[:, 1][x, y].T = cv_trans * [u, v, 1].TParameters:----------@src_pts: Kx2 np.arraysource points, each row is a pair of coordinates (x, y)@dst_pts: Kx2 np.arraydestination points, each row is a pair of transformedcoordinates (x, y)reflective: True or Falseif True:use reflective similarity transformelse:use non-reflective similarity transformReturns:----------@cv2_trans: 2x3 np.arraytransform matrix from src_pts to dst_pts, could be directly usedfor cv2.warpAffine()"""trans, trans_inv = get_similarity_transform(src_pts, dst_pts, reflective)cv2_trans = cvt_tform_mat_for_cv2(trans)cv2_trans_inv = cvt_tform_mat_for_cv2(trans_inv)return cv2_trans, cv2_trans_invif __name__ == '__main__':"""u = [0, 6, -2]v = [0, 3, 5]x = [-1, 0, 4]y = [-1, -10, 4]# In Matlab, run:## uv = [u'; v'];# xy = [x'; y'];# tform_sim=cp2tform(uv,xy,'similarity');## trans = tform_sim.tdata.T# ans =# -0.0764 -1.6190 0# 1.6190 -0.0764 0# -3.2156 0.0290 1.0000# trans_inv = tform_sim.tdata.Tinv# ans =## -0.0291 0.6163 0# -0.6163 -0.0291 0# -0.0756 1.9826 1.0000# xy_m=tformfwd(tform_sim, u,v)## xy_m =## -3.2156 0.0290# 1.1833 -9.9143# 5.0323 2.8853# uv_m=tforminv(tform_sim, x,y)## uv_m =## 0.5698 1.3953# 6.0872 2.2733# -2.6570 4.3314"""u = [0, 6, -2]v = [0, 3, 5]x = [-1, 0, 4]y = [-1, -10, 4]uv = np.array((u, v)).Txy = np.array((x, y)).Tprint('\n--->uv:')print(uv)print('\n--->xy:')print(xy)trans, trans_inv = get_similarity_transform(uv, xy)print('\n--->trans matrix:')print(trans)print('\n--->trans_inv matrix:')print(trans_inv)print('\n---> apply transform to uv')print('\nxy_m = uv_augmented * trans')uv_aug = np.hstack((uv, np.ones((uv.shape[0], 1))))xy_m = np.dot(uv_aug, trans)print(xy_m)print('\nxy_m = tformfwd(trans, uv)')xy_m = tformfwd(trans, uv)print(xy_m)print('\n---> apply inverse transform to xy')print('\nuv_m = xy_augmented * trans_inv')xy_aug = np.hstack((xy, np.ones((xy.shape[0], 1))))uv_m = np.dot(xy_aug, trans_inv)print(uv_m)print('\nuv_m = tformfwd(trans_inv, xy)')uv_m = tformfwd(trans_inv, xy)print(uv_m)uv_m = tforminv(trans, xy)print('\nuv_m = tforminv(trans, xy)')print(uv_m)#調用 tfm, tfm_inv = get_similarity_transform_for_cv2(src_pts, ref_pts)STN網絡結構:
STN網絡由Localisation Network ,Grid generator,Sampler,3個部分組成。
Localisation Network:
該網絡就是一個簡單的回歸網絡。將輸入的圖片進行幾個卷積操作,然后全連接回歸出6個角度值(假設是仿射變換),2*3的矩陣。
Grid generator:
網格生成器負責將V中的坐標位置,通過矩陣運算,計算出目標圖V中的每個位置對應原圖U中的坐標位置。即生成T(G)。
這里的Grid采樣過程,對于二維仿射變換(旋轉,平移,縮放)來說,就是簡單的矩陣運算。
上式中,s代表原始圖的坐標,t代表目標圖的坐標。A為Localisation Network網絡回歸出的6個角度值。
整個Grid生成過程就是,首先你需要想象上圖中V-FeatureMap中全是白色或者全是黑色,是沒有像素信息的。也就是說V-FeatureMap還不存在,有的只是V-FeatureMap的坐標位置信息。然后將目標圖V-FeatureMap中的比如(0,0)(0,1)......位置的坐標,與2*3變換矩陣運算。就會生成出在原始圖中對應的坐標信息,比如(5,0)(5,1)......。這樣所有的目標圖的坐標都經過這樣的運算就會將每個坐標都產生一個與之對應的原圖的坐標,即T(G)。然后通過T(G)和原始圖U-FeatureMap的像素,將原始圖中的像素復制到V-FeatureMap中,從而生成目標圖的像素。
Sampler:
采樣器根據T(G)中的坐標信息,在原始圖U中進行采樣,將U中的像素復制到目標圖V中。
實驗結果:
作者分別在MNIST,Street View House Numbers ,CUB-200-2011 birds dataset 這3個數據集上做了實驗。
MNIST實驗:
R:rotation (旋轉)
RTS:rotation, scale and translation (旋轉,縮放,平移)
P:projective transformation (投影)
E:elastic warping (彈性變形)
從作圖可以看出,FCN 錯誤率為13.2% , CNN 錯誤率為3.5% , 與之對比的 ST-FCN 錯誤率為2.0% ,ST-CNN 錯誤率為 1.7%??梢钥闯鯯TN的效果還是非常明顯的。
Street View House Numbers實驗:
可以看出不管是64像素還是128像素,ST-CNN比傳統的CNN錯誤率要低一些。
CUB-200-2011 birds dataset實驗:
右圖紅色框檢測頭部,綠色框檢測身體。
這個數據集是屬于細粒度分類的一個數據集。好多做細粒度分類的文章都會在該數據集上做實驗。從這個實驗可以看出,STN可以有attention的效果,可以訓練的更加關注ROI區域。
實驗結果有0.8個百分點的提升。
References:
https://github.com/kevinzakka/spatial-transformer-network
總結
 
                            
                        - 上一篇: 常见的nlp 自然语言处理模型
- 下一篇: 空间变换网络简单介绍
