简介:面向毫米波雷达数据处理应用场景,提供聚类算法系列博文配套的代码和数据集,适合正在学习机器学习、雷达目标检测的开发者或研究人员。资源压缩包共40个文件,以8个脚本和32个文本数据集构成,总计848KB。脚本覆盖K均值、基于密度聚类、谱聚类等算法实现,并附有轮廓系数、紧密度、分离度等评估函数;文本数据集包含多种公开标准聚类数据与仿真点云数据。已有875人学习下载。通过这套资料,读者可免去自行构造数据的繁琐,直接运行代码观察不同聚类算法在多样数据上的表现,并结合真实标签计算多项聚类有效性指标,便于对比不同算法在凸形、密度不均及流形分布数据上的适用性与参数调整。同时,数据集与脚本结构对应清晰,可帮助快速迁移到车载毫米波雷达的目标检测与聚类分析任务中,提升动手实践能力。 很多人第一次接触毫米波雷达点云,都会觉得“这就是一堆点嘛,直接聚类就好了”,可真把数据喂进去才发现,效果和想象中完全不是一回事。雷达点云和激光雷达点云长得像,脾气完全不同:激光雷达的点密、分布均匀,聚起来很顺畅;毫米波雷达点云稀疏、抖动大、不同距离下密度差异悬殊,同一个目标可能分裂成好几块,噪声点还特别多。这篇博文就是我在写毫米波雷达数据处理聚类算法系列时,沉淀下来的配套代码和数据集导航,里面包含代码的模块设计、数据集怎么选、DBSCAN这类算法在真实雷达数据上怎么调参,以及我踩过的一些坑。不管你是刚开始接触雷达感知,还是已经在做相关开发,应该都能从里面找到可以直接上手的部分。
1. 为什么毫米波雷达点云离不开聚类这一步
先聊聊为什么要做聚类。毫米波雷达每次扫描会输出几十到几百个点,每个点携带距离、方位角、径向速度、回波强度等信息。一个真正的目标——比如前方三四十米的一辆车——在雷达看来并不是一个点,而是车身上多个反射点叠在一起:保险杠、车尾边缘、后视镜区域可能各自产生了多个点。行人也是一样,腿、躯干、手臂都会产生反射。如果不对这些点做归组,后续的跟踪模块就不知道该拿哪个点去建航迹,分类模块也没法判断“这一簇点到底是一辆车还是一个人”。
聚类的目标,就是在一个扫描周期内,把物理上属于同一个目标的点归到同一个集合里,顺便把孤立的噪声点识别出来丢掉。这一步做得好不好,直接影响后面跟踪和识别的准确率。
毫米波雷达点云有几个特点,是聚类时必须考虑的:
- 稀疏性:一个目标上可能只有几个到十几个点,远没有激光雷达那么密集。
- 密度不均匀:近距离点密集,远距离点稀疏,固定聚类半径很容易顾此失彼。
- 多普勒速度不一致:车上不同反射点的径向速度可能存在差异,比如车辆转弯时两端速度明显不同,只看空间距离容易把一个目标拆成两半。
- 多径与噪声:雷达波经过地面、护栏反射后再回到接收机,会产生虚假点,这类点聚不进去也没关系,但算法要能把它当作噪声容忍掉。
所以在毫米波雷达数据处理链路中,聚类算法既要做目标点云的划分,又要承担噪声过滤的任务。它有别于图像处理里“前背景分割”的概念,更接近“在噪声环境中进行密度分布的重新组织”。这也是为什么基于密度的聚类算法在雷达领域特别受欢迎。
这个系列涉及的算法,以DBSCAN为主,同时也整理了KMeans、层次聚类等常见方法在雷达场景下的适用性对比。平时看论文的时候,我也会顺手把一些针对雷达点云设计的改进聚类方法记录下来,后续会逐步补充到代码仓库里。
2. 配套代码结构:拿到就能跑的四个模块
这个系列配套的代码仓库,分成几个独立的模块,目的就是让读者能很快把整个流程跑通,再针对自己的数据做修改。目录结构大概是这样的:
radar_cluster/ ├── config.py # 全局参数配置文件 ├── data_loader.py # 数据加载和格式转换 ├── cluster/ │ ├── __init__.py │ ├── dbscan.py # DBSCAN聚类实现与封装 │ ├── kmeans.py # KMeans聚类封装 │ └── utils.py # 坐标转换、距离计算等工具函数 ├── visualize.py # 聚类结果可视化 ├── evaluate.py # 聚类效果评估 └── main.py # 主运行入口依赖环境非常轻量:Python 3.8以上,numpy、scikit-learn、matplotlib、pandas,都是做数据处理常用的库。不建议一上来就引入深度学习框架,雷达点云聚类用传统的机器学习算法效率更高、结果可解释性更强。
dbscan.py核心只做一件事:把scikit-learn的DBSCAN封装成适合雷达点云处理的形式。因为实际用的时候,我们往往需要自定义距离度量,或者按照距离自适应调整邻域半径,直接调原生接口反而麻烦。
# cluster/dbscan.py 核心封装示例 import numpy as np from sklearn.cluster import DBSCAN as SkDBSCAN def adaptive_eps(range_values, base_eps=1.5, slope=0.05, min_eps=1.0): """根据距离自适应获取每个点的聚类半径。 雷达远距离点的空间间隔更大,固定半径容易把远距离目标漏检, 所以让半径随距离线性增加。 """ return np.maximum(min_eps, base_eps + slope * range_values) def cluster_points(points, eps=1.5, min_samples=3, distance_metric='euclidean'): """执行DBSCAN聚类。 points: Nx3或Nx4数组,列为[x, y, z]或[x, y, z, range_rate]。 """ model = SkDBSCAN(eps=eps, min_samples=min_samples, metric=distance_metric) labels = model.fit_predict(points) return labelsdata_loader.py负责把不同来源的数据统一成numpy数组。不同公开数据集的字段命名差异很大,我一般会在这一层把所有数据转换成统一的格式:
frame_id, x, y, z, range_rate, rcs, label其中label是有标注数据集里的真值类别,没有的话填-1。统一格式之后,cluster模块不用关心数据是从RadarScenes来的还是自己采集的,只管处理。
主流程图很简单:
load_data()读入一帧点云;preprocess()做坐标转换、滤除无效点;cluster()执行聚类;visualize()画出聚类前后对比图。
如果你只是想来跑个demo,在项目根目录下准备一份点云CSV,然后执行:
python main.py --data_path ./data/demo_frame.csv --show_plot代码会输出一个2D散点图,不同聚类簇用不同颜色标记,噪声点统一显示为灰色。
3. 数据集应该怎么选:公开数据源与自采方案
雷达点云聚类的公开数据集,比图像和激光雷达数据集少得多,但也不是没有。我实测下来,有三个数据集最适合拿来学习和验证聚类算法:RadarScenes、Astyx和nuScenes。它们的标注质量、传感器类型和点云密度差异比较大,适合不同目的的实验。
| 数据集 | 传感器类型 | 点云格式 | 标注类型 | 适用场景 |
|---|---|---|---|---|
| RadarScenes | 多部毫米波雷达 | ndarray(含距离、角度、多普勒、RCS) | 逐点目标类别 | 真实道路场景聚类验证 |
| Astyx | 高分辨率毫米波雷达 | pcd文件 | 3D目标框 | 小目标聚类、高精度验证 |
| nuScenes | 5部毫米波雷达+激光雷达+相机 | json格式 | 3D目标框 | 多模态联合研究 |
| 自采数据 | TI/大陆等雷达套件 | csv/npy | 按需标注 | 特定场景算法验证 |
先说RadarScenes,这个数据集来自德国乌尔姆大学的真实道路采集,包含大量城市路况。它最大的优点是每个点都有逐点语义标签,标记出这个点是行人、车辆、自行车还是其他目标。对于聚类课题来说,这简直是天然的评估集:你可以把聚类结果和点级标签对比,直接算出聚类的纯度、召回率,不用自己人工标注。
Astyx的高分辨率雷达数据质量非常高,点云相对更密集,适合用来测试聚类算法在各种姿态目标上的表现。但目前它的公开数据量不大,场景覆盖不如RadarScenes广。
nuScenes是自动驾驶多模态研究的常用数据集,包含激光雷达、相机和毫米波雷达,如果你后续想把聚类和激光雷达检测结果做对比,或者做多传感器融合,这个数据集会非常合适。但它的毫米波雷达点云密度相对较低,单独做雷达点云聚类实验时效果不如RadarScenes直观。
还有一个容易踩的坑:不要拿KITTI来做毫米波雷达点云聚类的实验。KITTI的光彩在于激光雷达和视觉,它没有提供毫米波雷达的数据,网上虽然有人转过所谓的KITTI雷达数据,但缺乏权威标注,拿来做聚类验证容易栽跟头。
数据集拿回来之后,不能直接用,必须预处理。我踩过的坑是:RadarScenes原数据提供的是极坐标下的距离和角度,你需要自己转成笛卡尔坐标才能做欧氏距离聚类。转换公式很简单,但坐标方向定义引用了不少时间。
# 极坐标转笛卡尔坐标 def polar_to_cartesian(range_m, azimuth_rad, elevation_rad=0.0): """毫米波雷达极坐标转笛卡尔坐标。 约定:x为车前方正方向,y为左侧正方向,z为垂直向上。 """ x = range_m * np.cos(elevation_rad) * np.cos(azimuth_rad) y = range_m * np.cos(elevation_rad) * np.sin(azimuth_rad) z = range_m * np.sin(elevation_rad) return x, y, z雷达安装方式不同,坐标系定义会不一样,代码里需要根据传感器的安装偏移量做一次外参修正。如果你用的是自己做的小型雷达套件,这个步骤务必用标定数据验证一次,不然聚类结果会莫名奇妙地偏向一侧。
4. DBSCAN调参实战:从固定值到距离自适应的演进
雷达点云聚类最常用的算法是DBSCAN,它的优势很明显:不需要预先设定聚类数量,能自动识别噪声点,适合稀疏分布数据。但DBSCAN的两个参数——邻域半径eps和最小样本数min_samples——在雷达点云上调起来有大学问。
我早期的实现是给全场景设同一个eps值,结果在近距离效果还行,到了中远距离目标漏检率一下子飙升。原因其实很直白:雷达的角度分辨率是固定的,距离越大,相邻角度之间的横向间隔越大。一辆20米外的车,点与点之间的间距可能只有0.3米;同样的车放到50米外,同一个目标反射点之间的间距就可能拉大到了1米。固定eps=1.5时,近处目标勉强能聚到一起,远处目标直接散架。
解决办法是让eps跟随距离自适应变化。我常用的公式是:
eps_i = max(min_eps, base_eps + slope * range_i)其中base_eps根据近距离的点云密度来定,slope控制半径随距离增长的幅度,min_eps防止过近处半径太小导致过分割。对这个参数组合,我在多组测试数据上的经验是:base_eps取1.2到1.8,slope取0.03到0.08,min_eps取0.8到1.0,起始这几个值都不会太离谱。
除了空间距离,多普勒速度是雷达点云独有的一个非常有区分的维度。两个空间位置相近、但径向速度差异很大的点,大概率不是同一个目标;反过来,一辆车上的点即便空间上稍有分散,只要速度一致,就很可能是一个整体。我后来把距离度量升级成综合距离,效果明显改善:
def enhanced_distance(p1, p2, w_speed=0.8): """空间距离 + 加权速度差异,构成综合距离。""" d_spatial = np.linalg.norm(p1[:3] - p2[:3]) d_speed = abs(p1[3] - p2[3]) if len(p1) > 3 else 0.0 return d_spatial + w_speed * d_speed给速度差异加上权重后,车辆转弯时不容易把车头和车尾拆成两个簇,行人那种速度变化大的目标也不会和旁边的静止护栏混在一起。速度维度的权重不是越大越好,太大会把同一目标上速度稍有差异的点拆开,我这个值是根据几组数据标定的,实际使用时建议自己在自己的数据集上扫一遍。
另一个容易被忽略的问题,是在笛卡尔坐标下聚类还是在极坐标下聚类。笛卡尔坐标直观、好可视化,但雷达原始测量是在距离-角度-速度域。同一辆车在极坐标下的距离维度很紧凑,在笛卡尔坐标下“距离轴”上的点会被映射到“x/y轴”上,坐标分布不再均匀。我实验下来,短距离场景用笛卡尔坐标问题不大,中远距离下极坐标聚类更稳定。不过极坐标聚类需要自定义距离函数,代码复杂度高一些,后续我写针对性的帖子细说。
min_samples这个参数,雷达点云一般设3到5就够了。设1、2,噪声点很容易聚成小簇,虚警特别多;设太高,行人和自行车这种点少的弱小目标直接丢了。建议先用默认的3跑一遍,看可视化结果再决定要不要调。
评估聚类效果时,不要只盯着轮廓系数看。雷达点云的轮廓系数容易虚高,因为噪声点被当作独立簇时轮廓系数也很漂亮,但这是没有意义的结果。我做评估时更看重两件事:第一,已知真值的目标有没有被完整地聚成单簇;第二,两个目标有没有被错误地并到一簇。有了逐点标注的数据集(比如RadarScenes),可以直接用调整互信息和簇纯度这类指标做定量对比。
5. 代码落地的隐藏细节:从demo到工程化的距离
把聚类代码从demo状态搬到实际工程里,会碰到很多意想不到的问题。这里列几个我实际踩过的坑,希望能帮你少绕弯路。
多帧点云积累问题。单帧雷达点云太稀疏,尤其在交通流量小的路段,一帧里一辆车可能就三五个点,聚类后稳定性很差。我的做法是做多帧积累,把连续几帧的点云拼到一起再聚类。但这带来一个副作用:车辆在运动,点云会拖影。必须在积累前用雷达本身测量的径向速度做运动补偿,不然一个静止目标会被拉成一条线,聚类结果自然就乱了。
运动补偿的基本思路是按本车速度和目标径向速度推算位移,把不同帧的点补偿到同一时刻坐标系下。做得精细一点还要考虑本车横摆角速度。
地面杂波和静态点处理。道路上的地面反射、雨水杂波、护栏引起的多径反射,会在聚类结果里形成不必要的簇。雷达点云一般包含RCS(雷达散射截面积)信息,地面杂波的RCS特征通常比较弱且不稳定,可以设一个RCS阈值先把明显杂波滤掉。如果雷达支持垂直视场,还可以利用高度差把地面点去掉。但这个方法要谨慎,路沿和减速带这类低矮目标也有些有用点,滤得太狠会直接把它们从点云里抹掉。
目标分裂与合并的内在矛盾。长车(卡车、巴士)在雷达里的反射点分布范围很大,聚成单簇容易导致簇内点间距过大,拆开又会导致一个车变成几个簇。我的处理方式是“先聚类、后合并”:聚类后对相邻簇检测质心距离和速度一致性,如果满足条件就合并。这个策略在目标较多的场景下比盲目调大eps更可控。合并逻辑可以参考下面的伪代码:
def merge_clusters(labels, points, max_distance=3.0, max_speed_diff=1.5): """基于质心距离和速度一致性,合并可能属于同一目标的簇。""" # 计算每簇质心和平均速度 # 遍历簇对,满足条件则合并标签嵌入式部署的性能问题。高级辅助驾驶Controller的算力有限,DBSCAN的原始实现是O(n^2)的复杂度,在点云规模大的时候会吃不消。scikit-learn的DBSCAN底层用了kd-tree或者ball-tree加速,效果不错,但嵌入式环境下不一定有完整的Python生态。工程化部署时一般会把聚类算法用C++重写,或者用雷达供应商提供的库来做。如果要在Python里先验证性能,建议用好numpy的向量化计算,避免在Python层写三重循环,这个优化往往能把单帧处理时间从几百毫秒降到几十毫秒。
与后续跟踪模块的衔接。聚类输出的簇只是“这一帧的目标点云”,还没有时间维度。要让目标在一段时间内连续存在,需要把聚类结果接入跟踪模块。雷达点云聚类后通常输出的是每个簇的质心、点数量、平均RCS和速度信息,这些特征值个数不多,用来关联不同帧的同一目标是足够的。做特征选择时想长远一点:后续跟踪可能需要哪些量,聚类代码最好在一开始就把这些量算好存好,别等到接跟踪模块时再回头改数据结构。
最后再说一个经验:宁可花时间多试几种距离度量,也不要急着改eps。雷达点云聚类的很多问题是“距离定义不对”造成的,而不是“半径参数没调好”。把速度维度加进距离度量,把坐标系从笛卡尔换成极坐标,往往比调一天参数更管用。如果你在跑这套代码时发现聚类结果很怪,优先检查坐标转换是否存在方向偏差,其次检查速度维度有没有参与聚类,最后再动参数。这套排查顺序帮我解决过绝大多数看起来“参数怎么调都没用”的问题。
本文还有配套的精品资源,点击获取