import numpy as np
import open3d as o3d
from scipy.spatial.transform import Rotation as R
from enum import Enum
import trimesh
from sklearn.cluster import DBSCAN
from scipy.interpolate import make_interp_spline
import networkx as nx
from rtree import index
import csv
import os
from tqdm import tqdm # 新增进度条,提升用户体验
# OMPL兼容支持
try:
from ompl import base as ob
from ompl import geometric as og
OMPL_AVAILABLE = True
except ImportError:
OMPL_AVAILABLE = False
print("警告:OMPL库未安装,自动切换为优化后的简化RRT*")
# -------------------------- 1. 枚举与全局配置 --------------------------
class WorkpieceMaterial(Enum):
CARBON_STEEL = "碳钢"
STAINLESS_STEEL = "不锈钢"
ALUMINUM = "铝合金"
class RobotBrand(Enum):
FANUC = "发那科"
ABB = "ABB"
KUKA = "库卡"
WELD_PROCESS_DB = {
(WorkpieceMaterial.CARBON_STEEL, 3, 1.0): ((120, 150), (18, 20), (0.05, 0.08)),
(WorkpieceMaterial.CARBON_STEEL, 6, 1.2): ((150, 190), (20, 24), (0.04, 0.06)),
(WorkpieceMaterial.CARBON_STEEL, 10, 1.2): ((190, 230), (24, 28), (0.03, 0.05)),
(WorkpieceMaterial.STAINLESS_STEEL, 3, 1.0): ((100, 130), (17, 19), (0.04, 0.07)),
(WorkpieceMaterial.STAINLESS_STEEL, 6, 1.2): ((130, 170), (19, 22), (0.03, 0.05)),
(WorkpieceMaterial.ALUMINUM, 3, 1.2): ((140, 180), (20, 22), (0.06, 0.09)),
(WorkpieceMaterial.ALUMINUM, 6, 1.6): ((180, 220), (22, 25), (0.05, 0.07)),
}
# -------------------------- 2. 点云预处理(大点数加速+局部密度自适应) --------------------------
class PointCloudProcessor:
def __init__(self, pcd_file_path: str, workpiece_material: WorkpieceMaterial, wire_diameter: float = 1.2):
self.pcd = self.load_point_cloud(pcd_file_path)
self.filtered_pcd = None
self.weld_seam_points = None
self.material = workpiece_material
self.wire_diameter = wire_diameter
self.voxel_size = self._auto_calculate_voxel_size()
self.max_point_num = 10000 # 最大处理点数,超过则动态下采样
def load_point_cloud(self, file_path: str) -> o3d.geometry.PointCloud:
pcd = o3d.io.read_point_cloud(file_path)
if not pcd.has_points():
raise ValueError("点云文件加载失败,无有效点数据")
# 动态下采样:超过max_point_num时增大voxel_size
point_num = len(pcd.points)
if point_num > self.max_point_num:
scale_factor = np.sqrt(point_num / self.max_point_num)
self.voxel_size *= scale_factor
print(f"点云点数过多({point_num}),动态调整voxel_size为{self.voxel_size:.6f}")
return pcd
def _auto_calculate_voxel_size(self) -> float:
bbox = self.pcd.get_axis_aligned_bounding_box()
diag_length = np.linalg.norm(bbox.max_bound - bbox.min_bound)
return diag_length / 500
def _calculate_block_local_density(self, points_block: np.ndarray, radius: float = 0.01) -> np.ndarray:
"""分块计算局部密度,加速大点数处理"""
n_points = len(points_block)
densities = np.zeros(n_points)
# 对每个点,计算块内半径r内的点数(避免全局计算)
for i in range(n_points):
dists = np.linalg.norm(points_block - points_block[i], axis=1)
densities[i] = np.sum(dists < radius)
return densities
def _get_local_dbscan_params(self, points: np.ndarray) -> tuple:
"""局部密度自适应DBSCAN参数(分块加速)"""
n_points = len(points)
if n_points <= 5000:
# 点数少,全局计算
radius = max(self.voxel_size * 5, 0.005)
densities = self._calculate_block_local_density(points, radius)
else:
# 点数多,分块计算(每块5000点)
n_blocks = (n_points + 4999) // 5000
densities = np.zeros(n_points)
radius = max(self.voxel_size * 5, 0.005)
for i in tqdm(range(n_blocks), desc="分块计算局部密度"):
start_idx = i * 5000
end_idx = min((i+1)*5000, n_points)
block = points[start_idx:end_idx]
block_densities = self._calculate_block_local_density(block, radius)
densities[start_idx:end_idx] = block_densities
# 动态调整k和eps
local_k = np.clip(5 + densities / 2, 5, 30).astype(int)
local_eps = self.voxel_size * (local_k / 2)
return local_eps, local_k
def preprocess_point_cloud(self) -> None:
print("开始点云预处理...")
# 第一步:下采样+全局离群点过滤
downsampled = self.pcd.voxel_down_sample(voxel_size=self.voxel_size)
points = np.asarray(downsampled.points)
center = np.mean(points, axis=0)
relative_distances = np.linalg.norm(points - center, axis=1)
max_relative_dist = np.percentile(relative_distances, 98)
valid_mask = relative_distances <= max_relative_dist
downsampled = downsampled.select_by_index(np.where(valid_mask)[0])
print(f"下采样后点数:{len(downsampled.points)}")
# 第二步:材质适配统计滤波
if self.material == WorkpieceMaterial.ALUMINUM:
self.filtered_pcd, _ = downsampled.remove_statistical_outlier(nb_neighbors=30, std_ratio=2.5)
elif self.material == WorkpieceMaterial.CARBON_STEEL:
self.filtered_pcd, _ = downsampled.remove_statistical_outlier(nb_neighbors=15, std_ratio=1.5)
else:
self.filtered_pcd, _ = downsampled.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0)
print(f"统计滤波后点数:{len(self.filtered_pcd.points)}")
# 第三步:局部密度自适应DBSCAN形态学滤波
self.filtered_pcd = self._morphological_filter(self.filtered_pcd)
print(f"DBSCAN滤波后点数:{len(self.filtered_pcd.points)}")
# 第四步:平面分割(自适应距离阈值)
plane_model, inliers = self.filtered_pcd.segment_plane(
distance_threshold=self.voxel_size * 2, ransac_n=3, num_iterations=1000
)
self.filtered_pcd = self.filtered_pcd.select_by_index(inliers, invert=True)
print(f"平面分割后点数:{len(self.filtered_pcd.points)}")
print("点云预处理完成!")
def _morphological_filter(self, pcd: o3d.geometry.PointCloud) -> o3d.geometry.PointCloud:
"""局部密度自适应DBSCAN:大点数加速版"""
points = np.asarray(pcd.points)
if len(points) < 10:
print("警告:点云数量过少,返回原始点云")
return pcd
# 获取自适应参数
local_eps, local_k = self._get_local_dbscan_params(points)
global_eps = np.median(local_eps)
global_k = int(np.median(local_k))
min_samples = max(global_k // 2, 3)
# DBSCAN聚类
print(f"DBSCAN参数:eps={global_eps:.6f},min_samples={min_samples}")
clustering = DBSCAN(eps=global_eps, min_samples=min_samples).fit(points)
# 异常处理:无有效簇时重试
if len(set(clustering.labels_)) == 1 and clustering.labels_[0] == -1:
print(f"警告:DBSCAN未识别有效簇,使用宽松参数重试(eps={global_eps*1.5:.6f})")
clustering = DBSCAN(eps=global_eps*1.5, min_samples=2).fit(points)
if len(set(clustering.labels_)) == 1 and clustering.labels_[0] == -1:
print("警告:重试失败,返回原始点云")
return pcd
# 保留最大簇
main_cluster_idx = np.argmax(np.bincount(clustering.labels_[clustering.labels_ != -1]))
main_cluster_points = points[clustering.labels_ == main_cluster_idx]
pcd_out = o3d.geometry.PointCloud()
pcd_out.points = o3d.utility.Vector3dVector(main_cluster_points)
return pcd_out
def extract_weld_seam(self, threshold: float = 0.02) -> None:
if self.filtered_pcd is None:
raise RuntimeError("请先执行点云预处理")
print("开始提取焊缝...")
# Alpha形状重建 mesh
mesh, _ = o3d.geometry.TriangleMesh.create_from_point_cloud_alpha_shape(self.filtered_pcd, alpha=0.03)
vertices = np.asarray(mesh.vertices)
# 计算曲率,筛选焊缝点(曲率高的点为焊缝边缘)
curvatures = self._calculate_curvature(mesh)
seam_candidates = vertices[curvatures > threshold]
# PCA+贪心最近邻排序(保证焊缝点顺序)
pca_mean = np.mean(seam_candidates, axis=0)
pca_cov = np.cov(seam_candidates - pca_mean, rowvar=False)
eigen_values, eigen_vectors = np.linalg.eig(pca_cov)
main_dir = eigen_vectors[:, np.argmax(eigen_values)]
seam_proj = seam_candidates @ main_dir
sorted_points = self._greedy_nearest_neighbor_sort(seam_candidates, seam_proj)
self.weld_seam_points = sorted_points
print(f"焊缝提取完成:{len(self.weld_seam_points)} 个路径点")
@staticmethod
def _calculate_curvature(mesh: o3d.geometry.TriangleMesh) -> np.ndarray:
"""计算顶点曲率"""
normals = np.asarray(mesh.vertex_normals)
vertices = np.asarray(mesh.vertices)
curvatures = np.zeros(len(vertices))
for i in range(len(vertices)):
# 找到相邻顶点
adj_vertices = []
for triangle in mesh.triangles:
if i in triangle:
adj_vertices.extend([v for v in triangle if v != i])
adj_vertices = list(set(adj_vertices))
if len(adj_vertices) < 3:
curvatures[i] = 0
continue
# 曲率=相邻法向量的方差
adj_normals = normals[adj_vertices]
curvatures[i] = np.var(adj_normals)
return curvatures
@staticmethod
def _greedy_nearest_neighbor_sort(points: np.ndarray, proj_values: np.ndarray) -> np.ndarray:
"""贪心最近邻排序,保证焊缝点顺序连续"""
init_order = np.argsort(proj_values)
sorted_points = points[init_order]
visited = [False] * len(sorted_points)
result = []
current_idx = 0
visited[current_idx] = True
result.append(sorted_points[current_idx])
for _ in tqdm(range(len(sorted_points)-1), desc="焊缝点排序"):
current_point = result[-1]
min_dist = float("inf")
next_idx = -1
# 局部搜索(减少计算量)
search_range = min(20, len(sorted_points) - len(result))
start_idx = max(0, current_idx - search_range)
end_idx = min(len(sorted_points), current_idx + search_range)
for i in range(start_idx, end_idx):
if not visited[i]:
dist = np.linalg.norm(sorted_points[i] - current_point)
if dist < min_dist:
min_dist = dist
next_idx = i
# 全局搜索(局部未找到时)
if next_idx == -1:
for i in range(len(sorted_points)):
if not visited[i]:
dist = np.linalg.norm(sorted_points[i] - current_point)
if dist < min_dist:
min_dist = dist
next_idx = i
visited[next_idx] = True
result.append(sorted_points[next_idx])
current_idx = next_idx
return np.array(result)
# -------------------------- 3. 路径规划(CSV均匀化+姿态导出) --------------------------
class WeldPathPlanner:
def __init__(self, seam_points: np.ndarray, obstacle_meshes: list = None, wire_diameter: float = 1.2):
self.seam_points = seam_points
self.obstacle_meshes = obstacle_meshes or []
self.wire_diameter = wire_diameter
self.tool_radius = 0.015 + wire_diameter / 1000
self.safety_margin = 0.02
self.planned_path = None
self.tool_orientation = None
self.local_coords = None
def _construct_local_coordinate_system(self) -> None:
"""构建局部坐标系(x:焊缝法向,y:焊缝横向,z:焊缝方向)"""
pca_mean = np.mean(self.seam_points, axis=0)
pca_cov = np.cov(self.seam_points - pca_mean, rowvar=False)
eigen_vectors = np.linalg.eig(pca_cov)[1]
forward_dir = eigen_vectors[:, np.argmax(np.linalg.eig(pca_cov)[0])] # z轴:焊缝方向
forward_dir = forward_dir / np.linalg.norm(forward_dir)
# 计算法向(x轴)
normals = []
for i in range(1, len(self.seam_points)-1):
vec1 = self.seam_points[i] - self.seam_points[i-1]
vec2 = self.seam_points[i+1] - self.seam_points[i]
normal = np.cross(vec1, vec2)
normal = normal / np.linalg.norm(normal)
normals.append(normal)
up_dir = np.mean(normals, axis=0) if normals else np.array([0, 1, 0]) # x轴:法向
up_dir = up_dir / np.linalg.norm(up_dir)
# 横向(y轴)
lateral_dir = np.cross(forward_dir, up_dir)
lateral_dir = lateral_dir / np.linalg.norm(lateral_dir)
self.local_coords = []
for point in self.seam_points:
self.local_coords.append((point, forward_dir, up_dir, lateral_dir))
def generate_smooth_path(
self,
offset_distance: float = 0.01,
use_ompl: bool = True,
max_turn_angle: float = 60.0,
smooth_degree: int = 3,
export_ompl_path: bool = True,
export_pose_type: str = "quaternion", # 姿态导出类型:quaternion/euler/none
path_step: float = 0.005 # 均匀化路径步长(米)
) -> None:
self._construct_local_coordinate_system()
# 生成初始偏移路径(焊接枪嘴位置)
offset_points = []
for (point, _, up_dir, _) in self.local_coords:
offset_point = point + up_dir * offset_distance # 沿法向偏移
offset_points.append(offset_point)
offset_points = np.array(offset_points)
# 路径优化
print("开始路径规划...")
if use_ompl and OMPL_AVAILABLE:
self.planned_path = self._ompl_rrt_star_optimize(offset_points)
if export_ompl_path:
self._export_path_to_csv(
self.planned_path,
self.tool_orientation,
filename="ompl_planned_path.csv",
pose_type=export_pose_type,
path_step=path_step
)
else:
self.planned_path = self._enhanced_rrt_star_optimize(offset_points)
# 工艺约束平滑
self.planned_path = self._process_smooth_path(
max_turn_angle=max_turn_angle,
smooth_degree=smooth_degree
)
# 路径均匀化(固定步长)
self.planned_path = self._uniformize_path(self.planned_path, step=path_step)
self._calculate_tool_orientation()
print(f"路径规划完成:均匀化后点数={len(self.planned_path)},步长={path_step}m")
def _ompl_rrt_star_optimize(self, start_goal_points: np.ndarray) -> np.ndarray:
"""OMPL RRT*:带收敛判断+碰撞检测"""
space = ob.SE3StateSpace()
bbox = self._get_path_bbox(start_goal_points)
bounds = ob.RealVectorBounds(3)
bounds.setLow(bbox[0])
bounds.setHigh(bbox[1])
space.setBounds(bounds)
si = ob.SpaceInformation(space)
si.setStateValidityChecker(ob.StateValidityCheckerFn(lambda s: self._ompl_is_state_valid(s, si)))
si.setup()
# 起点和目标
start = ob.State(space)
start().setX(start_goal_points[0][0])
start().setY(start_goal_points[0][1])
start().setZ(start_goal_points[0][2])
goal = ob.State(space)
goal().setX(start_goal_points[-1][0])
goal().setY(start_goal_points[-1][1])
goal().setZ(start_goal_points[-1][2])
# 问题定义与规划器
pdef = ob.ProblemDefinition(si)
pdef.setStartAndGoalStates(start, goal, 0.05)
planner = og.RRTstar(si)
planner.setProblemDefinition(pdef)
planner.setMaxIterations(2000)
planner.setMaxPlanningTime(10.0)
pdef.setOptimizationObjective(ob.PathLengthOptimizationObjective(si))
# 运行规划
solved = planner.solve(10.0)
if solved:
path = pdef.getSolutionPath()
path.interpolate()
path_np = []
for i in range(path.getStateCount()):
s = path.getState(i)
path_np.append([s()[0], s()[1], s()[2]])
return np.array(path_np)
else:
print("OMPL RRT*规划失败,切换为优化后的简化RRT*")
return self._enhanced_rrt_star_optimize(start_goal_points)
def _ompl_is_state_valid(self, state: ob.State, si: ob.SpaceInformation) -> bool:
"""OMPL状态有效性检查(碰撞检测)"""
x = state()[0]
y = state()[1]
z = state()[2]
tool_sphere = trimesh.creation.sphere(radius=self.tool_radius + self.safety_margin, center=[x, y, z])
for obs_mesh in self.obstacle_meshes:
if trimesh.collision.collision_pair(tool_sphere, obs_mesh)[0]:
return False
return si.satisfiesBounds(state)
def _enhanced_rrt_star_optimize(self, start_goal_points: np.ndarray) -> np.ndarray:
"""优化简化RRT*:收敛判断+路径重连"""
collision_meshes = [obs for obs in self.obstacle_meshes if isinstance(obs, trimesh.Trimesh)]
class Node:
def __init__(self, point):
self.point = point
self.parent = None
self.cost = 0.0
nodes = [Node(start_goal_points[0])]
space_idx = index.Index()
space_idx.insert(0, self._point_to_bbox(start_goal_points[0]))
bbox = self._get_path_bbox(start_goal_points)
max_iter = 2000
goal_point = start_goal_points[-1]
goal_radius = 0.05
convergence_iter = 0
best_cost = float("inf")
for _ in tqdm(range(max_iter), desc="简化RRT*规划"):
# 采样策略
sample_point = self._random_sample(bbox) if np.random.random() < 0.8 else goal_point
# 最近节点查找
nearest_idx = list(space_idx.nearest(self._point_to_bbox(sample_point), 1))[0]
nearest_node = nodes[nearest_idx]
# 新节点生成(自适应步长)
direction = sample_point - nearest_node.point
dir_norm = np.linalg.norm(direction)
if dir_norm > 0:
direction /= dir_norm
step_size = 0.01 if self._is_obstacle_dense(nearest_node.point, collision_meshes) else 0.02
new_point = nearest_node.point + direction * step_size
# 碰撞检测
if self._check_segment_collision(new_point, nearest_node.point, collision_meshes):
continue
# 路径重连
new_node = Node(new_point)
new_node.cost = nearest_node.cost + np.linalg.norm(new_point - nearest_node.point)
new_node.parent = nearest_node
near_indices = list(space_idx.intersection(self._point_to_bbox(new_point, radius=0.1)))
for near_idx in near_indices:
near_node = nodes[near_idx]
if not self._check_segment_collision(new_point, near_node.point, collision_meshes):
new_cost = near_node.cost + np.linalg.norm(new_point - near_node.point)
if new_cost < new_node.cost:
new_node.cost = new_cost
new_node.parent = near_node
# 插入新节点
space_idx.insert(len(nodes), self._point_to_bbox(new_point))
nodes.append(new_node)
# 收敛判断
if np.linalg.norm(new_point - goal_point) < goal_radius:
if not self._check_segment_collision(new_point, goal_point, collision_meshes):
goal_node = Node(goal_point)
goal_node.cost = new_node.cost + np.linalg.norm(goal_point - new_point)
goal_node.parent = new_node
nodes.append(goal_node)
space_idx.insert(len(nodes)-1, self._point_to_bbox(goal_point))
if goal_node.cost < best_cost * 1.05:
convergence_iter += 1
if convergence_iter >= 50:
break
best_cost = min(best_cost, goal_node.cost)
# 提取路径
if len(nodes) == 0 or not np.allclose(nodes[-1].point, goal_point, atol=0.1):
print("简化RRT*优化失败,使用原始偏移路径")
return start_goal_points
path = []
current_node = nodes[-1]
while current_node is not None:
path.append(current_node.point)
current_node = current_node.parent
return np.array(path[::-1])
def _process_smooth_path(self, max_turn_angle: float = 60.0, smooth_degree: int = 3) -> np.ndarray:
"""工艺平滑:自定义转角和平滑程度"""
if len(self.planned_path) < 3:
return self.planned_path
# 过滤急转角
max_angle_rad = np.radians(max_turn_angle)
smooth_path = [self.planned_path[0]]
for i in range(1, len(self.planned_path)-1):
vec1 = self.planned_path[i] - self.planned_path[i-1]
vec2 = self.planned_path[i+1] - self.planned_path[i]
vec1_norm = vec1 / np.linalg.norm(vec1)
vec2_norm = vec2 / np.linalg.norm(vec2)
angle = np.arccos(np.clip(np.dot(vec1_norm, vec2_norm), -1.0, 1.0))
if angle <= max_angle_rad:
smooth_path.append(self.planned_path[i])
smooth_path.append(self.planned_path[-1])
# B样条平滑
smooth_degree = np.clip(smooth_degree, 1, 5)
if len(smooth_path) <= smooth_degree:
smooth_degree = len(smooth_path) - 1
t = np.linspace(0, 1, len(smooth_path))
spline_x = make_interp_spline(t, [p[0] for p in smooth_path], k=smooth_degree)
spline_y = make_interp_spline(t, [p[1] for p in smooth_path], k=smooth_degree)
spline_z = make_interp_spline(t, [p[2] for p in smooth_path], k=smooth_degree)
t_smooth = np.linspace(0, 1, len(smooth_path) * (smooth_degree + 1))
return np.column_stack([spline_x(t_smooth), spline_y(t_smooth), spline_z(t_smooth)])
def _uniformize_path(self, path: np.ndarray, step: float = 0.005) -> np.ndarray:
"""路径均匀化:固定步长采样"""
if len(path) < 2:
return path
# 计算路径总长度和累计长度
dists = np.linalg.norm(np.diff(path, axis=0), axis=1)
total_length = np.sum(dists)
cum_dists = np.cumsum(dists)
cum_dists = np.insert(cum_dists, 0, 0)
# 生成均匀步长的采样点
uniform_dists = np.arange(0, total_length, step)
if uniform_dists[-1] < total_length - 1e-6:
uniform_dists = np.append(uniform_dists, total_length)
# 插值生成均匀路径
uniform_path = []
for d in uniform_dists:
# 找到d所在的线段
idx = np.searchsorted(cum_dists, d)
if idx == 0:
uniform_path.append(path[0])
elif idx >= len(cum_dists):
uniform_path.append(path[-1])
else:
# 线性插值
t = (d - cum_dists[idx-1]) / (cum_dists[idx] - cum_dists[idx-1])
point = (1 - t) * path[idx-1] + t * path[idx]
uniform_path.append(point)
return np.array(uniform_path)
def _export_path_to_csv(
self,
path: np.ndarray,
orientation: np.ndarray,
filename: str = "planned_path.csv",
pose_type: str = "quaternion",
path_step: float = 0.005
) -> None:
"""导出均匀化路径+姿态信息到CSV"""
# 确保路径和姿态点数一致
if len(orientation) != len(path):
# 姿态插值匹配路径点数
t_path = np.linspace(0, 1, len(path))
t_orient = np.linspace(0, 1, len(orientation))
orientation = np.array([
np.interp(t_path, t_orient, orientation[:, i]) for i in range(4)
]).T
# 路径均匀化
uniform_path = self._uniformize_path(path, step=path_step)
# 姿态均匀化(插值)
t_uniform = np.linspace(0, 1, len(uniform_path))
t_original = np.linspace(0, 1, len(orientation))
uniform_orient = np.array([
np.interp(t_uniform, t_original, orientation[:, i]) for i in range(4)
]).T
# 创建导出文件夹
if not os.path.exists("exported_paths"):
os.makedirs("exported_paths")
filepath = os.path.join("exported_paths", filename)
# 写入CSV
with open(filepath, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
# 表头
if pose_type == "quaternion":
writer.writerow(["X", "Y", "Z", "QX", "QY", "QZ", "QW"])
for p, o in zip(uniform_path, uniform_orient):
writer.writerow([p[0], p[1], p[2], o[0], o[1], o[2], o[3]])
elif pose_type == "euler":
writer.writerow(["X", "Y", "Z", "Roll", "Pitch", "Yaw"])
for p, o in zip(uniform_path, uniform_orient):
# 四元数转欧拉角(XYZ顺序,弧度)
euler = R.from_quat(o).as_euler('xyz')
writer.writerow([p[0], p[1], p[2], euler[0], euler[1], euler[2]])
else:
writer.writerow(["X", "Y", "Z"])
writer.writerows(uniform_path)
print(f"均匀化路径已导出至:{filepath}(步长={path_step}m)")
def _calculate_tool_orientation(self) -> None:
"""计算工具姿态(四元数)"""
self.tool_orientation = []
for (point, forward_dir, up_dir, lateral_dir) in self.local_coords:
# 构建旋转矩阵(x:法向,y:横向,z:焊缝方向)
rot_matrix = np.column_stack([up_dir, lateral_dir, forward_dir])
# 旋转矩阵转四元数(x,y,z,w)
quat = R.from_matrix(rot_matrix).as_quat()
self.tool_orientation.append(quat)
self.tool_orientation = np.array(self.tool_orientation)
# 辅助函数
def _get_path_bbox(self, points: np.ndarray) -> tuple:
min_bound = np.min(points, axis=0) - 0.1
max_bound = np.max(points, axis=0) + 0.1
return (min_bound, max_bound)
def _random_sample(self, bbox: tuple) -> np.ndarray:
return np.random.uniform(bbox[0], bbox[1])
def _point_to_bbox(self, point: np.ndarray, radius: float = 0.01) -> tuple:
return (point[0]-radius, point[1]-radius, point[2]-radius,
point[0]+radius, point[1]+radius, point[2]+radius)
def _check_segment_collision(self, p1: np.ndarray, p2: np.ndarray, collision_meshes: list) -> bool:
"""线段碰撞检测"""
num_samples = int(np.linalg.norm(p2-p1)/0.005) + 1 # 每0.5cm采样一次
samples = np.linspace(p1, p2, num_samples)
tool_radius = self.tool_radius + self.safety_margin
for sample in samples:
tool_sphere = trimesh.creation.sphere(radius=tool_radius, center=sample)
for obs_mesh in collision_meshes:
if trimesh.collision.collision_pair(tool_sphere, obs_mesh)[0]:
return True
return False
def _is_obstacle_dense(self, point: np.ndarray, collision_meshes: list) -> bool:
"""判断障碍物密度"""
tool_sphere = trimesh.creation.sphere(radius=self.tool_radius + self.safety_margin + 0.05, center=point)
for obs_mesh in collision_meshes:
if trimesh.collision.collision_pair(tool_sphere, obs_mesh)[0]:
return True
return False
# -------------------------- 4. 焊接程序生成(兼容均匀化路径) --------------------------
class WeldProgramGenerator:
def __init__(
self,
path: np.ndarray,
orientation: np.ndarray,
robot_brand: RobotBrand,
workpiece_material: WorkpieceMaterial,
plate_thickness: float = 5.0,
wire_diameter: float = 1.2
):
self.path = path
self.orientation = orientation
self.robot_brand = robot_brand
self.material = workpiece_material
self.plate_thickness = plate_thickness
self.wire_diameter = wire_diameter
self.weld_params = self._match_weld_params()
self.program = []
def _match_weld_params(self) -> dict:
"""匹配焊接工艺参数"""
key = (self.material, self.plate_thickness, self.wire_diameter)
if key not in WELD_PROCESS_DB:
similar_keys = [k for k in WELD_PROCESS_DB.keys() if k[0] == self.material and k[2] == self.wire_diameter]
if not similar_keys:
raise ValueError(f"无匹配工艺参数:{self.material.value}, {self.plate_thickness}mm, {self.wire_diameter}mm")
similar_keys.sort(key=lambda x: abs(x[1]-self.plate_thickness))
key = similar_keys[0]
print(f"警告:无完全匹配参数,使用相近参数:板厚{key[1]}mm")
current_range, voltage_range, speed_range = WELD_PROCESS_DB[key]
return {
"current": (current_range[0] + current_range[1]) / 2,
"voltage": (voltage_range[0] + voltage_range[1]) / 2,
"speed": (speed_range[0] + speed_range[1]) / 2
}
def generate_program(self, program_name: str = "WELD_PROG01") -> tuple:
"""生成机器人焊接程序"""
if self.robot_brand == RobotBrand.FANUC:
robot_program = self._generate_fanuc_program(program_name)
elif self.robot_brand == RobotBrand.ABB:
robot_program = self._generate_abb_program(program_name)
elif self.robot_brand == RobotBrand.KUKA:
robot_program = self._generate_kuka_program(program_name)
else:
raise ValueError(f"不支持的机器人品牌:{self.robot_brand.value}")
tat_file = self._generate_tat_file(program_name)
return robot_program, tat_file
def _generate_fanuc_program(self, program_name: str) -> str:
"""生成Fanuc TP程序"""
self.program.clear()
self.program.append(f"/PROG {program_name}")
self.program.append("/ATTR")
self.program.append("OWNER = MNEDITOR;")
self.program.append(f"COMMENT = \"{self.material.value} {self.plate_thickness}mm 自动焊接程序(焊丝{self.wire_diameter}mm)\";")
self.program.append("PROTECT = READ_WRITE;")
self.program.append("/MN")
self.program.append(" UNITS MM; ! 声明单位为毫米")
self.program.append(f" CALL WELD_INIT({self.weld_params['current']:.0f}, {self.weld_params['voltage']:.1f});")
self.program.append(f" SPEED {self.weld_params['speed']:.3f};")
self.program.append(" ACC 1.0;")
self.program.append(" FINE;")
point_idx = 1
# 确保姿态点数与路径点数一致
if len(self.orientation) != len(self.path):
t_path = np.linspace(0, 1, len(self.path))
t_orient = np.linspace(0, 1, len(self.orientation))
self.orientation = np.array([
np.interp(t_path, t_orient, self.orienta