做移动机器人避障,我前后折腾了快两个月才算把整条链路跑通。最开始图省事,直接用单线激光雷达,成本低、接口也简单,可真正上车跑起来才发现问题很尴尬——只能扫到某个高度平面上的东西,桌子腿看到了,桌面上探出来的板子、低矮台阶、斜着伸过来的推车把手全部发现不了。后来换成奥比中光的dabai相机,配合ROS从点云处理一路做到避障算法集成,踩了数不清的坑,也沉淀了不少可复用的经验。今天就把这套实战流程完整拆开讲,从驱动配置、点云滤波、障碍物检测到和导航栈联动,每个环节都给到可以直接照做的方案和参数。如果你正打算用dabai或其他深度相机做机器人的感知避障,这篇文章应该能帮你少走一半弯路。
这套方案面向的场景很明确:室内移动底盘、服务机器人、轻量级AMR这一类,需要在动态环境里识别前方的障碍物并实时避让。会涉及一点Linux基础、ROS基本概念和PCL点云库的使用,但我会把每个命令和参数的意义都解释清楚,纯新手跟着走基本也能跑起来,老手直接跳到自己关心的章节就行。
1. 项目背景与方案选型
1.1 为什么选dabai相机做避障感知
在定下奥比中光dabai之前,我把市面主流的深度相机都摸了一遍。Intel RealSense D435确实是老牌选择,驱动成熟、资料多,但价格高出一截;Kinect系列虽然便宜,体积大而且停产多年,还得考虑供电问题,装在小车上一股试验台的味道。dabai系列的定位很讨巧:它是主动结构光方案的深度相机,能直接给出对齐后的彩色图和深度图,再转成点云,室内近距离的深度精度在厘米级别,对这个级别的避障任务完全够用。
还有一个很现实的因素是成本。做项目预算卡得紧的时候,dabai的性价比明显更合适。踩过的坑是结构光方案在户外强光环境会失效,正午阳光直射下深度图会出现大量空洞,所以这套方案更适合室内或者光线可控的场景。如果是室外机器人,优先考虑双目主动立体或者激光雷达,别在结构光上硬磕。
另一个让我决定用dabai的原因是它的ROS支持。官方提供Linux SDK和ROS1/ROS2驱动包,能直接把深度图、彩色图、点云以标准话题发出来,这就省去了自己写底层USB驱动的巨大工作量。要知道SDK和驱动适配才是这类相机最耗时间的部分,这部分省下来的精力,足够我把滤波和避障逻辑打磨几轮。
1.2 整体数据流与软件架构
整个系统跑起来之后,数据是沿着一条很清晰的流水线走的。dabai相机通过USB3.0接到工控机,驱动节点把深度图和彩色图发到ROS话题上,随后ORBBEC的SDK内部会生成点云并发布。我这边拿到原始点云之后不直接送给规划模块,而是先做一轮滤波处理,把视野范围限制在机器人前方真正关心的区域,同时大幅降低点云点数,保证下游节点计算不卡顿。
滤波后的干净点云进入障碍物检测节点,这一步我用的是欧式聚类,把物理上连成一片的点云分割成一个个独立障碍物,然后给每个障碍物生成一个包围框。接着,这些障碍物信息以两种形式进入避障链路:一种是转成2D激光扫描话题,喂给move_base的costmap障碍物层;另一种是以可视化Marker发出来,方便在RViz里直观确认检测效果。
整体架构可以表达成下面这条链路:
dabai相机 -> 驱动节点 -> /camera/depth/points(原始点云) -> 直通滤波 -> 体素滤波 -> 离群点剔除 -> /filtered_points -> 欧式聚类 -> 障碍物包围框 -> costmap obstacle_layer / move_base 避障规划这套架构的好处是每个节点之间职责分明,随便替换任何一个环节都不会影响整体。比如今天不想用聚类,可以直接把点云投成LaserScan接到costmap;以后想融合激光雷达,也只需要在costmap里多加一个observation source,代码都不用动。项目的核心价值不在于某一个算法有多炫,而是形成了一条稳定、可调试、可扩展的感知到决策通路。
2. 环境搭建与驱动配置
2.1 ROS版本选型与一键搭建环境
dabai相机在ROS1和ROS2下都有驱动包,选哪个取决于你已有的项目基础。我这次用的是Ubuntu 20.04加ROS Noetic,主要原因是团队原本的导航代码基于ROS1写的,迁移成本小。如果你是完全新开项目,我更推荐直接上Ubuntu 22.04加ROS 2 Humble,毕竟ROS1已经停止维护,新项目没必要押注老生态。
ROS本身的安装是个劝退很多新手的环节,也是最容易卡住的点。我见过太多人卡在装ROS这一步就放弃了。现在已经有大佬整理了一键安装脚本,比如鱼香ROS的一键安装,基本上把ROS1、ROS2各版本、依赖组件、环境变量配置全部封装好了,跑完脚本之后reboot一下,就能直接用。对新手来说非常友好,省去了手动配源、逐个装包、配置环境变量的痛苦。
当然,如果你想在这个过程中顺便把ROS基础补扎实,也可以跟着赵虚左的ROS教程文档一步一步来,他的文档对概念讲解很清楚,特别是工作空间、功能包、话题通信这些核心概念,值得花几天过一遍。装完之后一定要先验证ROS能否正常使用:
roscore新开一个终端执行这行命令,能正常输出日志不报错,说明ROS基础环境没有问题。有ROS1基础的人可能觉得这句话多余,但每年都有新手因为环境变量没source,导致后面所有launch文件都起不来。
2.2 dabai相机驱动编译与标定经验
驱动编译是另一个容易出幺蛾子的环节。dabai官方SDK发布形式通常是源码包加ROS wrapper,整体流程是:
# 创建工作空间 mkdir -p ~/dabai_ws/src cd ~/dabai_ws/src # 拉取SDK与ROS驱动源码(以官方仓库为准) git clone <官方SDK仓库地址> git clone <官方ROS wrapper仓库地址> cd ~/dabai_ws catkin_make source devel/setup.bash编译过程中最常见的坑是依赖缺失。比如PCL、Eigen、OpenCV这些库没装全,编译到一半报头文件找不到。遇到这种问题,不要慌,缺什么装什么:
sudo apt install libpcl-dev libeigen3-dev libopencv-dev编译通过后还有一个很关键但容易忽略的步骤:配置USB设备权限。Linux下访问USB设备默认需要root权限,如果每次启动节点都要sudo,那也太难受了。官方SDK一般会提供udev规则文件,把它复制到/etc/udev/rules.d/目录下,然后reload规则并重新插拔USB,当前用户就能直接访问相机:
sudo cp <sdk目录>/misc/99-orbbec-usb.rules /etc/udev/rules.d/ sudo udevadm control --reload-rules设备权限搞定之后,先启动一下驱动节点,确认话题能正常发出来。官方launch文件通常会一次性启动相机和点云生成节点:
roslaunch orbbec_camera dabai.launch启动成功后,查看话题列表,常见的核心话题有这几个:
/camera/depth/image_raw 深度图 /camera/depth/points 点云 /camera/color/image_raw 彩色图 /camera/color/camera_info 相机内参接下来就是标定。很多人会忽略这一步直接开跑,结果后续避障总感觉位置偏,回头排查半天发现是标定错了。dabai出厂时内参标定基本可用,但你装在车上的安装角度、高度以及相机本身的畸变,都会影响外参精度。我强烈建议至少做一次标定,方法很简单:打印一张棋盘格,用ROS自带的camera_calibration工具录制十几个不同角度的图像,运行标定节点后角点识别的标定板画面里四个角都变绿了,点CALIBRATE等它计算完,最后SAVE和COMMIT写入标定参数。
实测下来标定一次大概需要二十分钟,但这二十分钟非常值。没有正确内外参的情况下,你后面做的所有点云测量都是带误差的,避障算法再好也白搭。
3. 点云处理:从原始点云到可用障碍信息
3.1 读懂点云话题和TF关系
dabai发出的原始点云话题是sensor_msgs/PointCloud2类型,里面每一个点都带三维坐标(x, y, z)。但是拿到点云之后,第一个要搞清楚的问题不是点有多少,而是这些坐标是相对于哪个坐标系说的。这个问题直接决定了后面所有避障判断对不对。
相机发布点云时,坐标是在相机自己的光学坐标系camera_link或camera_color_optical_frame下的。而机器人避障更关心的是障碍物相对于车体base_link的位置。这两者之间必须靠TF变换关联起来。启动驱动后,输入:
rosrun tf view_frames会生成一个TF树,能直观看到base_link到camera_link的动态变换是否连接正确。如果看到相机帧是个孤立节点,或者根本没出现在树里,那说明缺了静态变换:
rosrun tf2_ros static_transform_publisher x y z yaw pitch roll base_link camera_link三个平移量和三个旋转角要根据你实际安装的位置量好填进去。这里务必拿尺子量准确,别目测估一个。坐标偏差一两厘米在点云上不明显,但反映到机器人末端执行器或底盘运动上就是明显的定位偏移。
3.2 核心滤波链路:直通滤波、体素滤波、离群点剔除
原始点云直接送给避障算法是不现实的,dabai一个视野内的原始点云轻松上万甚至几十万点,下游聚类和costmap更新根本扛不住,延迟一上来避障就成了摆设。我实际使用中,链路里固定了三个滤波步骤,每一步的目的和处理结果都不一样。
第一步是直通滤波。它的作用简单粗暴:只保留相机前方我们要关注的立体空间。对室内底盘机器人来说,太远的点对避障没有意义,反而引入噪声;太高和太低的点要么是天花板要么是地面,同样应该丢掉。实操中我直接用pcl_ros提供好的工具,不用写一行代码:
# 保留相机前方0.2m到3.0m的点 rosrun pcl_ros passthrough input:=/camera/depth/points output:=/filtered/z filter_field_name:="z" filter_limit_min:=0.2 filter_limit_max:=3.0 # 保留左右各0.8m范围内的点,限制机器人的通行宽度 rosrun pcl_ros passthrough input:=/filtered/z output:=/filtered/zy filter_field_name:="y" filter_limit_min:=-0.8 filter_limit_max:=0.8 # 保留高度0.05m到0.8m之间的点,去掉地面和过高区域 rosrun pcl_ros passthrough input:=/filtered/zy output:=/filtered/region filter_field_name:="z" filter_limit_min:=0.05 filter_limit_max:=0.8注意一个细节:PassThrough一次只能处理一个字段,所以多个维度限制要串联多次执行,别想着一个节点搞定三个方向。这个链条跑完之后,点云量大约能减掉七成,而且剩下的大部分都是机器人前进方向上真正需要避开的障碍物表面。
第二步是体素滤波下采样。经过直通滤波后点云还是密,特别是一面近处的墙可能密密麻麻全是点。体素滤波的原理是把空间划分成一个个小立方体,每个立方体只保留一个重心点。这样点云密度大幅下降但几何形状基本保留。我实际设置的体素大小是2厘米:
rosrun pcl_ros voxel_grid input:=/filtered/region output:=/filtered/voxel leaf_size:=0.02 0.02 0.022厘米的体素对室内避障来说已经足够精确,既不会丢失桌腿之类的细节,又能把点云量控制在聚类算法能轻松处理的量级。如果机器人算力比较弱,可以放宽到3厘米;高动态场景也可以调更小,代价是CPU占用上升。
第三步是统计离群点剔除。深度相机难免产生一些孤立的飞点,尤其反光物体边缘,会突然冒出几个坐标离谱的点。统计滤波的思路是计算每个点到邻居的平均距离,距离超出统计阈值的点被判定为噪声剔除。这一步我直接写在了一个C++节点里,和后面的聚类节点放在一起,避免多一层话题转发的延迟。
以上三步做完,点云从原来的几万点变成几千点,而且干净稳定,这时候再进避障算法,实时性才真正有保障。整套滤波链路全部用命令行和轻量工具就能搭起来,强烈建议新手先这样跑通,再考虑把代码整合成统一节点。
3.3 点云可视化与调试手段
滤波参数调得好不好,别靠猜,直接在RViz里看效果最直观。启动RViz后,把Fixed Frame设成camera_link或者刚才配好的base_link,然后用PointCloud2显示插件订阅处理后的点云话题,颜色显示方式选RGB8或者Intensity都行。
这里有个调试小习惯我要多说一句:每次改滤波参数之前,先录一段rosbag,而不是开着车实时调试。因为机器人在动的时候,点云每帧都在变,你根本分不清某个奇怪现象是参数问题、相机抖动还是外部环境干扰。我的做法是固定摆几个障碍物在机器人前方,录一段两三分钟的数据:
rosbag record -O test_scene.bag /camera/depth/points /camera/depth/image_raw /camera/color/image_raw /camera/color/camera_info /tf /tf_static然后在回放模式下慢慢调参数。这个习惯帮我省了大量时间,遇到诡异问题也可以反复重放同一段数据定位原因。一定要养成“先录包、再调参”的调试习惯,否则很多问题都是玄学。
另一个必要的监控命令是看话题频率:
rostopic hz /camera/depth/points rostopic hz /filtered/voxeldabai点云话题如果低于10Hz,就要检查USB供电和带宽了。滤波后的点云频率勉强能维持10~15Hz,如果再低说明体素叶子设得太小或者CPU不够,需要适当放宽滤波参数。
4. 避障算法集成:把点云变成移动规划能用的东西
4.1 点云接入costmap的三种主流方式
点云处理完只是拿到了“干净的障碍物表达”,真正要让机器人躲开这些东西,还需要把信息转成move_base/Nav2能理解的数据。我在实际项目里测试过三种方式,各有优劣,说下适用场景。
第一种是最省事的:把3D点云投影转换成2D的sensor_msgs/LaserScan,然后直接接入costmap。具体做法是把点云按角度网格化,每个角度的障碍物距离取这个角度扇形区域内最近的点。这样做的好处是通用性极强,所有基于激光雷达的导航栈都能无缝使用,ROS生态里甚至还有现成的pointcloud_to_laserscan功能包。缺点是点云的高度信息全丢了,比如一个悬在半空中的障碍物,投影后和地面障碍物没区别,在某些场景下反而会误报。
第二种是相对精细的做法:先做欧式聚类,把点云中的障碍物一个个分割出来,然后用聚类后的包围框信息更新costmap。这样能保留每个障碍物的体积信息,对动态避障更友好。这也是我最终选择的方案,下面一节会详细讲实现。
第三种是直接用3D体素层,也就是voxel_costmap,把滤波后的点云直接灌进costmap的obstacle层。这种方式最真实,保留了全部三维信息,导航规划也最精确,但代价是计算量大,对算力要求高。如果工控机性能普通,还是不建议上来就搞3D costmap,很容易CPU拉满。
实际项目中我建议从第一种方案快速跑通整个导航链路,然后再平滑升级到第二种方案。这样既能在早期快速验证相机和车体本身没有问题,又能逐步把感知能力做细,不会一上来就被一堆同时出现的问题淹没。
4.2 基于欧式聚类的障碍物检测实现
我最终落地的障碍物检测节点,核心算法是PCL的欧式聚类。这个算法的逻辑很直观:点云里的点如果距离足够近,就认为它们属于同一个物体,聚类过程不断把临近点合并成一个簇。关键的参数只有一个,就是聚类容差,单位是米,表示相邻两个点之间距离达到多少以内才算同一个簇。
核心代码骨架大致是这样:
// 建立KD-Tree加速近邻搜索 pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZ>()); pcl::EuclideanClusterExtraction<pcl::PointXYZ> ec; ec.setClusterTolerance(0.08); // 聚类容差,室内场景我常用0.05~0.1m ec.setMinClusterSize(30); // 小于30个点的簇视为噪声 ec.setMaxClusterSize(10000); // 防止超大簇拖慢处理 ec.setSearchMethod(tree); ec.setInputCloud(filtered_cloud); ec.extract(clusters); // 输出多个cluster for (auto& cluster : clusters) { // 把一个cluster点云分离出来,计算最小包围盒 pcl::PointCloud<pcl::PointXYZ>::Ptr cluster_cloud(new pcl::PointCloud<pcl::PointXYZ>()); for (auto& idx : cluster.indices) { cluster_cloud->points.push_back(filtered_cloud->points[idx]); } Eigen::Vector4f min_pt, max_pt; pcl::getMinMax3D(*cluster_cloud, min_pt, max_pt); // 这里根据min_pt/max_pt生成障碍物BoundingBox // 同时可以计算每个簇的中心点、尺寸、距离等信息 }聚类容差这个参数需要根据障碍物类型和相机安装高度去调。室内测试时,两只并排纸箱之间的距离大概十几厘米,如果容差设得太大,两个独立箱子会被误判成一个障碍物,机器人可能找不到穿过的缝隙;容差设太小,一个箱子的两个面又会被拆成好几个簇,造成避障抖动。我一般在0.05到0.1米之间试,先跑rosbag回放,看检测结果是否稳定。
除了聚类本身,还要做两级过滤。第一级按点数过滤,一个稳定的障碍物不可能只有二三十个点,那基本是噪声或极远处的细枝末节,直接丢弃。第二级按包围盒尺寸过滤,针对场景先定一个最小障碍物尺寸,比如长宽都小于5厘米的对象直接忽略,这样能有效过滤掉地面上的小石子或其他环境杂物。
聚类结果我用visualization_msgs/Marker以立方体框的形式发布出来,这样在RViz里一眼就能看到每个障碍物的检测位置和大小,调试非常直观。实际联调时,这一层Marker显示出的检测框是否稳定贴着障碍物,是判断前面所有滤波参数是否合理的直观标准。
4.3 与move_base/Nav2联调的参数经验
检测到障碍物后,最后一步是让导航栈真正用上这些信息。ROS1的move_base和ROS2的Nav2核心思路一致,都是通过costmap把地图、传感器障碍物、机器人轮廓放在一起,然后用规划算法寻找安全路径。costmap的配置决定了避障是太敏感还是太迟钝,这里直接给一份我调过的costmap配置:
obstacle_range: 3.0 # 3米外的障碍物不标记 raytrace_range: 3.5 # 3.5米外的障碍物不清理 footprint: [[0.25, 0.0], [-0.25, -0.15], [-0.25, 0.15]] # 机器人足迹多边形 inflation_radius: 0.25 # 障碍物膨胀半径 cost_scaling_factor: 3.0 # 代价衰减速度 observation_sources: point_cloud point_cloud: data_type: PointCloud2 topic: /filtered/voxel marking: true clearing: true min_obstacle_height: 0.05 max_obstacle_height: 1.0inflation_radius是最需要反复调的一个参数。它决定了机器人离障碍物要保持安全距离,设置太大,机器人过窄门洞的时候会觉得两边都是障碍物,直接认为通道不可通行;设置太小,机器人贴着障碍物走,容易蹭到甚至撞上。我的经验是先设成机器人宽度的1.2倍左右,然后在实际测试里慢慢缩小,直到找到一个“能通过又不危险”的平衡点。
还有一个非常关键的细节是max_obstacle_height。dabai相机安装高度一高,桌面上方悬空的箱子也会落进costmap里,导致机器人明明可以从底下钻过去却被拦住了。限制这个参数后,悬空障碍物只参与聚类显示,不参与2D避障,室内的通行能力提升明显。
如果你用的不是move_base而是Nav2,配置思路几乎一样,只是yaml文件组织方式略有差异,核心的obstacle layer和inflation layer参数是共通的。ROS2环境下还要额外注意使用ros2 param dump导出完整参数,再对着改动,不然有些默认值藏得比较深,很难查。
5. 实战中的坑与排查实录
5.1 点云异常类问题
先说点云本身的各种奇怪表现。最常见的一种是点云大面积空洞或NaN。打开RViz后发现某个区域黑漆漆一片,甚至整个视野都没数据,多数情况不是算法问题,而是相机没拿到足够数据。先看深度图/camera/depth/image_raw,如果深度图本身就有大量黑色区域,那说明是相机端问题。光线不足、反光物体、过近的物体都会导致结构光深度解算失败,这时候适当补光或者调整相机曝光参数会有明显改善。
另一种情况是点云一边正常一边缺失,或者整体偏色。这通常是USB带宽不够或者供电不稳。dabai这类深度相机数据量很大,必须插在USB3.0口上,插到2.0口上虽然能认到设备,但点云传输会断断续续。要是分线器供电不足,更常见的是跑几分钟后相机掉线,此时dmesg会看到一堆USB错误日志。解决方法是优先直连主板USB口,必要时用带外接供电的集线器。
还有一类问题只会在长时间运行后出现:点云逐渐漂移,横向一个物体越看越歪。这个大概率是相机发热导致深度标定参数轻微漂移,结构光相机都有这个毛病。我的经验是开机后先跑个十分钟让相机热稳定,再开始正式运行,漂移会小很多。如果对精度要求极高,就要考虑更高阶的深度相机方案了。
5.2 避障误检与漏检类问题
避障效果“时灵时不灵”是另一个高频问题。最常见的是误检:机器人正前方明明空无一物,costmap里却冒出一个障碍块。打开RViz看一眼聚类Marker就会发现,往往是地面上的一小片反光点、或者远处墙角边缘的几个飞点被聚类成了障碍。解决办法就是在滤波阶段把离群点剔除做狠一点,同时调大聚类的最小点数,让一两团“孤魂野鬼”没资格成为障碍物。
漏检问题同样烦人。黑色物体、吸光材质的椅子脚、玻璃桌面这类东西,在深度图里几乎不出点,聚类自然无从谈起。这种情况想纯靠点云解决不太现实,我的建议是不要迷信单一传感器,在底盘前方加一路超声波或者低线束激光做盲区补充,用多传感器融合兜底。点云负责精细避障,超声波负责近距离的“底线防守”,两者配合才能覆盖绝大多数真实场景。
还有一个很有迷惑性的漏检场景:机器人通过两把椅子之间的缝隙时,明明缝隙宽度足够,规划却绕路。不是漏检,是聚类把两把椅子连成了一整个障碍。这种问题基本就是聚类容差太大导致的,把clusterTolerance调小一点同时适当降低体素滤波叶子大小,两个对象之间的边界就会清晰起来,缝隙自然能被发现。
5.3 我踩过的几个隐蔽大坑
最后分享几个比较隐蔽、但一旦踩中会让你排查到崩溃的坑。
第一个坑是相机安装的位置和角度。很多人安装相机时光顾着美观,把相机放在车头正中央水平朝前,结果车开起来之后,车头正前方一米以内完全是盲区,柜子腿都到脸上了costmap还没反应。深度相机都有一个最小工作距离,太近的物体是测不到的。我的经验是相机安装在距地面20到40厘米高度,且略微向下俯视5到10度,这样既能覆盖车前近距离的路面,又能看到远处障碍物。安装完先在地面放个纸箱测一下最近检出距离,再决定俯仰角。
第二个坑是分布式系统里传点云。如果你用的是主从机方案,把点云topic从相机机转发到导航机,几个G的带宽很快就会被点云挤爆。我一开始没注意这个问题,导致避障延迟高得离谱,后来改成在采集端就把滤波做掉,只把降采样后的点云或者LaserScan发到主控端,带宽占用立刻降了一个数量级,实时性也上来了。点云这类大流量话题,一般只建议在同一台机器上走,跨机转发明智地做压缩或降级。
第三个坑是回放rosbag时的TF时间戳问题。调试时回放老包,经常遇到“TF_OLD_DATA”的刷屏警告,导航止步不前。这是因为录制时的TF历史时间戳和当前系统时间对不上,解决方案有两个:要么在回放时用rosbag play --clock配合use_sim_time=true,让系统时间跟随bag时间;要么在launch文件里设置tf_buffer_time一个足够大的缓冲值。我用的是前一种,配合参数设置后基本稳定。
第四个坑是把raytrace_range和obstacle_range理解反了。obstacle_range是传感器能检测到并标记障碍物的最远距离,raytrace_range是传感器能清除已标记障碍物的最远距离。如果raytrace_range小于obstacle_range,就可能出现这种情况:远端标记的障碍物永远清不掉,机器人走到那里才发现前面其实空无一物。两个参数要保持raytrace_range略大于obstacle_range,清障范围才能覆盖标记范围。
真要说这条链路里最容易被低估的环节,我反而觉得是前面那些看起来“不高级”的基础工作。避障算法本身不算难,难的是让一坨点云稳定、干净、准时地出现在规划模块面前。我现在每拿到一台新相机,第一件事不是调算法,而是做三件“笨事”:拿棋盘格标定、看TF树、录几分钟rosbag反复回放。把这三件基础工作做扎实,后面的事情基本就顺了。如果你也在折腾dabai或者其他深度相机的避障集成,建议也先做这三件事,遇到任何奇怪问题,欢迎在评论区一起交流。