前言

本篇文章收录于语义分割专栏,如果对语义分割领域感兴趣的,可以去看看专栏,会对经典的模型以及代码进行详细的讲解哦!其中会包含可复现的代码!带大家深入语义分割的领域,将从原理,代码深入讲解,希望大家能从中有所收获,其中很多内容都包含着自己的一些想法以及理解,如果有错误的地方欢迎大家批评指正。

论文名称: SegNet: A Deep Convolutional Encoder-Decoder Architecture for Image Segmentation

论文地址:SegNet: A Deep Convolutional Encoder-Decoder Architecture for Image Segmentation

欢迎来到语义分割系列第三篇哦,本文将继续带大家来学习语义分割领域的经典模型:Segnet。

背景介绍

同样的我们继续来看看Segnet出现的历史背景,其是Segnet是与FCN同时期的研究工作,不过稍晚些于FCN,所以其在论文中很多的实验都是与FCN进行比较分析的。像上节我们讲的针对于医疗图像分割的Unet一样,Segnet也是针对于某些特定的任务的,其主要针对于无人驾驶任务和AR任务的,尤其是无人驾驶任务。

那么对于无人驾驶任务和AR任务,最重要的是什么呢?那肯定是实时性和准确性了,特别对于无人驾驶任务,我们需要在很短的时间内处理,并且需要具有相当不错的准确性。并且其准确性有个非常重要的方面就是其边界的准确性,需要车辆能够精确地识别和分割道路、行人、车辆、交通标志、车道线等元素。

再看当时方法的局限,首先关于手工方法就不说了,难以应对复杂的情况。而当时典型的语义分割模型FCN同样也存在着局限性,FCN在上采样的时候使用的是反卷积,其会丢失非常重要的空间信息,这会导致边界模糊,而这种情况不是我们希望看到的。其次FCN的计算量是比较大的,难以满足实时性的要求。

Segnet核心剖析

是的,你遇到这些问题你怎么办?希望大家都能够设身处地去思考,我们才能够明白每一项工作的创新意义,同时进行深入思考,很多时候你可能也会有自己的想法,每个创新性的想法都是不经意间的,希望大家都能够有思维的碰撞。好了,话说回来,我们来看看Segnet的作者是怎么做的,看看人家如何进行解决的。

池化索引(pooling Indices)

其实我认为Segnet最核心的创新部分就是采用了池化索引(pooling Indices)的方法了。什么意思呢,看下图,我们来详细讲解一下。

Segnet同样的也是使用了编码器-解码器的结构。Segnet的上采样方式与FCN不同,FCN采用的是反卷积的方式,而Segnet每次池化操作时,它不仅保留了池化后的特征图,还记录了每个池化区域中最大值的位置(即池化索引)。在解码器部分,它将这些池化索引传递过来,用于指导上采样过程。通过这样的方式,编码器在低分辨率下检测到的语义信息(通过卷积特征)和原始图像中的精确空间位置信息(通过池化索引)得以结合,显著提高了分割边界的定位精度。

并且非常重要的一点,SegNet通过使用池化索引进行指导的上采样,避免了使用计算量大的反卷积操作。这使得SegNet模型参数量少,计算复杂度低,非常适合需要实时处理的场景,就像无人驾驶或者AR场景。

通过pooling Indices获得的上采样feature map是稀疏的,其实从图中就能看出,所以在解码器部分会有对应的卷积结构,将稀疏的feature map通过卷积变成稠密的feature map。

当然其弊端也会非常明显,在上采样的过程中缺少了学习的过程,所以其在精度上是无法与当时的FCN进行比较的,但是综合精度、实时性、轻便性来说,Segnet则是更好的考量。

其他细节

编码器解码器的对称结构

其实在其原论文中多次强调了编码器解码器的对称结构的重要性,因为像在FCN中,其编码器解码器的结构是及其不对称的,也就是下采样上采样的过程不对称,其编码器参数有134M但解码器参数仅仅只有0.5M。这就会导致个什么情况呢?解码器的部分的参数相当难以训练。其实如果看过FCN论文也会发现,其详细讲述了FCN的训练过程,也说明了其是难以训练的。

所以设计了一个端到端的、编码器网络中每个编码器都被逐步连接到解码器网络中的SegNet。这种想法很简单,也就是保存多个尺度上提取到的特征和全局的上下文信息,为上采样时提供更多的可用信息,从而保留更多高频细节,实现精细的分割。结构图如下所示:

Segnet模型代码

首先是我们的crop函数,为什么需要用到这个,因为在测试的时候,我们不会对图像进行resize操作的,所以其就不一定是32的倍数,在下采样的过程中可能会出现从45->22的情况,但是上采样过程中就会变成22->44,这样就会造成shape的不匹配,所以需要对齐两者的shape大小。

def crop(upsampled, bypass):

    h1, w1 = upsampled.shape[2], upsampled.shape[3]
h2, w2 = bypass.shape[2], bypass.shape[3] # 计算差值
deltah = h2 - h1
deltaw = w2 - w1 # 计算填充的起始和结束位置
# 对于高度
pad_top = deltah // 2
pad_bottom = deltah - pad_top
# 对于宽度
pad_left = deltaw // 2
pad_right = deltaw - pad_left # 对 upsampled 进行中心填充
upsampled_padded = F.pad(upsampled, (pad_left, pad_right, pad_top, pad_bottom), "constant", 0) return upsampled_padded

然后就是我们的Segnet模型代码了。其实还是非常好理解的,其编码器的结构就是VGG的结构,只不过其在maxpooling的时候需要保存索引,然后就是解码器的结构,其实就是对编码器做个对称就行了。写好模型参数之后,非常重要的,记得要进行参数的初始化哈,这样能够利于之后的训练过程。

class SegNet(nn.Module):
def __init__(self,num_classes=12):
super(SegNet, self).__init__()
self.encoder1 = nn.Sequential(
nn.Conv2d(3,64,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Conv2d(64,64,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
)
self.encoder2 = nn.Sequential(
nn.Conv2d(64,128,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.Conv2d(128,128,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
)
self.encoder3 = nn.Sequential(
nn.Conv2d(128,256,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.Conv2d(256,256,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.Conv2d(256,256,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(256),
nn.ReLU(),
)
self.encoder4 = nn.Sequential(
nn.Conv2d(256,512,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.Conv2d(512,512,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.Conv2d(512,512,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(),
)
self.encoder5 = nn.Sequential(
nn.Conv2d(512,512,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.Conv2d(512,512,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.Conv2d(512,512,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(),
) self.decoder1 = nn.Sequential(
nn.Conv2d(512,512,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.Conv2d(512,512,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.Conv2d(512,512,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(),
)
self.decoder2 = nn.Sequential(
nn.Conv2d(512,512,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.Conv2d(512,512,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.Conv2d(512,256,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(256),
nn.ReLU(),
)
self.decoder3 = nn.Sequential(
nn.Conv2d(256,256,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.Conv2d(256,256,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.Conv2d(256,128,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
)
self.decoder4 = nn.Sequential(
nn.Conv2d(128,128,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.Conv2d(128,64,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
)
self.decoder5 = nn.Sequential(
nn.Conv2d(64,64,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Conv2d(64,64,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Conv2d(64,num_classes,kernel_size=1),
) self.max_pool = nn.MaxPool2d(2,2,return_indices=True)
self.max_uppool = nn.MaxUnpool2d(2,2) self.initialize_weights() def initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d) or isinstance(m, nn.ConvTranspose2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0) def forward(self, x):
x1 = self.encoder1(x)
x,pool_indices1 = self.max_pool(x1)
x2 = self.encoder2(x)
x,pool_indices2 = self.max_pool(x2)
x3 = self.encoder3(x)
x,pool_indices3 = self.max_pool(x3)
x4 = self.encoder4(x)
x,pool_indices4 = self.max_pool(x4)
x5 = self.encoder5(x)
x,pool_indices5 = self.max_pool(x5) x = self.max_uppool(x,pool_indices5)
x = crop(x, x5)
x = self.decoder1(x)
x = self.max_uppool(x,pool_indices4)
x = crop(x, x4)
x = self.decoder2(x)
x = self.max_uppool(x,pool_indices3)
x = crop(x, x3)
x = self.decoder3(x)
x = self.max_uppool(x,pool_indices2)
x = crop(x, x2)
x = self.decoder4(x)
x = self.max_uppool(x,pool_indices1)
x = crop(x, x1)
x = self.decoder5(x) return x

结语

希望上列所述内容对你有所帮助,如果有错误的地方欢迎大家批评指正!

并且如果可以的话希望大家能够三连鼓励一下,谢谢大家!

如果你觉得讲的还不错想转载,可以直接转载,不过麻烦指出本文来源出处即可,谢谢!

参考资料

本文参考了下列的文章内容,集百家之长汇聚于此,同时包含自己的思考想法

SegNet图像分割网络直观详解 - 知乎

【语义分割专栏】3:Segnet原理篇的更多相关文章

  1. 语义分割学习之SegNet的C++编译

    Abstract 安装好Segnet并使用Python进行训练和测试之后,考虑项目的应用,需要在C++的工程环境下进行继续开发,所以这里的主要内容是用C++建立工程,使用相应的数据集和权重参数文件进行 ...

  2. 【Keras】基于SegNet和U-Net的遥感图像语义分割

    上两个月参加了个比赛,做的是对遥感高清图像做语义分割,美其名曰"天空之眼".这两周数据挖掘课期末project我们组选的课题也是遥感图像的语义分割,所以刚好又把前段时间做的成果重新 ...

  3. 语义分割(semantic segmentation) 常用神经网络介绍对比-FCN SegNet U-net DeconvNet,语义分割,简单来说就是给定一张图片,对图片中的每一个像素点进行分类;目标检测只有两类,目标和非目标,就是在一张图片中找到并用box标注出所有的目标.

    from:https://blog.csdn.net/u012931582/article/details/70314859 2017年04月21日 14:54:10 阅读数:4369 前言 在这里, ...

  4. 比较语义分割的几种结构:FCN,UNET,SegNet,PSPNet和Deeplab

    简介 语义分割:给图像的每个像素点标注类别.通常认为这个类别与邻近像素类别有关,同时也和这个像素点归属的整体类别有关.利用图像分类的网络结构,可以利用不同层次的特征向量来满足判定需求.现有算法的主要区 ...

  5. 多篇开源CVPR 2020 语义分割论文

    多篇开源CVPR 2020 语义分割论文 前言 1. DynamicRouting:针对语义分割的动态路径选择网络 Learning Dynamic Routing for Semantic Segm ...

  6. 几篇关于RGBD语义分割文章的总结

      最近在调研3D算法方面的工作,整理了几篇多视角学习的文章.还没调研完,先写个大概.   基于RGBD的语义分割的工作重点主要集中在如何将RGB信息和Depth信息融合,主要分为三类:省略. 目录 ...

  7. 语义分割--全卷积网络FCN详解

    语义分割--全卷积网络FCN详解   1.FCN概述 CNN做图像分类甚至做目标检测的效果已经被证明并广泛应用,图像语义分割本质上也可以认为是稠密的目标识别(需要预测每个像素点的类别). 传统的基于C ...

  8. 【Semantic segmentation Overview】一文概览主要语义分割网络(转)

    文章来源:https://www.tinymind.cn/articles/410 本文来自 CSDN 网站,译者蓝三金 图像的语义分割是将输入图像中的每个像素分配一个语义类别,以得到像素化的密集分类 ...

  9. 自动网络搜索(NAS)在语义分割上的应用(二)

    前言: 本文将介绍如何基于ProxylessNAS搜索semantic segmentation模型,最终搜索得到的模型结构可在CPU上达到36 fps的测试结果,展示自动网络搜索(NAS)在语义分割 ...

  10. 语义分割:基于openCV和深度学习(一)

    语义分割:基于openCV和深度学习(一) Semantic segmentation with OpenCV and deep learning 介绍如何使用OpenCV.深度学习和ENet架构执行 ...

随机推荐

  1. Docker镜像(image)详解

    如果曾经做过 VM 管理员,则可以把 Docker 镜像理解为 VM 模板,VM 模板就像停止运行的 VM,而 Docker 镜像就像停止运行的容器:而作为一名研发人员,则可以将镜像理解为类(Clas ...

  2. 面试官:工作中优化MySQL的手段有哪些?

    MySQL 是面试中必问的模块,而 MySQL 中的优化内容又是常见的面试题,所以本文来看"工作中优化MySQL的手段有哪些?". 工作中常见的 MySQL 优化手段分为以下五大类 ...

  3. 【JDBC】总结

    JDBC核心技术 第1章:JDBC概述 1.1 数据的持久化 持久化(persistence):把数据保存到可掉电式存储设备中以供之后使用.大多数情况下,特别是企业级应用,数据持久化意味着将内存中的数 ...

  4. ASP.NET 日志路径

    默认路径 protected void Button_StreamWrite_Click(object sender, EventArgs e) {     StreamWriter sw = new ...

  5. Win10微软拼音输入法设置-注册表

    修改候选项窗口 HKEY_CURRENT_USER\Software\Microsoft\InputMethod\CandidateWindow\CHS\1 EnableFixedCandidateC ...

  6. 正反代理-nginx安装

    参考文章:https://www.cnblogs.com/ysocean/p/9384877.html 先预祝一下成功 废话不多说,开始吧,步骤不多 下载地址 https://nginx.org/en ...

  7. Python科学计算系列7—微分方程

    1.可分离变量方程 例1:求下列微分方程法通解 先化简此方程如下: 代码如下: from sympy import * x = symbols('x') f = symbols('f', cls=Fu ...

  8. vue获取浏览器地址栏参数

    this.accountId = this.$route.query.id

  9. 1安装docker

    1安装docker 1.1主机环境 ssh://192.168.30.30:22   root   123QWEasd 1.2安装依赖 docker依赖于系统的一些必要的工具,可以提前安装. yum ...

  10. 操作系统:CPU工作模式-- 执行程序的三种模式

    按照 CPU 功能升级迭代的顺序,CPU 的工作模式有实模式.保护模式.长模式,这几种工作模式下 CPU 执行程序的方式截然不同,下面我们一起来探讨这几种工作模式. 从一段死循环的代码说起 int m ...