第19章 - 质量检查、精度评定与测试
点云成果的质检分两个层面:数据本身的检查(有没有空值、范围对不对、分类占比是否合理)和算法结果的精度评定(分类的混淆矩阵、高程的中误差、与参考数据的一致性)。前者靠 pdal info 与统计量就能覆盖,后者需要”预测 vs 真值”的成对比较。
PDAL 里能直接做这两件事的工具是 pdal info(统计)、pdal eval(分类精度)、pdal delta(与参考文件逐点对比)。后两者在 PDAL 2.6 起被标记为 DEPRECATED,官方计划在 2.7 从默认安装移除、2.8 完全移除——但本机的 PDAL 2.10.0(OSGeo4W 发行版)里 pdal eval、pdal delta、pdal hausdorff、pdal chamfer 仍然存在。这意味着你必须先 pdal --help 确认自己的发行版有没有,再决定用命令行还是用 Python 自己实现。
本章给出可复现的质检流程与判定标准,不讨论”精度应该多高”这种取决于项目的指标。
19.1 先做数据体检
19.1.1 一次 summary 拿到基本盘
pdal info block.laz --summary
输出里有四类信息:file_size、points(数量)、bounds(XYZ 范围)、metadata(头信息与 SRS)。验收时至少核对:
| 字段 | 期望 | 异常含义 |
|---|---|---|
count |
与生产记录一致 | 读取时丢点、文件损坏 |
bounds |
落在测区范围内 | SRS 错、坐标偏移、混入其他测区 |
metadata.srs.units.horizontal |
与坐标系一致(米/度) | SRS 标错 |
metadata.srs.vertical |
非空 | 没有垂直基准声明(第 11 章) |
metadata.scale_x/y/z |
满足精度要求(投影坐标 ≥0.01) | 精度被量化 |
metadata.dataformat_id / minor_version |
与交付要求一致 | 格式不达标 |
19.1.2 统计量
pdal info block.laz --stats --dimensions X,Y,Z,Intensity,Classification
--dimensions 限制统计的维度,避免对大文件把每个维度都算一遍。返回每个维度的 count/min/max/mean/stddev/variance。三个常用判据:
Z的min/max与测区高差是否吻合(异常值通常来自噪声或错误分类);Classification的分布:地面点占比在城区通常 10%–30%,植被占比高的区域会更高;占比突变提示分类参数问题;Intensity的stddev:接近 0 说明强度未归一化或被清零。
19.1.3 抽样与完整性
完整的数据体检还要看空间覆盖:有没有空洞、瓦片是否齐全。两种做法:
# 1) 边界:hexbin 生成覆盖范围多边形
pdal translate block.laz boundary.geojson hexbin --filters.hexbin.boundary=boundary.geojson
# 2) 瓦片清单:tindex 检查瓦片集
pdal tindex create tiles.json tiles/*.laz
pdal tindex list tiles.json
pdal tindex 会为每个瓦片记录 bounds、count、filename,是查”有没有漏瓦片”最快的办法。
19.2 分类精度:pdal eval
19.2.1 输入要求
pdal eval <predicted> <truth> --labels <标签列表>:两个文件必须在同一坐标系、同一点序上——工具按点索引对齐 Classification,不做空间匹配。实践中这意味着真值文件必须是预测文件的副本,只改了分类值;如果你的真值是人工标注的独立样本,得先用 filters.overlay 或空间连接把标签落到同一点序上。
19.2.2 实测输出
用两个分类完全一致的 2000 点文件跑:
pdal eval pred.las truth.las --labels 2
{
"confusion_matrix": "[[2000,0],[0,0]]",
"f1_score": 1,
"labels": [
{
"accuracy": 1,
"f1_score": 1,
"intersection_over_union": 1,
"label": "2",
"precision": 1,
"sensitivity": 1,
"specificity": 0,
"support": 2000
}
],
"mean_intersection_over_union": 1,
"overall_accuracy": 1,
"pdal_version": "2.10.0 (git-version: eb9126)",
"predicted_file": "pred.las",
"truth_file": "truth.las"
}
指标含义:
| 指标 | 定义 | 用途 |
|---|---|---|
overall_accuracy |
正确点数 / 总点数 | 总体概览,类别不平衡时会虚高 |
precision |
TP / (TP+FP) | 误检率:把非地面判成地面的代价 |
sensitivity(召回) |
TP / (TP+FN) | 漏检率:地面点漏判的代价 |
specificity |
TN / (TN+FP) | 负类识别率 |
f1_score |
2·P·R/(P+R) | 综合指标 |
intersection_over_union |
TP / (TP+FP+FN) | 对类别不平衡更稳健 |
confusion_matrix |
行优先,行是真值标签 | 定位具体混淆方向 |
上例里 specificity=0 是一个容易误读的值:这个测试只有”2”一个标签,负类样本数为 0,分母为零所以补 0。真实评测必须至少包含两个类别。
19.2.3 正确的评测姿势
- 真值与预测必须同一 SRS、同一范围、同一点数;
- 标签列表覆盖所有要评的类别,未列出的类别会进混淆矩阵的 catch-all 行;
- 按区域分块评测:整体指标会掩盖局部问题,按瓦片或按地形类型分组算 F1,才能定位”哪种地形分类差”;
- 保存 JSON 原始输出,不要只记一个 F1 数字——指标之间的背离(高 OA、低 IoU)往往才是问题所在。
19.2.4 如果发行版没有 eval
两个替代方案:
- Python 绑定 + scikit-learn:读两个文件的
Classification到 NumPy,直接调sklearn.metrics.classification_report,输出比pdal eval更全(含宏平均、加权平均); - 自写 C++ kernel:按第 13 章的 stage 写法扩展
Kernel,注册成kernels.myeval,适合需要固定口径的团队。
19.3 几何一致性:pdal delta
19.3.1 用途
pdal delta <source> <candidate> 为 source 的每个点在 candidate 里找最近邻,统计 X/Y/Z 差值。典型场景:重投影前后对比、两次处理结果对比、DEM 采样点与原始点对比。
pdal delta pred.las truth.las
输出(实测,除文件名与维度外结构固定):
{
"X": { "max": 0, "mean": 0, "min": 0 },
"Y": { "max": 0, "mean": 0, "min": 0 },
"Z": { "max": 0, "mean": 0, "min": 0 },
"candidate": "truth.las",
"source": "pred.las"
}
max 是最有用的判据:两个文件坐标完全一致时 max=0;max 显著大于 0 说明有系统性偏移或点序错位。--detail 输出逐点差值 CSV,--alldims 对所有维度计算而不只是 XYZ。
19.3.2 delta 的边界
它是最近邻匹配,不是同名点匹配。两个文件点集不完全相同时,”最近邻距离”包含采样密度带来的误差:候选点越稀疏,delta 越大,即使几何完全一致。所以:
- 对比同一份数据的两种处理结果 → delta 可信;
- 对比不同源、不同密度的数据 → delta 只能看趋势,不能当精度指标。
跨源精度评定应该用检查点(人工实测的 GCP/检查点)或独立的参考面(DEM),而不是点对点最近邻。
19.4 回归测试:gtest 与固定基准
19.4.1 PDAL 自带的测试组织
PDAL 用 GoogleTest,测试代码分布在各模块的 test/ 目录,主入口在 pdal/pdal_test_main.cpp。C++ 侧的单元测试主要是”同样的输入产生同样的输出”:
TEST(StatsFilterTest, basic)
{
PointTable table;
BufferReader reader;
// ... 构造固定点集
StatsFilter filter;
filter.setInput(reader);
filter.prepare(table);
PointViewSet views = filter.execute(table);
MetadataNode m = filter.getMetadata();
EXPECT_EQ(m.findChild("statistic").children().size(), 3u);
}
19.4.2 组合级回归:JSON + 基准输出
对 pipeline 组合,更实用的是”固定输入 + 固定 pipeline + 固定输出哈希”:
pdal pipeline regression/ground.json
# 与仓库中保存的 ground_expected.csv 对比
diff <(head -1000 regression/out.csv) <(head -1000 regression/ground_expected.csv)
要点:比较前 1000 行而不是整文件,避免浮点末位与平台差异导致的假失败;同时保留一份完整输出用于人工排查。
19.4.3 数据质量断言写进 pipeline
把质检逻辑直接放进 pipeline,比事后写脚本更可靠。例如检查 Z 范围并让越界数据直接失败:
[
"input.laz",
{ "type": "filters.range", "limits": "Z[-100:9000]" },
{ "type": "filters.stats", "dimensions": "Z" },
{ "type": "writers.las", "filename": "checked.laz" }
]
注意 filters.range 是”删点”不是”报错”。要硬失败得看 filters.stats 的输出或自己写 stage;用 --stream 时可以在外层脚本读 pdal info --stats 判断后决定是否继续。
19.5 检查点法:工程上真正在用的精度评定
pdal delta 是点对点比,工程验收更常用的是检查点法:人工实测若干检查点(RTK/全站仪),与点云或 DEM 在同样位置的高程对比,算中误差。PDAL 没有专门的检查点内核,用 filters.locate + 一次小脚本就能做。
19.5.1 取点云在检查点位置的高程
filters.locate 从点云中找出离给定坐标最近的点:
pdal translate block.laz nearest.las locate \
--filters.locate.x=400512.34 \
--filters.locate.y=5700345.67 \
--filters.locate.dimension=Z
输出是一个只含单点的 LAS,pdal info --stats 就能读到它的 Z。批量检查点用循环或 Python 绑定,几百个点也只是秒级。
19.5.2 中误差的计算与判定
对 n 个检查点,高程中误差:
RMSE = sqrt( Σ(Z_点云 - Z_检查点)² / n )
判定要点:
| 项目 | 说明 | ||
|---|---|---|---|
| 检查点数量 | 一般要求 ≥ 20 个,且空间上均匀分布 | ||
| 分布 | 覆盖平地、坡地、建筑边缘等不同地形,不能只在平坦区取点 | ||
| 剔除规则 | 粗差点( | Δ | > 3σ)可剔除,但必须在报告里注明剔除数量与规则 |
| 单位 | 与坐标单位一致(米),不要混淆椭球高与正高(第 11 章) |
一个容易忽略的细节:点云在检查点附近可能没有点(空洞),最近邻点的距离本身就带来误差。实践里会限制最大匹配距离(例如 1 m),超过就判该检查点无效,而不是硬取一个远处的点。
19.5.3 与 DEM 精度的区别
点云精度与 DEM 精度不是一回事。DEM 是插值产物,精度取决于格网大小与插值算法:
writers.gdal的resolution决定格网分辨率,1 m 格网的高程中误差通常大于原始点云;- 用
output_type选择插值方式(idw、min、max、mean),不同方式在陡坡上的偏差方向不同; - 验收 DEM 时要用独立检查点,不能拿生成 DEM 的同一批点云去评自己。
19.6 排错清单
| 现象 | 原因 | 修法 |
|---|---|---|
pdal eval 报命令不存在 |
发行版在 2.6 后移除了弃用 kernel | 用 Python + scikit-learn 替代,或自行编译 |
| eval 输出全 1 或全 0 | 两文件分类相同 / 标签列表没覆盖 | 检查输入;--labels 列出全部类别 |
specificity=0 |
负类样本数为 0 | 评测集至少包含两类 |
| delta 的 max 很大但视觉一致 | 点序错位或采样密度差异 | 确认同源;或改用检查点评估 |
pdal info --stats 极慢 |
需要读全量点 | 只对必要维度开 --dimensions,或先抽样 |
| tindex 缺瓦片 | 文件命名/范围重叠 | 检查 tiles.json 的 bounds 与清单 |
| 回归测试偶发失败 | 浮点末位差异 | 只在固定行数/固定维度上比较,或加容差 |
19.7 生产建议
质检要可复现。 把 pdal info --summary 的 JSON、pdal eval 的 JSON、pipeline 版本、数据集标识一起存档。三个月后回查”当时精度是多少”时,只有原始 JSON 靠得住。
指标成对使用。 分类评 OA + IoU,几何评 max + mean,数据完整性评 count + 覆盖范围。单个数字总是可以被优化到好看。
评测集与训练/生产数据隔离。 用同一批数据调参再在同一批上评测,得到的指标没有意义。检查点应独立采集并单独版本管理。
把弃用工具尽早替换。 eval/delta/hausdorff/chamfer 都已标记弃用,团队里应该趁早把评测脚本迁到 Python 绑定 + scikit-learn/scipy,避免升级 PDAL 时整条质检链断掉。
19.8 本章小结
| 主题 | 结论 |
|---|---|
| 数据体检 | pdal info --summary 看 count/bounds/SRS/scale;--stats 看分布 |
| 分类精度 | pdal eval pred truth --labels ... 输出混淆矩阵与 F1/IoU;2.6 起弃用 |
| 几何一致性 | pdal delta source candidate 输出 XYZ 的 max/mean/min;最近邻匹配有局限 |
| 弃用状态 | eval/delta/hausdorff/chamfer 自 2.6 标记 DEPRECATED,2.10 部分发行版仍有 |
| 覆盖检查 | filters.hexbin 出边界,pdal tindex 管瓦片清单 |
| 回归测试 | gtest 单元测试 + 固定 pipeline 的定长输出对比 |
| 生产原则 | JSON 存档、指标成对、评测集独立、尽早替换弃用工具 |