246 lines
9.2 KiB
Python
246 lines
9.2 KiB
Python
"""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
|