deformable convolution(可变形卷积)算法解析及代码分析

可变形卷积是指卷积核在每个元素上额外增长了一个参数方向参数,这样卷积核就能在训练过程当中扩展到很大的范围。git

可变形卷积的论文为:Deformable Convolutional Networks【1】github

而以前google一篇论文对这篇论文有指导意义:Spatial Transformer Networks【2】微信

论文【1】的github代码地址为https://github.com/felixlaumon/deform-convapp

——————————————————————————————————————————dom

可变形卷积很好理解,但如何实现呢?实现方面须要关注两个限制:函数

一、如何将它变成单独的一个层,而不影响别的层;this

二、在前向传播实现可变性卷积中,如何能有效地进行反向传播。google

这两个问题的答案分别是:spa

一、在实际操做时,并非真正地把卷积核进行扩展,而是对卷积前图片的像素从新整合,rest

变相地实现卷积核的扩张;

二、在图片像素整合时,须要对像素进行偏移操做,偏移量的生成会产生浮点数类型,

而偏移量又必须转换为整形,直接对偏移量取整的话没法进行反向传播,这时采用双线性差值的方式来获得对应的像素。

——————————————————————————————————————————

可变性卷积的流程为:

一、原始图片batch(大小为b*h*w*c),记为U,通过一个普通卷积,卷积填充为same,即输出输入大小不变,

对应的输出结果为(b*h*w*2c),记为V,输出的结果是指原图片batch中每一个像素的偏移量(x偏移与y偏移,所以为2c)。

二、将U中图片的像素索引值与V相加,获得偏移后的position(即在原始图片U中的坐标值),须要将position值限定为图片大小之内。

position的大小为(b*h*w*2c),但position只是一个坐标值,并且仍是float类型的,咱们须要这些float类型的坐标值获取像素。

三、例,取一个坐标值(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)的像素值,这里采用双线性差值的方式计算

(一方面获得的像素准确,另外一方面能够进行反向传播)。

四、在获得position的全部像素后,即获得了一个新图片M,将这个新图片M做为输入数据输入到别的层中,如普通卷积。

———————————————————————————————————————————

以上是可变性卷积的实现流程,但实际代码实现起来涉及到比较多的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])
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 
写的不详细,欢迎添加微信交流:

小弟不才,同时谢谢友情赞助: