第15章 - 点云分割、聚类与特征提取
第 10 章把点云分成”地面/非地面”,那是二分类。真实项目很快会遇到更细的问题:一片建筑区里有多少栋独立建筑(聚类)、每栋楼的立面和屋顶怎么区分(几何特征)、小区范围怎么用一个多边形圈出来(边界提取)。这些问题不是”再加一个滤波器”能解决的,它们都需要先定义邻域,再在邻域上算某种量。
PDAL 在这块的 stage 可以分成三组:
| 组别 | stage | 输出 |
|---|---|---|
| 邻域与几何特征 | filters.normal、filters.eigenvalues、filters.covariancefeatures、filters.optimalneighborhood |
法向量、特征值、13 个协方差特征、最优邻域 |
| 聚类与分割 | filters.cluster、filters.dbscan、filters.lloydkmeans、filters.supervoxel、filters.litree |
ClusterID 或专用 ID 维度 |
| 边界与密度 | filters.hexbin、filters.radialdensity、filters.label_duplicates |
矢量边界文件、密度维度、重复标记 |
本章的实测数据来自 PDAL 2.10.0:用 readers.faux 生成的合成点云,跑通 filters.cluster + filters.covariancefeatures + filters.hexbin,把真实列名、默认值和报错原文记录下来。
15.1 先理解邻域
15.1.1 knn 与 radius
几乎所有特征与聚类都归结为一个问题:对每个点,找它的邻居。PDAL 提供两种方式:
| 方式 | 参数 | 适用 | 局限 |
|---|---|---|---|
| k 近邻 | knn |
密度均匀的数据;特征描述 | 稀疏区邻居过远,稠密区邻居过近 |
| 半径搜索 | radius + min_k |
物理尺度有明确含义(如 0.5 m 邻域) | 密度差异大时邻居数量剧烈变化 |
filters.eigenvalues 与 filters.normal 的 knn 默认 8,filters.covariancefeatures 的 knn 默认 10。半径模式要求指定 min_k(默认 3),邻居不足时跳过该点。
15.1.2 用最优邻域代替固定 knn
固定 knn 在不同密度区域表现不一致。filters.optimalneighborhood 为每个点搜索使特征最稳定的邻域,输出两个维度:
OptimalKNN:最优邻居数;OptimalRadius:对应半径。
参数只有两个:k_min 默认 10,k_max 默认 14。之后 filters.covariancefeatures 设 optimized: true 就会读这两个维度,不再用固定 knn。代价是额外的搜索开销,但在地形起伏、植被与建筑混合的场景里,分类边界会明显更干净。
15.2 协方差特征
15.2.1 特征从哪来
对邻域内点的坐标求 3×3 协方差矩阵,做特征值分解得到 λ₀ ≥ λ₁ ≥ λ₂,再组合成各种描述子。filters.covariancefeatures 支持 11 个:
| 特征 | 含义 | 高值对应 | ||
|---|---|---|---|---|
Linearity |
(λ₀−λ₁)/λ₀ | 线状地物(电线、杆、路缘) | ||
Planarity |
(λ₁−λ₂)/λ₀ | 平面(屋顶、地面、墙面) | ||
Scattering |
λ₂/λ₀ | 杂乱体(植被、点云噪声) | ||
Verticality |
1− | n_z | 竖直结构(立面) | |
Omnivariance |
(λ₀λ₁λ₂)^(1/3) | 各向同性程度 | ||
Anisotropy |
(λ₀−λ₂)/λ₀ | 方向性 | ||
Eigenentropy |
−Σλᵢlnλᵢ | 无序度 | ||
EigenvalueSum |
λ₀+λ₁+λ₂ | 局部点密度与尺度 | ||
SurfaceVariation |
λ₂/(λ₀+λ₁+λ₂) | 表面起伏 | ||
DemantkeVerticality |
1− | e₃·z | 竖直性(Demantke 定义) | |
Density |
邻域内点数/体积 | 密度 |
前四个(Linearity/Planarity/Scattering/Verticality)构成所谓 “Dimensionality” 预设,也是 feature_set 的默认值。
15.2.2 参数与算例
| 参数 | 默认 | 说明 |
|---|---|---|
knn |
10 | 固定邻域大小 |
feature_set |
"Dimensionality" |
逗号分隔的特征名、预设名或 "all" |
mode |
"SQRT" |
特征值处理方式:SQRT(开方,等价标准差)、Normalized(归一化到和为 1)、Raw(原始值) |
radius |
无 | 指定后走半径搜索 |
min_k |
3 | 半径搜索时的最小邻居数 |
stride |
1 | 邻居采样步长 |
optimized |
false |
使用 OptimalKNN/OptimalRadius |
threads |
1 | 仅标准模式有效 |
一个实测 pipeline(去掉 Density,理由见 15.2.3):
[
{
"type": "readers.faux", "filename": "faux", "count": 500,
"mode": "Normal", "mean_x": 0, "mean_y": 0, "mean_z": 0,
"stdev_x": 5, "stdev_y": 5, "stdev_z": 1, "seed": 7
},
{ "type": "filters.optimalneighborhood" },
{
"type": "filters.covariancefeatures",
"knn": 8, "feature_set": "all", "optimized": true, "threads": 1
},
{ "type": "writers.text", "filename": "clus.csv", "format": "csv" }
]
输出列名(PDAL 2.10.0 实测):
"X","Y","Z","OffsetTime","OptimalKNN","OptimalRadius","Linearity","Planarity",
"Scattering","Verticality","Omnivariance","Anisotropy","Eigenentropy",
"EigenvalueSum","SurfaceVariation","DemantkeVerticality","Density"
注意 Density 这一列只在跑了 optimalneighborhood 之后才出现——这正是下一个坑。
15.2.3 坑:Density 需要前置 stage
直接把 feature_set 设成 "all" 而不跑 optimalneighborhood,会得到(实测原文):
PDAL: filters.covariancefeatures: Density feature requires OptimalKNN and OptimalRadius
dimensions, which are missing in the input PointView.
两种修法:一是显式列出不依赖最优邻域的特征,"Linearity,Planarity,Scattering,Verticality" 或直接用默认 "Dimensionality";二是先加 filters.optimalneighborhood 并把 optimized 设为 true。第二种是官方推荐路线,因为 optimalneighborhood 的输出本身就是后续分类的好特征。
15.2.4 eigenvalues 与 normal 的区别
filters.eigenvalues 输出三个原始特征值维度 Eigenvalue0/1/2(参数只有 knn 默认 8 和 normalize 默认 false),适合你自己组合特征;filters.normal 输出 NormalX/NormalY/NormalZ 与 Curvature,适合做表面朝向分析、法向量一致性检查。filters.normal 还有 always_up(默认 true,强制法向量朝上)和 refine(默认 false,把法向量传播到未计算点)。
15.3 聚类:四种算法怎么选
15.3.1 filters.cluster:欧氏距离聚类
先做欧氏聚类提取(Euclidean Cluster Extraction),再给每个点打标签。规则简单:两点距离 ≤ tolerance 就归为一类,再按点数过滤。
| 参数 | 默认 | 说明 |
|---|---|---|
tolerance |
1.0 | 两点距离阈值(坐标单位) |
min_points |
1 | 小于该点数的簇被丢弃 |
max_points |
2^64−1 | 大于该点数的簇被丢弃 |
is3d |
true |
false 时只在 XY 平面内找邻居(圆柱) |
输出维度 ClusterID:从 1 开始编号,不属任何簇的点为 0。
[
"input.las",
{ "type": "filters.cluster", "tolerance": 2.0, "min_points": 50 },
{ "type": "writers.las", "filename": "cluster.las", "extra_dims": "ClusterID" }
]
tolerance 是唯一需要调的量。点间距 0.1 m 的机载数据,建筑之间距离通常 5 m 以上,取 1.0 就够;地面激光扫描点间距 2 cm 且场景连续,取 1.0 会把整片区域连成一簇,这时要按实际空隙调到 0.1–0.3。
15.3.2 filters.dbscan:密度聚类
DBSCAN 不要求簇的形状,能在噪声里找出任意形状的簇。参数:
| 参数 | 默认 | 说明 |
|---|---|---|
eps |
1.0 | 邻域半径 |
min_points |
6 | 核心点的最小邻居数(经验值:2×维度数) |
dimensions |
X,Y,Z |
参与距离计算的维度 |
与 filters.cluster 的区别:DBSCAN 有”噪声点”概念,密度不足的点不进任何簇;欧氏聚类只要连通就算一类。数据里有大量孤立噪声时,DBSCAN 的结果更干净。
15.3.3 filters.lloydkmeans:按 K 均值切
Lloyd K-means 适合”已知要分成几类”的场景(如按高度分带、按强度分级)。参数只有 clusters(默认 10)、max_iterations(默认 10)和 dimensions(默认 X,Y,Z)。它不做几何连通性判断,簇可能在空间上不连续——这是特性不是缺陷,但用它做建筑单体提取会得到奇怪的结果。
15.3.4 supervoxel 与 litree
filters.supervoxel 做超体素分割,输出 ClusterID,参数 seed_resolution 默认 1.0、spatial_importance、normal_importance 等,适合先过分割再合并的分割流水线。filters.litree 是树木单株分割:minimum_points 默认 10、minimum_height 默认 3.0、search_radius 默认 100.0,输入要求有 HeightAboveGround 维度(先用 filters.hag_nn/filters.hag_dem 算)。
15.4 边界与密度:hexbin
15.4.1 它解决什么问题
给一个测区点云计算”数据覆盖范围”的多边形,最直接的做法是算凸包,但点云常有凹口和空洞。filters.hexbin 的思路是:把区域切成六边形格网,统计每格点数,保留点数超过阈值的格子,再把这些格子并起来求边界。
| 参数 | 默认 | 说明 |
|---|---|---|
density |
无 | 输出密度格网的文件名 |
boundary |
无 | 输出边界多边形的文件名 |
edge_length |
不设则自动估计 | 六边形边长(坐标单位) |
sample_size |
5000 | 自动估边时采样点数 |
threshold |
15 | 一个六边形至少多少点才算”有数据” |
ogrdriver |
GeoJSON |
输出矢量格式 |
smooth |
true |
用 GEOS 简化边界 |
precision |
8 | WKT 有效位数 |
h3_grid |
false |
改用 H3 六边形格网 |
h3_resolution |
自动 | H3 分辨率 0–15 |
15.4.2 实测输出
[
{
"type": "readers.faux", "filename": "faux", "count": 50000,
"mode": "Uniform", "bounds": "([0,100],[0,100],[0,5])", "seed": 11
},
{
"type": "filters.hexbin",
"density": "hex_density.geojson",
"boundary": "hex_boundary.geojson",
"edge_length": 10.0, "threshold": 15
}
]
100×100 的均匀点云、边长 10 的六边形,产出 56 个密度格与 1 个边界多边形;每个密度 feature 的属性只有两个字段:
{ "ID": 8589934593, "COUNT": 1292 }
ID 是 H3 风格的整数格 ID(h3_grid 关闭时也仍用这个字段名),COUNT 是落在该格内的点数。boundary 输出的是 1 个多边形 feature,可直接给 filters.crop 做后续裁剪,或作为成果范围写进元数据。
阈值 threshold 直接决定边界形状:调大,边缘零散点被排除,边界收紧;调小,边界外扩。用默认 15 时,低密度边缘区域通常会被削掉一圈,这是符合预期的。
15.5 一个完整的建筑提取流程
把上面的 stage 串起来,从原始点云到建筑单体:
[
"input.las",
{ "type": "filters.expression", "expression": "Classification == 6" },
{ "type": "filters.optimalneighborhood", "k_min": 10, "k_max": 14 },
{
"type": "filters.covariancefeatures",
"optimized": true, "feature_set": "Linearity,Planarity,Scattering,Verticality",
"threads": 4
},
{ "type": "filters.expression", "expression": "Planarity > 0.6 && Verticality < 0.4" },
{ "type": "filters.cluster", "tolerance": 1.5, "min_points": 200 },
{ "type": "filters.expression", "expression": "ClusterID > 0" },
{
"type": "writers.las", "filename": "buildings.las",
"extra_dims": "ClusterID,Planarity,Verticality", "forward": "header"
}
]
每一步的作用:先只用已标为建筑的点(LAS 分类 6);用最优邻域算特征;用 Planarity/Verticality 剔除植被和车辆;聚类;丢掉噪声簇;写出带 ClusterID 的结果。实际项目里 Planarity/Verticality 的阈值要按数据源标定,0.6/0.4 只是起点。
15.6 排错清单
| 现象 | 原因 | 修法 |
|---|---|---|
Density feature requires OptimalKNN and OptimalRadius |
feature_set 含 Density 但没跑到 optimalneighborhood |
先去 Density,或加 optimalneighborhood 并设 optimized |
所有点 ClusterID=0 |
tolerance 太小,或 min_points 太大 |
按点间距调 tolerance;先用 pdal info --stats 看分布 |
| 整片区域连成一簇 | tolerance 相对于点间距过大 |
降到点间距的 1–3 倍 |
filters.litree 报缺 HeightAboveGround |
没算 HAG | 前置 filters.hag_nn 或 filters.hag_dem |
| 特征维度不在输出里 | writer 没指定 extra_dims |
writers.las 加 "extra_dims": "all" 或列出维度名 |
writers.text 输出列数不对 |
order 里写了不存在的维度 |
用 pdal info --schema 核对维度名 |
| hexbin 边界比数据范围小一圈 | threshold 默认 15 削掉了低密度边缘 |
调小 threshold,或指定更小的 edge_length |
15.7 生产建议
先定尺度,再选参数。 knn、tolerance、edge_length 都是长度量纲,必须由数据点间距推导。入库前先跑 pdal info --stats 拿到 X/Y/Z 的 count 与范围,估算平均点间距,再设参数——照着别人的参数表抄是最常见的错。
特征计算放在分类之前。 协方差特征是”局部形状”的客观描述,可以先用它们做无监督筛查,再决定分类策略。把特征计算放在 pipeline 前段,也能让后续多个分支复用同一份结果。
聚类结果一定要保留 ClusterID。 写成 LAS 时用 extra_dims 带上,否则下游无法按簇聚合。对 BPF/Parquet 这类支持任意列的格式更省事。
边界数据与点云分开管理。 filters.hexbin 产出的 GeoJSON 多边形本质上是”数据覆盖范围”,应该跟着元数据走(入库 PostGIS、写进 STAC item 的 geometry),而不是每次分析重新算。
15.8 本章小结
| 主题 | 结论 |
|---|---|
| 邻域 | knn 与 radius+min_k 二选一;最优邻域用 filters.optimalneighborhood(k_min=10、k_max=14) |
| 协方差特征 | filters.covariancefeatures 默认 knn=10、feature_set=Dimensionality、mode=SQRT |
| Density | 依赖 OptimalKNN/OptimalRadius,否则报错;需前置 optimalneighborhood |
| 欧氏聚类 | filters.cluster:tolerance=1.0、min_points=1、is3d=true;ClusterID 从 1 起,0 为噪声 |
| DBSCAN | eps=1.0、min_points=6、dimensions=X,Y,Z |
| K-means | clusters=10、max_iterations=10 |
| 边界 | filters.hexbin:edge_length 不设则自动估,threshold=15、sample_size=5000,输出 GeoJSON |
| 树木分割 | filters.litree 需要 HeightAboveGround 维度 |