公司动态
智能车图像处理(一) 从灰度到赛道:全局阈值算法的实战与选型
1. 智能车图像处理入门从灰度到二值第一次拿到总钻风摄像头时那种兴奋感至今难忘。接上逐飞的底层库看着TFT显示屏上跳出的120×188分辨率图像虽然只是简单的灰度画面但这就是我们智能车的眼睛。你可能也和我当初一样好奇这一片灰蒙蒙的画面怎么就能让小车识别赛道呢这里有个生活化的比喻想象你在夜晚用手电筒照地面浅色地砖会比深色地砖反射更多光线。摄像头也是这个原理白色赛道会反射更多光线在灰度图像中呈现较亮的像素接近255而蓝色背景则呈现较暗的像素接近0。我们的任务就是找到那个分水岭——把亮暗像素分开的阈值。全局阈值算法就像是用同一把尺子丈量整个图像。在固定光照的实验室环境下特别管用因为赛道和背景的灰度差异稳定。我整理了几个关键数据对比场景特征白色赛道像素值蓝色背景像素值理想阈值区间标准实验室环境180-22050-80100-130强光照射区域230-25590-120150-180阴影区域120-15030-6080-100提示实际阈值需要根据具体摄像头和灯光条件调整建议先用图像调试工具观察灰度分布2. 迭代法阈值计算像调收音机一样找最佳阈值2.1 算法原理动态平衡的艺术迭代法的精妙之处在于它像老式收音机调频一样自动寻找最佳阈值。我这样向新手解释假设你要把教室里的同学按身高分成两组可以先随便猜个中间值比如170cm然后计算两组平均身高再取新平均值作为新分界线如此反复直到分界线稳定。具体到代码实现这个猜阈值-计算-调整的过程非常直观void Iteration_Threshould(void) { uint16_t i,j,N00,N10,flag0; float T0,T1,T2,T_center; uint32_t S00,S10; T2 BlackThres; // 初始阈值建议取图像平均灰度 do { for(i0;iROW_H;i) { for(j0;jCOL_W;j) { if(image[i][j] T2) { // 低于阈值归为背景 S0 image[i][j]; N0; } else { // 高于阈值归为赛道 S1 image[i][j]; N1; } } } T0 S0/N0; // 背景组平均灰度 T1 S1/N1; // 赛道组平均灰度 T_center (T0T1)/2; // 新阈值 // 判断阈值变化是否小于误差范围 if(fabs(T2 - T_center) ERROR) flag1; else flag0; T2 T_center; BlackThres T2; } while(flag); }2.2 实战技巧避开那些坑在实际调参时我踩过几个典型的坑初始阈值选择不要直接用128应该先计算图像平均灰度。比如对于实验室场景初始值设为100会更快收敛误差范围ΔT设置2-5比较合适太小会导致迭代次数过多太大可能提前终止极端情况处理当图像全白或全黑时需要加入保护逻辑防止除零错误有次比赛前夜我们的迭代法突然失效最后发现是摄像头镜头沾了灰尘导致整体灰度下降。后来我在代码里加入了灰度校验机制// 检查图像是否有效 if(avg_gray 30 || avg_gray 220) { return DEFAULT_THRESHOLD; // 启用备用阈值 }3. 大津法Otsu数学家的优雅方案3.1 类间方差最大化的智慧大津法的精妙在于它把阈值选择转化为数学优化问题。用高中生能懂的话说就像把黑白芝麻分开找到那个让两堆芝麻差异最大化的分界线。具体来说就是计算每个可能阈值对应的类间方差选择使方差最大的阈值。算法实现时有个优化技巧不需要遍历所有像素利用灰度直方图能大幅减少计算量。以下是经过实测的优化版本uint8 Otsu_Threshold(uint8 *image, uint16 col, uint16 row) { uint16 hist[256] {0}; float prob[256], omega[256]; float max_var 0; uint8 best_thresh 128; // 统计灰度直方图可间隔采样加速 for(int i0; irow; i2) { for(int j0; jcol; j2) { hist[image[i*colj]]; } } // 计算概率分布 float total (row/2)*(col/2); for(int i0; i256; i) { prob[i] hist[i] / total; } // 寻找最佳阈值 for(int t0; t256; t) { omega[t] 0; float mu10, mu20, sigma0; // 计算背景类概率 for(int i0; it; i) omega[t] prob[i]; if(omega[t]0 || omega[t]1) continue; // 计算背景均值 for(int i0; it; i) mu1 i * prob[i]; mu1 / omega[t]; // 计算前景均值 for(int it1; i256; i) mu2 i * prob[i]; mu2 / (1-omega[t]); // 计算类间方差 sigma omega[t]*(1-omega[t])*(mu1-mu2)*(mu1-mu2); if(sigma max_var) { max_var sigma; best_thresh t; } } return best_thresh; }3.2 性能优化实战在K60芯片上跑大津法我总结了几条加速经验降采样处理120×188的图像可以间隔2像素采样速度提升4倍灰度分级将256级灰度压缩到64级几乎不影响效果查表法预计算概率乘积项提前终止当sigma开始下降时立即终止循环实测数据对比优化方法原耗时(ms)优化后(ms)内存占用原始算法12.5-1.2KB降采样2×212.53.21.2KB64级灰度12.52.1256B综合优化12.51.8320B4. 二值化实战从理论到赛道4.1 代码实现要点得到阈值后二值化操作就简单了。但要注意这些细节void Binarization() { for(int i0; iMT9V03X_CSI_H; i) { for(int j0; jMT9V03X_CSI_W; j) { // 注意这里是原始图像与阈值比较 if(mt9v03x_csi_image[i][j] threshold) { image_deal[i][j] WHITE; // 赛道 } else { image_deal[i][j] BLACK; // 背景 } // 同时显示到LCD image_showlcd[i][j] image_deal[i][j]; } } }4.2 特殊场景处理遇到这些情况时需要特殊处理反光区域在赛道标记线附近添加局部阈值补偿阴影过渡采用滞后阈值法类似Canny边缘检测摄像头噪点二值化前先进行3×3中值滤波一个实用的抗干扰技巧是差比和算法这是逐飞库里的黑科技// 差比和公式|A-B|/(AB) if(abs(pixelA - pixelB) * 100 / (pixelA pixelB) threshold) { // 判断为边缘 }最后要提醒的是二值化只是图像处理的第一步。在实际比赛中我们还需要处理赛道边界提取十字路口识别环岛特征判断坡道检测等这些后续处理的质量很大程度上取决于二值化的效果。建议在调试时保存原始图像和对应的二值化结果建立案例库供后续分析。