Files
massive-cloud/alg/平整度、平面度、轮廓测量v1.5/archive/Denoise.py
lipan 29f1d45d66 feat: 算法升级、整车配准集成与性能分析文档
算法更新: delivery v1.2.1、平整度v1.5、测距v2.2、整车配准v1.3
适配器: vehicle-registration 新增、各算法 manifest 和 adapter 更新
后端: taskCadFiles/source 字段迁移、derivedPointClouds 注册、取消任务联动
前端: ElapsedTime 时长显示、两步算法流程、canRun 逻辑修复
文档: 新增5份算法性能分析报告
2026-06-25 08:22:20 +00:00

431 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()