"""Excel结果导出器""" from pathlib import Path from typing import List, Dict, Any from datetime import datetime from openpyxl import Workbook from openpyxl.styles import Font, Alignment, PatternFill, Border, Side from openpyxl.utils import get_column_letter from measurements.base import MeasurementResult class ExcelExporter: """将测量结果导出为Excel文件""" def __init__(self, output_dir: str): """ 初始化导出器 Args: output_dir: 输出目录路径 """ self.output_dir = Path(output_dir) self.output_dir.mkdir(parents=True, exist_ok=True) def export(self, results: List[MeasurementResult], filename: str = None, task_configs: List[Dict[str, Any]] = None) -> str: """ 导出测量结果到Excel Args: results: 测量结果列表 filename: 输出文件名(不含扩展名),默认使用时间戳 task_configs: 任务配置列表(用于提取描述和参数) Returns: str: 导出的Excel文件路径 """ if filename is None: filename = f"measurement_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}" output_path = self.output_dir / f"{filename}.xlsx" # 创建任务名称到配置的映射 task_config_map = {} if task_configs: for config in task_configs: task_config_map[config.get('name')] = config # 创建工作簿 wb = Workbook() ws = wb.active ws.title = "测量结果" # 写入标题 self._write_header(ws) # 写入数据 row = 2 for result in results: task_config = task_config_map.get(result.task_name, {}) row = self._write_result(ws, result, row, task_config) # 调整列宽 self._adjust_column_widths(ws) # 保存文件 wb.save(output_path) return str(output_path) def _write_header(self, ws): """写入表头""" headers = [ "任务名称", "任务类型", "任务描述", "点位参数", "状态", "平均距离 (mm)", "标准差 (mm)", "最小距离 (mm)", "最大距离 (mm)", "采样点数", "源ROI点数", "目标ROI点数", "源平面内点率", "目标平面内点率", "错误信息" ] # 样式 header_font = Font(bold=True, color="FFFFFF") header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") header_alignment = Alignment(horizontal="center", vertical="center") border = Border( left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin') ) for col, header in enumerate(headers, 1): cell = ws.cell(row=1, column=col) cell.value = header cell.font = header_font cell.fill = header_fill cell.alignment = header_alignment cell.border = border def _format_task_params(self, task_config: Dict[str, Any]) -> str: """ 格式化任务参数为字符串 Args: task_config: 任务配置字典 Returns: str: 格式化后的参数字符串 """ task_type = task_config.get('type', '') params_parts = [] if task_type == 'plane_to_plane': # 平面到平面:显示源ROI和目标ROI source_roi = task_config.get('source_roi', {}) target_roi = task_config.get('target_roi', {}) if source_roi: params_parts.append(f"源ROI: min{source_roi.get('bbox_min', [])} max{source_roi.get('bbox_max', [])}") if target_roi: params_parts.append(f"目标ROI: min{target_roi.get('bbox_min', [])} max{target_roi.get('bbox_max', [])}") elif task_type == 'point_to_plane': mode = task_config.get('mode', '') if mode == 'exact_points': # 精确点模式:显示目标点坐标 target_points = task_config.get('target_points', []) if target_points: params_parts.append(f"目标点({len(target_points)}个): {target_points}") plane = task_config.get('plane', {}) if plane: params_parts.append(f"平面: {plane.get('a', 0)}x+{plane.get('b', 0)}y+{plane.get('c', 0)}z+{plane.get('d', 0)}=0") elif mode == 'bbox_filtered': # 包围盒过滤模式:显示包围盒 bboxes = task_config.get('bboxes', []) if bboxes: bbox_strs = [f"{bbox.get('name', '')}: min{bbox.get('min', [])} max{bbox.get('max', [])}" for bbox in bboxes] params_parts.append(f"包围盒: {'; '.join(bbox_strs)}") filter_info = task_config.get('filter', {}) if filter_info: params_parts.append(f"过滤: {filter_info.get('type', '')} axis={filter_info.get('axis', '')} top_n={filter_info.get('top_n', '')}") elif task_type == 'point_to_point': # 点到点:显示点对 point_pairs = task_config.get('point_pairs', []) if point_pairs: pair_strs = [f"{pair.get('name', '')}: {pair.get('point1', [])} <-> {pair.get('point2', [])}" for pair in point_pairs] params_parts.append(f"点对({len(point_pairs)}对): {'; '.join(pair_strs)}") return '\n'.join(params_parts) if params_parts else "无" def _write_result(self, ws, result: MeasurementResult, row: int, task_config: Dict[str, Any]) -> int: """ 写入单个测量结果 Args: ws: 工作表 result: 测量结果 row: 起始行号 task_config: 任务配置 Returns: int: 下一行号 """ border = Border( left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin') ) # 基本信息 ws.cell(row=row, column=1).value = result.task_name ws.cell(row=row, column=2).value = result.task_type ws.cell(row=row, column=3).value = task_config.get('description', '') ws.cell(row=row, column=4).value = self._format_task_params(task_config) ws.cell(row=row, column=5).value = "成功" if result.success else "失败" # 状态颜色 status_cell = ws.cell(row=row, column=5) if result.success: status_cell.fill = PatternFill(start_color="C6EFCE", end_color="C6EFCE", fill_type="solid") status_cell.font = Font(color="006100") else: status_cell.fill = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid") status_cell.font = Font(color="9C0006") # 测量数据 if result.success and result.data: data = result.data ws.cell(row=row, column=6).value = round(data.get('mean_distance', 0), 2) ws.cell(row=row, column=7).value = round(data.get('std_distance', 0), 2) ws.cell(row=row, column=8).value = round(data.get('min_distance', 0), 2) ws.cell(row=row, column=9).value = round(data.get('max_distance', 0), 2) ws.cell(row=row, column=10).value = data.get('sample_count', 0) ws.cell(row=row, column=11).value = data.get('source_roi_points', 0) ws.cell(row=row, column=12).value = data.get('target_roi_points', 0) ws.cell(row=row, column=13).value = f"{data.get('source_inlier_ratio', 0) * 100:.1f}%" ws.cell(row=row, column=14).value = f"{data.get('target_inlier_ratio', 0) * 100:.1f}%" else: # 失败时显示错误信息 ws.cell(row=row, column=15).value = result.error_message # 应用边框和对齐 for col in range(1, 16): cell = ws.cell(row=row, column=col) cell.border = border # 点位参数列使用左对齐,其他居中 if col == 4: cell.alignment = Alignment(horizontal="left", vertical="center", wrap_text=True) else: cell.alignment = Alignment(horizontal="center", vertical="center") return row + 1 def _adjust_column_widths(self, ws): """自动调整列宽""" column_widths = { 1: 25, # 任务名称 2: 18, # 任务类型 3: 30, # 任务描述 4: 60, # 点位参数 5: 10, # 状态 6: 15, # 平均距离 7: 15, # 标准差 8: 15, # 最小距离 9: 15, # 最大距离 10: 12, # 采样点数 11: 12, # 源ROI点数 12: 12, # 目标ROI点数 13: 15, # 源平面内点率 14: 15, # 目标平面内点率 15: 40 # 错误信息 } for col, width in column_widths.items(): ws.column_dimensions[get_column_letter(col)].width = width