尧图精选

基于单目视觉和深度学习的ROS跟随小车实战:YOLO目标检测与PID控制

🕒 发布时间:2026/9/28 4:18:56 📁 来源:尧图网络
前阵子帮朋友调了一辆“人走车走、人停车停”的ROS智能小车整个项目最核心的一条技术路线就是基于单目视觉和深度学习的目标识别配合ROS的消息通信完成自适应跟随。做下来最大的感受是单目方案虽然便宜但要把检测、测距、控制三件事串成一条靠谱的闭环里面藏着不少容易被忽略的细节。这篇文章就把我实际踩过的坑、调过的参数、改过的代码完整梳理一遍适合手里已经有ROS小车底盘、想给车加上跟随功能的同学参考也适合刚入门机器人方向、想把深度学习和ROS结合起来做课程设计或比赛项目的朋友。1. 方案选型为什么把单目视觉和深度学习搭在一起1.1 单目视觉凭什么够用精度和成本的博弈先说清一个很多人纠结的问题为什么不用双目摄像头或者激光雷达在室外无人机、自动驾驶这种场景里单目确实不够用因为单个相机本质上丢失了深度信息。但在室内跟随小车这个场景里目标是一个已知大致尺寸的人而不是任意障碍物这时候单目就够用了。核心原理是针孔相机模型的相似三角形测距distance (real_height × focal_length) / bbox_pixel_heightreal_height 是目标实际高度成年人按1.7m左右估计focal_length 是相机内参里的焦距bbox_pixel_height 是目标检测框的高度。只要检测框能稳定框住人距离误差控制在±20cm以内完全可行。这个精度对跟随控制来说已经相当理想——你又不是要贴着人走保持一米左右的间距这点误差完全不影响。至于为什么不直接用双目一是双目测距依赖两个相机的基线标定标定不好误差反而更大二是室内光线变化时双目匹配鲁棒性很难保证三是成本问题。单目摄像头几十块钱配上好点的算力板子也就几百整体性价比高很多。激光雷达呢差速底盘配单线雷达确实能测距但雷达给的是点云不是“人”。你没法直接知道前面这个目标是不是人是正在走的人还是静止的衣架得额外加目标检测算法那又绕回深度学习了。所以单目视觉 深度学习这条路线本质上是用最便宜的传感器配合算力去换智能性方向是对的。1.2 深度学习模型选型YOLO系还是传统视觉目标检测方案一开始我也纠结过。先试过OpenCV自带的人体检测HOG SVM效果只能说勉强能用人稍微侧身、穿宽松衣服、光线暗一点检测框就乱跳基本没法用于实时控制。后来上了YOLO系列检测稳定性一下就上来了。轻量级选择建议YOLOv5n或者YOLOv8n模型参数量小算力要求低。我在Jetson Nano上跑YOLOv8nTensorRT优化后能达到40FPS以上对一秒钟只输出10次控制指令的跟随系统来说绰绰有余。选型对比可以看这张表方案优点缺点适合场景HOG SVM轻量CPU可跑对姿态敏感鲁棒性差固定机位行人检测帧差法/背景建模无需标注目标静止即丢失抗干扰差固定背景监控YOLOv5n/v8n精度高能跑嵌入式需要GPU或NPU加速移动端实时检测MobileNet-SSD比YOLO更轻精度略低资源极受限时如果是用树莓派4B这种纯CPU平台YOLOv8n跑起来会比较吃力大概只有5~10FPS。这时候要么换成更轻的NCNN版本要么干脆选Jetson Nano这种带GPU的板子。我自己实测下来算力平台的选择往往比模型选择更关键卡在5FPS的时候整个跟随控制就是“打嗝式”的目标行人都走远了控制指令还没跟上。1.3 ROS版本和通信架构怎么定先画好话题图ROS版本的选择跟Ubuntu系统绑定。用Ubuntu 20.04就配ROS Noetic用Ubuntu 22.04就配ROS 2 Humble。新手我强烈建议用Noetic因为教程最多、踩坑记录最全。ROS 2虽然设计更先进但很多底层驱动包和配套代码还不完善做小车项目没必要给自己增加额外负担。如果是纯新手不会装ROS可以用鱼香ROS的一键安装脚本这个工具在安装ROS、换源、装依赖这几个环节能省下大量时间。我第一次装Noetic手动折腾了半天用脚本十分钟搞定。整个系统的通信架构就是经典的ROS话题发布订阅模型我一开始就画好了话题图后面写代码脑子很清楚camera_node └─发布 /camera/image_raw (sensor_msgs/Image) yolo_detect_node ├─订阅 /camera/image_raw └─发布 /detection/bbox (自定义消息目标类别框xywh置信度) follow_controller_node ├─订阅 /detection/bbox └─发布 /cmd_vel (geometry_msgs/Twist) serial_driver_node ├─订阅 /cmd_vel └─串口发送 #PWM速度# 指令给STM32/Arduino下位机这样设计的好处是每个节点独立运行、独立崩溃、独立重启。摄像头坏了不影响检测节点检测节点挂了控制器还能保持上次速度。实际调试时我只用rostopic echo /detection/bbox就能确定问题出在检测还是控制不用全链路翻代码调试效率高很多。2. 系统拆解从摄像头到电机的一条完整链路2.1 检测节点图像在哪变成“目标位置”检测节点是整个系统的眼睛它的任务是把图像转换成“目标在图像中的位置信息”。我用的是OpenCV读取图像 YOLO模型推理的方式代码结构大概是这样的#!/usr/bin/env python3 import cv2 import rospy import numpy as np from sensor_msgs.msg import Image from cv_bridge import CvBridge from follow_bot.msg import Detection class YoloDetector: def __init__(self): self.bridge CvBridge() self.sub rospy.Subscriber(/camera/image_raw, Image, self.image_cb) self.pub rospy.Publisher(/detection/bbox, Detection, queue_size1) self.net cv2.dnn.readNetFromONNX(yolov8n.onnx) self.net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA) self.net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA) def image_cb(self, msg): frame self.bridge.imgmsg_to_cv2(msg, bgr8) # 推理后找person类取最大框发布检测结果 bbox self.detect_person(frame) if bbox is not None: det Detection() det.x, det.y, det.w, det.h bbox det.confidence conf self.pub.publish(det)这里有个很关键的工程细节图像分辨率不是越大越好。我一开始用1280×720检测节点的FPS直接掉到十几帧控制卡顿。后来改成640×480FPS回到30以上跟随效果反而更好。因为对跟随任务来说检测框的稳定性比分辨率重要——分辨率低一点检测框的像素抖动反而小控制指令更平滑。另一个细节是推理后端选型。在Jetson Nano上ONNX Runtime自带的CUDA执行器有时不稳定我用DNN_BACKEND_CUDA配合TensorRT导出的engine文件速度提升明显。如果懒得转换模型格式直接使用OpenCV的DNN模块加载ONNX也可以但能效比差距在20%左右。对于四百块钱的Jetson Nano这种老平台而言每一分算力都要省着用。2.2 跟随策略距离与角度的双环控制检测框拿到之后怎么变成小车的速度和转向这里我拆成两个独立的控制环角度环和距离环。角度环的目标是让目标始终处于画面中心。定义图像中心横坐标为cx320640宽目标框中心横坐标为obj_x偏差就是err_angle obj_x - cx。这个偏差映射到角速度上angular_z -angle_p * err_angle / cxangle_p是比例系数。比如目标偏右obj_x cx偏差为正车需要右转所以角速度为负。这里要特别强调角速度映射时最好除以画面宽度做归一化这样无论图像分辨率怎么改参数都不用重新调。距离环的目标是让小车和目标保持一个合适的距离。根据单目测距公式先把检测框高度转换成目标距离dist然后与期望距离比较err_dist target_dist - dist # target_dist 设成 0.9m linear_x dist_p * err_dist # 限幅 linear_x np.clip(linear_x, -0.2, 0.5) linear_x 0.0 if abs(err_dist) 0.1 else linear_x这里做了死区处理。abs(err_dist) 0.1时速度归零避免车辆在目标距离附近来回窜动。死区大小可以根据车的底盘特性调整我用的这个底盘死区0.1m比较合适太小则车身会轻微抖太大会觉得反应迟钝。为什么不是直接按比例一直加速因为跟随系统的目标是“稳定保持距离”而不是“追上目标走”。如果人站在那不动车应该停下来所以距离误差小时速度必须为0这就是死区存在的意义。比例系数也不能太大不然人会感觉到车在后面冲刺式地逼近又急刹车。2.3 底层驱动与速度映射怎么让下位机“听指挥”上位机算出来的是/cmd_vel话题上的线速度和角速度geometry_msgs/Twist但STM32/Arduino这类下位机不认ROS话题它们只认串口指令所以中间需要一层串口桥接。我定义了一个简单的串口协议格式如下#PWM L150 R150#\nL是左轮PWM值R是右轮PWM值范围0~255。差速底盘的左右轮速度由线速度和角速度组合得到float v msg-linear.x; float w msg-angular.z; float wheel_base 0.16f; // 轮距单位m float v_left v - w * wheel_base / 2.0f; float v_right v w * wheel_base / 2.0f; // 速度(m/s)映射到PWM int pwm_left map_speed_to_pwm(v_left); int pwm_right map_speed_to_pwm(v_right);串口侧我用的是pyserial库300ms超时重发防止串口数据丢失。串口波特率设115200数据量很小稳定无压力。这里有个前提没交代清楚你手上的底盘电机有没有编码器没有编码器的普通直流电机只能靠PWM开环控制响应有延迟但跟随这种低速场景勉强够用。有编码器的底盘可以上PID速度闭环让左右轮的PWM映射更准确跟随过程走直线更稳。实测开环底盘在速度变化时车身会跑偏需要加一个转向修正量来补偿这是我的实际教训。3. 实操过程从零搭出一台能跟着人走的车3.1 硬件选型与供电配置硬件选型是最容易踩坑的地方。我用的这套配置性价比很高可以参考部件型号/规格说明算力主控Jetson Nano 4GB跑YOLOv8n约8W功耗摄像头免驱USB摄像头 90°广角640×480 30FPS下位机STM32F103ZET6接收串口输出PWM电机驱动TB6612比L298N效率高底盘4轮差速底盘带编码器最好电池3S锂电 降压模块主控和电机分电源供电供电是一个大坑。Jetson Nano启动峰值电流要5V/4A舵机和电机动作时电流波动又很大如果共用一个电源电压跌落会导致主控重启或者摄像头闪断。我的建议是动力电源和控制电源完全分开电池给电机驱动板供电一路经过降压模块给主控供电GND接在一起做共地这样既能防止压降干扰又不会烧掉主控板。还有一点摄像头尽量用USB免驱的不要用那种需要厂家私有驱动的工业相机否则在ROS下驱动配置会让你怀疑人生。视角选90°左右不要太窄太窄了人在侧面稍微偏一点就出画面跟随过程中频繁丢失目标。3.2 软件环境搭建和模型部署软件环境分四步走。第一步装ROS第二步装深度学习推理环境第三步写节点第四步联调。ROS环境安装我前面提过鱼香ROS一键脚本是新人福音。装完记得初始化工作空间mkdir -p ~/follow_ws/src cd ~/follow_ws/src catkin_init_workspace cd ~/follow_ws catkin_make echo source ~/follow_ws/devel/setup.bash ~/.bashrc深度学习环境部分Jetson Nano需要先刷JetPack系统自带CUDA和TensorRT不需要自己装NVIDIA驱动会省掉很多麻烦。然后装onnxruntime或者直接用OpenCV的DNN模块后者依赖更少我建议先用它能跑通流程再优化速度。模型转换这块要单独说。YOLOv8官方导出ONNX的方式是yolo export modelyolov8n.pt formatonnx opset12然后在Python里用cv2.dnn.readNetFromONNX加载。ONNX里的输出层往往带有多层输出YOLOv8的输出预处理和YOLOv5的老版本不同需要自己写解码函数。新手建议直接用ultralytics提供的Python推理接口虽然速度慢一点但至少能先跑通。跑通后再考虑用OpenCV DNN或者TensorRT优化性能。我把自己写的检测节点脚本放在工作空间的scripts目录下记得给权限chmod x ~/follow_ws/src/follow_bot/scripts/*.py然后配置CMakeLists.txt加catkin_install_python否则rosrun找不到节点。3.3 联调步骤与PID参数整定联调我强烈建议按“分而治之”的顺序做不要在一步都没验证的情况下直接上路跑。第一步只启动摄像头节点用rqt_image_view看图像流是否正常。这一步能排查摄像头驱动问题。第二步启动检测节点用rostopic echo /detection/bbox看检测框是否稳定输出。正常情况下行人在画面走动时检测框横坐标应当平稳变化高度会随着距离远近明显变化。如果检测框乱跳优先怀疑的是阈值太低把置信度阈值提高到0.5试一下。第三步手动发布/cmd_vel话题看底盘是否响应。用rostopic pub -r 10 /cmd_vel geometry_msgs/Twist {linear: {x: 0.2}, angular: {z: 0.0}}发速度和转向底盘应该转起来。如果底盘不动问题多半在串口协议不匹配用串口调试助手先裸发字符串测一下。第四步把控制器节点跑起来但把底盘架空观察速度和转向指令的变化是否符合预期。这一步能发现方向反了、正负号反了、死区过大这些问题而且不用满屋子追人调试人站着就能测完。PID参数整定是很多人觉得最玄学的地方。我的经验是先只调PI和D都设0从小到大试。转向环P从0.1开始如果发现车来回摆动就减小P如果转向太慢、目标容易丢就增大P。我最后调到0.35左右转向干净利落不震荡。距离环P从0.5起步调到0.8时会出现轻微推背感降到0.7后舒适得多。死区也是调舒适度的关键位置。速度死区我设为0.1m转向死区设为10像素。目标在画面中心±10像素内就不打方向这对低速场景是有效的能防止电机频繁嗡嗡响。4. 常见问题排查与避坑记录4.1 目标检测不稳定的几种典型场景检测框是在跟随中最容易出问题的地方。我实际遇到的第一个坑是室内灯光频闪导致的画面明暗条纹YOLO在这种光照变化下偶尔会漏检主要表现为一两帧没有目标输出。解决方法是上一帧检测结果做保留如果连续5帧都没有目标才确认目标丢失相当于加了一个简单的时序滤波。第二个高频问题是逆光场景。背着窗户走的时候摄像头对着窗户方向人变成黑乎乎的一坨检测框极不稳定。解决办法是开启摄像头自动曝光模式并且不要在室内灯光下和阳光直射的窗户边测试同一个阈值。实际测试中这种场景很难完全避免我最后的做法是检测丢失后小车直接原地停车而不是乱转去找人给人的感觉反而更自然。第三个问题是多人场景下的“跟错人”。YOLO会把每个检测到的人都输出我的简单策略就是取最大面积的框因为跟随对象一般离车近、框大。但如果旁边有人横穿最大框会在两人之间跳变。更稳妥的工程做法是给上一帧目标框加上预测用卡尔曼滤波或简单的IOU匹配虽然增加一点代码量但效果提升非常明显。4.2 PID调节中的坑PID参数不是一劳永逸的它跟底盘特性强相关。我在调参过程中遇到一个有意思的现象轮距0.16m的差速底盘同样的P值在不同速度下转向增益不同高速时容易过冲低速时又显得太迟钝。这本质上是底盘惯性导致的。解决方法是在角速度输出时加上一个速度相关的阻尼系数angular_z * (1.0 - 0.3 * abs(linear_x) / max_linear)这个公式的意思是速度越快转向增益越小明显改善了高速转弯的稳定性。还有一个非常容易忽视的问题发布频率。PID控制节点如果以30Hz发布但底盘串口节点只能以10Hz处理那么多余的速度指令会在缓冲区里堆积导致实际转向滞后、抖动。我的控制器特意把发布频率降到10Hz和底盘的串口接收频率匹配曲线的平滑度和跟随稳定性反而提升了。4.3 进一步扩展的方向项目做完之后后面的扩展空间其实很大。最直接的一个是加目标跟踪把YOLO这个“每帧独立猜”的检测器变成“带记忆”的跟踪器遇到遮挡、那人短暂走到柱子后面这种情况小车还能保持原地等住而不是丢目标。另一个是深度估计的升级。单目测距在目标实际身高和统计身高差距大的场景下误差很大。可以训练一个小型回归网络直接估计目标距离或者尝试用单目深度估计模型如MiDaS做像素级深度图然后取目标框中心区域的深度中值作为距离。这个方案比尺寸测距准很多但对算力要求也高Jetson Nano这种低功耗板子会比较吃力。再往后做就是多传感器融合了加上IMU做航向修正加上单线激光雷达做避障让小车在跟随的同时还能绕着障碍物走。这个路线会让工程复杂不少但从自适应跟随到自主导航一步一个脚印走过来这套单目视觉加深度学习的基线架构不会白搭后面所有模块都能在这个框架上继续生长。最后再分享一个小技巧如果你的检测框横坐标一直跳动导致转向不稳别急着调PID先在检测节点里给检测框做一次平滑滤波用EMA就行代码三行对整体稳定性的提升比调半天PID来得快得多。检测质量上去了控制参数整定的效率也会高很多。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →