feat: 算法升级、整车配准集成与性能分析文档
算法更新: delivery v1.2.1、平整度v1.5、测距v2.2、整车配准v1.3 适配器: vehicle-registration 新增、各算法 manifest 和 adapter 更新 后端: taskCadFiles/source 字段迁移、derivedPointClouds 注册、取消任务联动 前端: ElapsedTime 时长显示、两步算法流程、canRun 逻辑修复 文档: 新增5份算法性能分析报告
This commit is contained in:
362
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/3D Douglas-Peucker.py
Normal file
362
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/3D Douglas-Peucker.py
Normal file
@@ -0,0 +1,362 @@
|
||||
import numpy as np
|
||||
from scipy.spatial import KDTree
|
||||
import matplotlib.pyplot as plt
|
||||
from mpl_toolkits.mplot3d import Axes3D
|
||||
|
||||
|
||||
def point_to_line_distance(point, line_start, line_end):
|
||||
"""
|
||||
计算点到三维线段的最短距离
|
||||
|
||||
Args:
|
||||
point: 待计算点 (x, y, z)
|
||||
line_start: 线段起点 (x, y, z)
|
||||
line_end: 线段终点 (x, y, z)
|
||||
|
||||
Returns:
|
||||
点到线段的垂直距离
|
||||
"""
|
||||
p = np.array(point)
|
||||
a = np.array(line_start)
|
||||
b = np.array(line_end)
|
||||
|
||||
# 计算向量
|
||||
ab = b - a
|
||||
ap = p - a
|
||||
|
||||
# 投影参数 t
|
||||
t = np.dot(ap, ab) / np.dot(ab, ab)
|
||||
|
||||
# 找到线段上的最近点
|
||||
if t <= 0:
|
||||
closest = a
|
||||
elif t >= 1:
|
||||
closest = b
|
||||
else:
|
||||
closest = a + t * ab
|
||||
|
||||
# 计算距离
|
||||
distance = np.linalg.norm(p - closest)
|
||||
return distance
|
||||
|
||||
|
||||
def point_to_plane_distance(point, plane_point1, plane_point2, plane_point3):
|
||||
"""
|
||||
计算点到三维平面的距离(三点确定一个平面)
|
||||
|
||||
Args:
|
||||
point: 待计算点 (x, y, z)
|
||||
plane_point1, plane_point2, plane_point3: 平面上的三个点
|
||||
|
||||
Returns:
|
||||
点到平面的垂直距离
|
||||
"""
|
||||
p = np.array(point)
|
||||
p1 = np.array(plane_point1)
|
||||
p2 = np.array(plane_point2)
|
||||
p3 = np.array(plane_point3)
|
||||
|
||||
# 计算平面法向量
|
||||
v1 = p2 - p1
|
||||
v2 = p3 - p1
|
||||
normal = np.cross(v1, v2)
|
||||
normal = normal / np.linalg.norm(normal)
|
||||
|
||||
# 计算点到平面的距离
|
||||
distance = abs(np.dot(normal, p - p1))
|
||||
return distance
|
||||
|
||||
|
||||
def douglas_peucker_3d(points, epsilon, use_plane=False):
|
||||
"""
|
||||
3D Douglas-Peucker 算法
|
||||
|
||||
Args:
|
||||
points: 三维点列表或numpy数组,形状 (N, 3)
|
||||
epsilon: 容差阈值
|
||||
use_plane: 是否使用平面距离(True)还是线段距离(False)
|
||||
|
||||
Returns:
|
||||
简化后的点云列表
|
||||
"""
|
||||
if len(points) <= 2:
|
||||
return points.tolist() if isinstance(points, np.ndarray) else points
|
||||
|
||||
# 转换为列表格式
|
||||
if isinstance(points, np.ndarray):
|
||||
points = points.tolist()
|
||||
|
||||
# 首尾点
|
||||
start = points[0]
|
||||
end = points[-1]
|
||||
|
||||
# 找到距离最大的点
|
||||
max_dist = 0
|
||||
max_index = 0
|
||||
|
||||
for i in range(1, len(points) - 1):
|
||||
if use_plane and len(points) > 2:
|
||||
# 使用平面距离(需要至少3个点确定平面)
|
||||
if i > 0 and i < len(points) - 1:
|
||||
# 使用当前点、前一个点和后一个点确定平面
|
||||
plane_p1 = points[i - 1]
|
||||
plane_p2 = points[i]
|
||||
plane_p3 = points[i + 1]
|
||||
dist = point_to_plane_distance(points[i], plane_p1, plane_p2, plane_p3)
|
||||
else:
|
||||
dist = point_to_line_distance(points[i], start, end)
|
||||
else:
|
||||
# 使用线段距离
|
||||
dist = point_to_line_distance(points[i], start, end)
|
||||
|
||||
if dist > max_dist:
|
||||
max_dist = dist
|
||||
max_index = i
|
||||
|
||||
# 递归简化
|
||||
if max_dist > epsilon:
|
||||
# 保留该点,递归处理左右两部分
|
||||
left = douglas_peucker_3d(points[:max_index + 1], epsilon, use_plane)
|
||||
right = douglas_peucker_3d(points[max_index:], epsilon, use_plane)
|
||||
|
||||
# 合并结果(避免重复端点)
|
||||
return left[:-1] + right
|
||||
else:
|
||||
# 只保留首尾点
|
||||
return [start, end]
|
||||
|
||||
|
||||
class DouglasPeucker3D:
|
||||
"""
|
||||
3D Douglas-Peucker 算法的面向对象封装
|
||||
支持点云简化、轨迹压缩、轮廓提取等
|
||||
"""
|
||||
|
||||
def __init__(self, epsilon=0.01, use_plane=False):
|
||||
"""
|
||||
Args:
|
||||
epsilon: 容差阈值
|
||||
use_plane: 是否使用平面距离(更适合曲面点云)
|
||||
"""
|
||||
self.epsilon = epsilon
|
||||
self.use_plane = use_plane
|
||||
|
||||
def simplify(self, points):
|
||||
"""
|
||||
简化点云
|
||||
|
||||
Args:
|
||||
points: (N, 3) 的 numpy 数组或列表
|
||||
|
||||
Returns:
|
||||
简化后的点云 (N', 3) numpy 数组
|
||||
"""
|
||||
if isinstance(points, np.ndarray):
|
||||
points = points.tolist()
|
||||
|
||||
simplified = douglas_peucker_3d(points, self.epsilon, self.use_plane)
|
||||
return np.array(simplified)
|
||||
|
||||
def simplify_trajectory(self, trajectory, time_weight=False):
|
||||
"""
|
||||
专门用于轨迹简化(考虑时间维度)
|
||||
|
||||
Args:
|
||||
trajectory: (N, 4) 的数组 [x, y, z, t] 或 [x, y, z]
|
||||
time_weight: 是否加入时间权重
|
||||
|
||||
Returns:
|
||||
简化后的轨迹
|
||||
"""
|
||||
if time_weight and trajectory.shape[1] == 4:
|
||||
# 时间加权:将时间作为第四维参与距离计算
|
||||
# 需要自定义距离函数
|
||||
pass
|
||||
|
||||
return self.simplify(trajectory[:, :3])
|
||||
|
||||
def extract_contour(self, points, return_indices=False):
|
||||
"""
|
||||
提取点云轮廓(结合法线估计)
|
||||
|
||||
Args:
|
||||
points: (N, 3) 点云
|
||||
return_indices: 是否返回原始点索引
|
||||
|
||||
Returns:
|
||||
轮廓点集
|
||||
"""
|
||||
# 先进行粗略简化
|
||||
simplified = self.simplify(points)
|
||||
|
||||
# 这里可以结合法线估计进一步提取边界点
|
||||
# 简化实现:返回简化后的点作为轮廓候选
|
||||
|
||||
if return_indices:
|
||||
# 查找简化点在原始点云中的最近邻索引
|
||||
from scipy.spatial import KDTree
|
||||
tree = KDTree(points)
|
||||
indices = []
|
||||
for p in simplified:
|
||||
_, idx = tree.query(p)
|
||||
indices.append(idx)
|
||||
return simplified, np.array(indices)
|
||||
|
||||
return simplified
|
||||
|
||||
|
||||
# ========== 示例和使用 ==========
|
||||
|
||||
def generate_test_curve(n_points=100, noise=0.02):
|
||||
"""
|
||||
生成测试用的三维曲线(螺旋线 + 噪声)
|
||||
"""
|
||||
t = np.linspace(0, 4 * np.pi, n_points)
|
||||
x = np.sin(t)
|
||||
y = np.cos(t)
|
||||
z = t / (2 * np.pi)
|
||||
|
||||
points = np.column_stack([x, y, z])
|
||||
|
||||
# 添加噪声
|
||||
if noise > 0:
|
||||
points += np.random.randn(n_points, 3) * noise
|
||||
|
||||
return points
|
||||
|
||||
|
||||
def visualize_simplification(original, simplified, title="3D Douglas-Peucker Simplification"):
|
||||
"""
|
||||
可视化原始点云和简化后的点云
|
||||
"""
|
||||
fig = plt.figure(figsize=(12, 6))
|
||||
|
||||
# 原始点云
|
||||
ax1 = fig.add_subplot(121, projection='3d')
|
||||
ax1.scatter(original[:, 0], original[:, 1], original[:, 2],
|
||||
c='blue', s=10, alpha=0.6, label='Original')
|
||||
ax1.plot(original[:, 0], original[:, 1], original[:, 2],
|
||||
'b-', alpha=0.3)
|
||||
ax1.set_title(f'Original: {len(original)} points')
|
||||
ax1.legend()
|
||||
|
||||
# 简化后点云
|
||||
ax2 = fig.add_subplot(122, projection='3d')
|
||||
ax2.scatter(simplified[:, 0], simplified[:, 1], simplified[:, 2],
|
||||
c='red', s=30, alpha=0.8, label='Simplified')
|
||||
ax2.plot(simplified[:, 0], simplified[:, 1], simplified[:, 2],
|
||||
'r-', linewidth=2)
|
||||
ax2.set_title(f'Simplified: {len(simplified)} points (ε={epsilon})')
|
||||
ax2.legend()
|
||||
|
||||
plt.suptitle(title)
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
|
||||
|
||||
def compare_epsilon(points, epsilons):
|
||||
"""
|
||||
比较不同 epsilon 值的简化效果
|
||||
"""
|
||||
results = {}
|
||||
for eps in epsilons:
|
||||
dp = DouglasPeucker3D(epsilon=eps)
|
||||
simplified = dp.simplify(points)
|
||||
results[eps] = {
|
||||
'points': simplified,
|
||||
'count': len(simplified),
|
||||
'compression_ratio': len(simplified) / len(points)
|
||||
}
|
||||
print(f"ε={eps:.4f}: {len(simplified)} 点 (压缩率: {results[eps]['compression_ratio']:.2%})")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def demo_point_cloud():
|
||||
"""
|
||||
点云简化演示(使用螺旋线模拟)
|
||||
"""
|
||||
print("=" * 50)
|
||||
print("3D Douglas-Peucker 算法演示")
|
||||
print("=" * 50)
|
||||
|
||||
# 生成测试数据
|
||||
points = generate_test_curve(200, noise=0.01)
|
||||
print(f"\n原始点云: {len(points)} 个点")
|
||||
|
||||
# 测试不同 epsilon
|
||||
epsilons = [0.01, 0.03, 0.05, 0.1, 0.2]
|
||||
print("\n不同容差阈值的简化效果:")
|
||||
results = compare_epsilon(points, epsilons)
|
||||
|
||||
# 可视化
|
||||
dp = DouglasPeucker3D(epsilon=0.05)
|
||||
simplified = dp.simplify(points)
|
||||
visualize_simplification(points, simplified,
|
||||
f"3D DP Simplification (ε=0.05, {len(simplified)}/{len(points)} points)")
|
||||
|
||||
|
||||
def demo_trajectory():
|
||||
"""
|
||||
轨迹简化演示(模拟移动轨迹)
|
||||
"""
|
||||
print("\n" + "=" * 50)
|
||||
print("3D 轨迹简化演示")
|
||||
print("=" * 50)
|
||||
|
||||
# 生成测试轨迹(随机路径)
|
||||
np.random.seed(42)
|
||||
n_points = 100
|
||||
t = np.linspace(0, 10, n_points)
|
||||
x = np.sin(t) + np.random.randn(n_points) * 0.02
|
||||
y = np.cos(t) + np.random.randn(n_points) * 0.02
|
||||
z = t / 5 + np.random.randn(n_points) * 0.01
|
||||
|
||||
trajectory = np.column_stack([x, y, z])
|
||||
print(f"原始轨迹: {len(trajectory)} 个点")
|
||||
|
||||
# 简化轨迹
|
||||
dp = DouglasPeucker3D(epsilon=0.08)
|
||||
simplified = dp.simplify_trajectory(trajectory)
|
||||
print(f"简化后轨迹: {len(simplified)} 个点 (压缩率: {len(simplified) / len(trajectory):.2%})")
|
||||
|
||||
# 可视化
|
||||
visualize_simplification(trajectory, simplified, "3D Trajectory Simplification")
|
||||
|
||||
|
||||
def demo_point_cloud_from_file(pcd_file=None):
|
||||
"""
|
||||
从文件加载点云并简化
|
||||
"""
|
||||
try:
|
||||
import open3d as o3d
|
||||
|
||||
if pcd_file is None:
|
||||
# 使用示例:生成随机点云
|
||||
print("\n生成随机点云进行演示...")
|
||||
points = np.random.randn(500, 3) * 0.5
|
||||
else:
|
||||
pcd = o3d.io.read_point_cloud(pcd_file)
|
||||
points = np.asarray(pcd.points)
|
||||
|
||||
print(f"原始点云: {len(points)} 个点")
|
||||
|
||||
# 简化
|
||||
dp = DouglasPeucker3D(epsilon=0.05)
|
||||
|
||||
# 注意:传统 DP 算法需要有序点集,对于无序点云需要先排序或使用其他方法
|
||||
# 这里演示对有序轨迹/轮廓的应用
|
||||
print("\n注意: DP算法适用于有序点集(如轮廓线、轨迹),")
|
||||
print("对于无序点云,建议先进行排序或使用其他简化方法(如体素滤波)")
|
||||
|
||||
except ImportError:
|
||||
print("需要安装 open3d 来读取点云文件: pip install open3d")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 运行演示
|
||||
demo_point_cloud()
|
||||
demo_trajectory()
|
||||
|
||||
# 如果需要处理实际点云文件
|
||||
# demo_point_cloud_from_file("your_pointcloud.pcd")
|
||||
95
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/BF.py
Normal file
95
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/BF.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
from scipy.spatial import KDTree
|
||||
import numpy as np
|
||||
from scipy.spatial import KDTree
|
||||
def chamfer_distance(pcd1, pcd2):
|
||||
"""
|
||||
计算两个点云之间的 CD 距离(Chamfer Distance)
|
||||
|
||||
Args:
|
||||
pcd1: Open3D 点云对象1
|
||||
pcd2: Open3D 点云对象2
|
||||
|
||||
Returns:
|
||||
cd: Chamfer Distance 值
|
||||
cd1: 点云1 到 点云2 的平均距离
|
||||
cd2: 点云2 到 点云1 的平均距离
|
||||
"""
|
||||
# 获取点坐标
|
||||
points1 = np.asarray(pcd1.points)
|
||||
points2 = np.asarray(pcd2.points)
|
||||
|
||||
# 构建 KD-Tree 加速最近邻搜索
|
||||
tree1 = KDTree(points1)
|
||||
tree2 = KDTree(points2)
|
||||
|
||||
# 计算 points1 中每个点到 points2 的最近距离
|
||||
dist1, _ = tree2.query(points1, k=1)
|
||||
cd1 = np.mean(dist1 ** 2) # 平方距离的平均值
|
||||
|
||||
# 计算 points2 中每个点到 points1 的最近距离
|
||||
dist2, _ = tree1.query(points2, k=1)
|
||||
cd2 = np.mean(dist2 ** 2) # 平方距离的平均值
|
||||
|
||||
# 总 CD 距离(双向平均)
|
||||
cd = cd1 + cd2
|
||||
|
||||
return np.sqrt(cd), cd1, cd2
|
||||
|
||||
def bilateral_filter(pcd, sigma_s=0.01, sigma_r=0.1):
|
||||
"""
|
||||
对点云进行双边滤波
|
||||
Args:
|
||||
pcd: Open3D点云对象
|
||||
sigma_s: 空间标准差
|
||||
sigma_r: 特征(法线)标准差
|
||||
"""
|
||||
points = np.asarray(pcd.points)
|
||||
normals = np.asarray(pcd.normals)
|
||||
|
||||
# 构建KD树
|
||||
tree = KDTree(points)
|
||||
|
||||
filtered_points = np.zeros_like(points)
|
||||
|
||||
for i, p in enumerate(points):
|
||||
# 搜索邻域点
|
||||
indices = tree.query_ball_point(p, r=2 * sigma_s)
|
||||
if len(indices) <= 1:
|
||||
filtered_points[i] = p
|
||||
continue
|
||||
|
||||
neigh_points = points[indices]
|
||||
neigh_normals = normals[indices]
|
||||
|
||||
# 计算空间权重
|
||||
spatial_dist = np.linalg.norm(neigh_points - p, axis=1)
|
||||
w_s = np.exp(-spatial_dist ** 2 / (2 * sigma_s ** 2))
|
||||
|
||||
# 计算特征(法线)权重
|
||||
normal_diff = np.linalg.norm(neigh_normals - normals[i], axis=1)
|
||||
w_r = np.exp(-normal_diff ** 2 / (2 * sigma_r ** 2))
|
||||
|
||||
# 双边权重
|
||||
w = w_s * w_r
|
||||
w = w / w.sum() # 归一化
|
||||
|
||||
# 加权平均
|
||||
filtered_points[i] = np.sum(neigh_points.T * w, axis=1)
|
||||
|
||||
# 更新点云
|
||||
filtered_pcd = o3d.geometry.PointCloud()
|
||||
filtered_pcd.points = o3d.utility.Vector3dVector(filtered_points)
|
||||
filtered_pcd.normals = o3d.utility.Vector3dVector(normals)
|
||||
|
||||
return filtered_pcd
|
||||
|
||||
|
||||
# 使用示例
|
||||
pcd = o3d.io.read_point_cloud("E:\point_distance\data\part_car_head.ply")
|
||||
pcd.estimate_normals()
|
||||
pcd_filtered = bilateral_filter(pcd, sigma_s=10, sigma_r=10)
|
||||
|
||||
cd=chamfer_distance(pcd,pcd_filtered)
|
||||
print(cd)
|
||||
54
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/CE.py
Normal file
54
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/CE.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
|
||||
|
||||
def compute_curvature_open3d(pcd, radius=0.1, max_nn=30):
|
||||
"""
|
||||
使用 Open3D 计算曲率
|
||||
|
||||
Args:
|
||||
pcd: Open3D 点云对象
|
||||
radius: 搜索半径
|
||||
max_nn: 最大邻居数
|
||||
|
||||
Returns:
|
||||
curvature: 曲率数组
|
||||
"""
|
||||
# 估计法线
|
||||
pcd.estimate_normals(
|
||||
search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=radius, max_nn=max_nn)
|
||||
)
|
||||
|
||||
# 计算曲率(Open3D 在估计法线时已计算)
|
||||
# 曲率值存储在 pcd.point.normals 的第三个分量?
|
||||
# 实际上 Open3D 需要单独计算
|
||||
|
||||
# 方法:使用 PCA 重新计算
|
||||
points = np.asarray(pcd.points)
|
||||
n_points = len(points)
|
||||
curvature = np.zeros(n_points)
|
||||
|
||||
tree = o3d.geometry.KDTreeFlann(pcd)
|
||||
|
||||
for i in range(n_points):
|
||||
[k, idx, _] = tree.search_radius_vector_3d(pcd.points[i], radius)
|
||||
|
||||
if k < 3:
|
||||
curvature[i] = 1.0
|
||||
continue
|
||||
|
||||
neighbors = points[idx]
|
||||
centroid = np.mean(neighbors, axis=0)
|
||||
centered = neighbors - centroid
|
||||
cov = np.cov(centered.T)
|
||||
eigenvalues = np.linalg.eigvalsh(cov)
|
||||
eigenvalues = np.sort(eigenvalues)
|
||||
|
||||
curvature[i] = eigenvalues[0] / (eigenvalues.sum() + 1e-8)
|
||||
|
||||
return curvature
|
||||
|
||||
|
||||
# 使用示例
|
||||
pcd = o3d.io.read_point_cloud("pointcloud.pcd")
|
||||
curvature = compute_curvature_open3d(pcd, radius=0.05)
|
||||
50
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/DBSCAN.py
Normal file
50
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/DBSCAN.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
|
||||
|
||||
def dbscan_open3d(pcd, eps=0.1, min_points=10):
|
||||
"""
|
||||
使用 Open3D 进行 DBSCAN 聚类
|
||||
Args:
|
||||
pcd: Open3D 点云对象
|
||||
eps: 邻域半径
|
||||
min_points: 最小邻域点数
|
||||
Returns:
|
||||
labels: 每个点的簇标签(-1 表示噪声)
|
||||
clusters: 各簇的点索引列表
|
||||
"""
|
||||
# DBSCAN 聚类
|
||||
labels = np.array(pcd.cluster_dbscan(eps=eps, min_points=min_points))
|
||||
|
||||
# 提取各簇索引
|
||||
unique_labels = np.unique(labels)
|
||||
clusters = []
|
||||
for label in unique_labels:
|
||||
if label == -1:
|
||||
continue
|
||||
clusters.append(np.where(labels == label)[0])
|
||||
|
||||
return labels, clusters
|
||||
|
||||
|
||||
# 使用示例
|
||||
pcd = o3d.io.read_point_cloud("E:\point_distance\data\part_car_head.ply")
|
||||
|
||||
# 预处理
|
||||
pcd = pcd.voxel_down_sample(voxel_size=0.05)
|
||||
pcd, _ = pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0)
|
||||
|
||||
# DBSCAN 聚类
|
||||
labels, clusters = dbscan_open3d(pcd, eps=0.1, min_points=50)
|
||||
|
||||
print(f"找到 {len(clusters)} 个聚类")
|
||||
print(f"噪声点: {np.sum(labels == -1)}")
|
||||
|
||||
# 为每个聚类着色
|
||||
colors = np.random.rand(len(clusters), 3)
|
||||
colored_points = np.zeros((len(pcd.points), 3))
|
||||
for i, cluster in enumerate(clusters):
|
||||
colored_points[cluster] = colors[i]
|
||||
pcd.colors = o3d.utility.Vector3dVector(colored_points)
|
||||
|
||||
o3d.visualization.draw_geometries([pcd])
|
||||
65
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/EN.py
Normal file
65
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/EN.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
|
||||
|
||||
def compute_curvature_open3d(pcd, radius=0.05, max_nn=30):
|
||||
"""
|
||||
使用 Open3D 计算点云曲率
|
||||
|
||||
Args:
|
||||
pcd: Open3D 点云对象
|
||||
radius: 搜索半径
|
||||
max_nn: 最大邻居数
|
||||
|
||||
Returns:
|
||||
curvatures: (N,) 曲率数组
|
||||
"""
|
||||
# 估计法线(同时获取协方差矩阵信息)
|
||||
pcd.estimate_normals(
|
||||
search_param=o3d.geometry.KDTreeSearchParamHybrid(radius=radius, max_nn=max_nn)
|
||||
)
|
||||
|
||||
# 获取点云数据
|
||||
points = np.asarray(pcd.points)
|
||||
n_points = len(points)
|
||||
curvatures = np.zeros(n_points)
|
||||
|
||||
# 构建 KD-Tree
|
||||
tree = o3d.geometry.KDTreeFlann(pcd)
|
||||
|
||||
for i in range(n_points):
|
||||
# 搜索邻域点
|
||||
[k, idx, _] = tree.search_radius_vector_3d(pcd.points[i], radius)
|
||||
|
||||
if k < 3:
|
||||
curvatures[i] = 1.0
|
||||
continue
|
||||
|
||||
# 获取邻域点坐标
|
||||
neighbors = points[idx]
|
||||
|
||||
# 计算协方差矩阵
|
||||
centroid = np.mean(neighbors, axis=0)
|
||||
centered = neighbors - centroid
|
||||
cov = np.cov(centered.T)
|
||||
|
||||
# 特征值分解
|
||||
eigenvalues = np.linalg.eigvalsh(cov)
|
||||
eigenvalues = np.sort(eigenvalues)
|
||||
|
||||
# 计算曲率
|
||||
curvatures[i] = eigenvalues[0] / (eigenvalues.sum() + 1e-8)
|
||||
|
||||
return curvatures
|
||||
|
||||
|
||||
# 使用示例
|
||||
pcd = o3d.io.read_point_cloud("E:\point_distance\data\PointCloud\\38.ply")
|
||||
curvatures = compute_curvature_open3d(pcd, radius=50, max_nn=30)
|
||||
|
||||
# 将曲率作为颜色可视化
|
||||
colors = np.zeros((len(pcd.points), 3))
|
||||
colors[:, 0] = curvatures*3 # 红色通道表示曲率
|
||||
colors[:, 1] = 1 - curvatures*3 # 绿色通道为互补色
|
||||
pcd.colors = o3d.utility.Vector3dVector(colors)
|
||||
o3d.visualization.draw_geometries([pcd])
|
||||
47
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/Eucilid.py
Normal file
47
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/Eucilid.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
|
||||
|
||||
def euclidean_clustering_open3d(pcd, radius=0.1, min_cluster_size=50, max_cluster_size=10000):
|
||||
"""
|
||||
使用 Open3D 进行欧几里得聚类
|
||||
Args:
|
||||
pcd: Open3D 点云对象
|
||||
radius: 聚类半径
|
||||
min_cluster_size: 最小簇大小
|
||||
max_cluster_size: 最大簇大小
|
||||
Returns:
|
||||
labels: 每个点的簇标签(-1 表示噪声/无效簇)
|
||||
clusters: 各簇的点索引列表
|
||||
"""
|
||||
# 欧几里得聚类
|
||||
labels = np.array(pcd.cluster_dbscan(eps=radius, min_points=min_cluster_size))
|
||||
|
||||
# 统计簇信息
|
||||
unique_labels = np.unique(labels)
|
||||
clusters = []
|
||||
|
||||
for label in unique_labels:
|
||||
if label == -1: # -1 表示噪声点
|
||||
continue
|
||||
cluster_indices = np.where(labels == label)[0]
|
||||
if min_cluster_size <= len(cluster_indices) <= max_cluster_size:
|
||||
clusters.append(cluster_indices)
|
||||
|
||||
return labels, clusters
|
||||
|
||||
|
||||
# 使用示例
|
||||
pcd = o3d.io.read_point_cloud("E:\point_distance\data\part_car_left_side.ply")
|
||||
labels, clusters = euclidean_clustering_open3d(pcd, radius=4, min_cluster_size=100)
|
||||
|
||||
print(f"找到 {len(clusters)} 个聚类")
|
||||
|
||||
# 为每个聚类着色
|
||||
colors = np.random.rand(len(clusters), 3)
|
||||
colored_points = np.zeros((len(pcd.points), 3))
|
||||
for i, cluster in enumerate(clusters):
|
||||
colored_points[cluster] = colors[i]
|
||||
|
||||
pcd.colors = o3d.utility.Vector3dVector(colored_points)
|
||||
o3d.visualization.draw_geometries([pcd])
|
||||
16
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/FPFH.py
Normal file
16
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/FPFH.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
|
||||
pcd = o3d.io.read_point_cloud("pointcloud.pcd")
|
||||
|
||||
# 1. 计算法线
|
||||
pcd.estimate_normals(search_param=o3d.geometry.KDTreeSearchParamKNN(k=30))
|
||||
|
||||
# 2. 提取FPFH特征
|
||||
fpfh = o3d.pipelines.registration.compute_fpfh_feature(
|
||||
pcd,
|
||||
o3d.geometry.KDTreeSearchParamKNN(k=50)
|
||||
)
|
||||
|
||||
# fpfh.data 是 (33, N) 的矩阵,每列是一个点的33维FPFH描述子
|
||||
print(fpfh.data.shape) # (33, N)
|
||||
17
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/NE.py
Normal file
17
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/NE.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
pcd = o3d.io.read_point_cloud("pointcloud.pcd")
|
||||
|
||||
# 估计法线
|
||||
pcd.estimate_normals(
|
||||
search_param=o3d.geometry.KDTreeSearchParamHybrid(
|
||||
radius=0.1, # 搜索半径
|
||||
max_nn=30 # 最大邻居数
|
||||
)
|
||||
)
|
||||
|
||||
# 可视化法线
|
||||
o3d.visualization.draw_geometries([pcd], point_show_normal=True)
|
||||
|
||||
# 获取法线数据
|
||||
normals = np.asarray(pcd.normals)
|
||||
315
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/P2P.py
Normal file
315
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/P2P.py
Normal file
@@ -0,0 +1,315 @@
|
||||
|
||||
import copy
|
||||
from scipy.spatial.transform import Rotation as R
|
||||
import trimesh
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
import time
|
||||
def rotation_matrix_scipy(roll_deg=10, pitch_deg=10, yaw_deg=10):
|
||||
"""
|
||||
使用 scipy 生成绕 XYZ 轴旋转的矩阵
|
||||
"""
|
||||
r = R.from_euler('xyz', [roll_deg, pitch_deg, yaw_deg], degrees=True)
|
||||
return r.as_matrix()
|
||||
|
||||
|
||||
|
||||
|
||||
def random_rotation_matrix():
|
||||
"""生成随机旋转矩阵"""
|
||||
angle = np.random.uniform(0, 2 * np.pi)
|
||||
axis = np.random.randn(3)
|
||||
axis = axis / np.linalg.norm(axis)
|
||||
|
||||
K = np.array([
|
||||
[0, -axis[2], axis[1]],
|
||||
[axis[2], 0, -axis[0]],
|
||||
[-axis[1], axis[0], 0]
|
||||
])
|
||||
R = np.eye(3) + np.sin(angle) * K + (1 - np.cos(angle)) * K @ K
|
||||
return R
|
||||
|
||||
|
||||
def random_translation_vector(range_xyz=0.5):
|
||||
"""生成随机平移向量"""
|
||||
return np.random.uniform(-range_xyz, range_xyz, 3)
|
||||
|
||||
|
||||
def transform_point_cloud(pcd, R, t):
|
||||
"""对点云应用变换"""
|
||||
transformed_pcd = copy.deepcopy(pcd)
|
||||
points = np.asarray(transformed_pcd.points)
|
||||
points_transformed = (R @ points.T).T + t
|
||||
transformed_pcd.points = o3d.utility.Vector3dVector(points_transformed)
|
||||
return transformed_pcd
|
||||
|
||||
|
||||
def registration_open3d(source_pcd, target_pcd, voxel_size=0.005):
|
||||
"""
|
||||
点云配准(RANSAC + ICP)
|
||||
|
||||
Returns:
|
||||
transformation: 4x4 变换矩阵
|
||||
fitness: 配准 fitness 分数
|
||||
rmse: 配准 RMSE
|
||||
"""
|
||||
# 1. 下采样
|
||||
source_down = source_pcd.voxel_down_sample(voxel_size)
|
||||
target_down = target_pcd.voxel_down_sample(voxel_size)
|
||||
|
||||
# 2. 估计法线
|
||||
radius = voxel_size * 2
|
||||
source_down.estimate_normals(o3d.geometry.KDTreeSearchParamHybrid(radius=radius, max_nn=30))
|
||||
target_down.estimate_normals(o3d.geometry.KDTreeSearchParamHybrid(radius=radius, max_nn=30))
|
||||
|
||||
# 3. 计算 FPFH 特征
|
||||
radius_feature = voxel_size * 5
|
||||
source_fpfh = o3d.pipelines.registration.compute_fpfh_feature(
|
||||
source_down, o3d.geometry.KDTreeSearchParamHybrid(radius=radius_feature, max_nn=100))
|
||||
target_fpfh = o3d.pipelines.registration.compute_fpfh_feature(
|
||||
target_down, o3d.geometry.KDTreeSearchParamHybrid(radius=radius_feature, max_nn=100))
|
||||
|
||||
# 4. RANSAC 粗配准
|
||||
distance_threshold = voxel_size * 1.5
|
||||
result_ransac = o3d.pipelines.registration.registration_ransac_based_on_feature_matching(
|
||||
source_down, target_down, source_fpfh, target_fpfh,
|
||||
mutual_filter=True,
|
||||
max_correspondence_distance=distance_threshold,
|
||||
estimation_method=o3d.pipelines.registration.TransformationEstimationPointToPoint(),
|
||||
ransac_n=3,
|
||||
checkers=[
|
||||
o3d.pipelines.registration.CorrespondenceCheckerBasedOnEdgeLength(0.9),
|
||||
o3d.pipelines.registration.CorrespondenceCheckerBasedOnDistance(distance_threshold)
|
||||
],
|
||||
criteria=o3d.pipelines.registration.RANSACConvergenceCriteria(100000, 0.999))
|
||||
|
||||
print(f"RANSAC 配准完成")
|
||||
source_pcd.estimate_normals(o3d.geometry.KDTreeSearchParamHybrid(radius=0.1, max_nn=30))
|
||||
target_pcd.estimate_normals(o3d.geometry.KDTreeSearchParamHybrid(radius=0.1, max_nn=30))
|
||||
# 5. ICP 精配准
|
||||
result_icp = o3d.pipelines.registration.registration_icp(
|
||||
source_pcd, target_pcd, distance_threshold / 2, result_ransac.transformation,
|
||||
o3d.pipelines.registration.TransformationEstimationPointToPlane(),
|
||||
o3d.pipelines.registration.ICPConvergenceCriteria(max_iteration=200))
|
||||
|
||||
print(f"ICP 精配准完成")
|
||||
print(f" Fitness: {result_icp.fitness:.4f}")
|
||||
print(f" RMSE: {result_icp.inlier_rmse:.6f} m")
|
||||
|
||||
return result_icp.transformation, result_icp.fitness, result_icp.inlier_rmse
|
||||
|
||||
|
||||
def visualize_registration_result(original_pcd, transformed_pcd, registered_pcd, title="Registration Result"):
|
||||
"""
|
||||
可视化原始点云、变换后点云和配准后点云
|
||||
|
||||
Args:
|
||||
original_pcd: 原始点云(源)
|
||||
transformed_pcd: 应用随机变换后的点云(目标)
|
||||
registered_pcd: 配准后的点云
|
||||
title: 窗口标题
|
||||
"""
|
||||
# 复制并着色
|
||||
source_colored = copy.deepcopy(original_pcd)
|
||||
target_colored = copy.deepcopy(transformed_pcd)
|
||||
registered_colored = copy.deepcopy(registered_pcd)
|
||||
|
||||
# 设置颜色
|
||||
source_colored.paint_uniform_color([0, 0, 1]) # 蓝色 - 原始点云
|
||||
target_colored.paint_uniform_color([0, 1, 0]) # 绿色 - 目标点云
|
||||
registered_colored.paint_uniform_color([1, 0, 0]) # 红色 - 配准后点云
|
||||
|
||||
# 创建坐标系(可选)
|
||||
# coordinate_frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=0.5)
|
||||
|
||||
# 可视化
|
||||
o3d.visualization.draw_geometries(
|
||||
[source_colored, target_colored],
|
||||
window_name="Before Registration (Blue: Source, Green: Target)",
|
||||
width=1024,
|
||||
height=768
|
||||
)
|
||||
|
||||
o3d.visualization.draw_geometries(
|
||||
[registered_colored, target_colored],
|
||||
window_name="After Registration (Red: Registered, Green: Target)",
|
||||
width=1024,
|
||||
height=768
|
||||
)
|
||||
|
||||
|
||||
def visualize_registration_comparison(original_pcd, transformed_pcd, registered_pcd):
|
||||
"""
|
||||
并排可视化配准前后的对比
|
||||
|
||||
Args:
|
||||
original_pcd: 原始点云
|
||||
transformed_pcd: 变换后的点云(目标)
|
||||
registered_pcd: 配准后的点云
|
||||
"""
|
||||
# 复制并着色
|
||||
source_before = copy.deepcopy(original_pcd)
|
||||
source_before.paint_uniform_color([0, 0, 1]) # 蓝色
|
||||
|
||||
target_before = copy.deepcopy(transformed_pcd)
|
||||
target_before.paint_uniform_color([0, 1, 0]) # 绿色
|
||||
|
||||
registered_after = copy.deepcopy(registered_pcd)
|
||||
registered_after.paint_uniform_color([1, 0, 0]) # 红色
|
||||
|
||||
target_after = copy.deepcopy(transformed_pcd)
|
||||
target_after.paint_uniform_color([0, 1, 0]) # 绿色
|
||||
|
||||
# 创建坐标系
|
||||
coordinate_frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=0.5)
|
||||
|
||||
# 方法1:分别显示两个窗口
|
||||
o3d.visualization.draw_geometries(
|
||||
[source_before],
|
||||
window_name="Before Registration (Blue: Source, Green: Target)",
|
||||
width=1024,
|
||||
height=768
|
||||
)
|
||||
o3d.visualization.draw_geometries(
|
||||
[target_before],
|
||||
window_name="Before Registration (Blue: Source, Green: Target)",
|
||||
width=1024,
|
||||
height=768
|
||||
)
|
||||
o3d.visualization.draw_geometries(
|
||||
[registered_after, source_before],
|
||||
window_name="After Registration (Red: Registered, Green: Target)",
|
||||
width=1024,
|
||||
height=768
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
def compute_point_to_mesh_distance(point_cloud, mesh):
|
||||
"""
|
||||
计算点云到 Mesh 的距离
|
||||
|
||||
Args:
|
||||
point_cloud: Open3D 点云对象
|
||||
mesh: Open3D 网格对象
|
||||
|
||||
Returns:
|
||||
distances: 每个点到最近三角形面的距离
|
||||
mean_distance: 平均距离
|
||||
rmse: 均方根误差
|
||||
max_distance: 最大距离
|
||||
"""
|
||||
# 计算点到 mesh 的距离
|
||||
distances = mesh.compute_point_cloud_distance(point_cloud)
|
||||
distances = np.asarray(distances)
|
||||
|
||||
mean_dist = np.mean(distances)
|
||||
rmse = np.sqrt(np.mean(distances ** 2))
|
||||
max_dist = np.max(distances)
|
||||
std_dist = np.std(distances)
|
||||
|
||||
return distances, mean_dist, rmse, max_dist, std_dist
|
||||
|
||||
|
||||
# 使用示例
|
||||
def demo(pcd,mesh):
|
||||
# 加载点云和 mesh
|
||||
# 计算距离
|
||||
distances, mean_dist, rmse, max_dist, std_dist = compute_point_to_mesh_distance(pcd, mesh)
|
||||
|
||||
print(f"平均距离: {mean_dist:.6f} m ({mean_dist * 1000:.3f} mm)")
|
||||
print(f"RMSE: {rmse:.6f} m ({rmse * 1000:.3f} mm)")
|
||||
print(f"最大距离: {max_dist:.6f} m ({max_dist * 1000:.3f} mm)")
|
||||
print(f"标准差: {std_dist:.6f} m ({std_dist * 1000:.3f} mm)")
|
||||
|
||||
# 可视化(按距离着色)
|
||||
pcd.colors = o3d.utility.Vector3dVector(get_color_by_distance(distances))
|
||||
o3d.visualization.draw_geometries([pcd, mesh])
|
||||
|
||||
|
||||
def get_color_by_distance(distances, max_dist=None):
|
||||
"""
|
||||
根据距离生成颜色(红:远,蓝:近)
|
||||
"""
|
||||
if max_dist is None:
|
||||
max_dist = np.percentile(distances, 95) # 使用95分位数
|
||||
|
||||
colors = np.zeros((len(distances), 3))
|
||||
norm_dist = np.clip(distances / (max_dist + 1e-8), 0, 1)
|
||||
|
||||
# 红色通道(距离越远越红)
|
||||
colors[:, 0] = norm_dist
|
||||
# 蓝色通道(距离越近越蓝)
|
||||
colors[:, 2] = 1 - norm_dist
|
||||
|
||||
return colors
|
||||
# ========== 主程序 ==========
|
||||
if __name__ == "__main__":
|
||||
# 1. 读取原始点云
|
||||
file_path = "E:\point_distance\data\\40_noisy.ply"
|
||||
|
||||
source_pcd = o3d.io.read_point_cloud(file_path)
|
||||
|
||||
# points=np.asarray(source_pcd.points)
|
||||
|
||||
t0=time.time()
|
||||
if len(source_pcd.points) == 0:
|
||||
print("读取失败,请检查文件路径")
|
||||
exit()
|
||||
|
||||
print(f"原始点云点数: {len(source_pcd.points)}")
|
||||
|
||||
# 2. 生成随机变换
|
||||
# R = random_rotation_matrix()
|
||||
R = rotation_matrix_scipy(10, 10, 10)
|
||||
t = random_translation_vector(range_xyz=0)
|
||||
|
||||
print(f"随机旋转矩阵:\n{R}")
|
||||
print(f"随机平移向量: {t}")
|
||||
|
||||
# 3. 创建目标点云(变换后的源点云)
|
||||
target_pcd = transform_point_cloud(source_pcd, R, t)
|
||||
|
||||
# 4. 点云配准
|
||||
print("\n开始点云配准...")
|
||||
transformation, fitness, rmse = registration_open3d(source_pcd, target_pcd, voxel_size=10)
|
||||
|
||||
# 5. 应用配准变换
|
||||
# registered_pcd = copy.deepcopy(source_pcd)
|
||||
# registered_pcd.transform(transformation)
|
||||
inv_transformation = np.linalg.inv(transformation)
|
||||
# 将目标点云变换回源点云坐标系
|
||||
target_recovered = copy.deepcopy(target_pcd)
|
||||
target_recovered.transform(inv_transformation)
|
||||
|
||||
|
||||
t1 = time.time()
|
||||
print(f"\n配准结果:")
|
||||
print(f" Fitness: {fitness:.4f}")
|
||||
print(f" RMSE: {rmse:.6f} m")
|
||||
print(f" 估计的变换矩阵:\n{transformation}")
|
||||
print(f"配准time:\n{t1-t0}")
|
||||
|
||||
|
||||
t2=time.time()
|
||||
# 1. 读取数据
|
||||
|
||||
points = np.asarray(target_recovered.points)
|
||||
mesh_trimesh = trimesh.load("E:\point_distance\data\\40.stl")
|
||||
# ========== 1. 读取数据 ==========
|
||||
# 2. 使用 trimesh 计算每个点到 Mesh 的最近距离
|
||||
# trimesh 的 nearest 方法非常高效且精确
|
||||
closest_points, distances, triangle_ids = mesh_trimesh.nearest.on_surface(points)
|
||||
t3 = time.time()
|
||||
|
||||
# distances 即为所求的距离数组
|
||||
print(f"平均距离: {np.mean(distances)}")
|
||||
print(f"最大距离: {np.max(distances)}")
|
||||
print(f" 距离计算: {t3 - t2:.4f} 秒")
|
||||
# 如果需要将结果存回 Open3D 点云进行可视化
|
||||
|
||||
|
||||
# 6. 可视化对比
|
||||
print("\n开始可视化...")
|
||||
visualize_registration_comparison(source_pcd, target_pcd,target_recovered)
|
||||
94
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/PF.py
Normal file
94
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/PF.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
from scipy.spatial import KDTree
|
||||
|
||||
|
||||
def compute_keep_ratio_and_density(pcd, filtered_pcd, sample_size=1000):
|
||||
"""
|
||||
计算直通滤波的保留率和密度变化
|
||||
|
||||
Args:
|
||||
pcd: 原始点云
|
||||
filtered_pcd: 滤波后点云
|
||||
sample_size: 密度计算采样点数(加速计算)
|
||||
|
||||
Returns:
|
||||
keep_ratio: 保留率 (%)
|
||||
density_change: 密度变化 (%)
|
||||
"""
|
||||
n_orig = len(pcd.points)
|
||||
n_filt = len(filtered_pcd.points)
|
||||
|
||||
# 1. 保留率
|
||||
keep_ratio = n_filt / n_orig * 100
|
||||
|
||||
# 2. 密度变化(基于平均最近邻距离)
|
||||
points_orig = np.asarray(pcd.points)
|
||||
points_filt = np.asarray(filtered_pcd.points)
|
||||
|
||||
# 采样(点数太多时加速)
|
||||
if len(points_orig) > sample_size:
|
||||
idx_orig = np.random.choice(len(points_orig), sample_size, replace=False)
|
||||
points_orig_sample = points_orig[idx_orig]
|
||||
else:
|
||||
points_orig_sample = points_orig
|
||||
|
||||
if len(points_filt) > sample_size:
|
||||
idx_filt = np.random.choice(len(points_filt), sample_size, replace=False)
|
||||
points_filt_sample = points_filt[idx_filt]
|
||||
else:
|
||||
points_filt_sample = points_filt
|
||||
|
||||
# 计算平均最近邻距离(密度指标)
|
||||
if len(points_orig_sample) > 5:
|
||||
tree_orig = KDTree(points_orig_sample)
|
||||
dist_orig, _ = tree_orig.query(points_orig_sample, k=2) # k=2 排除自身
|
||||
avg_dist_orig = np.mean(dist_orig[:, 1])
|
||||
else:
|
||||
avg_dist_orig = 0
|
||||
|
||||
if len(points_filt_sample) > 5:
|
||||
tree_filt = KDTree(points_filt_sample)
|
||||
dist_filt, _ = tree_filt.query(points_filt_sample, k=2)
|
||||
avg_dist_filt = np.mean(dist_filt[:, 1])
|
||||
else:
|
||||
avg_dist_filt = 0
|
||||
|
||||
# 密度变化(距离增大 = 密度减小)
|
||||
if avg_dist_orig > 0:
|
||||
density_change = (avg_dist_filt - avg_dist_orig) / avg_dist_orig * 100
|
||||
else:
|
||||
density_change = 0
|
||||
|
||||
return keep_ratio, density_change
|
||||
|
||||
|
||||
# ========== 使用示例 ==========
|
||||
if __name__ == "__main__":
|
||||
# 读取点云
|
||||
pcd = o3d.io.read_point_cloud(r"E:\point_distance\data\part_car_head.ply")
|
||||
|
||||
# 直通滤波(Z轴)
|
||||
points = np.asarray(pcd.points)
|
||||
mask = (points[:, 2] >= 0.0) & (points[:, 2] <= 2.0)
|
||||
filtered_points = points[mask]
|
||||
|
||||
filtered_pcd = o3d.geometry.PointCloud()
|
||||
filtered_pcd.points = o3d.utility.Vector3dVector(filtered_points)
|
||||
|
||||
# 计算指标
|
||||
keep_ratio, density_change = compute_keep_ratio_and_density(pcd, filtered_pcd)
|
||||
|
||||
print("=" * 40)
|
||||
print("直通滤波指标")
|
||||
print("=" * 40)
|
||||
print(f"保留率: {keep_ratio:.2f}%")
|
||||
print(f"密度变化: {density_change:+.2f}%")
|
||||
|
||||
# 解释
|
||||
if density_change > 0:
|
||||
print(f" → 点云变稀疏了 {density_change:.2f}%")
|
||||
elif density_change < 0:
|
||||
print(f" → 点云变密集了 {abs(density_change):.2f}%")
|
||||
else:
|
||||
print(f" → 密度基本不变")
|
||||
380
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/RANSC.py
Normal file
380
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/RANSC.py
Normal file
@@ -0,0 +1,380 @@
|
||||
# import numpy as np
|
||||
# import open3d as o3d
|
||||
# import random
|
||||
#
|
||||
#
|
||||
# def ransac_plane(points, threshold=0.01, max_iterations=1000, min_inliers=100):
|
||||
# """
|
||||
# RANSAC 平面拟合(单个)
|
||||
# """
|
||||
# best_inliers = []
|
||||
# best_plane = None
|
||||
# n_points = len(points)
|
||||
#
|
||||
# if n_points < 3:
|
||||
# return None, []
|
||||
#
|
||||
# for _ in range(max_iterations):
|
||||
# sample_idx = random.sample(range(n_points), 3)
|
||||
# p1, p2, p3 = points[sample_idx]
|
||||
#
|
||||
# v1 = p2 - p1
|
||||
# v2 = p3 - p1
|
||||
# normal = np.cross(v1, v2)
|
||||
# norm = np.linalg.norm(normal)
|
||||
# if norm < 1e-8:
|
||||
# continue
|
||||
# normal = normal / norm
|
||||
# a, b, c = normal
|
||||
# d = -np.dot(normal, p1)
|
||||
#
|
||||
# distances = np.abs(np.dot(points, normal) + d)
|
||||
# inliers = np.where(distances < threshold)[0]
|
||||
#
|
||||
# if len(inliers) > len(best_inliers) and len(inliers) >= min_inliers:
|
||||
# best_inliers = inliers
|
||||
# best_plane = (a, b, c, d)
|
||||
#
|
||||
# if len(best_inliers) > 0.9 * n_points:
|
||||
# break
|
||||
#
|
||||
# # 用内点重新拟合
|
||||
# if best_inliers is not None and len(best_inliers) > 3:
|
||||
# inlier_points = points[best_inliers]
|
||||
# centroid = np.mean(inlier_points, axis=0)
|
||||
# _, _, vh = np.linalg.svd(inlier_points - centroid)
|
||||
# normal = vh[2]
|
||||
# normal = normal / np.linalg.norm(normal)
|
||||
# a, b, c = normal
|
||||
# d = -np.dot(normal, centroid)
|
||||
# best_plane = (a, b, c, d)
|
||||
#
|
||||
# return best_plane, best_inliers
|
||||
#
|
||||
#
|
||||
# def sequential_ransac_planes(points, threshold=0.01, max_iterations=1000, min_inliers=100, max_planes=5):
|
||||
# """
|
||||
# 迭代 RANSAC 拟合多个平面
|
||||
# """
|
||||
# remaining_points = points.copy()
|
||||
# remaining_indices = np.arange(len(points))
|
||||
#
|
||||
# planes = []
|
||||
# inliers_list = []
|
||||
#
|
||||
# for i in range(max_planes):
|
||||
# if len(remaining_points) < min_inliers:
|
||||
# break
|
||||
#
|
||||
# plane, inliers = ransac_plane(remaining_points, threshold, max_iterations, min_inliers)
|
||||
#
|
||||
# if plane is None or len(inliers) < min_inliers:
|
||||
# break
|
||||
#
|
||||
# planes.append(plane)
|
||||
# original_indices = remaining_indices[inliers]
|
||||
# inliers_list.append(original_indices)
|
||||
#
|
||||
# # 移除内点
|
||||
# mask = np.ones(len(remaining_points), dtype=bool)
|
||||
# mask[inliers] = False
|
||||
# remaining_points = remaining_points[mask]
|
||||
# remaining_indices = remaining_indices[mask]
|
||||
#
|
||||
# print(f"平面 {len(planes)}: {len(inliers)} 个内点, 剩余 {len(remaining_points)} 个点")
|
||||
#
|
||||
# return planes, inliers_list
|
||||
#
|
||||
#
|
||||
# def create_plane_mesh(plane, bounds):
|
||||
# """
|
||||
# 创建平面网格用于可视化
|
||||
# """
|
||||
# a, b, c, d = plane
|
||||
# x_min, x_max = bounds[0]
|
||||
# y_min, y_max = bounds[1]
|
||||
#
|
||||
# x = np.linspace(x_min, x_max, 50)
|
||||
# y = np.linspace(y_min, y_max, 50)
|
||||
# X, Y = np.meshgrid(x, y)
|
||||
#
|
||||
# if abs(c) > 1e-8:
|
||||
# Z = -(a * X + b * Y + d) / c
|
||||
# else:
|
||||
# Z = np.zeros_like(X)
|
||||
#
|
||||
# vertices = []
|
||||
# triangles = []
|
||||
# rows, cols = X.shape
|
||||
#
|
||||
# for i in range(rows):
|
||||
# for j in range(cols):
|
||||
# vertices.append([X[i, j], Y[i, j], Z[i, j]])
|
||||
#
|
||||
# for i in range(rows - 1):
|
||||
# for j in range(cols - 1):
|
||||
# idx = i * cols + j
|
||||
# triangles.append([idx, idx + 1, idx + cols])
|
||||
# triangles.append([idx + 1, idx + cols + 1, idx + cols])
|
||||
#
|
||||
# mesh = o3d.geometry.TriangleMesh()
|
||||
# mesh.vertices = o3d.utility.Vector3dVector(np.array(vertices))
|
||||
# mesh.triangles = o3d.utility.Vector3iVector(np.array(triangles))
|
||||
# mesh.compute_vertex_normals()
|
||||
#
|
||||
# return mesh
|
||||
#
|
||||
#
|
||||
# def visualize_multiple_planes(points, planes, inliers_list):
|
||||
# """
|
||||
# 可视化多个平面(点云 + 平面网格)
|
||||
#
|
||||
# Args:
|
||||
# points: (N, 3) 点云坐标
|
||||
# planes: 平面方程列表 [(a,b,c,d), ...]
|
||||
# inliers_list: 每个平面对应的内点索引列表
|
||||
# """
|
||||
# # 颜色列表(每个平面不同颜色)
|
||||
# colors = [
|
||||
# [1, 0, 0], # 红色
|
||||
# [0, 1, 0], # 绿色
|
||||
# [0, 0, 1], # 蓝色
|
||||
# [1, 1, 0], # 黄色
|
||||
# [1, 0, 1], # 品红
|
||||
# [0, 1, 1], # 青色
|
||||
# [1, 0.5, 0], # 橙色
|
||||
# [0.5, 0, 1], # 紫色
|
||||
# ]
|
||||
#
|
||||
# geometries = []
|
||||
#
|
||||
# # 1. 为每个平面的内点着色
|
||||
# point_colors = np.zeros((len(points), 3))
|
||||
# for i, inliers in enumerate(inliers_list):
|
||||
# color = colors[i % len(colors)]
|
||||
# point_colors[inliers] = color
|
||||
#
|
||||
# # 未分配的点(剩余点)设为灰色
|
||||
# unassigned = np.where(np.sum(point_colors, axis=1) == 0)[0]
|
||||
# point_colors[unassigned] = [0.5, 0.5, 0.5]
|
||||
#
|
||||
# colored_pcd = o3d.geometry.PointCloud()
|
||||
# colored_pcd.points = o3d.utility.Vector3dVector(points)
|
||||
# colored_pcd.colors = o3d.utility.Vector3dVector(point_colors)
|
||||
# geometries.append(colored_pcd)
|
||||
#
|
||||
# # 2. 为每个平面创建网格
|
||||
# for i, (plane, inliers) in enumerate(zip(planes, inliers_list)):
|
||||
# # 计算该平面对应点的范围
|
||||
# inlier_points = points[inliers]
|
||||
# if len(inlier_points) > 0:
|
||||
# x_min, x_max = inlier_points[:, 0].min(), inlier_points[:, 0].max()
|
||||
# y_min, y_max = inlier_points[:, 1].min(), inlier_points[:, 1].max()
|
||||
#
|
||||
# # 稍微扩展边界
|
||||
# x_range = x_max - x_min
|
||||
# y_range = y_max - y_min
|
||||
# x_min -= x_range * 0.1
|
||||
# x_max += x_range * 0.1
|
||||
# y_min -= y_range * 0.1
|
||||
# y_max += y_range * 0.1
|
||||
#
|
||||
# mesh = create_plane_mesh(plane, [[x_min, x_max], [y_min, y_max]])
|
||||
# mesh.paint_uniform_color(colors[i % len(colors)])
|
||||
# # 设置半透明
|
||||
# mesh.paint_uniform_color([colors[i % len(colors)][0],
|
||||
# colors[i % len(colors)][1],
|
||||
# colors[i % len(colors)][2]])
|
||||
# geometries.append(mesh)
|
||||
#
|
||||
# # 可视化
|
||||
# o3d.visualization.draw_geometries(
|
||||
# geometries,
|
||||
# window_name="Multiple Planes - RANSAC",
|
||||
# width=1024,
|
||||
# height=768
|
||||
# )
|
||||
#
|
||||
#
|
||||
# def visualize_single_plane(points, plane, inliers):
|
||||
# """
|
||||
# 可视化单个平面(兼容单平面情况)
|
||||
# """
|
||||
# visualize_multiple_planes(points, [plane], [inliers])
|
||||
#
|
||||
#
|
||||
# # ========== 主程序 ==========
|
||||
# if __name__ == "__main__":
|
||||
# # 读取点云
|
||||
# file_path = r"E:\point_distance\data\part_car_head.ply"
|
||||
# pcd = o3d.io.read_point_cloud(file_path)
|
||||
#
|
||||
# if len(pcd.points) == 0:
|
||||
# print("读取失败,请检查文件路径")
|
||||
# exit()
|
||||
#
|
||||
# points = np.asarray(pcd.points)
|
||||
# print(f"原始点云点数: {len(points)}")
|
||||
#
|
||||
# # 计算点云范围
|
||||
# points_range = points.max(axis=0) - points.min(axis=0)
|
||||
# print(f"点云范围: X[{points_range[0]:.3f}], Y[{points_range[1]:.3f}], Z[{points_range[2]:.3f}]")
|
||||
#
|
||||
# # 设置阈值
|
||||
# threshold = points_range[2] * 0.02
|
||||
# print(f"使用阈值: {threshold:.4f}")
|
||||
#
|
||||
# # ========== 单平面拟合 ==========
|
||||
# # print("\n" + "=" * 50)
|
||||
# # print("单平面拟合")
|
||||
# # print("=" * 50)
|
||||
# #
|
||||
# # plane, inliers = ransac_plane(points, threshold=threshold, max_iterations=1, min_inliers=100)
|
||||
# #
|
||||
# # if plane is not None:
|
||||
# # a, b, c, d = plane
|
||||
# # print(f"拟合平面: {a:.4f}x + {b:.4f}y + {c:.4f}z + {d:.4f} = 0")
|
||||
# # print(f"内点数量: {len(inliers)} / {len(points)} ({len(inliers) / len(points) * 100:.1f}%)")
|
||||
# # visualize_single_plane(points, plane, inliers)
|
||||
#
|
||||
# # ========== 多平面拟合 ==========
|
||||
# print("\n" + "=" * 50)
|
||||
# print("多平面拟合")
|
||||
# print("=" * 50)
|
||||
#
|
||||
# planes, inliers_list = sequential_ransac_planes(
|
||||
# points,
|
||||
# threshold=threshold,
|
||||
# max_iterations=1,
|
||||
# min_inliers=100,
|
||||
# max_planes=1
|
||||
# )
|
||||
#
|
||||
# print(f"共提取 {len(planes)} 个平面")
|
||||
# for i, (plane, inliers) in enumerate(zip(planes, inliers_list)):
|
||||
# a, b, c, d = plane
|
||||
# print(f"平面 {i + 1}: {a:.4f}x + {b:.4f}y + {c:.4f}z + {d:.4f} = 0, 内点数: {len(inliers)}")
|
||||
#
|
||||
# if len(planes) > 0:
|
||||
# visualize_multiple_planes(points, planes, inliers_list)
|
||||
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
import random
|
||||
|
||||
|
||||
def create_square_plane_with_noise(size_mm=1000, noise_std_mm=0.5, num_points=10000):
|
||||
"""创建正方形平面点云并添加高斯噪声"""
|
||||
size_m = size_mm / 1000.0
|
||||
noise_std_m = noise_std_mm / 1000.0
|
||||
|
||||
half_size = size_m / 2
|
||||
x = np.random.uniform(-half_size, half_size, num_points)
|
||||
y = np.random.uniform(-half_size, half_size, num_points)
|
||||
z = np.zeros(num_points)
|
||||
|
||||
points = np.column_stack([x, y, z])
|
||||
noise_z = np.random.normal(0, noise_std_m, num_points)
|
||||
points[:, 2] += noise_z
|
||||
|
||||
return points
|
||||
|
||||
|
||||
def estimate_plane_ransac_open3d(points, threshold_mm=0.5, max_iterations=2000):
|
||||
"""使用 Open3D 内置 RANSAC 平面估计"""
|
||||
pcd = o3d.geometry.PointCloud()
|
||||
pcd.points = o3d.utility.Vector3dVector(points)
|
||||
|
||||
threshold_m = threshold_mm / 1000.0
|
||||
|
||||
plane_model, inliers = pcd.segment_plane(
|
||||
distance_threshold=threshold_m,
|
||||
ransac_n=3,
|
||||
num_iterations=max_iterations
|
||||
)
|
||||
|
||||
a, b, c, d = plane_model
|
||||
|
||||
# 计算误差
|
||||
ideal_normal = np.array([0, 0, 1])
|
||||
estimated_normal = np.array([a, b, c])
|
||||
cos_angle = np.dot(ideal_normal, estimated_normal)
|
||||
cos_angle = np.clip(cos_angle, -1, 1)
|
||||
angle_error = np.degrees(np.arccos(cos_angle))
|
||||
offset_error = abs(d)
|
||||
|
||||
inlier_points = points[inliers]
|
||||
distances = np.abs(np.dot(inlier_points, estimated_normal) + d)
|
||||
rmse = np.sqrt(np.mean(distances ** 2))
|
||||
|
||||
print(f"\nRANSAC 平面拟合结果 (Open3D):")
|
||||
print(f" 估计平面: {a:.6f}x + {b:.6f}y + {c:.6f}z + {d:.6f} = 0")
|
||||
print(f" 内点数量: {len(inliers)} / {len(points)} ({len(inliers) / len(points) * 100:.1f}%)")
|
||||
print(f" 法线角度误差: {angle_error:.4f} 度")
|
||||
print(f" 偏移误差: {offset_error:.6f} m ({offset_error * 1000:.3f} mm)")
|
||||
print(f" 平面拟合 RMSE: {rmse:.6f} m ({rmse * 1000:.3f} mm)")
|
||||
|
||||
return plane_model, inliers
|
||||
|
||||
|
||||
def visualize_plane_fitting(points, plane):
|
||||
"""可视化原始点云和拟合平面"""
|
||||
# 原始点云
|
||||
pcd = o3d.geometry.PointCloud()
|
||||
pcd.points = o3d.utility.Vector3dVector(points)
|
||||
pcd.paint_uniform_color([0.6, 0.8, 1.0])
|
||||
|
||||
# 创建平面点云
|
||||
a, b, c, d = plane
|
||||
x_min, x_max = points[:, 0].min(), points[:, 0].max()
|
||||
y_min, y_max = points[:, 1].min(), points[:, 1].max()
|
||||
|
||||
x = np.linspace(x_min, x_max, 50)
|
||||
y = np.linspace(y_min, y_max, 50)
|
||||
X, Y = np.meshgrid(x, y)
|
||||
|
||||
if abs(c) > 1e-8:
|
||||
Z = -(a * X + b * Y + d) / c
|
||||
else:
|
||||
Z = np.zeros_like(X)
|
||||
|
||||
plane_points = np.column_stack([X.ravel(), Y.ravel(), Z.ravel()])
|
||||
|
||||
pcd_plane = o3d.geometry.PointCloud()
|
||||
pcd_plane.points = o3d.utility.Vector3dVector(plane_points)
|
||||
pcd_plane.paint_uniform_color([1, 0, 0]) # 红色
|
||||
|
||||
o3d.visualization.draw_geometries(
|
||||
[pcd, pcd_plane],
|
||||
window_name="Plane Fitting",
|
||||
width=1024,
|
||||
height=768
|
||||
)
|
||||
|
||||
|
||||
# ========== 主程序 ==========
|
||||
if __name__ == "__main__":
|
||||
# 参数设置
|
||||
size_mm = 1000
|
||||
noise_std_mm = 0.5
|
||||
num_points = 100000
|
||||
|
||||
print(f"参数:")
|
||||
print(f" 正方形边长: {size_mm} mm")
|
||||
print(f" 噪声标准差: {noise_std_mm} mm")
|
||||
print(f" 点数: {num_points}")
|
||||
|
||||
# 生成点云
|
||||
points = create_square_plane_with_noise(size_mm, noise_std_mm, num_points)
|
||||
|
||||
# 检查数据范围
|
||||
print(f"\n数据范围:")
|
||||
print(f" X: [{points[:, 0].min():.3f}, {points[:, 0].max():.3f}]")
|
||||
print(f" Y: [{points[:, 1].min():.3f}, {points[:, 1].max():.3f}]")
|
||||
print(f" Z: [{points[:, 2].min():.6f}, {points[:, 2].max():.6f}]")
|
||||
|
||||
# 使用 Open3D RANSAC
|
||||
plane, inliers = estimate_plane_ransac_open3d(points, threshold_mm=0.5, max_iterations=2000)
|
||||
|
||||
# 可视化
|
||||
visualize_plane_fitting(points, plane)
|
||||
147
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/RG.py
Normal file
147
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/RG.py
Normal file
@@ -0,0 +1,147 @@
|
||||
import numpy as np
|
||||
from scipy.spatial import KDTree
|
||||
from sklearn.neighbors import NearestNeighbors
|
||||
|
||||
|
||||
def estimate_normals(points, k=20):
|
||||
"""用 PCA 估计法线"""
|
||||
n_points = len(points)
|
||||
normals = np.zeros_like(points)
|
||||
|
||||
nbrs = NearestNeighbors(n_neighbors=k).fit(points)
|
||||
distances, indices = nbrs.kneighbors(points)
|
||||
|
||||
for i in range(n_points):
|
||||
neighbors = points[indices[i]]
|
||||
centroid = np.mean(neighbors, axis=0)
|
||||
cov = np.cov((neighbors - centroid).T)
|
||||
eig_vals, eig_vecs = np.linalg.eig(cov)
|
||||
normal = eig_vecs[:, np.argmin(eig_vals)]
|
||||
# 统一法线方向
|
||||
if normal[2] < 0:
|
||||
normal = -normal
|
||||
normals[i] = normal
|
||||
|
||||
return normals
|
||||
|
||||
|
||||
def compute_curvature(points, normals, k=20):
|
||||
"""计算曲率(特征值比值)"""
|
||||
curvature = np.zeros(len(points))
|
||||
nbrs = NearestNeighbors(n_neighbors=k).fit(points)
|
||||
distances, indices = nbrs.kneighbors(points)
|
||||
|
||||
for i in range(len(points)):
|
||||
neighbors = points[indices[i]]
|
||||
centroid = np.mean(neighbors, axis=0)
|
||||
cov = np.cov((neighbors - centroid).T)
|
||||
eig_vals, _ = np.linalg.eig(cov)
|
||||
curvature[i] = np.min(eig_vals) / (np.sum(eig_vals) + 1e-8)
|
||||
|
||||
return curvature
|
||||
|
||||
|
||||
def region_growing(points, normals, curvature,
|
||||
angle_threshold=30.0, # 法线夹角阈值(度)
|
||||
curvature_threshold=0.05, # 曲率阈值
|
||||
min_cluster_size=100):
|
||||
"""
|
||||
区域生长算法
|
||||
Args:
|
||||
points: (N, 3) 点云坐标
|
||||
normals: (N, 3) 法线
|
||||
curvature: (N,) 曲率
|
||||
angle_threshold: 法线夹角阈值(度)
|
||||
curvature_threshold: 曲率阈值
|
||||
min_cluster_size: 最小簇大小
|
||||
Returns:
|
||||
labels: (N,) 每个点的簇标签(-1 表示未分类/噪声)
|
||||
"""
|
||||
n_points = len(points)
|
||||
labels = np.full(n_points, -1, dtype=int)
|
||||
|
||||
# 按曲率升序排序(曲率小的作为种子)
|
||||
sorted_indices = np.argsort(curvature)
|
||||
|
||||
# 构建 KD-Tree 用于快速邻域搜索
|
||||
tree = KDTree(points)
|
||||
|
||||
label_id = 0
|
||||
angle_threshold_rad = np.radians(angle_threshold)
|
||||
cos_threshold = np.cos(angle_threshold_rad)
|
||||
|
||||
for seed_idx in sorted_indices:
|
||||
if labels[seed_idx] != -1:
|
||||
continue
|
||||
|
||||
# 开始一个新区域
|
||||
current_region = []
|
||||
seeds = [seed_idx]
|
||||
labels[seed_idx] = label_id
|
||||
|
||||
while seeds:
|
||||
current_seed = seeds.pop(0)
|
||||
current_region.append(current_seed)
|
||||
|
||||
# 搜索半径内的邻居(动态半径或固定半径)
|
||||
neighbors = tree.query_ball_point(points[current_seed], r=0.2)
|
||||
|
||||
for neighbor in neighbors:
|
||||
if labels[neighbor] != -1:
|
||||
continue
|
||||
|
||||
# 检查法线夹角
|
||||
dot_product = np.abs(np.dot(normals[current_seed], normals[neighbor]))
|
||||
if dot_product > cos_threshold:
|
||||
labels[neighbor] = label_id
|
||||
|
||||
# 如果曲率足够小,作为新种子
|
||||
if curvature[neighbor] < curvature_threshold:
|
||||
seeds.append(neighbor)
|
||||
|
||||
# 检查区域大小
|
||||
region_size = np.sum(labels == label_id)
|
||||
if region_size < min_cluster_size:
|
||||
# 区域太小,标记为噪声
|
||||
labels[labels == label_id] = -1
|
||||
else:
|
||||
label_id += 1
|
||||
|
||||
return labels
|
||||
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
import open3d as o3d
|
||||
|
||||
# 读取点云
|
||||
pcd = o3d.io.read_point_cloud("E:\point_distance\data\\bell_pepper_6.pcd")
|
||||
points = np.asarray(pcd.points)
|
||||
|
||||
# 预处理:体素下采样 + 去噪
|
||||
pcd = pcd.voxel_down_sample(0.0002)
|
||||
pcd, _ = pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0)
|
||||
points = np.asarray(pcd.points)
|
||||
|
||||
# 计算法线和曲率
|
||||
print("计算法线...")
|
||||
normals = estimate_normals(points, k=30)
|
||||
|
||||
print("计算曲率...")
|
||||
curvature = compute_curvature(points, normals, k=30)
|
||||
|
||||
# 区域生长分割
|
||||
print("区域生长...")
|
||||
labels = region_growing(points, normals, curvature,
|
||||
angle_threshold=25.0,
|
||||
curvature_threshold=0.05,
|
||||
min_cluster_size=100)
|
||||
|
||||
print(f"分割完成,共 {len(np.unique(labels)) - (1 if -1 in labels else 0)} 个区域")
|
||||
|
||||
# 可视化
|
||||
colors = np.random.rand(len(np.unique(labels)), 3)
|
||||
point_colors = np.array([colors[label] if label != -1 else [0.5, 0.5, 0.5]
|
||||
for label in labels])
|
||||
pcd.colors = o3d.utility.Vector3dVector(point_colors)
|
||||
o3d.visualization.draw_geometries([pcd])
|
||||
52
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/ROR.py
Normal file
52
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/ROR.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
from scipy.spatial import KDTree
|
||||
def chamfer_distance(pcd1, pcd2):
|
||||
"""
|
||||
计算两个点云之间的 CD 距离(Chamfer Distance)
|
||||
|
||||
Args:
|
||||
pcd1: Open3D 点云对象1
|
||||
pcd2: Open3D 点云对象2
|
||||
|
||||
Returns:
|
||||
cd: Chamfer Distance 值
|
||||
cd1: 点云1 到 点云2 的平均距离
|
||||
cd2: 点云2 到 点云1 的平均距离
|
||||
"""
|
||||
# 获取点坐标
|
||||
points1 = np.asarray(pcd1.points)
|
||||
points2 = np.asarray(pcd2.points)
|
||||
|
||||
# 构建 KD-Tree 加速最近邻搜索
|
||||
tree1 = KDTree(points1)
|
||||
tree2 = KDTree(points2)
|
||||
|
||||
# 计算 points1 中每个点到 points2 的最近距离
|
||||
dist1, _ = tree2.query(points1, k=1)
|
||||
cd1 = np.mean(dist1 ** 2) # 平方距离的平均值
|
||||
|
||||
# 计算 points2 中每个点到 points1 的最近距离
|
||||
dist2, _ = tree1.query(points2, k=1)
|
||||
cd2 = np.mean(dist2 ** 2) # 平方距离的平均值
|
||||
|
||||
# 总 CD 距离(双向平均)
|
||||
cd = cd1 + cd2
|
||||
|
||||
return np.sqrt(cd), cd1, cd2
|
||||
pcd = o3d.io.read_point_cloud("E:\point_distance\data\part_car_left_side.ply")
|
||||
points = np.asarray(pcd.points)
|
||||
colors = np.tile([0.8, 0.7, 1.0], (len(pcd.points), 1))
|
||||
pcd.colors = o3d.utility.Vector3dVector(colors)
|
||||
o3d.visualization.draw_geometries([pcd],window_name="1")
|
||||
# 半径离群点移除
|
||||
cl, ind = pcd.remove_radius_outlier(nb_points=5, radius=20)
|
||||
|
||||
# 提取内点
|
||||
inlier_cloud = pcd.select_by_index(ind)
|
||||
outlier_cloud = pcd.select_by_index(ind, invert=True)
|
||||
|
||||
print(f"原始点云: {len(pcd.points)} 点")
|
||||
print(f"滤波后: {len(inlier_cloud.points)} 点")
|
||||
cd,cd1,cd2=chamfer_distance(pcd1=pcd,pcd2=inlier_cloud)
|
||||
print(cd)
|
||||
60
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/SOR.py
Normal file
60
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/SOR.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
from scipy.spatial import KDTree
|
||||
from plyfile import PlyData
|
||||
def chamfer_distance(pcd1, pcd2):
|
||||
"""
|
||||
计算两个点云之间的 CD 距离(Chamfer Distance)
|
||||
|
||||
Args:
|
||||
pcd1: Open3D 点云对象1
|
||||
pcd2: Open3D 点云对象2
|
||||
|
||||
Returns:
|
||||
cd: Chamfer Distance 值
|
||||
cd1: 点云1 到 点云2 的平均距离
|
||||
cd2: 点云2 到 点云1 的平均距离
|
||||
"""
|
||||
# 获取点坐标
|
||||
points1 = np.asarray(pcd1.points)
|
||||
points2 = np.asarray(pcd2.points)
|
||||
|
||||
# 构建 KD-Tree 加速最近邻搜索
|
||||
tree1 = KDTree(points1)
|
||||
tree2 = KDTree(points2)
|
||||
|
||||
# 计算 points1 中每个点到 points2 的最近距离
|
||||
dist1, _ = tree2.query(points1, k=1)
|
||||
cd1 = np.mean(dist1 ** 2) # 平方距离的平均值
|
||||
|
||||
# 计算 points2 中每个点到 points1 的最近距离
|
||||
dist2, _ = tree1.query(points2, k=1)
|
||||
cd2 = np.mean(dist2 ** 2) # 平方距离的平均值
|
||||
|
||||
# 总 CD 距离(双向平均)
|
||||
cd = cd1 + cd2
|
||||
|
||||
return cd, cd1, cd2
|
||||
|
||||
pcd = o3d.io.read_point_cloud("E:\point_distance\data\part_car_head.ply")
|
||||
colors = np.tile([1, 0, 0], (len(pcd.points), 1))
|
||||
pcd.colors = o3d.utility.Vector3dVector(colors)
|
||||
# ply_data = PlyData.read(r"E:\point_distance\data\part_car_head.ply")
|
||||
# colors=np.asarray(ply_data.colors)
|
||||
# with open("E:\point_distance\data\part_car_head.ply", 'r',encoding="utf-8") as f:
|
||||
# lines = f.readlines()
|
||||
# for line in lines[:30]: # 打印头部
|
||||
# print(line.strip())
|
||||
# o3d.visualization.draw_geometries([pcd])
|
||||
# points = np.asarray(pcd.points)
|
||||
# colors = np.asarray(pcd.colors)
|
||||
# normals = np.asarray(pcd.normals)
|
||||
cl, ind = pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=50.0)
|
||||
inlier_cloud = pcd.select_by_index(ind)
|
||||
colors=np.tile([0,1,0],(len(inlier_cloud.points),1))
|
||||
inlier_cloud.colors=o3d.utility.Vector3dVector(colors)
|
||||
o3d.visualization.draw_geometries([inlier_cloud])
|
||||
o3d.visualization.draw_geometries([pcd])
|
||||
o3d.visualization.draw_geometries()
|
||||
cd,cd1,cd2=chamfer_distance(pcd1=pcd,pcd2=inlier_cloud)
|
||||
print(cd)
|
||||
96
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/VGF.py
Normal file
96
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/VGF.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
from scipy.spatial import KDTree
|
||||
|
||||
|
||||
def evaluate_voxel_filter(pcd, leaf_size=0.05):
|
||||
"""
|
||||
评估体素滤波效果,计算各项指标
|
||||
|
||||
Args:
|
||||
pcd: 原始点云
|
||||
leaf_size: 体素边长
|
||||
|
||||
Returns:
|
||||
metrics: 指标字典
|
||||
filtered_pcd: 滤波后点云
|
||||
"""
|
||||
# 执行体素滤波
|
||||
filtered_pcd = pcd.voxel_down_sample(leaf_size)
|
||||
|
||||
n_orig = len(pcd.points)
|
||||
n_filt = len(filtered_pcd.points)
|
||||
|
||||
# 1. 基础统计指标
|
||||
keep_ratio = n_filt / n_orig * 100
|
||||
downsample_rate = (n_orig - n_filt) / n_orig * 100
|
||||
compression_ratio = n_orig / n_filt if n_filt > 0 else 0
|
||||
|
||||
# 2. 密度变化指标
|
||||
points_orig = np.asarray(pcd.points)
|
||||
points_filt = np.asarray(filtered_pcd.points)
|
||||
|
||||
# 计算平均最近邻距离(采样加速)
|
||||
sample_size = min(1000, n_filt)
|
||||
if n_filt > sample_size:
|
||||
idx = np.random.choice(n_filt, sample_size, replace=False)
|
||||
points_sample = points_filt[idx]
|
||||
else:
|
||||
points_sample = points_filt
|
||||
|
||||
if len(points_sample) > 5:
|
||||
tree = KDTree(points_sample)
|
||||
dist, _ = tree.query(points_sample, k=2)
|
||||
avg_distance = np.mean(dist[:, 1])
|
||||
else:
|
||||
avg_distance = 0
|
||||
|
||||
# 3. 输出指标
|
||||
metrics = {
|
||||
'leaf_size': leaf_size,
|
||||
'n_orig': n_orig,
|
||||
'n_filt': n_filt,
|
||||
'keep_ratio': keep_ratio,
|
||||
'downsample_rate': downsample_rate,
|
||||
'compression_ratio': compression_ratio,
|
||||
'avg_point_distance': avg_distance,
|
||||
'estimated_density': 1 / (avg_distance ** 3) if avg_distance > 0 else 0
|
||||
}
|
||||
|
||||
return metrics, filtered_pcd
|
||||
|
||||
|
||||
# ========== 使用示例 ==========
|
||||
if __name__ == "__main__":
|
||||
# 读取点云
|
||||
pcd = o3d.io.read_point_cloud(r"E:\point_distance\data\part_car_head.ply")
|
||||
|
||||
# 测试不同体素大小
|
||||
leaf_sizes = [2, 15, 20, 2]
|
||||
|
||||
print("=" * 60)
|
||||
print("体素滤波指标评估")
|
||||
print("=" * 60)
|
||||
print(f"{'体素边长':<10} {'保留率':<12} {'下采样率':<12} {'压缩比':<10} {'平均间距':<12}")
|
||||
print("-" * 60)
|
||||
|
||||
for leaf_size in leaf_sizes:
|
||||
metrics, filtered = evaluate_voxel_filter(pcd, leaf_size)
|
||||
print(f"{leaf_size:<10} {metrics['keep_ratio']:>8.2f}% "
|
||||
f"{metrics['downsample_rate']:>8.2f}% "
|
||||
f"{metrics['compression_ratio']:>8.1f}x "
|
||||
f"{metrics['avg_point_distance']:>8.4f}m")
|
||||
|
||||
# 可视化对比
|
||||
print("\n可视化对比...")
|
||||
orig_colored = o3d.geometry.PointCloud()
|
||||
orig_colored.points = pcd.points
|
||||
orig_colored.paint_uniform_color([0, 0, 1]) # 蓝色
|
||||
# o3d.visualization.draw_geometries([orig_colored],window_name=f"orignal (blue,{len(pcd.points)}")
|
||||
metrics, filtered = evaluate_voxel_filter(pcd, leaf_size=20)
|
||||
filtered.paint_uniform_color([1, 0, 0]) # 红色
|
||||
# o3d.visualization.draw_geometries([filtered], window_name=f"Voxel (red,{len(filtered.points)}")
|
||||
o3d.visualization.draw_geometries(
|
||||
[orig_colored, filtered],
|
||||
window_name=f"Original (Blue, {len(pcd.points)} pts) vs Voxel (Red, {len(filtered.points)} pts)"
|
||||
)
|
||||
141
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/distance_mesh.py
Normal file
141
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/distance_mesh.py
Normal file
@@ -0,0 +1,141 @@
|
||||
import open3d as o3d
|
||||
import numpy as np
|
||||
from scipy.spatial import KDTree
|
||||
import copy
|
||||
|
||||
|
||||
def compute_point_to_mesh_distance(point_cloud, mesh):
|
||||
"""
|
||||
计算点云到 Mesh 的距离
|
||||
|
||||
Args:
|
||||
point_cloud: Open3D 点云对象
|
||||
mesh: Open3D 网格对象
|
||||
|
||||
Returns:
|
||||
distances: 每个点到最近三角形面的距离
|
||||
"""
|
||||
# 方法1:使用 Open3D 内置函数(新版本)
|
||||
try:
|
||||
distances = mesh.compute_point_cloud_distance(point_cloud)
|
||||
return np.asarray(distances)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# 方法2:使用 mesh 顶点最近邻(近似)
|
||||
mesh_points = np.asarray(mesh.vertices)
|
||||
points = np.asarray(point_cloud.points)
|
||||
|
||||
tree = KDTree(mesh_points)
|
||||
distances, indices = tree.query(points)
|
||||
|
||||
return distances
|
||||
|
||||
|
||||
def compute_statistics(distances):
|
||||
"""计算距离统计信息"""
|
||||
stats = {
|
||||
'mean': np.mean(distances),
|
||||
'rmse': np.sqrt(np.mean(distances ** 2)),
|
||||
'max': np.max(distances),
|
||||
'min': np.min(distances),
|
||||
'std': np.std(distances),
|
||||
'median': np.median(distances),
|
||||
}
|
||||
|
||||
# 分位数
|
||||
percentiles = [50, 75, 90, 95, 99]
|
||||
for p in percentiles:
|
||||
stats[f'percentile_{p}'] = np.percentile(distances, p)
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def visualize_distance(point_cloud, mesh, distances, max_dist=None):
|
||||
"""可视化距离(按距离着色)"""
|
||||
pcd_colored = copy.deepcopy(point_cloud)
|
||||
|
||||
if max_dist is None:
|
||||
max_dist = np.percentile(distances, 95)
|
||||
|
||||
# 生成颜色(红:远,蓝:近)
|
||||
colors = np.zeros((len(distances), 3))
|
||||
norm_dist = np.clip(distances / (max_dist + 1e-8), 0, 1)
|
||||
colors[:, 0] = norm_dist # 红色通道(远)
|
||||
colors[:, 2] = 1 - norm_dist # 蓝色通道(近)
|
||||
|
||||
pcd_colored.colors = o3d.utility.Vector3dVector(colors)
|
||||
|
||||
# Mesh 着色为灰色
|
||||
mesh_colored = copy.deepcopy(mesh)
|
||||
mesh_colored.paint_uniform_color([0.5, 0.5, 0.5])
|
||||
|
||||
# 可视化
|
||||
o3d.visualization.draw_geometries(
|
||||
[mesh_colored, pcd_colored],
|
||||
window_name="Distance to Mesh (Blue=Near, Red=Far)",
|
||||
width=1024,
|
||||
height=768
|
||||
)
|
||||
|
||||
|
||||
def print_statistics(stats):
|
||||
"""打印统计信息"""
|
||||
print("=" * 60)
|
||||
print("点云到 Mesh 距离统计")
|
||||
print("=" * 60)
|
||||
print(f"平均距离: {stats['mean']:.6f} m ({stats['mean'] * 1000:.3f} mm)")
|
||||
print(f"RMSE: {stats['rmse']:.6f} m ({stats['rmse'] * 1000:.3f} mm)")
|
||||
print(f"中位数: {stats['median']:.6f} m ({stats['median'] * 1000:.3f} mm)")
|
||||
print(f"标准差: {stats['std']:.6f} m ({stats['std'] * 1000:.3f} mm)")
|
||||
print(f"最大距离: {stats['max']:.6f} m ({stats['max'] * 1000:.3f} mm)")
|
||||
print(f"最小距离: {stats['min']:.6f} m ({stats['min'] * 1000:.3f} mm)")
|
||||
print("\n距离分位数:")
|
||||
print(f" 50%: {stats['percentile_50']:.6f} m ({stats['percentile_50'] * 1000:.3f} mm)")
|
||||
print(f" 75%: {stats['percentile_75']:.6f} m ({stats['percentile_75'] * 1000:.3f} mm)")
|
||||
print(f" 90%: {stats['percentile_90']:.6f} m ({stats['percentile_90'] * 1000:.3f} mm)")
|
||||
print(f" 95%: {stats['percentile_95']:.6f} m ({stats['percentile_95'] * 1000:.3f} mm)")
|
||||
print(f" 99%: {stats['percentile_99']:.6f} m ({stats['percentile_99'] * 1000:.3f} mm)")
|
||||
|
||||
|
||||
# ========== 主程序 ==========
|
||||
if __name__ == "__main__":
|
||||
# 读取文件(使用原始字符串避免转义问题)
|
||||
file_path = r"E:\point_distance\data\40_noisy.ply"
|
||||
mesh_path = r"E:\point_distance\data\40.stl"
|
||||
|
||||
print("读取点云...")
|
||||
source_pcd = o3d.io.read_point_cloud(file_path)
|
||||
|
||||
print("读取 Mesh...")
|
||||
mesh = o3d.io.read_triangle_mesh(mesh_path)
|
||||
|
||||
# 检查读取是否成功
|
||||
if len(source_pcd.points) == 0:
|
||||
print("错误:点云读取失败,请检查文件路径")
|
||||
exit()
|
||||
|
||||
if len(mesh.vertices) == 0:
|
||||
print("错误:Mesh 读取失败,请检查文件路径")
|
||||
exit()
|
||||
|
||||
print(f"\n点云点数: {len(source_pcd.points)}")
|
||||
print(f"Mesh 顶点数: {len(mesh.vertices)}")
|
||||
print(f"Mesh 面数: {len(mesh.triangles)}")
|
||||
|
||||
# 可选:Mesh 法线计算(用于更好的可视化)
|
||||
mesh.compute_vertex_normals()
|
||||
|
||||
# 计算距离
|
||||
print("\n计算点云到 Mesh 的距离...")
|
||||
distances = compute_point_to_mesh_distance(source_pcd, mesh)
|
||||
|
||||
# 计算统计信息
|
||||
stats = compute_statistics(distances)
|
||||
|
||||
# 打印统计信息
|
||||
print_statistics(stats)
|
||||
|
||||
# 可视化
|
||||
print("\n可视化...")
|
||||
visualize_distance(source_pcd, mesh, distances)
|
||||
25
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/test5.py
Normal file
25
alg/平整度、平面度、轮廓测量v1.5/archive/relative_code/test5.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import trimesh
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
import time
|
||||
# 1. 读取数据
|
||||
mesh_trimesh = trimesh.load("E:\point_distance\data\\40.stl")
|
||||
pcd_o3d = o3d.io.read_point_cloud("E:\point_distance\data\\40_noisy.ply")
|
||||
points = np.asarray(pcd_o3d.points)
|
||||
start_time = time.time()
|
||||
|
||||
# ========== 1. 读取数据 ==========
|
||||
|
||||
t0 = time.time()
|
||||
# 2. 使用 trimesh 计算每个点到 Mesh 的最近距离
|
||||
# trimesh 的 nearest 方法非常高效且精确
|
||||
closest_points, distances, triangle_ids = mesh_trimesh.nearest.on_surface(points)
|
||||
t1=time.time()
|
||||
|
||||
# distances 即为所求的距离数组
|
||||
print(f"平均距离: {np.mean(distances)}")
|
||||
print(f"最大距离: {np.max(distances)}")
|
||||
print(f" 距离计算: {t1 - t0:.4f} 秒")
|
||||
# 如果需要将结果存回 Open3D 点云进行可视化
|
||||
dists_o3d = o3d.utility.DoubleVector(distances.tolist())
|
||||
# 注意:Open3D 点云对象本身不直接存储标量距离字段用于内置可视化着色
|
||||
Reference in New Issue
Block a user