ARTICLE DETAIL

资讯详情

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

智能车轮腿组室外视觉识别方案设计与开源实战

智能车轮腿组室外视觉识别方案设计与开源实战 各位做智能车竞赛的同学或者在嵌入式视觉方向踩坑的朋友大家好。第 21 届智能车竞赛的轮腿组应该让不少人印象很深。这个组别既保留了轮式车辆的速度优势又加入了腿部结构的越障能力再加上室外场地的强光、阴影、路面变化整体难度比室内赛道高了一截。更让我觉得有意思的是网上关于轮腿组的开源方案非常少。多数队伍还是沿用室内摄像头循迹的思路到了室外就出现反光过曝、阴影误判、坡道丢失目标等一系列问题。我这次采用的是一套比较“猎奇”的方案轮腿机械结构 室外视觉识别 开源代码最后拿到了国一。这篇文章我会把整套方案的设计思路、关键代码、调参过程和踩坑记录完整分享出来。内容会偏实战适合两类读者正在准备智能车竞赛尤其是轮腿组、室外视觉组想找一套完整参考方案的参赛队员。对嵌入式视觉、运动控制、轮腿结构感兴趣的开发者想了解这类系统是怎么从零搭起来的。文章不会只贴代码我会把每一个模块“为什么这么设计”也讲清楚。1. 背景与核心概念1.1 智能车竞赛中的轮腿组是什么先给第一次接触智能车竞赛的读者简单介绍一下。全国大学生智能车竞赛是面向在校大学生的一项嵌入式系统竞赛参赛队伍需要基于竞赛指定的单片机平台自己设计车模机械结构、编写控制算法、调试传感器让小车在赛道上自主运行。轮腿组是第 21 届竞赛中出现的新组别。它的特点从名字就能看出来轮车辆主要靠轮子驱动速度相对较快。腿底盘带有腿部结构可以主动抬升或调整姿态越过路肩、坡道等障碍。轮腿结构本质上是一种“轮腿混合式移动平台”。它比纯轮式多了一个自由度可以通过腿部动作改变车身高度和姿态比纯腿式结构简单控制也更容易收敛。在我个人看来轮腿组最难的还不是机械结构本身而是“轮”和“腿”之间的控制耦合。车辆高速行驶时腿部动作会影响重心位置重心变化又会反馈到视觉传感器的姿态上传感器姿态变化又会影响赛道识别结果。这条链路相互影响处理不好就会变成一边跑一边晃。1.2 室外视觉方案为什么有难度说完轮腿组再来说室外视觉。很多室内智能车方案采用灰度摄像头或线性 CCD因为室内光线稳定赛道元素颜色统一背景干扰少。到了室外情况完全不一样光照强度变化大上午、中午、下午的光照角度不同图像亮度差异明显。阴影会导致赛道边缘出现假边界阳光直射区域容易过曝。路面材质不统一柏油路、水泥路、砖路的灰度特征不一样。室外场地可能出现树叶、纸屑、积水反光等干扰物。所以室外视觉方案不能直接照搬室内的“图像二值化 边缘提取”思路。你需要一个对光照鲁棒的预处理流程还需要在控制层面加入置信度判断让小车在图像质量差的时候“知道”自己看到了什么、没看到什么。1.3 “猎奇方案”解决了什么问题说回我这次的开源方案。我把它称为“猎奇”是因为相比传统方案它有几个不太一样的设计视觉处理不使用高算力平台而是在竞赛主控芯片上直接跑图像处理算法流程非常轻量。轮腿控制采用分层结构上层做视觉寻迹下层做姿态平衡两层通过状态机协同。室外图像处理不追求“完美的二值化”而是通过多特征投票判断赛道元素。全流程开源包括图像处理、控制算法、上位机调参工具和机械结构说明。这套方案解决的核心问题是在室外光照复杂、赛道元素多样、轮腿姿态不断变化的情况下让小车依然能稳定判断赛道边界和障碍位置。下面我会按实际开发顺序从系统架构讲到代码实现。2. 整体方案架构2.1 系统组成我的轮腿视觉小车从硬件角度看可以划分为以下部分模块作用说明主控板运行控制算法和图像处理竞赛指定的单片机平台摄像头模块采集赛道前方图像采用数字摄像头输出 RGB 或灰度图像轮腿底盘驱动车辆前进、转向、越障左右轮独立驱动带腿部抬升舵机姿态传感器检测车身倾角和角速度用于轮腿平衡控制一般使用 IMU编码器检测轮速用于速度闭环控制电源模块为主控、舵机、电机供电需要单独稳压避免电机启动拉低电压调试模块输出图像和参数使用无线串口或 SD 卡记录日志这里要提醒大家轮腿车的供电设计非常重要。舵机和电机启动瞬间电流很大如果和主控共用电源且没有隔离会导致单片机复位。我的做法是电源分开电机/舵机一路主控/传感器一路中间做好共地。2.2 信息流向整个系统的信息流可以简单描述为摄像头采集图像 ↓ 图像预处理灰度化、滤波 ↓ 赛道元素识别边界、坡道、路肩 ↓ 计算横向偏差和转角输出 ↓ 轮腿运动状态机 ↓ 平衡控制器 速度控制器 ↓ 电机和舵机输出为了提高稳定性我在控制回路里加入了一个“视觉置信度”变量。当图像识别置信度较低时车辆会主动降速等待更多有效帧而不是盲目打角。2.3 开源仓库结构下面是开源项目的目录结构读者可以对照这个结构定位代码smartcar-wheel-leg/ ├── doc/ │ ├── 机械结构说明.md │ └── 硬件接线图.png ├── firmware/ │ ├── image_process/ │ │ ├── preprocess.c │ │ ├── preprocess.h │ │ ├── track_detect.c │ │ └── track_detect.h │ ├── control/ │ │ ├── balance.c │ │ ├── balance.h │ │ ├── speed_control.c │ │ └── speed_control.h │ ├── state_machine/ │ │ ├── state_machine.c │ │ └── state_machine.h │ └── main.c ├── host_tool/ │ ├── image_viewer.py │ └── parameter_tuning.py └── README.md后面我会挑最核心的几段代码展开完整的代码内容可以直接看开源仓库。3. 环境准备与工具链3.1 硬件清单与接线建议我这次使用的硬件配置如下给大家做参考主控芯片竞赛指定系列的单片机具体型号不同赛区可能有差异本文不写死代码遵循标准外设库写法。摄像头数字灰度摄像头分辨率为 188×120 左右帧率 50fps 以上。姿态传感器六轴 IMU用于获取俯仰角和角速度。电机直流减速电机带编码器左右各一个。舵机用于腿部抬升选择扭矩足够的数字舵机。底盘自行设计加工的轮腿结构前轮为万向轮或驱动轮后轮为驱动轮。接线建议遵循以下原则摄像头数据线尽量短避免信号干扰。IMU 安装在车身重心附近且尽量靠近底盘中心。编码器信号线需要上拉电阻具体看编码器类型。舵机电源和主控电源必须隔离。3.2 软件开发环境软件开发环境主要分为两部分嵌入式工程编译环境使用竞赛指定的 IDE 或交叉编译工具链例如 Keil、IAR 或 GCC 工具链。上位机分析环境使用 Python 3 和 OpenCV 编写图像分析工具用于离线查看小车采集到的图像调整阈值参数。Python 环境建议安装以下依赖pip install opencv-python numpy matplotlib pyserial其中pyserial用于读取小车通过无线串口发送回来的图像数据。如果暂时没有硬件也可以把小车 SD 卡里保存的图片导入到上位机里分析。3.3 工程构建说明嵌入式工程遵循以下编译方式cd firmware make clean make如果你没有使用 Makefile 工程而是使用 IDE那么需要把以下目录添加到工程中firmware/image_process/ firmware/control/ firmware/state_machine/所有源文件都使用标准 C 语言编写不依赖特定厂商的私有库移植到其他主控平台时只需要替换底层驱动接口。4. 室外视觉识别核心原理4.1 图像采集与预处理室外环境第一个要解决的问题是光照变化。我采用的预处理流程是灰度化。高斯滤波去除噪声。自适应阈值分割。形态学开运算去除小噪点。这里关键的一点是“自适应阈值”。室内方案常用固定阈值二值化到了室外固定阈值常常出现这种情况同一块路面上午识别为白色下午识别为黑色。自适应阈值的思路是对图像中每一个像素点计算它周围一个局部区域的平均灰度然后根据这个局部平均值决定当前像素是赛道还是背景。核心代码如下// 文件路径firmware/image_process/preprocess.c // 自适应阈值二值化 // src: 输入灰度图 // dst: 输出二值图0为背景1为目标 // wh: 局部窗口半宽 void adaptive_threshold(unsigned char *src, unsigned char *dst, int width, int height, int wh) { long sum 0; int count 0; int x, y, dx, dy; int x0, x1, y0, y1; for (y 0; y height; y) { for (x 0; x width; x) { x0 x - wh; if (x0 0) x0 0; x1 x wh; if (x1 width) x1 width - 1; y0 y - wh; if (y0 0) y0 0; y1 y wh; if (y1 height) y1 height - 1; sum 0; count 0; for (dy y0; dy y1; dy) { for (dx x0; dx x1; dx) { sum src[dy * width dx]; count; } } if (src[y * width x] * count sum) { dst[y * width x] 1; } else { dst[y * width x] 0; } } } }这里有一个性能问题需要说明。上面这种写法是“最暴力的实现”每个像素都要计算邻域均值复杂度是O(width × height × wh²)在 MCU 上跑起来很慢。实际项目中更推荐用“积分图”来优化。积分图可以在常数时间内计算任意矩形区域的像素和处理一帧图像只需要做一次完整遍历。示例如下// 文件路径firmware/image_process/preprocess.c // 使用积分图加速的均值计算 static void compute_integral_image(unsigned char *src, unsigned long *integral, int width, int height) { int x, y; unsigned long row_sum 0; for (y 0; y height; y) { row_sum 0; for (x 0; x width; x) { row_sum src[y * width x]; if (y 0) { integral[y * width x] row_sum; } else { integral[y * width x] integral[(y - 1) * width x] row_sum; } } } }使用积分图之后自适应阈值二值化的耗时大概可以降到原来的几十分之一一帧 188×120 图像在常见主控上可以控制在几个毫秒级别。4.2 赛道元素识别室外赛道元素一般包括赛道边界通常由路肩或色带表示。坡道需要识别坡道的起止位置。障碍物比如路肩、锥桶等。十字路口需要判断是否直行。我在方案里没有只依赖单一特征而是采用“多特征投票”的思路。每个赛道元素用不同的特征检测器然后综合各方判断得出结果。以边界检测为例特征 1二值化后左右两侧黑白跳变点。这是传统寻迹线思路。特征 2颜色空间中的色相特征如果赛道边界是红色路肩可以检测红色区域。特征 3赛道边缘线的连续性和斜率。当三个特征里至少两个认为“这里是边界”才会将边界作为有效输入。这种方式在室外非常实用因为某个特征可能受到光照影响失效但多个特征同时失效的概率会小很多。这里我贴一段基于“逐行扫描边界点”的简化代码// 文件路径firmware/image_process/track_detect.c // 从二值图中提取左右边界点 // binary: 二值图 // left_boundary, right_boundary: 输出边界数组 // row_start, row_stop: 扫描范围 void extract_boundary(unsigned char *binary, int width, int height, int *left_boundary, int *right_boundary, int row_start, int row_stop) { int row, col; int left_found, right_found; for (row row_start; row row_stop; row) { left_found -1; right_found -1; // 从左往右找左侧边界 for (col 0; col width; col) { if (binary[row * width col] 1) { left_found col; break; } } // 从右往左找右侧边界 for (col width - 1; col 0; col--) { if (binary[row * width col] 1) { right_found col; break; } } left_boundary[row] left_found; right_boundary[row] right_found; } }在实际代码中我会对边界点做滤波把明显跳变的异常点剔除掉。例如某一行的左边界突然比相邻行偏移了 40 个像素大概率不是赛道弯道而是图像噪声。4.3 透视变换与偏差计算摄像头装在车身上拍摄到的赛道是一个“近大远小”的透视画面。为了计算车辆相对赛道中心线的横向偏差需要对图像做逆透视变换。不过我要说明一点在单片机平台上做完整的逆透视变换矩阵运算是可行的但成本偏高。我采用的是一种近似方案将图像按行划分成多个区域每个区域给予不同权重。图像近处的行权重高因为近处赛道信息最可靠也是转向的主要依据。图像远处的行权重低主要用来提前感知弯道方向。横向偏差的计算公式为偏差 Σ单行中点 - 图像中心× 行权重 / Σ行权重用代码表示为// 文件路径firmware/image_process/track_detect.c // 计算赛道中心线的横向偏差 // 返回值小于0表示车辆偏左大于0表示车辆偏右 int calc_lateral_error(int *left_boundary, int *right_boundary, int width, int height, int row_start, int row_stop) { int row; int mid, center width / 2; long weighted_error 0; long weight_sum 0; long weight; for (row row_start; row row_stop; row) { if (left_boundary[row] 0 || right_boundary[row] 0) { continue; } mid (left_boundary[row] right_boundary[row]) / 2; // 近处权重大远处权重小 weight (row - row_start 1); weighted_error (mid - center) * weight; weight_sum weight; } if (weight_sum 0) { return 0; } return (int)(weighted_error / weight_sum); }这样得到的是一个一维误差量可以直接输入给转向控制器。相比输出完整的赛道图像一维偏差的计算量小很多控制延迟也更低。4.4 坡道和障碍识别轮腿组必须处理坡道和障碍这需要车辆提前做出“抬腿”或“调整姿态”的决策。我的识别逻辑是坡道识别观察边界线的纵向分布当远处赛道消失或者边界线的斜率发生大范围突变时判定为坡道入口。障碍识别观察赛道内部是否出现高于路面的物体通过二值图中赛道区域内的块状目标来判断。当检测到坡道入口状态机会在合适距离内执行“抬腿准备”等车辆真正进入坡道时腿部已经调整到对应角度。这样不会在坡道中间才匆忙抬腿导致车身姿态突变。5. 轮腿运动控制实现5.1 轮腿结构模型轮腿结构可以简化成一个倒立摆模型加一个速度模型。车身俯仰角由 IMU 测量。轮子速度由编码器测量。腿部角度由舵机反馈位置控制。车辆直行时腿部固定在一个高度此时控制问题退化为“轮式倒立摆平衡 速度跟踪”。遇到坡道或障碍时腿部角度变化重心高度改变此时平衡控制器需要根据新的重心位置调整输出。所以在控制架构上我采用串级控制内环做姿态平衡外环做速度跟踪腿部动作单独作为前馈输入。5.2 平衡控制实现平衡控制是轮腿车的核心。如果车辆是两轮自平衡结构那姿态环的频率至少要 500Hz 以上如果是四轮稳定结构频率可以低一些。我的方案采用了两轮驱动加辅助轮的设计既保留了速度优势又比纯两轮结构更容易控制。但在高速转弯时依然需要动态调整腿部姿态来抵抗离心力带来的倾斜。平衡控制器使用串级 PID角度环输入目标俯仰角与实际俯仰角的偏差输出目标角速度。角速度环输入目标角速度与实际角速度的偏差输出电机 PWM。// 文件路径firmware/control/balance.c // 简化版串级平衡控制器 // 调用周期2ms float balance_control(float target_angle, float current_angle, float current_gyro, float dt) { static float angle_error_last 0.0f; static float angle_integral 0.0f; float angle_error target_angle - current_angle; float target_gyro; // 角度环PD angle_integral angle_error * dt; if (angle_integral 100.0f) angle_integral 100.0f; if (angle_integral -100.0f) angle_integral -100.0f; target_gyro Kp_angle * angle_error Ki_angle * angle_integral Kd_angle * (angle_error - angle_error_last) / dt; angle_error_last angle_error; // 角速度环P return Kp_gyro * (target_gyro - current_gyro); }这里的Kp_angle、Ki_angle、Kd_angle、Kp_gyro都需要实际调参。调参顺序是先调角速度环的Kp_gyro让车辆在受到扰动时能快速回正。再调角度环的Kp_angle让车辆能稳定在目标角度附近。最后加入Kd_angle和Ki_angle消除稳态误差和振荡。注意平衡控制的输出直接控制电机 PWM所以输出量需要做限幅防止电机瞬间过流。5.3 运动状态机整个车辆运行过程是一个状态机状态触发条件动作初始化上电腿部回中IMU校准待运行按下启动键进入平衡状态等待视觉信号循迹视觉有效根据偏差控制转向和速度坡道准备检测到坡道入口腿部抬升到预定角度坡道通过车辆倾角明显变化调整速度保持稳定坡道结束倾角恢复腿部恢复回到正常循迹紧急停止视觉连续丢失减速停车状态机的实现建议采用查表法或者 switch-case避免在中断里写复杂逻辑。下面是一个简化的状态机片段// 文件路径firmware/state_machine/state_machine.c // 状态机主循环调用周期10ms void state_machine_run(void) { switch (current_state) { case STATE_INIT: if (system_ready()) { current_state STATE_READY; } break; case STATE_READY: if (start_flag) { current_state STATE_TRACKING; } break; case STATE_TRACKING: if (race_control_flag) { current_state STATE_SLOPE_PREPARE; } break; case STATE_SLOPE_PREPARE: if (slope_entered()) { current_state STATE_SLOPE_PASSING; } break; case STATE_SLOPE_PASSING: if (slope_exited()) { current_state STATE_TRACKING; } break; case STATE_EMERGENCY_STOP: brake(); break; default: break; } }状态切换的时候还要做一件事重置相关控制器的积分项。否则上一状态的误差累积会带入新状态导致刚切换时就出现较大的控制量跳变。6. 完整实战代码解析6.1 主循环代码主循环的逻辑非常简单重点是保证各模块的执行频率图像处理25ms 一次。状态机10ms 一次。平衡控制2ms 一次由定时器中断触发。速度控制5ms 一次。// 文件路径firmware/main.c // 主循环 int main(void) { system_init(); // 时钟、GPIO、外设初始化 imu_init(); // IMU初始化 camera_init(); // 摄像头初始化 motor_init(); // 电机PWM初始化 servo_init(); // 舵机初始化 state_machine_init(); // 状态机初始化 while (1) { // 图像处理任务约25ms执行一次 if (camera_frame_ready()) { camera_get_frame(frame_buffer); preprocess_image(frame_buffer, binary_buffer); extract_boundary(binary_buffer, left_b, right_b); lateral_error calc_lateral_error(left_b, right_b, W, H, 20, H - 10); track_element detect_track_element(binary_buffer, left_b, right_b); image_ready 1; } // 状态机任务约10ms执行一次 if (tick_10ms) { state_machine_run(); } // 根据视觉信息计算目标速度 if (image_ready) { target_speed compute_target_speed(lateral_error, track_element); image_ready 0; } } }6.2 转向控制转向控制沿用经典的比例控制加上一个微分项让转向更平滑// 文件路径firmware/control/speed_control.c // 根据横向偏差计算左右轮速度差 void compute_steering(int lateral_error, int target_speed, int *left_speed, int *right_speed) { int speed_diff; // 偏差单位是像素乘以系数转换为速度差 speed_diff steering_kp * lateral_error; // 限幅 if (speed_diff MAX_SPEED_DIFF) speed_diff MAX_SPEED_DIFF; if (speed_diff -MAX_SPEED_DIFF) speed_diff -MAX_SPEED_DIFF; *left_speed target_speed - speed_diff; *right_speed target_speed speed_diff; }实际运行时左右速度还需要经过编码器的闭环控制才能保证实际速度和目标速度一致。建议使用增量式 PID 做速度环这样积分饱和问题会小很多。6.3 上位机调参工具对于室外视觉方案来说调参效率决定了开发效率。如果每次改阈值都要重新烧录固件一天也调不了几次。我的做法是写了一个简单的 Python 上位机通过串口实时接收小车发来的图像和控制参数键盘调整参数后实时下发。这样极大提高了调参效率。这里给一个简化版本# 文件路径host_tool/parameter_tuning.py # 通过串口实时调整二值化阈值 # 用法python parameter_tuning.py --port COM3 import serial import cv2 import numpy as np def read_frame(ser): head ser.read(4) if len(head) 4: return None # 自定义协议AA 55 图像长度 if head[0] ! 0xAA or head[1] ! 0x55: return None length head[2] * 256 head[3] data ser.read(length) if len(data) length: return None img np.frombuffer(data, dtypenp.uint8).reshape(120, 188) return img def main(): ser serial.Serial(COM3, 115200, timeout1) thr 128 # 阈值 while True: img read_frame(ser) if img is not None: cv2.imshow(raw, img) _, binary cv2.threshold(img, thr, 255, cv2.THRESH_BINARY) cv2.imshow(binary, binary) key cv2.waitKey(1) 0xFF if key ord(a): thr max(0, thr - 5) ser.write(bytes([thr])) print(loop threshold:, thr) elif key ord(d): thr min(255, thr 5) ser.write(bytes([thr])) print(loop threshold:, thr) elif key ord(q): break ser.close() cv2.destroyAllWindows() if __name__ __main__: main()对于集成到 full 工程里的参数同步建议在串口协议中定义一个“参数帧”可以传输多个变量。这样到了赛场即使光线变化也不需要打开电脑重新编译用串口工具就能现场调整参数。7. 常见问题与排查思路7.1 问题排查表格下面整理我在整个开发过程中遇到过的典型问题以及对应的排查思路。问题现象常见原因解决思路摄像头图像全黑摄像头初始化失败或曝光时间太短检查 I2C 配置、摄像头时钟和曝光寄存器图像过曝赛道边界丢失室外光线太强降低曝光时间启用自适应阈值图像闪烁边界时有时无供电不稳或摄像头帧率不足独立供电提高帧率检查线缆接触车辆起步时抖动剧烈平衡环参数过强PWM饱和降低角度环 P增加角速度环阻尼高速时转向过大侧翻转向比例系数太大引入速度-转向耦合速度越高转向权重越小上坡时视觉丢失摄像头仰角过大坡道超出视野在坡道准备阶段记录最后一帧有效偏差结合 IMU 数据盲走舵机响应慢抬腿滞后舵机供电不足或控制周期太长单独供电缩短舵机控制周期程序偶尔跑飞数组越界或内存溢出检查边界数组下标开启栈保护7.2 室外视觉的排错顺序如果你在室外跑车时发现视觉不稳定我建议按照下面的顺序排查先看原始图像。打开上位机图像显示观察图是否清楚是否有严重过曝或欠曝。再看二值化结果。如果二值化后赛道连不成一片优先调整曝光时间和自适应阈值窗口大小。再看边界提取结果。在图像上画出左右边界点确认边界是否连续是否出现异常跳变。最后看偏差输出。用手推动小车左右移动观察偏差是否平滑变化。如果以上每一步都正常但小车跑起来仍然不好那问题大概率在控制环节而不是视觉环节。7.3 关于灰度与阈值的经验很多队伍习惯用“智能车灰度”这个概念也就是把彩色图像转换成灰度图后再处理。灰度图确实能减少运算量但室外环境下纯灰度容易丢失颜色信息。如果你的赛道元素包含红色路肩、蓝色锥桶等建议保留颜色通道信息作为辅助特征。我在方案中是双路并行灰度图用于边界提取彩色特征图用于障碍和路肩识别。这样既可以保证处理速度又不至于被室外光照“一票否决”。8. 最佳实践与工程建议8.1 参数要能“现场改”竞赛现场的光线和你平时实验室的光线一定不同。这就意味着你准备的很多参数到现场都要重新调整。所以我的建议是所有阈值参数、PID 参数都做成全局变量。提供串口调参通道。上位机能一键保存参数到 Flash。小车启动时可以从 Flash 加载上次参数。不要把所有参数都硬编码在代码里否则到了现场只能反复烧录非常浪费时间。8.2 善用日志记录小车在赛道上跑的时候我们无法实时看到它内部的所有状态。这时候日志就是你的眼睛。我建议在小车上记录以下信息每一帧的横向偏差。每一帧的赛道元素识别结果。状态机切换时刻。电机 PWM 输出值。车身倾角。车辆速度。跑完一次之后把 SD 卡里的日志用 Python 脚本绘图就能很直观地看到哪个时刻发生了问题。下面是一段简单的日志分析代码# 文件路径host_tool/plot_log.py # 绘制车辆运行日志曲线 import csv import matplotlib.pyplot as plt times [] errors [] speeds [] angles [] with open(log.csv, r) as f: reader csv.DictReader(f) for row in reader: times.append(float(row[time])) errors.append(float(row[lateral_error])) speeds.append(float(row[speed])) angles.append(float(row[angle])) fig, axes plt.subplots(3, 1, figsize(10, 8), sharexTrue) axes[0].plot(times, errors, labellateral_error) axes[0].legend() axes[1].plot(times, speeds, labelspeed) axes[1].legend() axes[2].plot(times, angles, labelangle) axes[2].legend() plt.xlabel(time (s)) plt.savefig(log_plot.png)看到偏差曲线在某段时间突然变成 0就说明视觉丢失再看角度曲线就能判断车辆当时是否已经冲到坡道上。这种排查效率比肉眼盯车高很多。8.3 安全性优先级竞赛再重要也不如设备和人员安全重要。调试过程中一定要给小车设置物理限位电池电压低于阈值时自动切断舵机动力。紧急停车按钮要保留到最后一刻。调试时使用支架把车架空避免脱手撞人。运行前检查轮胎螺丝、舵机臂是否紧固。另外所有涉及 MOSFET、电机驱动、电池管理的操作都要在断电状态下进行确认接线正确后再上电。8.4 代码版本管理智能车项目周期长代码改动频繁强烈建议用 Git 管理代码。每次调参、改结构、改算法之前先提交一个版本。很多队伍在比赛前一周频繁改代码改到后面连“哪个版本是能跑的”都忘记了。这是非常痛的经历。使用 Git 之后你可以随时回退到之前的稳定版本还能用分支测试新方案不污染主代码。8.5 开源的意义我把整个方案开源不只是为了分享代码本身更希望能提供一个“可以讨论的起点”。智能车竞赛的生态一直很依赖“传帮带”不同学校之间的交流其实非常重要。如果你拿到开源代码后不知道怎么用我建议先不要急着改算法而是做这几件事根据仓库的机械结构说明把车模搭建起来保证硬件可用。跑通出厂固件确认电机、舵机、摄像头、IMU 都能正常通信。打开上位机实时查看图像感受室外光照对图像的影响。先用遥控器或手动模式验证轮腿运动再切换自动循迹模式。最后再按照自己的习惯替换掉里面的控制参数。这样的路径最平稳也不容易因为一上来就出问题而打击信心。9. 总结与后续学习建议回头来看这套“轮腿 室外视觉”的方案能拿国一本质上不是我实现了多前沿的算法而是把一个复杂的系统认真分成了几个可测试、可调参、可定位问题的模块视觉模块解决“车看到了什么”。状态机模块解决“车接下来要做什么”。平衡控制模块解决“车该怎么站得住”。速度与转向模块解决“车该怎么走”。每一个模块单独拿出来都不算难让我花费最多时间的是它们之间的配合以及室外环境下各种极端情况的处理。如果你接下来想继续深入可以从以下几个方向入手把平衡控制器从 PID 换成 LQR 或 MPC对比在不同路面上的表现。在视觉模块中加入深度相机或激光测距感知坡度和障碍物距离。尝试用轻量级神经网络在 MCU 上做赛道元素分类。把当前的串级控制写成 Simulink 仿真模型离线调好参数再上实车。也希望你的智能车项目不要只停留在“能跑”的阶段。尝试去理解每一个参数变化带来的影响尝试自己设计一个小实验来验证某个猜想——这些能力在竞赛结束后依然会伴随你很久。如果本文对你有帮助建议收藏备用也欢迎讨论你的轮腿方案和室外视觉处理思路。祝调试顺利赛场见。
返回列表