智能车竞赛视觉组技术方案:双摄像头循迹与AprilTag识别实战

在准备第二十一届全国大学生智能汽车竞赛华南赛区的过程中,我们团队(华工智能视觉组)经历了从方案选型、代码调试到最终稳定进国赛的完整闭环。智能视觉组别因其融合了传统循迹控制与前沿的机器视觉识别,挑战性和趣味性并存。本文将系统复盘我们最终进国赛版本的完整技术方案,涵盖硬件选型、核心算法、代码架构以及关键的调参避坑经验,目标是提供一份可供后来者直接复现或深度借鉴的实战指南。

1. 智能视觉组核心任务与备赛思路解析

全国大学生智能汽车竞赛的智能视觉组,其核心任务是要求小车在完成基础循迹(如电磁、摄像头巡线)的同时,利用车载摄像头对赛道旁的视觉标签进行实时识别,并根据识别结果执行相应的动作,例如搬运物料、定点停车、声光提示等。这要求参赛队伍不仅要有扎实的嵌入式控制和机械调校功底,更需要掌握图像处理与机器学习的基本应用。

1.1 竞赛规则与得分要点分析(以21届为例)

根据第二十一届竞赛规则,智能视觉组的任务通常包含以下几个关键得分点:

  1. 基础循迹:小车需稳定、快速地沿赛道中心线行驶,这是完成所有高级任务的前提。
  2. 视觉标签识别:在特定区域(如物料区、任务点)识别AprilTag、数字、颜色或特定图案等视觉标签。识别准确率和速度直接影响任务得分。
  3. 任务执行:根据识别结果,控制机械臂、舵机等执行机构完成抓取、放置、按压等动作。执行的成功率和耗时是拉开差距的关键。
  4. 整体完成时间:在成功完成所有任务的前提下,用时越短,成绩越好。

因此,我们的备赛策略非常明确:“稳”字当头,先保证基础循迹和视觉识别的100%成功率,再逐步优化速度。

1.2 华工智能视觉组技术栈选型

经过多轮迭代测试,我们最终确定了以下稳定可靠的技术方案:

  • 主控平台:逐飞科技的i.MX RT1064核心板。其主频高(600MHz),内存大,外设丰富,能够同时流畅运行图像处理算法和电机控制程序。
  • 图像传感器:总钻风MT9V034全局快门摄像头。全局快门在高速运动下无果冻效应,是智能车竞赛的经典选择。
  • 循迹方案双摄像头方案。一个摄像头(MT9V034)用于视觉识别,另一个摄像头(可能同型号或OV系列)专用于循迹。将循迹和识别解耦,避免了单摄像头既要巡线又要找标签导致的资源竞争和算法干扰,稳定性极大提升。
  • 图像处理与识别:传统图像处理(OpenCV算法思想) + 轻量级机器学习。对于AprilTag,使用专门的轻量级检测库;对于颜色、形状识别,采用阈值分割、轮廓查找等传统算法,在RT1064上足以达到实时性要求。
  • 开发环境:Keil MDK。配合逐飞提供的开源库,可以快速搭建工程,专注于上层应用逻辑。

这套方案的核心优势在于解耦与专精:循迹摄像头只关心赛道边线,视觉识别摄像头只关心前方的标签,两者通过主控芯片进行数据融合和决策,系统鲁棒性显著增强。

2. 硬件系统搭建与机械调校

稳定的硬件是算法得以发挥的基础。很多队伍前期出现的问题,根源都在硬件。

2.1 车模机械调整关键点

  1. 重心配置:电池、主控等重物应尽量放置在车体中心并降低高度。低重心能有效减少过弯时的侧倾,提高高速稳定性。
  2. 前轮前束与主销后倾:根据规则允许的范围进行微调。适当的前束可以提高直道稳定性,适当的主销后倾角能提供一定的回正力矩。这部分需要结合赛道实际情况反复路试。
  3. 摄像头支架刚性:务必保证摄像头支架牢固、无抖动。任何微小的震动都会导致图像模糊,严重影响循迹和识别精度。我们使用了碳纤维杆配合3D打印的加固件,效果很好。
  4. 接线与供电:所有线缆应捆扎整齐,避免干扰传感器(特别是电磁组)或卷入车轮。电机、舵机与核心板、传感器应分开供电,并使用大电容进行电源滤波,防止大电流负载导致电压骤降,引起单片机复位。

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参数调试步骤

  1. 归零:先将Kp,Ki,Kd全部设为0。
  2. Kp(比例):逐渐增大Kp,让小车在直道上能对偏差有反应,开始“摇摆”地回到中线。此时摇摆收敛即可,不要过大。
  3. Kd(微分):增加Kd来抑制Kp引起的振荡。观察小车过弯,Kd能提供“预见性”,防止冲出赛道。调试目标是过弯平滑,出弯迅速回正。
  4. 慎用Ki(积分):在智能车这种快速动态系统中,积分项容易导致超调和振荡。除非存在静态误差(如小车始终偏一侧),否则一般设为0或极小值。
  5. 分段参数:在代码中根据偏差绝对值或偏差变化率设置多组PID参数。例如,小偏差时用一组柔和参数保证直道稳定;大偏差时用一组激进参数快速过弯。

5.2 视觉识别调试工具

利用串口无线模块(如蓝牙、WiFi)将调试信息发送到上位机(如匿名上位机、逐飞的上位机、自己编写的Python工具)是必不可少的。

  • 发送图像二值化结果:将摄像头二值化后的图像通过串口发送,在上位机实时显示,可以直观判断阈值是否合适。
  • 发送边线坐标:将提取到的左右边线点坐标发送出去,绘制成曲线,可以分析巡线算法的可靠性。
  • 发送识别结果与坐标:将识别到的标签ID、中心坐标、置信度实时发送,便于判断识别区域和稳定性。
  • 发送控制量:发送舵机打角值、电机目标速度等,可以绘制控制曲线,分析系统响应。

5.3 系统性测试清单

在每次修改代码或参数后,按顺序进行测试:

  1. 静态测试:小车架空,观察电机、舵机响应是否符合预期,串口数据是否正常。
  2. 低速闭环测试:在赛道上以很低的速度运行,测试循迹基础逻辑是否正确,能否走完全程。
  3. 视觉触发测试:在低速下,手持标签在摄像头前移动,测试识别是否准确,任务状态机切换是否正确。
  4. 逐速测试:从低速开始,逐步提高速度上限,每提高一次,进行多次全赛道测试,确保稳定性。
  5. 压力测试:连续运行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. 进阶优化与国赛准备建议

当小车能够稳定完赛后,以下优化可以进一步提升成绩和鲁棒性:

  1. 运动控制优化

    • 速度规划:不是全程全速。在直道加速,入弯前减速,弯心保持,出弯加速。可以预先对赛道进行分段,设置每段的目标速度。
    • 前馈控制:在已知的弯道(如通过元素识别)提前给舵机一个偏角,减少纯反馈控制的滞后。
    • 差速控制:在弯道中,让内侧轮速略低于外侧轮速,实现更流畅的过弯。
  2. 视觉识别增强

    • 多标签处理:当视野中出现多个标签时,根据位置、大小进行优先级排序,选择最可能是目标的那一个。
    • 融合定位:结合编码器里程计信息,粗略判断小车是否到达了任务区域,从而决定是否开启识别,避免误识别赛道旁的干扰物。
  3. 系统容错设计

    • 看门狗:务必启用硬件看门狗(IWDG),在程序跑飞时能自动复位。
    • 传感器冗余校验:例如,用编码器速度反馈辅助判断电机是否堵转。
    • 安全状态:设计一个独立的“安全守护”任务,监控主要任务的心跳。一旦某个任务卡死,能强制系统进入安全停车状态。
  4. 国赛适应性训练

    • 多变光照:在强光、弱光、侧光等不同环境下测试,确保算法适应性。
    • 赛道干扰:自己制作一些类似标签的干扰物放在赛道旁,测试识别的抗干扰能力。
    • 压力测试:进行上百次的连续发车测试,统计成功率,寻找并消除随机性故障。

备战国赛,除了技术打磨,心态和团队协作同样重要。合理安排进度,定期组织模拟比赛,做好代码版本管理(如使用Git),确保每个成员都清晰理解系统全貌。最后,祝各位在智能车竞赛的舞台上,稳定发挥,赛出佳绩。