简介:这份资源面向从事林业遥感、激光雷达点云处理与单木参数提取的学习者和研究人员,聚焦如何借助PCL库中的欧氏距离聚类完成从点云数据到单木分割的完整落地,解决聚类半径与阈值难以把握、分割结果无法直接复用的问题。压缩包共3个文件,包含1个cpp源码、1个xyz示例点云和1个txt聚类输出结果,整体约93KB,体积轻量,源码可直接编译运行,样例数据与结果文件便于对照验证算法效果,类型分工明确:源码负责实现、点云用于输入测试、文本记录输出簇信息。目前已有815人学习下载,说明该方案在点云单木分割场景中具备一定的参考价值。读者可获得可直接调用的欧氏距离聚类代码框架、配套的测试点云与聚类结果样例,并据此理解邻域搜索、距离阈值设定与点簇合并的关键细节,快速迁移到自身数据上开展实验,减少从零搭建聚类流程的试错成本。
1. 点云单木分割别急着上模型:先用欧氏距离聚类把基线跑通
欧氏距离聚类做点云单木分割,最容易翻车的不是算法本身,而是 eps 和 min_points 跟点云密度不匹配。很多人拿到林地点云第一反应是上深度学习,但真到项目里要快速出一版单木标签、做标注辅助或给后续模型做基线,欧氏距离聚类往往更直接:它不需要训练,参数少,每一簇都能解释成空间上连续的一团点。这份资源把欧氏距离聚类封装成 API,输入点云坐标和几个参数,输出每点的单木簇 ID、每簇统计和诊断信息,省去从零写邻域搜索、标签扩展和结果落盘的重复劳动。适合想先跑通单木分割链路、再做精细分割的从业者,也适合拿它当参照系,判断后续模型到底提升了多少。但它不是万能药,树冠粘连严重时,单靠空间距离会合成大簇,边界要心里有数。
2. 欧氏距离聚类 API 的输入输出与预处理链路:为什么 eps 不是拍脑袋填的
2.1 欧氏距离聚类在点云上的几何含义
欧氏距离聚类在点云上的核心动作很朴素:给定半径 eps,对每个点找它周围 eps 米以内的邻居,如果邻居数达到 min_points,就把它当核心点;从核心点出发,把密度可达的邻居不断扩进来,直到一圈圈扩不动为止。换成单木分割的场景,它依赖一个几何假设:同一棵树的枝干、树冠点在三维空间里是连续的,不同树之间至少存在一段空隙。只要空隙宽度大于 eps,聚类就有机会把两棵树分开。
这里要区分两个概念。核心点决定“能不能长出一个簇”,边界点决定“簇的边缘到哪里”。如果 min_points 设得太高,树冠边缘稀疏的点会被打成噪声;如果 eps 设得太大,相邻树冠之间的空隙被跨过去,两棵树就会粘成一簇。API 里看起来只是两个数字,实际对应的是点云密度、树冠间隙和降采样尺度三件事。点云密度越高,同样 eps 内点数越多,min_points 要跟着上调;点云单位是厘米时,eps 填 0.5 不是 0.5 米,而是 0.5 厘米,结果会全成噪声或全成一簇。
我一般会先算最近邻距离中位数,再决定 eps 初值。最近邻距离中位数反映的是点间平均间距,eps 取它的 2 到 3 倍,通常能让同一棵树内部连通,又不至于跨过树冠间隙。min_points 不要拍脑袋填 10,先看 eps 邻域内期望点数,再取 20% 到 30% 作为起点,然后用簇数、噪声率和单簇点数分布去调。欧氏距离聚类的优势是无需预设簇数、解释性强、参数少;边界也很清楚:它只认空间距离,不认颜色、强度、冠层高度,遇到树冠交叉、藤蔓连接、点云空洞,单靠它不够。
2.2 API 的输入契约与输出结构
这份资源的 API 入口通常围绕一份点云和一组聚类参数展开。常见做法是支持两种输入:小数据量调试时直接传内联点数组,大点云传服务端可读的文件路径,避免 JSON 体积过大。请求体里除了 eps、min_points,还会带体素下采样、统计滤波、最小簇过滤和输出格式。输出不只是一串标签,还会带每簇统计和诊断信息,这对排查问题很关键。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| points | number[N][3] | 与 file_path 二选一 | 内联点云坐标,适合几千点调试 |
| file_path | string | 与 points 二选一 | 服务端可读的点云路径 |
| eps | number | 是 | 邻域半径,单位必须与坐标单位一致 |
| min_points | integer | 是 | 核心点最小邻域点数 |
| voxel_size | number | 否 | 体素下采样边长,0 表示不降采样 |
| statistical_k | integer | 否 | 统计滤波近邻数 |
| statistical_std_ratio | number | 否 | 统计滤波标准差倍数 |
| min_cluster_size | integer | 否 | 过滤小簇的点数阈值 |
| output | string | 否 | summary、labels 或 both |
| 响应字段 | 类型 | 说明 |
|---|---|---|
| labels | integer[N] | 每个点的簇 ID,-1 表示噪声 |
| clusters | object[] | 每簇点数、质心、包围盒、高程范围 |
| diagnostics | object | 输入点数、预处理后点数、簇数、噪声率、耗时、参数回显 |
输入契约里最容易被忽略的是坐标单位。API 不会替用户猜单位,eps 必须和 x、y、z 的单位一致。另一个容易踩坑的是 file_path:服务端进程要有读权限,路径分隔符和编码要跟运行环境一致。输出结构里 diagnostics 是排查黑匣子,先看预处理后点数有没有骤减,再看噪声率是不是高得离谱,最后才去看簇数。没有诊断信息的 API 调参只能靠猜,有诊断信息才能判断问题出在预处理、参数还是点云本身。
2.3 预处理链路:体素下采样、统计滤波与坐标归一化
单木分割 API 内部一般不是拿原始点云直接聚类。原始点云动辄几百万点,KDTree 查询内存和时间都吃不消,而且密度不均匀会让 eps 在一个区域合适、在另一个区域失效。常见预处理链路是:先统计滤波去掉明显离群点,再体素下采样让密度均匀,最后做坐标归一化,把点云平移到原点附近再聚类。
体素下采样的做法是把空间切成边长 voxel_size 的网格,每个网格里保留一个代表点,常见是取均值或取中心。voxel_size 越大,点数越少,聚类越快,但小枝和树冠边缘会被抹掉。单木分割里我一般会把 voxel_size 控制在 0.05 到 0.1 米这个量级,具体看点云密度和树冠大小;如果原始点云间距已经大于 0.1 米,再降采样就会把一棵树拆散。统计滤波先算每个点到 k 个近邻的平均距离,再按全局均值加标准差倍数的阈值剔除。statistical_k 常用 20 到 50,statistical_std_ratio 常用 1.0 到 2.0,倍率越小剔得越狠。
坐标归一化不是可有可无。点云坐标如果是大地坐标,数值很大,KDTree 的浮点精度和距离计算都会受影响;减去质心或最小坐标后,eps 仍然是米,但数值稳定得多。API 内部做归一化时要把偏移量记下来,输出质心和包围盒时再反算回去,否则导出的单木位置会整体偏移。预处理顺序也有讲究:先统计滤波再下采样,避免离群点被体素均值摊进正常点里;先下采样再统计滤波,计算量小,但可能把孤立的树梢点当成噪声。两种顺序没有绝对对错,关键是 API 要把顺序和参数回显出来,保证结果可复现。
2.4 为什么把欧氏距离聚类封装成 API 值得做
把欧氏距离聚类封装成 API 的价值不在算法 novelty,而在把一条容易写散的链路固定下来。自己写脚本时,读点云、去噪、下采样、聚类、统计、导出经常散在几个文件里,换一份数据就忘了上次 eps 是多少,调参记录也留不下来。API 把输入参数、预处理、聚类和诊断信息绑在一次请求里,批量跑的时候每一组参数都有回显,结果落盘时能带上参数快照,后面复核不用靠记忆。
另一个价值是边界清晰。API 只负责空间连续簇的划分,不承诺处理树冠粘连,也不承诺输出树种。这样用的人知道它适合做初筛、标注辅助、算法基线,不会把单木分割的全部希望压在一个无监督聚类上。对熟手来说,API 的 eps、min_points、voxel_size、min_cluster_size 都是可替换的旋钮;对新手来说,先跑通最小调用,看到 labels 和 clusters 长什么样,再回头理解参数,比一上来读完整实现更有效。需要提醒的是,API 不是越黑盒越好,诊断信息越完整,后面排查越省事。
3. 从原始点云到单木标签:API 调用、参数扫描与结果落盘
3.1 环境准备与最小调用示例
先跑最小链路,不要一上来就批量跑全图。准备一份几千到几万点的测试点云,确认坐标单位、字段顺序和服务地址。常见点云文件是 CSV 或文本,前三列是 x、y、z;如果带强度、分类字段,读入后只取前三列送进 API。下面这段代码用本地服务地址做一次最小调用,重点看 diagnostics 和 clusters 的数量,不急着看每个点的标签。
import requests url = "http://127.0.0.1:8000/segment" payload = { "file_path": "/data/test_plot.csv", # 服务端可读路径,先确认权限 "eps": 0.5, # 单位与点云坐标一致,示例按米 "min_points": 10, # 核心点最小邻域点数 "voxel_size": 0.08, # 体素下采样边长,0 表示不降采样 "statistical_k": 30, # 统计滤波近邻数 "statistical_std_ratio": 1.5, # 标准差倍数,越小剔除越狠 "min_cluster_size": 30, # 小于该点数的簇直接过滤 "output": "both" # 返回标签和簇统计 } resp = requests.post(url, json=payload, timeout=300) resp.raise_for_status() result = resp.json() print(result["diagnostics"]) print("cluster count:", len(result["clusters"]))逻辑说明:请求体把点云路径和聚类参数一起发给 API,服务端完成读取、预处理、聚类、统计后返回 JSON。timeout 设 300 秒是给大一点的点云留余量;raise_for_status 让 HTTP 错误直接抛出来,避免把错误响应当成结果解析。参数说明:eps 是邻域半径,示例 0.5 米;min_points 是核心点阈值,示例 10;voxel_size 是下采样边长,示例 0.08 米;min_cluster_size 用来过滤碎簇,示例 30 点。第一次跑不要把 min_points 设太高,否则容易全返回噪声,先确认链路通再调。
提示:内联 points 只适合调试。几十万点以上的点云走 JSON 内联会撑大请求体,常见做法是传 file_path 或先分块再调。
3.2 eps 与 min_points 的联动调参
eps 和 min_points 不是两个独立旋钮,它们一起决定核心点密度。eps 变大,邻域内点数自然变多,min_points 不变时核心点变多,簇容易膨胀;eps 变小,邻域内点数变少,min_points 不变时核心点变少,一棵树容易被切成几段。调参时不要单独扫一个参数,至少做二维网格,记录簇数、噪声率和单簇点数分布。
import itertools import requests import numpy as np url = "http://127.0.0.1:8000/segment" base = { "file_path": "/data/test_plot.csv", "voxel_size": 0.08, "statistical_k": 30, "statistical_std_ratio": 1.5, "min_cluster_size": 30, "output": "summary" } rows = [] for eps, min_points in itertools.product([0.3, 0.5, 0.7], [5, 10, 20]): payload = {**base, "eps": eps, "min_points": min_points} result = requests.post(url, json=payload, timeout=300).json() diag = result["diagnostics"] counts = [c["n"] for c in result.get("clusters", [])] rows.append({ "eps": eps, "min_points": min_points, "n_clusters": diag["n_clusters"], "noise_ratio": diag["noise_ratio"], "cluster_p50": float(np.percentile(counts, 50)) if counts else 0, "cluster_p95": float(np.percentile(counts, 95)) if counts else 0, }) for row in rows: print(row)逻辑说明:用 itertools.product 做 eps 与 min_points 的网格扫描,每次请求只返回摘要,减少传输量。参数说明:eps 候选按点云单位给,示例是米;min_points 候选从 5 到 20,覆盖稀疏到中等密度;cluster_p50 和 cluster_p95 用来判断单簇点数是不是过小或过大。看结果时不要只盯 n_clusters,簇数接近真实株数但噪声率很高,说明大量边缘点被丢掉;簇数偏少且 p95 很大,说明相邻树冠在粘连。调参记录要连 voxel_size 一起记,否则换一份密度不同的点云,之前的 eps 没有参考价值。
3.3 批量处理与结果落盘
单份点云调好参数后,批量处理要解决两件事:请求失败可重试,结果文件带参数快照。批量脚本不要把所有点云塞进一个请求,按文件或按空间块提交,单次请求失败不影响其他文件。落盘时至少保存点坐标、标签和参数 JSON,后面复核时能还原当时用的 eps、min_points 和预处理参数。
import json import numpy as np import requests url = "http://127.0.0.1:8000/segment" params = { "eps": 0.5, "min_points": 10, "voxel_size": 0.08, "statistical_k": 30, "statistical_std_ratio": 1.5, "min_cluster_size": 30, "output": "both" } file_path = "/data/test_plot.csv" payload = {**params, "file_path": file_path} result = requests.post(url, json=payload, timeout=300).json() points = np.array(result["points"], dtype=float) # API 回传的点坐标 labels = np.array(result["labels"], dtype=int) # -1 为噪声,其余为簇 ID out = np.column_stack([points, labels]) np.savetxt( "tree_labels.csv", out, delimiter=",", header="x,y,z,label", comments="", fmt="%.4f,%.4f,%.4f,%d" ) with open("tree_labels_params.json", "w", encoding="utf-8") as f: json.dump(params, f, ensure_ascii=False, indent=2)逻辑说明:把 API 返回的点坐标和标签按列拼起来,导出 CSV,方便后续在点云查看器里按 label 着色;参数单独存 JSON,避免结果文件和参数脱节。参数说明:fmt 里前三列保留四位小数,标签用整数;如果 API 不回传 points,就用本地读入的原始点云与 labels 对齐,注意预处理后的点序是否和原始点序一致。批量跑的时候建议给每个文件生成一个结果目录,目录里放 labels、params 和 diagnostics,后面抽查不用重新请求。
3.4 单簇统计与异常簇过滤
聚类跑完不能只看簇数。每簇点数、质心、包围盒和高程范围是判断异常的第一手材料。单簇点数过小,可能是树冠边缘碎点或下采样过度;单簇包围盒水平尺度明显大于周边树冠,可能是多棵树粘在一起;质心离群且高程异常,可能是低矮灌丛或地面残留。API 返回 clusters 时,这些统计最好直接带出来;如果没有,就在本地按 labels 算一遍。
import numpy as np def cluster_stats(points, labels): stats = [] for cid in np.unique(labels): if cid < 0: continue p = points[labels == cid] bbox = p.max(axis=0) - p.min(axis=0) stats.append({ "cluster_id": int(cid), "n": int(len(p)), "centroid": p.mean(axis=0).round(3).tolist(), "bbox_xyz": bbox.round(3).tolist(), "z_min": float(p[:, 2].min()), "z_max": float(p[:, 2].max()), }) return stats stats = cluster_stats(points, labels) for s in sorted(stats, key=lambda x: x["n"], reverse=True)[:10]: print(s)逻辑说明:按标签分组,算每簇点数、质心、包围盒和高程范围,先打印点数最大的前 10 簇,快速看有没有超大簇。参数说明:bbox_xyz 是 x、y、z 三个方向的跨度,单木分割里重点看水平方向最大跨度和竖直方向高度;z_min 和 z_max 用来判断簇是不是从地面一直连到树冠。过滤异常簇时不要只看 min_cluster_size,超大簇要单独标记出来,后面用切片或二次分割处理。
4. 避坑与排查:欧氏距离聚类单木分割常见的五个翻车点
4.1 同一棵树被切成多段
现象:聚类簇数明显多于目测株数,单簇点数偏小,几个簇的质心在水平方向上离得很近,高程范围也重叠。原因通常是 eps 太小,或者 voxel_size 太大把树冠内部点云打散了,也可能是统计滤波把树冠边缘的稀疏点剔得太狠。树冠内部本来就有空洞,点云从不同视角采集时密度不均,eps 一旦小于空洞宽度,密度可达链就断了。
解决:先把 voxel_size 降一半再跑,看簇数是否下降;如果下降明显,说明下采样过度。再把 eps 按最近邻距离中位数的 2 到 3 倍重新设,不要凭感觉加。min_points 可以适当降低,让稀疏树冠边缘也能成为核心点,但不要低到把噪声也拉进来。统计滤波的 std_ratio 可以先放宽到 2.0,保留更多树冠点。调完看单簇点数分布,如果原本被切开的几个簇合并后点数接近周围单木,基本就对了。
4.2 相邻树冠粘连成一簇
现象:某个簇的点数远大于其他簇,包围盒水平尺度明显超过周边树冠,质心落在两棵树中间;可视化时能看到一簇跨过树冠间隙。原因是 eps 太大,或者点云里存在跨树的连接点,比如低矮枝条、藤蔓、未分类的地面点。欧氏距离聚类只认空间距离,两棵树冠只要有一串点桥接,就会并成一簇。
解决:先减小 eps,再看大簇是否拆开;如果拆开后出现大量碎簇,说明 eps 不是唯一问题,需要先清理连接点。常见做法是提高 min_points 让稀疏桥接点不再是核心点,或者先按高程切片,把地面和低矮灌丛剔除再聚类。对于树冠交叉严重的区域,单靠欧氏距离聚类不够,API 输出的大簇要标记出来,后续用冠层高度模型或分水岭做二次切分。不要为了拆大簇把 eps 一路调小,否则其他正常树会被切碎。
4.3 API 返回空结果或点数骤减
现象:请求成功但 labels 全是 -1,clusters 为空;或者 diagnostics 里预处理后点数比输入点数少了一个数量级。原因可能是 min_points 大于降采样后单木点数,也可能是坐标单位设错,eps 实际只有几厘米甚至几毫米;还可能是 file_path 读进来的字段顺序不是 x、y、z,导致距离计算完全错位。
解决:先看 diagnostics 的输入点数和预处理后点数,确认预处理有没有异常缩减;再看参数回显里的 eps 和 min_points 是不是请求里填的值。把 min_points 降到 3 到 5,eps 按坐标单位放大十倍跑一次,如果突然出现大量簇,基本就是单位问题。字段顺序问题更隐蔽,读入后打印前五行,确认前三列是坐标而不是强度或分类。排查顺序是先通链路,再谈精度,不要一上来就怀疑算法。
4.4 不同密度点云结果不可比
现象:同一片林分两期点云,用同一组 eps 和 min_points,簇数差很多;密度高的那期簇数偏少,密度低的那期碎簇多。原因是两期点云密度不同,同样 eps 内点数不同,min_points 的物理含义就变了。没有统一体素下采样时,密度差异会直接变成聚类结果差异。
解决:批量处理前统一 voxel_size,让不同点云落到相近的点间距;min_points 按下采样后的密度重新估,不要跨数据集直接复制。API 请求里把 voxel_size 和 eps 一起回显,结果目录里保存参数快照。如果两期数据采集设备不同,先做最近邻距离分布对比,中位数差得远就分别定 eps,不要强行用同一套参数。可比性来自空间尺度一致,不是来自参数数字一样。
4.5 坐标尺度不一致导致 eps 失效
现象:点云坐标数值很大,聚类全成一簇或全成噪声;或者 x、y 是米、z 是厘米,树冠在竖直方向被压扁或拉长。原因是大地坐标直接送进 KDTree,浮点精度和距离计算受影响;三轴单位不统一时,欧氏距离本身就没有物理意义。API 内部如果做了归一化但没有回显偏移量,导出的质心还会整体偏移。
解决:送进 API 前先检查三轴单位,统一到米;坐标数值大时先减去质心或最小坐标,聚类完再把偏移量加回去。请求里最好带一个 normalize 开关或 offset 字段,API 回显实际使用的偏移量。排查时打印点云包围盒,如果 x、y 跨度几百米而 z 只有几米,单位大概率不一致;如果坐标值在百万级,先归一化再聚类。这个坑不显眼,但一旦踩中,调参怎么调都不对。
4.6 排查顺序:先看诊断信息再调参
遇到结果不对,不要第一反应改 eps。先看 diagnostics:输入点数、预处理后点数、噪声率、簇数、耗时。预处理后点数骤减,查滤波和路径;噪声率超过一半,查 min_points 和单位;簇数远大于预期,查 eps 和下采样;簇数远小于预期,查 eps 和连接点。再看 clusters 的点数分布和包围盒,找出超大簇和超小簇。最后才做切片可视化,确认是欠分割还是过分割。把这个顺序固定下来,比反复盲调参数省时间。
5. 结果验证与精度评估:簇数对不上时看哪些指标
5.1 用单木点云数量分布做粗筛
聚类结果出来后,先看每簇点数的分布。健康的结果通常有一个主峰,尾部有一些小簇和少量大簇;如果小簇特别多,说明过分割或噪声没清干净;如果大簇特别大,说明粘连。不要只用平均点数,平均值会被超大簇拉偏,用分位数更稳。下面这段代码直接对 clusters 里的 n 做分位数统计,先判断分布形态,再决定要不要抽样。
import numpy as np counts = np.array([c["n"] for c in result["clusters"]], dtype=int) print("cluster count:", len(counts)) print("percentiles:", np.percentile(counts, [5, 25, 50, 75, 95]).round(1)) print("min/max:", counts.min(), counts.max())逻辑说明:对每簇点数取 5%、25%、50%、75%、95% 分位数,看分布是否长尾。参数说明:如果 5% 分位数低于 min_cluster_size 附近,说明还有碎簇没过滤;如果 95% 分位数是 50% 分位数的很多倍,说明存在超大簇,需要单独检查。这个统计不能替代精度评估,但能快速把明显异常的结果挑出来。
| 指标 | 计算方式 | 用途 | 异常信号 |
|---|---|---|---|
| 簇数 | clusters 长度 | 与目测株数粗比 | 远大于或远小于预期 |
| 噪声率 | 噪声点数 / 预处理后点数 | 判断参数是否过严 | 超过 50% 要查单位或 min_points |
| 单簇点数中位数 | 每簇 n 的中位数 | 判断单木是否完整 | 中位数远低于 min_points 的倍数 |
| 最大簇/中位簇 | 最大 n 除以中位 n | 判断粘连 | 比值过大说明有大簇 |
5.2 用包围盒与冠幅一致性做抽样核验
单木分割不只看簇数,还要看每簇的空间形态。阔叶树和针叶树的冠幅范围不同,但同一片林分里,单木水平包围盒的最大跨度通常落在某个区间内。如果某簇水平跨度是周边单木的两三倍,基本可以判定为多树粘连;如果某簇水平跨度很小但竖直方向拉得很长,可能是树干被单独分出来,或者地面点混进来了。抽样核验不用全查,按点数从大到小取前 10% 和随机 5% 就够。
import numpy as np def horizontal_span(points, labels, cid): p = points[labels == cid] bbox = p.max(axis=0) - p.min(axis=0) return float(max(bbox[0], bbox[1])) spans = [] for c in result["clusters"]: cid = c["cluster_id"] spans.append((cid, c["n"], horizontal_span(points, labels, cid))) spans.sort(key=lambda x: x[1], reverse=True) for cid, n, span in spans[:10]: print("cluster", cid, "n", n, "span", round(span, 3))逻辑说明:对每簇算水平方向最大跨度,按点数排序打印前 10 簇,优先检查大簇。参数说明:horizontal_span 取 x、y 方向跨度的最大值,单位与坐标一致;如果某簇 span 明显大于同林分冠幅经验范围,标记为疑似粘连。这个代码只做粗筛,最终还要结合切片看形态,不能只看数字。
5.3 用切片可视化定位误分割
数字指标能筛异常,但定位误分割还是要看切片。常见做法是按 z 值分层,比如每 2 米一层,统计每层里各簇的点数占比;如果一棵树的点云从下层到上层突然换成另一个簇 ID,说明树冠被切断;如果两个簇在多个高程层里交替出现且水平位置重叠,说明粘连或标签抖动。可视化不一定要写复杂代码,导出带 label 的 CSV 后按颜色看就行。
import numpy as np z = points[:, 2] edges = np.arange(np.floor(z.min()), np.ceil(z.max()) + 2, 2.0) for lo, hi in zip(edges[:-1], edges[1:]): mask = (z >= lo) & (z < hi) if mask.sum() == 0: continue ids, cnt = np.unique(labels[mask], return_counts=True) top = sorted(zip(ids, cnt), key=lambda x: x[1], reverse=True)[:3] print(f"z[{lo:.0f},{hi:.0f})", top)逻辑说明:按 2 米高程切片,统计每层点数最多的前三个簇 ID,观察同一水平位置的簇 ID 是否随高程跳变。参数说明:切片厚度按树高和点云密度调,低矮林分可以 1 米一层;top 只打印前三,避免输出过多。如果某一层突然出现大量 -1,说明该层点云被当成噪声,要回头查 min_points 和滤波参数。
5.4 形成可复现的验证记录
验证不是跑一次就完,尤其是批量处理时,要留下可复现的记录。每份点云至少记录:输入文件、eps、min_points、voxel_size、统计滤波参数、簇数、噪声率、疑似异常簇 ID、抽样核验结论。记录不用复杂,一个 CSV 或 JSON 就够,关键是参数和结果绑在一起。后面换参数重跑时,能直接对比哪组参数把大簇拆开了、哪组参数把碎簇合并了。
| 记录项 | 示例 | 作用 |
|---|---|---|
| 输入文件 | test_plot.csv | 追溯数据源 |
| eps / min_points | 0.5 / 10 | 复现聚类参数 |
| voxel_size | 0.08 | 判断密度是否统一 |
| 簇数 / 噪声率 | 42 / 0.18 | 粗判结果质量 |
| 异常簇 ID | 7, 19 | 后续重点核查 |
| 抽检结论 | 7 号疑似双树粘连 | 指导二次分割 |
6. 把 API 用进生产流程:分块、并行与参数模板的三个进阶技巧
6.1 大场景分块与边界重叠
整片点云一把梭,内存和耗时都不可控,常见做法是按水平方向分块,块与块之间留重叠。块大小按点云密度和内存定,20 到 30 米比较常见;重叠留 1 到 2 米,避免边界树被切断。每块单独调 API,聚类完按簇质心去重:两个簇质心距离小于冠幅半径、且高程范围重叠,就合并或保留点数多的那个。分块参数不要写死在代码里,放进请求配置,后面换场景只改配置。
| 参数 | 建议范围 | 说明 |
|---|---|---|
| block_size | 20-30 m | 按内存和点云密度调 |
| overlap | 1-2 m | 边界树不被切断 |
| 去重距离 | 冠幅半径量级 | 质心接近的重复簇合并 |
| 单块点数上限 | 按服务端内存定 | 超限先下采样或再分块 |
6.2 并行调用与内存控制
批量分块后可以并行调 API,但并发数不是越大越好。每个请求都会在服务端占内存做 KDTree 和邻域查询,并发 4 到 8 比较稳,超过之后响应时间会明显拉长。客户端要设置超时和重试,失败块单独记录,不要因为一个块失败丢掉整批结果。服务端最好限制单请求最大点数,超过就拒绝并返回明确错误,比默默 OOM 好排查。
6.3 参数模板按林型固化
调好一组参数后不要每次重新填,按林型或点云密度存成模板。模板里放 eps、min_points、voxel_size、统计滤波参数和 min_cluster_size,请求时只覆盖 file_path。模板要带一行备注:适用点间距范围、坐标单位、典型冠幅。换数据时先跑最近邻距离中位数,跟模板备注对不上就重新调,不要硬套。从那以后我每次接新点云都先算最近邻距离中位数,再决定 eps,不再凭手感填。希望帮到你。
本文还有配套的精品资源,点击获取