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:
2026-06-25 08:22:20 +00:00
parent cec0766e67
commit 29f1d45d66
266 changed files with 1311475 additions and 2846 deletions

View 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")

View 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)

View 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)

View 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])

View 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])

View 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])

View 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)

View 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)

View 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)

View 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" → 密度基本不变")

View 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)

View 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])

View 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)

View 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)

View 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)"
)

View 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)

View 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 点云对象本身不直接存储标量距离字段用于内置可视化着色