"""任务管理器 - 负责加载配置、调度任务、汇总结果""" 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']:,} 个节点")