目录
基本原理
代码解读
Offsets转换
双线性插值
参考
本文主要从代码的角度解析下deformable convolution的原理,因为在看代码的时候各种transpose、stack、tile、reshape操作,看了一会维度就搞晕了,因此将过程注释一下方便理解,本文以keras实现GitHub - kastnerkyle/deform-conv: Deformable Convolution in TensorFlow / Keras代码为例。
对于一个3*3的卷积核,当滑动到feature map图(a)中绿点所示位置时,与这9个像素点进行卷积。Deformable Convolution如图(b)所示,其过程如下:
这里引用参考博客里提到的两点
如何将它变成单独的一个层,而不影响别的层。
答:在实际操作时,并不是真正地把卷积核进行扩展,而是对卷积前图片的像素重新整合,变相地实现卷积核的扩张。
Deformable conv到底学了什么?
答:它不是学习了offsets!而是学习了根据不同特征,提取该特征所需要的相应的offsets的能力。这个能力其实就是,上面分支的“conv”的卷积层的参数。这就是deformable的强大之处,比如,一条狗站着和趴着,当deformable卷积学习时,看到了趴着的狗,上面的分支的conv可以将这个feature作为输入,然后输出“趴着的狗”所需要的offsets,这时候的offsets基本是让下面的分支的卷积的采样点呈现“扁平框”的样式。当然了,如果是看着了“站着的狗”,offsets就会让下面分支的卷积的采样点呈现“更加四四方方点了”。
cnn.py中的第48行第一次调用ConvOffset2D这个类(因为对代码进行了reformat以及注释,下面提到的具体行数不一定和原始代码一致)
l_offset = ConvOffset2D(32, name='conv12_offset')(l)
在layers.py中定义了ConvOffset2D类
class ConvOffset2D(Conv2D):
"""ConvOffset2D"""
def __init__(self, filters, init_normal_stddev=0.01, **kwargs):
"""Init"""
self.filters = filters
super(ConvOffset2D, self).__init__(
self.filters * 2, (3, 3), padding='same', use_bias=False,
# TODO gradients are near zero if init is zeros
kernel_initializer='zeros',
# kernel_initializer=RandomNormal(0, init_normal_stddev),
**kwargs
)
def call(self, x):
# TODO offsets probably have no nonlinearity?
x_shape = x.get_shape() # (b,h,w,c)
offsets = super(ConvOffset2D, self).call(x) # (b,h,w,2c)
offsets = self._to_bc_h_w_2(offsets, x_shape) # (b*c,h,w,2)
x = self._to_bc_h_w(x, x_shape) # (b*c,h,w)
x_offset = tf_batch_map_offsets(x, offsets) # (b*c,h*w)
x_offset = self._to_b_h_w_c(x_offset, x_shape) # (b,h,w,c)
return x_offset
首先call中得到输入x的shape为(b,h,w,c),分别表示输入feature map x的batch size、height、width、channel。__init__中重新初始化了继承的Conv2D类,将输入卷积核数乘以2,因为每个像素点都会得到xy两个方向的偏移,call中第二行得到offsets。第三四行分别对输入x和offset进行transpose、reshape,得到注释所示的shape。
tf_batch_map_offsets是根据前面得到的offsets对输入x中的每个像素位置进行转换,并通过双线性插值得到转换后位置的值,代码如下
def tf_batch_map_offsets(input, offsets, order=1):
"""Batch map offsets into input
Parameters
---------
input : tf.Tensor. shape = (b, s, s) # (b*c,h,w)
offsets: tf.Tensor. shape = (b, s, s, 2) # (b*c,h,w,2)
"""
input_shape = tf.shape(input) # (b*c,h,w)
batch_size = input_shape[0] # b*c
input_size = input_shape[1] # h
offsets = tf.reshape(offsets, (batch_size, -1, 2)) # (b*c,h*w,2)
grid = tf.meshgrid(
tf.range(input_size), tf.range(input_size), indexing='ij'
) # list, len=2, grid[0].shape=(input_size,input_size)
# list中的两个tensor的shape都是h*w,分别为每一点的纵坐标和横坐标
grid = tf.stack(grid, axis=-1) # (h,w,2) 先按行后按列,每一点的yx坐标
grid = tf.cast(grid, 'float32')
grid = tf.reshape(grid, (-1, 2)) # (h*w,2) 顺序也是第一行从左到右、然后第二行...
grid = tf_repeat_2d(grid, batch_size) # (batch_size, h*w, 2)
coords = offsets + grid # grid是feature_map中每一点相对左上原点的实际坐标
mapped_vals = tf_batch_map_coordinates(input, coords) # (b*c,h*w)
return mapped_vals
这里input_size就是输入特征图的宽高,(这里假设输入特征图是正方形),假设input_size=4,第五行tf.meshgrid的输出如下
[, ]
输出是一个list,len(list)=2,两个元素都是shape=(4,4)的数组,其中分别为每个位置的y和x坐标。
第六行tf.stack输出如下
tf.Tensor(
[[[0 0]
[0 1]
[0 2]
[0 3]]
[[1 0]
[1 1]
[1 2]
[1 3]]
[[2 0]
[2 1]
[2 2]
[2 3]]
[[3 0]
[3 1]
[3 2]
[3 3]]], shape=(4, 4, 2), dtype=int32)
输出shape=(4,4,2),按照先行后列的顺序,每一点的yx坐标
第八行tf.reshape输出如下
tf.Tensor(
[[0. 0.]
[0. 1.]
[0. 2.]
[0. 3.]
[1. 0.]
[1. 1.]
[1. 2.]
[1. 3.]
[2. 0.]
[2. 1.]
[2. 2.]
[2. 3.]
[3. 0.]
[3. 1.]
[3. 2.]
[3. 3.]], shape=(16, 2), dtype=float32)
第九行tf_repeat_2d将grid坐标repeat batch_size个,这里的batch_size实际上原来的batch_size * channel,假设这里的batch_size=2,得到输出
然后加上offsets就得到了偏移后的坐标。
接下来函数tf_batch_map_coordinates就是通过双线性插值得到偏移后坐标位置的像素值
首先看下线性插值,已知两点 (\(x_{0}\), \(y_{0}\)) 与 (\(x_{1}\), \(y_{1}\)),要计算 [\(x_{0}\), \(y_{0}\)] 区间内某一位置 x 在直线上的y值(或某一位置y在直线上的x值)用x和 \(x_{0},x_{1}\) 的距离作为一个权重,用于 \(y_{0}\) 和 \(y_{1}\) 的加权
双线性插值本质上就是在两个方向上做线性插值。如图所示,x(p)的浮点坐标为(i+u, j+v), (其中i、j均为浮点坐标的整数部分,u、v为浮点坐标的小数部分,是取值[0,1)区间的浮点数),则这个点的像素值f(i+u, j+v) 可由可由坐标i、j、u、v以及像素值x(q1)、x(q2)、x(q3)、x(q4)确定
x(p)的计算公式如下
t1 = (1-u)*x(q1) + u*x(q2)
t2 = (1-u)*x(q3) + u*x(q4)
x(p) = (1-v)*t1 + v*t2
函数tf_batch_map_coordinates如下
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) (b*c,h,w)
coords : tf.Tensor. shape = (b, n_points, 2) (b*c,h*w,2)
"""
input_shape = tf.shape(input) # (b*c,h,w)
batch_size = input_shape[0] # b*c
input_size = input_shape[1] # h
n_coords = tf.shape(coords)[1] # h*w
coords = tf.clip_by_value(coords, 0, tf.cast(input_size, 'float32') - 1)
coords_lt = tf.cast(tf.floor(coords), 'int32') # (b*c,h*w,2)
coords_rb = tf.cast(tf.ceil(coords), 'int32') # (b*c,h*w,2)
coords_lb = tf.stack([coords_lt[..., 0], coords_rb[..., 1]], axis=-1) # (b*c,h*w,2)
coords_rt = tf.stack([coords_rb[..., 0], coords_lt[..., 1]], axis=-1) # (b*c,h*w,2)
idx = tf_repeat(tf.range(batch_size), n_coords) # (b*c*h*w,) 即h*w个0,h*w个1,h*w个2... 一直到h*w个b*c-1
def _get_vals_by_coords(input, coords):
indices = tf.stack([
idx, tf_flatten(coords[..., 0]), tf_flatten(coords[..., 1])
], axis=-1) # (b*c*h*w, 3)
vals = tf.gather_nd(input, indices) # (b*c,h,w), (b*c*h*w, 3) -> (b*c*h*w)
vals = tf.reshape(vals, (batch_size, n_coords)) # (b*c,h*w)
return vals
vals_lt = _get_vals_by_coords(input, coords_lt) # x(q1) (b*c,h,w),(b*c,h*w,2)->(b*c,h*w)
vals_rb = _get_vals_by_coords(input, coords_rb) # x(q4)
vals_lb = _get_vals_by_coords(input, coords_lb) # x(q3)
vals_rt = _get_vals_by_coords(input, coords_rt) # x(q2)
coords_offset_lt = coords - tf.cast(coords_lt, 'float32') # (b*c,h*w,2)-(b*c,h*w,2)=(b*c,h*w,2)
# coords_offset_lt[...,0] = u, coords_offset_lt[...,1] = v
vals_t = vals_lt + (vals_rt - vals_lt) * coords_offset_lt[..., 0]
# t1 = vals_t = (1-u) * vals_lt + u * vals_rt = vals_lt + (vals_rt - vals_lt) * u = vals_lt + (vals_rt - vals_lt) * coords_offset_lt[..., 0]
vals_b = vals_lb + (vals_rb - vals_lb) * coords_offset_lt[..., 0]
# t2 = vals_b = (1-u) * vals_lb + u *vals_rb = vals_lb + (vals_rb - vals_lb) * u = vals_lb + (vals_rb - vals_lb) * coords_offset_lt[..., 0]
mapped_vals = vals_t + (vals_b - vals_t) * coords_offset_lt[..., 1]
# x(p) = mapped_vals = (1-v) * t1 + v * t2 = t1 + (t2 - t1) * v = vals_t + (vals_b - vals_t) * coords_offset_lt[..., 1]
return mapped_vals # (b*c,h*w)
Deformable Convolutional Networks解读_爆米花好美啊的博客-CSDN博客
deformable convolution(可变形卷积)算法解析及代码分析_mykeylock的博客-CSDN博客_可变形卷积代码
Deformable Conv及其变体_Hungryof的博客-CSDN博客