Files
pqAutomationApp/app/export/excel_exporter.py
2026-04-22 11:02:16 +08:00

507 lines
18 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.
"""测试结果 Excel 导出screen_module / sdr_movie / hdr_movie 统一实现)。"""
import datetime
import os
import traceback
EXCEL_EXPORT_CONFIG = {
"screen_module": {
"title": "屏模组性能测试数据报告",
"type_label": "屏模组",
"log_prefix": "屏模组",
"curve_type": "gamma",
"has_accuracy": False,
"column_widths": {"A": 18, "B": 18, "C": 18, "D": 18, "E": 18,
"F": 15, "G": 15},
},
"sdr_movie": {
"title": "SDR Movie 性能测试数据报告",
"type_label": "SDR Movie",
"log_prefix": "SDR Movie",
"curve_type": "gamma",
"has_accuracy": True,
"column_widths": {c: 18 for c in "ABCDEFG"},
},
"hdr_movie": {
"title": "HDR Movie 性能测试数据报告",
"type_label": "HDR Movie",
"log_prefix": "HDR Movie",
"curve_type": "eotf",
"has_accuracy": True,
"column_widths": {c: 18 for c in "ABCDEFG"},
},
}
# ==================== 样式 ====================
def _build_styles(Font, Alignment, PatternFill, Border, Side):
thin = Side(style="thin")
return {
"title_font": Font(name="微软雅黑", size=16, bold=True, color="FFFFFF"),
"title_fill": PatternFill(start_color="4472C4", end_color="4472C4",
fill_type="solid"),
"title_alignment": Alignment(horizontal="center", vertical="center"),
"section_font": Font(name="微软雅黑", size=13, bold=True, color="FFFFFF"),
"section_fill": PatternFill(start_color="5B9BD5", end_color="5B9BD5",
fill_type="solid"),
"section_alignment": Alignment(horizontal="center", vertical="center"),
"header_font": Font(name="微软雅黑", size=10, bold=True, color="FFFFFF"),
"header_fill": PatternFill(start_color="70AD47", end_color="70AD47",
fill_type="solid"),
"header_alignment": Alignment(horizontal="center", vertical="center",
wrap_text=True),
"data_font": Font(name="微软雅黑", size=10),
"data_alignment": Alignment(horizontal="center", vertical="center"),
"label_font": Font(name="微软雅黑", size=10, bold=True),
"thin_border": Border(left=thin, right=thin, top=thin, bottom=thin),
}
# ==================== 通用写入 ====================
def _write_title(ws, styles, title_text):
ws.merge_cells("A1:G1")
ws["A1"] = title_text
ws["A1"].font = styles["title_font"]
ws["A1"].fill = styles["title_fill"]
ws["A1"].alignment = styles["title_alignment"]
ws.row_dimensions[1].height = 35
def _write_basic_info(ws, styles, row, type_label):
ws.merge_cells(f"A{row}:B{row}")
ws[f"A{row}"] = "📋 测试基本信息"
ws[f"A{row}"].font = styles["section_font"]
ws[f"A{row}"].fill = styles["section_fill"]
ws[f"A{row}"].alignment = styles["section_alignment"]
ws.row_dimensions[row].height = 25
row += 1
info_items = [
("测试时间", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
("测试类型", type_label),
]
for label, value in info_items:
ws[f"A{row}"] = label
ws[f"B{row}"] = value
ws[f"A{row}"].font = styles["label_font"]
ws[f"B{row}"].font = styles["data_font"]
ws[f"A{row}"].border = styles["thin_border"]
ws[f"B{row}"].border = styles["thin_border"]
row += 1
return row
def _write_section_header(ws, styles, row, text, span="A{r}:G{r}"):
ws.merge_cells(span.format(r=row))
ws[f"A{row}"] = text
ws[f"A{row}"].font = styles["section_font"]
ws[f"A{row}"].fill = styles["section_fill"]
ws[f"A{row}"].alignment = styles["section_alignment"]
ws.row_dimensions[row].height = 25
return row + 1
def _write_headers(ws, styles, row, headers):
for col_idx, header in enumerate(headers, start=1):
cell = ws.cell(row=row, column=col_idx)
cell.value = header
cell.font = styles["header_font"]
cell.fill = styles["header_fill"]
cell.alignment = styles["header_alignment"]
cell.border = styles["thin_border"]
return row + 1
def _apply_row_style(ws, row, cols, styles, label_cols=()):
for col in cols:
cell = ws[f"{col}{row}"]
cell.font = (styles["label_font"] if col in label_cols
else styles["data_font"])
cell.border = styles["thin_border"]
def _apply_data_row(ws, row, cols, styles):
for col in cols:
cell = ws[f"{col}{row}"]
cell.font = styles["data_font"]
cell.alignment = styles["data_alignment"]
cell.border = styles["thin_border"]
# ==================== 分区:色域 ====================
def _section_gamut(ws, styles, row, results, log):
rgb_data = results.get_intermediate_data("gamut", "rgb")
gamut_final_result = None
if "gamut" in results.test_items:
gamut_final_result = results.test_items["gamut"].final_result
if not (rgb_data and len(rgb_data) >= 3):
return row
row = _write_section_header(ws, styles, row, "🎨 色域测试数据")
if gamut_final_result:
xy_coverage = gamut_final_result.get("coverage", 0)
uv_coverage = (
gamut_final_result.get("uv_coverage", 0)
or gamut_final_result.get("uv_space_coverage", 0)
or gamut_final_result.get("coverage_uv", 0)
or 0
)
ws[f"A{row}"] = "参考标准"
ws[f"B{row}"] = gamut_final_result.get("reference", "DCI-P3")
ws[f"A{row}"].font = styles["label_font"]
ws[f"B{row}"].font = styles["data_font"]
ws[f"A{row}"].border = styles["thin_border"]
ws[f"B{row}"].border = styles["thin_border"]
row += 1
ws[f"A{row}"] = "XY 色域覆盖率"
ws[f"B{row}"] = f"{xy_coverage:.2f}%"
ws[f"C{row}"] = "UV 色域覆盖率"
ws[f"D{row}"] = f"{uv_coverage:.2f}%"
_apply_row_style(ws, row, ["A", "B", "C", "D"], styles,
label_cols=("A", "C"))
row += 1
row = _write_headers(
ws, styles, row,
["点位", "x 坐标", "y 坐标", "亮度 (cd/m²)", "", "", ""],
)
rgb_labels = ["Red", "Green", "Blue"]
for i, result in enumerate(rgb_data[:3]):
x, y, lv = result[0], result[1], result[2]
ws[f"A{row}"] = rgb_labels[i]
ws[f"B{row}"] = x
ws[f"C{row}"] = y
ws[f"D{row}"] = lv
ws[f"B{row}"].number_format = "0.0000"
ws[f"C{row}"].number_format = "0.0000"
ws[f"D{row}"].number_format = "0.00"
_apply_data_row(ws, row, ["A", "B", "C", "D"], styles)
row += 1
log(" 添加色域数据", level="info")
return row + 1
# ==================== 分区Gamma / EOTF ====================
def _section_curve(ws, styles, row, results, log, curve_type):
if curve_type == "gamma":
section_title = "📊 Gamma 曲线数据"
label_prefix = "Gamma"
data_key = "gamma"
value_header = "Gamma 值"
else:
section_title = "📊 EOTF 曲线数据"
label_prefix = "EOTF"
data_key = "eotf"
value_header = "EOTF 值"
gray_data = results.get_intermediate_data("shared", "gray")
if not gray_data:
gray_data = results.get_intermediate_data(curve_type, "gray")
curve_final_result = None
if curve_type in results.test_items:
curve_final_result = results.test_items[curve_type].final_result
if not (gray_data and len(gray_data) > 0 and curve_final_result):
return row
value_list = curve_final_result.get(data_key, [])
L_bar_list = curve_final_result.get("L_bar", [])
row = _write_section_header(ws, styles, row, section_title)
valid_values = [
item[3]
for item in value_list
if isinstance(item, (list, tuple))
and len(item) >= 4
and 0.5 < item[3] < 5.0
]
if valid_values:
avg_v = sum(valid_values) / len(valid_values)
ws[f"A{row}"] = f"平均 {label_prefix}"
ws[f"B{row}"] = f"{avg_v:.3f}"
ws[f"C{row}"] = f"最大 {label_prefix}"
ws[f"D{row}"] = f"{max(valid_values):.3f}"
ws[f"E{row}"] = f"最小 {label_prefix}"
ws[f"F{row}"] = f"{min(valid_values):.3f}"
_apply_row_style(ws, row, ["A", "B", "C", "D", "E", "F"], styles,
label_cols=("A", "C", "E"))
row += 1
row = _write_headers(
ws, styles, row,
["灰阶 (%)", "x 坐标", "y 坐标", "实测亮度\n(cd/m²)",
"归一化亮度\n(L_bar)", value_header, ""],
)
total_points = len(gray_data)
for i in range(total_points - 1, -1, -1):
gray_level = (100 - int(i * 100 / (total_points - 1))
if total_points > 1 else 0)
x, y, lv = gray_data[i][0], gray_data[i][1], gray_data[i][2]
L_bar_val = L_bar_list[i] if i < len(L_bar_list) else 0
val = None
if (i < len(value_list)
and isinstance(value_list[i], (list, tuple))
and len(value_list[i]) >= 4):
val = value_list[i][3]
ws[f"A{row}"] = gray_level
ws[f"B{row}"] = x
ws[f"C{row}"] = y
ws[f"D{row}"] = lv
ws[f"E{row}"] = L_bar_val
if val is not None and 0.5 < val < 5.0:
ws[f"F{row}"] = val
ws[f"F{row}"].number_format = "0.000"
else:
ws[f"F{row}"] = "N/A"
ws[f"A{row}"].number_format = "0"
ws[f"B{row}"].number_format = "0.0000"
ws[f"C{row}"].number_format = "0.0000"
ws[f"D{row}"].number_format = "0.00"
ws[f"E{row}"].number_format = "0.0000"
_apply_data_row(ws, row, ["A", "B", "C", "D", "E", "F"], styles)
row += 1
log(f" 添加 {label_prefix} 数据", level="info")
return row + 1
# ==================== 分区:色度一致性 ====================
def _section_cct(ws, styles, row, results, log):
gray_data = results.get_intermediate_data("shared", "gray")
if not gray_data:
gray_data = results.get_intermediate_data("cct", "gray")
if not (gray_data and len(gray_data) > 1):
return row
gray_data_no_black = gray_data[:-1]
row = _write_section_header(ws, styles, row, "🌈 色度一致性数据")
x_coords = [d[0] for d in gray_data_no_black]
y_coords = [d[1] for d in gray_data_no_black]
ws[f"A{row}"] = "x 坐标范围"
ws[f"B{row}"] = f"{min(x_coords):.4f} ~ {max(x_coords):.4f}"
ws[f"C{row}"] = "y 坐标范围"
ws[f"D{row}"] = f"{min(y_coords):.4f} ~ {max(y_coords):.4f}"
_apply_row_style(ws, row, ["A", "B", "C", "D"], styles,
label_cols=("A", "C"))
row += 1
row = _write_headers(
ws, styles, row,
["灰阶 (%)", "x 坐标", "y 坐标", "亮度 (cd/m²)", "", "", ""],
)
total_points = len(gray_data)
for i in range(len(gray_data_no_black) - 1, -1, -1):
x, y, lv = (gray_data_no_black[i][0],
gray_data_no_black[i][1],
gray_data_no_black[i][2])
gray_level = (100 - int(i * 100 / (total_points - 1))
if total_points > 1 else 0)
ws[f"A{row}"] = gray_level
ws[f"B{row}"] = x
ws[f"C{row}"] = y
ws[f"D{row}"] = lv
ws[f"A{row}"].number_format = "0"
ws[f"B{row}"].number_format = "0.0000"
ws[f"C{row}"].number_format = "0.0000"
ws[f"D{row}"].number_format = "0.00"
_apply_data_row(ws, row, ["A", "B", "C", "D"], styles)
row += 1
log(" 添加色度一致性数据", level="info")
return row + 1
# ==================== 分区:对比度 ====================
def _section_contrast(ws, styles, row, results, log):
contrast_final_result = None
if "contrast" in results.test_items:
contrast_final_result = results.test_items["contrast"].final_result
if not contrast_final_result:
return row
row = _write_section_header(ws, styles, row, "⚫⚪ 对比度测试数据")
max_lv = contrast_final_result.get("max_luminance", 0)
min_lv = contrast_final_result.get("min_luminance", 0)
contrast_ratio = contrast_final_result.get("contrast_ratio", 0)
info_items = [
("最大亮度(白场)", f"{max_lv:.2f} cd/m²"),
("最小亮度(黑场)", f"{min_lv:.4f} cd/m²"),
("对比度", f"{contrast_ratio:.0f}:1"),
]
for label, value in info_items:
ws[f"A{row}"] = label
ws[f"B{row}"] = value
ws[f"A{row}"].font = styles["label_font"]
ws[f"B{row}"].font = styles["data_font"]
ws[f"A{row}"].border = styles["thin_border"]
ws[f"B{row}"].border = styles["thin_border"]
row += 1
log(" 添加对比度数据", level="info")
return row + 1
# ==================== 分区:色准 ====================
def _section_accuracy(ws, styles, row, results, log):
accuracy_final_result = None
if "accuracy" in results.test_items:
accuracy_final_result = results.test_items["accuracy"].final_result
if not accuracy_final_result:
return row
row = _write_section_header(ws, styles, row, "🎯 色准测试数据")
avg_delta_e = accuracy_final_result.get("avg_delta_e", 0)
max_delta_e = accuracy_final_result.get("max_delta_e", 0)
min_delta_e = accuracy_final_result.get("min_delta_e", 0)
excellent_count = accuracy_final_result.get("excellent_count", 0)
good_count = accuracy_final_result.get("good_count", 0)
poor_count = accuracy_final_result.get("poor_count", 0)
ws[f"A{row}"] = "平均 ΔE"
ws[f"B{row}"] = f"{avg_delta_e:.2f}"
ws[f"C{row}"] = "最大 ΔE"
ws[f"D{row}"] = f"{max_delta_e:.2f}"
ws[f"E{row}"] = "最小 ΔE"
ws[f"F{row}"] = f"{min_delta_e:.2f}"
_apply_row_style(ws, row, ["A", "B", "C", "D", "E", "F"], styles,
label_cols=("A", "C", "E"))
row += 1
ws[f"A{row}"] = "优秀 (ΔE<3)"
ws[f"B{row}"] = f"{excellent_count}"
ws[f"C{row}"] = "良好 (3≤ΔE<5)"
ws[f"D{row}"] = f"{good_count}"
ws[f"E{row}"] = "偏差 (ΔE≥5)"
ws[f"F{row}"] = f"{poor_count}"
_apply_row_style(ws, row, ["A", "B", "C", "D", "E", "F"], styles,
label_cols=("A", "C", "E"))
row += 1
color_patches = accuracy_final_result.get("color_patches", [])
delta_e_values = accuracy_final_result.get("delta_e_values", [])
color_measurements = accuracy_final_result.get("color_measurements", [])
if color_patches and delta_e_values:
row = _write_headers(
ws, styles, row,
["序号", "颜色名称", "x 坐标", "y 坐标",
"亮度 (cd/m²)", "ΔE 2000", "等级"],
)
for idx, (color_name, delta_e) in enumerate(
zip(color_patches, delta_e_values), start=1):
if delta_e < 3:
grade = "优秀"
elif delta_e < 5:
grade = "良好"
else:
grade = "偏差"
x_val, y_val, lv_val = "N/A", "N/A", "N/A"
if color_measurements and idx - 1 < len(color_measurements):
m = color_measurements[idx - 1]
if len(m) >= 3:
x_val, y_val, lv_val = m[0], m[1], m[2]
ws[f"A{row}"] = idx
ws[f"B{row}"] = color_name
ws[f"C{row}"] = x_val
ws[f"D{row}"] = y_val
ws[f"E{row}"] = lv_val
ws[f"F{row}"] = delta_e
ws[f"G{row}"] = grade
ws[f"A{row}"].number_format = "0"
if isinstance(x_val, (int, float)):
ws[f"C{row}"].number_format = "0.0000"
if isinstance(y_val, (int, float)):
ws[f"D{row}"].number_format = "0.0000"
if isinstance(lv_val, (int, float)):
ws[f"E{row}"].number_format = "0.00"
ws[f"F{row}"].number_format = "0.00"
_apply_data_row(ws, row, ["A", "B", "C", "D", "E", "F", "G"], styles)
row += 1
log(" 添加色准数据(含 xy 坐标和亮度)")
return row + 1
# ==================== 主入口 ====================
def export_excel_report(result_dir, current_test_type, selected_items,
results, log):
"""根据测试类型生成统一格式的 Excel 报告。
仅 current_test_type ∈ EXCEL_EXPORT_CONFIG 时应调用本函数;
内部吞掉 ImportErroropenpyxl 未装)与其他 Exception 并通过 log 记录。
"""
if current_test_type not in EXCEL_EXPORT_CONFIG:
return
cfg = EXCEL_EXPORT_CONFIG[current_test_type]
try:
import openpyxl
from openpyxl.styles import (
Font, Alignment, PatternFill, Border, Side,
)
log("=" * 60)
log(f"开始生成{cfg['log_prefix']} Excel 数据报告...")
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "测试数据"
styles = _build_styles(Font, Alignment, PatternFill, Border, Side)
_write_title(ws, styles, cfg["title"])
row = _write_basic_info(ws, styles, 3, cfg["type_label"])
row += 1 # 空行
if "gamut" in selected_items:
row = _section_gamut(ws, styles, row, results, log)
if cfg["curve_type"] == "gamma" and "gamma" in selected_items:
row = _section_curve(ws, styles, row, results, log, "gamma")
if cfg["curve_type"] == "eotf" and "eotf" in selected_items:
row = _section_curve(ws, styles, row, results, log, "eotf")
if "cct" in selected_items:
row = _section_cct(ws, styles, row, results, log)
if "contrast" in selected_items:
row = _section_contrast(ws, styles, row, results, log)
if cfg["has_accuracy"] and "accuracy" in selected_items:
row = _section_accuracy(ws, styles, row, results, log)
for col, width in cfg["column_widths"].items():
ws.column_dimensions[col].width = width
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
excel_filename = f"测试数据_{timestamp}.xlsx"
excel_path = os.path.join(result_dir, excel_filename)
wb.save(excel_path)
log(f"已保存: {excel_filename}", level="success")
log("=" * 60, level="seperator")
except ImportError:
log("未安装 openpyxl 库,跳过 Excel 导出", level="error")
log(" 安装方法: pip install openpyxl")
except Exception as e:
log(f"Excel 导出失败: {str(e)}", level="error")
log(traceback.format_exc())