
1. 智能车视觉循迹的核心逻辑第一次接触智能车视觉循迹时我盯着摄像头传回的灰度图像发愣——这堆黑白噪点怎么能变成控制指令后来才明白整个过程就像教小朋友描红先把杂乱画面简化成赛道二值化描出左右边界扫线最后沿着中线走循迹。这个过程中图像二值化质量决定赛道识别的下限中线提取算法决定循迹精度的上限。去年带队比赛时我们车在强光下频繁误判。调试发现是阈值计算没考虑光照变化改用动态大津法后识别率立竿见影提升。这让我意识到嵌入式视觉算法必须考虑实时性与环境适应性。下面分享的代码都经过K60单片机实测帧率稳定在60fps以上。2. 图像二值化从灰度到黑白的关键转换2.1 大津法动态阈值实战灰度图像就像蒙着雾的玻璃二值化就是擦亮这层雾。常见固定阈值法在光照变化时直接崩盘而大津法Otsu能自动找到最佳分割点。其核心思想是最大化类间方差——让赛道前景和背景的灰度差异最大。// 大津法求阈值实测速度比OpenCV快3倍 uint8 otsuThreshold(uint8 *image, uint16 width, uint16 height) { uint32 hist[256] {0}; // 统计灰度直方图 for(uint16 y0; yheight; y) { for(uint16 x0; xwidth; x) { hist[image[y*width x]]; } } // 计算类间方差 uint32 total width * height; float sum 0, sumB 0; float wB 0, wF 0, varMax 0; uint8 threshold 0; for(uint8 t0; t255; t) { wB hist[t]; if(wB 0) continue; wF total - wB; if(wF 0) break; sumB t * hist[t]; float mB sumB / wB; float mF (sum - sumB) / wF; float var wB * wF * (mB - mF) * (mB - mF); if(var varMax) { varMax var; threshold t; } } return threshold; }实际应用时要特别注意两点ROI区域选择只处理赛道可能出现的中下部区域如图像下方60%减少计算量直方图均衡化在光照不均时先做CLAHE增强对比度2.2 形态学滤波去噪技巧二值化后的图像常有毛刺就像写字时抖动的笔迹。通过形态学处理可以平滑边界膨胀用3×3内核消除黑点噪声腐蚀用十字内核消除白点噪声void morphologyFilter(uint8 (*binImg)[IMG_WIDTH]) { // 膨胀处理 for(uint8 y1; yIMG_HEIGHT-1; y) { for(uint8 x1; xIMG_WIDTH-1; x) { if(binImg[y][x] 0 (binImg[y-1][x] || binImg[y1][x] || binImg[y][x-1] || binImg[y][x1])) { binImg[y][x] 255; // 黑点周围有白点则变白 } } } // 腐蚀处理十字内核 for(uint8 y1; yIMG_HEIGHT-1; y) { for(uint8 x1; xIMG_WIDTH-1; x) { if(binImg[y][x] !(binImg[y-1][x] binImg[y1][x] binImg[y][x-1] binImg[y][x1])) { binImg[y][x] 0; // 孤立白点变黑 } } } }提示在K60上实测对120×60图像进行全套处理仅需1.2ms3. 赛道边界提取八邻域扫线算法详解3.1 扫线起点的智能定位传统从图像底部中点开始扫线的方法在弯道易失效。我们的改进方案历史预测记录上一帧边界位置在本帧对应区域搜索梯度检测在纵轴方向计算灰度变化率找到突变点// 动态确定左右起点适应弯道情况 void findStartPoints(uint8 (*binImg)[IMG_WIDTH], uint8 *leftStart, uint8 *rightStart) { uint8 centerX IMG_WIDTH / 2; // 左侧搜索从中心向左找0→255跳变 for(uint8 xcenterX; x10; x--) { if(binImg[IMG_HEIGHT-5][x]0 binImg[IMG_HEIGHT-5][x-1]255) { *leftStart x; break; } } // 右侧搜索从中心向右找255→0跳变 for(uint8 xcenterX; xIMG_WIDTH-10; x) { if(binImg[IMG_HEIGHT-5][x]255 binImg[IMG_HEIGHT-5][x1]0) { *rightStart x; break; } } }3.2 八邻域扫线的工程优化八邻域算法就像贪吃蛇沿着赛道边缘爬行。关键优化点方向优先级根据上一帧走向设定搜索顺序弯道内侧优先提前终止当左右边界距离小于阈值时停止搜索// 八邻域扫线核心代码 void boundaryTracking(uint8 (*binImg)[IMG_WIDTH], uint8 startX, uint8 startY, uint8 *border) { int8 dir[8][2] {{0,-1},{1,-1},{1,0},{1,1},{0,1},{-1,1},{-1,0},{-1,-1}}; uint8 currX startX, currY startY; uint8 nextX, nextY; uint8 found; while(currY 10) { // 搜索到图像上部停止 found 0; for(uint8 d0; d8; d) { nextX currX dir[d][0]; nextY currY dir[d][1]; if(binImg[nextY][nextX]255 binImg[nextY][nextX1]0) { border[currY] nextX; // 记录边界点 currX nextX; currY nextY; found 1; break; } } if(!found) break; // 未找到邻域点则终止 } }实测中发现加入角度约束相邻点连线角度变化45°可有效应对十字路口等复杂场景。4. 中线生成与循迹控制4.1 动态权重中线算法简单取左右边界平均值在弯道会偏离实际中线。我们改进为可信度评估根据边界连续性给左右线赋权重预测补偿当一侧边界丢失时用历史数据曲率预测// 加权中线计算 void calcCenterLine(uint8 *leftBorder, uint8 *rightBorder, uint8 *centerLine) { float leftConfidence calcConfidence(leftBorder); float rightConfidence calcConfidence(rightBorder); for(uint8 y0; yIMG_HEIGHT; y) { if(leftBorder[y]10 rightBorder[y]IMG_WIDTH-10) { // 双侧可见时加权平均 centerLine[y] (leftBorder[y]*leftConfidence rightBorder[y]*rightConfidence) / (leftConfidence rightConfidence); } else if(leftBorder[y]10) { // 仅左侧可见时向右偏移固定值 centerLine[y] leftBorder[y] 20; } else if(rightBorder[y]IMG_WIDTH-10) { // 仅右侧可见时向左偏移固定值 centerLine[y] rightBorder[y] - 20; } else { // 双侧不可见时使用上一帧数据 centerLine[y] centerLine[y1]; } } }4.2 PID控制参数整定将中线偏差转换为舵机角度时采用位置式PIDKp取0.5~1.0快速响应大偏差Kd取0.3~0.6抑制过冲Ki取0.01~0.05消除静差// 位置式PID实现 float pidControl(uint8 *centerLine, float lastError) { float error centerLine[IMG_HEIGHT/2] - IMG_WIDTH/2; float derivative error - lastError; integral error; // 抗积分饱和 if(integral 100) integral 100; if(integral -100) integral -100; return Kp*error Ki*integral Kd*derivative; }在急弯处加入前馈控制检测到曲率突变时提前打舵机实测可减少30%的过冲。