十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

智能车走马观碑组:目标识别与绕行策略的嵌入式实践

智能车走马观碑组:目标识别与绕行策略的嵌入式实践 智能车竞赛做到“走马观碑”这个组别很多人第一反应是“识别目标板然后绕过去”但真正跑过赛道才知道这个项目说简单可以很简单——塞一个灰度传感器、检测到黑块就绕说难也可以非常难——要在高速入弯前稳定识别、精确判断绕行方向、还要保证绕完之后不丢线、不冲出去。这篇文章我会把我实际调试中积累的经验拆开来讲包括目标板的图像识别思路、三种绕行策略的优缺点对比、以及可以直接抄走的嵌入式C代码尽量让正在备赛或者准备入坑的朋友少走几周弯路。先说清楚这个任务的核心链路。走马观碑组的本质是视觉感知 运动决策两件事。感知端要把“前方出现目标板”变成一个可靠的二值信号决策端要把这个信号变成“绕左还是绕右、打多少角度、什么时候回正”的一组动作。很多队伍挂在这一步不是因为识别不出来而是识别率不稳定——十次里有八次能识别剩下两次失误直接导致车队没成绩。所以这篇文章里我会花比较多篇幅讲如何把识别做到“几乎不出错”然后再谈绕行策略的取舍。这个内容适合正在备赛的智能车队伍、对嵌入式视觉入门感兴趣的同学、以及想了解轻量级图像处理在单片机上怎么落地的开发者。我做这个项目用的是某款常见的总钻风摄像头 主控单片机方案没有用树莓派或者K210这类带系统的方案原因后面会详细说但可以先给结论——在竞赛场景下算法越简单、越确定、越不带“神经网络味道”越容易稳定出成绩。1. 项目整体设计与思路拆解1.1 走马观碑组的任务本质先理解“走马观碑”这个名字的含义。它取自“走马观碑”这个典故形容快速行进中仍然能看清碑文。放到智能车竞赛里就是赛车在高速行驶过程中必须识别赛道边缘或者赛道中出现的特定标识物也就是目标板并根据目标板的类型、位置、姿态做出对应的避让动作。这和普通的“巡线 避障”有一个关键差别避障通常只关心“有没有障碍物”而走马观碑还要关心“目标板到底是什么状态”。第21届、第22届的规则里目标板往往带有不同方向的特征比如十字标记、斜线标记、或者不同颜色的底色。这就意味着你的识别不能只看“面积够不够大”还要看“形状对不对、特征符不符”。1.2 方案选型为什么走纯视觉而不是传感器融合当时我们队里出现过一次激烈讨论要不要在车头加一排红外对管或者激光测距辅助判断目标板与车的相对位置最后的结论是不加。原因有三点目标板在竞赛场地内是平面标识物不是立体障碍物红外对管的检测逻辑很难区分“目标板”和“赛道边界波浪纹”。传感器融合需要额外的标定工作而赛场上的光线条件、地面材质会直接影响红外反射率视觉方案至少可以通过图像特征来过滤干扰。竞赛规则内的目标板是有标准尺寸和标准颜色的这在客观上给视觉识别提供了非常强的先验信息——不用去做通用目标检测只需要做一个“针对特定标识的专用检测器”。所以最终方案是单车载摄像头 主控单片机跑图像处理 编码器辅助速度闭环。我甚至不建议在识别环节加入编码器或者陀螺仪信息因为目标板识别本质上是图像域的任务和车跑了多远、转了多少度没有直接关系加进来反而会让状态判断变复杂。1.3 核心流程拆解整个系统从摄像头取帧到舵机打角的链路是这样的摄像头采集一帧灰度图分辨率通常取120x160或者188x120视主控性能而定。对图像做二值化把目标板的深色边框或特征标记从浅色背景中分离出来。提取连通域或者叫“斑块”用面积、长宽比、填充率这些几何特征筛掉噪声。对筛选出的候选区域做特征判定判断它是不是目标板、目标板的方向状态。计算目标板在图像中的横向位置映射成“左绕 / 右绕”的决策。进入绕行状态机控制舵机完成绕行动作结束后恢复到正常巡线状态。这套流程的核心难点集中在第3步和第4步因为摄像头看到的东西永远是“带噪声的”尤其是光线变化引起的二值化阈值漂移。下面我会详细讲我用的是什么方法。2. 目标板识别的核心实现2.1 为什么选择灰度二值化而不是彩色识别很多新手队伍一开始会纠结“要不要识别颜色”。这里我先泼一盆冷水竞赛场地的光线条件远比你想象的恶劣。室内赛场顶灯频闪、窗户反光、甚至旁边队伍的车壳反光都会让彩色图像的颜色分布发生剧烈偏移。你今天标定的红色阈值明天上午可能就失效了。所以我最终采用的是灰度图 自适应二值化方案。目标板的颜色虽然五花八门但它的核心特征往往是“与赛道背景有足够的灰度差”。换句话说不管你用的是蓝底白十字还是白底红十字只要目标板的边框、内部标记与赛道灰度对比明显灰度二值化就能把它分离出来。灰度方案还有一个好处处理速度快。以总钻风摄像头为例输出的一帧灰度图在120x160分辨率下只有不到2万个像素点即使做全图遍历二值化在单片机上也能轻松跑满30帧/秒以上这为后面的绕行决策留了充足的算力余量。2.2 自适应二值化与固定阈值的取舍我最早用的是固定阈值二值化比如灰度大于80算白、小于80算黑。实测下来有个很典型的问题上午十点的阳光照在赛道上阈值80还勉强能用下午三点反光严重赛道白色区域的灰度能飙到200以上但阴影里的灰度可能只有不到50同一个阈值根本没有办法同时处理这两种情况。我的解决办法是局部自适应阈值 全局平均灰度偏移补偿。最简单有效的做法是计算当前帧全图的灰度均值然后用这个均值动态调整二值化阈值。比如uint8_t compute_threshold(uint8_t *image, uint16_t width, uint16_t height) { uint32_t sum 0; uint16_t total width * height; for (uint16_t i 0; i total; i) { sum image[i]; } uint8_t mean sum / total; // 基准阈值 均值 固定偏移偏移量需要实车调试 int16_t threshold (int16_t)mean 18; if (threshold 0) return 0; if (threshold 255) return 255; return (uint8_t)threshold; }这里“18”的偏移量不是拍脑袋定的它表示目标板的深色特征与赛道背景的最小灰度差期望值。你可以在你们场地上做一次灰度直方图统计取“目标板深色区域灰度峰值”和“赛场背景灰度峰值”的中值再和全局均值比较就能算出一个适合自己场地环境的偏移量。这是个非常实用的经验不要追求一个万能阈值而是要追求一个“跟随环境变化”的阈值。目标板和场地是相对固定的它们的灰度差不会因为光线增强而消失所以自适应阈值在理论上一定能找到目标板。2.3 连通域提取与噪声过滤二值化之后图像里往往不会只有目标板一个白色斑块地面上可能还有一些杂色、轮胎印、观众席透进来的光斑都会形成面积不小的连通域。这时候就要用几何特征做筛选。我最常用的筛选条件是面积限制目标板离车有一定距离在图像里占据的像素面积有大致范围。比如50cm外是120个像素80cm外是50个像素我通常设置最小面积40像素、最大面积不做硬限制但会结合“高宽比”来过滤。高宽比竞赛标准目标板通常是正方形或者横长方形所以连通域的外接矩形宽高比应该在0.5到2.0之间。一条长长的轮胎印在高宽比上就会被排除掉。填充率目标板是规则的几何图形像素面积与外接矩形面积的比值一般不低。如果填充率低于某个值比如0.4就说明这个斑块大概率不是完整的目标板。以下是核心的筛选函数实现typedef struct { uint16_t area; uint16_t min_x; uint16_t max_x; uint16_t min_y; uint16_t max_y; } blob_t; uint8_t is_target_board(blob_t *blob) { // 面积过滤至少40像素 if (blob-area 40) return 0; uint16_t width blob-max_x - blob-min_x 1; uint16_t height blob-max_y - blob-min_y 1; // 长宽比过滤0.5 ~ 2.0 float ratio (float)width / (float)height; if (ratio 2.0f || ratio 0.5f) return 0; // 填充率过滤 float fill_rate (float)blob-area / (float)(width * height); if (fill_rate 0.4f) return 0; return 1; }这段代码看起来简单但实际调试时有两个细节非常关键。第一连通域的扫描要用并查集或者两遍扫描算法不要用递归深度搜索因为嵌入式平台栈空间很小递归深了直接硬错误。第二float运算在主控单片机上很慢能转成整数比较就转成整数比较。比如高宽比0.5到2.0可以变成width * 2 height之类的方式尽量避免浮点运算。2.4 靶心十字特征的判定如果只是“绕过目标板”那识别到连通域就够用了。但走马观碑组里往往还要判断目标板内部的十字或斜线标记方向。这一步比单纯找矩形要难因为要用到图像内部的像素分布特征。我的方案是拿到目标板连通域的外接矩形后在矩形中心区域做一个简单的十字扫描沿着水平中心线统计白色像素的连续段。沿着垂直中心线统计白色像素的连续段。如果水平方向出现1个明显贯穿的白色条带且垂直方向也出现1个明显贯穿的白色条带则确定为“十字形”目标板。如果只有水平方向有条带而垂直方向没有则可能是“横向标记”目标板。这个逻辑用代码实现很简单但要注意一个坑目标板在图像里不一定是正向的。车在行驶过程中目标板在图像里可能带有旋转角度外接矩形的中心线不再和图像坐标系平行。所以我实际实现时会先对外接矩形内的图像做一次轻量级的投影统计而不是直接取图像坐标系的水平/垂直方向。uint8_t detect_cross(uint8_t *binary_img, uint16_t width, uint16_t height, blob_t *blob) { uint16_t center_x (blob-min_x blob-max_x) / 2; uint16_t center_y (blob-min_y blob-max_y) / 2; // 扫描水平中心线统计连通白色段的数量 uint8_t h_segments 0; uint8_t prev 0; for (uint16_t x blob-min_x; x blob-max_x; x) { uint8_t pixel binary_img[center_y * width x]; if (pixel !prev) h_segments; prev pixel; } // 扫描垂直中心线 uint8_t v_segments 0; prev 0; for (uint16_t y blob-min_y; y blob-max_y; y) { uint8_t pixel binary_img[y * width center_x]; if (pixel !prev) v_segments; prev pixel; } // 十字目标板水平和垂直各有一个明显条带 if (h_segments 1 v_segments 1) return 1; return 0; }当然这种方法的鲁棒性依赖中心点选得准。如果外接矩形内混进了干扰物中心点偏了判定就会失效。稳妥的做法是取多个扫描行/列做投票比如扫描中心线附近上中下三行每条线出一个判断最后少数服从多数。这会增加一点计算量但换来的稳定性非常值得。3. 绕行策略的深入优化3.1 三种绕行策略的对比与适用场景识别到目标板并确定其方向后接下来就是绕行。绕行策略的好坏直接影响车能不能在最短时间内、最稳定地绕过目标板并回到赛道中心线上。我调试过三种方案各有各的适用场景策略实现难度稳定性适用场景固定定时器绕行低一般目标板位置固定不变、车速比较恒定状态机分段绕行中较高目标板相对位置变化不大、但车速变化明显动态PID补偿绕行高高目标板位置、车速、曲率都存在较大变化固定定时器绕行是最容易想到的方案一旦识别到目标板就固定打左满舵或者右满舵500毫秒然后回正。这个方法最大的问题是它完全无视车速和当前位置。车速慢一点500毫秒可能还没绕完车速快一点500毫秒已经冲过头了很可能直接压到目标板。状态机分段绕行是把绕行过程拆成多个阶段向右拉方向、直行脱离、向左回正、重新寻线。每个阶段持续的时间由编码器积分距离或者图像丢失状态来触发。这个方案比固定定时器稳了不少因为它在关键时刻是根据“实际走了多远”来判断的而不仅仅是“过了多久”。动态PID补偿绕行则更进一步它把图像中目标板中心与图像中心的偏差作为PID控制器的输入持续输出一个叠加在巡线舵机控制量上的补偿量。这样当目标板在图像里偏向左侧时车会提前向左打一点方向保证绕行轨迹是一条平滑的弧线而不是生硬的折线。稳定性和速度上限最高但代码复杂度和调参工作量也最大。3.2 状态机分段绕行的实现细节我最终用的是“状态机分段绕行 图像交叉验证”的组合方案兼顾稳定性和实现复杂度。状态机的核心定义如下typedef enum { RUN_NORMAL 0, // 正常巡线 RUN_AVOID_START, // 开始绕行 RUN_AVOID_STRAIGHT, // 直线脱离阶段 RUN_AVOID_RETURN, // 回正阶段 } run_state_t;每一次状态切换都依赖不同的触发条件具体逻辑如下RUN_NORMAL→RUN_AVOID_START连续3帧图像内识别到目标板并且目标板中心横向偏差超过设定阈值比如40像素。连续3帧是为了防止单帧误识别导致误绕行。RUN_AVOID_START→RUN_AVOID_STRAIGHT持续输出转向控制量之后目标板中心开始移出图像视野当连续2帧找不到目标板时进入直线脱离阶段。RUN_AVOID_STRAIGHT→RUN_AVOID_RETURN编码器累积距离达到设定的脱离长度比如40cm或者重新在图像边缘找到赛道边界线。RUN_AVOID_RETURN→RUN_NORMAL图像中赛道中心线恢复到接近图像中心位置并且持续5帧以上稳定说明车已经回到正常巡线状态。这个状态机的核心优势在于每个阶段都有明确的退出条件不会像定时器方案那样开环运行。你在实际调试时会发现RUN_AVOID_STRAIGHT阶段的“脱离长度”是最难调的。调短了车还没完全离开目标板就开始回正会蹭到目标板边缘调长了车会偏出去太远回正后离赛道中心线太远容易在下一个弯道入弯失败。我自己的调试办法是先在地上贴一条标记线让车以不同速度跑过目标板记录每次从开始绕行到目标板完全脱离视野的车轮编码器读数取中位数作为初始值然后在此基础上微调。3.3 绕行方向判断与动态补偿方向判断的优先级要非常明确。我按照以下逻辑来确定绕行方向如果目标板中心在图像偏左那么目标板相对车的位置在偏右方车应该向右绕行。如果目标板中心在图像偏右车应该向左绕行。如果目标板中心几乎居中则参考赛道边界的方向向有更多空间的一侧绕行。这个逻辑看起来简单但如果你直接用“目标板中心减去图像中心”的差值符号来决定方向你会发现在某些特定编码下差值符号和实际方向是反的。这是因为摄像头安装方式不同有的队伍是前视安装有的队伍是斜下安装图像左右方向和车身左右方向可能一致也可能镜像。所以我强烈建议在写绕行方向代码之前先做一个打印实验——把目标板放在车右前方看图像里目标板中心是在图像中心的左边还是右边确认之后再把方向逻辑写死。动态补偿部分的实现是通过将目标板横向偏移量映射成一个补偿转向角叠加到基础巡线PID输出上。以我用的主控和舵机为例int16_t base_steer get_line_pid_output(); // 巡线PID输出 int16_t avoid_offset compute_target_offset(); // 目标板与图像中心的偏移 int16_t avoid_gain 30; // 绕行补偿增益实车调参 if (state RUN_AVOID_START || state RUN_AVOID_STRAIGHT) { if (avoid_direction AVOID_RIGHT) { steer_output base_steer - avoid_gain * avoid_offset / 100; } else { steer_output base_steer avoid_gain * avoid_offset / 100; } }这里要注意的是avoid_offset要归一化到合理范围否则图像中的横向偏差值会被直接放大成一个很大的转向角导致车猛地一甩。我一般会把它限制在-100到100之间再乘一个小于1的增益系数。实际调参时增益从20开始每次加5观察车在绕行过程中是否有明显的抖动或甩尾。3.4 绕过之后如何平稳回到赛道线很多人忽视的一个细节是绕行成功之后车不能“一下子”回到正常巡线状态否则会因为巡线PID的积分饱和或者微分冲击产生严重的抖动。我的处理方法是在RUN_AVOID_RETURN状态下对巡线PID的积分项做清零处理同时把PID输出与前一帧的输出做“限幅渐变”。也就是说绕行回正阶段允许的最大转向变化量不是立即恢复而是每帧最多改变一定数值比如每帧10个舵机PWM单位这样车在回到中心线的过程中轨迹会更平滑。还有一个实用技巧回正状态后强制让车保持直线行驶一小段距离比如编码器走10cm再交还给巡线逻辑。这能有效避免因为回正过程中赛道线位置变化太快而导致的误判和振荡。4. 实战代码与关键函数解析4.1 主循环与状态机整合下面是一段可以运行在主控单片机上的精简代码框架。为了适合展示我做了些简化但整体结构和实际项目一致。void main_loop(void) { while (1) { // 1. 采集图像并二值化 camera_capture(frame); uint8_t threshold compute_threshold(frame, IMG_W, IMG_H); binary_image(frame, threshold, IMG_W, IMG_H); // 2. 提取连通域并筛选目标板 blob_t target; uint8_t found find_target_blob(frame_binary, IMG_W, IMG_H, target); // 3. 状态机更新 state_update(found, target); // 4. 生成舵机PWM int16_t steer calc_steer_output(found, target); servo_set_pwm(steer); // 5. 速度闭环控制 speed_pid_run(); } }这段主循环非常简单但里面有几个性能优化的关键点不要在主循环内做printf打印调试调试信息会严重拖慢循环频率。我都是把状态变量放进一个环形缓冲区通过串口定时批量输出。连通域的扫描算法建议用两遍扫描第一遍标记等价对第二遍合并。不要用深度优先填充因为图像中白色区域较大时递归会爆栈或者占用大量堆栈空间。二值化后的图像一定要复用同一块内存不要在每帧重新malloc嵌入式环境下的动态内存分配很容易产生碎片跑几分钟后系统就挂了。4.2 目标板寻找与状态机完整代码目标板寻找函数find_target_blob是整个系统的核心内部按顺序执行“扫描标记连通域、计算几何特征、过滤非目标板”三个步骤。这里给出一个可直接参考的实现思路uint8_t find_target_blob(uint8_t *binary, uint16_t w, uint16_t h, blob_t *out) { // 使用一个标记数组记录每个像素是否已被访问 static uint8_t visited[IMG_W * IMG_H]; // 提前分配好避免栈开销 memset(visited, 0, w * h); for (uint16_t y 1; y h - 1; y) { for (uint16_t x 1; x w - 1; x) { uint16_t idx y * w x; if (binary[idx] !visited[idx]) { blob_t blb; flood_fill(binary, visited, w, h, x, y, blb); if (is_target_board(blb)) { *out blb; return 1; } } } } return 0; }这里我用的是标记数组而不是修改原图这样二值图在其他地方比如调试串口输出还需要用到。flood_fill虽然是用栈模拟的但相比递归版本已经安全很多。需要注意的是static uint8_t visited[IMG_W * IMG_H]会占用一块不小的静态内存如果你的主控SRAM比较紧张可以考虑用位图来压缩内存占用。状态机更新函数state_update是绕行策略的灵魂代码实现如下void state_update(uint8_t found, blob_t *target) { static uint8_t found_cnt 0; switch (current_state) { case RUN_NORMAL: if (found) { found_cnt; if (found_cnt 3) { // 判断绕行方向 uint16_t img_center IMG_W / 2; if (target-min_x (target-max_x - target-min_x) / 2 img_center) { avoid_direction AVOID_RIGHT; // 目标偏左 - 向右绕 } else { avoid_direction AVOID_LEFT; } current_state RUN_AVOID_START; found_cnt 0; } } else { found_cnt 0; } break; case RUN_AVOID_START: if (!found) { distance_cnt 0; current_state RUN_AVOID_STRAIGHT; } break; case RUN_AVOID_STRAIGHT: distance_cnt encoder_read_delta(); if (distance_cnt avoid_straight_length) { pid_reset(); current_state RUN_AVOID_RETURN; } break; case RUN_AVOID_RETURN: if (is_line_centered() stable_cnt 5) { current_state RUN_NORMAL; } else { stable_cnt (is_line_centered()) ? stable_cnt 1 : 0; } break; } }这个状态机有一个隐含的好处它在绕行过程中不是一直依赖“目标板是否可见”这一个条件而是通过“发现目标板后的图像丢失”来确认已经接近甚至经过目标板随后利用距离积分来精确控制绕行距离最后让视觉巡线逻辑确认回归。每一步的触发条件都有冗余和交叉验证所以即使某帧图像出现误识别也不会立刻让状态机乱跳。4.3 巡线PID与绕行的融合控制这里有一个容易出错的地方就是巡线PID的输出和绕行控制量的融合顺序。千万不要简单地在巡线PID输出后面加上一个固定偏置那样会导致绕行过程中仍然在按照赛道线做修正最终绕行轨迹变成波浪形。我的做法是在绕行状态下暂停巡线PID的积分更新仅保留一个很弱的比例项用于感知赛道大致方向同时以更大的权重叠加绕行控制量。回归正常巡线状态后再逐步恢复PID的完整输出。int16_t calc_steer_output(uint8_t found, blob_t *target) { int16_t line_steer get_line_pid_output(); int16_t avoid_steer 0; if (current_state RUN_AVOID_START || current_state RUN_AVOID_STRAIGHT) { // 目标板偏移量越大补偿转角越大但要限制上限 int16_t offset_px compute_offset_in_pixels(target); avoid_steer avoid_gain * offset_px / 100; if (avoid_steer AVOID_STEER_MAX) avoid_steer AVOID_STEER_MAX; if (avoid_steer -AVOID_STEER_MAX) avoid_steer -AVOID_STEER_MAX; pid_free_integral(); // 暂停积分 return avoid_steer line_steer * 0.3f; // 弱化巡线项 } if (current_state RUN_AVOID_RETURN) { pid_reset_integral(); return line_steer * 0.8f; // 逐渐恢复巡线权重 } return line_steer; }注意代码里的line_steer * 0.3f这个权重系数实际使用中不建议直接用浮点可以改成(line_steer * 3) / 10。在嵌入式平台上浮点运算会占用额外的CPU时间虽然单次影响不大但放在控制主循环里每一帧都算一下累计起来会让主循环频率下降不少。另外这个融合方式有个前提目标板出现的时候赛道线仍然在图像视野中。如果目标板太大、太近把整个视野都挡住了那line_steer本身就会失真这时候你就需要进入一个“纯盲绕”模式完全依靠编码器距离来走完绕行轨迹。这也是为什么状态机里RUN_AVOID_STRAIGHT阶段设计了“图像完全看不到目标板和赛道线”的处理逻辑。5. 常见问题与排查技巧实录5.1 目标板识别率不稳定怎么办我在调试中遇到过最典型的现象是目标板识别率在上午测试时还有95%到了下午直接降到60%。排查了很久才发现问题不在算法而在摄像头曝光时间。总钻风摄像头默认的曝光时间是自动调节的光照变化时画面的亮度会上下浮动。当你刚好在阳光照射较强、曝光时间被迫缩短的时候目标板暗色区域的灰度值会被压得很低甚至和二值化背景的灰度区分不开。解决方法是把摄像头设为手动曝光固定一个中等偏低的曝光时间然后通过自适应二值化来吸收剩余的光照变化。这个操作听起来简单但很多人第一反应是去调图像处理的阈值而不是调摄像头本身的曝光参数导致浪费时间。还有一次我们发现目标板识别率在过弯时会突然下降。最后定位到原因是车在高速过弯时摄像头安装架发生微小形变导致图像中心偏移。这个问题的排查思路是录制一段过弯时的图像数据逐帧看目标板在画面中的位置变化而不是只看识别结果。因为图像中心偏移一两行像素在肉眼看来不明显但对连通域搜索范围影响很大。5.2 绕行距离调不准的两个隐藏因素绕行距离avoid_straight_length是我调得最痛苦的一个参数。它受两个隐藏因素影响第一编码器的分辨率误差。我们的车轮直径和编码器轮直径不完全一致它俩之间有一个比例系数。如果直接用编码器读数当距离车速越快轮胎打滑越明显编码器读数和实际行驶距离的偏差就越大。解决方法是做一个多速度下的标定实验把“编码器读数”和“实际距离”的关系拟合成一条分段线性曲线。第二舵机响应延迟带来的轨迹偏差。从状态机进入绕行状态到舵机真正转到目标角度中间有大约20-40毫秒的延迟。车速3m/s时20毫秒车已经跑了6cm绕行轨迹的起点已经比预期晚了6cm。这也是为什么状态机里RUN_AVOID_START阶段不宜设置得太短要给舵机响应留出时间。这两个因素叠加起来会导致你在地面贴了标记线精心调试好的距离参数到了赛场上因为轮胎磨损、地面摩擦系数不同又失效。我最后的应对方法是赛前至少留出一个下午用新轮胎、场地实际地面重新标定一次距离参数不要抱着“这周已经调好了”的想法。5.3 回正后车身倾斜严重这个问题出现得非常多原因也很简单绕行状态退出后车往往不在赛道中心线上而巡线PID的积分项还残留着绕行时的大误差导致回正阶段出现严重的超调。我处理这个问题用的是“软回正”策略在RUN_AVOID_RETURN阶段不直接把巡线PID输出作为舵机控制量而是先强制直行固定距离等车头接近赛道中心线方向后再逐渐放开巡线PID的权重。这个“逐渐放开”的过程可以在10帧左右完成每帧巡线权重递增10%。这样做的代价是回正过程会比直接切回巡线慢大约200毫秒但换来的是车身姿态稳定尤其是在连续弯道里这个稳定性比速度更重要。5.4 无法同时满足识别速度和稳定性最后聊一个很多队伍都会陷入的误区总想让识别速度更快于是把图像分辨率降低、把算法简化结果识别率暴跌。我的实际经验是50ms的处理周期完全够用。换算过来就是20帧每秒的处理速度对于最高时速3-4m/s的智能车来说50ms内车最多前进20cm这个控制粒度对于绕行一个20x20cm左右的目标板来说足够精确了。所以我不建议为了追求30帧甚至60帧而牺牲识别质量。更合理的思路是把处理周期稳定在40-50ms多出来的时间用于做一些简单的图像预处理增强比如中值滤波去噪点或者对二值化图做形态学开运算去掉小的孤立白点。这些操作虽然每帧只增加不到1ms但对识别率的提升非常明显。6. 现场调试流程与参数调整建议6.1 一套高效的标定顺序很多队伍到了赛场才开始调参现场乱成一团。我的建议是把调试拆成以下四个阶段每个阶段有明确的目标不要跨阶段调参静态标定阶段在场地固定位置放好目标板不跑车只验证识别算法的输出是否正确。这个阶段调的是二值化阈值、连通域过滤参数、十字特征判定的阈值。确保车停在不同距离、不同角度时都能输出正确识别结果和偏差值。低速绕行阶段车速控制在1m/s左右只验证绕行状态机和绕行方向是否正确。这个阶段不要碰速度闭环也不要碰图像识别参数只看车的绕行轨迹是否合理、回正是否平稳。全速绕行阶段逐步提高车速记录不同速度下的绕行效果。这个阶段的重点是调avoid_straight_length和舵机补偿增益。每提升一次速度先跑10次确认成功率在90%以上再继续加高速度。抗干扰阶段在赛道旁边放置其他杂物、关闭部分灯光模拟不同光线条件验证识别和绕行是否依然可靠。这个阶段如果发现误识别优先检查图像预处理环节而不是直接加判断条件。这个顺序看起来慢但每一步都有明确的目标能让你在赛前快速定位问题不至于赛场上瞎调。6.2 关键参数速查表下面是我在这个项目中实际用到的核心参数表可以作为你调试的起点值参数参考值调整依据图像分辨率120x160主控性能越好可适当提高二值化偏移量18根据场地灰度中值差调整目标板最小面积40像素根据最小识别距离调整目标板长宽比0.5 ~ 2.0根据规则目标板形状调整最小填充率0.4防止不规则反光斑块误判绕行方向偏差阈值40像素根据赛道宽度调整绕行补偿增益30从20开始逐步调大最大补偿转角60防止绕行过猛直线脱离距离40cm通过目标板距离实测标定回正稳定帧数5帧防止提前切回巡线这个表里的数值不是通用答案但它提供了一个非常合理的起点。如果你现在是一张白纸用这些参数上车大概率能跑起来,之后根据自己场地再做微调。6.3 参数调整中容易犯的三个错误第一个错误是同时调多个参数。比如你觉得绕行不稳就同时改了补偿增益、脱离距离和舵机PD参数。结果是车从“绕行不稳”变成“完全没法跑”因为你根本不知道是哪个参数导致的。正确做法是每次只动一个参数跑3-5次看效果再决定下一步调什么。第二个错误是用记忆代替记录。我今天在这个场地调好了觉得参数没问题结果第二天换了场地全废。我在项目里专门写了一个参数保存功能每次调完参都可以一键存储到EEPROM同时打印出当前参数组编号。这样切换场地、切换赛道时可以快速恢复对应参数不用重新调。第三个错误是忽略摄像头安装角度对绕行轨迹的影响。摄像头俯仰角稍微偏一点图像中的目标板横向位置与车的实际相对位置的映射关系就会变化。这个问题不体现在目标板识别上而是体现在绕行方向判断的边界上。比如某个角度下目标板明明在车正前方图像里却显示偏左导致车绕错了方向。解决方法是定期用水平尺确认摄像头安装支架是否保持水平尤其是在经历碰撞和运输之后。7. 现场比赛策略与应急处理7.1 赛前检查清单比赛当天最容易出状况的往往不是算法而是硬件和现场环境。我给自己列了一个检查清单每次上场前按顺序走一遍摄像头镜头是否清洁灰尘和指纹会严重降低图像清晰度从而影响二值化效果。摄像头曝光参数是否被误改很多摄像头在重启后会自动恢复到默认自动曝光模式需要重新检查。电池电压是否正常舵机和电机在低压时的响应速度差异很大会影响绕行动作的稳定。场地光线是否和调试时一致如果赛场的灯光布局和调试场地不同需要重新调整二值化偏移量。这个清单看起来很简单但每一届比赛现场都会有人在开场前慌张地改代码最后连赛道都没跑完。先检查硬件和环境再去动代码参数。7.2 绕行失败的应急预案即使你的系统调试得再充分比赛现场也有可能因为突发的反光、目标板位置偏差导致绕行失败。我的应急预案是在代码里增加一个“手动强制绕行”模式当裁判宣布发车时我可以根据现场观察到的目标板位置通过遥控器或者按键强制设定绕行方向和绕行距离。这样即使视觉识别在某一帧出现了失误我也可以靠人工介入保证至少有一个有效成绩。这个方法看起来有点“土”但在实际比赛里真的救过我一次。当时现场光线和调试场地区别太大目标板的十字特征识别一直不稳定三次试跑两次失败。我靠手动绕行模式拿到一个有效成绩后续才在适应场地后慢慢调试恢复。7.3 时间分配建议如果你现在是赛前两周才第一次上车调试我建议时间按以下比例分配30%的时间花在目标板识别的稳定性上这是绕行的前提。20%的时间花在绕行状态机和距离参数调校上。30%的时间花在完整赛道模拟跑圈上重点看绕行后的回正衔接。20%的时间留给抗干扰测试和应急预案准备。不要把90%的时间都花在图像识别上因为绕行策略和完整赛道跑通的配合才是决定最终成绩的关键。识别做得再好绕行不连贯成绩同样不会理想。8. 写在最后的一点心得走马观碑组这个项目最有意思的地方在于它不是一个纯粹的“视觉算法题”也不是一个纯粹的“控制题”而是两者深度耦合的工程题。目标板识别做得再好如果绕行策略跟不上成绩依然不理想绕行策略做得再顺滑如果识别时灵时不灵整个状态机就会变成摆设。把这两个模块拆开各自调通都不算难真正考验人的是它们之间的接口设计和异常处理逻辑。我个人在实际调试中最深的体会是不要追求“理论最优”的方案要追求“现场最稳”的方案。你完全可以用深度学习模型来识别目标板精度可能很高但它在单片机上跑不动或者帧率上不来反而拖垮了绕行控制。反过来说一个基于灰度二值化和几何特征的传统视觉方案虽然在复杂背景下不够“智能”但它在竞赛这个受限场景下确实够用、够稳、够快。这篇文章里的代码和参数都是我从实际项目中提炼出来的你完全可以照着搭一版跑起来然后再针对自己的赛道特征逐步调整。最后再分享一个小技巧参数调优时一定要录视频记录不要只靠眼睛看车跑完的瞬间。绕行过程中车身姿态的变化、回正时的偏移量肉眼很难捕捉但慢放视频就能看得一清二楚。很多困扰我好几天的问题最后都是靠慢放视频定位到具体是哪个环节出了偏差。
返回列表