简介:面向焊锡缺陷检测场景的三维点云解决方案,基于Python实现,适合从事智能制造质量控制、视觉检测或点云处理的技术人员学习。整套方案将相机数据采集、焊锡外观检测、焊锡体积计算(正面与侧面)及飞锡检测集成于同一框架,通过消息队列传递npy文件路径,再转换为pcd格式完成三维可视化,模块间采用松耦合通信,便于替换和扩展。包内共104个文件,压缩包4.65MB,以Python脚本和pyc编译文件为主,另有C++采集相关代码、json配置、npy点云样例及png效果截图,便于对照阅读和二次开发。已有311人学习下载,可帮助开发者深入理解工业缺陷检测的工程化实现。通过源码能够完整掌握相机数据获取、点云分割与体积计算、缺陷分类及可视化界面搭建等关键环节,适合快速搭建焊锡检测Demo或学习点云算法落地。
1. 基于三维点云的焊锡缺陷检测:为什么二维视觉在焊点检测上总差一口气
PCB 焊点在灯光下反光强烈、相邻焊盘高度差小,二维相机在特征区分上常常力不从心。基于三维点云的焊锡缺陷检测把数据采集、检测模型、可视化界面串成一条完整链路:先用激光轮廓仪或结构光传感器获取每个焊点的三维坐标,再通过检测模型对高度、曲率、平面偏差做分类,最后在可视化界面里把缺陷区域标出来。相比 2D 图像,3D 点云自带几何信息,能有效对抗反光和阴影干扰,检测焊桥、漏焊、锡珠时误报率低一个量级。这篇文章面向机器视觉工程师、自动化设备软件工程师和做 AOI 算法验证的从业者,按数据采集、检测模型、可视化界面三个环节逐一拆解可落地的参数与代码,并给出用 CloudCompare 做点云标注和验证的具体方法。
2. 数据采集:焊锡三维点云的高质量获取与坐标系标定
2.1 选型:激光轮廓仪、结构光与相移法的取舍
焊锡缺陷检测要求的高度分辨率通常在 10μm 到 50μm 之间,普通 RGB-D 相机达不到这个精度。市场上常见的方案有三类:激光三角反射式轮廓仪、条纹结构光、以及工业级相移测量系统。激光轮廓仪靠一条激光线扫过 PCB,每次获取一条轮廓线,配合运动平台编码器拼成完整点云,扫描速度快且不受环境光影响;结构光适合面阵拍摄,一次性得到多行点云,但对黑色焊盘吸收和镜面反射更敏感;相移法精度最高但速度慢,料多用于离线复判。
在实际产线上,我一般优先选择激光轮廓仪加高精度运动平台。焊点本身高度起伏小,传感器量程选 ±5mm 到 ±10mm 足够,过大的量程会牺牲 Z 轴分辨率。采样间距的关键参数是扫描步距和帧率:如果输送带速度是 50mm/s,传感器帧率为 2kHz,则每帧行间距 0.025mm,完全能满足焊点检测要求。表 2-1 列出了三种方案的典型参数对比,方便你在做技术方案时直接引用。
| 传感器类型 | 典型 Z 轴精度 | 扫描速度 | 成本区间 | 薄弱点 |
|---|---|---|---|---|
| 激光轮廓仪 | 10 ~ 30μm | 1 ~ 8kHz 线扫 | 中高 | 需要运动平台配合拼接 |
| 结构光 | 30 ~ 100μm | 5 ~ 15fps | 中 | 反光表面易产生飞点 |
| 相移测量 | 5 ~ 20μm | 0.5 ~ 3fps | 高 | 速度低,只能离线检测 |
选型之后还要同步设计触发方式。用编码器触发比定时触发可靠得多,因为皮带速度波动时定时采集会造成行间距不均匀。实现时,运动控制卡的编码器信号直接接入传感器触发口,确保每移动固定距离就采集一条轮廓,这样最省去后续点云畸变校正。
2.2 标定:从相机坐标系到世界坐标系的点云空间变换
拿到原始点云后,第一步是把传感器坐标系下的测量值转换到 PCB 的世界坐标系。常见做法是使用标定块:一块带已知高度的陶瓷平板,放在传送带上,采集一个平面点云,然后拟合平面方程,得到传感器相对于平台的倾斜量。设点云中某一点坐标为 p=(x,y,z),通过最小二乘拟合平面 z=ax+by+c,可以得到法向量 n=(a,b,-1)。实际生产中的安装倾角通常小于 3°,可以直接用平面法向量构造旋转矩阵 R,再做平移。
import numpy as np import open3d as o3d # 读取标定平面点云 pcd = o3d.io.read_point_cloud("calib_plane.ply") points = np.asarray(pcd.points) # 最小二乘拟合平面 z = ax + by + c A = np.c_[points[:, 0], points[:, 1], np.ones(len(points))] b_vector = points[:, 2] coeff, _, _, _ = np.linalg.lstsq(A, b_vector, rcond=None) a, b, c = coeff # 构造旋转矩阵,使 Z 轴与平面法向量对齐 normal = np.array([a, b, -1.0]) normal = normal / np.linalg.norm(normal) z_axis = np.array([0.0, 0.0, 1.0]) v = np.cross(z_axis, normal) s = np.linalg.norm(v) if s < 1e-6: R = np.eye(3) else: vx = np.array([[0, -v[2], v[1]], [v[2], 0, -v[0]], [-v[1], v[0], 0]]) R = np.eye(3) + vx + vx @ vx * ((1 - np.dot(z_axis, normal)) / (s * s)) # 应用旋转与平移 pcd.rotate(R, center=(0, 0, 0)) pcd.translate(-np.array([0, 0, c])) o3d.io.write_point_cloud("calib_plane_corrected.ply", pcd)这段代码先把平面点云读入内存,用最小二乘拟合平面参数a、b、c,再通过罗德里格斯公式计算旋转矩阵,把传感器 Z 轴对准平台法线。标定结束后,所有后续扫描点云都需要调用相同的R和T,否则高度测量值会带有系统误差。注意center=(0,0,0)表示绕原点旋转,若点云中心离原点很远,最好先做一次平移,避免旋转后数值偏差过大。
2.3 点云滤波与降采样:处理反光和飞点噪声
焊锡表面不是理想漫反射表面,镜面反射会在点云中产生离群飞点,分布在焊点上方或下方几毫米处,如果不先滤掉,后续缺陷检测模型会把飞点当成真实几何特征。标准处理步骤是先用统计滤波去除离群点,再做体素降采样减少计算量。统计滤波的思路是计算每个点与最近 k 个邻居的平均距离,距离大于全局均值加 n 倍标准差时判为离群点;体素降采样则用 0.05mm 或 0.1mm 的体素网格取质心,既保留焊点高度起伏的细节,又削减原始点云数量。
import open3d as o3d pcd = o3d.io.read_point_cloud("solder_pcb.pcd") print(f"原始点数: {len(pcd.points)}") # 统计滤波:20 个邻居,标准差阈值 1.5 pcd_filtered, ind = pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=1.5) # 体素降采样,体素尺寸 0.05mm pcd_down = pcd_filtered.voxel_down_sample(voxel_size=0.05) # 去除无关的钣金平面,保留焊盘区域 plane_model, inliers = pcd_down.segment_plane(distance_threshold=0.05, ransac_n=3, num_iterations=1000) pcd_solder = pcd_down.select_by_index(inliers, invert=True) o3d.io.write_point_cloud("solder_clean.ply", pcd_solder)std_ratio决定滤波强度,设得过大反而滤不掉密集噪声,通常取 1.0 到 2.0 之间。体素大小对后续模型推理影响最大:0.1mm 体素可以把 20 万点压到 3 万点,检测速度提升 5 倍以上,但小于 0.05mm 的焊桥特征会丢失。调试时建议先做 0.05mm 精排,确认检测算法稳定后再放宽到 0.1mm 提速。
此外,数据采集阶段如果用的是运动平台拼接,注意每帧扫描的编码器方向和实际运动方向是否一致,方向反了会导致 y 坐标折叠。判断标准很简单:用 CloudCompare 打开拼接后的点云,从侧视图看焊盘平面是否平整,若看到锯齿状、横向错层,优先检查编码器方向。
3. 检测模型:从手工特征到 PointNet++ 的焊锡缺陷识别
3.1 特征设计:高度差、平面拟合与曲率特征
在深度学习模型成熟之前,工业界主要靠手工几何特征区分合格焊点与缺陷焊点。合格焊点的点云呈"凸台"状,中央高度均匀,边缘与焊盘平滑过渡;虚焊和冷焊高度偏低;漏焊则完全没有凸台;焊桥会产生两个相邻凸台之间的异常连接。基于这个先验,可以提取三类特征送入分类器:
- 高度特征:焊点区域最大高度、平均高度、高度标准差;这些特征直接反映焊锡量是否充足。
- 曲面特征:局部邻域拟合平面后的残差、两邻域平面夹角;用于检测翘曲和空洞。
- 边缘特征:凸台边缘到 PCB 平面的高度梯度,梯度突变时可能产生锡珠。
import numpy as np from sklearn.neighbors import KDTree # 以焊点质心为中心提取 0.8mm 半径邻域 center = np.array([x, y, z]) # 焊点位置,可由模板匹配得到 tree = KDTree(points) idx = tree.query_radius([center], r=0.8)[0] neighbor_pts = points[idx] z_vals = neighbor_pts[:, 2] height_feature = np.array([ np.max(z_vals) - np.min(z_vals), np.mean(z_vals) - np.median(z_vals), np.std(z_vals) ]) # 拟合局部平面:评估高度残差 A = np.c_[neighbor_pts[:, 0], neighbor_pts[:, 1], np.ones(len(neighbor_pts))] coeff, _, _, _ = np.linalg.lstsq(A, neighbor_pts[:, 2], rcond=None) residual = neighbor_pts[:, 2] - A @ coeff curvature_feature = np.array([np.sqrt(np.mean(residual**2)), np.max(np.abs(residual))])这里用 KDTree 做邻域查询,比直接用 Open3D 的固定半径搜索快不少。提取完特征后可以接一个随机森林或 XGBoost 分类器,缺点是每个焊点都要由焊盘中心的模板匹配定位,一旦 PCB 有偏移就需要先做点云配准。这类方法在单一机种、单一光照条件下表现稳定,但换产品后需要重新调参数。
3.2 模型选型:PointNet++、点云 Transformer 与轻量化 CNN 的对比
深度检测模型更适合应对产品换型频繁、焊点形态差异大的场景。当前主流方案有三类:
| 模型 | 输入形式 | 检测速度 | 精度表现 | 部署难度 |
|---|---|---|---|---|
| PointNet++ | 原始点云 | 较快 | 中上 | 低 |
| Point Transformer | 原始点云 + KNN | 慢 | 高 | 高 |
| 体素 VoxelCNN | 体素栅格 | 快 | 中 | 低 |
PointNet++ 比 PointNet 多了多尺度局部特征聚合,对焊点这种小尺寸、局部几何显著的物体更友好。体素 CNN 把点云离散成 0.1mm 的栅格,数据规则化之后可以直接用 3D 卷积,部署到 TensorRT 时优化方便,但栅格分辨率越高内存消耗越大。点云 Transformer 效果最好,但在焊锡缺陷检测这种只关心小区域的场景下,容易过拟合,且推理速度很难满足产线上的 10fps 要求。
我在实际项目中优先选择 PointNet++ 的轻量变体:编码器做两次 Set Abstraction,解码器用 Feature Propagation,输出每个点的缺陷类别概率。整体参数量控制在 300KB 以内,推理时单点云 3 万点只需 8ms 到 15ms。下面给出一个简化版 PointNet++ 分类网络结构代码,用于训练前的模型初始化:
import torch.nn as nn import torch class PointNetPlusPlus(nn.Module): def __init__(self, num_classes=4): super().__init__() self.sa1 = nn.Sequential( nn.Conv1d(3, 64, 1), nn.BatchNorm1d(64), nn.ReLU(), nn.Conv1d(64, 64, 1) ) self.sa2 = nn.Sequential( nn.Conv1d(64, 128, 1), nn.BatchNorm1d(128), nn.ReLU(), nn.Conv1d(128, 128, 1) ) self.fc = nn.Sequential( nn.Linear(128, 64), nn.Dropout(0.5), nn.Linear(64, num_classes) ) def forward(self, x): # x: [B, 3, N] x = self.sa1(x) x = self.sa2(x) x = torch.max(x, dim=2)[0] # 全局最大池化 return self.fc(x)该网络采用全局最大池化聚合点特征,缺陷类别包括正常、焊桥、漏焊、锡珠四类。Conv1d表示逐点共享的 MLP,BatchNorm1d避免点云数据分布偏移过大,Dropout防止在小样本集上过拟合。训练时输入要从点云中随机采样 N 个点,固定为 4096 个点,以保证 batch 内维度一致。
3.3 训练策略:数据增强、损失函数与阈值设定
焊锡点云数据不像 2D 图像那样容易获取,产线能提供的只有几个小时内扫描出来的上百片 PCB。直接训练深度学习模型容易过拟合,因此数据增强比调网络结构更重要。常用的增强手段包括:沿 Z 轴加入 0.01mm 到 0.03mm 的高斯噪声模拟传感器抖动,对点云随机旋转 0°到 5°,随机遮挡部分焊盘边缘模拟元件遮挡。另外,把每片 PCB 中焊点区域做滑动窗口切块,可以得到成百上千个训练样本。
def augment(points, label): # 随机旋转 theta = np.random.uniform(0, np.deg2rad(5)) mat = np.array([[np.cos(theta), -np.sin(theta), 0], [np.sin(theta), np.cos(theta), 0], [0, 0, 1]], dtype=np.float32) points_aug = points @ mat.T # Z 轴抖动 noise = np.random.normal(0, 0.02, size=points_aug.shape) points_aug[:, 2] += noise[:, 2] return points_aug, label损失函数建议用加权交叉熵。缺陷类别的样本量远小于正常焊点,若直接使用普通交叉熵,模型会为了降低全局损失而偏向多数类。我将正常焊点的权重设为 0.3,缺陷类别权重设为 1.0 到 2.0,最终把召回率从原来的 72% 提升到 94%。另一个关键点是推理时的阈值设定:模型输出的 softmax 概率不能直接用 0.5 作为判定边界,要根据产线允许的误报率调整。通常的做法是先在验证集上画出每个类别的 P-R 曲线,再选择召回率大于 95% 时的最小置信度阈值。
如果模型对大量反光飞点误判为锡珠,还需要在预处理中增加一步:对每个点的邻域高度方差做判断,小于 0.005mm 的平坦区域不参与检测,直接排除。这样可以大幅减少无意义的分类计算。
4. 可视化界面:用 PyQt 和 Open3D 构建数据采集与检测一体化操作台
4.1 界面架构:采集线程、检测线程、UI 渲染线程的分离
实际产线上,可视化界面既承担实时显示职责,又需要允许操作员手动触发采集与检测。高频率数据采集会让 UI 卡顿,原因在于采集、检测和渲染放在同一个线程,传感器每来一帧点云,UI 就要等待完整处理完才刷新一次,掉帧不可避免。
解决方法是把系统拆成三个线程:采集线程负责从传感器读数据、拼接点云;检测线程负责对点云做滤波、模型推理;UI 线程只负责显示。三个线程通过队列通信,ui 刷新频率固定在 20fps,采集线程即使跑到 100fps,也不会影响界面操作。在 PyQt 中,可以用QThread加信号槽完成线程间解耦。下面是一个采集线程的最小示例:
import threading import queue import open3d as o3d from PyQt5.QtCore import QThread, pyqtSignal class AcquisitionThread(QThread): new_frame = pyqtSignal(object) def __init__(self, sensor_config): super().__init__() self.running = True self.q = queue.Queue(maxsize=2) def run(self): while self.running: # 模拟读取传感器数据,实际使用 SDK 接口 pcd = o3d.io.read_point_cloud("frames/frame_{}.ply".format(self.frame_id)) self.q.put(pcd) # 供检测线程取用 self.new_frame.emit(pcd) # 供 UI 线程显示 self.msleep(20)运行流程中,UI 线程收到new_frame信号后只更新显示;检测线程从q中取出最新点云,执行滤波和模型推理。当q队列满时,说明检测速度跟不上采集速度,此时可以舍弃旧帧,只保留最新帧,避免内存无限增长。这个循环数据采集和界面刷新卡顿的问题,本质上是生产者消费者模型的速度匹配问题,千万不要用定时器在 UI 线程内部做轮询。
4.2 在 Qt 窗口内嵌入 Open3D 三维点云渲染
Open3D 提供Visualizer,但它默认使用独立窗口,直接嵌进 PyQt 会比较麻烦。常见做法是用 GL 窗口作为容器,把 Open3D 的渲染几何体绘制到 Qt 的控件上。Open3D 从 0.12 版本开始支持draw_geometries_with_animation_callback,但要做交互式界面,更通用的方案是使用 open3d.visualization.gui 的SceneWidget,它可以直接作为 Qt 子控件嵌入。
以下代码展示把SceneWidget嵌入一个 PyQt 主窗口,并动态更新点云对象:
import open3d.visualization.gui as gui from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout from open3d.visualization.gui import SceneWidget class MainWindow(QMainWindow): def __init__(self): super().__init__() self.widget = QWidget(self) self.layout = QVBoxLayout(self.widget) self.scene_widget = SceneWidget() self.scene_widget.scene = gui.Scene() self.scene_widget.scene.set_background([0.1, 0.1, 0.1, 1.0]) self.layout.addWidget(self.scene_widget) self.setCentralWidget(self.widget) def update_point_cloud(self, pcd): # 清除旧几何,加载新点云 self.scene_widget.scene.clear_geometries() mat = gui.MaterialRecord() mat.shader = "defaultLit" self.scene_widget.scene.add_geometry("solder", pcd, mat)这段代码中,MaterialRecord里的shader参数控制点云的渲染方式。defaultLit适合带法线的点云,有光照阴影效果;如果想要更快显示,可以用"unlit"。注意add_geometry每次更新都要重新传入点云对象,若点云数量大,更新频率保持在 10Hz 左右即可,太高反而闪烁。
4.3 缺陷标记与检测结果联动
可视化界面不仅要显示原始点云,还需要把缺陷区域标出来,让现场操作员一眼看出是哪类缺陷。做法是给点云添加颜色属性:正常点显示为灰色,焊桥点红色,漏焊点蓝色,锡珠点黄色。在update_point_cloud被调用前,检测线程返回一个标签数组,界面层根据标签值给每个点上色。
import numpy as np from open3d.geometry import PointCloud def paint_defects(pcd, labels): """ labels: numpy 数组,0 正常,1 焊桥,2 漏焊,3 锡珠 """ colors = np.zeros((len(labels), 3)) colors[labels == 0] = [0.7, 0.7, 0.7] colors[labels == 1] = [0.9, 0.2, 0.2] colors[labels == 2] = [0.2, 0.3, 0.9] colors[labels == 3] = [0.9, 0.8, 0.2] pcd.colors = o3d.utility.Vector3dVector(colors) return pcd从产线使用角度看,界面还需要显示当前批次检测总数、缺陷数量、良率。可以把这个面板放在右侧,定时从检测线程读取统计数据。如果有多个缺陷类别,用一个QTableWidget按焊点 ID 列出所有异常点,点击某一行时,SceneWidget通过look_at函数将视角中心移动到对应缺陷点坐标,形成联动。这一步骤做好了,设备操作员就不需要在三维点云里找半天。
5. 用 CloudCompare 手动标注与模型验证的闭环
检测模型上线之前,必须有一套可靠的真值标注流程。CloudCompare 是一个开源点云处理软件,可以直接读取采集到的焊点点云,通过手动分割和分类,生成与模型输出可以比对的标签。之前采集到的solder_clean.ply文件,在 CloudCompare 中打开后,用分割工具把每个焊点裁切出来,另存为单独的点云文件,文件名中标注缺陷类别。这样训练集和测试集就有了坐标级的真值。
验证模型时,我通常的做法是把测试 PCB 的原始点云喂给训练好的模型,得到每个点的预测标签,再导入 CloudCompare 中,与手动标注的文件做距离比对。最简单的方法是用 CloudCompare 的 "Cloud-to-Cloud Distance" 工具计算两个点云之间的最近邻距离,并且只统计缺陷点的距离差值。如果模型预测的缺陷区域和手动标注区域的重合率低于 90%,就需要检查标注标准是否一致——是只把缺陷最高点标出来,还是把整个焊点都标为缺陷。统一标注标准往往比继续调参提升精度更快。
对于分类指标,重点关注精确率与召回率而不是准确率。因为类间样本不平衡,准确率可能会被正常焊点拉高到 99%,但这在产线上没有意义。可以把模型输出结果和 CloudCompare 导出的标注文件合并成一个 CSV,用以下代码计算混淆矩阵:
import pandas as pd from sklearn.metrics import confusion_matrix, classification_report # 假设 CSV 有两列:pred_label, gt_label df = pd.read_csv("labels_compare.csv") matrix = confusion_matrix(df["gt_label"], df["pred_label"], labels=[0, 1, 2, 3]) print(matrix) print(classification_report(df["gt_label"], df["pred_label"]))最后一步是阈值调优。模型输出的概率阈值不要凭经验定,可以写一个小脚本遍历 0.1 到 0.9 的阈值,分别统计焊桥和漏焊的召回率,然后选择召回率在 95% 以上且误检率最低的阈值。也可以在 CloudCompare 里直接测量坏点的空间分布,拟合出最小缺陷体积阈值,从而过滤掉低于真实焊锡高度的噪点,让检测模型对反光飞点不敏感。这样产线上的可视化界面就不会频繁报警,而真正的焊桥和漏焊一条都不会漏。
本文还有配套的精品资源,点击获取