ARTICLE DETAIL

资讯详情

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

智能车竞赛新手避坑指南:从图像处理到PID调参的完整实战

智能车竞赛新手避坑指南:从图像处理到PID调参的完整实战 第一次带队参加全国大学生智能车竞赛赛前看强队视频见到的是各种“飞檐走壁”式的过弯和贴地冲刺真到了自己的练习场地发现连一条直道都跑不直。标题里那句“佬们轻点虐”说到底是新队面对老队伍时最真实的状态。这里分享的是我们第一年从零准备智能车竞赛的完整思路包括硬件选型、软件框架、图像处理、PID 调参、现场排查和赛前清单。如果你所在的学校也没有历史代码、没有学长传承希望这份记录能帮你少走一点弯路。1. 第一次参赛要把“智能车竞赛”拆成三个工程问题1.1 竞赛本质让车在一条未知赛道上稳定跑完全程很多新队第一年最容易犯的错是把智能车竞赛理解成“写一个很厉害的控制算法”。实际上比赛日给你的有效调试时间非常有限赛道是现场才知道的裁判只看三次发车里有没有一次能稳定跑完。强队之所以看起来“飞檐走壁”不是某一个参数调得特别夸张而是感知、控制、机械、电源、轮速、图像曝光这些都稳定下来车才能在变化的环境中复现同样的表现。第一年参赛最应该把目标定成让车在练习赛道上稳定跑完再逐步提高速度。不要一开始就去模仿老队伍的极限速度也不要花大量时间在一个不稳定的硬件平台上调高级算法。先保证每次上电后的行为一致再谈跑得快。1.2 三个核心子问题感知、控制、调试智能车竞赛可以从工程上拆成三条主线分别是感知、控制和调试。感知解决“车现在在赛道什么位置”控制解决“车应该给多大油门和多大转向”调试解决“改了一个参数后怎么判断车有没有变好”。这三条主线缺一不可。子问题需要解决的现象第一年最容易出现的问题感知车要知道赛道边界、中线、弯道摄像头固定不牢、曝光不稳定、阈值写死控制车要按目标速度和方向运动PID 参数凭感觉乱调车要么冲出赛道要么原地发抖调试修改参数后能判断效果是否变好没有日志、没有波形只能靠肉眼猜第一年最容易忽略的是调试链路。很多队伍直到比赛前一周还在用复写纸改参数调一次要重新烧录一次完全不知道上一次是哪个版本。这个习惯不改比赛现场会很被动。1.3 新队最好走“最小闭环”路线如果学校之前没有参加过队伍里也没有人能解答基本问题建议把备赛分成四个阶段而不是直接进入“优化算法”阶段。第一阶段让电机能转编码器能读数串口能输出速度。第二阶段让摄像头图像能实时显示到上位机阈值能自适应。第三阶段让车沿中线在直道上稳定走再处理弯道。第四阶段把赛道元素单独拆出来比如坡道、颠簸、十字、环岛逐一写测试逻辑。每个阶段结束都要有一个“检查点”车放在地上按照预期动作运行录像保存日志标明当前版本。这样做的好处是出问题的时候能快速回滚到上一个可用版本。老队伍常说“第一次参赛最重要的是积累调试方法”这句话只有自己走一遍才会理解。2. 把硬件和软件环境固定下来后面才能少返工2.1 第一年参赛的硬件组合建议智能车竞赛有很多组别不同组别对传感器要求不同。以最常用的摄像头组为例第一年不需要追求最高性能关键是资料多、稳定性好、好排查。下面是一套比较稳妥的入门组合。硬件模块作用第一年特别要注意的坑MCU 主控采集图像、处理算法、输出控制管脚分配尽量避开摄像头和串口冲突灰度摄像头获取赛道图像摄像头安装在车头容易晃动曝光和帧率要实测电机驱动模块驱动两个直流电机驱动芯片发热、信号线不稳会导致车突然窜动编码器获取实际车速编码器线要屏蔽转速不稳定常因为接触不良电池和稳压模块给整车供电电池电压变化会导致电机输出变化电源树要设计清楚采购时尽量选学校周边能快速拿到货的型号。第一年不建议用太冷门的芯片因为遇到问题后查资料的成本非常高。我们当时选择的主控芯片型号很常见论坛里的案例也多很多报错一搜就能找到类似现象。2.2 软件环境先把编译、烧录、串口链路跑通软件环境看起来简单但第一年经常在这里卡住。很多队伍一开始直接写算法结果编译报错、烧录失败、串口乱码三件事叠加在一起根本分不清是哪一层出了问题。建议先建一个最小工程只做一件事读取编码器数据通过串口打印速度。工程能跑通之后再逐步加入摄像头和 PID。# Linux 环境下使用 arm-none-eabi-gcc 的示例编译流程 make clean make -j4# 烧录命令示例实际命令取决于调试器和芯片型号 openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c program build/chassis_control.bin 0x08000000 verify reset exit不要把编译和烧录命令记成固定的“咒语”。要搞清楚命令里每个参数的作用指定目标芯片、指定下载地址、指定调试器接口。否则换一块开发板自己就不知道怎么处理了。2.3 工程目录和版本管理第一年虽然没有太多代码但建议从第一天就用 Git 管理工程。智能车越是到后期越容易因为改了一行阈值导致整车行为变化。如果没有版本管理出问题后很难回到上个稳定版本。smartcar/ ├── Core/ # 启动文件、时钟、中断入口 ├── Drivers/ # 外设驱动如串口、PWM、编码器 ├── User/ │ ├── control/ # PID、速度规划、转向控制 │ ├── image/ # 图像采集、二值化、中线提取 │ ├── sensors/ # 编码器、摄像头、陀螺仪等 │ └── main.c # 主循环 ├── build/ # 编译输出 └── logs/ # 串口日志和测试记录每次修改只改尽量少的文件提交信息写清楚“改了什么、为什么改、测试结果如何”。例如modify: 弯道目标速度降低解决大弯甩尾。这样到比赛现场即使多人配合也能迅速知道当前版本的状态。3. 图像处理主线从一帧灰度图到转向偏差3.1 摄像头采集与二值化摄像头是摄像头组的“眼睛”。第一次跑通摄像头时先不要闭眼直接写边缘检测而是先做一件事把一帧图像显示到上位机确认图像亮度、对比度、视野范围是否合适。灰度图像转二值图是最常用的处理方式把每个像素变成黑或白再用黑白边界代表赛道边界。这里的核心参数是阈值。固定阈值在室内灯光不变时可能够用但比赛现场光线变化很大推荐写一个简单的动态阈值比如根据整幅图像的直方图计算阈值。uint8_t get_dynamic_threshold(uint8_t *image, uint32_t size) { uint32_t sum 0; for (uint32_t i 0; i size; i) { sum image[i]; } return (uint8_t)(sum / size); // 示例用平均灰度作为阈值 } void image_to_binary(uint8_t *image, uint8_t *binary, uint32_t size) { uint8_t thr get_dynamic_threshold(image, size); for (uint32_t i 0; i size; i) { binary[i] (image[i] thr) ? 255 : 0; } }上面的平均灰度阈值只是一个最简单的示例实际中还可以用大津法效果更稳定。阈值计算会消耗时间建议在图像采集完成后的处理阶段集中计算不要在中断里做。3.2 提取赛道中线得到二值图后下一步是找到每一行的赛道中线。常用的思路是从第row行左边向右扫找到第一个黑到白的跳变点作为左边界再从右向左扫找到第一个白到黑的跳变点作为右边界。左右边界的平均值就是该行的中线点。#define IMG_W 188 #define IMG_H 120 #define INVALID -1 int find_left_edge(uint8_t *binary, int row, int left, int right) { for (int col left; col right; col) { if (binary[row * IMG_W col] 255 binary[row * IMG_W col 1] 0) { return col; } } return INVALID; } int find_right_edge(uint8_t *binary, int row, int left, int right) { for (int col right; col left; col--) { if (binary[row * IMG_W col] 255 binary[row * IMG_W col - 1] 0) { return col; } } return INVALID; }中线提取不是越复杂越好。如果某一行找不到边界不要硬算可以把这一行的中线值继承上一行或者直接跳过这一行用附近有效行的值插值。否则图像上只要有一个噪点中线就会跳到很离谱的位置。3.3 计算偏差并平滑控制算法需要的是一个数值也就是车相对于赛道中线的横向偏差。通常选择图像中靠近车头的前瞻行比如第REF_ROW行用该行的中线和图像中心坐标计算偏差。float get_steering_error(uint8_t *binary) { int left find_left_edge(binary, REF_ROW, 0, IMG_W - 1); int right find_right_edge(binary, REF_ROW, 0, IMG_W - 1); if (left 0 || right 0) { return last_valid_error; } int center (left right) / 2; float error center - IMG_W / 2; return error; }算出偏差后直接拿原始误差去转方向盘车会抖得很厉害。因为摄像头每一帧的边界检测都会有一点抖动所以需要做平滑。比较简单是一阶低通滤波也叫指数移动平均。#define FILTER_ALPHA 0.6f float smooth_error FILTER_ALPHA * raw_error (1.0f - FILTER_ALPHA) * last_error;FILTER_ALPHA越大越相信当前帧响应快但容易抖FILTER_ALPHA越小越平滑但反应慢。第一年调试时建议先把这个值放在上位机里实时调整找到既不抖又能跟住弯道的平衡点。4. 控制算法速度环和转向环一起调4.1 电机控制基本结构智能车通常使用两个直流电机通过 PWM 控制转速。PWM 占空比越大电机两端平均电压越高转速越快。但 PWM 占空比和实际车速并不是线性关系受电池电压、负载、轮胎打滑影响很大所以要用编码器测出实际转速再通过 PID 闭环控制。控制环要分成两层内层是速度环让实际车速尽量接近目标车速。外层是转向环根据图像偏差计算出方向同时限制目标车速。第一年常见的问题是只调转向不调速度结果车在弯道里因为速度太快直接冲出去。速度环的作用不是让车“跑得快”而是让车在坡道、颠簸、电池电压下降时也能保持稳定。4.2 一个可用的位置式 PID 模板下面是一段适合初学的 PID 结构体代码。实际使用时可以把积分限幅、输出限幅、微分滤波器都加进去。typedef struct { float kp; float ki; float kd; float integral; float last_err; float output_max; float integral_max; } PID; float pid_update(PID *pid, float target, float actual) { float err target - actual; pid-integral err; // 积分限幅防止饱和 if (pid-integral pid-integral_max) pid-integral pid-integral_max; if (pid-integral -pid-integral_max) pid-integral -pid-integral_max; float out pid-kp * err pid-ki * pid-integral pid-kd * (err - pid-last_err); pid-last_err err; // 输出限幅 if (out pid-output_max) out pid-output_max; if (out -pid-output_max) out -pid-output_max; return out; }这个模板里最容易出问题的是积分项。如果车一直达不到目标速度积分会一直累加最后输出太大等误差反转时车会明显冲一下。所以第一年宁可不用积分也不要让积分饱和。4.3 转向 PD 和速度规划转向环通常不用积分用 PD 就够。因为转向的目标是偏差为 0稳态误差本身不大而积分项会让转向反应过慢出现左右摆动。float steer STEER_KP * error STEER_KD * (error - last_error);速度规划可以写成最简单的形式偏差越大目标速度越低。float target_speed base_speed - fabs(error) * speed_scale; if (target_speed min_speed) target_speed min_speed;这样写的思路是直道偏差小车可以跑快进入弯道后偏差变大自动减速。比赛里的“飞檐走壁”式过弯本质是车在高速状态下还能保持稳定而不是一直以同一个速度跑完全场。速度规划就是提前把风险规避掉。4.4 PID 参数调整速查表调参时不要一次调多个参数。先把转向 PD 调稳再调速度 PID。下面表格可以作为调参时的参考。参数调太小的表现调太大的表现第一年推荐调试顺序转向 Kp过弯跟不上反应慢直道上左右摆动先从小到大找到不发散的最小值转向 Kd入弯晚切弯不果断过弯抖动、转向卡顿在 Kp 稳定后再加一点阻尼速度 Kp实际速度跟不上目标电机啸叫、速度振荡用固定目标速度测试速度 Ki有静态误差上坡掉速积分饱和出弯冲车只有长时间跟不上目标才加速度 Kd加减速过渡慢速度波动明显必要时加不是必须调参一定要有数据支撑。开一个上位机波形把目标速度和实际速度画出来确认速度环没有振荡再上赛道跑。肉眼只看车“看起来跑得挺快”很难发现速度环其实一直在震荡。5. 现场出现“飞檐走壁”式失控时按这个顺序查5.1 先描述现象再判断大概原因比赛或练习时车突然冲出赛道很多人的第一反应是“改 PID”。但失控的原因可能根本不在控制算法。现场排查要先区分现象失控现象优先检查方向次要检查方向直道高速左右摆动转向 Kd 是否过小机械虚位摄像头架是否松动坡道后跑偏丢线处理是否跳变电池电压是否明显下降颠簸后突然加速编码器是否丢波速度 PID 积分是否异常过弯直接冲出去速度规划是否降速转向最大角度是否足够上电后第一次跑正常第二次变慢电池电量、电机过热电源稳压模块“飞檐走壁”如果指的是高速过弯和坡道组合那最容易出的问题往往是两个一是摄像头前瞻太近车在坡道顶点看不到远处赛道二是速度 PID 在负载变化时来不及调整导致出坡瞬间车速突然变大。不要只盯着转向。5.2 用日志还原现场没有日志现场排查只能靠猜。建议在车跑的过程中把关键数据通过 SD 卡或无线串口保存下来至少包括时间点、目标速度、实际速度、图像偏差、转向输出。[15123] target_speed1.80 actual_speed1.62 error12.30 steer31.50 [15124] target_speed1.40 actual_speed1.55 error31.00 steer58.20 [15125] target_speed1.40 actual_speed1.48 error33.50 steer61.10 [15126] target_speed1.40 actual_speed1.90 error2.10 steer5.30上面这组日志里第 15126 行实际速度从 1.48 突然跳到 1.90而目标速度只有 1.40。这说明速度环可能异常常见原因是轮子空转、编码器安装松动或者 PID 的输出突然饱和。如果没有日志只看到“车冲出去”很容易错误地改成转向参数。5.3 现场排查流程要固定下来第一年可以在电脑前贴一张“失控排查流程”按照固定顺序执行不要东查一个地方西查一个地方。先确认输入是否正常摄像头图像是否清晰赛道边界是否和实际一致。再确认文件路径和参数版本当前烧录的程序是不是自己以为的版本。检查硬件接线电机信号线、编码器线、电池电压是否稳定。查看串口日志目标值和实际值偏差在哪个环节开始变大。最后才改算法一次只改一个参数记录改前改后。这个顺序看起来简单但能避免大部分无效调试。我们第一次遇到“直道突然甩头”时折腾了两天最后发现是摄像头支架上一颗螺丝松了。硬件问题没有排查清楚之前调参数只是浪费时间。6. 第一次参赛最值得做好的赛前清单和现场节奏6.1 赛前一周硬件和配置检查清单第一次参赛的队伍赛前一周最容易陷入“还在改代码”的状态。实际上竞赛比的不是现场改代码的能力而是稳定复现的能力。赛前一定要做一次完整的静态检查。逐个拧紧所有机械螺丝尤其是摄像头支架、电机固定座、编码器安装座。检查电池充电状态确认稳压模块输出电压在合理范围。备份三份代码当前稳定版、备用测试版、最后提交版。确认串口、烧录器、备用摄像头、备用电机驱动都有冗余。把所有参数整理成一张表保存成配置文件不要写死在代码的多个位置。在练习场地录制一段完整跑图视频记录当时的参数版本。不要等比赛现场再去验证“电池充满后速度会不会变快”这类问题。电源电压变化会直接影响 PWM 输出如果在赛前没有测试过低电压状态现场车越跑越慢会让你误以为算法出了问题。6.2 比赛现场调试节奏到了比赛现场人的注意力很容易被强队吸引。真正有用的做法是把现场当成一次固定流程来执行。到达后先准备一块平地测试电机正反转、编码器读数、转向角度极限。试车时只调两件事摄像头曝光阈值和车在弯道里的目标速度。每次试车后把日志和视频保存下来标注现场光线、温度和参数版本。如果连续三次试车都出现同一现象说明问题大概率是硬件或结构不要再继续调参数。比赛现场不要在机械结构上做大改。第一年很容易出现“看到强队用某个结构我们也想马上改”的冲动。实际上临时更换机械结构会让所有标定参数失效风险远大于收益。6.3 对“佬们轻点虐”的正确理解技术比赛里强队不会因为你是新手就降低标准但他们大多愿意分享经验。第一次参赛最好的心态不是“求放过”而是“多问多记多拍”。大部分老队伍踩过的坑都写在他们的调试记录和代码注释里主动请教往往能获得比网上资料更直接的经验。第一年参赛真正值得带回去的不是一张证书而是一套可以重复使用的工程方法怎么拆需求、怎么搭环境、怎么记录版本、怎么用日志定位问题、怎么在有限时间内做取舍。这些方法沉淀下来之后下一年再备赛就不再是从零开始了。如果现在你所在的队伍也刚起步建议先把这条主线跑通图像能出图串口能打日志速度环能锁得住转向环能跟得上中线最后再加入坡道、颠簸和高速过弯。等车能在赛道上稳定跑完时你自然会明白所谓的“飞檐走壁”其实只是别人把每一个普通环节都调到了稳定状态而已。
返回列表