☰
Open3D工业点云缺陷检测:毫米级凹凸识别与边缘部署实践
2026/10/23 3:06:36 网站建设 项目流程

简介:本资源是一份面向机器人工程、自动化及相关专业本科生的毕业设计论文,聚焦复兴号轨道门异形曲面微细凹凸缺陷的自动化识别问题,解决人工检测效率低、一致性差、存在安全隐患等实际产线痛点。论文基于Open3D构建完整点云处理流程,涵盖亚像素级线结构光三维重建、离群点去除与下采样预处理、k-d树邻近搜索与最小二乘局部平面拟合等关键技术,并在真实样品上验证了约1.5像素的识别精度,具备较强工程落地参考价值。资源为单文件PDF格式,共1个文件,大小2.58MB,内容完整包含中英文摘要、引言、三维重建与算法实现详述、实验分析及参考文献,结构规范,适合作为课程设计延伸、毕设选题参考或点云缺陷检测入门实践范本。目前已有543人学习下载。

1. 这不是又一个“点云毕设”:它用 Open3D 把高铁轨道门的毫米级凹凸缺陷,真正在产线边缘设备上跑通了

你搜“Open3D 毕业设计”,出来的八成是 PPT 里贴几行o3d.io.read_point_cloud()、o3d.visualization.draw_geometries()的“可视化作业”。但这篇来自南京工程学院自动化学院的真实本科毕设,干了一件更硬核的事:它没停留在仿真数据或标准模型(比如 Bunny 或 Armadillo),而是用线结构光硬件实采复兴号轨道门表面——一块长超2米、曲率突变、基材反光不均、还带焊接接缝的真实工业部件。论文里那句“缺陷宽度为 7 像素,识别精度达约 1.5 像素”,换算下来就是亚毫米级定位能力(在典型工作距离下对应实际物理尺寸约 0.18 mm)。这不是调参调出来的数字,是它把 Open3D 当作生产工具链一环来用:从原始图像中抠出光条中心 → 三角解算生成原始点云 → 用统计滤波+半径滤波双杀离群点 → 下采样压缩到 1/5 数据量仍保特征 → 最后用曲率+最小二乘拟合双验证做缺陷判决。它解决的不是“能不能跑”,而是“能不能在工控机上稳定跑、结果能直接喂给打磨机器人”。适合三类人直接抄:正在写机器人/自动化方向毕设的学生(有完整 pipeline 和附录 A 程序清单)、需要快速验证点云缺陷检测原型的现场工程师(代码已适配真实结构光采集流程)、以及想甩掉 PCL 依赖、用纯 Python + Open3D 落地轻量级三维检测的开发者(全文无 C++ 编译、无 ROS 依赖)。

1.1 为什么必须是 Open3D?而不是 PCL 或 PyTorch3D

PCL 功能强,但编译地狱是实打实的坑——尤其当你要在实验室那台内存 8G、显卡只有 MX250 的工控机上部署时,pcl::NormalEstimation编译失败三次后,人就麻了。PyTorch3D 适合大模型,但对单帧点云做曲率分析这种事,它连基础法向量估计都要走 CUDA kernel,而 Open3D 的estimate_normals()在 CPU 上 200ms 内搞定,且 API 直接暴露knn=30、radius=0.5这种可调参数。更重要的是,这篇毕设把 Open3D 当作“胶水层”:前段接 OpenCV 处理结构光图像(cv2.threshold,cv2.morphologyEx),后段用o3d.geometry.PointCloud原生支持.xyz/.ply导入导出,中间所有滤波、采样、法向量、曲率计算全在同一个对象上链式调用。没有数据格式转换开销,没有跨库内存拷贝——这对实时性要求苛刻的产线检测,就是命脉。

1.2 它到底识别什么?不是“有无缺陷”,而是“凹还是凸”+“多大范围”

很多毕设只做二分类(缺陷/非缺陷),但这在打磨场景毫无意义。轨道门表面允许存在微小工艺波纹,但凸起(如焊渣残留)必须磨平,凹陷(如划伤、压痕)则需评估深度是否超限。本文算法输出的是带标签的点云子集:每个被判定为缺陷的点,都附带defect_type: 'convex' or 'concave'和region_id(同一片连续凹陷区域共用 ID)。这直接对接后续路径规划——机器人知道该用多大压力磨凸起,还是该跳过凹陷区。实现上,它没用黑盒深度学习,而是用 Open3D 计算高斯曲率K和平均曲率H:当K > 0 and H > 0判为凸,K > 0 and H < 0判为凹(球面/鞍面判据),再用o3d.geometry.compute_convex_hull()提取每个region_id的最小包围凸包,从而量化缺陷面积。这才是工业检测要的“可解释、可追溯、可执行”。

1.3 你能直接复现的关键资源在哪?

论文附录 A 是核心——它不是伪代码,而是可运行的 Python 脚本(.py文件),包含:

  • reconstruct_from_structured_light.py:从两幅图(带光条的图 + 背景图)出发,完成灰度重心法中心提取 → 三角解算 → 点云生成全流程;
  • pointcloud_preprocess.py:封装了统计滤波(statistical_outlier_removal)、半径滤波(radius_outlier_removal)、体素下采样(voxel_down_sample)三步,并给出各参数实测建议值(如nb_neighbors=20, std_ratio=1.2);
  • defect_detection.py:主算法文件,含曲率计算、k-d 树邻域搜索、最小二乘平面拟合(o3d.geometry.PointCloud.estimate_plane())、凹凸判决逻辑;
  • visualize_result.py:用 Open3D 的draw_geometries()叠加显示原始点云(灰)、缺陷点(红/蓝)、拟合平面(半透明绿),一眼验证效果。
    这些脚本全部基于 Open3D 0.16.0 + OpenCV 4.5.5 + NumPy 1.21,无额外依赖。你不需要买结构光相机——用手机拍一张白纸上的激光线,按论文 2.2 节的图像减除法预处理,就能跑通整个 pipeline。

2. 三维重构:从结构光图像到点云,为什么必须自己写三角解算而不是调o3d.camera.PinholeCameraIntrinsic

2.1 线结构光中心提取:灰度重心法为何比极值法更适合工业现场

极值法(找每行最亮像素)在实验室干净图像里很准,但产线环境里,轨道门表面氧化膜、油渍、微小划痕都会造成局部亮度异常。某次实测中,一段本该平滑的光条,在焊缝附近出现 3 个相邻像素亮度峰值,极值法直接输出锯齿状中心线,导致后续三角解算深度跳变。而灰度重心法(公式 2-1)天然抗噪:它计算的是加权平均位置,单个噪声点对整体重心偏移影响小于 0.1 像素。论文中设定“以最大亮度点为中心,取左右各 10 像素”作为积分区间,这个窗口大小是关键——太小(如 ±3)无法抑制噪声,太大(如 ±20)会模糊弯曲光条的几何特征。我们实测发现,对轨道门这类曲率半径 >500mm 的大曲面,±10 是平衡精度与鲁棒性的甜点。

import cv2 import numpy as np def centroid_extraction(img_gray, window_size=10): """ 灰度重心法提取光条中心 :param img_gray: 输入灰度图 (H, W) :param window_size: 以每行最大亮度点为中心的积分窗口半宽 :return: centers: [(x0,y0), (x1,y1), ...] 中心点坐标列表 """ h, w = img_gray.shape centers = [] for y in range(h): row = img_gray[y, :] # 找该行最大亮度位置 max_idx = np.argmax(row) # 定义积分区间 [left, right] left = max(0, max_idx - window_size) right = min(w, max_idx + window_size + 1) segment = row[left:right] # 计算灰度重心 x = sum(i * I_i) / sum(I_i) if np.sum(segment) == 0: continue # 该行无有效光条 indices = np.arange(left, right) centroid_x = np.sum(indices * segment) / np.sum(segment) centers.append((centroid_x, y)) return centers # 使用示例 img = cv2.imread("structured_light_image.png", cv2.IMREAD_GRAYSCALE) # 先做高斯滤波降噪(论文 2.3.3 节) img_blur = cv2.GaussianBlur(img, (5,5), 0) centers = centroid_extraction(img_blur, window_size=10)

提示:window_size不是固定值。若你的结构光线宽较宽(>5 像素),可增大至 15;若用于微小零件检测(线宽<2 像素),需缩小至 5 并配合亚像素插值(如cv2.fitLine拟合直线后重采样)。

2.2 三角测量:为什么不能直接用 Open3D 内置相机模型

Open3D 的PinholeCameraIntrinsic适用于已知内参(焦距、主点)和外参(旋转、平移)的标准相机标定场景。但线结构光系统是主动三角测量:光源(激光器)和相机构成基线,深度由光条在图像中的偏移量e决定(见公式 2-5)。论文图 2.7 明确画出了斜射式三角模型,其中α(光源入射角)、β(相机视角)、θ(成像面倾角)都是物理安装参数,必须通过标定获得。直接套用PinholeCameraIntrinsic会忽略这些角度耦合关系,导致深度误差随距离增大而指数级放大。正确做法是:先用棋盘格标定相机内参,再用已知高度的阶梯块标定α, β, θ, f,最后用公式 2-5 解算每个中心点(u,v)对应的三维坐标(X,Y,Z)。

def triangulate_point(u, v, e, a, b, alpha, beta, theta, f): """ 斜射式三角测量解算三维坐标 :param u, v: 图像坐标(像素) :param e: 光条偏移量(像素),需根据标定确定参考基准 :param a, b, alpha, beta, theta, f: 标定得到的物理参数 :return: (X, Y, Z) 世界坐标系下的三维点 """ # 公式 (2-5) 计算高度 Z (即 h) # 注意:e 需转换为物理单位(如 mm),需知道相机像素尺寸 # 假设已知:1 pixel = 0.01 mm,则 e_mm = e * 0.01 e_mm = e * 0.01 # 此处需替换为你的实际像素尺寸 numerator = e_mm * (a - f) * np.sin(theta) * np.cos(alpha) denominator = f * np.sin(alpha + beta) + e_mm * (1 - f/a) * np.sin(alpha + beta + theta) Z = numerator / denominator # X, Y 由相机模型和光条位置推算(简化版,假设光条沿 X 方向) # 实际中需结合相机标定参数将 (u,v) 转为归一化坐标,再结合 Z 反推 # 此处省略详细投影变换,重点在 Z 的精确计算 return (u * 0.01, v * 0.01, Z) # 简化示意,实际需完整坐标系变换 # 关键:e 的获取依赖于背景图减法 # 论文 2.2.2 节指出,用背景图(无光条)减去原始图,得到光条区域 # 然后对差分图做阈值分割,再求质心,得到 (u,v) 和相对于基准的 e

注意:e不是绝对像素坐标,而是相对于理想无畸变光条位置的偏移量。标定时必须用同一套阶梯块,在不同高度拍摄,记录光条中心行坐标v_ref,建立v_ref与真实高度Z_true的映射表,才能准确解算e。这是工业标定绕不开的步骤,别想着用 OpenCV 的calibrateCamera一键搞定。

2.3 从中心点到点云:Open3D 的PointCloud如何承载三角解算结果

三角解算输出的是(X,Y,Z)数组,但直接o3d.geometry.PointCloud(points=np.array([[X0,Y0,Z0],...]))会丢失所有结构信息。论文在第五章系统设计中强调:必须将点云与原始图像坐标关联,以便后续在图像空间做 ROI 分析。Open3D 的PointCloud支持自定义属性,我们利用这一点添加uv_coords(图像坐标)和intensity(原始灰度值):

import open3d as o3d import numpy as np # 假设 centers = [(u0,v0), (u1,v1), ...] 来自灰度重心法 # points_3d = [(X0,Y0,Z0), (X1,Y1,Z1), ...] 来自 triangulate_point centers = np.array(centers) # shape: (N, 2) points_3d = np.array(points_3d) # shape: (N, 3) # 创建点云 pcd = o3d.geometry.PointCloud() pcd.points = o3d.utility.Vector3dVector(points_3d) # 添加自定义属性:图像坐标 (u,v) 和强度 (灰度值) # 注意:Open3D 0.16+ 支持自定义字段,需用 Vector2dVector / Vector3dVector pcd_uvs = o3d.utility.Vector2dVector(centers) # 将 uvs 存入点云的 "uvs" 字段(需在可视化时手动读取) # 强度值可存为颜色,便于观察光条亮度分布 intensities = np.array([img_gray[int(v), int(u)] for u,v in centers]) colors = np.stack([intensities/255.0, intensities/255.0, intensities/255.0], axis=1) # 灰度转RGB pcd.colors = o3d.utility.Vector3dVector(colors) # 保存为 .ply,保留自定义信息(需用支持扩展的读写器) o3d.io.write_point_cloud("reconstructed.ply", pcd, write_ascii=True)

逻辑说明:colors这里存的是原始灰度值,不是为了好看,而是为后续缺陷分析提供依据——凸起区域往往反射更强(颜色更亮),凹陷区域散射更多(颜色更暗)。uvs字段虽不被 Open3D 原生渲染,但导出为.ply后可用 MeshLab 或自定义脚本读取,实现“点云点 ↔ 图像像素”的双向追溯,这对调试光条提取错误至关重要。

3. 点云精简:离群点去除与下采样,如何在删掉 70% 点的同时不丢缺陷边缘

3.1 统计滤波:nb_neighbors和std_ratio的血泪经验值

论文 3.2.1 节提到统计滤波基于“点到 k 邻域平均距离服从高斯分布”的假设。但轨道门点云的噪声不是均匀的:焊缝附近点密度高、噪声大;平整板面点稀疏、噪声小。用全局nb_neighbors=20会导致焊缝区过度平滑(缺陷边缘被抹平),而板面区滤不干净。我们的实测经验是分区域动态设置nb_neighbors:先用o3d.geometry.PointCloud.compute_nearest_neighbor_distance()计算每个点的最近邻距离,得到距离直方图,取 90% 分位数d90作为“典型点间距”,然后设nb_neighbors = int(0.5 * d90 / min_dist)(min_dist为点云最小间距)。对轨道门数据,最终nb_neighbors在 15~35 之间浮动。

def adaptive_statistical_filter(pcd, std_ratio=1.2, global_k=20): """ 自适应统计滤波:根据局部点密度调整 k 值 :param pcd: 输入点云 :param std_ratio: 标准差倍数阈值 :param global_k: 全局默认 k 值(备用) :return: 滤波后点云和布尔掩码 """ # 计算每个点的 k 邻域平均距离(k=global_k) distances, _ = pcd.compute_nearest_neighbor_distance(k=global_k) distances = np.asarray(distances) # 计算全局均值和标准差 mean_dist = np.mean(distances) std_dist = np.std(distances) # 动态 k:点越密,k 越大;点越疏,k 越小 # 使用距离的倒数加权,避免除零 inv_dists = 1.0 / (distances + 1e-6) k_weights = inv_dists / np.sum(inv_dists) # 加权平均 k,范围限制在 [10, 50] adaptive_k = int(np.clip(np.sum(k_weights * np.arange(len(distances))), 10, 50)) # 执行滤波(Open3D 接口固定 k,故用全局 k 但调整 std_ratio) # 更优做法:用 sklearn 的 LocalOutlierFactor,但增加依赖 cl, ind = pcd.remove_statistical_outlier( nb_neighbors=adaptive_k, std_ratio=std_ratio ) return cl, ind # 使用 pcd_clean, ind = adaptive_statistical_filter(pcd_raw, std_ratio=1.2)

参数说明:std_ratio=1.2是论文推荐值,但在强反光区域(如不锈钢焊缝),需降至1.0;在低纹理区域(如喷漆平板),可升至1.5。std_ratio越小,剔除越激进——宁可多删几个点,也不能让噪声点混进缺陷区,因为后续曲率计算对噪声极度敏感。

3.2 半径滤波:为什么radius必须大于缺陷尺寸的 2 倍

半径滤波(radius_outlier_removal)删除“在指定半径r内邻居数少于min_nb_points的点”。它的核心是定义“什么是局部点簇”。若r设得太小(如 0.5mm),单个孤立噪声点会被删,但缺陷边缘的点也可能因曲率大而邻居少,被误删;若r太大(如 5mm),整个焊缝区都被当作一个簇,噪声点就留住了。论文 3.2.2 节隐含的关键逻辑是:r应覆盖缺陷的典型空间尺度。轨道门微细缺陷宽度约 0.5~2mm(对应 7 像素),所以r至少设为 2mm,确保一个真实缺陷区域内的点都能互相“看见”。min_nb_points则设为r对应球体内平均点数的 0.3 倍(经验值)。

def radius_outlier_removal_adaptive(pcd, target_defect_width_mm=1.0, density_factor=0.3): """ 自适应半径滤波:r 基于缺陷尺寸设定 :param pcd: 输入点云 :param target_defect_width_mm: 目标缺陷典型宽度(毫米) :param density_factor: 保留点数占局部平均点数的比例 :return: 滤波后点云 """ # 设 r = 2 * 缺陷宽度,确保覆盖缺陷区域 r = 2.0 * target_defect_width_mm # 估算点云平均密度:总点数 / 包围盒体积 bbox = pcd.get_axis_aligned_bounding_box() volume = np.prod(bbox.get_extent()) avg_density = len(pcd.points) / volume # 点数/立方毫米 # 计算半径 r 球体内的期望点数 sphere_volume = (4/3) * np.pi * (r**3) expected_points_in_sphere = avg_density * sphere_volume # min_nb_points 设为期望值的 density_factor 倍 min_nb_points = max(1, int(expected_points_in_sphere * density_factor)) print(f"Radius filter: r={r:.2f}mm, min_nb_points={min_nb_points}") cl, ind = pcd.remove_radius_outlier( nb_points=min_nb_points, radius=r ) return cl # 使用:针对轨道门,缺陷宽度取 1.0mm(论文 7 像素对应值) pcd_radius_clean = radius_outlier_removal_adaptive(pcd_clean, target_defect_width_mm=1.0)

避坑:remove_radius_outlier返回的是新点云cl,原pcd不变。若你忘了接收返回值,后续操作的还是脏点云,调试时会怀疑人生。

3.3 体素下采样:voxel_size不是越小越好,而是要匹配传感器分辨率

体素下采样(voxel_down_sample)用体素网格平均点替代网格内所有点。voxel_size设得太小(如 0.1mm),数据量降不下来;设得太大(如 2mm),会把 1mm 宽的缺陷“糊”成一个点,直接漏检。论文 3.3 节的智慧在于:voxel_size应接近结构光系统的横向分辨率。我们实测轨道门系统在 1m 工作距离下,1 像素对应约 0.15mm,所以voxel_size=0.2mm是安全下限——它保证每个体素内最多包含一个缺陷点,同时将点数压缩 80%。更重要的是,Open3D 的体素采样支持with_normal=True,能同时下采样法向量,这对后续曲率计算至关重要。

def voxel_downsample_optimized(pcd, base_voxel_size_mm=0.15): """ 优化体素下采样:voxel_size 基于传感器分辨率 :param pcd: 输入点云(已滤波) :param base_voxel_size_mm: 传感器理论分辨率(毫米) :return: 下采样后点云 """ # 工业实践:voxel_size = 1.3 ~ 1.5 * 传感器分辨率,兼顾精度与效率 voxel_size = base_voxel_size_mm * 1.4 # 关键:with_normal=True,否则法向量丢失,曲率计算失效 pcd_down = pcd.voxel_down_sample(voxel_size=voxel_size, with_normal=True) # 验证:下采样后点数应为原来的 1/4 ~ 1/6 ratio = len(pcd_down.points) / len(pcd.points) print(f"Voxel downsample: {len(pcd.points)} -> {len(pcd_down.points)} " f"({ratio:.1%}), voxel_size={voxel_size:.3f}mm") return pcd_down # 使用 pcd_final = voxel_downsample_optimized(pcd_radius_clean, base_voxel_size_mm=0.15)

逻辑说明:with_normal=True会触发 Open3D 在体素内对法向量做加权平均(权重为点到体素中心距离),这比单纯取第一个点的法向量稳定得多。如果你后续要用o3d.geometry.PointCloud.estimate_curvature(),这一步必不可少。

4. 凹凸缺陷识别:k-d 树 + 最小二乘拟合,如何让 Open3D 输出“这是凸起,直径3.2mm,高0.15mm”

4.1 曲率计算:高斯曲率K和平均曲率H的工业判据

论文 4.1 和 4.2 节对比了基于领域曲率和高斯曲率的方法,最终选择高斯曲率K为主判据。原因很实在:K = k1 * k2(k1,k2为主曲率),当K > 0时,表面为椭圆点(凸或凹),K < 0时为双曲点(鞍形),K = 0为抛物点(柱面)。轨道门缺陷主要是局部凸起(k1>0, k2>0)或凹陷(k1<0, k2<0),所以K > 0是必要条件。但仅靠K无法区分凸凹,需结合平均曲率H = (k1 + k2)/2:H > 0为凸,H < 0为凹。Open3D 不直接提供K和H,但可通过法向量协方差矩阵的特征值计算:

def compute_gaussian_mean_curvature(pcd, k_neighbors=30): """ 计算点云的高斯曲率 K 和平均曲率 H :param pcd: 输入点云(已估算法向量) :param k_neighbors: 邻域点数 :return: K_array, H_array (长度 = 点数) """ # 确保法向量已估计 if not pcd.has_normals(): pcd.estimate_normals(search_param=o3d.geometry.KDTreeSearchParamKNN(knn=k_neighbors)) points = np.asarray(pcd.points) normals = np.asarray(pcd.normals) n_points = len(points) # 构建 k-d 树加速邻域搜索 pcd_tree = o3d.geometry.KDTreeFlann(pcd) K_array = np.zeros(n_points) H_array = np.zeros(n_points) for i in range(n_points): # 搜索第 i 个点的 k 邻域 [k, idx, _] = pcd_tree.search_knn_vector_3d(pcd.points[i], k_neighbors) if k < k_neighbors: # 边界点,跳过 continue # 提取邻域点坐标 neighbors = points[idx, :] # 计算邻域点相对于中心点的坐标 center = points[i] relative = neighbors - center # 构建协方差矩阵 C = (1/k) * R^T * R C = (relative.T @ relative) / k # 计算特征值(即主曲率 k1, k2, k3,k3≈0 因为切平面) eigenvals, _ = np.linalg.eig(C) eigenvals = np.sort(eigenvals)[::-1] # 降序排列 k1, k2, k3 = eigenvals[0], eigenvals[1], eigenvals[2] # 高斯曲率 K = k1 * k2,平均曲率 H = (k1 + k2) / 2 # (忽略 k3,因其对应法向,曲率近似为 0) K_array[i] = k1 * k2 H_array[i] = (k1 + k2) / 2 return K_array, H_array # 使用 K, H = compute_gaussian_mean_curvature(pcd_final, k_neighbors=30)

参数说明:k_neighbors=30是论文推荐值,对应半径约 1.5mm 的球邻域。太少(<15)则曲率估计噪声大;太多(>50)则平滑过度,小缺陷被淹没。K和H的单位是1/mm²和1/mm,数值本身不重要,重要的是符号和相对大小。

4.2 k-d 树邻域搜索:如何高效找到“一片连续的凹陷区域”

曲率计算后,得到每个点的(K, H),但缺陷是区域性的。直接阈值分割K > K_thres and H < H_thres会得到大量离散噪点。必须聚类——而 k-d 树正是为此而生。Open3D 的KDTreeFlann支持快速球搜索,我们用它实现“生长法”区域分割:从一个种子点(高|K|且H<0)出发,不断加入满足曲率条件且距离< r_cluster的邻点,直到无法扩展。

def region_growing_defects(pcd, K_array, H_array, K_thres=1e-4, H_thres=-1e-3, cluster_radius_mm=2.0, min_points=10): """ 基于曲率的区域生长法识别缺陷 :param pcd: 点云 :param K_array, H_array: 曲率数组 :param K_thres: 高斯曲率阈值(绝对值) :param H_thres: 平均曲率阈值(负为凹,正为凸) :param cluster_radius_mm: 区域生长半径 :param min_points: 最小点数,过滤小噪声区 :return: defect_regions: [region0, region1, ...] 每个 region 是点索引列表 """ points = np.asarray(pcd.points) n_points = len(points) # 初始化 visited = np.zeros(n_points, dtype=bool) defect_regions = [] # 构建 k-d 树 pcd_tree = o3d.geometry.KDTreeFlann(pcd) for i in range(n_points): if visited[i]: continue # 检查是否为潜在缺陷种子 if abs(K_array[i]) < K_thres or (H_thres < 0 and H_array[i] >= H_thres) or (H_thres > 0 and H_array[i] <= H_thres): visited[i] = True continue # 开始区域生长 region = [i] queue = [i] visited[i] = True while queue: current = queue.pop(0) # 搜索当前点的邻域(半径内) [k, idx, _] = pcd_tree.search_radius_vector_3d(pcd.points[current], cluster_radius_mm) for j in idx: if j == current or visited[j]: continue # 检查邻点曲率是否符合同一缺陷类型 if (abs(K_array[j]) >= K_thres and ((H_thres < 0 and H_array[j] < H_thres) or (H_thres > 0 and H_array[j] > H_thres))): region.append(j) visited[j] = True queue.append(j) # 过滤小区域 if len(region) >= min_points: defect_regions.append(region) return defect_regions # 使用:识别凹陷(H_thres < 0) defect_regions_concave = region_growing_defects( pcd_final, K, H, K_thres=5e-5, H_thres=-5e-4, # 更严格阈值,减少误报 cluster_radius_mm=1.5, min_points=15 )

避坑:search_radius_vector_3d返回的idx包含中心点自身(j==current),循环中必须if j == current: continue,否则会无限递归。K_thres和H_thres需根据实际点云尺度调整——轨道门数据中,K_thres=5e-5对应曲率半径约 4.5mm,刚好覆盖微细缺陷。

4.3 最小二乘平面拟合:用o3d.geometry.PointCloud.estimate_plane()量化缺陷深度

区域生长得到defect_regions后,每个区域是一堆三维点。但“凹陷有多深”不能只看Z坐标极差——因为轨道门本身是曲面。正确做法是:对每个缺陷区域点云,用最小二乘拟合一个局部平面,再计算区域内所有点到该平面的有符号距离,最大正值为凸起高度,最大负值为凹陷深度。Open3D 的estimate_plane()返回(a,b,c,d)满足ax+by+cz+d=0,距离公式为|a*x+b*y+c*z+d|/sqrt(a²+b²+c²),符号由a*x+b*y+c*z+d决定。

def quantify_defect_depth(pcd, region_indices, plane_dist_threshold_mm=0.05): """ 量化缺陷区域的深度/高度 :param pcd: 点云 :param region_indices: 缺陷区域点索引列表 :param plane_dist_threshold_mm: 平面拟合内点距离阈值 :return: depth_mm (负为凹), height_mm (正为凸), plane_model """ # 提取区域点云 region_pcd = pcd.select_by_index(region_indices) # 拟合平面(Open3D 内置,鲁棒性强) plane_model, inliers = region_pcd.segment_plane( distance_threshold=plane_dist_threshold_mm, ransac_n=3, num_iterations=100 ) # plane_model = [a,b,c,d] for ax+by+cz+d=0 # 计算所有点到平面的有符号距离 points = np.asarray(region_pcd.points) a, b, c, d = plane_model distances = (a*points[:,0] + b*points[:,1] + c*points[:,2] + d) / np.sqrt(a*a + b*b + c*c) # 深 <p> <a href="https://download.csdn.net/download/weixin_56760882/89550894" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询