1. 为什么需要赛道边界优化策略
智能车在简单直道上跑直线很容易,但遇到环岛、三岔路这些复杂路况就傻眼了。我之前用直接找中线的方法,发现车子经常跑偏甚至冲出赛道。后来才明白,必须先准确识别赛道边界,才能让车子知道哪里能走、哪里不能走。
赛道边界识别就像人开车要看马路牙子一样重要。如果连边界都找不准,再好的控制算法也是白搭。TC264芯片作为智能车常用的主控,处理摄像头图像时最头疼的就是边界识别。传统方法在直道还行,一到弯道和特殊元素就抓瞎。
八邻域法和逐行遍历法是两种主流的边界识别方案。八邻域法像侦探一样追踪边界走向,逐行遍历法则像扫地机器人一样一行行扫描。两种方法各有优劣,接下来我会结合代码详细分析它们的适用场景。
2. 八邻域法的原理与实现
2.1 什么是八邻域
想象你站在一个像素点上,周围有8个邻居包围着你。八邻域法就是通过检查这8个邻居的颜色(黑或白),来判断当前点是不是赛道边界。就像玩扫雷游戏时,要通过周围数字推测雷的位置。
具体到智能车场景,赛道通常是黑色,背景是白色。边界点就是黑色赛道与白色背景的交界处。八邻域法会从起点开始,沿着边界"爬行",记录下所有边界点的坐标。
2.2 代码实现详解
先看找起点的代码。起点位于图像底部,左右各一个:
#define WHITE_IMG 255 #define BLACK_IMG 0 #define ROW 120 #define COL 188 struct DIV { int Row[240]; int Col[240]; //存储边界坐标 }; struct DIV left, right; //左右边界 int right_flag = 0; //右起点标志 int left_flag = 0; void LeftStartFind() { for(int row=ROW-5; row>40 && left.Row[0]==254; row--) { for(int col=5; col<COL-55 && left.Col[0]==254; col++) { if(IMG_DATA[row][col]==BLACK_IMG && IMG_DATA[row][col+1]==BLACK_IMG) { if(IMG_DATA[row][col+2]==WHITE_IMG && IMG_DATA[row][col+3]==WHITE_IMG) { left.Row[0]=row; left.Col[0]=col+1; left_flag=1; } } } } }这段代码在图像底部扫描,寻找"黑-黑-白-白"的像素组合。找到后记录为左边界起点,右边界同理。
找到起点后开始边界追踪:
void search_neighborhood() { L_edge_count = 0; if(left_findflag) { L_edge[0].row = L_start_y; L_edge[0].col = L_start_x; int16 curr_row = L_start_y; int16 curr_col = L_start_x; dire_left = 0; for(int i=1; i<L_search_amount; i++) { if(dire_left!=2 && image_use[curr_row-1][curr_col-1]==BLACK && image_use[curr_row-1][curr_col]==WHITE) { //左上黑,右边白的情况 curr_row--; curr_col--; L_edge[i].row = curr_row; L_edge[i].col = curr_col; } // 其他6种情况类似... } } }这段代码会根据当前点周围8个像素的情况,决定下一个边界点的位置。就像走迷宫时用手摸着墙走,总能找到出口。
2.3 实战经验分享
在实际使用中,我发现几个关键点:
- 图像二值化阈值要调准,否则边界识别全是错的
- 遇到十字路口要给图像加黑框,防止边界丢失
- 限制最大搜索点数,避免死循环
- 记录上一个点的方向,防止回头走
八邻域法的优势是边界连续性好,适合弯道追踪。缺点是计算量稍大,在复杂路口容易迷路。
3. 逐行遍历法的原理与实现
3.1 基本思路
如果说八邻域法是沿着边界"爬行",逐行遍历法就是从上到下"扫描"每一行。它在每行寻找"黑-黑-白-白"的跳变点作为边界,就像用尺子一行行量赛道宽度。
这种方法实现简单,计算量小,适合处理直道和简单弯道。但在复杂路况下,边界连续性不如八邻域法。
3.2 代码解析
void left_jump() { if(left.Row[0] == 254) return; for(int pin=1; pin<240; pin++) { int row = left.Row[0] - pin; if(row <= row_lim) break; int colmin = left.Col[pin-1] - 10; int colmax = left.Col[pin-1] + 10; for(int col=colmin; col<=colmax; col++) { if(IMG_DATA[row][col]==BLACK_IMG && IMG_DATA[row][col+1]==BLACK_IMG && IMG_DATA[row][col+2]==WHITE_IMG && IMG_DATA[row][col+3]==WHITE_IMG) { left.Row[pin] = row; left.Col[pin] = col+1; break; } } } }代码逻辑很直观:
- 从上一行的边界点出发
- 在当前行搜索±10像素范围
- 找到"黑黑白白"模式就记录为边界
- 找不到就结束搜索
3.3 性能优化技巧
经过多次实测,我总结了几个优化点:
- 搜索范围不要太大,10-15像素足够
- 遇到断点可以尝试上下几行插值补全
- 对边界点做平滑滤波,减少抖动
- 可以结合跳变点数量判断特殊元素
逐行遍历法在TC264上运行效率很高,适合资源受限的场景。但遇到环岛等复杂路况时,建议结合其他方法使用。
4. 两种方法的对比与选型建议
4.1 性能对比
| 指标 | 八邻域法 | 逐行遍历法 |
|---|---|---|
| 计算复杂度 | 较高 | 较低 |
| 内存占用 | 较大 | 较小 |
| 边界连续性 | 好 | 一般 |
| 抗干扰能力 | 较强 | 较弱 |
| 特殊元素处理 | 较好 | 需要辅助判断 |
4.2 适用场景
八邻域法适合:
- 弯道多的赛道
- 需要高精度边界
- 处理器资源充足
逐行遍历法适合:
- 直道为主的赛道
- 低功耗场景
- 需要快速原型开发
4.3 混合使用方案
在实际项目中,我经常混合使用两种方法:
- 平时用逐行遍历法快速扫描
- 检测到弯道时切换八邻域法
- 特殊元素单独处理
这种方案在TC264上实测效果很好,既能保证速度,又能处理复杂路况。具体实现可以设置一个弯道判断阈值,当边界曲率超过阈值就切换算法。
5. 常见问题与调试技巧
5.1 边界丢失问题
这是最让人头疼的问题,通常有几个原因:
- 二值化阈值设置不当 - 建议用动态阈值算法
- 光照变化影响 - 加装遮光罩或自动曝光
- 赛道反光 - 尝试不同摄像头角度
- 算法参数不合适 - 调整搜索范围和步长
我常用的调试方法是把边界点标记出来显示在屏幕上,这样一眼就能看出问题在哪。
5.2 误识别处理
误识别经常发生在赛道交叉口附近,解决方法包括:
- 增加边界有效性检查
- 结合历史数据滤波
- 设置最大允许跳变距离
- 对特殊区域做特殊处理
比如可以记录连续几帧的边界位置,如果某点突然跳变很大,就认为是误识别。
5.3 性能优化
在TC264上优化性能的几个技巧:
- 使用查表法加速二值化
- 限制处理区域(ROI)
- 适当降低图像分辨率
- 使用DMA传输图像数据
- 关键函数用汇编优化
我曾经通过这几种方法,把处理时间从15ms降到了6ms,效果非常明显。
6. 进阶优化方向
6.1 动态阈值算法
固定阈值在光照变化时效果很差。可以尝试:
- 大津法自动阈值
- 局部自适应阈值
- 基于直方图的阈值选择
我实现过一个简化版的大津法,虽然计算量稍大,但适应性强很多。
6.2 边界预测与滤波
利用历史数据可以提升稳定性:
- 卡尔曼滤波预测边界位置
- 滑动窗口平均滤波
- 基于运动模型的预测
这些方法在高速过弯时特别有用,可以减少边界抖动。
6.3 特殊元素识别
对于环岛、十字路等特殊元素,需要特殊处理:
- 识别路口特征模式
- 预置特殊处理逻辑
- 结合其他传感器数据
我通常会在代码里预留特殊元素的处理接口,方便后续扩展。
在实际调车过程中,边界识别是最基础也最关键的一环。建议先用简单方法跑起来,再逐步优化。TC264的性能完全足够实现一个鲁棒的边界识别系统,关键是要多测试、多调整参数。