Deformable Convolution(可变形卷积)代码解析(有错误,修改中)

目录

基本原理

代码解读

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)所示,其过程如下:

  1. 首先通过一层卷积得到offset,即图中的蓝色箭头,箭头的大小和方向可以通过xy方向的偏移获得
  2. 原位置即绿点加上offset得到偏移后的蓝点位置
  3. 通过双线性插值得到蓝点的值
  4. 3*3的卷积核与这9个蓝点进行卷积

Deformable Convolution(可变形卷积)代码解析(有错误,修改中)_第1张图片

这里引用参考博客里提到的两点

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

答:在实际操作时,并不是真正地把卷积核进行扩展,而是对卷积前图片的像素重新整合,变相地实现卷积核的扩张。

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。

Offsets转换

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}\) 的加权

Deformable Convolution(可变形卷积)代码解析(有错误,修改中)_第2张图片

双线性插值本质上就是在两个方向上做线性插值。如图所示,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)确定

  1. 先在x方向上做线性插值得到t1、t2的像素值
  2. 再在y方向做线性插值最终得到x(p)的像素值

Deformable Convolution(可变形卷积)代码解析(有错误,修改中)_第3张图片

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博客

你可能感兴趣的:(深度学习,计算机视觉,人工智能)