2. 激光雷达基础

做激光雷达感知,第一关就是跟点云数据打交道。你拿到的文件可能是 .pcd、.bin、.ply 或者 .las,每种格式背后都有它的设计逻辑和适用场景。我个人习惯先把这些格式摸透,不然写代码时总感觉在盲人摸象。

这一章,咱们就掰开揉碎聊聊这些格式长什么样,以及点云内部的数据结构——说白了就是每个点都带了哪些信息。搞懂了这些,后面做预处理、检测、跟踪才心里有底。

点云数据格式与数据结构总览 点云数据 PCD 格式 BIN 格式 PLY 格式 LAS 格式 XYZI (x,y,z,intensity) XYZRGB (x,y,z,r,g,b) 文件格式 数据结构

2.1 四种主流点云格式

先说说最常见的四种格式。你想想看,不同厂商的激光雷达、不同数据集,用的格式可能完全不一样。我最早做KITTI数据集时,满眼都是 .bin 文件,后来转到自动驾驶项目,又跟 .pcd 杠上了。

格式扩展名特点常见场景
PCD.pcdPoint Cloud Library 原生格式,支持 ASCII 和二进制PCL 生态、科研原型
BIN.bin纯二进制,无头信息,紧凑高效KITTI、Waymo 等数据集
PLY.plyPolygon File Format,支持顶点和面片三维重建、Mesh 处理
LAS.lasASPRS 标准,含分类、回波等丰富属性测绘、地理信息系统
💡 我的经验: 做自动驾驶感知,.bin 和 .pcd 是主力。BIN 格式读取极快,适合大规模训练;PCD 格式自带头信息,调试时一目了然。

2.2 PCD 格式详解

PCD 是 PCL 库的亲儿子。它的头信息里会写明点云有多少个点、每个字段是什么类型。我习惯用 ASCII 版本做可视化调试,但训练时一定转成二进制,速度差好几倍。

# 一个典型的 PCD 文件头
VERSION 0.7
FIELDS x y z intensity
SIZE 4 4 4 4
TYPE F F F F
COUNT 1 1 1 1
WIDTH 115384
HEIGHT 1
VIEWPOINT 0 0 0 1 0 0 0
POINTS 115384
DATA ascii
0.1 0.2 0.3 45
0.4 0.5 0.6 120
...

注意 HEIGHT 为 1 表示无序点云,如果大于 1 就是有序点云(比如来自 Velodyne 的扫描线)。

🔧 避坑指南: 我曾经因为没检查 VIEWPOINT 字段,把点云坐标搞偏了好几米。如果你用 PCL 的 pcl::io::loadPCDFile,默认会应用视角变换,记得确认传感器坐标系。

2.3 BIN 格式——又快又糙

BIN 格式说白了就是一堆 float32 的二进制流,没有头,没有注释。每个点通常占 4 个 float:x, y, z, intensity。KITTI 数据集就是这么干的。

// 读取 KITTI bin 文件的伪代码
FILE *fp = fopen("000000.bin", "rb");
float data[4];
while (fread(data, sizeof(float), 4, fp) == 4) {
    float x = data[0];
    float y = data[1];
    float z = data[2];
    float intensity = data[3];
    // 处理每个点
}

读取速度极快,但缺点也很明显——你不知道里面到底存了几个字段。我踩过坑:有一次数据集偷偷在 intensity 后面加了 ring 和 timestamp,我按老代码读,结果全乱了。

⚠️ 注意: 用 BIN 格式前,一定先确认每个点的字节数和字段顺序。建议写个小脚本先打印前 10 个点的十六进制,对照文档验证。

2.4 PLY 格式——不止于点

PLY 格式比 PCD 更灵活,它除了存点坐标,还能存法向量、颜色、甚至三角面片。做三维重建的同学应该很熟悉。不过做目标检测时,我很少用 PLY,因为它的面片信息对检测来说太冗余了。

ply
format ascii 1.0
element vertex 50000
property float x
property float y
property float z
property uchar red
property uchar green
property uchar blue
element face 100000
property list uchar int vertex_indices
end_header
0.1 0.2 0.3 255 0 0
...

看到 element face 了吗?这就是它跟纯点云格式最大的区别。如果你只需要点坐标,用 PCD 或 BIN 更轻量。

2.5 LAS 格式——测绘老大哥

LAS 是测绘领域的标准格式,由 ASPRS 维护。它除了 xyz,还包含分类(地面、植被、建筑等)、回波次数、扫描角度等。我参与过一个路侧感知项目,客户给的就是 LAS 格式,里面连点云分类都做好了。

属性说明典型值
Classification点云分类2=地面, 6=建筑, 1=未分类
Return Number回波序号1, 2, 3...
Intensity回波强度0~65535
GPS TimeGPS 时间戳浮点数

不过 LAS 格式解析起来稍微麻烦点,需要依赖 liblas 或 laspy。我个人建议:如果不是测绘背景,优先转成 PCD 或 BIN 再处理。

2.6 点云数据结构:XYZI 与 XYZRGB

聊完文件格式,咱们看看点云内部的数据结构。说白了,每个点就是一组向量,最常见的两种是 XYZI 和 XYZRGB。

  • XYZI:x, y, z 坐标 + intensity(反射强度)。激光雷达的标配,intensity 对区分目标材质很有用。
  • XYZRGB:x, y, z 坐标 + 颜色(红绿蓝)。通常来自 RGB 相机投影或彩色激光雷达。
🔍 实际项目中的选择: 我做过一个实验,只用 XYZ 做检测,mAP 掉了 3 个点。加上 intensity 后,对行人、骑车人的召回明显提升。所以别小看这个强度值,它里面藏着很多信息。

有些数据集还会扩展字段,比如 ring(激光线束编号)、timestamp(相对时间戳)。在 PCL 中,你可以自定义 PointXYZIR 结构体来容纳这些信息。

// PCL 自定义点类型示例
struct PointXYZIR {
    float x, y, z;      // 坐标
    float intensity;     // 强度
    uint16_t ring;       // 线束编号
    double timestamp;    // 时间戳
    EIGEN_MAKE_ALIGNED_OPERATOR_NEW
} EIGEN_ALIGN16;
POINT_CLOUD_REGISTER_POINT_STRUCT(PointXYZIR,
    (float, x, x)
    (float, y, y)
    (float, z, z)
    (float, intensity, intensity)
    (uint16_t, ring, ring)
    (double, timestamp, timestamp)
)</precode>

        <p>嗯,这里要注意:自定义点类型一定要注册到 PCL 的宏里,不然序列化会出问题。我曾经因为这个 bug 排查了一下午。</p>

        <h3>2.7 格式转换与实战建议</h3>

        <p>实际项目中,你经常需要在不同格式间切换。我一般用 PCL 的 <code>pcl::io</code> 模块做转换,或者写 Python 脚本用 <code>numpy</code> + <code>struct</code> 处理 BIN 文件。</p>

        <div class="tip">
            <strong>⚡ 效率建议:</strong> 训练数据管线里,尽量统一成 BIN 格式。它没有头信息解析开销,内存映射(mmap)也方便。我自己的项目里,所有原始数据落地都是 <code>.bin</code>,只有可视化时才转成 <code>.pcd</code>。
        </div>

        <p>最后分享一个我常用的转换片段,把 BIN 转成 PCD(ASCII 格式方便调试):</p>

        <pre><code>import numpy as np
import struct

def bin_to_pcd(bin_path, pcd_path):
    points = np.fromfile(bin_path, dtype=np.float32).reshape(-1, 4)
    with open(pcd_path, 'w') as f:
        f.write("# .PCD v0.7 - Point Cloud Data\n")
        f.write(f"FIELDS x y z intensity\nSIZE 4 4 4 4\nTYPE F F F F\nCOUNT 1 1 1 1\n")
        f.write(f"WIDTH {points.shape[0]}\nHEIGHT 1\nPOINTS {points.shape[0]}\nDATA ascii\n")
        for p in points:
            f.write(f"{p[0]:.6f} {p[1]:.6f} {p[2]:.6f} {p[3]:.0f}\n")

你看,核心逻辑就这么几行。搞懂了数据格式,后面的预处理、特征提取才能玩得转。