Deformable ConvNets v1:
論文地址:https://github.com/chengdazhi/Deformable-Convolution-V2-PyTorch
工程地址:https://github.com/felixlaumon/deform-conv
論文地址: Deformable ConvNets v2: More Deformable, Better Results
工程地址:https://github.com/chengdazhi/Deformable-Convolution-V2-PyTorch
這里需要強調一下,N代表kernel_size*kernel_size,並不是通道數
offset = self.p_conv(x)
x.shape torch.Size([32,64,28,28])
offset.shape torch.Size([32,18,28,28])
這一步輸入圖片學習偏移量offset,輸入的feature map尺寸是(64,28,28)分別表示通道數,h和w。輸出之所以是18通道的,是因為kernel size是3,卷積核有33=9個點,每個點都有x偏移量和y偏移量(從這里看得出每個輸入通道做同樣的偏移處理),18通道的前九個通道表示x偏移量,后9個通道表示y偏移量。然后再經過一次卷積
對應的看上圖下面那個,可以看到原來是方形33的,每個小塊再加了一點點偏移,卷積。根據代碼,應該是每個channel都用同一個偏移量,原始輸入的feature map每個點應該都有一個3*3的滑框,再加上對應的一個18個偏移量。每個點對應的偏移都不一樣。
以下轉摘於:https://blog.csdn.net/mykeylock/article/details/77746499
可變形卷積是指卷積核在每一個元素上額外增加了一個參數方向參數,這樣卷積核就能在訓練過程中擴展到很大的范圍。
可變形卷積的論文為:Deformable Convolutional Networks【1】
而之前google一篇論文對這篇論文有指導意義:Spatial Transformer Networks【2】
論文【1】的github代碼地址為https://github.com/felixlaumon/deform-conv
——————————————————————————————————————————
可變形卷積很好理解,但如何實現呢?實現方面需要關注兩個限制:
1、如何將它變成單獨的一個層,而不影響別的層;
2、在前向傳播實現可變性卷積中,如何能有效地進行反向傳播。
這兩個問題的答案分別是:
1、在實際操作時,並不是真正地把卷積核進行擴展,而是對卷積前圖片的像素重新整合,
變相地實現卷積核的擴張;
2、在圖片像素整合時,需要對像素進行偏移操作,偏移量的生成會產生浮點數類型,
而偏移量又必須轉換為整形,直接對偏移量取整的話無法進行反向傳播,這時采用雙線性差值的方式來得到對應的像素。
——————————————————————————————————————————
可變性卷積的流程為:
1、原始圖片batch(大小為bhwc),記為U,經過一個普通卷積,卷積填充為same,即輸出輸入大小不變,
對應的輸出結果為(bhw2c),記為V,輸出的結果是指原圖片batch中每個像素的偏移量(x偏移與y偏移,因此為2c)。
2、將U中圖片的像素索引值與V相加,得到偏移后的position(即在原始圖片U中的坐標值),需要將position值限定為圖片大小以內。
position的大小為(bhw*2c),但position只是一個坐標值,而且還是float類型的,我們需要這些float類型的坐標值獲取像素。
3、例,取一個坐標值(a,b),將其轉換為四個整數,floor(a), ceil(a), floor(b), ceil(b),將這四個整數進行整合,
得到四對坐標(floor(a),floor(b)), ((floor(a),ceil(b)), ((ceil(a),floor(b)), ((ceil(a),ceil(b))。這四對坐標每個坐標都對應U
中的一個像素值,而我們需要得到(a,b)的像素值,這里采用雙線性差值的方式計算
(一方面得到的像素准確,另一方面可以進行反向傳播)。
4、在得到position的所有像素后,即得到了一個新圖片M,將這個新圖片M作為輸入數據輸入到別的層中,如普通卷積。
——————————————————————————————————————————
https://blog.csdn.net/scut_salmon/article/details/97050908
代碼似乎也是按照這個流程寫的,不過上面的輸出結果不應該是(b*h*w*2c),而是(b*h*w* (2kernel_size*kernel_size)),因為只需要記錄可變形卷積濾波器變形之后的位置,
濾波器的原始尺寸是kernel_size*kernel_size,需記錄x坐標和y坐標,故為2*kernel_size*kernel_size
offset = self.p_conv(x)
#x.shape torch.Size([32,64,28,28])
#offset.shape torch.Size([32,18,28,28])
這一步輸入圖片學習偏移量offset,輸入的feature map尺寸是(64,28,28)分別表示通道數,h和w。輸出之所以是18通道的,是因為kernel size是3,卷積核有3*3=9個點,每個點都有x偏移量和y偏移量(從這里看得出每個輸入通道做同樣的偏移處理),18通道的前九個通道表示x偏移量,后9個通道表示y偏移量。
以上是可變性卷積的實現流程,但實際代碼實現起來涉及到比較多的tensor操作,比較繁瑣。
代碼實現主要的文件有
cnn.py:采用keras定義了所有訓練需要的層,可變形卷積層為ConvOffset2D,
layer.py:定義了ConvOffset2D可變形卷積類,主要包括keras中需要的call函數與init函數,
call函數首先調用普通卷積,然后調用deform_conv.py中函數實際計算。
deform_conv.py:真正實現可變形卷積計算的文件。
layer.py主要代碼:
def __init__(self, filters, init_normal_stddev=0.01, **kwargs):
self.filters = filters
super(ConvOffset2D, self).__init__(
self.filters * 2, (3, 3), padding='same', use_bias=False,
kernel_initializer=RandomNormal(0, init_normal_stddev),
**kwargs
)
def call(self, x):
"""Return the deformed featured map"""
#獲取x大小,x大小為(b,h,w,c),分別為batch_size,圖片高度,圖片寬度,特征圖大小
x_shape = x.get_shape()
#調用普通卷積獲得輸出,輸出結果為(b,h,w,2c)表示圖片中每個像素需要偏移的量(x,y)
offsets = super(ConvOffset2D, self).call(x)
#reshape一下輸出,方便后續操作,(b*c,h,w,2)表示共有b*c個圖片,每個圖片為h*w大小,每個像素對應2個方向
# offsets: (b*c, h, w, 2)
offsets = self._to_bc_h_w_2(offsets, x_shape)
#將原始輸入也重新reshape一下方便后續操作
# x: (b*c, h, w)
x = self._to_bc_h_w(x, x_shape)
#調用deform_conv.py中的函數根據原始圖片與偏移量生成新圖片數據。
# X_offset: (b*c, h, w)
x_offset = tf_batch_map_offsets(x, offsets)
# x_offset: (b, h, w, c)
x_offset = self._to_b_h_w_c(x_offset, x_shape)
return x_offset
def compute_output_shape(self, input_shape):
"""Output shape is the same as input shape
Because this layer does only the deformation part
"""
return input_shape
@staticmethod
def _to_bc_h_w_2(x, x_shape):
"""(b, h, w, 2c) -> (b*c, h, w, 2)"""
x = tf.transpose(x, [0, 3, 1, 2])
x = tf.reshape(x, (-1, int(x_shape[1]), int(x_shape[2]), 2))
return x
@staticmethod
def _to_bc_h_w(x, x_shape):
"""(b, h, w, c) -> (b*c, h, w)"""
x = tf.transpose(x, [0, 3, 1, 2])
x = tf.reshape(x, (-1, int(x_shape[1]), int(x_shape[2])))
return x
@staticmethod
def _to_b_h_w_c(x, x_shape):
"""(b*c, h, w) -> (b, h, w, c)"""
x = tf.reshape(
x, (-1, int(x_shape[3]), int(x_shape[1]), int(x_shape[2]))
)
x = tf.transpose(x, [0, 2, 3, 1])
return x
deform_conv.py主要代碼:
def tf_flatten(a):
"""Flatten tensor"""
return tf.reshape(a, [-1])每個點都有x偏移量和y偏移量(從這里看得出每個輸入通道做同樣的偏移處理),18通道的前九個通道表示x偏移量,后9個通道表示y偏移量。
def tf_repeat(a, repeats, axis=0):
"""TensorFlow version of np.repeat for 1D"""
# https://github.com/tensorflow/tensorflow/issues/8521
assert len(a.get_shape()) == 1
a = tf.expand_dims(a, -1)
a = tf.tile(a, [1, repeats])
a = tf_flatten(a)
return a
def tf_repeat_2d(a, repeats):
"""Tensorflow version of np.repeat for 2D"""
assert len(a.get_shape()) == 2
a = tf.expand_dims(a, 0)
a = tf.tile(a, [repeats, 1, 1])
return a
def tf_map_coordinates(input, coords, order=1):
"""Tensorflow verion of scipy.ndimage.map_coordinates
Note that coords is transposed and only 2D is supported
Parameters
----------
input : tf.Tensor. shape = (s, s)
coords : tf.Tensor. shape = (n_points, 2)
"""
assert order == 1
coords_lt = tf.cast(tf.floor(coords), 'int32')
coords_rb = tf.cast(tf.ceil(coords), 'int32')
coords_lb = tf.stack([coords_lt[:, 0], coords_rb[:, 1]], axis=1)
coords_rt = tf.stack([coords_rb[:, 0], coords_lt[:, 1]], axis=1)
vals_lt = tf.gather_nd(input, coords_lt)
vals_rb = tf.gather_nd(input, coords_rb)
vals_lb = tf.gather_nd(input, coords_lb)
vals_rt = tf.gather_nd(input, coords_rt)
coords_offset_lt = coords - tf.cast(coords_lt, 'float32')
vals_t = vals_lt + (vals_rt - vals_lt) * coords_offset_lt[:, 0]
vals_b = vals_lb + (vals_rb - vals_lb) * coords_offset_lt[:, 0]
mapped_vals = vals_t + (vals_b - vals_t) * coords_offset_lt[:, 1]
return mapped_vals
def sp_batch_map_coordinates(inputs, coords):
"""Reference implementation for batch_map_coordinates"""
coords = coords.clip(0, inputs.shape[1] - 1)
mapped_vals = np.array([
sp_map_coordinates(input, coord.T, mode='nearest', order=1)
for input, coord in zip(inputs, coords)
])
return mapped_vals
def tf_batch_map_coordinates(input, coords, order=1):
"""Batch version of tf_map_coordinates
Only supports 2D feature maps
Parameters
----------
input : tf.Tensor. shape = (b, s, s)
coords : tf.Tensor. shape = (b, n_points, 2)
Returns
-------
tf.Tensor. shape = (b, s, s)
"""
input_shape = tf.shape(input)
batch_size = input_shape[0]
input_size = input_shape[1]
n_coords = tf.shape(coords)[1]
coords = tf.clip_by_value(coords, 0, tf.cast(input_size, 'float32') - 1)
#得到目標坐標左上角(left top)的整數坐標
coords_lt = tf.cast(tf.floor(coords), 'int32')
#得到又下角的整數坐標
coords_rb = tf.cast(tf.ceil(coords), 'int32')
#得到左下角的整數坐標
coords_lb = tf.stack([coords_lt[..., 0], coords_rb[..., 1]], axis=-1)
#得到右上角的整數坐標
coords_rt = tf.stack([coords_rb[..., 0], coords_lt[..., 1]], axis=-1)
#idx為索引展開,idx大小為(b*c*h*w),形如(0,0,0,0,0,1,1,1,1,1,2,2,2,2,2,3,3,3,3,3)
#b*c為5,h*w為4,總數為所有圖片所有坐標總數
idx = tf_repeat(tf.range(batch_size), n_coords)
def _get_vals_by_coords(input, coords):
#stack完后,每一個點表示一個坐標
#形如
#(0,0,0,0,0,1,1,1,1,1,2,2,2,2,2,3,3,3,3,3)
# (3,2,1,2,3,1,2,3,0,0,0,3,2,1,1,2,3,2,0,0,2)
# (3,2,1,0,0,2,0,3,1,2,3,0,0,2,3,0,1,2,0,2,3)
indices = tf.stack([
idx, tf_flatten(coords[..., 0]), tf_flatten(coords[..., 1])
], axis=-1)
vals = tf.gather_nd(input, indices)
vals = tf.reshape(vals, (batch_size, n_coords))
return vals
#以下為分別得到左上,左下,右上,右下四個點的像素值。
vals_lt = _get_vals_by_coords(input, coords_lt)
vals_rb = _get_vals_by_coords(input, coords_rb)
vals_lb = _get_vals_by_coords(input, coords_lb)
vals_rt = _get_vals_by_coords(input, coords_rt)
#用雙線性插值得到像素值。
coords_offset_lt = coords - tf.cast(coords_lt, 'float32')
vals_t = vals_lt + (vals_rt - vals_lt) * coords_offset_lt[..., 0]
vals_b = vals_lb + (vals_rb - vals_lb) * coords_offset_lt[..., 0]
mapped_vals = vals_t + (vals_b - vals_t) * coords_offset_lt[..., 1]
return mapped_vals
def sp_batch_map_offsets(input, offsets):
"""Reference implementation for tf_batch_map_offsets"""
batch_size = input.shape[0]
input_size = input.shape[1]
#生成grid,grid表示將一個圖片的所有坐標變成兩列,每一行兩個元素表示x,y
(grid的最后大小為(b*c,h*w,2)
offsets = offsets.reshape(batch_size, -1, 2)
grid = np.stack(np.mgrid[:input_size, :input_size], -1).reshape(-1, 2)
grid = np.repeat([grid], batch_size, axis=0)
#將原始坐標與坐標偏移量相加,得到目標坐標,coords的大小為(b*c,h*w,2)
coords = offsets + grid
#目標坐標需要在圖片最大坐標范圍內,將目標坐標進行切割限制
coords = coords.clip(0, input_size - 1)
#根據原始輸入與目標坐標得到像素。
mapped_vals = sp_batch_map_coordinates(input, coords)
return mapped_vals
def tf_batch_map_offsets(input, offsets, order=1):
"""Batch map offsets into input
Parameters
---------
input : tf.Tensor. shape = (b, s, s)
offsets: tf.Tensor. shape = (b, s, s, 2)
Returns
-------
tf.Tensor. shape = (b, s, s)
"""
input_shape = tf.shape(input)
batch_size = input_shape[0]
input_size = input_shape[1]
offsets = tf.reshape(offsets, (batch_size, -1, 2))
grid = tf.meshgrid(
tf.range(input_size), tf.range(input_size), indexing='ij'
)
grid = tf.stack(grid, axis=-1)
grid = tf.cast(grid, 'float32')
grid = tf.reshape(grid, (-1, 2))
grid = tf_repeat_2d(grid, batch_size)
coords = offsets + grid
mapped_vals = tf_batch_map_coordinates(input, coords)
return mapped_vals