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,431 @@
import open3d as o3d
import numpy as np
from scipy.spatial import KDTree
import copy
import argparse
import os
# ========== 1. Chamfer Distance 计算 ==========
def chamfer_distance(pcd1, pcd2, sqrt=True):
"""
计算两个点云之间的 CD 距离(Chamfer Distance)
Args:
pcd1: Open3D 点云对象1
pcd2: Open3D 点云对象2
sqrt: 是否对最终结果开平方根
Returns:
cd: Chamfer Distance 值
cd1: 点云1 到 点云2 的平均距离
cd2: 点云2 到 点云1 的平均距离
"""
points1 = np.asarray(pcd1.points)
points2 = np.asarray(pcd2.points)
if len(points1) == 0 or len(points2) == 0:
return float('inf'), 0, 0
tree1 = KDTree(points1)
tree2 = KDTree(points2)
dist1, _ = tree2.query(points1, k=1)
cd1 = np.mean(dist1 ** 2)
dist2, _ = tree1.query(points2, k=1)
cd2 = np.mean(dist2 ** 2)
cd = cd1 + cd2
if sqrt:
return np.sqrt(cd), cd1, cd2
return cd, cd1, cd2
# ========== 添加高斯噪声 ==========
def add_gaussian_noise(pcd, noise_std=4.0):
"""
给点云添加高斯噪声
Args:
pcd: Open3D 点云对象
noise_std: 噪声标准差
Returns:
noisy_pcd: 添加噪声后的点云
"""
noisy_pcd = copy.deepcopy(pcd)
points = np.asarray(noisy_pcd.points)
noise = np.random.normal(0, noise_std, points.shape)
noisy_points = points + noise
noisy_pcd.points = o3d.utility.Vector3dVector(noisy_points)
return noisy_pcd
# ========== 2. 统计离群点移除 (SOR) ==========
def statistical_outlier_removal(pcd, nb_neighbors=20, std_ratio=2.0):
"""
统计离群点移除
"""
cl, ind = pcd.remove_statistical_outlier(nb_neighbors=nb_neighbors, std_ratio=std_ratio)
inlier_cloud = pcd.select_by_index(ind)
outlier_cloud = pcd.select_by_index(ind, invert=True)
metrics = {
'original_points': len(pcd.points),
'filtered_points': len(inlier_cloud.points),
'removed_points': len(outlier_cloud.points),
'keep_ratio': len(inlier_cloud.points) / len(pcd.points) * 100 if len(pcd.points) > 0 else 0,
'method': 'SOR',
'params': {'nb_neighbors': nb_neighbors, 'std_ratio': std_ratio}
}
return inlier_cloud, outlier_cloud, metrics
# ========== 3. 半径离群点移除 (ROR) ==========
def radius_outlier_removal(pcd, nb_points=5, radius=20):
"""
半径离群点移除
"""
cl, ind = pcd.remove_radius_outlier(nb_points=nb_points, radius=radius)
inlier_cloud = pcd.select_by_index(ind)
outlier_cloud = pcd.select_by_index(ind, invert=True)
metrics = {
'original_points': len(pcd.points),
'filtered_points': len(inlier_cloud.points),
'removed_points': len(outlier_cloud.points),
'keep_ratio': len(inlier_cloud.points) / len(pcd.points) * 100 if len(pcd.points) > 0 else 0,
'method': 'ROR',
'params': {'nb_points': nb_points, 'radius': radius}
}
return inlier_cloud, outlier_cloud, metrics
# ========== 4. 直通滤波 (PF) ==========
def pass_through_filter(pcd, axis='z', min_val=-6000.0, max_val=6000.0):
"""
直通滤波
"""
points = np.asarray(pcd.points)
axis_map = {'x': 0, 'y': 1, 'z': 2}
idx = axis_map[axis]
mask = (points[:, idx] >= min_val) & (points[:, idx] <= max_val)
filtered_points = points[mask]
filtered_pcd = o3d.geometry.PointCloud()
filtered_pcd.points = o3d.utility.Vector3dVector(filtered_points)
metrics = {
'original_points': len(pcd.points),
'filtered_points': len(filtered_points),
'removed_points': len(pcd.points) - len(filtered_points),
'keep_ratio': len(filtered_points) / len(pcd.points) * 100 if len(pcd.points) > 0 else 0,
'method': 'PF',
'params': {'axis': axis, 'min_val': min_val, 'max_val': max_val}
}
return filtered_pcd, metrics
def compute_keep_ratio_and_density(pcd, filtered_pcd, sample_size=1000):
"""计算直通滤波的保留率和密度变化"""
n_orig = len(pcd.points)
n_filt = len(filtered_pcd.points)
if n_orig == 0 or n_filt == 0:
return 0, 0
keep_ratio = n_filt / n_orig * 100
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)
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
# ========== 5. 体素滤波 (VGF) ==========
def voxel_grid_filter(pcd, leaf_size=0.05):
"""
体素滤波
"""
filtered_pcd = pcd.voxel_down_sample(leaf_size)
n_orig = len(pcd.points)
n_filt = len(filtered_pcd.points)
metrics = {
'original_points': n_orig,
'filtered_points': n_filt,
'removed_points': n_orig - n_filt,
'keep_ratio': n_filt / n_orig * 100 if n_orig > 0 else 0,
'downsample_rate': (n_orig - n_filt) / n_orig * 100 if n_orig > 0 else 0,
'compression_ratio': n_orig / n_filt if n_filt > 0 else 0,
'method': 'VGF',
'params': {'leaf_size': leaf_size}
}
if n_filt > 5:
points_sample = 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_sample[idx]
tree = KDTree(points_sample)
dist, _ = tree.query(points_sample, k=2)
metrics['avg_point_distance'] = np.mean(dist[:, 1])
return filtered_pcd, metrics
# ========== 6. 双边滤波 (BF) ==========
def bilateral_filter(pcd, sigma_s=0.01, sigma_r=0.1):
"""
双边滤波
"""
if len(pcd.points) == 0:
return pcd, {'original_points': 0, 'filtered_points': 0, 'removed_points': 0, 'keep_ratio': 0, 'method': 'BF', 'params': {}}
# 确保有法线
if not pcd.has_normals():
pcd.estimate_normals()
points = np.asarray(pcd.points)
normals = np.asarray(pcd.normals)
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
if w.sum() > 0:
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)
metrics = {
'original_points': len(pcd.points),
'filtered_points': len(filtered_points),
'removed_points': 0,
'keep_ratio': 100,
'method': 'BF',
'params': {'sigma_s': sigma_s, 'sigma_r': sigma_r}
}
return filtered_pcd, metrics
# ========== 主函数 ==========
def main():
parser = argparse.ArgumentParser(description='点云去噪算法对比 (SOR, ROR, PF, VGF, BF)')
parser.add_argument('--ply_path', '-p', type=str, default="./High_Speed_Data/PointCloud/11.ply",
help='输入点云文件路径 (.ply)')
parser.add_argument('--noise_std', '-n', type=float, default=4.0,
help='高斯噪声标准差 (默认: 4.0)')
parser.add_argument('--voxel_size', '-v', type=float, default=0.05,
help='体素滤波大小 (默认: 0.05)')
parser.add_argument('--sor_neighbors', type=int, default=20,
help='SOR 邻域点数 (默认: 20)')
parser.add_argument('--sor_std_ratio', type=float, default=2.0,
help='SOR 标准差倍数 (默认: 2.0)')
parser.add_argument('--ror_points', type=int, default=5,
help='ROR 半径内最小点数 (默认: 5)')
parser.add_argument('--ror_radius', type=float, default=20,
help='ROR 搜索半径 (默认: 20)')
parser.add_argument('--bf_sigma_s', type=float, default=0.01,
help='双边滤波空间标准差 (默认: 0.01)')
parser.add_argument('--bf_sigma_r', type=float, default=0.1,
help='双边滤波特征标准差 (默认: 0.1)')
parser.add_argument('--no_visualize', action='store_true',
help='不显示可视化窗口')
args = parser.parse_args()
# 检查文件是否存在
if not os.path.exists(args.ply_path):
print(f"错误: 点云文件不存在 - {args.ply_path}")
return
# 1. 读取原始点云
print(f"读取点云: {args.ply_path}")
pcd_clean = o3d.io.read_point_cloud(args.ply_path)
if len(pcd_clean.points) == 0:
print("读取失败,请检查文件路径")
return
print(f"原始无噪点云点数: {len(pcd_clean.points)}")
# ========== 添加高斯噪声 ==========
print("\n" + "=" * 50)
print(f"添加高斯噪声 (std={args.noise_std})")
print("=" * 50)
pcd_noisy = add_gaussian_noise(pcd_clean, args.noise_std)
print(f"加噪后点云点数: {len(pcd_noisy.points)}")
# 计算噪声水平
points_clean = np.asarray(pcd_clean.points)
points_noisy = np.asarray(pcd_noisy.points)
noise_level = np.mean(np.linalg.norm(points_noisy - points_clean, axis=1))
print(f"平均噪声偏移量: {noise_level:.4f}")
# 加噪点云着色
pcd_noisy.paint_uniform_color([1.0, 0.7, 0.5])
# ========== 运行各去噪算法 ==========
results = {}
# 1. SOR
print("\n" + "=" * 50)
print("统计离群点移除 (SOR)")
print("=" * 50)
sor_filtered, sor_outlier, sor_metrics = statistical_outlier_removal(
pcd_noisy, nb_neighbors=args.sor_neighbors, std_ratio=args.sor_std_ratio)
results['SOR'] = sor_metrics
print(f"保留率: {sor_metrics['keep_ratio']:.2f}%")
print(f"移除: {sor_metrics['removed_points']} 点")
# 2. ROR
print("\n" + "=" * 50)
print("半径离群点移除 (ROR)")
print("=" * 50)
ror_filtered, ror_outlier, ror_metrics = radius_outlier_removal(
pcd_noisy, nb_points=args.ror_points, radius=args.ror_radius)
results['ROR'] = ror_metrics
print(f"保留率: {ror_metrics['keep_ratio']:.2f}%")
print(f"移除: {ror_metrics['removed_points']} 点")
# 3. 直通滤波 (PF)
print("\n" + "=" * 50)
print("直通滤波 (PF)")
print("=" * 50)
points = np.asarray(pcd_noisy.points)
z_min, z_max = points[:, 2].min(), points[:, 2].max()
pf_filtered, pf_metrics = pass_through_filter(pcd_noisy, axis='z', min_val=z_min, max_val=z_max)
results['PF'] = pf_metrics
print(f"保留率: {pf_metrics['keep_ratio']:.2f}%")
keep_ratio, density_change = compute_keep_ratio_and_density(pcd_noisy, pf_filtered)
print(f"密度变化: {density_change:+.2f}%")
# 4. 体素滤波 (VGF)
print("\n" + "=" * 50)
print("体素滤波 (VGF)")
print("=" * 50)
vgf_filtered, vgf_metrics = voxel_grid_filter(pcd_noisy, leaf_size=args.voxel_size)
results['VGF'] = vgf_metrics
print(f"保留率: {vgf_metrics['keep_ratio']:.2f}%")
print(f"压缩比: {vgf_metrics['compression_ratio']:.1f}x")
# 5. 双边滤波 (BF)
print("\n" + "=" * 50)
print("双边滤波 (BF)")
print("=" * 50)
bf_filtered, bf_metrics = bilateral_filter(pcd_noisy, sigma_s=args.bf_sigma_s, sigma_r=args.bf_sigma_r)
results['BF'] = bf_metrics
print(f"保留率: {bf_metrics['keep_ratio']:.2f}%")
# Chamfer Distance 计算
print("\n" + "=" * 50)
print("Chamfer Distance 评估 (与干净点云对比)")
print("=" * 50)
for name, metrics in results.items():
if name == 'BF':
filtered = bf_filtered
elif name == 'SOR':
filtered = sor_filtered
elif name == 'ROR':
filtered = ror_filtered
elif name == 'PF':
filtered = pf_filtered
elif name == 'VGF':
filtered = vgf_filtered
else:
continue
if len(filtered.points) > 0:
cd, _, _ = chamfer_distance(pcd_clean, filtered)
print(f"{name}: CD = {cd:.6f} m")
print("-" * 40)
cd_noisy, _, _ = chamfer_distance(pcd_clean, pcd_noisy)
print(f"加噪点云 (未经滤波): CD = {cd_noisy:.6f} m")
# 可视化
if not args.no_visualize:
print("\n可视化对比...")
pcd_clean.paint_uniform_color([0, 0, 1]) # 蓝色 - 原始干净点云
pcd_noisy.paint_uniform_color([1.0, 0.7, 0.5]) # 浅橙色 - 加噪点云
sor_filtered.paint_uniform_color([0, 1, 0]) # 绿色 - SOR 结果
o3d.visualization.draw_geometries(
[pcd_clean, pcd_noisy],
window_name=f"Clean (Blue, {len(pcd_clean.points)}) vs Noisy (Orange, {len(pcd_noisy.points)})"
)
o3d.visualization.draw_geometries(
[pcd_clean, sor_filtered],
window_name=f"SOR: Clean (Blue, {len(pcd_clean.points)}) vs Filtered (Green, {len(sor_filtered.points)})"
)
if __name__ == "__main__":
main()