智能车竞赛视觉组技术方案:双摄像头循迹与AprilTag识别实战
在准备第二十一届全国大学生智能汽车竞赛华南赛区的过程中,我们团队(华工智能视觉组)经历了从方案选型、代码调试到最终稳定进国赛的完整闭环。智能视觉组别因其融合了传统循迹控制与前沿的机器视觉识别,挑战性和趣味性并存。本文将系统复盘我们最终进国赛版本的完整技术方案,涵盖硬件选型、核心算法、代码架构以及关键的调参避坑经验,目标是提供一份可供后来者直接复现或深度借鉴的实战指南。
1. 智能视觉组核心任务与备赛思路解析
全国大学生智能汽车竞赛的智能视觉组,其核心任务是要求小车在完成基础循迹(如电磁、摄像头巡线)的同时,利用车载摄像头对赛道旁的视觉标签进行实时识别,并根据识别结果执行相应的动作,例如搬运物料、定点停车、声光提示等。这要求参赛队伍不仅要有扎实的嵌入式控制和机械调校功底,更需要掌握图像处理与机器学习的基本应用。
1.1 竞赛规则与得分要点分析(以21届为例)
根据第二十一届竞赛规则,智能视觉组的任务通常包含以下几个关键得分点:
- 基础循迹:小车需稳定、快速地沿赛道中心线行驶,这是完成所有高级任务的前提。
- 视觉标签识别:在特定区域(如物料区、任务点)识别AprilTag、数字、颜色或特定图案等视觉标签。识别准确率和速度直接影响任务得分。
- 任务执行:根据识别结果,控制机械臂、舵机等执行机构完成抓取、放置、按压等动作。执行的成功率和耗时是拉开差距的关键。
- 整体完成时间:在成功完成所有任务的前提下,用时越短,成绩越好。
因此,我们的备赛策略非常明确:“稳”字当头,先保证基础循迹和视觉识别的100%成功率,再逐步优化速度。
1.2 华工智能视觉组技术栈选型
经过多轮迭代测试,我们最终确定了以下稳定可靠的技术方案:
- 主控平台:逐飞科技的i.MX RT1064核心板。其主频高(600MHz),内存大,外设丰富,能够同时流畅运行图像处理算法和电机控制程序。
- 图像传感器:总钻风MT9V034全局快门摄像头。全局快门在高速运动下无果冻效应,是智能车竞赛的经典选择。
- 循迹方案:双摄像头方案。一个摄像头(MT9V034)用于视觉识别,另一个摄像头(可能同型号或OV系列)专用于循迹。将循迹和识别解耦,避免了单摄像头既要巡线又要找标签导致的资源竞争和算法干扰,稳定性极大提升。
- 图像处理与识别:传统图像处理(OpenCV算法思想) + 轻量级机器学习。对于AprilTag,使用专门的轻量级检测库;对于颜色、形状识别,采用阈值分割、轮廓查找等传统算法,在RT1064上足以达到实时性要求。
- 开发环境:Keil MDK。配合逐飞提供的开源库,可以快速搭建工程,专注于上层应用逻辑。
这套方案的核心优势在于解耦与专精:循迹摄像头只关心赛道边线,视觉识别摄像头只关心前方的标签,两者通过主控芯片进行数据融合和决策,系统鲁棒性显著增强。
2. 硬件系统搭建与机械调校
稳定的硬件是算法得以发挥的基础。很多队伍前期出现的问题,根源都在硬件。
2.1 车模机械调整关键点
- 重心配置:电池、主控等重物应尽量放置在车体中心并降低高度。低重心能有效减少过弯时的侧倾,提高高速稳定性。
- 前轮前束与主销后倾:根据规则允许的范围进行微调。适当的前束可以提高直道稳定性,适当的主销后倾角能提供一定的回正力矩。这部分需要结合赛道实际情况反复路试。
- 摄像头支架刚性:务必保证摄像头支架牢固、无抖动。任何微小的震动都会导致图像模糊,严重影响循迹和识别精度。我们使用了碳纤维杆配合3D打印的加固件,效果很好。
- 接线与供电:所有线缆应捆扎整齐,避免干扰传感器(特别是电磁组)或卷入车轮。电机、舵机与核心板、传感器应分开供电,并使用大电容进行电源滤波,防止大电流负载导致电压骤降,引起单片机复位。
2.2 电路设计与传感器布局
- 电机驱动电路:确保MOS管散热良好,驱动能力充足。可以适当增加并联MOS管数量以降低内阻和发热。
- 摄像头接口:使用排线连接时,注意固定接口,防止在颠簸中松动。条件允许的话,可以对排线接口进行打胶固定。
- 视觉识别摄像头安装:安装高度和俯角需要仔细计算。要确保在识别区域内,标签图像既不会太小(难以识别),也不会因透视畸变过大而影响识别算法。通常需要制作一个可多维度调节的摄像头云台。
3. 核心算法实现:循迹与视觉识别
这是整个系统的软件核心。我们采用模块化设计,将循迹、识别、控制、决策等任务分开。
3.1 基于摄像头的自适应循迹算法
我们的循迹摄像头采集图像后,处理流程如下:
// 伪代码流程,基于逐飞库的框架 void track_line_process(void) { // 1. 图像采集 Get_Image(&track_cam, track_image_buffer); // 2. 图像预处理 // 二值化:采用动态阈值或大津法,适应不同光照 uint8_t threshold = OtsuThreshold(track_image_buffer, IMAGE_WIDTH, IMAGE_HEIGHT); Binaryzation(track_image_buffer, binary_image, threshold); // 3. 边线提取 // 使用“边线搜索”算法,从图像底部向上,左右两侧分别搜索跳变点 for (int row = IMAGE_HEIGHT - 1; row >= 0; row -= SEARCH_STEP) { Find_Left_Edge_Point(binary_image, row, &left_edge[row]); Find_Right_Edge_Point(binary_image, row, &right_edge[row]); } // 4. 中线计算与偏差生成 // 选取图像下方几行可靠的中线点,计算平均偏差 float error_sum = 0.0f; int valid_count = 0; for (int i = 0; i < USEFUL_ROW; i++) { if (left_edge[i].valid && right_edge[i].valid) { center_point[i] = (left_edge[i].col + right_edge[i].col) / 2; error_sum += (center_point[i] - IMAGE_CENTER_COL); valid_count++; } } if (valid_count > 0) { g_track_error = error_sum / valid_count; // 最终的横向偏差 } else { // 丢线处理,使用上一次偏差或执行恢复策略 g_track_error = g_last_track_error; } // 5. 控制量计算 (PD控制) g_track_pid.d_error = g_track_error - g_last_track_error; g_track_pid.output = g_track_pid.kp * g_track_error + g_track_pid.kd * g_track_pid.d_error; g_last_track_error = g_track_error; // 6. 输出控制:舵机打角 Set_Steer_Angle(DEFAULT_ANGLE + g_track_pid.output); }关键优化点:
- 动态二值化:固定阈值在光照变化时极易失效。我们实现了大津法(Otsu)或根据图像灰度统计进行动态计算,鲁棒性大幅提升。
- 丢线处理:当搜索不到边线时,不能简单地将偏差置零。我们采用记忆上一次有效偏差或根据赛道元素(如十字、环岛)进行预测的策略,帮助小车快速找回赛道。
- 控制分段:直道、弯道、入弯、出弯使用不同的PID参数。可以通过偏差大小或偏差变化率来动态切换。
3.2 视觉标签识别算法(以AprilTag为例)
AprilTag是一种高效的视觉基准库,其识别速度快、抗干扰能力强。我们在RT1064上移植了轻量级的AprilTag 3库。
// AprilTag识别任务流程 void vision_tag_detect_task(void) { // 1. 采集识别专用摄像头的图像 Get_Image(&vision_cam, vision_image_buffer); // 2. 图像预处理(可适当降分辨率以提速) image_u8_t im = { .width = VISION_WIDTH, .height = VISION_HEIGHT, .stride = VISION_WIDTH, .buf = vision_image_buffer }; // 3. 调用AprilTag检测器 zarray_t *detections = apriltag_detector_detect(tag_detector, &im); // 4. 解析结果 if (zarray_size(detections) > 0) { apriltag_detection_t *det; zarray_get(detections, 0, &det); // 通常只处理第一个检测到的标签 g_detected_tag_id = det->id; // 获取标签ID g_tag_center_x = det->c[0]; // 标签中心图像坐标X g_tag_center_y = det->c[1]; // 标签中心图像坐标Y // 5. 计算距离或姿态(可选,用于精细控制) // 需要已知标签的物理尺寸和相机内参 // estimate_tag_pose(det, ...); } else { g_detected_tag_id = -1; // 未识别到标签 } zarray_destroy(detections); // 6. 根据识别结果,设置全局任务标志位 if (g_detected_tag_id != -1) { switch (g_detected_tag_id) { case 0: g_task_command = PICK_UP_RED; break; case 1: g_task_command = PICK_UP_BLUE; break; case 2: g_task_command = DROP_AT_ZONE_A; break; // ... 其他任务映射 default: break; } } }关键优化点:
- 识别区域限定(ROI):只在赛道规定的任务区域内进行全图识别,其他区域直接跳过,极大节省计算时间。
- 多帧确认:连续识别到相同ID的标签N帧后,才判定为有效识别,避免单帧误触发。
- 标签ID过滤:只关注当前赛段可能出现的标签ID,忽略无关ID。
- 资源管理:AprilTag检测器在初始化后常驻内存,避免重复创建销毁。图像可降采样至QVGA(320x240)或更低分辨率,在RT1064上能达到60fps以上的识别速度。
4. 系统软件架构与任务调度
一个清晰的软件架构能让调试事半功倍。我们采用“前台-后台”与有限状态机(FSM)结合的模式。
4.1 主程序框架
// main.c 主循环框架 int main(void) { // 硬件初始化 board_init(); camera_init(&track_cam, TRACK_CAM_ADDR); camera_init(&vision_cam, VISION_CAM_ADDR); motor_init(); steer_init(); uart_init(); // 用于调试输出 // AprilTag等算法初始化 apriltag_init(); // 全局变量初始化 g_system_state = STATE_BOOT; g_task_command = CMD_NONE; while (1) { // 1. 状态机调度 switch (g_system_state) { case STATE_BOOT: if (check_start_condition()) g_system_state = STATE_RUNNING; break; case STATE_RUNNING: running_state_handler(); break; case STATE_TASK_EXECUTING: task_execute_handler(); break; case STATE_ERROR: error_handler(); break; } // 2. 后台任务(固定频率执行) static uint32_t last_tick = 0; if (get_systick() - last_tick > 10) { // 每10ms执行一次 last_tick = get_systick(); // 图像采集与处理(放在不同周期,避免冲突) if (g_image_capture_flag) { track_line_process(); // 循迹处理 g_image_capture_flag = 0; } // 电机PID控制 motor_pid_control(); // 调试信息发送 send_debug_info(); } // 视觉识别任务(频率可略低于循迹,如20ms) static uint32_t last_vision_tick = 0; if (get_systick() - last_vision_tick > 20) { last_vision_tick = get_systick(); vision_tag_detect_task(); } } }4.2 状态机设计示例:任务执行状态
// 任务执行状态处理函数 void task_execute_handler(void) { switch (g_task_command) { case PICK_UP_RED: // 控制机械臂运动到预定义位置 set_servo_angle(ARM, PICK_ANGLE); delay_ms(300); // 闭合夹爪 set_servo_angle(CLAW, CLOSE_ANGLE); delay_ms(200); // 抬起机械臂 set_servo_angle(ARM, CARRY_ANGLE); g_task_command = CMD_NONE; // 清除命令 g_system_state = STATE_RUNNING; // 返回循迹状态 break; case DROP_AT_ZONE_A: // 行驶到投放区(可通过编码器或视觉辅助定位) move_forward_encoder(1000); // 前进一定距离 // 执行投放动作 set_servo_angle(CLAW, OPEN_ANGLE); delay_ms(200); g_task_command = CMD_NONE; g_system_state = STATE_RUNNING; break; // ... 其他任务 } }5. 调试技巧与参数整定方法论
调车是智能车竞赛中最耗时但也最关键的环节。
5.1 循迹PID参数调试步骤
- 归零:先将
Kp,Ki,Kd全部设为0。 - 调
Kp(比例):逐渐增大Kp,让小车在直道上能对偏差有反应,开始“摇摆”地回到中线。此时摇摆收敛即可,不要过大。 - 调
Kd(微分):增加Kd来抑制Kp引起的振荡。观察小车过弯,Kd能提供“预见性”,防止冲出赛道。调试目标是过弯平滑,出弯迅速回正。 - 慎用
Ki(积分):在智能车这种快速动态系统中,积分项容易导致超调和振荡。除非存在静态误差(如小车始终偏一侧),否则一般设为0或极小值。 - 分段参数:在代码中根据偏差绝对值或偏差变化率设置多组PID参数。例如,小偏差时用一组柔和参数保证直道稳定;大偏差时用一组激进参数快速过弯。
5.2 视觉识别调试工具
利用串口无线模块(如蓝牙、WiFi)将调试信息发送到上位机(如匿名上位机、逐飞的上位机、自己编写的Python工具)是必不可少的。
- 发送图像二值化结果:将摄像头二值化后的图像通过串口发送,在上位机实时显示,可以直观判断阈值是否合适。
- 发送边线坐标:将提取到的左右边线点坐标发送出去,绘制成曲线,可以分析巡线算法的可靠性。
- 发送识别结果与坐标:将识别到的标签ID、中心坐标、置信度实时发送,便于判断识别区域和稳定性。
- 发送控制量:发送舵机打角值、电机目标速度等,可以绘制控制曲线,分析系统响应。
5.3 系统性测试清单
在每次修改代码或参数后,按顺序进行测试:
- 静态测试:小车架空,观察电机、舵机响应是否符合预期,串口数据是否正常。
- 低速闭环测试:在赛道上以很低的速度运行,测试循迹基础逻辑是否正确,能否走完全程。
- 视觉触发测试:在低速下,手持标签在摄像头前移动,测试识别是否准确,任务状态机切换是否正确。
- 逐速测试:从低速开始,逐步提高速度上限,每提高一次,进行多次全赛道测试,确保稳定性。
- 压力测试:连续运行10-20圈,观察是否有内存泄漏、程序跑飞、电机过热等问题。
6. 常见问题与故障排查
以下是我们在备赛过程中遇到的高频问题及解决方案:
| 问题现象 | 可能原因 | 排查思路与解决方案 |
|---|---|---|
| 小车启动后原地抖动或跑偏 | 1. 电机初始化相位错误。 2. 陀螺仪未校准或安装不水平。 3. 机械结构卡滞。 | 1. 交换电机线序或检查驱动芯片初始化代码。 2. 重新校准陀螺仪,确保车体静止水平。 3. 检查轮胎、齿轮是否顺畅,底盘是否平整。 |
| 循迹时频繁丢线 | 1. 摄像头阈值不适用当前光照。 2. 边线搜索算法容错太低。 3. 摄像头抖动或焦距不对。 | 1. 改用动态阈值算法(大津法)。 2. 增加无效点的容忍度,优化丢线后的预测策略。 3. 加固摄像头支架,重新调整焦距使赛道清晰。 |
| 识别AprilTag时有时无 | 1. 图像模糊(运动模糊或失焦)。 2. 光照不均,标签过曝或过暗。 3. 识别区域(ROI)设置不当。 | 1. 确保使用全局快门摄像头,并调好焦距。 2. 调整摄像头曝光时间或增益,或增加补光灯。 3. 精确计算并缩小ROI,减少处理面积,提高帧率。 |
| 执行任务时车身晃动导致失败 | 1. 执行机构动作时,未考虑车身惯性。 2. 机械臂动作太快,冲击大。 | 1. 在执行任务前,先让小车减速或短暂停车。 2. 为舵机动作增加缓启动/缓停止控制,分散冲击力。 |
| 长时间运行后程序死机 | 1. 堆栈溢出。 2. 中断冲突。 3. 内存泄漏(如频繁malloc/free)。 | 1. 增大任务堆栈大小,检查递归调用深度。 2. 梳理中断优先级,避免在中断中处理复杂任务。 3. 使用静态内存分配,避免动态分配。 |
7. 进阶优化与国赛准备建议
当小车能够稳定完赛后,以下优化可以进一步提升成绩和鲁棒性:
运动控制优化:
- 速度规划:不是全程全速。在直道加速,入弯前减速,弯心保持,出弯加速。可以预先对赛道进行分段,设置每段的目标速度。
- 前馈控制:在已知的弯道(如通过元素识别)提前给舵机一个偏角,减少纯反馈控制的滞后。
- 差速控制:在弯道中,让内侧轮速略低于外侧轮速,实现更流畅的过弯。
视觉识别增强:
- 多标签处理:当视野中出现多个标签时,根据位置、大小进行优先级排序,选择最可能是目标的那一个。
- 融合定位:结合编码器里程计信息,粗略判断小车是否到达了任务区域,从而决定是否开启识别,避免误识别赛道旁的干扰物。
系统容错设计:
- 看门狗:务必启用硬件看门狗(IWDG),在程序跑飞时能自动复位。
- 传感器冗余校验:例如,用编码器速度反馈辅助判断电机是否堵转。
- 安全状态:设计一个独立的“安全守护”任务,监控主要任务的心跳。一旦某个任务卡死,能强制系统进入安全停车状态。
国赛适应性训练:
- 多变光照:在强光、弱光、侧光等不同环境下测试,确保算法适应性。
- 赛道干扰:自己制作一些类似标签的干扰物放在赛道旁,测试识别的抗干扰能力。
- 压力测试:进行上百次的连续发车测试,统计成功率,寻找并消除随机性故障。
备战国赛,除了技术打磨,心态和团队协作同样重要。合理安排进度,定期组织模拟比赛,做好代码版本管理(如使用Git),确保每个成员都清晰理解系统全貌。最后,祝各位在智能车竞赛的舞台上,稳定发挥,赛出佳绩。
