拿到一台激光雷达之后:从连上线到用起点云的完整路径
- 按仓库文档的真实顺序走通「确认通信方式 → 编译 → 跑示例 → 接 ROS → rviz 看第一眼」
- 知道点云到手之后该做的第一批处理:外参变换、范围裁剪、体素降采样、离群点与自身点剔除
- 认得出新手最常撞的那几堵墙:网段没配对、串口模式的鸡生蛋、坐标系搞混、强度当颜色用
雷达到手,盒子拆开,一根线,一台机器人,一台装着 Ubuntu 的开发机。你在网上看过无数张漂亮的点云图,蓝紫渐变的走廊、纤毫毕现的桌椅。你觉得离那张图只差一条命令。
然后你在第一步就卡了一下午——终端里的示例程序打印完初始化信息,就再也没有下文了。没有报错,没有超时提示,就是安静地卡在那儿。
这种卡壳几乎是每个人的必经之路,而且原因往往低级到让人生气:网卡不在同一个网段上。
这篇不讲选型、不做对比,只走一条路:从线插上到点云能用,中间每一步该做什么、每一步为什么这么做、哪一步最容易翻车。
先把边界交代清楚:本文基于 unilidar_sdk2 与 unilidar_sdk 两个公开仓库的文档与代码结构写成,没有真机验证。 涉及具体操作的地方,我只复述仓库 README 里写了的内容;涉及通用工程经验的地方,我会明确说这是通用经验。测距表现、点云密度、实际延迟这类必须接上硬件才能知道的事,一概不写。
第零步:先搞清楚它打算用什么线跟你说话
大部分传感器教程的第一句是「安装驱动」。激光雷达这里不一样,第一句应该是:先确认这台雷达当前处在哪种通信模式。
unilidar_sdk2 的 README 里,雷达的工作模式被设计成一个 uint32_t 整数,每一位控制一项开关。SDK 头文件里暴露的接口是:
virtual void setLidarWorkMode(uint32_t mode) = 0;
这个位域里有一位专管通信方式:第 3 位为 0 是网口模式,为 1 是串口模式。 其余各位管的是视场模式、二维/三维测量、IMU 开关、上电是否自动开始旋转——完整的位表和两代 SDK 之间的接口演进,我在两代雷达 SDK 对着读那篇里摊开讲过,这里只关心跟「怎么连上」有关的那一位。
README 直接给了新手需要的那句话:常规用法是其余各位全为 0,你唯一要决定的是走网口还是走串口——网口填 0,串口填 8(也就是把第 3 位置 1)。出厂默认值是 0,即网口模式。
记住「出厂默认是网口」这件事,它决定了后面一个很别扭的操作顺序。
网口模式:把网卡配到雷达认识的网段上
按 README 的说法,网口模式的示例程序是 example_lidar_udp.cpp。跑它之前有一个前置动作:
用网线把雷达连到电脑上,然后确认电脑对应的那块网卡,被配置到了雷达的默认目标 IP 地址:
192.168.1.2
这一行是整篇 README 里最容易被跳过、也最容易让人卡一下午的一行。
我把它翻译成人话。雷达是个 UDP 发送方,它把数据包往一个固定的目标地址上打。这个目标地址出厂时被设成了 192.168.1.2。你的电脑如果不是这个地址,那些包发出去就没人接——雷达那边毫不知情,照转照发,你这边一片安静。
所以你要做的不是「给电脑随便配个静态 IP」,而是把网卡配成雷达要发给的那个地址本身。这是有线直连传感器时的通用做法:不走路由器、不走 DHCP,两端手工约定地址。
顺带一说,很多人这里会顺手 ping 192.168.1.2 想验证连通性,结果 ping 的其实是自己。雷达自己的 IP 和它的目标 IP 是两回事,ping 不通也未必说明有问题。UDP 单向推送的设备,用 ping 来判断死活并不可靠,真正的验证方式是跑示例程序或者直接抓包。
网卡配好之后,跑示例:
../bin/example_lidar_udp
README 里贴了这个程序的输出样式:初始化成功、设置工作模式、打印硬件与固件版本、停转再启转、报告一个脏污百分比和时间延迟,然后开始交替刷出 IMU 消息和点云消息。IMU 消息里有四元数、角速度、线加速度三组字段;点云消息里带帧时间戳、帧号,以及每个点的六个字段。
那些具体数值我不引用——它们是某一次运行的采样,不是产品指标。但输出的结构本身值得看:一次运行里 IMU 消息的条数明显多于点云消息,这直观地告诉你 IMU 的更新频率远高于点云帧率。这个频率差是后面所有惯性融合算法能成立的前提。
还有一个细节:程序会打印「dirty percentage」,一个脏污百分比。厂商愿意把光窗脏污程度作为一个常规输出项暴露出来,说明这是现场会真实发生的问题。 激光雷达的光窗上落灰、沾水汽、贴上指纹,点云质量会实实在在下降。这个数字的用途很实在:你的程序可以监控它,超过阈值就提醒人去擦一擦。
如果要改雷达的默认 IP,README 说可以参考用户手册用上位机改;仓库的示例目录里也直接给了一个 set_ip_address.cpp。多台雷达接在同一网段时,这个程序就是你的救命稻草。
串口模式:一个必须绕一圈的鸡生蛋
串口模式的示例是 example_lidar_serial.cpp,默认串口名 README 里也写了:
/dev/ttyACM0
但在跑它之前,README 第 3.4 节先给了一段很关键的提醒,我认为它值得单独拎出来讲,因为这是个典型的自举陷阱:
雷达出厂是网口模式。你想用串口,就得先把它切到串口模式。而切模式这个动作本身,得通过当前生效的通信方式下达——也就是说,你必须先用网口连上它,把工作模式设成 8,然后断电重启,它才会以串口模式启动。
只有网线?那你连不上。只有串口线?那你也切不过去。
README 给了两条出路:用官方上位机软件切换,或者用网口跑一次 example_lidar_udp,在程序里把工作模式设成 8。而仓库示例目录里那两个专门的工具——set_to_serial_mode.cpp 和 set_to_udp_mode.cpp——把这件事做成了两个可以直接编译运行的命令。
我很喜欢这个设计,因为它解开的是一类很常见的死结:凡是「请使用我们的上位机软件完成 XX」的操作,在自动化产线和无 GUI 的开发机上都是墙。 一个愿意把配置动作做成命令行可执行文件的 SDK,说明作者预设了你会把它装到真实设备上批量跑,而不只是在办公室里演示一次。
顺手记一条通用经验:串口设备在 Linux 上第一次跑,八成会因为当前用户不在串口设备的用户组里而报权限错误。这跟雷达无关,是所有串口设备的通病,遇到别急着怀疑硬件。
编译:一个标准得没什么可说的 CMake 工程
C++ SDK 的编译方式,README 里就四行:
cd unitree_lidar_sdk
mkdir build
cd build
cmake .. && make -j2
没有自定义脚本、没有一堆环境变量、没有要求你先装十个依赖——一个标准 CMake 工程。这本身就是好事:越标准的构建方式,你把它塞进自己项目的成本越低。
编译产物是几个示例可执行文件,落在 bin/ 下。把示例目录列出来,正好是一张「这颗雷达能做什么」的清单:
unitree_lidar_sdk/examples/
example_lidar_udp.cpp —— 网口模式取数据
example_lidar_serial.cpp —— 串口模式取数据
set_ip_address.cpp —— 改雷达 IP
set_to_serial_mode.cpp —— 切到串口模式
set_to_udp_mode.cpp —— 切回网口模式
两个「读数据」,三个「改配置」。配置类工具占了五分之三,这个比例本身就在提示你:真实项目里,你花在「让它按你要的方式工作」上的时间,不比花在「读它的数据」上少。
头文件目录也值得扫一眼:
unitree_lidar_sdk/include/
unitree_lidar_sdk.h —— 主接口
unitree_lidar_sdk_config.h —— 配置
unitree_lidar_sdk_pcl.h —— 与 PCL 的对接
unitree_lidar_protocol.h —— 通信协议
unitree_lidar_utilities.h —— 工具
udp_handler.h —— UDP 收发
example.h
unitree_lidar_sdk_pcl.h 的存在说明官方已经帮你把「SDK 内部的点云结构」和「PCL 的点云类型」之间的转换写好了。这是省事的一环——PCL 是 C++ 世界里点云处理的事实标准库,能直接转成 PCL 类型,意味着几百个现成算法你都能直接用。
而 unitree_lidar_protocol.h 和 unitree_lidar_utilities.h 这一对,README 第 6 节说得明白:如果你想自己解析原始的网口或串口报文,参考用户手册和这两个头文件。这是一条退路条款——万一预编译的静态库在你的架构上用不了,你还能自己撸一个解析器。
接进 ROS:从此你不用再关心它是网口还是串口
如果你的项目本来就跑在 ROS 生态里,那前面那些 UDP、串口的细节可以整个交给驱动包吃掉。
仓库顶层是三段式的:原生 C++ SDK 一份、ROS 包一份、ROS2 包一份。README 里给的验证环境是 Ubuntu 20.04、PCL-1.10,ROS 侧是 noetic、ROS2 侧是 foxy(以官方仓库当前文档为准)。
ROS 包的用法:
cd unitree_lidar_ros
catkin_make
source devel/setup.bash
roslaunch unitree_lidar_ros run.launch
ROS2 包的用法:
colcon build
source install/setup.bash
ros2 launch unitree_lidar_ros2 launch.py
两个包发出来的话题名和坐标系名是一致的:
- 点云话题
unilidar/cloud,坐标系名unilidar_lidar - IMU 话题
unilidar/imu,坐标系名unilidar_imu
想改通信方式或者话题名,改配置文件即可。这里两个包的位置不一样,容易找错:ROS 版在 unitree_lidar_ros/src/unitree_lidar_ros/config/config.yaml,ROS2 版则直接写在 unitree_lidar_ros2/src/unitree_lidar_ros2/launch/launch.py 里。前者是独立的 yaml,后者是 Python 脚本里的参数——这是 ROS 1 和 ROS 2 各自的习惯,不是谁做错了。
README 只给了话题名和坐标系名,没写消息类型。按 ROS 生态的通行做法,点云走 sensor_msgs/PointCloud2、IMU 走 sensor_msgs/Imu,包的依赖里也确实列了 PCL——但具体字段布局请以节点源码为准,因为带 ring 和逐点 time 的点云,各家在 PointCloud2 里的字段命名并不完全统一,这一点后面还会咬你一口。ROS 的话题、消息、坐标系这套模型如果还不熟,可以先补机器人操作系统 ROS 那篇。
顺带一提,ROS 包的 launch 目录里有两个文件:run.launch 和 run_without_rviz.launch。后者的用途很明确——当你把这个驱动接到别的算法节点后面时,你不想每次都弹一个 rviz 窗口出来。 一个驱动包同时提供带可视化和不带可视化两个入口,是它被设计成「会被别人调用」而不只是「被人手工运行」的证据。
第一步永远是:在 rviz 里看一眼
程序跑通了,话题有数据了。下一步做什么?
先看,不要先算。
我见过太多人跳过这一步,直接把点云接进 SLAM 算法,然后花三天调参数,最后发现雷达装反了。ROS 包默认就带 rviz 配置(rviz/view.rviz),launch 起来就有图——官方把这一步的成本降到了零,你没有理由跳过。
看的时候,眼睛要专门去找这几件事:
一、Fixed Frame 对不对。 rviz 里如果 Fixed Frame 设的不是 unilidar_lidar,而 TF 树里又没有对应的变换,你会看到一个空白窗口加一句红字警告。这是新手在 rviz 里遇到的头号问题,跟雷达本身没有半点关系。
二、地面在哪。 一颗装在机器人身上的雷达,点云里应该有一大片近似水平的点——那是地面。如果这片"地面"是斜的,要么雷达装歪了,要么你以为的安装角度和实际差了几度。这个偏差在 rviz 里一眼可见,而在算法里会表现为「建出来的图慢慢往一边塌」,排查成本差着两个数量级。
三、有没有一圈近距离的密集点。 雷达装在机器人身上,机身、支架、云台、线束都可能进到视场里。这些点在 rviz 里表现为紧贴原点的一圈稠密"毛刺",它们是彻头彻尾的噪声,而且是每帧都在同一位置出现的噪声——对配准算法的毒性极大,因为算法会认为「这一坨结构完全没动」,从而低估你的运动。
四、有没有大块的扇形空洞。 某个方向完全没有点,通常意味着被机器人自身结构挡住了。挡住就是挡住,算法解决不了,只能改安装位置。趁早发现,能省掉后面所有的困惑。
这四件事,看图三十秒,比读三天日志有效。
点云到手之后,第一批该做的处理
看过图,确认雷达装得没问题,才轮到处理。
README 里贴的示例输出显示,每个点带的是这六个字段:
(x, y, z, intensity, time, ring)
三维坐标、强度、逐点时间偏移、线号。关于这六个字段各自的含义、以及为什么 time 字段决定了点云正不正,两代 SDK 那篇里已经讲透了,这里我只强调结论和它对你实操的约束:
载体在动的时候,一帧点云里的点根本不是在同一个位置采到的。 雷达扫完一圈需要时间,这段时间里机器人已经往前挪了。不做运动补偿,直墙会变弯、走廊两侧会不平行。补偿的办法是用高频的 IMU 数据把每个点按它自己的 time 反推回帧起始时刻——这也是这颗雷达内置 IMU、README 花大篇幅给出两个坐标系之间齐次变换矩阵 T_LI 的根本原因。
对你的实操约束是:在你的处理链路里,务必保住 time 和 ring 这两个字段。 很多点云工具在做格式转换时会默默地只保留 xyz 和 intensity,把这两个字段丢掉。丢了之后你不会立刻报错,只会在几周后发现建图效果就是上不去。这是一个非常隐蔽的坑。
下面是通用的第一批处理,顺序大致是这样:
1. 外参变换:从雷达坐标系到机器人本体坐标系
雷达给你的所有坐标,都是雷达自己坐标系里的。README 定义得很清楚:右手系,原点在雷达底部安装面的中心,+X 轴与底部出线方向相反,+Y 轴由 +X 逆时针转 90 度得到,+Z 轴垂直于底面。
但你的机器人有自己的本体坐标系。导航要的是「障碍物在机器人正前方多远」,不是「障碍物在雷达坐标系里的 X 是多少」。这两者之间差的那个旋转加平移,就是外参。
在 ROS 里,这件事的标准做法是发布一个从机器人本体坐标系到 unilidar_lidar 的静态 TF 变换。之后所有节点都能通过 TF 树自动完成转换,你不用在每个节点里手写矩阵乘法。
外参怎么得到?两条路:从机械设计图纸量,或者做标定。图纸法快,精度取决于装配公差;标定法准,但要额外的流程。通用的经验是先用图纸值跑起来,等发现精度不够了再上标定——一上来就搞标定,容易在还没验证整条链路能不能通的时候就陷进去。
这里有个特别值得记住的方向问题:README 说 +X 轴与出线方向相反。这意味着你想让雷达的正前方对准机器人的正前方,线缆就得朝后走。装机之前多想这一秒,能省掉后面拆一次的功夫。
2. 范围裁剪:把不需要的点直接扔掉
裁剪是性价比最高的一步:一次简单的条件判断,能砍掉相当比例的点。
常见的裁剪维度有三个。按距离裁:太远的点回波弱、误差大,太近的点可能是机身自身;按高度裁:做地面导航时,远高于机器人的顶棚、灯具通常是无用信息;按强度裁:回波强度极低的点置信度不高。
裁剪要放在整条链路的最前面。所有后续步骤的耗时都跟点数正相关,越早把点扔掉,全链路收益越大。
3. 体素降采样:让密度均匀,也让数据量可控
原始点云里,近处的点密、远处的点疏。这种密度不均会让很多算法产生偏向——近处的点因为数量多,在优化里的权重被无形放大了。
体素栅格降采样(voxel grid)解决的正是这个问题:把空间切成边长固定的小立方格,每个格子里的所有点只保留一个代表点(常见做法是取质心)。这一步同时干掉了数据量大和密度不均两个毛病。
唯一的调参点是格子边长:太大丢细节,太小等于没降。通用做法是根据你的应用尺度来定——做室内导航和做精细的三维重建,合理的取值差得很远。
4. 离群点剔除:那些孤零零的假点
激光雷达会产生一些物理上不存在的点。物体边缘处的激光光斑一半打在前景一半打在背景,返回的距离是个中间值,于是空中就凭空多出一个点;玻璃、镜面、水面也会制造各种假回波。
这类点的共同特征是孤立——周围没有邻居。所以剔除的思路很直接:统计每个点到最近 K 个邻居的平均距离,明显偏大的判为离群点扔掉。PCL 里有现成实现。
5. 剔除打到机器人自身的点
前面在 rviz 里看到的那圈"毛刺",现在要处理掉。
通用做法是定义一个或几个自体包围盒:在机器人本体坐标系里划定几个立方体区域,落在里面的点一律丢弃。这一步必须放在外参变换之后做——因为包围盒是在本体坐标系里描述的,只有先把点转过去,判断才有意义。
包围盒宁可划大一点。多丢掉几个真实的点,代价是那一小片区域看不见;漏掉几个自身点,代价是里程计系统性地低估运动量。两害相权,前者轻得多。
新手最常撞的几堵墙
下面这些是通用经验,不是官方文档内容,但常见的问题就集中在这几类:
收不到数据,第一嫌疑永远是网络配置。 网卡是不是配到了雷达要发的那个目标地址上、有没有配错到别的网卡(笔记本同时有有线和无线时特别容易搞混)、子网掩码对不对。这几项排完,大部分"没数据"就解决了。
其次是防火墙。 UDP 数据被防火墙默默丢掉,是个非常安静的失败——没有报错、没有日志、就是收不到。在开发机上临时放行对应端口或者暂时关掉防火墙,可以快速排除这个变量。
多台设备时的 IP 冲突。 两颗出厂设置相同的雷达接在同一个网段上,症状会非常诡异:数据时有时无、点云看起来像两个场景叠在一起。仓库里给的 set_ip_address 就是为这个准备的。通用建议是:设备一到手,先做地址规划,一台一台改好并贴标签,再往系统里装。
坐标系定义搞混。 这是最贵的一类错误,因为它不会立刻表现出来。X 轴和 Y 轴弄反、Z 轴方向搞反、把雷达坐标系当成本体坐标系用——这些错误的共同特点是「跑得起来、看着也差不多、但结果慢慢地偏」。防它的办法只有一个:每次改完外参,回 rviz 看一眼,确认地面是平的、正前方真的在正前方。
把强度当颜色用。 强度值反映的是回波能量,跟距离、入射角、材质反射率都有关。它在 rviz 里被映射成好看的渐变色,很容易让人误以为那是物体的"颜色"。用它做材质判断之前,先明白它跟这三个因素纠缠在一起——同一面白墙,正对着看和斜着看,强度就不一样。
时间戳对不齐。 雷达的时钟和你主机的时钟不是同一个。README 的示例输出里,同时打印了 system stamp 和消息自带的 stamp 两个时间——它把这两个时间并排放出来,本身就是在提醒你它们不是一回事。 多传感器融合时,用错时间基准会让融合结果莫名其妙地差,而且极难排查。
丢字段。 前面说过,格式转换时 time 和 ring 容易被悄悄丢掉。养成习惯:链路里每加一个处理环节,就确认一次这两个字段还在。
往下走:从点云到轨迹和地图
到这一步,你手里有了一帧帧干净的、坐标系正确的、密度均匀的点云。
再往下就是配准、里程计、建图这条经典链路——把相邻两帧对齐得到帧间位姿,把一连串位姿累积成轨迹,再把所有帧按估计出的位姿拼成地图。这一段不建议自己从零写,成熟的开源方案已经把最难的部分铺好了,你要做的是接上、调参、然后把精力放在「用这张图做什么」上。这条路怎么走,在足式机器人上的 Point-LIO 实践那篇里有完整展开。
如果你还想横向了解各类传感器各自擅长什么、在什么场景下会失效,传感器图鉴里有系统的梳理;这一卷里其他关于宇树软件栈的文章,都在卷 U 的目录下。
最后留三句话给正准备插上第一根线的你:
一,先看图,再算数。 rviz 里三十秒能发现的问题,进了算法要花三天。
二,最贵的错误都是"跑得起来但慢慢偏"的那种。 坐标系、外参、时间基准,这三样错了都不报错。所以每改一次,都回去看一眼。
三,配置类工具和读数据的示例一样重要。 仓库里那三个改 IP、切模式的小程序不是配角——真到了装机和批量部署的时候,它们才是你天天要用的东西。