第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 正确的评测姿势

  1. 真值与预测必须同一 SRS、同一范围、同一点数;
  2. 标签列表覆盖所有要评的类别,未列出的类别会进混淆矩阵的 catch-all 行;
  3. 按区域分块评测:整体指标会掩盖局部问题,按瓦片或按地形类型分组算 F1,才能定位”哪种地形分类差”;
  4. 保存 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 存档、指标成对、评测集独立、尽早替换弃用工具