Files
massive-cloud/alg/train_measurement_system_v3.2_0629/src/task_manager.py

511 lines
20 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 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']:,} 个节点")