简介:全国大学生智能车竞赛三轮摄像头组的完整工程代码包,面向智能车参赛团队与嵌入式视觉开发者,覆盖摄像头采集、图像处理、路径规划、PID电机控制等核心环节。压缩包共1183个文件、10.85MB,以C源文件、头文件与目标文件为主,附带Makefile与Eclipse工程配置,目录结构完整,便于直接导入学习。已有527人学习下载。其中既包括Canny边缘检测、霍夫变换等图像处理算法,也包括位置式PID与增量式PID的电机控制实现,并对环岛、坡道、短路、避障等典型赛题场景给出了处理策略;工程内还包含Eclipse项目配置、库文件、硬件接口分配说明以及团队自定义的user目录模块,便于理清代码结构和二次开发。适合具备一定嵌入式基础的选手快速搭建三轮摄像头小车、理解从图像采集到决策执行的完整流程。
1. 三轮摄像头组代码到底在做什么:先让车「看见」,再让车「听话」
第一次把这套代码烧进车模,绝大多数人的反应是:为什么摄像头看到的是花的?为什么车子一上电就往赛道外打轮?第十八届全国大学生智能车竞赛三轮摄像头组代码,本质上不是某个自动驾驶框架,而是一个跑在单片机里的闭环系统:灰度摄像头把一张幅面不大的图像传进 MCU,二值化后提取赛道边界,算出一条中线,再用 PD 控制舵机、PI 控制电机,让一辆三轮车模自己沿着赛道跑完一圈。
它解决的核心问题可以用两句话概括:一是把摄像头看到的画面变成「赛道在哪里」的判断,二是把这个判断变成「打多少方向、给多大油门」的执行。适合读这篇文章的人有两类——正在备赛的队员,想从别人代码里快速上手的随口说一句「这代码能直接跑吗」的同学;以及接手往届代码、想搞清楚哪里能改哪里不能改的维护者。前者需要知道参数从哪调,后者需要知道坑在哪,这篇都覆盖。
2. 图像采集与二值化:把摄像头看到的 188×120 像素变成赛道线
2.1 摄像头选型:灰度、全局快门、分辨率怎么定
智能车摄像头这个圈子里,三轮摄像头组的主流方案是灰度全局快门 CMOS,最常见的感光芯片是 MT9V034 这一类。选全局快门而不是卷帘快门,是因为车在高速运动时卷帘快门会把垂直于运动方向的赛道的边线拍成斜的,弯道里这个斜角会被放大成明显的偏差误差。灰度而不是彩色,是因为三轮组没有识别红蓝 T 字等彩色元素的需求,灰度图像带宽减半、处理耗时减半,省下来的 CPU 时间都用于元素判断,这笔账在 60 帧以上的处理节奏里相当划算。
分辨率不是越高越好,这是新手最容易踩的思维定式。摄像头场中断下 188×120 的分辨率,一帧大约 2.2 万个像素,在 100MHz 级单片机里逐行扫描加边线提取能压到 2ms 以内;如果提升到 376×240,像素量变成四倍,即使跑同样的算法,帧率也大概率掉到 40 帧以下。三轮车模过弯时车速普遍在 2m/s 以上,40 帧意味着每帧之间车已经走了 5cm 以上,一环岛补线信息滞后就会让车直接冲出去。因此我一般建议优先保住帧率,而不是保住分辨率。
参数权衡可以按下面这张表来定:
| 方案 | 分辨率 | 帧率 | 二值化耗时 | 适用场景 |
|---|---|---|---|---|
| 高分辨率灰度 | 376×240 | 尚可 | 高 | 需要看清远处断路口,但牺牲动态响应 |
| 标准灰度 | 188×120 | 高 | 低 | 三轮组最常选,平衡前瞻与帧率 |
| 彩色全局快门 | 188×120 | 中 | 中 | 如果规则要求识别彩色元素才考虑 |
2.2 用 DMA 把一帧图像搬进单片机内存:核心骨架
很多新手一开始用轮询方式读摄像头,每来一个像素时钟就读一次 IO 口,结果是图像也能出来,但控制周期被拖到 10ms 以上,车跑起来像「一卡一卡地走」。正确做法是让 DMA 在后台搬运像素数据,CPU 只在场中断到来时干两件事:标记一帧开始,等 DMA 搬完一整帧后再做图像处理。下面这段是采集骨架的示意,实际使用时按自己所用单片机的库函数改。
/* 以摄像头场中断 + DMA 搬运为例的采集骨架 * 实际使用时请按自己所用单片机的库函数改写 */ #define IMG_W 188 // 每行像素数 #define IMG_H 120 // 需要采集的行数 volatile uint8_t frame_done = 0; // 一场图像采集完成标志 uint8_t image[IMG_H][IMG_W]; // 存放一帧灰度图 void camera_init(void) { // 1. 初始化摄像头:配置分辨率、帧率、曝光 // 不同摄像头寄存器地址不一样,这里只写流程 // 常见做法是写 camera_reg_write(addr, value) 序列 // 2. 配置 DMA:把 PCLK 采样到的数据按行写入 image // dma_init(通道号, (uint32_t)&image[0][0], 硬件触发源); } void vsync_isr(void) // 场中断,VSYNC 触发 { frame_done = 0; // 新的一帧开始 } void dma_transfer_done_isr(void) // 一行搬完的中断 { static uint16_t line = 0; line++; if (line >= IMG_H) { frame_done = 1; // 整帧完成,主循环开始处理 line = 0; } }逻辑说明:VSYNC场中断标志着新的一帧开始,之后的每一行数据在PCLK驱动下由 DMA 自动填入image数组,CPU 全程不参与逐像素搬运。dma_transfer_done_isr负责统计已搬运的行数,搬满IMG_H行后置位frame_done,主循环只在置位后处理图像。这样做的好处是控制周期稳定,图像采集时间不再占用 CPU 时间片。
参数说明里最需要注意的是IMG_W和IMG_H必须与摄像头实际输出分辨率一致。另一个容易出错的地方是行中断里用静态变量line计数,如果 DMA 传输中途发生异常导致行数错位,整帧图像会从某一行开始劈成两半,这个问题在第五章排查里会专门说。
2.3 二值化与动态阈值:固定阈值为什么上不了场
灰度图直接做边线提取也能跑,但灰度值受光照影响太大,同样一段赛道,上午十点和下午三点的灰度分布能差出三四十个灰度级。所以常规做法是先二值化:把灰度图变成 0 和 1,0 代表赛道深色底,1 代表白色引导线。固定阈值的问题在于,一旦环境光变化或者赛道路面材质不均匀,同一阈值会把白色边线断成一段一段的,边线提取直接失败。
比赛里最常用的动态阈值是大津法(OTSU),它按灰度直方图把像素分成前景和背景两类,再让两类之间的类间方差最大。下面这段是可在单片机上直接跑的实现。
/* 大津法求二值化阈值:统计灰度直方图,找类间方差最大的灰度值 */ uint8_t otsu_threshold(const uint8_t *gray, int w, int h) { int hist[256] = {0}; int total = w * h; float sum = 0; int i; for (i = 0; i < total; i++) { // 第一遍扫图像,统计直方图 hist[gray[i]]++; sum += gray[i]; } float sumB = 0, wB = 0, varMax = 0; uint8_t threshold = 0; for (i = 0; i < 256; i++) { wB += hist[i]; // 前景像素数累加 if (wB == 0) continue; float wF = total - wB; // 背景像素数 if (wF == 0) break; sumB += (float)(i * hist[i]); float mB = sumB / wB; // 前景灰度均值 float mF = (sum - sumB) / wF; // 背景灰度均值 float var = wB * wF * (mB - mF) * (mB - mF); if (var > varMax) { varMax = var; threshold = (uint8_t)i; } } return threshold; }逻辑说明:大津法把整幅图按阈值分成「赛道底」和「引导线」两部分,然后找让两部分灰度均值差异最大的那个分割点。代码里wB是灰度从 0 到当前值i的像素占比,mB和mF分别是两类的均值,var是类间方差。选方差最大的i作为阈值,就是大津法的结果。
参数说明:比赛时全图 188×120 做直方图约为 2.2 万次像素遍历,在单片机上是可接受的,但一场图像处理如果全图 OTSU 再加边线提取,会占用不少时间。所以我一般只取图像下方 40~60 行做直方图。下方是近处赛道,灰度信息最稳定,上方远处因为反光、阴影干扰大,反而不能代表整幅图。把h参数从 120 改成 60,阈值计算耗时能减半,效果差别不大。
3. 边线与中线提取:从二值图到「能打方向的偏差」
3.1 边线搜索:逐行找跳变沿,不是逐行找白点
二值化之后,边线提取的常规做法是从图像最下方一行开始,逐行向上搜索左右两条赛道边线。这里有一个新手常见的误区:以为要遍历整行找所有白点再取最左最右。实际上赛场图像里干扰点很多,反光、轮胎印都可能被二值化成白色,直接取极值会把干扰点当成边线。更稳的办法是基于上一帧边线位置,在当前行只搜索附近一小段区域内的跳变沿。
#define MAX_COLS 188 #define SEARCH_HALF 15 // 以“上一帧边线为中心”的搜索半径 int16_t left_edge[IMG_H]; // 存储每一行的左边界列号 int16_t right_edge[IMG_H]; // 存储每一行的右边界列号 void extract_edges(uint8_t binary[IMG_H][MAX_COLS], int16_t last_left, int16_t last_right) { for (int row = IMG_H - 1; row >= 0; row--) { // 左边界:从上一帧左边界附近向右找 0->1 的跳变 int start = last_left - SEARCH_HALF; int end = last_left + SEARCH_HALF; if (start < 1) start = 1; if (end > MAX_COLS - 2) end = MAX_COLS - 2; left_edge[row] = -1; // -1 表示这一行丢线 for (int c = start; c < end; c++) { if (binary[row][c] == 0 && binary[row][c + 1] == 1) { left_edge[row] = c + 1; break; } } // 右边界:从上一帧右边界附近向左找 1->0 的跳变 // 逻辑与左边界对称,注意跳变方向是 1 -> 0 right_edge[row] = -1; start = last_right - SEARCH_HALF; end = last_right + SEARCH_HALF; if (start < 1) start = 1; if (end > MAX_COLS - 2) end = MAX_COLS - 2; for (int c = end; c > start; c--) { if (binary[row][c] == 1 && binary[row][c + 1] == 0) { right_edge[row] = c; break; } } } }逻辑说明:这里用的是「黑底白线」的图像假设——0 是深色赛道,1 是白色引导线。左边界是从深色进入白色的跳变点,右边界是从白色回到深色的跳变点。每一行搜索范围都被限制在上一帧边线附近 ±15 列,这样既避免了全行扫描的计算量,又天然过滤掉了远离赛道的干扰白点。
参数说明里最关键的是SEARCH_HALF。这个值如果设太小,遇到弯道曲率大的地方边线变化超过 15 列,会跟丢;设太大,搜索区域变大,计算量上升,还容易把赛道外的噪点收进来。我一般会把它设成 20,同时配合一个条件:如果当前行搜索失败,把搜索半径临时扩到 35 再试一次,再失败才标记丢线。这个「先小范围找,再扩大范围找」的策略,比单一大搜索半径稳定得多。
3.2 中线生成与丢线处理:边线都丢了拿什么当基准
左右边线都找到后,中线就是简单的(left_edge[row] + right_edge[row]) / 2。但赛道上丢线是常态:环岛内侧、坡道顶端、十字中心都可能让某一行甚至连续几十行只有单边边线。这时如果直接放弃中线,偏差会出现一个很大的跳变,车就会猛地打一下方向。
常规处理是分三种情况。双边都在,正常取平均;单边丢线,用丢线前的赛道宽度对丢线侧做对称外推;双边都丢,说明车可能正处在环岛内部或者坡道顶端,这时的中线已经没意义,直接沿用上一帧处理结果,交给元素状态机去判断当前该信什么。这个逻辑写成函数就是下面这样。
int16_t clac_mid(int16_t row, int16_t last_valid_width) { if (left_edge[row] >= 0 && right_edge[row] >= 0) { return (left_edge[row] + right_edge[row]) / 2; } if (left_edge[row] >= 0 && right_edge[row] < 0) { return left_edge[row] + last_valid_width / 2; // 右丢,向右外推 } if (right_edge[row] >= 0 && left_edge[row] < 0) { return right_edge[row] - last_valid_width / 2; // 左丢,向左外推 } return -1; // 双边都丢,交给状态机处理 }逻辑说明:last_valid_width是最近一次左右边线都有效时的赛道宽度,要跟着每一帧更新。当一条边线丢失时,假设赛道宽度在短时间内变化不大,用这个宽度把丢掉的边线补出来。注意这里的外推方向是固定的,如果车正压着边线走,外推出来的中线会偏,所以单边外推只作为过渡,不能长时间依赖。
参数说明:外推的last_valid_width建议做低通滤波,不要用某一帧的瞬时值。我在实际调试里会把宽度滤波系数设成 0.8 当前帧加 0.2 历史值,这样过十字和环岛时宽度变化不会引起中线突跳。另外,双边丢线时中线返回 -1,这个 -1 必须在上层逻辑里被识别,不能再传给方向环当作一个正常偏差来算。
3.3 常见赛道元素判据:十字、环岛、坡道、断路怎么区分
第十九届之前的赛道元素基本围绕十字、环岛、坡道、断路来出题,三轮摄像头组要跑得稳,核心不是把每个元素单独识别得多准,而是用状态机把元素串起来。常见做法是先定义赛道状态,再按特征切换状态。
typedef enum { NORMAL, // 普通赛道 CROSS, // 十字 ISLAND, // 环岛 SLOPE, // 坡道 BREAK // 断路 } track_state; track_state cur_state = NORMAL;判据一般是这样几组。十字的特征是:连续 20 行以上左右边线同时存在,赛道宽度突然比正常宽度宽出 30% 以上,而且中线基本保持在图像中心附近。满足这三点才判十字,少一个都不行——因为环岛的入口区域同样会出现宽度变宽的假象。环岛的特征最典型:一侧边线连续丢失超过 30 行,另一侧边线保持且呈现出连续的内弧形状。这里要注意,摄像头环岛的中心不是靠一张图就能判断出来的,需要多帧连续观察,看到「一侧持续丢线 + 另一侧曲率持续变大」的状态累计超过若干帧,才进入环岛状态。
坡道的特征要看灰度分布:上坡时远处图像整体变亮,二值化后上方出现大片的「无边线区域」,而且这个区域是从远处逐渐向近处扩大的。断路则是赛道上人为断开的一段,二值化图像里左右边线的跳变方向会在某一行突然反转——左边线找不到了,右边线还在;或者两边线同时向两侧外张。这几组特征在代码里建议写成独立函数,每帧都算一次,但切状态时要加帧数计数,连续满足 3~5 帧才切换,避免单帧噪声造成状态抖动。
4. 转向与速度控制:PD 打方向盘、PI 控油门
4.1 从图像偏差到舵机 PWM:方向环怎么搭
方向环的输入是中线与图像中心的偏差。但直接用整幅图像中线的平均值有一个问题:远处的偏差和近处的偏差混在一起,近处偏差变化快、远处偏差变化慢,加权平均后方向响应会变得迟钝。常见做法是取两层偏差加权,近处用下方 30 行算出的中线偏移,远处用上方 30 行算出的中线偏移,近处权重小、远处权重大,让车既看近处保稳定,又看远处预判弯道。
/* 方向环 PD 控制:输入加权偏差,输出舵机 PWM 增量 */ static int16_t last_err = 0; int16_t steering_ctrl(int16_t mid_line, int16_t center) { int16_t err = mid_line - center; // 中线偏离图像中心的像素数 int16_t out = Kp_steer * err + Kd_steer * (err - last_err); last_err = err; if (out > 150) out = 150; // 舵机限幅,防止打死方向 if (out < -150) out = -150; return out; // 返回的是 PWM 增量 }逻辑说明:Kp_steer是比例系数,决定方向盘跟着偏差打多少;Kd_steer是微分系数,对偏差变化率做阻尼。err - last_err就是偏差的变化趋势,如果偏差在快速增大,微分项会提前加大输出,抑制过弯时的摆动。最后输出的 150 限幅不是随便定的,它对应到舵机 PWM 上大约是占空比可调范围的三分之一到二分之一,超过这个范围舵机会饱和,转向失控。
参数说明里最容易出问题的是Kp_steer和Kd_steer的量纲。这里的偏差单位是像素,输出单位是 PWM 增量,不同分辨率下同一条赛道的像素偏差差异很大。所以换摄像头分辨率后必须重新标定,不能拿着 188×120 调好的参数直接套 160×120。另外last_err要用静态变量保持,它必须在每次控制周期都被更新,如果控制周期不固定,微分项会失真,所以方向环的控制周期建议固定为 10ms 一次。
4.2 编码器测速与速度环:增量式 PI 的基本盘
速度环一般由编码器反馈构成闭环,三轮车模的编码器装在驱动轮上,一个控制周期内读脉冲数,换算成实际轮速。控制周期我习惯固定为 20ms,也就是 50Hz,和方向环错开,避免两个环在同一时刻抢占 CPU。速度环的目标速度不是固定值,而是由赛道状态决定的——直道给高速,弯道和环岛给相对低速,坡道给中速。
/* 增量式 PI 速度环:目标速度来自状态机 */ static int16_t integral = 0; int16_t speed_ctrl(int16_t target_speed, int16_t current_speed) { int16_t err = target_speed - current_speed; integral += err; if (integral > 3000) integral = 3000; // 积分限幅,防止过冲 if (integral < -3000) integral = -3000; int16_t out = Kp_speed * err + Ki_speed * integral; return out; // 输出到油门比 }逻辑说明:速度环的integral是积分项,用来消除稳态误差——如果车因为负载或坡度始终达不到目标速度,积分项会一点点补上去。积分限幅是必须的,否则上坡路段积分会积到一个很大的值,下坡后仍然持续输出大油门,车会突然加速冲出去。增量式 PI 每次输出的只是增量,最终油门需要在主循环里做累加,同时也要对累加后的油门做限幅。
参数说明:Ki_speed不宜设大,速度环的响应本来就比重,积分系数太大会让车出现「喘振」——一快一慢地走。我调速度环的顺序是先把Ki_speed设为 0,只调Kp_speed让车能跟上目标速度,再加入积分补偿上坡和负载变化。目标速度表建议按状态机来组织,比如直道 220、弯道 140、环岛入口 120、坡道 180,这些数值直接在代码顶部宏定义,方便现场改。
4.3 参数整定顺序与三个必调参数
三轮摄像头组的调参顺序,强烈建议先方向后速度。方向环不稳定的车,速度越快死得越惨。具体步骤是:第一步,把车架起来让后轮悬空,把速度环目标设成固定值,只调方向环;第二步,给一个很小的Kp_steer,让车在直道上能基本走直线,然后慢慢加大到直道上不来回摆;第三步,加入Kd_steer,把弯道入弯时车头的摆动压住。方向环稳定后再调速度,顺序是速度Kp、速度Ki,最后调不同元素下的目标速度表。
三个必调参数优先级如下表,排在前面的先调,调完再动后面的:
| 参数 | 作用 | 调大的后果 | 调小的后果 |
|---|---|---|---|
| 转向 Kp | 决定转向响应速度 | 直道摆动、弯道抖动 | 入弯迟钝、冲出赛道 |
| 转向 Kd | 阻尼,抑制摆动 | 转向发涩、过弯迟缓 | 摆动抑制不住 |
| 速度 Ki | 消除稳态误差,上坡能力 | 喘振、油门突变 | 上坡无力、速度掉太多 |
这里有个值得注意的细节:转向Kd不要试图完全消除摆动,收敛不了的高频抖动往往不是参数问题,而是机械间隙或图像处理周期抖动。真遇到转向参数怎么调都在抖,优先查舵机供电电压和图像处理耗时,方向环的微分项不是用来给电源滤波的。
5. 常见问题排查:赛道上的 5 个翻车现场与修复顺序
5.1 环岛补线没退出去,车直接冲进草地
现象:环岛识别成功,入口也进去了,但在环岛内部绕了半圈后没有按预期切回赛道,而是直接冲出环岛外缘。
原因:最常见的是「出环判定」条件太松。很多代码在补完环岛内线后,看到赛道宽度恢复就提前退出环岛状态,但此时补线还残留在图像中,宽度恢复只是一个假象。另一种原因是环岛内部长时间丢线,中线外推把路径越推越偏。
解决:出环判定加两个条件同时满足——连续若干帧检测到完整左右边线,且当前赛道宽度接近正常宽度。同时把环岛内的目标速度降到直线速度的六成左右,给补线算法留出纠偏余量。调试时在上位机里观察环岛过程的完整录像,确认补线是从入口一直持续到出口,而不是中途断开。
5.2 晚上调的阈值,第二天早上图像全白
现象:前一天晚上在实验室调好的固定阈值,第二天到赛场开机,二值化图像变成全白或者全黑,边线完全看不见。
原因:固定阈值本质上是个黑匣子,它只在「当前光照」下成立。赛场灯光角度、场地反光、赛道新旧程度都会改变灰度分布,同一个阈值在早上和下午可以差出几十个灰度级。
解决:换动态阈值。快速做法不是全图大津法,而是取图像下方 60 行、每 4 列抽一列,做一个采样版大津法,每帧实时更新。实测在 100MHz 单片机上一帧采样耗时不到 1ms,效果远好于任何手动调出来的固定值。
5.3 舵机一直小幅抖动,弯道像「打摆子」
现象:直道上车能走,但舵机一直有高频小幅摆动,过弯时摆动幅度更大,看起来像人打摆子。
原因:转向Kp_steer过大是首要嫌疑,但多数情况下是微分项在起作用——控制周期不稳定时,err - last_err会被放大,微分项产生额外抖动。另外舵机供电电压如果被电机拉开,也会出现周期性摆角漂移。
解决:先用示波器或电压表量一下跑起来之后舵机供电电压,如果低于舵机标称工作电压的 90%,加独立稳压。排除供电问题后再降Kd_steer,最后把方向环控制周期改成固定周期定时器触发,而不是在主循环里用延时凑周期。
5.4 图像行错位,一帧画面劈成两半
现象:上位机里看到的图像上半部分和下半部分不连续,中间某一行开始整体向左或向右偏移,有时候还会出现隔行错位。
原因:DMA 搬运行数与场中断没对齐。最常见的是摄像头输出分辨率和 DMA 配置的行宽不一致,或者行中断计数因为丢行干扰出现累积误差,导致后续所有行都错位。
解决:在vsync_isr里把行计数器清零,同时把 DMA 的传输长度和摄像头实际输出的行宽严格对应。更稳的做法是每次 DMA 一行的搬运完成后,重新装载同样的地址和长度,而不是依赖一个总计数器去切行。这样即使某一行丢数据,下一行也能自动对齐。
5.5 坡道下坡丢线后,车把十字误判成环岛
现象:下坡时车头仰起,摄像头视野里赛道变宽、边线大量丢失,状态机在十字和环岛之间来回跳,车开始乱打方向。
原因:坡道顶端的图像特征和十字、环岛入口有相似性——都是「边线突然变化 + 局部丢线」。单看一帧图像很难区分,所以误判的根因是状态切换条件太急,用了单帧特征而不是连续多帧累积。
解决:所有元素状态切换都加帧数确认,比如连续 3 帧满足特征才切换。同时坡道判据里增加灰度均值条件:坡道顶端整幅图像的灰度均值会比普通赛道高出很多,而十字和环岛的灰度均值无明显变化。用这个条件把坡道和其余元素区分开,误判基本能消除。
6. 最后一步调试技巧:参数存 Flash,图像存上位机
6.1 把调好的参数存进 Flash,别每次改参都重新烧代码
方向环、速度环那么多参数,如果每次改一个数值都要重新编译烧写,一上午就浪费在下载程序上了。更合理的做法是把参数定义成一个结构体,通过串口命令修改,然后写入单片机内部 Flash。上电时先从 Flash 加载参数,没有有效的才用默认值。这样比赛现场调车,只需要带一个串口模块和调试助手,参数随手改随手存。
typedef struct { int16_t kp_steer; int16_t kd_steer; int16_t kp_speed; int16_t ki_speed; int16_t speed_straight; int16_t speed_turn; } params_t; params_t g_params = { 50, 120, 80, 5, 220, 140 }; void save_params_to_flash(void) { // 调用单片机 Flash 写入函数,把 g_params 整体写入指定扇区 // 写入前先擦除扇区,注意 Flash 擦写寿命,不要每帧都写 }这一招对比赛场景来说价值很大,它的意义不是「省事」,而是让现场调参时敢于去试大范围的数值——因为试错了能一秒一键恢复,而不是反复烧写,调参的速度和胆子都会上来。
6.2 用上位机回放一帧图像,比看车跑一百圈都有用
车跑起来之后靠肉眼追着看,只能看出「跑得稳不稳」,看不出「哪里判错了」。我更习惯的做法是:单片机每一帧把二值化图像、左右边线和最终偏差打成一段数据包,通过串口发到上位机保存下来。赛车跑完一圈,回放这段数据,能清清楚楚地看到环岛入口处边线是哪一行开始丢的、中线外推了多远、状态机是哪一帧切进环岛的。图像回放比任何现场观察都能更快定位逻辑问题。
我个人的习惯是:先把车架起来确认每一帧图像都干净稳定,再上赛道跑;先把元素状态机跑通,再提速度;先把参数存在车上,再谈调参效率。这套流程是我踩了几年坑之后总结出来的,顺序反了,往往会在赛前最后几天反复翻车。希望这些经验能让你少走一段弯路,也祝你的车能稳稳跑完每一圈。
本文还有配套的精品资源,点击获取