feat: update algorithm integrations and point cloud overlays

This commit is contained in:
2026-06-30 16:46:36 +08:00
parent ddca7e5436
commit 7e2e56a50d
64 changed files with 4356 additions and 2781 deletions

View File

@@ -0,0 +1,510 @@
"""任务管理器 - 负责加载配置、调度任务、汇总结果"""
import yaml
import numpy as np
import time
from pathlib import Path
from typing import List, Dict, Any
from datetime import datetime
from pointcloud_loader import PointCloudLoader
from measurements.base import MeasurementResult
from measurements.plane_distance import PlaneToPlaneDistance
from measurements.point_to_plane import PointToPlaneDistance
from measurements.point_to_point import PointToPointDistance
from measurements.plane_with_holes import PlaneToPlaneWithHoles
from measurements.point_to_plane_with_holes import PointToPlaneWithHoles
from measurements.point_to_point_with_holes import PointToPointWithHoles
from measurements.line_to_line import LineToLineDistance
from measurements.line_to_plane import LineToPlaneDistance
from excel_exporter import ExcelExporter
from performance_logger import PerformanceLogger
from measurement_logger import MeasurementLogger
from spatial_index import LinearScanIndex, GridIndex, GridIndexOptimized, OctreeIndex, KDTreeIndex
class TaskManager:
"""测量任务管理器"""
def __init__(self):
"""初始化任务管理器"""
self.config = None
self.point_cloud = None
self.spatial_index = None # 空间索引
self.tasks = []
self.results = []
self.pointcloud_path_override = None
self.output_dir_override = None
def load_config(self, config_path: str) -> bool:
"""
加载配置文件
Args:
config_path: 配置文件路径
Returns:
bool: 是否加载成功
"""
try:
with open(config_path, 'r', encoding='utf-8') as f:
self.config = yaml.safe_load(f)
# 加载点云(支持相对路径)
pointcloud_path = self.pointcloud_path_override or self.config.get('pointcloud_file')
if not pointcloud_path:
print("错误: 配置文件中未指定点云文件路径")
return False
# 处理相对路径
pointcloud_path = self._resolve_path(pointcloud_path, config_path)
# 加载点云
print(f"\n{'='*70}")
print("阶段1: 加载点云")
print("="*70)
print(f" 文件: {Path(pointcloud_path).name}")
load_start = time.time()
self.point_cloud = PointCloudLoader.load_ply(pointcloud_path)
load_time = time.time() - load_start
print(f" 点云数量: {len(self.point_cloud):,} 个点")
print(f" 加载耗时: {load_time:.2f} 秒")
# 保存点云加载信息供性能日志使用
self.pointcloud_load_time = load_time
self.pointcloud_filename = Path(pointcloud_path).name
# 构建空间索引
print(f"\n{'='*70}")
print("阶段2: 构建空间索引")
print("="*70)
index_start = time.time()
self._build_spatial_index()
self.index_build_time = time.time() - index_start
# 解析任务
print(f"\n{'='*70}")
print("阶段3: 解析测量任务")
print("="*70)
tasks_config = self.config.get('tasks', [])
if not tasks_config:
print("警告: 配置文件中没有定义任务")
return True
print(f"\n找到 {len(tasks_config)} 个测量任务")
for task_config in tasks_config:
task = self._create_task(task_config)
if task:
self.tasks.append(task)
print(f" - {task.name} ({task.task_type})")
return True
except FileNotFoundError:
print(f"错误: 配置文件不存在: {config_path}")
return False
except yaml.YAMLError as e:
print(f"错误: 配置文件格式错误: {e}")
return False
except Exception as e:
print(f"错误: 加载配置失败: {e}")
return False
def _resolve_path(self, path: str, config_path: str) -> str:
"""
解析路径(支持相对路径和绝对路径)
Args:
path: 待解析的路径
config_path: 配置文件路径(用于相对路径的基准)
Returns:
str: 解析后的绝对路径
"""
path_obj = Path(path)
if path_obj.is_absolute():
return str(path_obj)
else:
# 相对于配置文件所在目录
config_dir = Path(config_path).parent
return str(config_dir / path_obj)
def override_pointcloud_path(self, path: str):
"""
覆盖点云文件路径(用于命令行参数)
Args:
path: 点云文件路径
"""
self.pointcloud_path_override = path
def override_output_dir(self, output_dir: str):
"""
覆盖输出目录(用于命令行参数)
Args:
output_dir: 输出目录路径
"""
self.output_dir_override = output_dir
def _create_task(self, task_config: Dict[str, Any]):
"""
根据配置创建测量任务
Args:
task_config: 任务配置字典
Returns:
BaseMeasurement: 测量任务实例
"""
task_type = task_config.get('type')
task_name = task_config.get('name', 'Unnamed Task')
# 处理带有孔中心检测的任务类型
if '4_hole_center' in task_type:
if 'plane_to_plane' in task_type:
return PlaneToPlaneWithHoles(
name=task_name,
point_cloud=self.point_cloud,
config=task_config,
spatial_index=self.spatial_index
)
elif 'point_to_plane' in task_type:
return PointToPlaneWithHoles(
name=task_name,
point_cloud=self.point_cloud,
config=task_config,
spatial_index=self.spatial_index
)
elif 'hole_center' in task_type and 'point_to_point' in task_type:
return PointToPointWithHoles(
name=task_name,
point_cloud=self.point_cloud,
config=task_config,
spatial_index=self.spatial_index
)
elif task_type == 'plane_to_plane':
return PlaneToPlaneDistance(
name=task_name,
point_cloud=self.point_cloud,
config=task_config,
spatial_index=self.spatial_index
)
elif task_type == 'point_to_plane':
return PointToPlaneDistance(
name=task_name,
point_cloud=self.point_cloud,
config=task_config,
spatial_index=self.spatial_index
)
elif task_type == 'point_to_point':
return PointToPointDistance(
name=task_name,
point_cloud=self.point_cloud,
config=task_config,
spatial_index=self.spatial_index
)
elif task_type == 'line_to_line':
return LineToLineDistance(
name=task_name,
point_cloud=self.point_cloud,
config=task_config,
spatial_index=self.spatial_index
)
elif task_type == 'line_to_plane':
return LineToPlaneDistance(
name=task_name,
point_cloud=self.point_cloud,
config=task_config,
spatial_index=self.spatial_index
)
else:
print(f"警告: 未知的任务类型: {task_type}")
return None
def run_all_tasks(self) -> List[Dict[str, Any]]:
"""
执行所有测量任务
Returns:
List[Dict]: 任务结果列表
"""
import time
self.results = []
# 创建性能日志记录器
if self.output_dir_override:
output_dir = Path(self.output_dir_override)
else:
output_dir = Path(__file__).parent.parent / "results"
perf_logger = PerformanceLogger(str(output_dir))
# 记录初始化阶段性能
if hasattr(self, 'pointcloud_load_time'):
perf_logger.log_pointcloud_loading(
self.pointcloud_load_time,
len(self.point_cloud),
self.pointcloud_filename
)
if hasattr(self, 'index_build_time'):
index_info = self.spatial_index.get_info() if self.spatial_index else {}
index_method = self.config.get('roi_extraction', {}).get('method', 'unknown')
perf_logger.log_spatial_index(
self.index_build_time,
index_method,
index_info
)
# 创建详细测量日志记录器
detail_logger = MeasurementLogger(str(output_dir))
for i, task in enumerate(self.tasks, 1):
print(f"\n[{i}/{len(self.tasks)}] 执行任务: {task.name}")
print("-" * 60)
# 记录任务开始到详细日志
detail_logger.log_task_start(i, len(self.tasks), task.name)
# 记录开始时间
start_time = time.time()
# 将详细日志记录器传递给任务
task.detail_logger = detail_logger
# 执行任务
result = task.execute()
# 记录结束时间
end_time = time.time()
execution_time = end_time - start_time
# 将执行时间添加到结果数据中
if result.data is None:
result.data = {}
result.data['execution_time'] = execution_time
# 获取详细的时间分布
timing_breakdown = result.data.get('timing_breakdown', {})
# 记录到性能日志
perf_logger.log_task(task.name, execution_time, timing_breakdown)
# 记录结果
result_dict = {
'name': result.task_name,
'type': result.task_type,
'status': 'success' if result.success else 'failed',
'data': result.data if result.success else None,
'error': result.error_message if not result.success else None,
'execution_time': execution_time
}
self.results.append(result_dict)
# 检查是否有宽度差值检查结果,如果有则生成额外的任务19结果
if result.success and result.data and 'width_variation' in result.data:
width_var = result.data['width_variation']
# 创建任务19的结果
task19_name = task.name.replace("任务18", "任务19").replace("门口宽度", "门口宽度差值")
task19_result_dict = {
'name': task19_name,
'type': result.task_type,
'status': 'success',
'data': {
'upper_width': width_var['upper_width'],
'middle_width': width_var['middle_width'],
'lower_width': width_var['lower_width'],
'diff_upper_middle': width_var['diff_upper_middle'],
'diff_middle_lower': width_var['diff_middle_lower'],
'diff_upper_lower': width_var['diff_upper_lower'],
'max_diff': max(abs(width_var['diff_upper_middle']),
abs(width_var['diff_middle_lower']),
abs(width_var['diff_upper_lower'])),
'tolerance': width_var['tolerance'],
'pass': width_var['pass'],
'execution_time': execution_time
},
'error': None,
'execution_time': execution_time
}
self.results.append(task19_result_dict)
# 打印任务19结果
print(f"\n [自动生成] {task19_name}")
print(f" 上部宽度: {width_var['upper_width']:.4f} mm")
print(f" 中部宽度: {width_var['middle_width']:.4f} mm")
print(f" 下部宽度: {width_var['lower_width']:.4f} mm")
print(f" 最大差值: {task19_result_dict['data']['max_diff']:.4f} mm")
print(f" 公差要求: ±{width_var['tolerance']:.1f} mm")
status = "✅ 通过" if width_var['pass'] else "❌ 超差"
print(f" 检查结果: {status}")
# 打印结果
if result.success:
print(f"[成功] 任务完成 (用时: {execution_time:.2f} 秒)")
if result.data:
print(f" 平均距离: {result.data.get('mean_distance', 0):.2f} mm")
print(f" 标准差: {result.data.get('std_distance', 0):.2f} mm")
print(f" 范围: [{result.data.get('min_distance', 0):.2f}, {result.data.get('max_distance', 0):.2f}] mm")
print(f" 测量点数: {result.data.get('point_count', result.data.get('sample_count', 0))}")
if 'source_inlier_ratio' in result.data:
print(f" 源平面内点率: {result.data.get('source_inlier_ratio', 0):.1%}")
if 'target_inlier_ratio' in result.data:
print(f" 目标平面内点率: {result.data.get('target_inlier_ratio', 0):.1%}")
if 'plane_inlier_ratio' in result.data:
print(f" 平面内点率: {result.data.get('plane_inlier_ratio', 0):.1%}")
# 记录成功到详细日志
detail_logger.log_task_success(execution_time)
if result.data:
# 记录测量结果到详细日志
if 'mean_distance' in result.data:
detail_logger.log_distance_result(
result.data.get('mean_distance', 0),
result.data.get('std_distance', 0),
result.data.get('min_distance', 0),
result.data.get('max_distance', 0)
)
else:
print(f"[失败] {result.error_message} (用时: {execution_time:.2f} 秒)")
# 记录失败到详细日志
detail_logger.log_task_failure(result.error_message, execution_time)
# 可视化(如果配置中启用)
if result.success and task.config.get('visualization', False):
self._visualize_result(task, result)
# 保存性能日志
perf_logger.save()
# 关闭详细测量日志
detail_logger.close()
# 导出Excel结果
self._export_results()
return self.results
def _visualize_result(self, task, result: MeasurementResult):
"""
可视化测量结果
Args:
task: 测量任务
result: 测量结果
"""
try:
import open3d as o3d
import matplotlib.pyplot as plt
print(" 生成可视化...")
data = result.data
if not data:
return
# 创建可视化窗口
vis = o3d.visualization.Visualizer()
vis.create_window(window_name=f"测量结果: {task.name}", width=1200, height=800)
# 显示源平面点云(蓝色)
source_pcd = o3d.geometry.PointCloud()
source_pcd.points = o3d.utility.Vector3dVector(data['_source_points'])
source_pcd.paint_uniform_color([0.2, 0.4, 0.8])
vis.add_geometry(source_pcd)
# 显示目标平面点云(绿色)
target_pcd = o3d.geometry.PointCloud()
target_pcd.points = o3d.utility.Vector3dVector(data['_target_points'])
target_pcd.paint_uniform_color([0.2, 0.8, 0.4])
vis.add_geometry(target_pcd)
# 显示采样点(红色)
sampled_pcd = o3d.geometry.PointCloud()
sampled_pcd.points = o3d.utility.Vector3dVector(data['_sampled_points'])
sampled_pcd.paint_uniform_color([0.9, 0.2, 0.2])
vis.add_geometry(sampled_pcd)
# 运行可视化
vis.run()
vis.destroy_window()
except Exception as e:
print(f" 可视化失败: {e}")
def _export_results(self):
"""导出结果到Excel"""
try:
# 转换为MeasurementResult对象列表
measurement_results = []
for result_dict in self.results:
measurement_results.append(MeasurementResult(
task_name=result_dict['name'],
task_type=result_dict['type'],
success=result_dict['status'] == 'success',
data=result_dict.get('data'),
error_message=result_dict.get('error')
))
# 导出Excel(支持命令行覆盖输出目录)
if self.output_dir_override:
output_dir = Path(self.output_dir_override)
else:
output_dir = Path(__file__).parent.parent / "results"
exporter = ExcelExporter(str(output_dir))
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"measurement_results_{timestamp}"
# 传递任务配置信息到导出器
excel_path = exporter.export(measurement_results, filename, self.config.get('tasks', []))
print(f"\n结果已导出到: {excel_path}")
except Exception as e:
print(f"\n导出Excel失败: {e}")
def _build_spatial_index(self):
"""根据配置构建空间索引"""
index_config = self.config.get('roi_extraction', {})
index_method = index_config.get('method', 'linear').lower()
if index_method == 'linear':
print(" 方法: 线性扫描(无索引)")
self.spatial_index = LinearScanIndex(self.point_cloud)
elif index_method == 'grid':
grid_size = index_config.get('grid_size', 100.0)
use_optimized = index_config.get('use_optimized', True) # 默认使用优化版
if use_optimized:
print(f" 方法: Grid网格索引(优化版 - numpy向量化)")
self.spatial_index = GridIndexOptimized(self.point_cloud, grid_size=grid_size)
else:
print(f" 方法: Grid网格索引(原始版 - Python循环)")
self.spatial_index = GridIndex(self.point_cloud, grid_size=grid_size)
elif index_method == 'octree':
max_depth = index_config.get('max_depth', 8)
max_points = index_config.get('max_points_per_node', 1000)
print(f" 方法: Octree八叉树索引")
self.spatial_index = OctreeIndex(self.point_cloud, max_depth=max_depth, max_points_per_node=max_points)
elif index_method == 'kdtree':
print(f" 方法: KDTree索引")
self.spatial_index = KDTreeIndex(self.point_cloud)
else:
print(f" 警告: 未知的索引方法 '{index_method}',使用线性扫描")
self.spatial_index = LinearScanIndex(self.point_cloud)
# 显示索引信息
info = self.spatial_index.get_info()
if 'grid_count' in info:
print(f" 详细信息: {info['grid_count']:,} 个网格, 平均 {info['avg_points_per_grid']:.1f} 点/格")
elif 'node_count' in info:
print(f" 详细信息: {info['node_count']:,} 个节点")