全国大学生智能车竞赛摄像头组实战解析1——从像素采集到赛道识别

张开发
2026/4/16 10:35:38 15 分钟阅读

分享文章

全国大学生智能车竞赛摄像头组实战解析1——从像素采集到赛道识别
1. 摄像头硬件选型与信号解析参加全国大学生智能车竞赛的同学们都知道摄像头是整个视觉系统的眼睛。我当年第一次参赛时在摄像头选型上踩过不少坑。现在回头看总钻风摄像头确实是性价比最高的选择它本质上是一个二维CCD传感器输出的是80x60分辨率的灰度图像。这个摄像头最特别的地方在于它的引脚设计。左边8个引脚负责输出灰度值0-255右边3个控制引脚CLK、HR、VS决定了当前输出的是哪个像素点的数据。这里有个小技巧实际使用时可以只靠VS场信号和CLK点信号就能定位像素位置HR行信号更多是作为冗余校验。// 典型的总钻风摄像头数据接收代码 uchar image[80*60]; // 图像存储数组 uint16 pixel_count 0; void VS_ISR() { // 场信号中断 pixel_count 0; // 新一帧开始 } void CLK_ISR() { // 点信号中断 image[pixel_count] GPIO_Read(); // 读取灰度值 }实测发现这种硬件触发方式比软件轮询稳定得多。记得我第一次调试时因为没处理好中断优先级导致图像出现断层。后来改用DMA直接存储帧率直接从30fps提升到60fps这个优化技巧分享给大家。2. 图像预处理与大津法二值化拿到原始图像后第一件事就是做二值化处理。这里推荐使用大津法OTSU算法它能自动计算最佳阈值。我对比过固定阈值法在赛道光线变化时大津法的稳定性要高出3倍以上。算法原理其实很形象把图像灰度直方图想象成山脉我们要找的就是两座山峰之间的山谷。具体实现时有个小技巧可以先对图像做3x3的中值滤波能有效消除单个噪点的影响。// 优化后的大津法实现 uint8 otsu_threshold(uchar *img) { uint16 hist[256] {0}; float sum 0, sumB 0; float wB 0, wF 0, mB, mF; float max 0, between; uint8 threshold 0; // 统计直方图 for(int i0; i4800; i) hist[img[i]]; // 计算总平均值 for(int i0; i256; i) sum i * hist[i]; // 寻找最佳阈值 for(int t0; t256; t) { wB hist[t]; if(wB 0) continue; wF 4800 - wB; if(wF 0) break; sumB t * hist[t]; mB sumB / wB; mF (sum - sumB) / wF; between wB * wF * (mB - mF) * (mB - mF); if(between max) { max between; threshold t; } } return threshold; }实际测试时发现当赛道出现反光时传统大津法可能会失效。我的解决方案是加入动态权重调整对图像下半部分靠近小车区域赋予更高权重这样处理后的阈值更贴合实际赛道情况。3. 赛道中线提取算法二值化后的图像就像黑白分明的赛道地图接下来要找到赛道中心线。新手常犯的错误是直接从下往上逐行扫描这样遇到弯道时容易丢失赛道。我改进的算法是双向搜索法从最底行开始找到初始左右边界向上搜索时以上一行的中线为起点向两侧搜索加入边界连续性检查防止突变// 改进的中线提取算法 void find_centerline() { uint8 left[60], right[60], center[60]; // 底部初始行处理 for(int x39; x0; x--) { if(image[59][x-1] - image[59][x] 1) { left[59] x; break; } } for(int x39; x79; x) { if(image[59][x1] - image[59][x] 1) { right[59] x; break; } } center[59] (left[59] right[59]) / 2; // 向上迭代处理 for(int y58; y0; y--) { int search_start center[y1]; // 关键点以上一行中线为起点 // 向左搜索 for(int xsearch_start; x0; x--) { if(image[y][x-1] - image[y][x] 1) { left[y] x; break; } } // 向右搜索 for(int xsearch_start; x79; x) { if(image[y][x1] - image[y][x] 1) { right[y] x; break; } } // 边界校验 if(abs(left[y]-left[y1])10) left[y]left[y1]-2; if(abs(right[y]-right[y1])10) right[y]right[y1]2; center[y] (left[y] right[y]) / 2; } }这个算法在去年区域赛的S弯道上表现特别出色相比传统方法赛道丢失率降低了70%。关键点在于加入了边界连续性约束防止单行识别错误影响整体结果。4. 特殊赛道元素处理实际比赛中最让人头疼的就是十字路口和环岛这些特殊元素。经过多次实测我发现十字路口有几个特征上方出现大面积黑色区域左右边界突然变宽中线在某个位置突然中断我的处理方案是三级判断机制初级判断检测边界突变中级判断统计上方区域黑点比例高级判断检查边界斜率变化// 十字路口判断与补线 void check_crossroad() { // 特征点检测 int top_black 0; for(int y30; y50; y2) { for(int x10; x70; x5) { if(image[y][x] 1) top_black; } } // 三重条件判断 if(abs(left[30]-left[50])30 abs(right[30]-right[50])30 top_black 15) { // 补线算法 float k_left (left[30]-left[50])/20.0; float k_right (right[30]-right[50])/20.0; for(int y50; y30; y--) { int x_left left[50] (50-y)*k_left; int x_right right[50] (50-y)*k_right; for(int xx_left; xx_right; x) { image[y][x] 0; // 补白线 } } } }在省赛时就靠这个算法我们的车在十字路口区域实现了零失误。有个细节要注意补线长度不宜过长一般20-30像素就够了否则会影响正常赛道的识别。5. 控制参数生成最后一步是把图像信息转化为控制参数。这里我推荐使用三线偏差法选取图像底部、中部和上部三个关键点的中线位置计算综合偏差。// 控制参数生成 void get_control_params() { int base 59; // 底部行 int middle 40; // 中间行 int top 20; // 顶部行 float err_base center[base] - 39.5; // 基准偏差 float err_mid center[middle] - 39.5; float err_top center[top] - 39.5; // 加权综合偏差 float total_err err_base*0.6 err_mid*0.3 err_top*0.1; // 曲率预估 float curvature (err_top - err_base)/40.0; // 发送给控制模块 send_to_pid(total_err, curvature); }这套参数生成方法在直道和弯道都有不错的表现。特别是在大弯道时加入曲率预估可以让小车提前减速这个技巧让我们在国赛时比对手平均快0.5秒/圈。调试时有个小经验先用上位机把中线显示出来观察不同速度下的识别效果。我一般会测试1m/s、2m/s、3m/s三种速度下的识别稳定性确保在全速时也不会丢线。

更多文章