在三维视觉工程实践中,点云分割是最容易踩坑的环节之一。无论是用激光扫描仪采集的工件表面,还是RGB-D相机获取的场景数据,当两个物体在空间上距离较近甚至存在接触时,常见的分割算法往往把它们粘连成一个整体,分不开、切不断。这个问题在机械臂抓取、零件检测等对分割精度要求高的场景里尤其致命。本文围绕DBSCAN聚类和区域生长这两类主流算法,分析粘连产生的原因,并给出切实可行的优化方案。

点云粘连的根源在哪里
要解决问题,得先弄清楚粘连的本质。点云粘连通常发生在两种情况下:一是两个物体表面之间的间隙小于点云的采样间距,导致点与点之间形成连续的过渡带;二是物体接触面上存在大量噪声点或边缘毛刺,这些离散点把本来独立的两个区域连接了起来。从图论的角度看,点云可以建模为一个图,每个点是顶点,邻域关系是边,一旦两个目标之间存在连通的边路径,任何基于连通性的算法都无法把它们分开。
DBSCAN的问题在于它纯粹依赖空间距离密度,对点的几何属性一视同仁。区域生长的问题则更隐蔽一些:生长准则如果只考虑点与点之间的距离,那么遇到过渡带时会毫不犹豫地跨过去。换句话说,这两类算法的默认逻辑都是只要空间上够近就归为一类,而粘连恰恰是空间上真的很近,这时候就必须引入距离之外的约束条件,比如法向量、曲率、颜色等信息。
还有一个容易被忽视的因素是下采样。很多工程师为了加速处理会先做体素下采样,但如果体素尺寸设置不当,两个物体的间隙区域会被合并成同一个体素质心,人为制造了粘连。一般来说,体素尺寸应该小于目标最小间隙的三分之一,否则下采样本身就是在制造问题。
DBSCAN聚类的优化:自适应半径与多约束扩展
DBSCAN有两个关键参数:邻域半径eps和最小点数min_samples。固定半径的DBSCAN在密度不均匀的点云上表现很差,近处物体点密度高、远处密度低,一个半径很难两全。优化思路是让eps自适应于局部密度,具体做法是先对每个点计算其到第k个最近邻的距离,然后根据这个距离的中位数动态确定该点所在区域的搜索半径。
另一个更有效的手段是改造邻域判定条件。标准的DBSCAN判断一个点是否属于邻域只看距离,我们可以额外要求法向量夹角小于阈值,这样即使两个物体的点在空间上贴得很近,只要它们的表面朝向不同,就不会被划入同一个簇。下面是基于Open3D的实现:
import open3d as o3d
import numpy as np
def adaptive_dbscan(pcd, k=10, angle_thresh=25):
"""带法向量约束的自适应DBSCAN分割"""
pcd.estimate_normals(
search_param=o3d.geometry.KDTreeSearchParamKNN(knn=k + 1))
points = np.asarray(pcd.points)
normals = np.asarray(pcd.normals)
kdtree = o3d.geometry.KDTreeFlann(pcd)
n = len(points)
eps_list = np.zeros(n)
# 计算每个点到第k个近邻的距离作为局部密度指标
for i in range(n):
[_, idx, dist] = kdtree.search_knn_vector_3d(points[i], k + 1)
eps_list[i] = dist[-1]
median_eps = np.median(eps_list) * 1.5 # 全局基础半径
cos_thresh = np.cos(np.radians(angle_thresh))
labels = np.full(n, -1)
cluster_id = 0
for i in range(n):
if labels[i] != -1:
continue
# 种子点搜索
queue = [i]
labels[i] = cluster_id
while queue:
cur = queue.pop()
radius = max(median_eps, eps_list[cur])
[_, idx, _] = kdtree.search_radius_vector_3d(points[cur], radius)
for j in idx:
if labels[j] == -1:
# 法向量约束:表面朝向差异过大则拒绝
dot = abs(np.dot(normals[cur], normals[j]))
if dot > cos_thresh:
labels[j] = cluster_id
queue.append(j)
cluster_id += 1
return labels这段代码的核心在于两点:一是半径取全局中位数与局部k近邻距离的较大值,兼顾了稀疏区域;二是用np.dot计算法向量夹角余弦值,只有两个点表面朝向足够接近时才允许它们进入同一个簇。法向量约束对于粘连场景的效果非常明显,比如两个箱子紧挨着堆放,侧面接触区域的点法向量方向相反或接近垂直,角度阈值设为25到30度通常就能把接触面切开。
需要注意的是,法向量的估计质量直接影响结果。在物体边缘处,法向量估计往往不稳定,容易出现抖动,可以在判断时对边缘点做单独处理,或者先做一次 bilateral 滤波平滑法向量。参数调优建议从k等于10起步,角度阈值从30度开始逐步收紧,观察分割结果的变化趋势。
区域生长算法的优化:曲率约束与平滑阈值分级
区域生长的经典做法是从曲率最小的种子点出发,把邻域内法向量夹角和曲率差都小于阈值的点合并进来。Open3D内置了cluster_dbscan,同时也提供了基于法向量的区域生长接口,但默认参数在粘连场景下基本不可用,问题就出在阈值设定上。
优化的关键思想是分级生长:先用严格的法向量阈值进行主体扩张,保证不越过物体边界;对边界附近的点,再引入曲率约束做二次判断。物体接触的过渡带通常伴随曲率的突变,而同一物体表面内部的曲率变化平缓,利用这一点可以在边界处有效刹车。此外,还可以给每个点设置生长优先级,法向量一致性高的点先生长,将平滑区域固定下来后再处理模糊区域,减少误扩散的连锁反应。
import open3d as o3d
import numpy as np
def curvature_guided_region_growing(pcd, knn=30,
normal_thresh=8,
curvature_thresh=0.05):
"""基于曲率引导的区域生长分割"""
pcd.estimate_normals(
search_param=o3d.geometry.KDTreeSearchParamKNN(knn=knn))
points = np.asarray(pcd.points)
normals = np.asarray(pcd.normals)
kdtree = o3d.geometry.KDTreeFlann(pcd)
n = len(points)
# 通过法向量变化率近似估计曲率
curvature = np.zeros(n)
for i in range(n):
[_, idx, _] = kdtree.search_knn_vector_3d(points[i], knn)
local_normals = normals[idx[1:]]
curvature[i] = np.linalg.norm(
local_normals - normals[i], axis=1).mean()
cos_thresh = np.cos(np.radians(normal_thresh))
# 曲率最小的点作为初始种子,属于最平滑的区域
seed = np.argmin(curvature)
labels = np.full(n, -1)
labels[seed] = 0
queue = [seed]
while queue:
cur = queue.pop(0)
[_, idx, _] = kdtree.search_knn_vector_3d(points[cur], knn)
for j in idx[1:]:
if labels[j] == -1:
dot = abs(np.dot(normals[cur], normals[j]))
# 双重约束:法向量接近且曲率差小
if (dot > cos_thresh and
abs(curvature[j] - curvature[cur])
< curvature_thresh):
labels[j] = 0
queue.append(j)
return labels这里用邻域内法向量的平均变化量来近似曲率,虽然不是严格意义上的高斯曲率,但计算简单且对分界足够敏感。法向量阈值建议设在5到10度之间,曲率差阈值需要根据点云尺度调整,可以先把所有点的近似曲率排序,取中间分位附近的差值作为参考。
实战参数调优与两类算法的选型建议
两类算法各有适用场景。DBSCAN的优势是对任意形状的簇都有效,不需要预先指定簇数量,适合杂物堆放、形状不规则的场景;区域生长的优势是边界贴合度高,适合表面平滑的工业件,比如金属外壳、注塑件。实际项目中可以先跑一遍法向量约束的DBSCAN做粗分割,再对疑似粘连的簇用区域生长做精细化切分,两步结合往往比单一算法效果好得多。
参数调优有一个实用技巧:利用簇内点的法向量方差作为粘连检测指标。一个正确的分割簇内部法向量分布相对集中,如果某个簇的法向量方差明显偏大,大概率是两个物体被粘在一起了,可以针对该簇单独调整阈值重新切分,形成自动化的迭代优化流程。
最后提醒几个工程细节:分割前务必做好统计离群点去除,噪声点会把法向量估计带偏;法向量估计时的knn取值建议为点数开立方再乘以10到20的量级;如果点云来自多视角拼接,拼接误差造成的重影也会表现为粘连,这时候应该先做配准优化而不是调分割参数。按照上面这些方法组合使用,绝大多数粘连问题都能得到明显缓解。