3. 点云生成与聚类

各位同学,今天我们来聊聊点云处理这个核心环节。说实话,点云这东西,刚接触时觉得挺简单——不就是一堆三维坐标点嘛?但真正做项目时才发现,这里面的门道可不少。

我最早做雷达融合时,拿到点云数据直接往上堆算法,结果系统跑起来卡得要命。后来才明白,点云处理的关键不在于算法多花哨,而在于数据结构选得对不对、预处理做没做到位。

点云数据结构:选对容器很重要

点云本质上是一组三维空间中的点,每个点通常包含(x, y, z)坐标,还可能带有强度、速度、时间戳等信息。但怎么组织这些数据,直接影响后续处理的效率。

我个人习惯用两种结构:

  • 无序点云:就是一个点列表,简单直接。适合小规模数据(几千点以内)。
  • 有序点云:按扫描线或帧结构组织,类似图像。毫米波雷达通常输出这种结构,因为每个chirp对应一条扫描线。

嗯,这里要注意:毫米波雷达的点云密度远低于激光雷达。我见过有人直接把激光雷达的PCL库搬过来用,结果发现KD-Tree建树的时间比处理点云本身还长。说白了,选数据结构要跟数据规模匹配。

// 一个简单的毫米波点云结构体
typedef struct {
    float x;          // 横向距离 (m)
    float y;          // 纵向距离 (m)  
    float z;          // 高度 (m)
    float velocity;   // 径向速度 (m/s)
    float snr;        // 信噪比 (dB)
    uint32_t timestamp; // 时间戳 (us)
} MMWavePoint;

// 点云帧结构
typedef struct {
    uint32_t point_num;
    MMWavePoint points[MAX_POINTS];
    uint64_t frame_id;
} PointCloudFrame;

点云滤波:先把垃圾扔掉

雷达原始点云里,噪声点、杂散点特别多。我做过统计,某些场景下有效点可能只占30%。所以滤波是第一步,也是最重要的一步。

常用的滤波方法:

  • 直通滤波:按距离、速度范围直接切掉。比如我只关心0-50米内的目标,超出的一律丢弃。
  • 统计滤波:计算每个点邻域内的平均距离,离群太远的点去掉。这个对去除随机噪声很有效。
  • 半径滤波:如果一个点周围指定半径内点数太少,就认为是孤立噪声点。
我的经验:直通滤波一定要放在最前面做。先砍掉大量无效数据,后续处理速度能快好几倍。我曾经在一个项目中,滤波前有5000个点,滤波后只剩800个,处理时间从50ms降到了8ms。

DBSCAN聚类:密度相连的才是兄弟

DBSCAN(Density-Based Spatial Clustering of Applications with Noise)是我最常用的聚类算法。为什么?因为它不需要预先指定聚类数量,而且能很好地处理噪声点。

核心思想很简单:

  • 如果一个点周围半径ε内有至少MinPts个点,它就是核心点
  • 核心点之间密度相连的归为一类
  • 剩下的就是噪声点

参数选择上,我一般这样设:

  • ε(邻域半径):毫米波雷达我常用0.5-1.5米。太小会把一个目标拆成好几块,太大又会把相邻目标合并。
  • MinPts(最小点数):3-5个。行人可能只有2-3个点,车辆可能有10-20个点。
避坑指南:我曾经在高速场景下把ε设成0.3米,结果一辆车被分成了七八个簇。后来发现是因为车速快导致点云稀疏,邻域半径应该相应增大。记住:参数不是固定的,要根据场景动态调整。
// DBSCAN核心逻辑(简化版)
void dbscan_cluster(PointCloud* cloud, float eps, int min_pts) {
    int cluster_id = 0;
    for (int i = 0; i < cloud->size(); i++) {
        if (cloud->points[i].visited) continue;
        cloud->points[i].visited = true;
        
        // 找邻域内的点
        std::vector<int> neighbors = range_query(cloud, i, eps);
        
        if (neighbors.size() < min_pts) {
            cloud->points[i].label = NOISE; // 标记为噪声
        } else {
            cluster_id++;
            expand_cluster(cloud, i, neighbors, cluster_id, eps, min_pts);
        }
    }
}

欧式聚类:距离近的就是一家人

欧式聚类(Euclidean Clustering)比DBSCAN更直观——就是按欧氏距离把相近的点归到一起。它其实可以看作是DBSCAN的一个特例,当MinPts=1时,两者等价。

实现步骤:

  1. 建KD-Tree加速近邻搜索
  2. 选一个未处理的点作为种子
  3. 找距离小于阈值的所有点,加入当前簇
  4. 对簇内新加入的点重复步骤3,直到没有新点加入
  5. 重复步骤2-4,直到所有点都被处理

欧式聚类的优点是速度快、实现简单。缺点是对密度变化敏感——稀疏区域和密集区域用同一个距离阈值效果不好。

实际项目中的选择:

  • 场景简单、点云均匀 → 用欧式聚类,速度快
  • 场景复杂、噪声多、密度变化大 → 用DBSCAN,鲁棒性好
  • 实时性要求极高 → 欧式聚类+适当下采样

知识体系总览

下面这张图概括了本章的核心逻辑,从原始点云到最终聚类结果的完整流程:

点云生成与聚类 — 知识体系 原始毫米波点云 点云滤波 直通滤波 | 统计滤波 | 半径滤波 聚类算法 DBSCAN聚类 密度相连,自动去噪 欧式聚类 距离阈值,简单高效 聚类结果 目标簇 + 噪声点 + 簇特征 关键参数 • ε: 0.5~1.5m • MinPts: 3~5 • 距离阈值: 0.8m • 滤波半径: 0.3m • 最小点数: 2 • 最大距离: 50m

实际项目中的避坑指南

最后分享几个我踩过的坑:

  • 坐标系统一:雷达坐标系和车体坐标系往往有偏移,聚类前一定要做好坐标变换。我见过有人忘了做这个,结果聚类出来的目标位置全偏了。
  • 速度信息别浪费:毫米波雷达有速度测量能力,聚类时把速度作为一个维度加入距离计算,能更好地区分相邻但运动方向不同的目标。
  • 实时性优化:如果每帧点云超过2000个点,建议先做下采样。我一般用体素滤波,把0.2m立方体内的点合并成一个,既保留形状又减少数据量。
小技巧:调试聚类参数时,先录一段典型场景的数据,离线跑一遍看看效果。参数调好了再上实车,能省很多时间。我每次做新项目都会建一个「参数调优记录表」,把不同场景下的最优参数记下来,后面直接复用。

好了,点云生成与聚类这部分就讲到这里。记住:数据结构选对、滤波做到位、聚类参数调好,这三步走稳了,后面的目标跟踪和融合才能做好。