ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

单目视觉+深度学习:基于ROS2的智能小车自适应跟随系统实现

单目视觉+深度学习:基于ROS2的智能小车自适应跟随系统实现 你可能以为智能小车做跟随必须上深度相机或者激光雷达。实际做下来你会发现单目摄像头加一个轻量级目标检测模型在大多数室内平地场景里已经能把“跟随人”这件事做得相当稳。这个结论不是凭空来的——我自己完整做了一套基于单目视觉和深度学习的ROS智能小车自适应跟随系统从硬件选型到算法落地折腾了两三个月中间踩了不少坑。这篇博客把整个方案从架构到实现细节完整梳理一遍重点讲“单目测距”和“自适应控制”这两个最容易翻车的地方。适合正在做智能车竞赛、物流小车项目或者想入门ROS2加深度学习实际部署的朋友参考。1. 单目跟随方案的取舍为什么先排除了深度相机和激光雷达1.1 三种测距方案的现实对比在做跟随系统之前我先把市面上主流的测距方案列了一遍。这里说的测距指的不是“有没有障碍物”而是“目标物体距离小车多远、在哪个方向”。三种方案分别是深度相机Intel RealSense、Kinect、单线激光雷达、单目摄像头加深度学习检测。它们各自的表现差异很大我用一张表把关键维度罗列出来。维度深度相机单线激光雷达单目深度学习硬件成本几百到上千元千元以上几十到几百元测距方式直接输出深度图直接输出点云距离检测几何模型推算语义识别需结合RGB图像基本无法识别目标天然带语义信息环境敏感性室外强光下容易失效对反光、黑色物体有干扰依赖光照和纹理安装体积较大需要固定支架需要视野开阔位置一个小摄像头即可主要难点点云配准、高功耗无语义无法判断跟随谁标定要求高、依赖地面假设这里最容易被忽略的一点是深度相机在室内确实好用但你把它装到小车上一旦走出户外或者对着阳光方向深度图里会大面积空洞RGB和深度对齐也会出问题。激光雷达单线版对环境光不敏感但它给的是“某个角度有障碍物”不是“这是我要跟的人”。而跟随系统真正的核心恰恰是“认准目标”这一点单目视觉有天然优势。所以我的选择很明确单目摄像头做感知深度学习模型负责识别目标测距由相机几何模型推算。整个系统的传感器成本不到两百块重量几乎可以忽略换到别的车体上只要重新标定一次相机角度就能复用。1.2 单目方案的真正优势不是成本而是通用性很多人觉得单目方案是“穷人的选择”是退而求其次。我实际做完的感受是单目方案最大的优点不是便宜而是它的可移植性和语义能力。摄像头拍到的是RGB图像图像里包含了丰富的颜色、纹理、边缘信息。深度学习模型能直接从图像中回答“画面里谁是目标”这是深度相机和雷达都做不到的。深度相机的问题在于它给你的是一堆三维点你还得想办法把点和人要跟的目标绑定到一起。激光雷达更麻烦一个行人点云和一个纸箱点云在纯几何特征上没那么好区分。单目方案把“识别”和“定位”拆开了识别交给网络定位交给几何模型。每一步都清晰可控。当然单目方案的代价也很明确测距精度不如主动式传感器极度依赖标定和地面平整度。所以后面我花了不少精力在测距模型和误差补偿上这部分会在第四章展开。1.3 这套系统的整体工作链路整个系统跑起来之后的数据流是这样的图像采集 → 目标检测深度学习模型推理 → 检测框提取 → 单目测距/测角地面平面假设 → 自适应控制器模糊PID → 线速度/角速度指令 → 底层电机驱动每一个环节的数据形态都在变化。摄像头出的是图像检测节点输出的是“目标类别、置信度、检测框位置”测距节点把检测框换算成“距离和偏角”控制节点再把距离和偏角映射成小车的速度指令。链路不长但每一环都有讲究。检测模型选不好后面测距全是噪声测距模型没标定好控制器再优秀也白搭控制器调不好车会在原地抽风。这篇博客后面的章节就按这条链路来拆每一步都给出我实际用到的参数和排错经验。2. 硬件平台与ROS环境一套能跑起来的基础配置2.1 底盘和底层控制编码器必须有智能小车的底盘选择很关键。我见过不少初学者为了省钱买那种不带编码器的玩具底盘装上电机驱动板就开跑。这种配置做遥控车没问题做跟随系统会非常痛苦。跟随系统的控制链路最后要落到“精确的车轮转速”上。底层如果没有编码器做闭环低速时电机会因为死区、摩擦不均匀而一顿一顿速度控制完全非线性。控制算法输出一个小速度值轮子要么不动要么突然猛转整个车就会抽搐。所以我的建议是底盘驱动轮必须带编码器底层用STM32F103ZET6、Arduino Mega或者ESP32做PID速度环和上位机通过串口通信。底层控制协议我用的是固定周期20ms每周期接收上位机下发的线速度和角速度内部换算成左右轮速再通过编码器PID跟踪目标轮速。底层的速度环PID和控制层的跟随PID是完全分开的不要混在一起。控制层管“该跑多快”底层管“怎么让轮子跑准”。2.2 上位机树莓派4B还是Jetson系列上位机负责跑检测模型、测距和控制逻辑。这里要看你跑什么级别的模型。我测试过的配置有两种第一种是树莓派4B加USB免驱摄像头。树莓派CPU算力有限我用了量化后的轻量模型推理速度大概在5到10FPS之间适合慢速跟随场景。优点是功耗低、体积小、社区资料多。第二种是Jetson Nano或者更高端的Xavier NX自带GPU能跑TensorRT加速。同样一个YOLOv5n模型在Jetson Nano上用TensorRT FP16推理能跑到20到30FPS跟随体验会好很多。缺点是贵而且散热风扇吵。我个人的建议是如果你还在学习和调试阶段先在x86主机上用USB摄像头把整套代码跑通逻辑没问题之后再移植到ARM平台上。不要在树莓派上边学ROS边调模型那样排错难度翻倍。摄像头方面我踩过一个很深的坑自动对焦。UVC摄像头默认开启了自动对焦和自动曝光这些功能对拍照是友好的但对视觉系统是灾难。自动对焦会让焦距不断变化标定出来的内参直接失效自动曝光会在目标经过窗户、灯光时突然改变画面亮度检测框跟着跳。买摄像头时一定要选支持手动锁定焦距和曝光的型号然后用v4l2-ctl把参数锁死。2.3 ROS发行版选择从Noetic到HumbleROS版本的选择会直接影响项目周期。老教程大多基于ROS1 Noetic配Ubuntu 20.04。ROS1的资料多但已经停止维护新装环境时依赖库很容易冲突。ROS2 Humble配Ubuntu 22.04是目前社区的主流方向多机通信、节点生命周期管理、参数服务都比ROS1好用得多做这种多节点系统优势明显。说实话刚开始装ROS2环境的时候依赖地狱问题没少折磨人。现在很多初学者会看到社区的一键安装脚本比如鱼香ROS一键安装确实能省不少折腾时间。我的态度是可以用脚本装但务必搞清楚它装了什么、装到了哪个目录、改了哪些环境变量。我见过好几个朋友用脚本装完后面自己手动编译功能包时源码路径找不到最后只能重装系统。如果你同时要跑Gazebo仿真验证底盘运动学建议直接在Ubuntu 22.04上装ROS2 HumbleGazebo是自带集成的不用再折腾版本匹配问题。2.4 底层控制的另一个选项micro-ROS如果你不想自己设计串口通信协议还有一个比较优雅的方案用ESP32跑micro-ROS把底层控制板直接变成ROS2的一等节点。这样上位机直接用话题给底层发指令底层状态也能通过话题回传省掉一整套自定义串口协议。我第一版用的是自绘的串口帧协议结构是帧头线速度角速度校验用起来没问题就是调试格式有点烦。后来把ESP32刷了micro-ROS上位机和底层之间的通信透明了很多。但micro-ROS的节点初始化、话题QoS匹配调试起来并不直观不适合完全没有通信协议经验的人。想省事就串口想要工程结构干净就micro-ROS两条路都能走通。3. 目标检测模型轻量化模型的选型、训练和部署3.1 模型选型YOLO系轻量网络是首选跟随系统的检测模型必须满足两个条件实时性和可迁移性。我对比了四个常见方案的实测表现YOLOv5n、YOLOv8n、YOLOX-Nano、MobileNet-SSD。模型参数量CPU平台FPS参考GPU平台FPS参考部署难度YOLOv5n约180万6-1025-35低导出ONNX方便YOLOv8n约310万5-820-30低训练生态好YOLOX-Nano约90万7-1225-40中后处理略繁琐MobileNet-SSD约560万4-715-25低但精度一般我最后主力用的是YOLOv5n因为它的部署链路最成熟导出ONNX之后不管是CPU上的ONNX Runtime还是Jetson上的TensorRT都能无痛转换。如果你更在意训练得上手速度YOLOv8n也不错接口更现代。MobileNet-SSD精度稍弱我实测在远距离小目标上容易漏检不推荐作为主方案。3.2 训练数据自采数据才是精度分水岭很多人直接拿COCO预训练权重跑检测发现“跟着人”挺好用的。确实COCO里的person类别足够通用室内外都能检测到人。但如果你要跟随的是一个特定目标比如物流小车要跟着搬运箱走那箱子的外观、颜色、光照变化都是COCO没覆盖的必须自己采集数据。自采数据有一套固定套路。先把摄像头装到车上固定好高度和俯仰角然后录制目标在不同距离、不同方向、不同光照条件下移动的视频。录制时覆盖1到5米的距离范围让目标从画面边缘走到中心再绕车半圈。视频录完用脚本按帧抽图一般每秒抽2到3帧就够避免相邻帧几乎相同导致过拟合。我这边实际标注了大约2000张图像。标注工具用labelImg只标目标类别。数据增强开mosaic、HSV扰动、随机翻转。从COCO预训练权重开始微调而不是从零训练这样2000张的数据量就够用了。训练参数大概是输入640x640batch size 16epochs 150左右具体数值要看你的显存大小。实际经验是白天的自然光、傍晚的暖光、夜晚的灯光三种场景各采一批模型在场景切换时才不会掉链子。如果只在一种光照下测识别率看起来很高一换环境就露馅。3.3 端侧部署ONNX Runtime和TensorRT模型训练好之后要导出成部署格式。YOLOv5n的导出流程是先用官方仓库的export.py把PyTorch权重转成ONNX然后在Jetson上用TensorRT把ONNX转成engine文件。在CPU平台上直接用ONNX Runtime的Python接口推理就够了。下面这个代码片段是一个简化版的推理循环我实际工程里差不多就是这个结构import cv2 import numpy as np import onnxruntime as ort sess ort.InferenceSession(yolov5n.onnx, providers[CUDAExecutionProvider, CPUExecutionProvider]) input_name sess.get_inputs()[0].name def detect(frame): img, ratio, (dw, dh) letterbox(frame, 640) img img[:, :, ::-1].transpose(2, 0, 1)[None] / 255.0 pred sess.run(None, {input_name: img.astype(np.float32)})[0] boxes nms(pred, conf_thres0.5, iou_thres0.45) return boxes这段代码有两个关键点。一是letterbox也就是把图像等比缩放到640x640多余部分补灰边不能直接resize否则目标比例失真检测框会偏。二是后处理NMS非极大值抑制不可省否则同一个目标会出一堆重叠框。平台差异方面我实测树莓派4B的CPU跑ONNX Runtime量化模型640输入大概在100到200毫秒一帧。Jetson Nano用TensorRT FP16引擎能到30到50毫秒一帧完全满足跟随需求。再往上用Xavier NX甚至可以对视频流做两路并行推理。3.4 检测后处理不滤波测距根本没法用检测模型直接输出的框是逐帧独立的抖动非常厉害。同一个目标在连续几帧里检测框的宽高和中心点位置会有几个像素到十几个像素的波动。这些波动放到测距环节就会被放大成几十厘米的距离跳变。所以检测结果必须做后处理。我用的是三层处理置信度过滤、EMA指数移动平均、面积突变过滤。置信度阈值设0.5低于这个值直接丢掉。EMA是对检测框中心坐标和框高度做平滑公式是filtered alpha * current (1 - alpha) * filteredalpha取0.3到0.5之间效果比较明显。面积突变过滤则是检查当前框面积和上一帧平滑结果比如果一帧之间面积跳变了20%以上就认为这一帧有可能是误检直接用上一帧结果顶替。另外要把检测频率和控制频率解耦。检测模型即使能跑30FPS也没必要每个控制周期都重新推理。我这边是检测线程保持10到15Hz控制线程20到50Hz。控制线程在没有新检测结果的间隙里用上一次检测的目标位置做一些简单的匀速外推预测。这样CPU占用不会一直满载控制稳定性反而更好。4. 单目测距建模把像素坐标变成小车能用的距离和角度4.1 针孔相机模型先讲清楚单目测距绕不开针孔相机模型。一句话解释相机的成像过程就是把三维空间中的点通过光心投影到二维图像平面上。用公式表达一个空间点(X, Y, Z)在相机坐标系下投影到像素坐标(u, v)的关系是u fx * X / Z cx v fy * Y / Z cy其中fx和fy是焦距相关参数cx和cy是光心在图像中的像素位置这四个数构成相机内参矩阵K。标定就是求这四个数加上畸变系数。很多教程把这个公式列出来就完事了但实际使用时真正重要的是反过来思考已知像素坐标(u, v)能推出空间中的什么信息答案是只能推出一条射线方向而无法确定距离。这就是单目“丢失深度”的本质。想恢复距离必须引入额外假设。跟随小车最常见的假设是目标所处的点是地面平面上的点。4.2 地面平面假设下的测距公式假设相机固定安装在小车上光心距离地面高度为h俯仰角为pitch。当我们要测的目标底部位于地面平面时已知目标框底边中心的像素纵坐标v前方距离d可以由相机几何关系推导出来。推导过程其实不复杂。像素纵坐标v可以换算出从相机光心出发的射线与光轴的夹角这个夹角加上相机本身的俯仰角就是射线相对于水平地面的俯仰角。然后利用三角关系高度h除以这个俯仰角的正切值就得到地面交点到相机的水平距离。实际工程里我用的近似公式是这样的tan_alpha (v0 - v_bottom) / fy d h / tan(pitch atan(tan_alpha))其中v0是图像中心行的像素坐标v_bottom是检测框底边中心的像素行号fy是焦距参数h是相机安装高度pitch是相机俯仰角。当pitch较小比如10度以内时这个公式的精度足够用。横向偏差同理。目标中心横坐标u与图像中心u0的差结合fx和距离d可以算出目标相对小车中轴线的横向偏移量或者直接换算成偏角angle atan((u - u0) / fx)这个angle就是控制环节需要的目标方向角。4.3 相机标定与安装角度的影响内参标定我用的标准棋盘格方法。OpenCV有现成的calibrateCamera接口ROS也提供了camera_calibration工具包把棋盘格图案打印出来贴平板上从不同角度拍二三十张照片就能标定。但真正让很多项目翻车的不是内参而是外参也就是相机相对地面的安装高度和俯仰角。内参展错可以通过公式精确标定外参展错往往被忽视。我见过一个项目里相机俯仰角标定差了5度远距离测距直接偏了快一米车跟着跟着就贴上去了。我用的是一种更工程化的简化标定法不在纯数学层面求外参而是直接在几个已知距离上标定映射关系。把目标放到1米、2米、3米、4米处记录对应的v_bottom值然后用多项式拟合出d关于v_bottom的映射函数。这样内参、外参、镜头畸变的影响全都被这个映射吸收进去了只要相机位置和角度不变化这个方法比单独标内外参更稳。4.4 测距误差为什么底部点必须准确单目测距的误差来源我实测下来主要有三个目标框底部不准确、地面不平整、相机安装松动。目标框底部不准确是最主要原因。检测模型输出的框不严格贴着地面尤其当目标穿深色裤子、地面颜色相近或者目标在远景时框底边可能上下偏差十几个像素。这个偏差在近距离时影响小在远距离时会被显著放大。我这里做过一个估算在3到4米处框底边偏差10个像素距离误差可能达到0.3到0.5米。这个误差足够让控制器产生明显的顿挫感。所以检测后处理里对框底边做了重点保护。EMA平滑的对象包含框底边坐标而且给底边坐标单独设置了变化率上限。地面不平整这个问题没法在算法里完全消除只能靠控制层的容错机制兜底。相机安装必须用固定支架锁紧后打上标记每次拆装后都要确认高度和俯仰角没变。5. 自适应跟随控制器PID参数不固定跟随才不会抽风5.1 跟随控制量的定义控制器的输入是测距节点输出的两个量目标距离d和目标偏角angle。我需要车保持在期望距离d_ref附近同时让目标保持在小车正前方也就是angle趋近于0。纵向控制误差是 e_d d - d_ref当目标太近时e_d为负车退太远时e_d为正车进。横向控制误差就是angle本身。控制量输出分别是线速度v和角速度omega。这里要注意一个工程细节线速度和角速度的控制器最好是分开的两个PID而不是用一个PID同时调两个量。因为两个量的动态特性差很多目标距离变化慢角度变化快共用一套参数会有冲突。我这边是纵向PID和转向PID完全独立各有各的Kp、Ki、Kd。最终指令通过差速模型换算成左右轮速发给底层。输出一定要做限幅。我设置线速度上限0.6米每秒角速度上限1.2弧度每秒。没有限幅的话控制器刚起步时误差大输出一个巨大速度值车会猛冲吓人也不安全。5.2 模糊自适应PID的落地固定PID参数下目标慢速移动时表现还行但目标突然转身、加速离开或者从画面边缘切进来时固定参数很容易超调或者响应过慢。这就是标题里“自适应”三个字要解决的问题。我给控制器加入了基于误差e和误差变化率ec的模糊规则表用查表加插值的方式动态调整三个PID参数。完整的模糊推理系统在资源紧张的单板机上太重实际工程里用一张二维查表就够。规则的核心逻辑是误差大时加大Kp、减小Kd让系统快速逼近目标误差小时减小Kp、加大Kd防止超调振荡误差变化率大时适当降低Ki避免积分项把系统推向振荡。我用的简化规则表大致长这样误差 \ 变化率误差变化小误差变化中误差变化大误差大Kp大 Ki中 Kd小Kp大 Ki小 Kd小Kp大 Ki小 Kd中误差中Kp中 Ki中 Kd中Kp中 Ki中 Kd中Kp中 Ki小 Kd大误差小Kp小 Ki大 Kd大Kp小 Ki中 Kd大Kp小 Ki中 Kd中控制器每个周期先查表得到一组调整系数乘上基础PID参数就是当前的最终参数。这种方式比固定PID灵活得多实现起来也不复杂一个二维数组加几行查表插值代码就搞定。5.3 目标丢失与异常跳变处理跟随系统最怕的不是目标抖而是目标直接消失。我实测在室内跟随人时人走过墙角或者被门框短暂遮挡检测框会突然消失几帧到几十帧。我设计的策略是分两段处理短时间丢失持续1秒以内车保持当前速度的30%继续滑行一小段同时等待目标重新出现连续丢失超过1秒车直接减速到零并原地小角度旋转搜索检测到目标后重新建立跟踪。这个策略能明显改善跟随体验——目标过门框时车不会一下急停一下猛冲。跳变处理也很重要。如果某一帧测距结果比上一帧跳变了0.5米以上大概率是误检或者检测框异常。处理办法是丢弃这一帧的测距值用上一帧结果替代并给这个异常做个计数连续多次异常才允许更新目标状态。多目标场景下我选择置信度最高且离画面中心最近的目标作为跟随对象。当跟随对象切换时要重置PID积分项和EMA滤波缓冲否则历史数据会拖累新目标的响应。5.4 实测调参顺序与心得调参顺序比参数本身更重要。我建议按下面三个阶段来每阶段都验证稳定了再进下一步第一阶段只在直道上跟随人慢走速度上限设0.2米每秒固定PID参数把纵向距离误差调到正负0.15米以内收敛。第二阶段加入转向。让人在前面走“S”型路线单独调转向PID观察车是否会出现来回摆头。出现摆头说明Kp_theta太大或Kd_theta太小降低Kp或者加大Kd。第三阶段才把模糊自适应表接进去做转弯跟随和变速跟随测试。我这里的实际调参起点可以给你当参考但一定要根据你的平台重新调纵向Kp是1.0Ki是0.05Kd是0.2转向Kp是1.8Ki是0.02Kd是0.3。期望距离d_ref设的是1.2米。调参时用rqt_plot或plotjuggler把距离误差曲线实时画出来比肉眼看车屁股判断准得多。一个小技巧调参前先确保检测和测距链路是稳的把距离和角度在图像上实时画出来观察几秒如果距离数值快速跳动先解决检测问题不要急着调PID。在测距噪声很大的情况下调PID得到的所有结论都是不可靠的。6. ROS工程化落地节点划分、消息通信与调试验证6.1 节点划分与话题设计ROS2的节点化设计让这套系统的每个功能模块都能独立调试和替换。我把整个系统拆成了五个节点每个节点只干一件事节点名输入话题输出话题职责usb_cam_node无/image_raw图像采集detect_node/image_raw/detect_box目标检测输出检测框distance_node/detect_box/target_pose测距测角输出距离和角度controller_node/target_pose/cmd_vel自适应控制发布运动指令serial_bridge_node/cmd_vel无串口转发到底层控制板自定义消息类型定义在专门的接口包里。detect_box消息包含header、class_id、confidence、检测框中心坐标和底边坐标target_pose消息包含header、distance、angle、is_valid四个字段。如果你只在节点内传数据用Python对象也凑合但一旦涉及rqt可视化或者rosbag回放标准消息类型能省大量时间。坐标系方面摄像头坐标系和底盘坐标系之间用一个静态TF变换关联。测距节点算出的距离默认是相对摄像头光心的如果摄像头不在底盘中心需要把它变换到底盘坐标系下再给控制器使用。我这里摄像头安装在车头前方10厘米处不做变换在低速跟随场景下误差不大但严谨的做法还是写一个静态变换。调试可视化节点我也留了一个独立节点它订阅detect_box和target_pose把距离、角度、检测框直接绘制到图像上发布到/debug_image话题。这样用rqt_image_view就能实时看到系统内部状态。这个调试节点成本极低价值极大。6.2 实测中的三个典型坑和排查链路第一个坑是摄像头自动曝光导致检测框乱跳。现象是目标经过窗户附近时画面突然过曝检测框消失或者乱飞。排查链路从看原始图像开始用rqt_image_view观察画面亮度变化确认是曝光问题后用v4l2-ctl手动锁定曝光时间和增益问题立刻缓解。如果发现锁定曝光后画面太暗需要换一个低照度性能更好的摄像头而不是放宽检测阈值。第二个坑是控制节点出现阻塞导致转向一顿一顿的。现象是跟随直线走很顺一转弯就卡顿。排查时我在每个节点打上时间戳日志发现controller_node循环周期从20毫秒跳到了100毫秒以上。原因是代码里某个回调函数里用了阻塞式的消息等待占用了控制周期。解决方法是所有等待都改成非阻塞方式控制循环用ROS的Rate机制保持固定频率。第三个坑是串口通信丢帧导致底盘指令不连续。现象是车偶尔会停一下再突然启动。排查链路是先看serial_bridge_node是否在稳定发送再用串口工具抓包看底层是否收到了完整帧。最后发现是底层控制板串口缓冲区溢出因为上位机发送频率和底层读取频率不匹配。解决办法是底层把串口读取放到独立任务中并用队列缓冲指令数据。6.3 从仿真到实车仿真里跑通导航不等于会做跟随和纯自主导航不同跟随系统里目标的位置是动态变化的仿真的意义更多在于验证坐标系变换和运动学模型而不是验证算法效果。你可能在Gazebo里能轻松复现距离保持但真机上光照、检测噪声、轮子打滑、地面摩擦系数这些干扰仿真里基本体现不出来。我的建议是先用rosbag录制几段真实图像数据包括目标直走、转弯、遮挡、光线变化等场景。离线回放这些数据来调试检测模型和测距参数等测距和检测稳定了再上实车调控制。这个方法能避免在实车上反复试错。如果你想做更完整的自主导航和跟随结合的物流小车方案可以参考“ROS小车自主导航仿真”里的建图与路径规划部分把全局导航作为上层跟随控制作为局部行为。但一定记得仿真里跑通导航和实车跟着人稳稳走完一圈中间隔着一整条调参的河。我个人的体会是单目跟随项目真正的时间消耗大头在调参和标定而不是编码。先把检测和测距链路用调试节点可视化跑通再开始调控制器每一步都确认稳定了再往下走。这套系统再往后扩展还可以换更轻量的模型跑到ESP32加micro-ROS的底层组合上或者把跟随目标从人换成AprilTag做物流工位对接链路里90%的代码都能复用。
返回列表