1. 点云数据基础:激光雷达工作原理、点云数据格式与坐标系变换
大家好,我是你们这门课的主讲。做了这么多年激光雷达SLAM和点云处理,我见过太多同学一上来就调算法,结果连点云数据是怎么来的都没搞清楚。说实话,这就像你开着一辆跑车却不知道油箱在哪——迟早要出问题。
这一章,咱们先把地基打牢。我会从激光雷达怎么干活讲起,再到点云文件里到底存了啥,最后聊聊坐标系那些绕不开的变换。嗯,都是干货,咱们开始吧。
1.1 激光雷达工作原理:它到底是怎么“看”世界的?
激光雷达,英文叫LiDAR(Light Detection And Ranging)。说白了,它就是通过发射激光束,然后测量激光从发射到反射回来所用的时间,来算出目标距离的。这原理听起来简单,但实际工程里坑不少。
我个人习惯把激光雷达分成两大类:机械旋转式和固态式。
- 机械旋转式:就是那个顶上有个“小桶”在转的。比如Velodyne的HDL-64、Hesai的Pandar系列。它通过电机带动激光收发模块旋转,实现360°水平扫描。垂直方向则靠多线激光(16线、32线、64线)覆盖。
- 固态式:没有旋转部件,靠光学相控阵(OPA)或微机电系统(MEMS)来偏转激光束。体积小、寿命长,但视场角通常有限。比如Livox的Horizon系列。
核心公式:距离 d = (c × Δt) / 2
其中 c 是光速(约3×10⁸ m/s),Δt 是激光飞行时间。注意要除以2,因为激光走了个来回。
我在项目中遇到过一件事:有一次用某款16线雷达,发现点云里总有“鬼影”——明明前面是空地,却出现了一些飘浮的点。排查了半天,原来是阳光太强,导致接收器饱和了。所以啊,环境光干扰是实际工程中必须考虑的因素。
1.2 点云数据格式:PCD、PLY、BIN,到底用哪个?
点云数据存成什么格式,这问题看着简单,但选错了能让你多写几百行解析代码。我常用的就三种:PCD、PLY、BIN。咱们一个一个说。
1.2.1 PCD格式(Point Cloud Data)
这是PCL库的亲儿子。它的特点是文件头是文本,数据体可以是二进制或ASCII。我个人习惯用二进制,因为文件小、读写快。
# 一个典型的PCD文件头
VERSION .7
FIELDS x y z intensity
SIZE 4 4 4 4
TYPE F F F F
COUNT 1 1 1 1
WIDTH 1000
HEIGHT 1
VIEWPOINT 0 0 0 1 0 0 0
POINTS 1000
DATA binary
注意看HEIGHT字段:如果HEIGHT=1,说明是无序点云;如果HEIGHT>1,说明是有序点云(比如深度图转过来的)。这个细节,我见过不少新手搞混。
1.2.2 PLY格式(Polygon File Format)
PLY格式在图形学里用得更多。它支持顶点+面片的存储,所以如果你要做网格重建,PLY是首选。它的文件头也是文本,但数据体同样支持二进制。
ply
format binary_little_endian 1.0
element vertex 1000
property float x
property float y
property float z
property uchar red
property uchar green
property uchar blue
element face 500
property list uchar int vertex_indices
end_header
这里有个坑:PLY的property list语法,解析起来比PCD麻烦。我建议你直接用现成的库(比如PCL或Open3D)来读写,别自己手写解析器。
1.2.3 BIN格式(裸二进制)
BIN格式最简单,也最“裸”。它没有文件头,直接存二进制数据。比如KITTI数据集用的就是这种格式:每个点用4个float(x, y, z, intensity),一共16字节。
// 读取KITTI bin文件的C++代码片段
std::ifstream file("000000.bin", std::ios::binary);
std::vector<float> data(4 * num_points);
file.read(reinterpret_cast<char*>(data.data()), data.size() * sizeof(float));
我的建议:
- 做算法原型、调试时,用PCD(PCL生态好)
- 做可视化、网格处理时,用PLY
- 做高性能读写、嵌入式部署时,用BIN
1.3 点云坐标系与变换:别让你的点云“飞”到天上去
坐标系问题,是点云处理里最容易出bug的地方,没有之一。我曾经因为坐标系搞反,导致一辆自动驾驶小车在测试场里“画龙”——嗯,那场面相当尴尬。
1.3.1 常见的坐标系
| 坐标系 | X轴 | Y轴 | Z轴 | 典型应用 |
|---|---|---|---|---|
| 雷达坐标系 | 车辆前进方向 | 左侧 | 天向 | Velodyne、Hesai |
| 相机坐标系 | 右 | 下 | 前 | OpenCV、图像投影 |
| 世界坐标系 | 东 | 北 | 天 | GPS、地图构建 |
| 车身坐标系 | 前 | 左 | 上 | ROS中的base_link |
你想想看,雷达坐标系和相机坐标系就差很多:雷达的Z轴朝上,相机的Z轴朝前。如果不做变换,直接把雷达点云投影到图像上,那结果肯定是一团糟。
1.3.2 坐标变换:旋转+平移
坐标变换说白了就是两个操作:先旋转,再平移。用数学公式表示就是:
P' = R * P + t
其中R是3×3的旋转矩阵,t是3×1的平移向量。P是原始点,P'是变换后的点。
实际工程中,我们通常用齐次坐标来统一表示:
| P' | = | R t | * | P |
| 1 | | 0 1 | | 1 |
这样做的好处是,多次变换可以连乘成一个矩阵,计算效率高。
避坑指南:
我曾经在写标定程序时,把旋转矩阵的转置和逆搞混了。记住:旋转矩阵是正交矩阵,它的逆等于它的转置。但如果你用了非正交的变换(比如带缩放的仿射变换),那就不能这么偷懒了。
1.3.3 实战:用Eigen库做坐标变换
在C++里,我推荐用Eigen库来处理坐标变换。它轻量、高效,而且和PCL配合得很好。
#include <Eigen/Dense>
// 定义旋转矩阵(绕Z轴旋转30度)
Eigen::Matrix3d R;
R = Eigen::AngleAxisd(30.0 * M_PI / 180.0, Eigen::Vector3d::UnitZ());
// 定义平移向量
Eigen::Vector3d t(1.0, 2.0, 0.5);
// 构建齐次变换矩阵
Eigen::Matrix4d T = Eigen::Matrix4d::Identity();
T.block<3,3>(0,0) = R;
T.block<3,1>(0,3) = t;
// 对点云进行变换
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_transformed(new pcl::PointCloud<pcl::PointXYZ>);
pcl::transformPointCloud(*cloud, *cloud_transformed, T);
这段代码看起来简单,但实际调试时,我建议你先拿一个点做测试。比如把原点(0,0,0)变换一下,看看结果是不是等于平移向量t。这个小技巧,能帮你快速定位问题。
1.4 本章知识体系总览
为了让你对本章内容有个整体印象,我画了一张图。它把激光雷达工作原理、点云数据格式、坐标系变换这三块串在了一起。
这张图里,从左到右就是咱们的学习路径。先搞懂雷达怎么采集数据,再学会怎么存这些数据,最后掌握怎么在不同坐标系之间转换数据。每一步都踩实了,后面的算法才不会跑偏。
本章核心要点:
- 激光雷达的核心是TOF测距,但实际使用要小心环境光干扰
- PCD适合算法调试,PLY适合网格处理,BIN适合高性能场景
- 坐标系变换用齐次矩阵,Eigen库是你的好帮手
- 调试时先拿一个点验证,能省下大量排查时间
好了,这一章就到这里。记住我今天说的这些,后面咱们开始真正动手写代码的时候,你会感谢自己现在打下的基础。