用大菠萝拍摄原始pcb点云数据进行分割(可以直接人工分割)
简单分割
进行仔细分割(这里我偷工减料,用软件分割 嘻嘻😁)
下面制作点云模板 读点云选择焊点附近特征点
通过模板点云和3个特征点与未检测的焊点进行定位
用icp配准来生成变化矩阵 用3个特征点乘icp变换矩阵
通过这些点来分割要检测的点云
犹豫定位的是大概位置所以这里要对点云进行精致处理这里我用了几种方法测试
直接用平面分割
效果一般
后面用别的方法
边缘效果太差了
后来想到了用边缘来定位平面(没有实现)
后面简单修修改改下面效果
这个效果勉强可以
下面给出部分代码
import open3d as o3d
import numpy as np
# 定义全局变量来存储选中的点
GLOBAL_SELECTED_POINTS = []
class PointCloudSelector:
def __init__(self, file_path):
"""
初始化点云选择器
:param file_path: 点云文件路径(支持.ply, .pcd, .xyz等格式)
"""
self.pcd = o3d.io.read_point_cloud(file_path) # 读取点云
self.selected_points = [] # 存储选中点的坐标
self.vis = o3d.visualization.VisualizerWithEditing() # 带编辑功能的可视化器
def start_selection(self):
"""启动交互式选点可视化界面"""
# 创建窗口并添加点云
self.vis.create_window(window_name='点云选点工具', width=1280, height=720)
self.vis.add_geometry(self.pcd)
# 设置可视化参数
opt = self.vis.get_render_option()
opt.point_size = 2.0 # 原始点大小
opt.background_color = np.array([0.1, 0.1, 0.1]) # 背景色
# 启动交互界面
print("操作指南:\n- 左键单击选择点\n- 按'R'重置选择\n- 按'S'保存结果\n- 按'Q'退出")
self.vis.run() # 阻塞直到窗口关闭
# 获取用户选择的点索引
picked_points = self.vis.get_picked_points()
self._process_selection(picked_points)
self.vis.destroy_window()
def _process_selection(self, indices):
"""处理选中的点索引"""
global GLOBAL_SELECTED_POINTS
if not indices:
print("未选择任何点!")
return
# 从索引获取坐标
points = np.asarray(self.pcd.points)
self.selected_points = points[indices]
GLOBAL_SELECTED_POINTS = self.selected_points.copy() # 将选中的点赋值给全局变量
# 高亮显示选中点
colors = np.asarray(self.pcd.colors)
if colors.size == 0:
colors = np.tile([0.5, 0.5, 0.5], (points.shape[0], 1))
# 将选中点标记为红色
colors[indices] = [1, 0, 0]
self.pcd.colors = o3d.utility.Vector3dVector(colors)
print(f"成功选择 {len(indices)} 个点")
def save_results(self, save_path="selected_points.ply"):
"""保存结果(支持.ply/.xyz格式)"""
if len(self.selected_points) == 0:
print("无选中点可保存!")
return
# 创建包含选中点的点云
selected_pcd = o3d.geometry.PointCloud()
selected_pcd.points = o3d.utility.Vector3dVector(self.selected_points)
# 保存文件
o3d.io.write_point_cloud(save_path, selected_pcd)
print(f"已保存选中点到 {save_path}")
def depth_to_point_cloud(depth_map, intrinsic_matrix):
# 获取深度图的高度和宽度
height, width = depth_map.shape
# 创建一个网格,其中每个像素对应于深度图中的一个点
x, y = np.meshgrid(np.arange(width), np.arange(height))
x = x.flatten()
y = y.flatten()
# 获取深度图中的深度值
depth = depth_map.flatten()
# 将像素坐标 (x, y) 转换为相机坐标系下的坐标 (X, Y, Z)
X = (x - intrinsic_matrix[0, 2]) * depth / intrinsic_matrix[0, 0]
Y = (y - intrinsic_matrix[1, 2]) * depth / intrinsic_matrix[1, 1]
Z = depth
# 将 (X, Y, Z) 坐标堆叠成点云
point_cloud = np.vstack((X, Y, Z)).T
return point_cloud
def rotate_point_cloud(pcd, rotation_matrix):
return np.dot(pcd, rotation_matrix.T)
def crop_point_cloud_around_selected(pcd, selected_points, radius=0.003):
"""
根据选中的点为中心,以指定半径进行点云裁剪
:param pcd: 输入的点云
:param selected_points: 选中的点
:param radius: 裁剪半径,单位为米
:return: 裁剪后的点云, 裁剪区域的索引
"""
all_points = np.asarray(pcd.points)
inlier_indices = []
for center_point in selected_points:
distances = np.linalg.norm(all_points - center_point, axis=1)
inlier_indices.extend(np.where(distances <= radius)[0])
inlier_indices = sorted(set(inlier_indices))
cropped_pcd = pcd.select_by_index(inlier_indices)
return cropped_pcd, inlier_indices
# 在主程序的裁剪部分修改为:
# 根据选中的点进行裁剪
if __name__ == "__main__":
# 使用示例
selector = PointCloudSelector("cloud.ply") # 替换为你的点云文件路径
selector.start_selection()
# 保存选中的点(默认保存为ply格式)
selector.save_results("selected_points.ply")
# 可选:保存包含原始点云+高亮选择的完整场景
o3d.io.write_point_cloud("full_scene_with_selection.ply", selector.pcd)
theta = np.radians(90)
rotation_matrix = np.array([
[np.cos(theta), -np.sin(theta), 0],
[np.sin(theta), np.cos(theta), 0],
[0, 0, 1]
])
inverse_rotation_matrix = rotation_matrix.T
print(GLOBAL_SELECTED_POINTS)
if len(GLOBAL_SELECTED_POINTS) > 0:
restored_selected_points = rotate_point_cloud(GLOBAL_SELECTED_POINTS, inverse_rotation_matrix)
print("恢复后的选中点:")
print(restored_selected_points)
else:
print("没有选中的点,无法进行恢复操作。")
if len(GLOBAL_SELECTED_POINTS) > 0:
selector1 = PointCloudSelector("cloud1.ply") # 替换为你的点云文件路径
original_pcd = selector1.pcd
# 执行裁剪并获取索引
cropped_pcd, inlier_indices = crop_point_cloud_around_selected(original_pcd, GLOBAL_SELECTED_POINTS,
radius=0.03)
# 为原始点云设置颜色
colors = np.asarray(original_pcd.colors)
if colors.size == 0:
colors = np.tile([0.5, 0.5, 0.5], (len(original_pcd.points), 1))
# 裁剪区域设为红色,其他保持原颜色
colors[inlier_indices] = [1, 0, 0]
original_pcd.colors = o3d.utility.Vector3dVector(colors)
这个代码主要实现选择焊点的关键点(可以理解中心点),我没有没有给出icp配准 ,这里我变换矩阵使用cloudcompare来获得的
import numpy as np
import open3d as o3d
def crop_point_cloud(point_cloud, centers, range_size):
"""
根据多个中心点坐标和范围大小对三维点云进行裁剪
:param point_cloud: 输入的点云数据,类型为 open3d.geometry.PointCloud
:param centers: 中心点坐标列表,类型为 numpy.ndarray,形状为 (n, 3),n 为中心点的数量
:param range_size: 范围大小,类型为 float
:return: 裁剪后的点云数据,类型为 open3d.geometry.PointCloud
"""
# 将点云数据转换为 numpy 数组
points = np.asarray(point_cloud.points)
mask = np.zeros(points.shape[0], dtype=bool)
# 遍历每个中心点
for center in centers:
# 计算每个点到当前中心点的距离
distances = np.linalg.norm(points - center, axis=1)
# 筛选出距离当前中心点在指定范围内的点
current_mask = distances <= range_size
# 合并筛选结果
mask = np.logical_or(mask, current_mask)
# 创建新的点云对象
cropped_point_cloud = o3d.geometry.PointCloud()
cropped_point_cloud.points = o3d.utility.Vector3dVector(points[mask])
return cropped_point_cloud
# 读取点云文件(假设为 .pcd 格式)
file_path = "cloud1.ply"
try:
point_cloud = o3d.io.read_point_cloud(file_path)
print(f"成功读取点云文件,点数: {len(point_cloud.points)}")
except Exception as e:
print(f"读取点云文件时出错: {e}")
exit(1)
# 定义多个中心点坐标和范围大小
centers = np.array([
[-43.51729965 , 33.06580048 , 1.83599997],
[-41.23210144 , 36.9178009 , 1.8585 ],
[-38.82870102 , 40.06980133 , 1.95299995]])
range_size = 2.2
# 进行点云裁剪
cropped_point_cloud = crop_point_cloud(point_cloud, centers, range_size)
print(f"裁剪后的点数: {len(cropped_point_cloud.points)}")
# 保存裁剪后的点云
output_file_path = "cropped_point_cloud.pcd"
o3d.io.write_point_cloud(output_file_path, cropped_point_cloud)
print(f"裁剪后的点云已保存到 {output_file_path}")
# 可视化裁剪前后的点云
vis = o3d.visualization.Visualizer()
vis.create_window(window_name="Original and Cropped Point Cloud")
if len(cropped_point_cloud.points) > 0:
cropped_point_cloud.paint_uniform_color([0, 1, 0])
# 添加裁剪后的点云
vis.add_geometry(cropped_point_cloud)
# 设置原始点云颜色为蓝色
if len(point_cloud.points) > 0:
point_cloud.paint_uniform_color([0, 0, 1])
# 添加原始点云
vis.add_geometry(point_cloud)
# 更新可视化窗口
vis.update_geometry(point_cloud)
vis.update_geometry(cropped_point_cloud)
vis.run()
vis.destroy_window()
根据点来定位焊点大概位置,我测试过有时候颜色不显示不知道什么原因没有查出来
import numpy as np
import open3d as o3d
def filter_points_by_plane_distance(point_cloud_array, plane_params, distance_threshold):
"""
根据平面法向距离阈值过滤点云
:param point_cloud_array: 点云数组 (n, 3),每行是一个点的 [x, y, z]
:param plane_params: 平面参数 (a, b, c, d),对应平面方程 ax + by + cz + d = 0
:param distance_threshold: 距离阈值,保留距离大于该值的点
:return: 过滤后的点云数组
"""
a, b, c, d = plane_params
# 计算每个点到平面的距离
distances = np.abs(a * point_cloud_array[:, 0] + b * point_cloud_array[:, 1] + c * point_cloud_array[:, 2] + d) / \
np.sqrt(a ** 2 + b ** 2 + c ** 2)
# 保留距离大于阈值的点
return point_cloud_array[distances > distance_threshold]
class ToolDetectBumpDefect:
def __init__(self, ui):
self.ui = ui
def getCloudXYZ(self):
# 读取点云文件(需确保文件路径正确)
pcd = o3d.io.read_point_cloud("cropped_point_cloud.pcd")
return pcd
def emitInfo(self, message, flag):
print(message)
def fitPlaneByPcl(self, cloud, dist_thresh):
# 使用 Open3D 进行平面拟合
plane_model, inliers = cloud.segment_plane(
distance_threshold=dist_thresh,
ransac_n=1,
num_iterations=1000
)
inliers_cloud = cloud.select_by_index(inliers)
return inliers_cloud, plane_model
def showPointCloud(self, cloud):
# 使用 Open3D 可视化点云
o3d.visualization.draw_geometries([cloud])
def genPlaneCloud(self, plane, step, cloud):
# 生成平面点云
points = np.asarray(cloud.points)
min_x = np.min(points[:, 0])
max_x = np.max(points[:, 0])
min_y = np.min(points[:, 1])
max_y = np.max(points[:, 1])
x = np.arange(min_x, max_x, step)
y = np.arange(min_y, max_y, step)
X, Y = np.meshgrid(x, y)
A, B, C, D = plane
Z = -(A * X + B * Y + D) / C
plane_points = np.vstack([X.flatten(), Y.flatten(), Z.flatten()]).T
plane_cloud = o3d.geometry.PointCloud()
plane_cloud.points = o3d.utility.Vector3dVector(plane_points)
return plane_cloud
def showTwoPointClouds(self, cloud1, cloud2):
# 可视化两个点云
o3d.visualization.draw_geometries([cloud1, cloud2])
def on_runBtn_clicked(self):
cloudXYZ = self.getCloudXYZ()
if len(cloudXYZ.points) == 0:
self.emitInfo("Point Cloud Clustgering: The input point cloud is empty.", True)
return
# 平面拟合
plane_model, inliers = cloudXYZ.segment_plane(
distance_threshold=0.08,
ransac_n=3,
num_iterations=1000
)
inliersPoints = cloudXYZ.select_by_index(inliers)
if len(inliersPoints.points) == 0:
return
# 打印分割后的点云数量
print(f"分割后的点云数量: {len(inliersPoints.points)}")
self.showPointCloud(inliersPoints)
# 获取另一半点云的索引
all_indices = np.arange(len(cloudXYZ.points))
outlier_indices = np.setdiff1d(all_indices, inliers)
outlierPoints = cloudXYZ.select_by_index(outlier_indices)
self.showPointCloud(outlierPoints)
# 打印另一半点云的数量
print(f"另一半点云的数量: {len(outlierPoints.points)}")
# 生成平面点云
planeCloud = self.genPlaneCloud(plane_model, float(self.ui.step), cloudXYZ)
# 打印平面模型参数
print(f"平面模型参数: {plane_model}")
self.showTwoPointClouds(cloudXYZ, planeCloud)
# 执行过滤(修改此处:转换点云格式并使用拟合的平面参数)
point_cloud_array = np.asarray(cloudXYZ.points)
distance_threshold = 0.25 # 距离阈值这里我按照相机误差设置的
filtered_cloud_array = filter_points_by_plane_distance(
point_cloud_array, plane_model, distance_threshold
)
# 将过滤结果转成Open3D点云
filtered_cloud = o3d.geometry.PointCloud()
filtered_cloud.points = o3d.utility.Vector3dVector(filtered_cloud_array)
print(f"原始点云数量: {len(cloudXYZ.points)}")
print(f"过滤后点云数量: {len(filtered_cloud.points)}")
self.showPointCloud(filtered_cloud)
# 模拟 UI 数据
class UI:
def __init__(self):
self.distThresh = "0.01"
self.step = "0.1"
self.lineEdit = "0.05"
if __name__ == "__main__":
ui = UI()
detector = ToolDetectBumpDefect(ui)
detector.on_runBtn_clicked()
最后这个就是一个大概分割点云的方法,在处理点云中要注意点云的方向(可以做个点云矫正让点云调整到一个标准的方向),这篇文章只是提供一种思路,代码不一定可以直接用,可以参考修改。
总结
- 从CAD模型提取设计图纸中的焊盘中心点坐标或者用良品点云中提取焊点中心点作为基准来定位焊点的位置
- 在点云数据处理中,将采集的多个视角的点云,对点云进行分离 PCB 主体(方向改正):利用平面分割提取 PCB 基板,排除塑料壳干扰。多视角icp,配准前统一新点云与模板的密度,避免尺度偏差。 采用点云配准算法,将齐次变换后的点云与预先准备的模板进行配准(类比于图像中的模板匹配)。一旦点云与模板点云配准,点云焊点的位置信息可以通过参考模板上焊点的位置 信息推算出来。可以采用阈值分割的方法将属于焊点的3D点云从印刷电路板中分离焊点候选区域,由于焊点大小 0.3 至 0.5 毫米这里需要精细处理,使用 RANSAC 算法拟合平面,过滤掉与平面距离过近的点(保留尽可能多的焊点)。通过平面方程ax + by + cz + d = 0和距离阈值(如 0.25 米)筛选目标点云。