三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

基于树莓派与视觉算法的智能小车:从硬件选型到PID控制的完整实现

基于树莓派与视觉算法的智能小车:从硬件选型到PID控制的完整实现

1. 项目背景与核心挑战

去年参加光电赛,我们团队抽到的是小车相关的题目,要求实现一个具备自主导航与目标识别能力的移动平台。题目本身没有限定具体的传感器方案,但“光电”二字其实已经暗示了视觉是核心得分点。当时市面上开源的小车方案很多,但要么是纯循迹的简单逻辑,要么是依赖激光雷达的“重装”方案,成本高且与“光电”主题的贴合度不够。我们决定走一条更贴合赛事精神的路:以低成本树莓派为主控,搭配普通USB摄像头,完全依靠视觉算法来实现环境感知、路径规划和动态避障。

这个决定背后有几个现实的考量。首先,比赛预算有限,一套像样的激光雷达加上配套的工控机,成本直接起飞,而树莓派加摄像头的组合几百块就能搞定。其次,视觉方案的可扩展性和“炫技”空间更大,比如我们后来做的动态目标跟踪和二维码导航,都是激光方案很难低成本实现的“亮点”。但挑战也是显而易见的:视觉处理对算力要求高,树莓派的CPU在跑操作系统、图像处理、控制逻辑时很容易捉襟见肘;环境光照变化、摄像头畸变、动态障碍物干扰,每一个都是坑。

我们最终完成的代码,不仅让小车在比赛场地里稳定跑完全程,还实现了几个超出题目要求的附加功能。比赛结束后,我觉得这套从零搭建的视觉处理框架、以及过程中趟过的无数个坑,对后来者应该很有价值,所以决定把核心代码和方案思路彻底开源。这不是一个简单的“调库”Demo,而是一个包含了传感器驱动、图像预处理、特征提取、决策控制、电机PID调速的完整工程,你可以直接用它作为基础,去开发你自己的AGV、巡检机器人或者更复杂的移动视觉平台。

2. 硬件选型与系统架构设计

硬件是地基,选错了后面代码写得再漂亮也白搭。我们的核心思路是:在有限的成本和算力下,实现功能、稳定性和开发效率的平衡。

2.1 主控与感知单元:为什么是树莓派4B + OV5647?

主控我们选择了树莓派4B 4GB版本。当时树莓派5还没发布,4B是性价比和性能的甜点。2GB内存跑带图形界面的ROS都有些吃力,4GB给了我们充足的缓冲空间。有人可能会问,为什么不用性能更强的Jetson Nano?原因很简单:功耗和生态。小车由电池供电,Jetson Nano的功耗和散热是个大问题。而树莓派庞大的社区意味着任何奇怪的问题,几乎都能找到解决方案,这对于争分夺秒的比赛和后续开发至关重要。

摄像头模块我们选择了树莓派官方的OV5647摄像头。这是一个非常关键的选择。很多人会直接用USB摄像头,因为即插即用方便。但我们放弃了USB方案,选择了通过CSI接口直接连接的OV5647。主要原因有三点:

  1. 带宽与延迟:CSI是专为摄像头设计的高速接口,数据传输的带宽和稳定性远高于USB。对于需要实时处理图像的视觉小车,降低图像采集的延迟至关重要,几十毫秒的延迟在高速移动和避障时可能就是撞上与错开的区别。
  2. 系统资源占用:使用raspistillpicamera库直接操作CSI摄像头,图像数据直接进入内存,CPU占用率低。而USB摄像头需要走USB总线,并由uvcvideo等内核模块处理,会额外消耗CPU资源。
  3. 固定与供电:OV5647模块小巧,通过排线连接,更容易在小车车体上固定,且供电来自树莓派本身,无需额外考虑USB供电问题。

当然,OV5647的缺点是其默认的广角镜头畸变较大。我们通过事先标定获取了相机内参和畸变系数,在代码中进行了实时校正,这个问题就解决了。

2.2 车体、电机与驱动:打造稳定的运动平台

车体我们选用了一款常见的两轮差速底盘。差速驱动结构简单,控制模型成熟,非常适合做路径规划和算法验证。电机是关键,我们选择了带编码器的直流减速电机。编码器是必须的,它是实现闭环速度控制、里程计推算的基础。没有编码器,你只能开环控制电机“大概转多快”,小车走直线都会飘。

电机驱动板选用的是基于TB6612FNG芯片的驱动模块。它比古老的L298N效率高、发热小,支持PWM调速和正反转控制,接口简单。我们将驱动板直接通过GPIO与树莓派连接。

这里有一个重要的经验:务必给树莓派和电机驱动使用独立的电源供电,并通过共地连接。电机在启停时会产生巨大的电流波动和反向电动势,如果和树莓派共用电源,极易导致树莓派重启或IO口损坏。我们采用了两块锂电池组分别供电,这是保证系统稳定的基石。

2.3 软件架构:轻量级ROS与自定义节点的融合

软件上,我们没有使用完整的ROS Desktop环境,而是安装了ROS Noetic的ros-base版本。因为小车不需要图形化界面,ros-base包含了核心的通信机制(节点、话题、服务),足够我们使用。这节省了大量的存储空间和启动时的内存占用。

我们的软件架构分为几个独立的节点,通过ROS话题进行通信:

  • camera_node: 负责驱动OV5647摄像头,发布校正后的图像话题 (/camera/image_raw) 和相机信息话题 (/camera/camera_info)。
  • vision_processing_node: 核心视觉处理节点。订阅图像话题,进行目标识别、车道线检测、障碍物分割等,并发布处理结果(如目标位置、可行区域边界)到决策话题。
  • decision_control_node: 决策与控制节点。订阅视觉处理结果和编码器数据(里程计),进行路径规划,并计算出左右轮的目标速度,发布到控制话题。
  • motor_driver_node: 电机驱动节点。订阅控制话题的目标速度,通过PID控制器结合编码器反馈,生成PWM信号驱动电机,实现精准调速。

这种模块化设计的好处是调试方便。我们可以单独测试视觉算法,用录制好的数据包(rosbag)回放;也可以单独测试电机控制,手动发布速度指令。整个系统的耦合度很低。

3. 核心视觉处理流程拆解

视觉处理是整个系统的大脑,我们将其 pipeline 分解为几个串行的步骤,每一步都做了针对性的优化。

3.1 图像预处理:不仅仅是“滤波”

从摄像头出来的原始图像不能直接用。我们的预处理流程包括:

  1. 畸变校正:使用事先标定好的相机内参矩阵和畸变系数,调用OpenCV的cv2.undistort()函数,消除镜头畸变。这是后续所有几何计算(如测距)准确的前提。
  2. ROI区域裁剪:小车的摄像头通常向前下方安装,图像上半部分很多是无关的天空或天花板。我们根据摄像头安装的俯仰角,动态计算一个感兴趣区域(ROI),只对这部分图像进行处理,减少了近30%的计算量。
  3. 颜色空间转换与阈值分割:这是识别特定目标(比如赛道的绿色引导线、红色的停止标志)的关键。我们并没有简单地在RGB空间做阈值分割,因为RGB对光照太敏感。对于车道线,我们转换到HSV颜色空间,针对黄色的色调(H)和饱和度(S)设定阈值,对亮度的变化相对鲁棒。
    # 示例:提取黄色车道线 hsv = cv2.cvtColor(roi_image, cv2.COLOR_BGR2HSV) lower_yellow = np.array([20, 100, 100]) # 注意:OpenCV中H范围是0-180 upper_yellow = np.array([30, 255, 255]) mask_yellow = cv2.inRange(hsv, lower_yellow, upper_yellow)
  4. 降噪与形态学操作:阈值分割后的二值图像通常有很多噪声点。我们先后使用了中值滤波去除椒盐噪声,以及开运算(先腐蚀后膨胀)来断开细小的连接、消除小的噪点,同时闭运算(先膨胀后腐蚀)来填充目标内部的小孔洞。这个顺序和核的大小需要根据实际场景微调。

3.2 特征提取:从像素到决策信息

预处理后,我们得到了干净的二值图像。接下来需要从中提取出可供决策的量化特征。

  • 对于车道线:我们采用“滑动窗口”法。将图像在垂直方向上分成若干水平条带,在每个条带中,对二值图像的非零像素点进行直方图统计,找到像素分布的中心点,作为该条带中车道线的位置。将这些中心点连接起来,就得到了车道线的拟合曲线。同时,我们计算曲线的曲率,用于判断前方是直道还是弯道。
  • 对于障碍物:我们使用了背景减除与轮廓检测的方法。对于静态或缓动障碍物,通过比较连续帧间图像的变化(或使用更高级的MOG2背景减除器),得到运动前景掩膜。然后使用cv2.findContours()查找轮廓,并计算每个轮廓的外接矩形。矩形的底边中心点的x坐标决定了障碍物相对于小车中心的横向偏移,矩形的高度和摄像头标定参数可以结合估算出障碍物的距离。
  • 对于特定目标(如二维码、AprilTag):我们集成了apriltag库。这类视觉标签提供了精准的6自由度位姿估计。我们通过检测标签的ID,可以直接知道小车相对于某个已知标签的位置和朝向,这对于实现精确定位和导航任务(如走到某个指定货架前)非常有用。

3.3 视觉里程计辅助:弥补编码器的不足

纯依靠轮式编码器推算里程计(Odometry)在打滑、空转时会有累积误差。我们实现了一个简单的视觉里程计(VO)模块作为补充。它并不进行复杂的特征点匹配与优化(那太耗资源),而是采用LK光流法跟踪图像中的角点特征,估算出相邻帧间小车的旋转和平移运动。虽然精度不如专业的V-SLAM,但将其结果与编码器数据进行简单的卡尔曼滤波融合,能显著提高在光滑地面急转弯时的位姿估计精度,成本几乎为零。

4. 决策规划与控制实现

有了视觉提供的“环境感知”,小车的大脑需要决定“怎么走”。

4.1 基于视觉的局部路径规划

我们的路径规划器非常简单高效,因为它紧密依赖视觉输入。决策节点订阅视觉处理节点发布的几个关键信息:

  • lane_center: 当前帧中,车道线中心的x坐标(图像坐标系)。
  • obstacles_list: 一个列表,包含每个检测到的障碍物的距离和横向位置。
  • free_space_boundary: 通过视觉可通行区域分析得到的左右边界。

规划器的工作流如下:

  1. 首选跟踪车道线:如果检测到清晰的车道线,则将lane_center作为目标点,计算小车当前中心与目标点的横向偏差(cross_track_error)。
  2. 动态避障逻辑:如果检测到前方有障碍物,则进入避障模式。规划器会根据障碍物的位置和大小,在free_space_boundary限定的范围内,重新计算一个临时目标点。例如,障碍物偏左,我们就生成一个略微偏右的路径点,让小车绕行。这里我们采用了“虚拟势场”的简化思想:障碍物产生斥力,车道中心产生引力,合力方向就是小车的期望行进方向。
  3. 恢复策略:绕过障碍物后,规划器会持续检查是否重新检测到车道线,并平滑地切换回车道跟踪模式,避免剧烈转向。

4.2 双闭环PID速度控制

决策规划器输出的是期望的航向(或横向位置),最终需要转化为左右轮的速度指令。我们采用了两层PID控制。

内环:轮速PID闭环这是最底层、最关键的环。motor_driver_node订阅到目标速度(target_speed_left,target_speed_right)后,将其与编码器实时反馈的实际速度进行比较,通过PID控制器计算PWM占空比的调整量。

# 伪代码示例 class WheelPIDController: def __init__(self, kp, ki, kd): self.kp, self.ki, self.kd = kp, ki, kd self.integral = 0 self.prev_error = 0 def compute(self, target_speed, current_speed, dt): error = target_speed - current_speed self.integral += error * dt derivative = (error - self.prev_error) / dt output = self.kp * error + self.ki * self.integral + self.kd * derivative self.prev_error = error # 限制输出范围,对应PWM占空比 return max(min(output, MAX_PWM), -MAX_PWM)

这个环保证了即使负载变化、电池电压下降,每个轮子也能精确地以指定速度转动,这是小车走直线、精准转弯的基础。参数kp, ki, kd需要在小车空载和负载情况下分别调试,找到一个平衡点。

外环:航向PID闭环决策节点计算出横向偏差cross_track_error后,将其输入另一个PID控制器,其输出是左右轮的速度差(speed_diff)。例如,当小车偏右时(误差为正),控制器会输出一个正的速度差,让左轮稍快于右轮,从而向左修正航向。 最终,左右轮的目标速度由基础速度和速度差共同决定:target_speed_left = base_speed + speed_diff / 2target_speed_right = base_speed - speed_diff / 2

这种双环结构,内环抗干扰保证执行精度,外环纠偏保证方向正确,是小车稳定运行的核心。

5. 工程实现中的关键坑点与优化

把算法跑通只是第一步,让它在树莓派上稳定、实时地运行才是真正的挑战。

5.1 树莓派上的性能优化实战

树莓派4B的CPU是ARM架构,单核性能有限。我们的视觉处理节点最初用纯Python+OpenCV写,处理一帧640x480的图像要接近200ms,根本无法实时。

优化1:算法层面精简

  • 坚决使用ROI,减少处理像素。
  • 将彩色图像转换和阈值化等操作,从使用cv2.cvtColorcv2.inRange改为直接使用NumPy的向量化操作,速度提升明显。
  • 对于滑动窗口找车道线,不是每一帧都从头开始搜索。如果上一帧找到了可靠的车道线,下一帧就在其附近一个小的邻域内搜索,这叫“搜索窗引导”,大幅减少了计算量。

优化2:利用硬件加速这是最关键的一步。树莓派的GPU(VideoCore VI)虽然不能用于通用计算,但可以通过picamera库的array输出,并设置use_video_port=True,让图像数据直接通过GPU的硬件编码通道传递到内存,效率远高于CPU处理。此外,OpenCV的一些操作(如resize,cvtColor)在编译时如果开启了NEON(ARM的SIMD指令集)支持,也会自动加速。我们使用的官方Raspbian系统自带的OpenCV通常已包含这些优化。

优化3:多进程与消息队列Python的GIL限制了多线程对CPU密集型任务的加速。我们将最耗时的视觉处理部分(如AprilTag检测、光流计算)封装成独立的函数,使用Python的multiprocessing模块放到另一个进程中运行。主进程通过队列(multiprocessing.Queue)传递图像数据并获取结果。这样可以利用树莓派的四核CPU,将帧率从5FPS提升到了15FPS以上,满足了实时性要求。

5.2 传感器同步与数据融合的陷阱

视觉和编码器是不同频率的数据源。摄像头可能是30FPS,编码器读取可能是100Hz。直接使用最新数据会导致控制抖动。

我们的解决方案是时间戳对齐与插值。所有消息都带有ROS的header.stamp时间戳。在决策节点中,我们维护一个短暂的里程计数据缓存。当收到一帧视觉处理结果时,我们根据这帧图像的时间戳,在里程计缓存中找到时间最接近的里程计数据,或者对前后两个里程计数据进行线性插值,得到一个“同步”后的位姿估计,用于当前的路径规划。这大大提高了多传感器数据的一致性。

5.3 电源管理与突发负载应对

在调试过程中,我们多次遇到小车在电机启动或急转弯时树莓派重启的情况。排查后发现,即使使用了独立电源,电机驱动板的大电流瞬间仍会通过地线引入噪声,影响树莓派的电源稳定性。

最终解决方案

  1. 电源隔离:在电机驱动板的电源输入端并联一个大容量(如1000uF)的电解电容,用于吸收电机启停时的瞬时大电流冲击。
  2. 信号隔离:在树莓派GPIO与电机驱动板PWM/方向信号线之间,加入了光耦隔离模块。这样,电机侧的电气噪声就被完全隔绝了。
  3. 树莓派稳压:给树莓派供电的锂电池输出端,接了一个高品质的DC-DC降压稳压模块,确保即使电池电压波动,供给树莓派的也是稳定的5V。

经过这些硬件层面的加固后,系统再也没有出现过因电源问题导致的异常。

6. 代码结构说明与快速上手

开源代码仓库包含了完整的项目,结构清晰。这里简要说明核心部分。

vision_robot/ ├── launch/ # ROS启动文件 │ └── robot_bringup.launch # 一键启动所有节点 ├── src/ │ ├── camera_driver/ # 摄像头驱动节点 │ ├── vision_processing/ # 视觉处理节点(核心算法) │ ├── decision_control/ # 决策与控制节点 │ └── motor_driver/ # 电机驱动与PID控制节点 ├── config/ # 参数配置文件(PID参数、颜色阈值等) ├── scripts/ # 实用脚本(相机标定、测试脚本) └── README.md # 详细的编译与运行指南

快速上手步骤:

  1. 硬件组装:按照文档连接树莓派、摄像头、电机驱动板、编码器和电池。确保电源隔离措施到位。
  2. 系统烧录与配置:在树莓派上安装Ubuntu Server 20.04或Raspbian,然后安装ROS Noetic。克隆我们的代码仓库到~/catkin_ws/src目录下。
  3. 依赖安装:运行install_dependencies.sh脚本,它会安装OpenCV、AprilTag库等必要的软件包。
  4. 参数校准:这是最重要的一步。
    • 相机标定:使用scripts/calibrate_camera.py脚本,打印一张棋盘格,拍摄多角度照片完成标定,生成的内参文件保存到config/
    • 颜色阈值调整:运行scripts/color_threshold_adjuster.py,这是一个图形化工具,可以实时调整HSV阈值,直到能稳定识别出你的赛道颜色或目标颜色,将参数保存。
    • PID参数整定:这是调车的过程。先调内环轮速PID:让小车悬空,分别给左右轮一个固定速度指令,观察实际速度是否能快速、无静差地跟上。再调外环航向PID:让小车在地上跑,观察其跟踪车道线的能力,适当调整参数。
  5. 启动运行:所有参数配置好后,一个命令启动所有功能:roslaunch vision_robot robot_bringup.launch。小车就会开始基于视觉进行自主运行了。

这个项目从硬件选型到软件调试,完整地走通了一个智能视觉小车的开发流程。它可能不是性能最强的,但一定是细节最丰富、坑点提示最全的参考之一。希望这份代码和详细的方案说明,能帮你更快地启动自己的移动机器人项目,把时间花在更有创造性的算法改进上,而不是在基础框架和硬件调试上反复踩坑。

← 返回列表