"""测试结果 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(" ✓ 添加色域数据") 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} 数据") 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(" ✓ 添加色度一致性数据") 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(" ✓ 添加对比度数据") 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 时应调用本函数; 内部吞掉 ImportError(openpyxl 未装)与其他 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 excel_path = os.path.join(result_dir, "测试数据.xlsx") wb.save(excel_path) log("✓ 已保存: 测试数据.xlsx") log("=" * 60) except ImportError: log("⚠️ 未安装 openpyxl 库,跳过 Excel 导出") log(" 安装方法: pip install openpyxl") except Exception as e: log(f"⚠️ Excel 导出失败: {str(e)}") log(traceback.format_exc())