【语义分割】语义分割上采样方法总结

在语义分割模型中,一般通过Backbone获得不同分辨率的特征图,然后将特征图融合生成预测结果,在此过程中,不可避免地需要将低分辨率特征图进行上采样提高其分辨率,本文统计了常用的上采样方法,并给出部分上采样算法的numpy实现代码,与opencv进行对比检验代码的正确性。部分代码给出pytorch使用示例。

内容

1. 插值

1、最近邻插值

2、双线性插值

3、其他插值方法

二、PixelShuffle

三、反池化(unpool)

四、转置卷积(deconvolution)

5. 附录

1. 插值

插值利用像素间的相互关系计算出插入的像素值,最简单常用的是最近邻插值和双线性插值(提供了numpy实现的代码),还有其他的插值方法本文不过多介绍。

1、最近邻插值

最近邻插值是最简单的插值方法。选择离目标点最近的点作为新的插入点,如下例所示:

【语义分割】语义分割上采样方法总结

numpy实现及opencv对比:

import cv2
from math import floor
import numpy as np


def interpolate_nearest(image, size):
    new_img = np.zeros(shape=size[::-1] + (image.shape[-1], )).astype('uint8')
    scale_h = image.shape[0] / size[1]
    scale_w = image.shape[1] / size[0]
    for i in range(size[1]):
        for j in range(size[0]):
            new_img[i, j] = image[int(floor(i * scale_h)), int(floor(j * scale_w))]
    return new_img


image = cv2.imread('512.png')
size = (256, 256)   # w, h
my_resized_image = interpolate_nearest(image, size)
cv_resized_image = cv2.resize(image, size, image, 0, 0, cv2.INTER_NEAREST)
assert np.allclose(my_resized_image, cv_resized_image), "Image not equal between your implemented and opencv."
cv2.imshow("opencv", cv_resized_image)
cv2.imshow('my_op', my_resized_image)
cv2.waitKey(0)

影响:

【语义分割】语义分割上采样方法总结

2、双线性插值

双线性插值根据插值位置与周围像素的距离计算当前的像素值,如下图P为待插值点,Q11、Q12、Q21、Q22为原像素点坐标,f(Q22)表示Q22位置的像素值。(深度学习中很多模型用这个)

【语义分割】语义分割上采样方法总结

为了计算P像素值,首先横向插值,计算R1,R2的像素值:

【语义分割】语义分割上采样方法总结

再纵向插值,根据上一步得到的R1和R2像素值,得到P的像素值:

【语义分割】语义分割上采样方法总结

numpy实现:

import cv2
from math import floor
import numpy as np


def interpolate_linear(image, size):
    h, w = image.shape[0:2]
    w_new, h_new = size
    h_scale = h / h_new
    w_scale = w / w_new

    h_index = np.linspace(0, h_new - 1, h_new)
    w_index = np.linspace(0, w_new - 1, w_new)
    wv, hv = np.meshgrid(w_index, h_index)
    hv = (hv + 0.5) * h_scale - 0.5
    wv = (wv + 0.5) * w_scale - 0.5
    # hv = hv * h_scale
    # wv = wv * w_scale
    hv[hv < 0] = 0
    wv[wv < 0] = 0

    h_down = hv.astype('int')
    w_down = wv.astype('int')
    h_up = h_down + 1
    w_up = w_down + 1
    h_up[h_up > (h - 1)] = h - 1
    w_up[w_up > (w - 1)] = w - 1

    pos_00 = image[h_down, w_down].astype('int')  # 左上
    pos_01 = image[h_up, w_down].astype('int')  # 左下
    pos_11 = image[h_up, w_up].astype('int')  # 右下
    pos_10 = image[h_down, w_up].astype('int')  # 右上

    m, n = np.modf(hv)[0], np.modf(wv)[0]
    m = np.expand_dims(m, axis=-1)
    n = np.expand_dims(n, axis=-1)
    a = pos_10 - pos_00
    b = pos_01 - pos_00
    c = pos_11 + pos_00 - pos_10 - pos_01
    image = np.round(a * n + b * m + c * n * m + pos_00).astype('uint8')
    return image


image = cv2.imread('512.png')
size = (256, 256)   # w, h
my_resized_image = interpolate_linear(image, size)
cv_resized_image = cv2.resize(image, size, image, 0, 0, cv2.INTER_LINEAR)
print(np.mean(np.abs(my_resized_image.astype('int') - cv_resized_image.astype('int')))) # 线性插值四舍五入数值计算像素值可能差1
assert np.allclose(my_resized_image, cv_resized_image, atol=1), "Image not equal between your implemented and opencv."
cv2.imshow("opencv", cv_resized_image)
cv2.imshow('my_op', my_resized_image)
cv2.waitKey(0)

影响:

【语义分割】语义分割上采样方法总结

3、其他插值方法

还有许多插值方法,这里不一一列举,可以参考opencv文档。

二、PixelShuffle

对于一个维度为[N, C, H, W]的特征图,需要将其上采样R倍,得到维度为[N, C/(R^2), H*R, W*R]的特征图。实现起来颇为简单,只需要reshape即可,代码如下(和torch.nn.PixelShuffle对齐):

import torch
import numpy as np


def pixel_shuffle_np(x, up_factor):
    n, c, h, w = x.shape
    new_shape = (n, c // (up_factor * up_factor), up_factor, up_factor, h, w)
    npresult = np.reshape(x, new_shape)
    npresult = npresult.transpose(0, 1, 4, 2, 5, 3)
    oshape = [n, c // (up_factor * up_factor), h * up_factor, w * up_factor]
    npreslut = np.reshape(npresult, oshape)
    return npreslut


np.random.seed(10001)
image = np.random.rand(2, 16, 224, 224)
scale = 4
np_image = pixel_shuffle_np(image, scale)
torch_pixel_shuffle = torch.nn.PixelShuffle(scale)
torch_image = torch_pixel_shuffle(torch.from_numpy(image))
assert np.allclose(np_image, torch_image.numpy()), "Implemented PixelShuffle is not the same with torch.nn.PixelShuffle."

三、反池化(unpool)

反池化过程如下图所示,对输入特征图进行池化时,保存最大值在原特征图中的索引,反池化时,将特征值放入对应索引中,其他位置填充0。

【语义分割】语义分割上采样方法总结

torch代码:

import torch
import numpy as np


inputs = np.array([1, 2, 6, 3, 3, 5, 2, 1, 1, 2, 2, 1, 7, 3, 4, 8], dtype='float').reshape([1, 1, 4, 4])
inputs = torch.from_numpy(inputs)
pool = torch.nn.MaxPool2d(2, stride=2, return_indices=True)
unpool = torch.nn.MaxUnpool2d(2, stride=2)
output, indices = pool(inputs)
output = unpool(output, indices)
print(output)

结果:

【语义分割】语义分割上采样方法总结

四、转置卷积(deconvolution)

上面介绍的方法都是没有参数的方法。有没有一种方法可以为不同的任务学习不同的参数?显然有,也就是转置卷积。

从一个简单的例子开始,如下图,输入为2×2,核大小为2×2,输入的每个数与核相乘,然后累加起来得到了3×3的输出,可以看到特征图变大了(3×3的输入经过2×2的卷积得到2×2的输出,转置卷积是卷积的逆操作)。

【语义分割】语义分割上采样方法总结

上面的转置卷积使得2×2的输入特征图变为3×3的输出特征图,能不能让输出特征图变得更大呢?显然可以,见下图,引入了步长后(下图的步长可以理解为输出特征图上的步长),输入特征图为2×2,核为2×2,输出特征图为4×4。

【语义分割】语义分割上采样方法总结

同理可以引入padding和dilation等参数,假设输入维度为[N, C_{in}, H_{in}, W_{in}],输出维度为[N, C_{out}, H_{out}, W_{out}],转置卷积的维度计算公式为:

H_{out}=(H_{in}-1)*stride[0]-2*padding[0]+dilation[0]*(kernel\_size[0]-1)+output\_padding[0]+1

W_{out}=(W_{in}-1)*stride[1]-2*padding[1]+dilation[1]*(kernel\_size[1]-1)+output\_padding[1]+1

代码实践(输入特征图:[1, 3, 50, 50], 输出特征图: [1, 3, 98, 98]):

import torch


x = torch.rand(1, 3, 50, 50)
transpose_conv = torch.nn.ConvTranspose2d(3, 3, kernel_size=3, stride=2, padding=2, output_padding=1)
y = transpose_conv(x)
print(x.shape, y.shape)
# 输出:
# torch.Size([1, 3, 50, 50]) torch.Size([1, 3, 98, 98])

5. 附录

所有示例代码

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
青葱年少的头像青葱年少普通用户
上一篇 2022年3月24日 下午1:41
下一篇 2022年3月24日 下午2:02

相关推荐