第19届全国大学生智能车竞赛——有关单边桥的一些思考和方法
写在前面
这是葱酱发布的第一篇正式的技术类贴文哦,写稿的时候状态不是很好,可能会有很多疏漏的地方呢,还请大家在评论区指正哦~
关于单边桥
有关单边桥的资料几乎在网上无法查找到,笔者参加的组别是摄像头基础组,这个组别是安徽省赛区组织的省级参赛组别,不设国赛。相比于镜头组和完全模型组,我们的车子是使用差速法控制的三轮车,没有多余的加速度/编码/陀螺仪传感器,也不允许自己加以改装。尽管如此,笔者仍认为该组别对于初学者而言,在图像处理方面的学习和调试仍有着一定的挑战性,加之单边桥元素的资料稀少,基本为笔者自创,故撰写成博文,供大家参考。
单边桥,顾名思义,就是用一侧车轮轧过的桥梁。具体效果请参见下图。
关于单边桥的设计和评分细节,比赛规则做出了如下要求:
单边桥使用路肩制作为等腰梯形形状。距离赛道黑色边界内边沿2.5cm,单边桥宽度5cm,长度45cm,2个斜坡长度相同,平台长度25cm。
评分细节:智能车B识别磁标②,表示前方50cm±2cm右侧有单边桥,要求智能车单轮通过单边桥行驶(即单轮始终处于单边桥上)。(满分20分)
——全国大学生智能汽车竞赛安徽赛区基础组比赛规则
综上所述,我们可以得知:单边桥位于赛道的右侧边缘,在检测到单边桥的特征后,车子应调整姿态,完成转向->对准->直行过桥->下桥->姿态调整的全过程,那么就让我们来愉快的开始吧!
问题的解决思路
单边桥的识别
在车辆位于单边桥之前时,由于道路标线的扫描机制是由中线的顶点向左右延伸,获取两侧的界限之后再合成新的中线顶点,由此循环往复。因此,在遇到道路右侧的单边桥后,图像的中线会出现一个向左的突变,再出现一个向右的突变。我们先捕捉这个向左的突变点。
寻找该突变,只需又下往上遍历,直到找到一个符合以下条件的点,循环停止,判定进入单边桥:
- 下方每个y索引的横坐标差量不大于2
- 上方每个y索引的横坐标差量不大于2
- 在突变的位置,上方的横坐标比下方横坐标小6个像素以上
这样,我们就能初步判定单边桥的特征了,但是,这样的突变点很容易在其他路况中出现,造成误判。因此,我引入了一个二级判断方案,即识别到中线的变化之后,在一个适当的参考点,从右到左进行行扫描,统计上升沿和下降沿,一般,如果可以发现上升-下降-上升三个明显的跳变,即认为是单边桥。
对应的代码如下。
1 | for(i=IMAGEWID-3;i>IMAGEWID/2+15;i--){ |
这样,就完成了下方特征点的抓取和单边桥的判断,单边桥的状态变为true
,阶段设为1,开始执行上桥的动作。
来自笔者的碎碎念:
将摄像头的视野抬高并且适当左转,可以让前瞻更开阔,中线偏向右侧。更利于单边桥的捕获。
另外!!减小阈值会显著增加灵敏度,但误判的概率也会同时增加。
上桥
发现单边桥时,车子应处于巡线直行的状态,此时应该执行一系列动作,让车头准确对准单边桥。我把动作分为2步。
粗略对准
这个步骤使车辆快速向右倾斜,对准单边桥,步骤为一个固定动作,期间不使用摄像头。对准后,阶段标志位变为2。
车辆执行了右转->回正->直行的操作,以下是对应的代码。
1 | if(bridge_stage == 1){ //检测到第一个突变,向右打角 |
精确对正
执行完1阶段后,离桥头仍有一定距离,摄像头可以看见中线从左向右的突变点。我的方案选取了一条较为合适的固定中线参考点,依据图像的左边缘和单边桥左边缘作为左右边界,生成了新的中线,且用这组数据进行寻迹操作,减小误差,使车子直行上单边桥。
其中,由单边桥确定的右侧边界需要进行补线操作,此时判定函数不断对图像扫描,当发现无法在限制范围内检测出单边桥的特征时,开始进行下桥的操作。
代码如下,其中补线等功能函数,我全部整理在了文末,此处展示代码的逻辑。
1.补线和修改判定线数据部分
1 | else if(bridge_flag == 1 && bridge_stage == 2){ |
2.判定函数:不断检测单边桥是否存在于视野内,否则跳到第三阶段,开始直行通过单边桥。
1 | else if(bridge_flag == 1){ |
这样,智能车就可以在车轮向右偏转之后,在单边桥处于视野中的一定时间内进行一些细微的姿态调整,以便让车轮平稳的压在单边桥上。
下桥
在单边桥的上边界在视野消失后,执行一段固定的代码,完成下桥的动作。代码如下:
1 | else if(bridge_stage == 3){ |
为什么要有一段代码motor_motion(3000, 1400, SPEED_PARAM_INIT);
,给电机施加一个比较大的右转动作呢?这是因为在车辆以较快的速度下桥时,由于姿态原因以及受力作用,车身姿态并非保持直行,而是偏向左侧冲出。因此,在完成一段时间的直行后,应当立马执行一个右转操作以调整姿态。
自此,单边桥的基础操作全部完成。调整车身姿态之后,摄像头打开并将标志位清空,恢复正常行驶。
一些补充
注意!
这些问题是笔者在调试车模时发生的偶然事件,参考价值有限。请选择性阅读本段。
笔者的代码经过测试,在同时开启十字路口、斑马线、避障、直角弯四种判断的情况下,基本不会和其他路况特征发生冲突。但仍然有一些特殊情况。比如在比较复杂的路段(前瞻可能捕捉到过远的场景)行驶时,尤其是经过十字路口、急转弯或者道路边缘存在未粘贴好的情况,有可能会导致误触发。解决这个问题的思路是引入了一个优先级判断机制,将单边桥的判断函数放在了比较靠后的位置,函数的大体实现如下。
1 | //------------------------------------------------------------------------------------------------------------------- |
另外,我的车子出现了上桥卡顿时传感器(如TOF、摄像头)无法继续获取数据的问题,经过痛苦的排查和测量,确定了问题可能来自电机的负载过大,以及电机驱动器发生了感应电动势的回流。从而致使MCU与传感器通信时,数据帧遭到破坏,从而导致了寄存器的配置错误,无法继续通过指令获取数据。对于这种情况,我在单边桥的下桥函数执行完毕之后将标志位bridge_stage
设置为4,同时使循环中的一个静态变量自增。此时车子正常寻迹,一定时间之后才启动传感器,并且减缓电机的转速,这样就可以解决传感器因为电机而死机的严重问题了。
总结
单边桥是一个简单而颇具讲究的比赛条目,对于基础组的车辆,不完善的硬件又加大了处理各种复杂路况条件的难度。在本次基础组的实践中,我从一无所知,到接触智能车的图像处理算法、简单的控制算法以及对参数的调节,重写了几乎全部的代码,也让自己的实践经历又添上了一笔颜色,在此也感谢学长的宝贵建议,以及各位读者的指正,愿今后参加更加高级的组别,不断完善技能树,取的更大的进步!
附录
这是本文使用的一些结构体的声明和函数的定义。
结构体定义
道路标线结构体:1
2
3
4
5typedef struct{ //存放赛道定位线的结构体
unsigned char Left_line_array[IMAGEWID];
unsigned char Middle_line_array[IMAGEWID];
unsigned char Right_line_array[IMAGEWID];
}Linedata_Typedef;补线
斜率计算:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24//-------------------------------------------------------------------------------------------------------------------
// @brief 最小二乘法求斜率
// @param x,y: 三对拟合直线上的坐标
// @return float: 直线斜率
// Sample usage: 在单边补线处计算边界斜率
//-------------------------------------------------------------------------------------------------------------------
float getAverageK(uint8 x1,uint8 y1,uint8 x2,uint8 y2,uint8 x3,uint8 y3){
int sum_x,sum_y,sum_xy,sum_x2;
int m_numerator,m_denominator;
int n=3;
float k;
sum_x=x1+x2+x3;
sum_y=y1+y2+y3;
sum_x2=x1*x1+x2*x2+x3*x3;
sum_xy=x1*y1+x2*y2+x3*y3;
m_numerator = n * sum_xy - sum_x * sum_y;
m_denominator = n * sum_x2 - sum_x * sum_x;
k = m_numerator / m_denominator;
return k;
}按照斜率对标线结构体补线:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43//-------------------------------------------------------------------------------------------------------------------
// @brief 单拐角补线
// @param linedata: 标线结构体
// direct: 标线方向,0x01为左,0x02为右
// yPos_start: 纵坐标起点
// yPos_end: 纵坐标终点
// k: 预设(如最小二乘估计)的斜率
// @return void
// Sample usage: 补线,适用于十字路口的二阶段
//-------------------------------------------------------------------------------------------------------------------
void editLineMap_K(Linedata_Typedef* linedata,char direct,uint8_t yPos_start,uint8_t yPos_end,float k)
{
//发散式补线
float b;
int i;
//x = ky+b
if(direct == 0x01){
b = linedata->Left_line_array[yPos_start] - k*yPos_start;
}
else {
b = linedata->Right_line_array[yPos_start] - k*yPos_start;
}
//链接
if(yPos_start<=yPos_end){
for(i=yPos_start;i<=yPos_end;i++){
if(direct == LEFT_BOUNDARY)
linedata->Left_line_array[i] = k*i+b;
else {
linedata->Right_line_array[i] = k*i+b;
}
}
}
else{
for(i=yPos_end;i<=yPos_start;i++){
if(direct == LEFT_BOUNDARY)
linedata->Left_line_array[i] = k*i+b;
else {
linedata->Right_line_array[i] = k*i+b;
}
}
}
}