文章目录
- 一、前言
- 二、结果
- 三、跟踪
- 3.1、检测输入
- 3.2、预测与运动补偿
- 3.3、第一次匹配
- 3.4、第二次匹配
- 3.5、第三次匹配
- 3.6、航迹的起始与信息的发布
- 四、后记
一、前言
- 红绿灯场景对当前无人驾驶来说是个灾难性的挑战。暂且不说复杂的十字路口,譬如简单的人行道红绿灯也算挑战。
- 这里简述下一般的处理方式:建图定位告知前方是个人行道路口,此时车子在经过红绿灯前应先停止,此时规划接受感知发送的信号,如果接收到红灯or黄灯,则车子停止。直到接受到绿灯则进行。
- 本篇会初略分享红绿灯感知包括但不限于检测+跟踪+分类。重点讲解如何稳定跟踪。
二、结果
先看结果:
视频B站链接:https://www.bilibili.com/video/BV1Vm411r7Fx/?spm_id_from=333.999.0.0
- 检测用的 yolo 系列模型,这部分已经很成熟了,主要内容是标注数据与训练。由于当前训练数据不够,可以看出除了正前方,左右两方的检测效果并不理想。
- 跟踪结合了 byteSort 与 BotSort,效果可以说相当稳定。抛开其他的不谈,对于我们重点观察的红绿灯(正前方红绿灯)可以说是稳稳的跟踪,也算遥遥领先。
- 分类用的较为简单的 卷积神经网络推理 DNN,效果算是可圈可点,也是由于数据量缺失,导致偶尔出现误检。分类类别分别为 unkonw、green、red、yellow。
在刚刚结束的首届深圳国际人工智能环卫机器人大赛,在人行道这个赛道就出现了红绿灯的考核,有不少企业的无人车就在这里栽了跟头。
放一张通宵比赛测试的图:(右一是博主)
# 三、跟踪因为是基于 ros 做的开发,同时红绿灯这个节点又包含了三个部分(检测+跟踪+分类)。
为了使代码美观、思路清晰,所以对三个部分封装。只需调用对应的一个接口就可以输出前后的消息,所以写代码前一定要把消息接口定义好。
所以跟踪代码只开放一个接口,这里就写做 update 吧。输入是检测的结果与图片,输出是跟踪后的框与id等,这里输出是用的引用的方式。
void update(std::vector<DetectResult>& objects, cv::Mat &img, std::vector<TrackResult>& tflTracks);
lightTracker->update(detectResults, image, trackResults);
三、跟踪
3.1、检测输入
第一部分肯定是对检测输入处理,无论是更换数据类型,还是保存相应的结构体 typedef struct,都是方便我们后续处理。
(1)我们这里分类保存检测的结果,分为高置信度检测的结果与低置信度的检测结果。
这是借鉴了我们 ByteTrack 匹配的方法。 ByteTrack 匹配采用了多次匹配的方法,首先将得分较高的目标框与历史轨迹相匹配,然后将得分较低的目标框与第一次没有匹配上的轨迹匹配,用于检测目标遮挡的情形。
当然这里还有自己处理的细节与亮点,我们比 ByteTrack 更多次的匹配验证与处理,效果更加完美。我们文章后面再一一道来。
if (objects.size() > 0)
{
for (auto i = 0; i < objects.size(); i++)
{
float score = objects.at(i).detConf;
STrack strack( objects.at(i), 10, true, 0.8);
if (score >= track_thresh)
{
detections.push_back(strack);
}
else
{
detections_low.push_back(strack);
}
}
}
(2)原来存在的跟踪航迹我们也做处理,区分上一次 update (匹配)过的航迹与上一帧未匹配(un_update)的航迹进行区分。
for (int i = 0; i < this->tracked_stracks.size(); i++)
{
if (!this->tracked_stracks[i].is_activated)
unconfirmed.push_back(&this->tracked_stracks[i]);
else
tracked_stracks.push_back(&this->tracked_stracks[i]);
}
这里主要是为了我们第一次做匹配做准备,区分重要 or 不重要的目标是我们可以从 ByteSort 学习到的思想,如何区分以及区分后如何处理,我们可以根据我们实际情况去操作。
3.2、预测与运动补偿
在我们匹配前,我们要对已有的所有航迹都进行一遍预测。大致的流程:输入检测结果——>预测航迹——>预测的航迹与检测结果匹配——>匹配上的预测结果更新,未匹配上的继续预测。
所以,我们在匹配之前一定要先 predict。预测之后我们对 我们目标框进行一个补偿。这个在之前博客提到过。这个借鉴了BOT-Sort思想。主要是处理相机发生剧烈抖动时。具体细节可以参考之前的博客。相应的思路与代码都有:相机运动补偿 。
3.3、第一次匹配
在我们 3.1 中我们筛选了出来了高置信度检测目标与上一次匹配过的跟踪目标。我们第一次匹配优先选择他们进行匹配。
匹配方式选用匈牙利匹配。匈牙利细节可以参考我之前的博客匈牙利匹配
计算目标与目标的方式选用了 L2 范式匹配而舍弃了传统的 iou 匹配。这主要是因为(1)红绿灯相对于人来说是小目标(2)红绿灯有时会伸缩种类不一致。
float Tracker::l2(std::vector<float> &tlwh1, std::vector<float> &tlwh2)
{
float dx, dy, dw, dh;
dx = cv::abs(tlwh1[0] - tlwh2[0]);
dy = cv::abs(tlwh1[1] - tlwh2[1]);
dw = cv::abs(tlwh1[2] - tlwh2[2]);
dh = cv::abs(tlwh1[3] - tlwh2[3]);
dx = cv::min(1.0, dx / 100.0);
dy = cv::min(1.0, dy / 100.0);
dw /= cv::max(tlwh1[2] + 1, tlwh2[2] + 1);
dh /= cv::max(tlwh1[3] + 1, tlwh2[3] + 1);
return 0.3 * dx + 0.3 * dy + 0.2 * dw + 0.2 * dh;
}
测试效果好于 iou 匹配。
3.4、第二次匹配
第一次匹配我们筛选了高置信度的检测目标与已有航迹(上一帧匹配过的)进行匹配。与高置信度匹配确保那些置信度较高的目标能够被稳定地跟踪。
这次我们选取低置信度的目标与第一次匹配没有匹配上的已有航迹匹配。distance_score 计算采用我们熟悉的 iou 匹配。
与低置信度检测框匹配主要是为了解决我们常见的遮挡与漏检的问题。
遮挡时,大概率会出现较低置信度的检测,我们不丢弃而是选择与重要的目标进行再匹配。
且低置信度目标可能包含了目标的运动趋势,利用这些消息可以提升我们跟踪的准确性与鲁棒性。
std::vector<std::vector<float>> Tracker::ious(std::vector<std::vector<float>> &atlbrs, std::vector<std::vector<float>> &btlbrs)
{
std::vector<std::vector<float>> ious;
if (atlbrs.size() * btlbrs.size() == 0)
return ious;
ious.resize(atlbrs.size());
for (int i = 0; i < ious.size(); i++)
{
ious[i].resize(btlbrs.size());
}
for (int k = 0; k < btlbrs.size(); k++)
{
std::vector<float> ious_tmp;
float box_area = (btlbrs[k][2] - btlbrs[k][0] + 1) * (btlbrs[k][3] - btlbrs[k][1] + 1);
for (int n = 0; n < atlbrs.size(); n++)
{
float iw = cv::min(atlbrs[n][2], btlbrs[k][2]) - cv::max(atlbrs[n][0], btlbrs[k][0]) + 1;
if (iw > 0)
{
float ih = cv::min(atlbrs[n][3], btlbrs[k][3]) - cv::max(atlbrs[n][1], btlbrs[k][1]) + 1;
if(ih > 0)
{
float ua = (atlbrs[n][2] - atlbrs[n][0] + 1)*(atlbrs[n][3] - atlbrs[n][1] + 1) + box_area - iw * ih;
ious[n][k] = iw * ih / ua;
}
else
{
ious[n][k] = 0.0;
}
}
else
{
ious[n][k] = 0.0;
}
}
}
return ious;
}
3.5、第三次匹配
前两次匹配大致都可以理解,那为什么还要第三次匹配呢?这是为了处理我们的[拖油瓶],哈哈哈哈!我们自己原有的航迹中有些航迹是一直可以匹配上,有些航迹可能连续好几帧都没有匹配上。按照我们之前的逻辑,我们也不能轻易的丢掉没有匹配的航迹
第三次匹配主要是处理我们那些潜在的航迹,给我们潜在的航迹一个重生的机会。这部分虽然目标不多,但也要进行处理,能救一个是一个。
所以第三次匹配的目标是,第一次匹配未匹配上的高置信度的检测目标 与 潜在的已有的航迹(上一帧未匹配上,但是还未 remove 掉的)。我这里保守设置了 10 帧才完全丢失 remove 掉航迹。
匹配计算方式同样采取了 iou 计算匹配。
3.6、航迹的起始与信息的发布
航迹的起始。
1、对于高置信度,但是未匹配上的目标,我们会把次目标放入我们的航迹管理中。
2、对前三次匹配上的目标,但是原本航迹并不是conform (上一次未 update)的目标。会重新加入我们的航迹管理中,且状态变为 Tracked。
3、去除可能相同的航迹,Tracked 的航迹可能与 Lost 的航迹有冲突,利用 iou 与航迹的生命周期进行剔除(存活了多少帧)。
void Tracker::remove_duplicate_stracks( std::vector<STrack> &resa, std::vector<STrack> &resb, std::vector<STrack> &stracksa, std::vector<STrack> &stracksb)
{
std::vector< std::vector<float> > pdist = iou_distance(stracksa, stracksb);
std::vector<std::pair<int, int> > pairs;
for (int i = 0; i < pdist.size(); i++)
{
for (int j = 0; j < pdist[i].size(); j++)
{
if (pdist[i][j] < 0.15)
{
pairs.push_back(std::pair<int, int>(i, j));
}
}
}
std::vector<int> dupa, dupb;
for (int i = 0; i < pairs.size(); i++)
{
int timep = stracksa[pairs[i].first].frame_id - stracksa[pairs[i].first].start_frame;
int timeq = stracksb[pairs[i].second].frame_id - stracksb[pairs[i].second].start_frame;
if (timep > timeq)
dupb.push_back(pairs[i].second);
else
dupa.push_back(pairs[i].first);
}
for (int i = 0; i < stracksa.size(); i++)
{
std::vector<int>::iterator iter = find(dupa.begin(), dupa.end(), i);
if (iter == dupa.end())
{
resa.push_back(stracksa[i]);
}
}
for (int i = 0; i < stracksb.size(); i++)
{
std::vector<int>::iterator iter = find(dupb.begin(), dupb.end(), i);
if (iter == dupb.end())
{
resb.push_back(stracksb[i]);
}
}
}
信息的发布。
1、第一次、第二次、第三次匹配,匹配上的目标
2、匹配上的目标,但是航迹不是 Tracked 状态的目标
四、后记
欢迎相互学习交流