1. 项目缘起与整体设计思路
机器人视觉避障这件事,说起来简单——不就是让机器人看到东西然后绕开嘛。但真到部署阶段,你会发现从传感器选型到算法落地,中间隔着无数个让人抓狂的细节。我前后在三个不同平台上折腾过这套流程,从室内小型移动底盘到室外低速巡检车,踩过的坑足够写一本小册子。这篇内容就是把这些经验整理出来,给正在做类似项目的朋友一个可参考的路线图。
ROS机器人视觉避障部署流程的核心目标,是让移动机器人利用视觉传感器(单目、双目或深度相机)感知前方障碍物,结合ROS的导航框架实现实时避障。它解决的是传统激光雷达在特定场景下的盲区问题——比如玻璃幕墙、低矮台阶、悬空线缆这类激光雷达容易漏检的目标。适合有一定ROS基础、正在做移动机器人导航开发的工程师,也适合刚接触机器人视觉的研究生用来搭建实验平台。
整套流程可以拆成四个大块:传感器选型与标定、感知算法部署、避障策略融合、系统联调与优化。这四个环节环环相扣,任何一个环节出问题,最后表现出来的都是“机器人要么撞墙要么原地打转”。我见过太多人一上来就调避障参数,结果发现是相机标定偏了几度,白白浪费一周时间。所以下面的内容会按照实际部署顺序展开,把每个环节的关键决策点和容易翻车的地方都讲清楚。
在方案选型上,我倾向于深度相机+视觉SLAM+局部规划器的组合。原因很直接:深度相机能直接输出点云或深度图,省去了单目测距的尺度不确定性;视觉SLAM提供全局定位,避免机器人跑着跑着就“迷失自我”;局部规划器负责实时避障,响应速度快。这套组合在算力有限的嵌入式平台上也能跑起来,比如Jetson Orin Nano这个级别的设备就足够。
注意:如果你的机器人本身已经装了激光雷达,视觉避障可以作为补充层叠加,而不是完全替代。两者融合能显著提升对玻璃、镜面等特殊材质的检测率。
2. 硬件选型与相机标定的关键细节
2.1 深度相机怎么选才不踩坑
市面上的深度相机主要分三类:结构光、双目立体视觉、ToF。结构光在近距离(0.3-1米)精度高,但室外强光下基本废掉;双目立体视觉依赖纹理,白墙前面容易丢深度;ToF受环境光影响小,但分辨率普遍偏低。我实测下来,室内场景用结构光方案性价比最高,室外或者光照变化大的场景优先考虑ToF。
选型时重点看三个参数:视场角、有效测距范围、输出帧率。视场角决定了机器人的“视野宽度”,一般水平视场角在60-90度之间比较合适。太小了盲区大,太大了边缘畸变严重。有效测距范围要覆盖机器人的刹车距离——假设机器人最高速度1.5米/秒,制动加速度0.5米/秒²,那刹车距离就是2.25米,相机的有效测距至少要到3米才安全。帧率建议不低于15帧,否则避障响应会有明显延迟。
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 水平视场角 | 60-90度 | 太小盲区大,太大畸变严重 |
| 有效测距 | ≥3米 | 需覆盖刹车距离的1.5倍 |
| 输出帧率 | ≥15fps | 低于此值避障延迟明显 |
| 深度分辨率 | ≥320×240 | 太低无法识别小障碍物 |
2.2 标定这件事,偷懒必翻车
相机标定分内参标定和外参标定两步。内参标定解决的是相机自身畸变和焦距问题,外参标定解决的是相机相对于机器人底盘的位姿关系。很多人只做内参不做外参,结果就是避障时障碍物位置总是偏那么一点,机器人贴着障碍物边缘蹭过去。
内参标定用ROS官方的camera_calibration包就行,准备一张棋盘格标定板,在相机视野内不同角度拍20-30张照片。标定完成后会得到一个ost.yaml文件,里面包含焦距、主点坐标和畸变系数。这里有个经验:标定板要占满画面三分之一以上,否则角点检测误差会放大。另外标定时要保证光照均匀,避免反光。
外参标定稍微麻烦一点。我的做法是在机器人前方地面贴一个已知尺寸的标记物,然后通过tf树把相机坐标系和底盘坐标系关联起来。具体操作是先用rosrun tf static_transform_publisher发布一个初始变换,然后手动测量相机光心到底盘中心的距离和角度,填入变换参数。实测下来,外参的平移误差控制在2厘米以内、旋转误差控制在1度以内,避障效果就比较理想了。
提示:标定完成后一定要做验证。在机器人前方不同位置放几个障碍物,用RViz看深度点云是否和实际位置吻合。如果偏差超过5厘米,建议重新标定。
3. 感知算法部署与点云处理
3.1 从深度图到障碍物点云
深度相机输出的原始数据是深度图,每个像素值代表该点到相机的距离。要用于避障,需要先转换成点云。ROS里用depth_image_proc包可以完成这个转换,核心是point_cloud_xyz节点。配置时需要注意fx、fy、cx、cy这四个参数必须和标定结果一致,否则点云会变形。
转换后的点云包含大量地面点和噪声点,直接拿去做避障会导致机器人频繁误刹车。所以下一步是地面分割。常用的方法有两种:RANSAC平面拟合和高度阈值过滤。RANSAC更鲁棒,但计算量大;高度阈值简单快速,但要求地面平整。我一般先用高度阈值粗筛,再用RANSAC精筛,兼顾速度和精度。
# 地面分割示例(基于高度阈值) import numpy as np def ground_filter(points, ground_height=0.1, tolerance=0.05): """ points: Nx3 numpy array ground_height: 相机安装高度(米) tolerance: 高度容差 """ z = points[:, 2] mask = np.abs(z + ground_height) > tolerance return points[mask]这段代码的逻辑是:相机安装高度已知,地面点的高度值应该接近负的安装高度。把接近这个值的点过滤掉,剩下的就是障碍物点云。实际使用时tolerance要根据地面平整度调整,室内平地0.03-0.05米够了,室外可能要0.1米。
3.2 障碍物聚类与特征提取
过滤完地面点之后,还需要把点云分成一个个独立的障碍物。这一步用欧式聚类最合适,ROS里对应的是pcl::EuclideanClusterExtraction。关键参数是cluster_tolerance,表示两个点之间多远算同一类。这个值设太小会把一个障碍物拆成多个,设太大会把相邻障碍物合并。
我的经验值是:cluster_tolerance设为障碍物最小尺寸的一半。比如你要检测的最小障碍物是20厘米宽的柱子,那这个值设0.1米左右。另外min_cluster_size和max_cluster_size也要根据实际场景调整,太小会把噪声当成障碍物,太大可能漏掉小目标。
聚类完成后,每个障碍物可以用质心、包围盒、最近点距离三个特征来描述。质心用于判断障碍物方位,包围盒用于估计尺寸,最近点距离用于判断碰撞风险。这三个特征传给避障模块就足够了。
| 特征 | 用途 | 计算方式 |
|---|---|---|
| 质心 | 判断障碍物方位 | 点云坐标均值 |
| 包围盒 | 估计障碍物尺寸 | 坐标极值差 |
| 最近点距离 | 评估碰撞风险 | 到机器人中心最小距离 |
4. 避障策略与局部规划器融合
4.1 为什么不能直接用全局规划器避障
全局规划器(如A*、Dijkstra)生成的是从起点到终点的最优路径,但它假设环境是静态的。一旦出现动态障碍物或者地图未更新的障碍物,全局路径就可能撞上去。所以避障必须依赖局部规划器,它负责在全局路径附近实时调整轨迹。
ROS里常用的局部规划器有DWA(动态窗口法)、TEB(时间弹性带)、MPC(模型预测控制)。DWA计算量小、参数直观,适合算力有限的平台;TEB生成的轨迹更平滑,适合狭窄通道;MPC理论最优但调参复杂。我一般先用DWA跑通基本功能,再根据需求决定是否换TEB。
4.2 视觉障碍物如何接入局部规划器
局部规划器通常接受两种形式的障碍物输入:代价地图和障碍物列表。视觉点云可以直接投影到代价地图上,也可以聚类后作为障碍物列表传入。两种方式各有优劣:代价地图融合度高,但更新频率受限于地图分辨率;障碍物列表响应快,但需要额外处理数据关联。
我的做法是双通道输入:点云投影到局部代价地图,用于精细避障;聚类后的障碍物列表用于快速反应。具体配置是在local_costmap的observation_sources里加入视觉点云话题,同时把聚类结果发布成costmap_2d能识别的PointCloud2格式。
# local_costmap_params.yaml 关键配置 local_costmap: observation_sources: scan camera_points scan: topic: /scan data_type: LaserScan marking: true clearing: true camera_points: topic: /camera/depth/points_filtered data_type: PointCloud2 marking: true clearing: true obstacle_range: 3.0 raytrace_range: 3.5这里obstacle_range表示障碍物标记的最大距离,raytrace_range表示清除障碍物的最大距离。视觉点云建议obstacle_range设3米左右,太远了噪声多,太近了反应不及。
4.3 避障参数调优的实战经验
调参这件事,理论是一回事,实际是另一回事。我总结下来,最影响避障效果的三个参数是:inflation_radius、cost_scaling_factor、max_vel_x。
inflation_radius是障碍物膨胀半径,决定了机器人离障碍物多远就开始避让。这个值至少要大于机器人内切圆半径。比如机器人宽40厘米,内切圆半径20厘米,那inflation_radius至少设0.25米。设太小会擦边,设太大会导致狭窄通道过不去。
cost_scaling_factor控制代价衰减速度。值越大,代价随距离衰减越快,机器人越倾向于贴近障碍物;值越小,机器人越保守。室内场景我一般设3-5,室外设2-3。
max_vel_x是最大前进速度。速度越快,避障反应时间越短。如果视觉处理延迟是100毫秒,那速度1米/秒时机器人已经前进了10厘米。所以视觉避障场景下,建议max_vel_x不超过1.5米/秒,除非你的处理延迟能压到50毫秒以内。
注意:调参时每次只改一个参数,改完跑同样的测试场景对比效果。同时改多个参数,出了问题根本不知道是哪个引起的。
5. 系统联调与典型问题排查
5.1 联调流程与验证方法
系统联调我一般分四步走:静态验证、低速动态验证、高速动态验证、边界场景验证。
静态验证是把机器人放在原地,在它前方不同距离、不同角度放障碍物,看RViz里点云和代价地图是否准确。这一步主要验证感知链路。
低速动态验证是让机器人以0.3米/秒的速度朝障碍物前进,看它是否能在安全距离停下或绕开。这一步验证避障触发逻辑。
高速动态验证是把速度提到正常巡航速度,测试避障的响应速度。如果高速时撞上去,说明处理延迟太大或者刹车距离计算有误。
边界场景验证是专门测试那些容易翻车的情况:玻璃墙、黑色吸光物体、强光直射、地面反光等。这些场景下视觉传感器容易失效,需要提前知道系统的能力边界。
5.2 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决思路 |
|---|---|---|---|
| 机器人对障碍物无反应 | 点云话题未订阅 | rostopic hz检查频率 | 检查costmap配置 |
| 频繁误刹车 | 地面点未滤除 | RViz看点云 | 调整地面分割阈值 |
| 避障时左右摇摆 | 代价地图震荡 | 观察代价地图变化 | 增大cost_scaling_factor |
| 狭窄通道过不去 | 膨胀半径过大 | 测量通道宽度 | 减小inflation_radius |
| 高速时撞障碍物 | 处理延迟大 | 打时间戳测延迟 | 降低速度或优化算法 |
| 玻璃墙检测不到 | 深度相机失效 | 换ToF或加超声波 | 多传感器融合 |
5.3 几个让我印象深刻的坑
第一个坑是时间同步。深度相机和底盘控制节点如果时间不同步,点云的时间戳和机器人位姿的时间戳对不上,导致障碍物位置计算错误。解决办法是用message_filters做时间同步,或者确保所有节点都用同一时钟源。
第二个坑是点云坐标系。深度相机输出的点云默认在相机坐标系下,如果忘记转换到底盘坐标系,避障方向会完全错乱。这个问题的隐蔽性在于,RViz里看起来点云位置是对的(因为RViz会自动做tf变换),但避障模块拿到的是原始坐标。所以一定要在点云处理节点里显式做坐标变换。
第三个坑是动态障碍物。视觉避障对静态障碍物效果很好,但对突然冲出来的人或车,反应往往不及。后来我在局部规划器里加了速度障碍法做预测,提前判断动态障碍物的运动趋势,效果改善明显。不过这也增加了计算量,需要权衡。
提示:建议在机器人上装一个小的显示屏或者用远程桌面实时看RViz,联调时能省很多时间。盲调参数效率极低。
6. 性能优化与扩展方向
6.1 算力优化:让嵌入式平台也能跑
视觉避障的计算瓶颈主要在点云处理和局部规划。点云处理方面,降采样是最有效的优化手段。用VoxelGrid滤波器把点云分辨率降到5厘米,点数能减少80%以上,而避障精度损失很小。另外ROI裁剪也很关键,只保留机器人前方一定角度和距离内的点云,其余全部丢弃。
局部规划方面,DWA的计算量主要来自速度采样。默认的采样数是vx_samples×vy_samples×vth_samples,三个方向各采样20个就是8000次模拟。实际使用时可以把vy_samples设为1(大多数差速机器人不需要横向速度),采样数直接降到400次,实时性大幅提升。
# DWA优化配置 DWAPlannerROS: vx_samples: 20 vy_samples: 1 vth_samples: 40 sim_time: 1.5 sim_granularity: 0.05sim_time是模拟时长,设太短避障距离不够,设太长计算量大。1.5秒是个比较平衡的值。sim_granularity是模拟步长,0.05米对应20Hz的模拟频率,再小意义不大。
6.2 多传感器融合的扩展思路
纯视觉避障有天然短板:受光照影响大、测距精度有限、对透明和反光物体无能为力。如果机器人本身有激光雷达或超声波,建议做分层融合。激光雷达负责中远距离的精确测距,视觉负责近距离的语义识别和盲区补充,超声波负责最后一道防撞保护。
融合的层次可以这样设计:激光雷达点云和视觉点云分别投影到代价地图,用不同的marking和clearing参数;超声波单独作为一个RangeSensor接入,只做紧急刹车触发。这样各传感器发挥各自优势,又不会互相干扰。
6.3 从避障到语义避障的升级路径
基础避障只关心“有没有障碍物”,语义避障还要关心“是什么障碍物”。比如前面是个人,机器人应该减速绕行;前面是堵墙,直接绕行就行;前面是玻璃,需要特别小心。实现语义避障需要在点云聚类之后加一个分类模块,可以用轻量级神经网络对障碍物点云做分类。
这一步的计算量比基础避障大不少,建议在Jetson Orin这个级别的平台上做。如果算力不够,可以退而求其次,用尺寸和位置启发式规则做简单分类:比如高度在1.5-2米、宽度在0.3-0.6米的障碍物大概率是人,需要更保守的避让策略。
我在实际项目里试过用PointNet做障碍物分类,在Jetson Xavier上单帧推理大概30毫秒,加上点云预处理和后续规划,整体延迟能控制在100毫秒以内,满足低速机器人的避障需求。如果速度要求更高,就需要更轻量的网络或者模型剪枝了。
最后分享一个我在部署过程中总结的小技巧:把避障参数做成动态可调的。用dynamic_reconfigure把关键参数暴露出来,联调时可以在RViz里实时拖动滑块看效果,比反复改配置文件重启节点效率高得多。这个习惯让我在多个项目里节省了大量调试时间。