From 982210a724adb04993b15c0d8a85f32407fd71fd Mon Sep 17 00:00:00 2001 From: "xinzhu.yin" Date: Tue, 21 Apr 2026 09:05:24 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=9B=E4=B8=80=E6=AD=A5=E4=BC=98=E5=8C=96sa?= =?UTF-8?q?ve=5Fresult=EF=BC=88=E6=8F=90=E5=8F=96=E5=87=BD=E6=95=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/export/__init__.py | 5 + app/export/excel_exporter.py | 504 ++++++++++++++++++++++++++++++ app/export/image_exporter.py | 38 +++ pqAutomationApp.py | 575 ++--------------------------------- 4 files changed, 571 insertions(+), 551 deletions(-) create mode 100644 app/export/__init__.py create mode 100644 app/export/excel_exporter.py create mode 100644 app/export/image_exporter.py diff --git a/app/export/__init__.py b/app/export/__init__.py new file mode 100644 index 0000000..6d6fe58 --- /dev/null +++ b/app/export/__init__.py @@ -0,0 +1,5 @@ +"""导出模块:测试结果图片与 Excel 报告。""" +from app.export.image_exporter import save_result_images +from app.export.excel_exporter import export_excel_report, EXCEL_EXPORT_CONFIG + +__all__ = ["save_result_images", "export_excel_report", "EXCEL_EXPORT_CONFIG"] diff --git a/app/export/excel_exporter.py b/app/export/excel_exporter.py new file mode 100644 index 0000000..439313a --- /dev/null +++ b/app/export/excel_exporter.py @@ -0,0 +1,504 @@ +"""测试结果 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()) diff --git a/app/export/image_exporter.py b/app/export/image_exporter.py new file mode 100644 index 0000000..da1002c --- /dev/null +++ b/app/export/image_exporter.py @@ -0,0 +1,38 @@ +"""测试结果图表 PNG 导出。""" +import os + + +# (item_key, fig_attr, filename, allowed_test_types_or_None, default_bbox_inches_auto) +IMAGE_SPECS = [ + ("gamut", "gamut_fig", "色域测试结果.png", None, True), + ("gamma", "gamma_fig", "Gamma曲线测试结果.png", + {"screen_module", "sdr_movie"}, True), + ("eotf", "eotf_fig", "EOTF曲线测试结果.png", {"hdr_movie"}, True), + ("cct", "cct_fig", "色度一致性测试结果.png", None, True), + ("contrast", "contrast_fig", "对比度测试结果.png", None, False), + ("accuracy", "accuracy_fig", "色准测试结果.png", + {"sdr_movie", "hdr_movie"}, True), +] + + +def save_result_images(result_dir, current_test_type, selected_items, + figure_provider, log): + """根据测试类型和已选项将各测试图表保存为 PNG。 + + figure_provider: callable(attr_name) -> matplotlib Figure 或 None + log: callable(msg) + """ + for item_key, fig_attr, filename, allowed_types, default_bbox in IMAGE_SPECS: + if item_key not in selected_items: + continue + if allowed_types is not None and current_test_type not in allowed_types: + continue + fig = figure_provider(fig_attr) + if fig is None: + continue + path = os.path.join(result_dir, filename) + if default_bbox: + fig.savefig(path, dpi=300) + else: + fig.savefig(path, dpi=300, bbox_inches="tight") + log(f"✓ 已保存: {filename}") diff --git a/pqAutomationApp.py b/pqAutomationApp.py index a296395..770c89c 100644 --- a/pqAutomationApp.py +++ b/pqAutomationApp.py @@ -29,6 +29,11 @@ from colormath.color_objects import xyYColor, LabColor from colormath.color_conversions import convert_color from colormath.color_diff import delta_e_cie2000 from app.views.pq_debug_panel import PQDebugPanel +from app.export import ( + save_result_images as _save_result_images_impl, + export_excel_report as _export_excel_report_impl, + EXCEL_EXPORT_CONFIG as _EXCEL_EXPORT_CONFIG, +) # Step 0/1 重构:资源工具和纯算法已迁移到 app/ 包,这里重新导入以保持 # 对原函数名/方法名的向后兼容(老代码内部仍用 self.calculate_* 调用)。 @@ -2864,37 +2869,9 @@ class PQAutomationApp: # ========== 延迟1秒后执行清理 ========== self.root.after(1000, cleanup_and_finish) - # ==================== Excel 导出配置 ==================== - _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 save_results(self): - """保存测试结果(图片 + Excel)""" + """保存测试结果(图片 + Excel)。实现委派给 app.export。""" save_dir = filedialog.askdirectory(title="选择保存测试结果的目录") if not save_dir: return @@ -2907,25 +2884,30 @@ class PQAutomationApp: current_test_type = self.test_type_var.get() selected_items = self.get_selected_test_items() + log = self.log_gui.log - self.log_gui.log(f"保存测试类型: {current_test_type}") - self.log_gui.log(f"已选测试项: {selected_items}") + log(f"保存测试类型: {current_test_type}") + log(f"已选测试项: {selected_items}") # 1) 图片 - self._save_result_images(result_dir, current_test_type, selected_items) + _save_result_images_impl( + result_dir, current_test_type, selected_items, + lambda attr: getattr(self, attr, None), + log, + ) # 2) Excel - if ( - current_test_type in self._EXCEL_EXPORT_CONFIG - and hasattr(self, "results") - and self.results - ): - self._export_excel_report(result_dir, current_test_type, selected_items) + if (current_test_type in _EXCEL_EXPORT_CONFIG + and hasattr(self, "results") and self.results): + _export_excel_report_impl( + result_dir, current_test_type, selected_items, + self.results, log, + ) # 3) 成功提示 - self.log_gui.log("=" * 50) - self.log_gui.log(f"✅ 测试结果已保存到目录: {result_dir}") - self.log_gui.log("=" * 50) + log("=" * 50) + log(f"✅ 测试结果已保存到目录: {result_dir}") + log("=" * 50) messagebox.showinfo("成功", f"测试结果已保存到目录:\n{result_dir}") except Exception as e: @@ -2935,515 +2917,6 @@ class PQAutomationApp: self.log_gui.log(traceback.format_exc()) messagebox.showerror("错误", f"保存测试结果失败: {str(e)}") - # ==================== 图片保存 ==================== - def _save_result_images(self, result_dir, current_test_type, selected_items): - """根据测试类型和已选项保存各测试图表 PNG。""" - image_specs = [ - ("gamut", "gamut_fig", "色域测试结果.png", None, True), - ("gamma", "gamma_fig", "Gamma曲线测试结果.png", - {"screen_module", "sdr_movie"}, True), - ("eotf", "eotf_fig", "EOTF曲线测试结果.png", {"hdr_movie"}, True), - ("cct", "cct_fig", "色度一致性测试结果.png", None, True), - ("contrast", "contrast_fig", "对比度测试结果.png", None, False), - ("accuracy", "accuracy_fig", "色准测试结果.png", - {"sdr_movie", "hdr_movie"}, True), - ] - for item_key, fig_attr, filename, allowed_types, default_bbox in image_specs: - if item_key not in selected_items: - continue - if allowed_types is not None and current_test_type not in allowed_types: - continue - fig = getattr(self, fig_attr, None) - if fig is None: - continue - path = os.path.join(result_dir, filename) - if default_bbox: - fig.savefig(path, dpi=300) - else: - fig.savefig(path, dpi=300, bbox_inches="tight") - self.log_gui.log(f"✓ 已保存: {filename}") - - # ==================== Excel 导出总入口 ==================== - def _export_excel_report(self, result_dir, current_test_type, selected_items): - """根据测试类型生成统一格式的 Excel 报告。""" - cfg = self._EXCEL_EXPORT_CONFIG[current_test_type] - try: - import openpyxl - from openpyxl.styles import ( - Font, - Alignment, - PatternFill, - Border, - Side, - ) - - self.log_gui.log("=" * 60) - self.log_gui.log(f"开始生成{cfg['log_prefix']} Excel 数据报告...") - - wb = openpyxl.Workbook() - ws = wb.active - ws.title = "测试数据" - - styles = self._build_excel_styles(Font, Alignment, PatternFill, Border, Side) - - self._excel_write_title(ws, styles, cfg["title"]) - row = self._excel_write_basic_info(ws, styles, 3, cfg["type_label"]) - row += 1 # 空行 - - if "gamut" in selected_items: - row = self._excel_section_gamut(ws, styles, row) - if cfg["curve_type"] == "gamma" and "gamma" in selected_items: - row = self._excel_section_curve(ws, styles, row, "gamma") - if cfg["curve_type"] == "eotf" and "eotf" in selected_items: - row = self._excel_section_curve(ws, styles, row, "eotf") - if "cct" in selected_items: - row = self._excel_section_cct(ws, styles, row) - if "contrast" in selected_items: - row = self._excel_section_contrast(ws, styles, row) - if cfg["has_accuracy"] and "accuracy" in selected_items: - row = self._excel_section_accuracy(ws, styles, row) - - 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) - - self.log_gui.log("✓ 已保存: 测试数据.xlsx") - self.log_gui.log("=" * 60) - - except ImportError: - self.log_gui.log("⚠️ 未安装 openpyxl 库,跳过 Excel 导出") - self.log_gui.log(" 安装方法: pip install openpyxl") - except Exception as e: - self.log_gui.log(f"⚠️ Excel 导出失败: {str(e)}") - import traceback - - self.log_gui.log(traceback.format_exc()) - - # ==================== 样式与通用写入 ==================== - @staticmethod - def _build_excel_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), - } - - @staticmethod - def _excel_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 - - @staticmethod - def _excel_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 - - @staticmethod - def _excel_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 - - @staticmethod - def _excel_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 - - @staticmethod - def _excel_apply_row_style(ws, row, cols, styles, label_cols=()): - """对一整行的指定列应用数据样式;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"] - - @staticmethod - def _excel_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 _excel_section_gamut(self, ws, styles, row): - rgb_data = self.results.get_intermediate_data("gamut", "rgb") - gamut_final_result = None - if "gamut" in self.results.test_items: - gamut_final_result = self.results.test_items["gamut"].final_result - - if not (rgb_data and len(rgb_data) >= 3): - return row - - row = self._excel_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}%" - self._excel_apply_row_style( - ws, row, ["A", "B", "C", "D"], styles, label_cols=("A", "C") - ) - row += 1 - - row = self._excel_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" - self._excel_apply_data_row(ws, row, ["A", "B", "C", "D"], styles) - row += 1 - - self.log_gui.log(" ✓ 添加色域数据") - return row + 1 - - # ==================== 分区:Gamma / EOTF(曲线类) ==================== - def _excel_section_curve(self, ws, styles, row, curve_type): - """curve_type: 'gamma' 或 'eotf'。两者数据结构一致,仅 key/显示名不同。""" - 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 = self.results.get_intermediate_data("shared", "gray") - if not gray_data: - gray_data = self.results.get_intermediate_data(curve_type, "gray") - - curve_final_result = None - if curve_type in self.results.test_items: - curve_final_result = self.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 = self._excel_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}" - self._excel_apply_row_style( - ws, row, ["A", "B", "C", "D", "E", "F"], - styles, label_cols=("A", "C", "E"), - ) - row += 1 - - # 数据表 - row = self._excel_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" - self._excel_apply_data_row( - ws, row, ["A", "B", "C", "D", "E", "F"], styles - ) - row += 1 - - self.log_gui.log(f" ✓ 添加 {label_prefix} 数据") - return row + 1 - - # ==================== 分区:色度一致性 ==================== - def _excel_section_cct(self, ws, styles, row): - gray_data = self.results.get_intermediate_data("shared", "gray") - if not gray_data: - gray_data = self.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 = self._excel_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}" - self._excel_apply_row_style( - ws, row, ["A", "B", "C", "D"], styles, label_cols=("A", "C") - ) - row += 1 - - row = self._excel_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" - self._excel_apply_data_row(ws, row, ["A", "B", "C", "D"], styles) - row += 1 - - self.log_gui.log(" ✓ 添加色度一致性数据") - return row + 1 - - # ==================== 分区:对比度 ==================== - def _excel_section_contrast(self, ws, styles, row): - contrast_final_result = None - if "contrast" in self.results.test_items: - contrast_final_result = self.results.test_items["contrast"].final_result - if not contrast_final_result: - return row - - row = self._excel_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 - - self.log_gui.log(" ✓ 添加对比度数据") - return row + 1 - - # ==================== 分区:色准(仅 SDR/HDR) ==================== - def _excel_section_accuracy(self, ws, styles, row): - accuracy_final_result = None - if "accuracy" in self.results.test_items: - accuracy_final_result = self.results.test_items["accuracy"].final_result - if not accuracy_final_result: - return row - - row = self._excel_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}" - self._excel_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} 个" - self._excel_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 = self._excel_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" - - self._excel_apply_data_row( - ws, row, ["A", "B", "C", "D", "E", "F", "G"], styles - ) - row += 1 - - self.log_gui.log(" ✓ 添加色准数据(含 xy 坐标和亮度)") - return row + 1 - new_pq_results = _run_new_pq_results run_test = _run_run_test run_screen_module_test = _run_run_screen_module_test