参考代码:RCLane
介绍:在这篇文章中介绍了一种新的车道线描述范式(基于点的车道线建模),也就是通过关键点(文章对应称之为relay points)和关键点之间的关系场构建推理出车道线。具体为文章通过segformer实现车道线二值分割得到分割结果 S S S,并同时通过不同的head预测 T T T(transfer map,用于确定临近点位置)和 D D D(distance map,用于确定终止点),之后在 S S S上做point-nms实现车道线上点的确立,以这些点为起点结合 D D D和 T T T推理出整条车道线。按照这样的思想该算法具有较强的车道线结构适应能力,如分离汇入、弯道场景也能很好建模,目前就公开数据集性能已经超过之前的condlanenet了,还是值得期待完整开源的。
文章的整体结构见下图所示:

这篇文章是以segformer作为benchmark,并在其基础上添加两个head,分别实现
D
D
D(distance map)和
T
T
T(transfer map)预测工作,也就是对应图中左边半边部分。需要注意的是
T
T
T和
D
D
D预测的都是双向的,这样可以从任意的一个点上进行推理得到整条线。对于推理的起点这里是通过对车道线二值分割结果进行point-nms选择实现的,最后对所有推理出来的线进做IoU-NMS操作得到最终输出结果。因而这篇文章的算法后处理是相对较重的。
对于这两个map的描述可见下图中的(a)图:

其中
T
ˉ
f
(
p
i
)
,
T
ˉ
b
(
p
i
)
\bar{T}_f(p_i),\bar{T}_b(p_i)
Tˉf(pi),Tˉb(pi)表示的是当前点
p
i
p_i
pi(车道线二值mask上的每一个点)到相邻两个点的affine filed(也就是分别对应的forward和backward方向),
D
ˉ
f
(
p
i
)
,
D
ˉ
b
(
p
i
)
\bar{D}_f(p_i),\bar{D}_b(p_i)
Dˉf(pi),Dˉb(pi)表示的是当前点
p
i
p_i
pi分别到端点的距离。
distance map:
该图描述的是车道线上点到两个端点的距离关系,这里依据车道线点的y坐标划分forward还是backward,具体为最小y坐标值(对应坐标点
p
e
n
d
f
=
(
x
e
n
d
f
,
y
e
n
d
f
)
p_{end}^f=(x_{end}^f,y_{end}^f)
pendf=(xendf,yendf))和最大y坐标值(对应坐标点
p
e
n
d
b
=
(
x
e
n
d
b
,
y
e
n
d
b
)
p_{end}^b=(x_{end}^b,y_{end}^b)
pendb=(xendb,yendb))。那么当前点
p
i
p_i
pi对应的distance map标注可以描述为:
D
ˉ
f
(
p
i
)
=
(
x
i
−
x
e
n
d
f
)
2
+
(
y
i
−
y
e
n
d
f
)
2
\bar{D}_f(p_i)=\sqrt{(x_i-x_{end}^f)^2+(y_i-y_{end}^f)^2}
Dˉf(pi)=(xi−xendf)2+(yi−yendf)2
D
ˉ
b
(
p
i
)
=
(
x
i
−
x
e
n
d
b
)
2
+
(
y
i
−
y
e
n
d
b
)
2
\bar{D}_b(p_i)=\sqrt{(x_i-x_{end}^b)^2+(y_i-y_{end}^b)^2}
Dˉb(pi)=(xi−xendb)2+(yi−yendb)2
transfer map:
该图描述的是车道线上点到相邻两个点的关系,这里通过构建当前点和相邻两个点(
p
i
b
=
(
x
i
b
,
y
i
b
)
p_i^b=(x_i^b,y_i^b)
pib=(xib,yib)和
p
i
b
=
(
x
i
b
,
y
i
b
)
p_i^b=(x_i^b,y_i^b)
pib=(xib,yib))的向量场进行描述,需要注意的是这里采样的相邻点是按照距离
d
d
d进行采样得到的,则向量场的描述为:
T
ˉ
f
(
p
i
)
=
(
x
i
f
−
x
i
,
y
i
f
−
y
i
)
\bar{T}_f(p_i)=(x_i^f-x_i,y_i^f-y_i)
Tˉf(pi)=(xif−xi,yif−yi)
T
ˉ
b
(
p
i
)
=
(
x
i
b
−
x
i
,
y
i
b
−
y
i
)
\bar{T}_b(p_i)=(x_i^b-x_i,y_i^b-y_i)
Tˉb(pi)=(xib−xi,yib−yi)
且需要满足采样的距离要求:
∣
∣
T
ˉ
f
(
p
i
)
∣
∣
2
=
∣
∣
T
ˉ
b
(
p
i
)
∣
∣
2
=
d
||\bar{T}_f(p_i)||_2=||\bar{T}_b(p_i)||_2=d
∣∣Tˉf(pi)∣∣2=∣∣Tˉb(pi)∣∣2=d
这里需要主要的是在分离汇入场景下当前点的下一个点是存在歧义的,也就是对于点
(
x
m
,
y
m
)
(x_m,y_m)
(xm,ym)它的下一个点是存在于车道线(分离汇入分别两根线分别标注)
{
(
x
1
,
y
1
)
,
…
,
(
x
m
,
y
m
)
,
…
,
(
x
n
1
1
,
y
n
1
1
)
}
\{(x_1,y_1),\dots,(x_m,y_m),\dots,(x_{n1}^1,y_{n1}^1)\}
{(x1,y1),…,(xm,ym),…,(xn11,yn11)}和
{
(
x
1
,
y
1
)
,
…
,
(
x
m
,
y
m
)
,
…
,
(
x
n
2
2
,
y
n
2
2
)
}
\{(x_1,y_1),\dots,(x_m,y_m),\dots,(x_{n2}^2,y_{n2}^2)\}
{(x1,y1),…,(xm,ym),…,(xn22,yn22)}中的,这里针对这样的歧义场景是随机选择该点的下一个点作为相邻点的,也就是从
(
x
m
+
1
1
,
y
m
+
1
1
)
(x_{m+1}^1,y_{m+1}^1)
(xm+11,ym+11)和
(
x
m
+
1
2
,
y
m
+
1
2
)
(x_{m+1}^2,y_{m+1}^2)
(xm+12,ym+12)中随机选择一个。
文中的这几个分量对于性能的影响见下表所示:

这篇文章的方法是包含3个预测头的,则其对应的监督损失函数也是对应的3个部分:二值分割损失(采用OHEM)、distance map和transfer map损失(采用SmoothL1),它们分别定义为:
L
s
e
g
=
1
N
p
o
s
+
N
n
e
g
(
∑
i
∈
S
p
o
s
y
i
l
o
g
(
p
i
)
+
∑
i
∈
S
n
e
g
(
1
−
y
i
)
l
o
g
(
1
−
p
i
)
)
L_{seg}=\frac{1}{N_{pos}+N_{neg}}(\sum_{i\in S_{pos}}y_ilog(p_i)+\sum_{i\in S_{neg}}(1-y_i)log(1-p_i))
Lseg=Npos+Nneg1(i∈Spos∑yilog(pi)+i∈Sneg∑(1−yi)log(1−pi))
L
D
=
1
N
p
o
s
∑
i
∈
S
p
o
s
L
S
m
o
o
t
h
L
1
(
D
(
p
i
)
,
D
ˉ
(
p
i
)
)
L_D=\frac{1}{N_{pos}}\sum_{i\in S_{pos}}L_{Smooth_{L_1}}(D(p_i),\bar{D}(p_i))
LD=Npos1i∈Spos∑LSmoothL1(D(pi),Dˉ(pi))
L
T
=
1
N
p
o
s
∑
i
∈
S
p
o
s
L
S
m
o
o
t
h
L
1
(
T
(
p
i
)
,
T
ˉ
(
p
i
)
)
L_T=\frac{1}{N_{pos}}\sum_{i\in S_{pos}}L_{Smooth_{L_1}}(T(p_i),\bar{T}(p_i))
LT=Npos1i∈Spos∑LSmoothL1(T(pi),Tˉ(pi))
总的损失函数就是三者相加:
L
t
o
t
a
l
=
L
s
e
g
+
L
D
+
L
T
L_{total}=L_{seg}+L_D+L_T
Ltotal=Lseg+LD+LT
在推理的过程中同时使用到3个头的信息,其具体步骤描述为:
Step1:
在二值分割结果的基础上使用point-nms选择起始种子点集合
p
s
t
a
r
t
p_{start}
pstart,对于这里种子点的选择依据现有能掌握到的信息是根据车道线点的分类概率得到的,具体可以参考(具体怎么实现的还需参考最后开源之后的实现):
def seg2keypoint(self, seg_map): # 输入为车道线分割概率
seg_map_flatten = seg_map.flatten()
# thresh = seg_map_flatten.topk(10000).values.min()
thresh, _ = self.topk(seg_map_flatten, 10000)
thresh = thresh.min()
row_series, col_series = torch.where(seg_map > thresh)
scores = seg_map[row_series, col_series]
# scores, idx = scores.sort(descending=True)
scores, idx = self.sort(scores)
# pts_pre = torch.stack([row_series, col_series], dim=-1)
pts_pre = self.stack(row_series, col_series)
pts = pts_pre[idx]
# pts_float = pts.float()
# dis_mat = (pts_float - pts_float[..., None, :]).pow(2).sum(dim=-1).sqrt()
pts_float = pts.astype(mindspore.float32)
dis_mat = self.pow(pts_float - pts_float[..., None, :], 2).sum(-1)
dis_mat = self.sqrt(dis_mat)
dis_mat = self.tmp_func(dis_mat)
# dis_mat.triu_(diagonal=1)
# keep = dis_mat.max(dim=0)[0] < self.tmp_func(self.r)
dis_mat = self.triu(dis_mat, 1)
keep = dis_mat.max(0) < self.tmp_func(self.r)
pts_ret = pts[keep]
# ret = torch.zeros_like(seg_map)
ret = self.zeroslike(seg_map)
ret[pts_ret[..., 0], pts_ret[..., 1]] = seg_map[pts_ret[..., 0], pts_ret[..., 1]]
return ret
Step2:
得到起始点
p
s
t
a
r
t
p_{start}
pstart之后便是以起始点出发依据distance map和transfer map推理得到整条车道线,这里distance map的作用其实就是确定车道线的终止位置。其可参见下图:

Step3:
对由起始点
p
s
t
a
r
t
p_{start}
pstart得到的车道线使用IoU-NMS进行去重过滤得到最后检测结果。
CULane:

CurveLanes:
