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 本章知识体系总览

为了让你对本章内容有个整体印象,我画了一张图。它把激光雷达工作原理、点云数据格式、坐标系变换这三块串在了一起。

第1章:点云数据基础 - 知识体系 激光雷达工作原理 点云数据格式 坐标系与变换 机械旋转式 vs 固态式 飞行时间测距 (TOF) 环境光干扰问题 PCD:PCL生态,有序/无序 PLY:支持顶点+面片 BIN:裸二进制,高性能 雷达/相机/世界/车身 旋转矩阵 + 平移向量 齐次坐标与Eigen实现 理解原理 → 选对格式 → 搞定变换 → 开始实战

这张图里,从左到右就是咱们的学习路径。先搞懂雷达怎么采集数据,再学会怎么存这些数据,最后掌握怎么在不同坐标系之间转换数据。每一步都踩实了,后面的算法才不会跑偏。

本章核心要点:

  • 激光雷达的核心是TOF测距,但实际使用要小心环境光干扰
  • PCD适合算法调试,PLY适合网格处理,BIN适合高性能场景
  • 坐标系变换用齐次矩阵,Eigen库是你的好帮手
  • 调试时先拿一个点验证,能省下大量排查时间

好了,这一章就到这里。记住我今天说的这些,后面咱们开始真正动手写代码的时候,你会感谢自己现在打下的基础。