尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于单目视觉与深度学习的ROS智能小车自适应跟随系统实战解析

基于单目视觉与深度学习的ROS智能小车自适应跟随系统实战解析 做机器人相关的项目很多人第一个想到的就是给小车装上“眼睛”。我这次做的这套“基于单目视觉与深度学习的ROS智能小车自适应跟随系统”简单说就是小车通过一颗几十块的普通USB摄像头用深度学习模型实时检测视野里的人再根据目标在画面里的位置和大小自动调整自己的转向和速度始终跟住目标并保持安全距离。整套逻辑跑在ROS框架下各模块独立成节点哪里出问题就替换哪里后期想加功能也方便。整套方案我前后折腾了三周中间踩了不少坑最后跑起来的效果我还是满意的室内环境下目标在1米到4米范围内跟随成功率稳定在90%以上转向平滑不抖目标短暂丢失后小车会自动旋转搜索重新看到人之后又能接上继续跟。这篇就把整个系统的设计思路、硬件选型、代码实现、调参心得都拆开讲清楚适合准备做ROS小车、机器人视觉跟随、或者正在纠结单目方案能不能用的朋友参考。我不光讲怎么搭还会讲为什么这么选、哪里容易翻车尽量让你看完能少走点弯路。1. 系统整体设计先想清楚再动手千万别上来就写代码1.1 跟随任务的核心逻辑机器人跟随本质上是一个闭环控制问题。环路的输入是“目标当前在图像中的位置”输出是“小车左右轮的转速差”中间要做的事就是反复计算偏差、修正偏差。拆开看就是三件事先感知目标在哪儿再决定往哪儿走最后驱动电机执行。这里有一个很容易踩的坑很多人一上来就急着训练模型、调参结果跑起来发现小车要么冲过头要么原地转圈。问题往往不在某个单独环节而是没有先想清楚整个闭环的输入输出关系。以我的项目为例感知层输出的是“目标框的中心坐标(x, y)、目标框的宽高(w, h)、置信度”。决策层只需要两路输出一是转向角速度控制小车左右转二是线速度控制小车快慢。目标离线越远、偏移越大转向和速度的修正就越强。这就像人走在路上盯住前面一个人你既不会死死盯着人看也不会完全不看而是根据人和你的相对位置关系不断微调步伐节奏。1.2 架构分层与ROS的解耦价值我采用的架构分三层感知层、决策层、执行层。感知层接摄像头跑深度学习目标检测决策层把检测框信息转换成控制指令执行层接收指令控制电机转速。这套分层好处很多最直接的就是调试方便哪一环出了问题直接把对应节点单独拿出来测就行不用把整台车拆了排查。ROS在这套系统里的角色就是“神经元之间的连接线”。每个模块是独立节点节点之间通过Topic通信数据和逻辑完全解耦。比如我想把YOLO换成其他检测模型只需要保证新的检测节点发布的目标框消息格式一致控制节点一行代码都不用改。再比如我想把USB摄像头换成CSI摄像头只需要替换图像发布节点。实测下来ROS这套话题通信机制非常适合做机器人视觉项目图像流走一个话题检测结果走一个话题控制指令走一个话题用rqt_graph可以看到实时拓扑用rostopic echo能直接打印消息数据调试感受比裸写多线程好太多。1.3 为什么选单目视觉深度学习不用雷达和双目我做之前对比过几种方案2D激光雷达、双目视觉、单目深度学习。激光雷达测距准确但只能扫一个平面检测不到“这是一米外的一个人”双目视觉有深度信息但标定麻烦且对光照敏感单目深度学习虽然拿不到逐像素的深度图但有一个优势是前两者都没法比的——它能“认出”目标。目标是人还是车还是箱子模型一框就明白了而且泛化性不错换一个场景也能用。加上成本极低一个普通USB摄像头就搞定对预算有限的实验室课题或个人项目非常友好。所谓“自适应”我理解是三层含义速度自适应根据目标距离自动增速降速、转向自适应根据目标偏移量自动调整方向轮的转角、状态自适应目标丢失后能自动进入搜索状态并恢复跟随。这三个“自适应”就是系统后期控制模块的核心目标。2. 环境搭建与硬件选型把钱花在刀刃上2.1 硬件清单与选型分析这个项目的硬件选型我列一个清单供参考都是实际用过的方案部件我的选择可选替代选型心得主控感知决策树莓派4B 4GBJetson Nano、X86迷你主机树莓派生态好、资料全但跑深度学习推理偏弱Jetson算力强可上TensorRT加速但散热和成本高下位机执行控制STM32F103ZET6Arduino Mega、ESP32STM32和ROS通过串口通信控制周期稳定写PWM驱动电机很方便摄像头USB免驱广角摄像头 720PCSI摄像头优先选畸变小的镜头后期能省很多标定功夫注意看FPS建议选30FPS以上的电机驱动TB6612FNGL298N、BTN7971TB6612体积小、内阻低发热控制好适合小车这种低压小功率场景底盘4轮差速小车底盘2轮万向轮、麦克纳姆轮差速底盘转向逻辑简单适合做跟随类项目控制代码好写电池3S锂电池降压模块18650电池组树莓派和电机驱动分别供电避免电机电流拉低主控电压导致重启这里分享一个很实用的经验感知主控和电机驱动一定要分开供电。刚开始我把树莓派和电机共用一个电源电机一启动电压就往下掉树莓派频繁重启排查了很久才找到原因。后来用两路供电一路给树莓派一路给电机驱动模块问题立刻消失。2.2 ROS环境配置软件环境我选的是Ubuntu 20.04 ROS Noetic。如果手里是树莓派用官方镜像再装Ubuntu 20.04比较顺手。初学者也可以用社区一键安装脚本快速搞定ROS基础环境省去不少手工配置的麻烦。装好后建议先建工作空间练练手mkdir -p ~/follow_ws/src cd ~/follow_ws catkin_make source devel/setup.bash整个过程建议自己敲一遍命令理解工作空间结构后面调试节点的时候会顺手很多。可以再加一行到.bashrc里避免每次开终端都要手输source。2.3 深度学习推理环境深度学习部分我一开始直接在树莓派上装了完整的PyTorch导致内存吃紧推理速度也很慢。后来做了两个优化第一把模型换成轻量级版本输入尺寸从640降到416检测速度明显提升第二用ONNX Runtime做CPU推理效果比直接在PyTorch里跑好不少。如果你用的是Jetson系列建议直接上TensorRT推理速度还能再翻一倍。模型选型方面项目里我用的是YOLO的轻量版本。业内有几个版本可选核心原则是“够小够快”在树莓派这种CPU设备上动辄上百兆参数的大模型根本跑不动轻量化模型在720P输入下能达到10到20帧每秒已经能支撑实时跟随。如果对精度有更高要求可以换精度更好的模型但速度会明显下降硬件配置允许的前提下可以做权衡。环境准备就绪后下一步就是写视觉感知模块。3. 视觉感知模块让小车“看见”并“认出”目标3.1 图像采集与发布做跟随系统图像采集的第一原则是控制帧率和分辨率。我的USB摄像头最高支持720P但我在实际运行时把分辨率降到320x240、帧率设为15FPS这样树莓派的CPU才有余量跑目标检测推理。图像分辨率不是越高越好能把目标清晰框出来就够用了。视觉管线里图像是一帧一帧持续到达的如果推理速度跟不上图像帧率队列就会越积越多延迟越来越大最终导致小车动作滞后。图像采集用OpenCV的VideoCapture接口然后用cv_bridge转成ROS的sensor_msgs/Image消息发布出去。代码逻辑不复杂关键是别在回调函数里做耗时操作否则图像队列会直接堵死。应该只做“采集-转换-发布”真正的检测逻辑放到单独线程或者节点里处理。3.2 YOLO目标检测ROS节点实战检测节点是整套视觉系统的核心。流程是订阅图像话题把ROS图像消息转成OpenCV图像丢给模型做推理拿到检测框后从中筛选出目标类别本项目里是人最后挑出面积最大的那个目标框作为跟随目标发布到检测结果话题。下面是检测节点的核心代码框架import rospy from sensor_msgs.msg import Image from std_msgs.msg import Header from follow_bot.msg import TargetBox from cv_bridge import CvBridge import cv2 import numpy as np # 用ONNX运行时加载模型树莓派上比PyTorch直接推理快很多 import onnxruntime as ort session ort.InferenceSession(yolov5s.onnx) bridge CvBridge() pub rospy.Publisher(/target_box, TargetBox, queue_size1) def image_callback(msg): frame bridge.imgmsg_to_cv2(msg, bgr8) # 缩放到模型输入尺寸减少计算量 img cv2.resize(frame, (416, 416)) img img.astype(np.float32) / 255.0 img img.transpose(2, 0, 1)[None] outputs session.run(None, {images: img}) # 这里拿到检测框后做NMS和后处理提取person类别的框 dets post_process(outputs[0]) target select_target(dets) # 取面积最大的person目标 if target is not None: pub.publish(convert_to_msg(target)) rospy.init_node(detect_node) rospy.Subscriber(/camera/image_raw, Image, image_callback) rospy.spin()实际项目还有NMS、锚框解码等后处理步骤但核心逻辑就是模型输出原始预测框后处理提取有效的目标框再筛选出面积最大的作为跟随目标。为什么选面积最大的因为距离越近的目标框越大被遮挡的概率也越低作为跟随目标更稳定。这个“最大框原则”在多人场景里很管用能避免目标在小车视野里来回跳。需要注意的是Topic队列长度。我在写这个项目时把队列长度设成1只保留最新一帧。如果设成10节点处理不过来时后面会积压消息越堆越旧小车反应会越来越迟钝。实时性场景中队列长度宁小勿大。3.3 单目测距的原理与标定实操单目相机没有深度信息那怎么估算目标距离这里用到一个经典的相似三角形原理。在一个标准的小孔成像模型里物体在图像中的像素高度h、物体真实物理高度H、相机焦距f以及物体到相机的距离Z满足关系Z f * H / h也就是说只要知道目标的物理高度再测出它框的像素高度用相机内参里的焦距就能估算出距离。我的目标是跟随行人所以选了成年人肩宽约40厘米作为参考宽度用目标框的宽度像素值来计算距离。实测下来在1到4米范围内误差约15%上下做速度控制完全够用。不过直接套公式有一个前提相机的焦距参数要准。我手里这颗USB摄像头没有出厂内参所以做了一次简易标定让目标站在距离相机0.5米、1米、2米、3米处分别记录目标框的宽度像素值用Z和1/h的数据拟合一条直线得到标定斜率这个过程不需要复杂的标定板几张实测数据就能拟合出可用的参数。拟合好之后距离估算的稳定性提升明显。另外目标框的像素值会有抖动我用指数移动平均EMA做了平滑公式是smoothed 0.7 * current 0.3 * smoothed_prev。这个0.7和0.3的比例可以根据实测效果微调数值太大会抖太小会反应迟钝。4. 自适应跟随控制策略PID只是基本盘4.1 转向控制把目标偏差点转成角速度转向控制的任务是让目标始终保持在图像中心。目标中心点的x坐标偏离图像中心越远小车就需要转得越快。我先把误差归一化到[-1, 1]error_x (target_center_x - image_center_x) / image_center_x然后交给一个PD控制器处理class SteeringController: def __init__(self, kp0.6, kd0.2): self.kp kp self.kd kd self.last_error 0.0 def update(self, error_x, dt): p_term self.kp * error_x d_term self.kd * (error_x - self.last_error) / max(dt, 1e-5) self.last_error error_x angular p_term d_term # 限制最大角速度防止原地转圈 angular max(-1.0, min(1.0, angular)) return angular这里D项特别重要。没有D项时小车转起来会过冲目标到达中心后小车还在转然后往反方向再修正形成振荡。加了D项之后误差变化越快抑制越强转向就稳下来了。我实测先把Kp调到0.4左右让小车能“跟上”再慢慢加Kd到0.15到0.25之间消除过冲。Kp太大小车会在中心附近来回摆动Kd太大则转向迟钝拉到最大值也追不上目标。4.2 距离控制让人和车保持“舒服”的距离距离控制是“自适应跟随”里最有体感的一部分。策略很直观目标太远就加速追上去目标太近就减速甚至停下来在安全范围内就保持当前速度。我给这套控制加了一个期望距离区间的概念比如期望保持1米到1.5米之间。目标距离大于1.5米速度给正值小于1米速度给负值倒车在区间内速度给一个很小的巡航值或者零。不过直接按误差给速度会有个问题距离误差突然很大时速度会瞬间拉满小车猛地往前冲特别不自然。我加了一个简单的斜坡限制目标速度变化速率不超过0.3米/秒的平方这样加速和减速都平滑实测人看起来也自然很多。我们在移动机器人控制里管这个叫速度过渡平滑作用就是避免加速度过猛。距离控制输出的是线速度和转向控制输出的角速度合成之后发给下位机。对于差速底盘左右轮速度分别是left_speed linear - angular * wheel_base / 2 right_speed linear angular * wheel_base / 2wheel_base是轮距这个值可以根据小车底盘参数填或者实测微调。4.3 状态机与目标丢失恢复这个模块是项目真正“活”起来的关键。如果目标检测框连续多帧消失系统必须有个兜底策略否则小车会停在原地发愣或者乱跑。我给控制系统设计了一个简单的三状态状态机跟随状态检测到目标正常跟随搜索状态目标丢失原地小角度旋转寻找目标待机状态搜索超时未找到目标停车等待状态切换逻辑if target_detected: lost_count 0 state FOLLOW elif state FOLLOW: lost_count 1 if lost_count 10: # 连续10帧没检测到目标 state SEARCH elif state SEARCH: # 以固定角速度旋转搜索 if search_timeout 10: state IDLE这里有一个细节我特意设了“连续10帧丢失”才进入搜索状态而不是丢一帧就立刻开始转。因为目标检测本来就有偶发漏检如果一丢就转小车会左右乱打方向盘实测非常影响跟随体验。这个“丢帧容忍”逻辑本质上是给状态机加了一个迟滞区间让状态切换不会太频繁。搜索状态下控制角速度固定为0.5 rad/s慢速旋转一旦检测到目标就立刻切回跟随状态整体恢复时间在1秒左右体验很好。5. ROS节点通信与下位机联动让大脑指挥手脚5.1 话题与自定义消息设计整个系统有四个核心节点图像采集节点、目标检测节点、控制决策节点、串口下发节点。它们之间的消息流转是图像采集节点发布图像话题目标检测节点订阅图像并发布目标框话题控制决策节点订阅目标框计算后发布速度指令话题串口下发节点订阅速度指令并通过串口发给下位机。自定义的目标框消息比直接用图像消息传递要高效得多。我用的是自研的TargetBox包含以下字段header时间戳x, y目标中心点坐标w, h目标框宽高distance估算距离confidence置信度自定义消息文件放msg目录编译后会自动生成Python/C调用接口使用体验和内置消息一致。在设计消息内容的时候有一个原则尽量在消息里用“语义化”的字段比如distance是明确的距离数值而不是发原始像素值让控制端去猜含义。这样每个节点都只关心自己需要的字段耦合度低以后想加一个新的决策算法也容易扩展。5.2 启动文件与节点组织我用launch文件把四个节点一次性启动方便很多launch node nameusb_cam pkgusb_cam typeusb_cam_node param namevideo_device value/dev/video0 / param nameimage_width value320 / param nameimage_height value240 / param namepixel_format valueyuyv / /node node namedetect_node pkgfollow_bot typedetect_node.py outputscreen / node namecontrol_node pkgfollow_bot typecontrol_node.py outputscreen / node nameserial_node pkgfollow_bot typeserial_node.py outputscreen / /launch调试的时候用outputscreen把节点日志打到终端非常直观。上线运行稳定后可以改成输出到日志文件避免刷屏。5.3 串口协议与下位机控制控制决策节点算出来的线速度和角速度最终要送到STM32。这里我用了一套简单的帧协议字节0字节1字节2字节3字节4字节5帧头0xAA帧头0x55线速度(带符号)角速度(带符号)校验和帧尾0x0D带符号的浮点数在串口传输时做了定点化乘以100转成整数比如0.35发35接收端再除以100恢复。校验和取前几个字节的和取低8位STM32收到后先校验不通过就丢弃。这套协议的帧格式不复杂但应付跟随场景完全够用实测下来丢包率很低。STM32端的核心逻辑就是收串口数据、解析、解算出左右轮PWM占空比然后驱动电机。代码不复杂但一定要加看门狗逻辑连续200毫秒没收到有效数据帧立即停车。这一步是安全兜底防止树莓派程序崩溃后小车还在高速乱跑。我自己在调试中就遇到过树莓派程序报错退出、但电机还在以最后速度跑的情况如果没有看门狗小车可能直接冲墙。那次之后我才深刻理解到机器人的“安全停车”不是可选项是必选项。5.4 调试三件套rqt、rostopic、PlotJugglerROS生态里调试工具很强大我用得最多的是三个rqt_image_view实时查看检测前后的图像确认检测框是否正常rostopic echo打印话题数据检查消息内容和频率PlotJuggler把距离、角速度变化曲线画出来调PID参数时非常直观特别是调PID靠嘴猜参数太难了。我把控制节点的角速度、线速度、目标距离发布成可视化曲线一边跑一边看波形。转向环有没有过冲速度环有没有抖动一眼就能看出来比看打印日志高效得多。6. 调试实录与常见问题速查6.1 调试流程心得我的调试顺序是先从“静态”到“动态”第一步让目标人站着不动调转向控制看小车能不能稳定对正目标第二步让人缓慢走动调距离控制看小车能不能保持安全距离跟住第三步加入目标丢失和搜索状态测试恢复能力最后才做完整的动态场景测试。不要一上来就跑全流程那样出了问题很难定位。每轮测试我都会记录一组数据目标距离、检测帧率、系统延迟、电机响应情况。其中系统延迟是影响跟随体验的关键指标很多跟随不灵敏的问题本质上是延迟太高。我的树莓派4B上检测耗时约60到80毫秒加控制下发和电机响应总延迟约150毫秒。这个延迟水平下目标正常速度行走跟随效果已经很自然。6.2 高频问题排查表现象可能原因解决方案目标检测卡顿、帧率低模型太大/输入尺寸过大换轻量模型输入尺寸降到416用ONNX Runtime加速小车转向时左右抖动Kd项过大或目标框抖动减小Kd对目标中心坐标做EMA平滑小车突然加速冲出去距离估算误差大重新标定距离-像素拟合曲线检测最大速度限制目标丢失后小车乱转丢失确认帧数太少增加“连续丢失帧数”阈值比如10帧再切入搜索状态光照变化后目标检测丢失模型泛化不足换更大模型增加曝光增强预处理考虑HDR摄像头电机启动时树莓派重启共地供电或供电不足两路独立供电电机和主控分开确保共地小车反应明显缓慢话题队列积压、延迟过高队列长度设为1检查每帧处理耗时关掉不必要的可视化节点6.3 调参与经验心得最后分享几个我反复踩坑后总结的经验。PID调参一定要先调P、再调DI项在跟随任务里基本用不上。没有D的时候系统会振荡表现为小车左右摆头这时候一点点加Kd直到摆头消失且响应不迟钝这个位置大概就是最优值。每个硬件的Kp、Kd都不一样别人给你一个现成的参数表往往不适用因为轮距、电机响应、摄像头视角都不同必须自己边跑边调。目标检测节点的“目标锁定”逻辑也很关键。多人场景下如果每次都选面积最大目标有可能会在两个人交错时瞬间跳到另一个人身上。我在检测节点里加了一个简单的目标ID记忆机制优先选择当前帧里与上一帧目标框IoU大于0.5的目标选不到才回退到面积最大原则。这个小改动让跟随的连贯性提升很明显不会出现“跟人跟到一半突然换了目标”的尴尬情况。整个项目做完我的体会是做机器人视觉项目花在系统集成和调试上的时间远多于写算法的时间。模型可以跑在本地但怎么把模型输出变成平滑的控制指令、怎么处理各种边缘情况、怎么保证系统安全才是真正决定项目成败的地方。后续如果继续扩展可以在这个系统上接入障碍物检测、语音切换跟随目标、多目标重识别等功能框架已经搭好扩展起来会顺畅得多。
返回列表