目录
一、模型概述
二、BackBone构建
三、语义分割分支
四、实例分割分支
五、代码汇总
一、模型概述
整个模型有一个backbone,以及两个分支,输入图片之后,先进入backbone,这一部分是两个分支共用参数,输出给两个分支,上面的图的彩色部分,是实例分割的分支,黑白部分,是语义分割的分支。将二者结合,通过聚类损失函数,进行反向传播,从而完成训练。
二、BackBone构建
可以使用的backbone有很多,这里我使用了resnet50,对网络修改一下就可以。
先看看resnet的forward部分,输入一张图之后,每一步得到的是怎样的输出,比如我输入的图是(B, 3, 360, 640),使用调试模式,进入模型forward部分,一步一步看输出形状。
之后我选出来八倍下采样之后的结果(B, 512, 45, 80),再写一个8倍上采样部分,结果(B, 8, 360,640)作为输出,给到两个分支。注意通道数的调整。(这里又扩大了一下通道数,目的是使用膨胀卷积,增加一下感受野,之后再一步步减小通道数,最后进行上采样。)
注意:这里我是直接进入torchvision里面,复制出来了resnet,并且直接删去了模型forward中的后半部分,因此才可以直接使用resnet输出的结果。
def __init__(self, embed_dim):
super(LaneNet, self).__init__()
self.embedding_dim = embed_dim
self.backbone = resnet50(pretrained=True)
self.upsample8x = nn.Sequential(
# 进行一次膨胀卷积,增加感受野,之后再一步步降下来,最后再上采样。
nn.Conv2d(512,1024,3, padding=4,dilation=4,bias=False),
nn.BatchNorm2d(1024),
nn.ReLU(),
nn.Conv2d(1024, 256, 3, padding=1, bias=False),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.Conv2d(256, 64, 3, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Conv2d(64, 8, 3, padding=1, bias=False),
nn.BatchNorm2d(8),
nn.ReLU(),
nn.Upsample(scale_factor=8, align_corners=True,mode='bilinear'))
三、语义分割分支
语义分割所需要的输出,是(B, 2, 360, 640),其中第二个维度的2,可以认为一个是用来判定车道线的,一个是用来判定背景的。这样的话,根据输入的形状,写一个专属分支即可,这里从backbone过来的时候是(B, 8, 360,640),就通过卷积+BN等层,生成目标形状即可。
self.seg = nn.Sequential(
nn.Conv2d(8,8,1),
nn.BatchNorm2d(8),
nn.ReLU(),
nn.Conv2d(8,2,1)
)
四、实例分割分支
这部分的目的,是对每一个像素点,给出一个embedding,比如我设定embedding的维度是4,那么,这部分的输出,就应该是(B, 4, 360, 640)。
这里embedding的意思,就是使用一个4维向量,来表示每一个像素点。为什么要用embedding呢?
假如我不用embedding,输出就是(B, 360, 640),也就是说,每一张图,都有360*640个像素点,每一个像素点有不同的值,代表不同的颜色,比如0是黑色等等。
但是,这里的实例分割,是要区别出每条车道线的。我们虽然从图中一眼就看出来了,车道线那一部分的像素点,那一坨就是一根线啊,但是电脑看不出来,它只知道那些点是像素点,并且有一个像素值而已。
因此我们要使用不同的方式,来表示这个点,并且能通过计算距离,得到两个点之间的相似度。假如一个点的像素是200,另一个的像素是201,他们不一定属于同一条车道线。但假如第一个点的向量表示是[2,2,3,3,4],另一个点的向量表示是[2,2,3,3,4.1],那这俩就很相似了,就可以认为他们是属于同一条车道线了。
因此,我们对每一个点,都进行一个embedding,就可以计算每个点与每个点之间的相似度,进一步也可以计算一条线和另一条线的相似度。有了这样一种方法,就可以计算损失了。具体的损失计算,另一篇文章记录。
self.embedding = nn.Sequential(
nn.Conv2d(8,8,1),
nn.BatchNorm2d(8),
nn.ReLU(),
nn.Conv2d(8,self.embedding_dim,1)
)
五、代码汇总
import torch
import torch.nn as nn
from resnet import resnet50
import torch.nn.functional as F
class LaneNet(nn.Module):
def __init__(self, embed_dim):
super(LaneNet, self).__init__()
self.embedding_dim = embed_dim
self.backbone = resnet50(pretrained=True)
self.upsample8x = nn.Sequential(
# 进行一次膨胀卷积,增加感受野,之后再一步步降下来,最后再上采样。
nn.Conv2d(512,1024,3, padding=4,dilation=4,bias=False),
nn.BatchNorm2d(1024),
nn.ReLU(),
nn.Conv2d(1024, 256, 3, padding=1, bias=False),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.Conv2d(256, 64, 3, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Conv2d(64, 8, 3, padding=1, bias=False),
nn.BatchNorm2d(8),
nn.ReLU(),
nn.Upsample(scale_factor=8, align_corners=True,mode='bilinear'))
self.embedding = nn.Sequential(
nn.Conv2d(8,8,1),
nn.BatchNorm2d(8),
nn.ReLU(),
nn.Conv2d(8,self.embedding_dim,1)
)
self.seg = nn.Sequential(
nn.Conv2d(8,8,1),
nn.BatchNorm2d(8),
nn.ReLU(),
nn.Conv2d(8,2,1)
)
self.seg_loss = nn.CrossEntropyLoss()
def forward(self,inputs):
# 通过Backbone之后的输出是(B,512,45,80),是8倍降采样之后的结果。
x = self.backbone(inputs)
# 上采样这里是先膨胀卷积一次,后慢慢降下来通道数,再一个8倍双线性插值上采样。
out = self.upsample8x(x)
# 去实例分割的那条路,embedding成4维的(B,4,360,640)
embedding = self.embedding(out)
# 去语义分割的那条路,变成2通道的,一个预测背景,一个预测车道线(B,2,360,640)
seg = self.seg(out)
return [embedding, seg]