1.1 项目结构
- cfgs: 默认/预设配置文件
- lib:
- datasets
culane.py: culane数据集加载器
lane_dataset.py: 将来自LaneDatasetLoader中的未经过处理的 annotations 转换为模型可以使用的形式
lane_dataset_loader.py: 每个数据集加载器实现的抽象类
llamas.py: llamas数据集加载器
nolabel_dataset.py: 加载不需注释的的数据集
tusimple.py: tusimple数据集加载器
- models:
laneatt.py: LaneATT模型的实现
matching.py: 用于gt和proposal匹配的效用函数
resnet.py: resnet 实现部分
nms: LaneATT模型的实现
config.py: LaneATT模型的实现
experiment.py: 跟踪和存储有关每个实验的信息
focal_loss.py: focal loss的实现
lane.py: 车道线表示
runner.py: 训练和测试循环
- utils:
culane_metric.py: 非官方的CULane数据集度量实现
gen_anchor_mask.py: 计算数据集中要在锚点筛选步骤中使用的每个锚点的频率(论文提到锚点的数量会限制速度,所以挑选使用频率最大的部分锚点)
gen_video.py: 从模型预测生成视频
llamas_metric.py llamas数据集的实用程序函数
speed.py: 测量模型的效率相关指标
tusimple_metric.py: tusimple数据集图片度量的官方实现
viz_dataset.py: 显示从数据集采样的图像(增强后)
- main.py:
运行实验的训练或测试阶段
2.1Anchor的含义与创建
Anchor用通俗的语言是指在特征图层中生成锚点,以起始点坐标根据锚点的坐标连接成线,呈现出一种射线状。如图所示
下面解析LaneATT中的Anchor模块的代码:
def __init__(self,
backbone='resnet34',
pretrained_backbone=True,
S=72,
img_w=640,
img_h=360, # 规定高度采样数与输入图像的期望宽度与高度
anchors_freq_path=None,
topk_anchors=None,
anchor_feat_channels=64):
super(LaneATT, self).__init__()
# Some definitions
self.feature_extractor, backbone_nb_channels, self.stride = get_backbone(backbone, pretrained_backbone)
self.img_w = img_w
self.n_strips = S - 1 # 高度方面的采样点
self.n_offsets = S # x方向上的偏移量
self.fmap_h = img_h // self.stride
fmap_w = img_w // self.stride # 算出fmap_w = 11
self.fmap_w = fmap_w
self.anchor_ys = torch.linspace(1, 0, steps=self.n_offsets, dtype=torch.float32)
self.anchor_cut_ys = torch.linspace(1, 0, steps=self.fmap_h, dtype=torch.float32)
self.anchor_feat_channels = anchor_feat_channels
# Anchor angles, same ones used in Line-CNN
# 左、下、右方创建anchor的角度范围,由于车道线主要聚集在下方,bottom的角度范围最多
self.left_angles = [72., 60., 49., 39., 30., 22.]
self.right_angles = [108., 120., 131., 141., 150., 158.]
self.bottom_angles = [165., 150., 141., 131., 120., 108., 100., 90., 80., 72., 60., 49., 39., 30., 15.]
# Generate anchors
# 定义72 , 128个start的起始点数量
self.anchors, self.anchors_cut = self.generate_anchors(lateral_n=72, bottom_n=128)
整段代码就是初始化一些参数,offsets =72,说明Lanatt是将车道线回归成72个点来组成最终的车道线。
def cut_anchor_features(self, features):
# definitions
batch_size = features.shape[0] # 批次大小
n_proposals = len(self.anchors) # 锚点数量
n_fmaps = features.shape[1] # 特征图深度
batch_anchor_features = torch.zeros((batch_size, n_proposals, n_fmaps, self.fmap_h, 1), device=features.device)
# 存储剪裁后的锚点特征张量
# actual cutting
for batch_idx, img_features in enumerate(features):
rois = img_features[self.cut_zs, self.cut_ys, self.cut_xs].view(n_proposals, n_fmaps, self.fmap_h, 1)
# 完成裁剪动作,形状为(n_proposals, n_fmaps, self.fmap_h, 1)
rois[self.invalid_mask] = 0 # 区域外的点赋为0
batch_anchor_features[batch_idx] = rois
return batch_anchor_features
这里要着重说明一下anchor与anchor_cut的区别:
由于锚点所确定的线是一条射线,因此在特征图像之外势必也会存在锚线,cut则会利用边缘位置来切割这些异常的anchor的点得到anchor_cut,因此backbone_feature预测需要将这些点置零,避免从图像之外的anchor外训练。上述代码首先定义“裁剪框”,通过for遍历实现剪裁。
def generate_anchors(self, lateral_n, bottom_n):
# anchor: 2, 1 ,2(x,y) 72(offsets)
# 2 * 72 * 6 , 128 * 15 = 2784
left_anchors, left_cut = self.generate_side_anchors(self.left_angles, x=0., nb_origins=lateral_n)
right_anchors, right_cut = self.generate_side_anchors(self.right_angles, x=1., nb_origins=lateral_n)
bottom_anchors, bottom_cut = self.generate_side_anchors(self.bottom_angles, y=1., nb_origins=bottom_n)
# 返回连接后的锚点的张量,与之对应的容器
return torch.cat([left_anchors, bottom_anchors, right_anchors]), torch.cat([left_cut, bottom_cut, right_cut])
设置左、下、右的边缘位置起始点位置。x=0则代表left,x=1则代表right。由于OpenCV的坐标问题,y=1代表下方的边缘位置。
def generate_side_anchors(self, angles, nb_origins, x=None, y=None):
# self.anchors, self.anchors_cut = self.generate_anchors(lateral_n=72, bottom_n=128) 在源码的48行已经规定
if x is None and y is not None:
# bottom: x从1——0取128个,y=1(因为OpenCV的坐标系问题,bottom对应y=1)
starts = [(x, y) for x in np.linspace(1., 0., num=nb_origins)]
elif x is not None and y is None:
# left: x=0 y从1——0中取72个starts
starts = [(x, y) for y in np.linspace(1., 0., num=nb_origins)]
else:
raise Exception('Please define exactly one of `x` or `y` (not neither nor both)')
# 求出anchors的数量
n_anchors = nb_origins * len(angles)
两条if语句为互斥条件,以第一条if语句为例: if x is None and y is not None:则代表bottom位置,从0~1中取128个起始点坐标。反之,在左(右)生成72个起始坐标。之所以下方的起始坐标更多是因为,车道线一般在bottom位置更多的聚集,自行想象车辆的运行情况。
# each row, first for x and second for y:
# 2 scores, 1 start_y, start_x, 1 lenght, S coordinates, score[0] = negative prob, score[1] = positive prob
# an_cut作为一个容器,将feature_map中的anchor放到容器中,完成高度的转换
# anchor: 2, 1 ,2(x,y) 72(offsets) 2个 :即 两个类别【正,负】,起始坐标,车道线len,以及offset
anchors = torch.zeros((n_anchors, 2 + 2 + 1 + self.n_offsets))
anchors_cut = torch.zeros((n_anchors, 2 + 2 + 1 + self.fmap_h))
for i, start in enumerate(starts):
for j, angle in enumerate(angles):
# n_stars * n_angles
k = i * len(angles) + j
anchors[k] = self.generate_anchor(start, angle)
anchors_cut[k] = self.generate_anchor(start, angle, cut=True)
return anchors, anchors_cut
def generate_anchor(self, start, angle, cut=False):
if cut:
anchor_ys = self.anchor_cut_ys
# 创建anchor的张量,形状为2 + 2 + 1 + self.fmap_h
anchor = torch.zeros(2 + 2 + 1 + self.fmap_h)
else:
anchor_ys = self.anchor_ys
anchor = torch.zeros(2 + 2 + 1 + self.n_offsets)
angle = angle * math.pi / 180. # degrees to radians
start_x, start_y = start # 获取起始坐标
# 初始化anchor的第2,3个元素
anchor[2] = 1 - start_y
anchor[3] = start_x
# 计算偏移量,注意opencv的坐标区别
anchor[5:] = (start_x + (1 - anchor_ys - 1 + start_y) / math.tan(angle)) * self.img_w
return anchor
———————————————————————————————————————————
先补一下LSS检测算法