在计算机视觉领域,目前神经网络的应用主要有图像识别,目标定位和检测,语义分割。图像识别就是告诉你图像是什么,目标定位和加测告诉你图像中目标在哪里,语义分割则是告诉你图像中的每个像素都归属于哪些类别。目前语义分割广泛应用于医学图像与无人驾驶登领域。由于最近在写一个关于钢材中缺陷检测的项目,因此记录一下有关于语义分割的学习历程。
UNet
说到语义分割,那就不得不提最经典的算法UNet了。该算法最初是为医学图像分割设计的,由于想过确实很好,后面被用于各个领域如卫星图像切割,工业瑕疵检测等。该模型采用一个encoder-decoder结构,encoder通过卷积和池化操作进行下采样,逐步提取中图像深层次的信息,decoder通过反卷积或插值进行上采样,并通过与encoder提取的特征进行拼接,获得图像深层和浅层的信息,最终得到原图像尺寸的分割结果。

下采样
下采样的操作主要是对图像进行连续的卷积,池化操作来实现的,具体操作为
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
| 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的图像分割神经网络集合,能够高效的搭建所需的神经网络。