在计算机视觉领域,目前神经网络的应用主要有图像识别,目标定位和检测,语义分割。图像识别就是告诉你图像是什么,目标定位和加测告诉你图像中目标在哪里,语义分割则是告诉你图像中的每个像素都归属于哪些类别。目前语义分割广泛应用于医学图像与无人驾驶登领域。由于最近在写一个关于钢材中缺陷检测的项目,因此记录一下有关于语义分割的学习历程。

UNet

说到语义分割,那就不得不提最经典的算法UNet了。该算法最初是为医学图像分割设计的,由于想过确实很好,后面被用于各个领域如卫星图像切割,工业瑕疵检测等。该模型采用一个encoder-decoder结构,encoder通过卷积和池化操作进行下采样,逐步提取中图像深层次的信息,decoder通过反卷积或插值进行上采样,并通过与encoder提取的特征进行拼接,获得图像深层和浅层的信息,最终得到原图像尺寸的分割结果。
UNet模型框架

下采样

下采样的操作主要是对图像进行连续的卷积,池化操作来实现的,具体操作为

1
2
3
4
self.down1 = Down(base_c, base_c * 2)
self.down2 = Down(base_c * 2, base_c * 4)
self.down3 = Down(base_c * 4, base_c * 8)
self.down4 = Down(base_c * 8, base_c * 16)

其中下采样层Down

1
2
3
4
5
6
7
# 下采样模块:MaxPool2d 结合双卷积操作
class Down(nn.Sequential):
def __init__(self, in_channels, out_channels):
super(Down, self).__init__(
nn.MaxPool2d(2, stride=2), # 下采样
DoubleConv(in_channels, out_channels) # 双卷积
)

MaxPool2d为池化层,步幅为2,经过池化层后,输出大小从(W,H)变为(W/2,H/2)
DoubleConv为双卷积层,该层通过两层卷积和批量归一化操作,提取出图像深一层的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 双卷积模块:基本单元,两个连续的卷积层,带有BatchNorm和ReLU激活
class DoubleConv(nn.Sequential):
def __init__(self, in_channels, out_channels, mid_channels=None):
# mid_channels = out_channels / 4
if mid_channels is None:
mid_channels = out_channels // 4
super(DoubleConv, self).__init__(
nn.Conv2d(in_channels, mid_channels, kernel_size=3, padding=1, bias=False),
nn.BatchNorm2d(mid_channels),
nn.ReLU(inplace=True),
nn.Conv2d(mid_channels, out_channels, kernel_size=3, padding=1, bias=False),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True)
)

上采样

上采样最关键的一步是如何在恢复图像尺寸的过程中,与下采样过程中得到的图像各层次特征结合起来。

1
2
3
4
self.up1 = Up(base_c * 16, base_c * 8 // factor, bilinear)
self.up2 = Up(base_c * 8, base_c * 4 // factor, bilinear)
self.up3 = Up(base_c * 4, base_c * 2 // factor, bilinear)
self.up4 = Up(base_c * 2, base_c, bilinear)

通过设计Up层的前向传播函数,将下采样过程中提取的特征进行拼接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Up(nn.Module):
def __init__(self, in_channels, out_channels, bilinear=True):
super(Up, self).__init__()
if bilinear:
self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=True)
self.conv = DoubleConv(in_channels, out_channels, in_channels // 2)
else:
self.up = nn.ConvTranspose2d(in_channels, in_channels // 2, kernel_size=2, stride=2)
self.conv = DoubleConv(in_channels, out_channels)

def forward(self, x1: torch.Tensor, x2: torch.Tensor) -> torch.Tensor:
# 上采样
x1 = self.up(x1)

# 计算空间尺寸差异并进行padding
diff_y = x2.size()[2] - x1.size()[2]
diff_x = x2.size()[3] - x1.size()[3]
x1 = F.pad(x1, [diff_x // 2, diff_x - diff_x // 2,
diff_y // 2, diff_y - diff_y // 2])

# 拼接x1和x2
x = torch.cat([x2, x1], dim=1)
x = self.conv(x)
return x

在上采样过程中,有两种方法,一种是直接进行反卷积,另一种是进行插值。这里使用了bilinear双线性插值,这会增加模型的训练时间,但综合表现会更好。
同时在前向传播过程中,需要输入两个参数,一个是上采样过程中的图像,另一个是相对应的下采样过程中的图像。具体过程如下

1
2
3
4
5
6
7
8
9
10
x1 = self.in_conv(x)
x2 = self.down1(x1)
x3 = self.down2(x2)
x4 = self.down3(x3)
x5 = self.down4(x4)

x = self.up1(x5, x4)
x = self.up2(x, x3)
x = self.up3(x, x2)
x = self.up4(x, x1)

UNet算是最经典的一个语义分割模型了,但在此次项目中,我主要是使用SMP进行模型的构建。

segmentation_models_pytorch(SMP)

segmentation_models_pytorch是一个基于PyTorch的图像分割神经网络集合,能够高效的搭建所需的神经网络。