import ttkbootstrap as ttk import tkinter as tk from tkinter import messagebox, filedialog import sys import threading import time import os import datetime import colour import json import traceback import numpy as np import matplotlib.pyplot as plt import matplotlib.image as mpimg import algorithm.pq_algorithm as pq_algorithm from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from app_version import APP_NAME, APP_VERSION, get_app_title from drivers.caSerail import CASerail from drivers.tvSerail import tvSerial from drivers.UCD323_Function import UCDController from drivers.UCD323_Enum import UCDEnum from app.pq.pq_config import PQConfig from app.pq.pq_result import PQResult from app.data_range_converter import convert_pattern_params from PIL import Image, ImageTk from app.views.collapsing_frame import CollapsingFrame # from app.views.pq_history_gui import PQHistoryGUI from app.views.pq_log_gui import PQLogGUI 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 # Step 0/1 重构:资源工具和纯算法已迁移到 app/ 包,这里重新导入以保持 # 对原函数名/方法名的向后兼容(老代码内部仍用 self.calculate_* 调用)。 from app.resources import ( backgroud_style_set, get_resource_path, load_icon, ) from app.tests.color_accuracy import ( calculate_color_accuracy as _calc_color_accuracy, calculate_delta_e_2000 as _calc_delta_e_2000, get_accuracy_color_standards as _get_accuracy_color_standards, ) from app.tests.eotf import calculate_pq_curve as _calc_pq_curve from app.tests.gamma import calculate_gamma as _calc_gamma from app.tests.gamut import calculate_gamut_coverage as _calc_gamut_coverage from app.plots.plot_accuracy import plot_accuracy as _plot_accuracy from app.plots.plot_cct import plot_cct as _plot_cct from app.plots.plot_contrast import plot_contrast as _plot_contrast from app.plots.plot_eotf import plot_eotf as _plot_eotf from app.plots.plot_gamma import plot_gamma as _plot_gamma from app.plots.plot_gamut import plot_gamut as _plot_gamut from app.views.chart_frame import ( clear_chart as _cf_clear_chart, create_result_chart_frame as _cf_create_result_chart_frame, init_accuracy_chart as _cf_init_accuracy_chart, init_cct_chart as _cf_init_cct_chart, init_contrast_chart as _cf_init_contrast_chart, init_eotf_chart as _cf_init_eotf_chart, init_gamma_chart as _cf_init_gamma_chart, init_gamut_chart as _cf_init_gamut_chart, on_chart_tab_changed as _cf_on_chart_tab_changed, update_chart_tabs_state as _cf_update_chart_tabs_state, ) from app.config_io import ( clear_config_file as _cfg_clear_config_file, get_config_path as _cfg_get_config_path, load_pq_config as _cfg_load_pq_config, save_pq_config as _cfg_save_pq_config, ) from app.tests.local_dimming import ( clear_ld_records as _ld_clear_ld_records, measure_ld_luminance as _ld_measure_ld_luminance, save_local_dimming_results as _ld_save_local_dimming_results, send_ld_window as _ld_send_ld_window, start_local_dimming_test as _ld_start_local_dimming_test, stop_local_dimming_test as _ld_stop_local_dimming_test, update_ld_results as _ld_update_ld_results, ) from app.device.connection import ( check_com_connections as _dev_check_com_connections, check_port_connection as _dev_check_port_connection, disconnect_com_connections as _dev_disconnect_com_connections, enable_com_widgets as _dev_enable_com_widgets, get_available_com_ports as _dev_get_available_com_ports, get_available_ucd_ports as _dev_get_available_ucd_ports, refresh_com_ports as _dev_refresh_com_ports, update_connection_indicator as _dev_update_connection_indicator, ) from app.runner.test_runner import ( get_current_test_result as _run_get_current_test_result, new_pq_results as _run_new_pq_results, on_custom_template_test_completed as _run_on_custom_template_test_completed, on_test_completed as _run_on_test_completed, on_test_error as _run_on_test_error, run_custom_sdr_test as _run_run_custom_sdr_test, run_hdr_movie_test as _run_run_hdr_movie_test, run_screen_module_test as _run_run_screen_module_test, run_sdr_movie_test as _run_run_sdr_movie_test, run_test as _run_run_test, send_fix_pattern as _run_send_fix_pattern, test_cct as _run_test_cct, test_color_accuracy as _run_test_color_accuracy, test_contrast as _run_test_contrast, test_custom_sdr as _run_test_custom_sdr, test_eotf as _run_test_eotf, test_gamma as _run_test_gamma, test_gamut as _run_test_gamut, ) from app.views.panel_manager import ( hide_all_panels as _pm_hide_all_panels, register_panel as _pm_register_panel, show_panel as _pm_show_panel, ) from app.views.panels.side_panels import ( create_local_dimming_panel as _side_create_local_dimming_panel, create_log_panel as _side_create_log_panel, toggle_hdr_debug_panel as _side_toggle_hdr_debug_panel, toggle_local_dimming_panel as _side_toggle_local_dimming_panel, toggle_log_panel as _side_toggle_log_panel, toggle_screen_debug_panel as _side_toggle_screen_debug_panel, toggle_sdr_debug_panel as _side_toggle_sdr_debug_panel, ) from app.views.panels.custom_template_panel import ( _clear_custom_result_row as _ctpl__clear_custom_result_row, _run_custom_row_single_step as _ctpl__run_custom_row_single_step, _update_custom_result_row as _ctpl__update_custom_result_row, append_custom_template_result as _ctpl_append_custom_template_result, auto_expand_custom_result_view as _ctpl_auto_expand_custom_result_view, clear_custom_template_results as _ctpl_clear_custom_template_results, copy_custom_result_table as _ctpl_copy_custom_result_table, create_custom_template_result_panel as _ctpl_create_custom_template_result_panel, fill_custom_result_test_data as _ctpl_fill_custom_result_test_data, set_custom_result_table_locked as _ctpl_set_custom_result_table_locked, show_custom_result_context_menu as _ctpl_show_custom_result_context_menu, start_custom_row_single_step as _ctpl_start_custom_row_single_step, start_custom_template_test as _ctpl_start_custom_template_test, ) from app.views.panels.main_layout import ( create_connection_content as _layout_create_connection_content, create_floating_config_panel as _layout_create_floating_config_panel, create_operation_frame as _layout_create_operation_frame, create_signal_format_content as _layout_create_signal_format_content, create_test_items_content as _layout_create_test_items_content, create_test_type_frame as _layout_create_test_type_frame, update_config_info_display as _layout_update_config_info_display, ) from app.views.panels.cct_panel import ( create_cct_params_frame as _cct_create_cct_params_frame, on_cct_param_change as _cct_on_cct_param_change, on_cct_param_focus_out as _cct_on_cct_param_focus_out, on_hdr_cct_param_focus_out as _cct_on_hdr_cct_param_focus_out, on_sdr_cct_param_focus_out as _cct_on_sdr_cct_param_focus_out, recalculate_cct as _cct_recalculate_cct, recalculate_gamut as _cct_recalculate_gamut, reload_cct_params as _cct_reload_cct_params, save_cct_params as _cct_save_cct_params, save_hdr_cct_params as _cct_save_hdr_cct_params, save_sdr_cct_params as _cct_save_sdr_cct_params, ) plt.rcParams["font.family"] = ["sans-serif"] plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"] class PQAutomationApp: def __init__(self, root): self.root = root self.root.title(get_app_title()) self.root.geometry("900x650") self.root.minsize(900, 650) self.root.protocol("WM_DELETE_WINDOW", self.on_closing) self.app_name = APP_NAME self.app_version = APP_VERSION self.config_cleared = False # 初始化设备连接状态 self.ca = None # CA410色度计 self.ucd = UCDController() # 信号发生器 # 初始化测试状态 self.testing = False self.test_thread = None # 采集节奏参数:默认在稳定性与速度之间取平衡,可按现场情况再微调。 self.pattern_settle_time = 0.4 self.pattern_progress_log_step = 5 # 创建主框架 self.main_frame = ttk.Frame(root) self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) backgroud_style_set() # 创建配置对象 self.config = PQConfig() self.results = None # 加载上次保存的设置 self.config_file = self.get_config_path() self.load_pq_config() # 如果加载的配置不是屏模组,强制切换为屏模组 if self.config.current_test_type != "screen_module": self.config.set_current_test_type("screen_module") # 初始化侧边栏功能显示状态 - 使用统一的页面管理 self.current_panel = None # 当前显示的面板名称 self.panels = {} # 存储所有面板的信息 self.log_visible = False # 创建左侧面板 self.left_frame = ttk.Frame(self.main_frame, width=180) self.left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=0, pady=5) self.left_frame.pack_propagate(False) # 创建左侧导航栏 self.sidebar_frame = ttk.Frame(self.left_frame, bootstyle="primary") self.sidebar_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=5) # self.sidebar_frame.pack_propagate(False) # 创建右侧内容区域 self.content_frame = ttk.Frame(self.main_frame) self.content_frame.pack( side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5 ) # 创建右侧内容区域的上中下三个分区 self.control_frame_top = ttk.Frame(self.content_frame) self.control_frame_top.pack( side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5 ) self.control_frame_middle = ttk.Frame(self.content_frame) self.control_frame_middle.pack( side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5 ) self.control_frame_bottom = ttk.Frame(self.content_frame) self.control_frame_bottom.pack( side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5 ) # 创建右上角悬浮配置框 self.create_floating_config_panel() # 创建右侧结果显示区域 self.result_frame = ttk.LabelFrame(self.control_frame_middle, text="测试结果") self.result_frame.pack( side=tk.BOTTOM, fill=tk.BOTH, expand=True, padx=0, pady=5 ) # 创建日志显示区域 self.create_log_panel() # 创建 Local Dimming 面板 self.create_local_dimming_panel() # 创建测试类型选择区域 self.create_test_type_frame() # 创建操作按钮区域 self.create_operation_frame() # 创建结果图表区域 self.create_result_chart_frame() # 创建客户模板结果显示区域(黑底表格) self.create_custom_template_result_panel() # 在所有控件创建完成后,统一初始化测试类型 self.root.after(100, self.initialize_default_test_type) # 状态栏 self.status_var = tk.StringVar(value="就绪") self.status_bar = ttk.Label( root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W ) self.status_bar.pack(side=tk.BOTTOM, fill=tk.X) def _dispatch_ui(self, fn, *args, **kwargs): """把 fn(*args, **kwargs) 调度到 Tk 主线程执行。 统一替代散落各处的 self.root.after(0, lambda: ...) 写法: - 自动捕获异常并记录日志,避免工作线程静默丢失 UI 更新失败; - 参数用位置/关键字传入,绕开 lambda 闭包捕获变量的常见坑; - 允许在 UI 销毁(如关闭窗口)后安全失败。 """ def _runner(): try: fn(*args, **kwargs) except Exception as exc: log = getattr(self, "log_gui", None) if log is not None: try: log.log(f"UI 调度异常: {exc}") except Exception: pass try: self.root.after(0, _runner) except Exception: pass get_config_path = _cfg_get_config_path def initialize_default_test_type(self): """初始化默认测试类型(在所有控件创建完成后调用)""" try: # 强制切换到屏模组 self.change_test_type("screen_module") if hasattr(self, "log_gui"): self.log_gui.log("✓ 默认测试类型已设置为屏模组") except Exception as e: if hasattr(self, "log_gui"): self.log_gui.log(f"初始化默认测试类型失败: {str(e)}") init_gamut_chart = _cf_init_gamut_chart init_gamma_chart = _cf_init_gamma_chart init_eotf_chart = _cf_init_eotf_chart init_cct_chart = _cf_init_cct_chart init_contrast_chart = _cf_init_contrast_chart init_accuracy_chart = _cf_init_accuracy_chart clear_chart = _cf_clear_chart create_floating_config_panel = _layout_create_floating_config_panel create_test_items_content = _layout_create_test_items_content create_cct_params_frame = _cct_create_cct_params_frame on_sdr_cct_param_focus_out = _cct_on_sdr_cct_param_focus_out save_sdr_cct_params = _cct_save_sdr_cct_params on_hdr_cct_param_focus_out = _cct_on_hdr_cct_param_focus_out save_hdr_cct_params = _cct_save_hdr_cct_params recalculate_cct = _cct_recalculate_cct recalculate_gamut = _cct_recalculate_gamut on_cct_param_change = _cct_on_cct_param_change on_cct_param_focus_out = _cct_on_cct_param_focus_out save_cct_params = _cct_save_cct_params reload_cct_params = _cct_reload_cct_params def update_test_items(self): """根据当前测试类型更新测试项目复选框""" # 先隐藏所有测试项目框架 for config in self.test_items.values(): config["frame"].pack_forget() current_test_type = self.config.current_test_type self.test_vars = {} if current_test_type in self.test_items: config = self.test_items[current_test_type] frame = config["frame"] frame.pack(fill=tk.X, padx=5, pady=5) # 添加测试类型标签 type_label = ttk.Label( frame, text=self.get_test_type_display_name(current_test_type), style="primary.TLabel", ) type_label.grid(row=0, column=0, columnspan=2, sticky=tk.W, padx=5, pady=3) # 从配置中读取保存的选择状态 saved_test_items = self.config.current_test_types[current_test_type].get( "test_items", [] ) # 添加复选框 for i, (text, var_name) in enumerate(config["items"]): # 修改:根据配置决定是否勾选 # 如果配置中有该测试项,则勾选;否则不勾选 is_checked = var_name in saved_test_items var = tk.BooleanVar(value=is_checked) self.test_vars[f"{current_test_type}_{var_name}"] = var ttk.Checkbutton( frame, text=text, variable=var, bootstyle="round-toggle", command=self.update_config_and_tabs, ).grid(row=i // 2 + 1, column=i % 2, sticky=tk.W, padx=10, pady=5) # 只有在 chart_notebook 已创建后才更新状态 if hasattr(self, "chart_notebook"): self.update_chart_tabs_state() # 更新色度参数框的显示状态 if hasattr(self, "cct_params_frame"): self.toggle_cct_params_frame() # ========== 新增方法: 更新配置并同步Tab状态 ========== def update_config_and_tabs(self): """更新配置并同步图表Tab状态""" self.update_config() self.update_chart_tabs_state() update_chart_tabs_state = _cf_update_chart_tabs_state def get_test_type_display_name(self, test_type): """获取测试类型的显示名称""" display_names = { "screen_module": "屏模组性能测试", "sdr_movie": "SDR Movie测试", "hdr_movie": "HDR Movie测试", } return display_names.get(test_type, test_type) create_signal_format_content = _layout_create_signal_format_content create_connection_content = _layout_create_connection_content get_available_ucd_ports = _dev_get_available_ucd_ports get_available_com_ports = _dev_get_available_com_ports refresh_com_ports = _dev_refresh_com_ports check_com_connections = _dev_check_com_connections update_connection_indicator = _dev_update_connection_indicator check_port_connection = _dev_check_port_connection enable_com_widgets = _dev_enable_com_widgets disconnect_com_connections = _dev_disconnect_com_connections create_test_type_frame = _layout_create_test_type_frame update_config_info_display = _layout_update_config_info_display create_operation_frame = _layout_create_operation_frame create_custom_template_result_panel = _ctpl_create_custom_template_result_panel show_custom_result_context_menu = _ctpl_show_custom_result_context_menu set_custom_result_table_locked = _ctpl_set_custom_result_table_locked start_custom_row_single_step = _ctpl_start_custom_row_single_step _clear_custom_result_row = _ctpl__clear_custom_result_row _run_custom_row_single_step = _ctpl__run_custom_row_single_step _update_custom_result_row = _ctpl__update_custom_result_row copy_custom_result_table = _ctpl_copy_custom_result_table fill_custom_result_test_data = _ctpl_fill_custom_result_test_data clear_custom_template_results = _ctpl_clear_custom_template_results auto_expand_custom_result_view = _ctpl_auto_expand_custom_result_view append_custom_template_result = _ctpl_append_custom_template_result start_custom_template_test = _ctpl_start_custom_template_test register_panel = _pm_register_panel show_panel = _pm_show_panel hide_all_panels = _pm_hide_all_panels create_log_panel = _side_create_log_panel create_local_dimming_panel = _side_create_local_dimming_panel toggle_local_dimming_panel = _side_toggle_local_dimming_panel toggle_log_panel = _side_toggle_log_panel create_result_chart_frame = _cf_create_result_chart_frame on_chart_tab_changed = _cf_on_chart_tab_changed def change_test_type(self, test_type): """切换测试类型""" # 切换测试类型时,自动隐藏日志面板和 Local Dimming 面板 if self.current_panel in ("log", "local_dimming"): self.hide_all_panels() # 先保存当前测试类型的色度参数 if hasattr(self, "cct_x_ideal_var"): try: current_type = self.config.current_test_type if current_type == "screen_module": self.save_cct_params() elif current_type == "sdr_movie": self.save_sdr_cct_params() elif current_type == "hdr_movie": if hasattr(self, "save_hdr_cct_params"): self.save_hdr_cct_params() except Exception as e: if hasattr(self, "log_gui"): self.log_gui.log(f"保存参数失败: {str(e)}") # 更新测试类型 self.test_type_var.set(test_type) if hasattr(self, "config") and hasattr(self.config, "set_current_test_type"): success = self.config.set_current_test_type(test_type) if not success and hasattr(self, "log_gui"): self.log_gui.log(f"切换测试类型失败: {test_type}") # 更新测试项目和侧边栏 self.update_test_items() self.update_sidebar_selection() self.on_test_type_change() # ========== ✅ 1. 切换信号格式 Tab ========== if hasattr(self, "signal_tabs"): try: # 定义测试类型与信号格式 Tab 的映射 tab_mapping = { "screen_module": 0, # 屏模组测试 "sdr_movie": 1, # SDR测试 "hdr_movie": 2, # HDR } target_tab = tab_mapping.get(test_type, 0) # 先启用所有 Tab for i in range(3): self.signal_tabs.tab(i, state="normal") # 切换到目标 Tab self.signal_tabs.select(target_tab) # 强制刷新显示 self.signal_tabs.update() self.root.update_idletasks() # 强制显示对应的 Frame if target_tab == 0: self.screen_module_signal_frame.tkraise() elif target_tab == 1: self.sdr_signal_frame.tkraise() elif target_tab == 2: self.hdr_signal_frame.tkraise() # 禁用其他 Tab for i in range(3): if i != target_tab: self.signal_tabs.tab(i, state="disabled") # 日志记录 if hasattr(self, "log_gui"): tab_names = ["屏模组测试", "SDR测试", "HDR"] self.log_gui.log(f"✓ 已切换到 {tab_names[target_tab]} 信号格式") except Exception as e: if hasattr(self, "log_gui"): self.log_gui.log(f"切换信号格式失败: {str(e)}") else: if hasattr(self, "log_gui"): self.log_gui.log("⚠️ signal_tabs 尚未创建") # ========== 2. 动态切换 Gamma/EOTF Tab ========== if hasattr(self, "chart_notebook"): try: current_tabs = list(self.chart_notebook.tabs()) # 获取当前 Tab 的索引 gamma_tab_id = str(self.gamma_chart_frame) eotf_tab_id = str(self.eotf_chart_frame) if test_type == "hdr_movie": # ========== HDR 测试:移除 Gamma,添加 EOTF ========== # 1. 如果 Gamma Tab 存在,移除它 if gamma_tab_id in current_tabs: gamma_index = current_tabs.index(gamma_tab_id) self.chart_notebook.forget(gamma_index) if hasattr(self, "log_gui"): self.log_gui.log("✓ 已隐藏 Gamma 曲线 Tab") # 2. 如果 EOTF Tab 不存在,添加它(在色域图之后) if eotf_tab_id not in current_tabs: self.chart_notebook.insert( 1, self.eotf_chart_frame, text="EOTF 曲线" ) if hasattr(self, "log_gui"): self.log_gui.log("✓ 已显示 EOTF 曲线 Tab") else: # ========== SDR/屏模组测试:移除 EOTF,添加 Gamma ========== # 1. 如果 EOTF Tab 存在,移除它 if eotf_tab_id in current_tabs: eotf_index = current_tabs.index(eotf_tab_id) self.chart_notebook.forget(eotf_index) if hasattr(self, "log_gui"): self.log_gui.log("✓ 已隐藏 EOTF 曲线 Tab") # 2. 如果 Gamma Tab 不存在,添加它(在色域图之后) if gamma_tab_id not in current_tabs: self.chart_notebook.insert( 1, self.gamma_chart_frame, text="Gamma 曲线" ) if hasattr(self, "log_gui"): self.log_gui.log("✓ 已显示 Gamma 曲线 Tab") # ========== 3. 仅在 SDR 测试显示客户模板结果 Tab ========== custom_tab_id = str(self.custom_template_tab_frame) current_tabs = list(self.chart_notebook.tabs()) if test_type == "sdr_movie": if custom_tab_id not in current_tabs: self.chart_notebook.add( self.custom_template_tab_frame, text="客户模板结果显示", ) if hasattr(self, "log_gui"): self.log_gui.log("✓ 已显示客户模板结果 Tab") else: if custom_tab_id in current_tabs: self.chart_notebook.forget(self.custom_template_tab_frame) if hasattr(self, "log_gui"): self.log_gui.log("✓ 已隐藏客户模板结果 Tab") # 刷新显示 self.chart_notebook.update_idletasks() except Exception as e: if hasattr(self, "log_gui"): self.log_gui.log(f"切换 Gamma/EOTF Tab 失败: {str(e)}") def update_sidebar_selection(self): """更新侧边栏按钮的选中状态""" # 重置所有按钮样式为默认 self.screen_module_btn.configure(style="Sidebar.TButton") self.sdr_movie_btn.configure(style="Sidebar.TButton") self.hdr_movie_btn.configure(style="Sidebar.TButton") # 设置当前选中按钮的样式 current_type = self.test_type_var.get() if current_type == "screen_module": self.screen_module_btn.configure(style="SidebarSelected.TButton") elif current_type == "sdr_movie": self.sdr_movie_btn.configure(style="SidebarSelected.TButton") elif current_type == "hdr_movie": self.hdr_movie_btn.configure(style="SidebarSelected.TButton") def on_test_type_change(self): """根据测试类型更新内容区域""" test_type = self.test_type_var.get() # 获取当前测试类型的配置 if hasattr(self, "config") and hasattr(self.config, "get_current_config"): current_config = self.config.get_current_config() # 更新配置信息显示 self.update_config_info_display() # SDR 选中时显示客户模版按钮 self.update_custom_button_visibility() def update_custom_button_visibility(self): """只在 SDR 测试时显示客户模版按钮""" if not hasattr(self, "custom_btn") or not hasattr(self, "test_type_var"): return if self.test_type_var.get() == "sdr_movie": if not self.custom_btn.winfo_manager(): self.custom_btn.pack(side=tk.LEFT, padx=5) else: if self.custom_btn.winfo_manager(): self.custom_btn.pack_forget() def start_test(self): """开始测试""" # 检查设备连接状态 if self.ca is None or self.ucd is None: messagebox.showerror("错误", "请先连接CA410和信号发生器") return # 检查是否已经在测试中 if self.testing: messagebox.showinfo("提示", "测试已在进行中") return # ✅ 禁用并隐藏单步调试 if hasattr(self, "debug_panel"): self.debug_panel.disable_all_debug() self.log_gui.log("✓ 单步调试已禁用") if hasattr(self, "debug_container"): self.debug_container.pack_forget() self.log_gui.log("✓ 单步调试面板已隐藏") # 获取测试类型和测试项目 test_type = self.test_type_var.get() test_items = self.get_selected_test_items() if not test_items: messagebox.showinfo("提示", "请至少选择一个测试项目") return # 自动收起配置项 if hasattr(self, "config_panel_frame"): try: if self.config_panel_frame.winfo_viewable(): self.config_panel_frame.btn.invoke() self.root.update_idletasks() time.sleep(0.2) except: pass # 禁用配置项按钮 try: self.config_panel_frame.btn.configure(state="disabled") except: pass # ✅ 新增:禁用色域参考标准下拉框 try: if hasattr(self, "screen_gamut_combo"): self.screen_gamut_combo.configure(state="disabled") if hasattr(self, "sdr_gamut_combo"): self.sdr_gamut_combo.configure(state="disabled") if hasattr(self, "hdr_gamut_combo"): self.hdr_gamut_combo.configure(state="disabled") except Exception as e: self.log_gui.log(f"禁用色域参考标准失败: {str(e)}") # 隐藏所有重新计算按钮 if hasattr(self, "recalc_cct_btn"): try: self.recalc_cct_btn.grid_remove() except: pass if hasattr(self, "sdr_recalc_cct_btn"): try: self.sdr_recalc_cct_btn.grid_remove() except: pass if hasattr(self, "hdr_recalc_cct_btn"): try: self.hdr_recalc_cct_btn.grid_remove() except: pass # 更新UI状态 self.testing = True self.start_btn.config(state=tk.DISABLED) self.stop_btn.config(state=tk.NORMAL) self.save_btn.config(state=tk.DISABLED) self.clear_config_btn.config(state=tk.DISABLED) self.status_var.set("测试进行中...") # 清空日志和图表 self.log_gui.clear_log() self.clear_chart() # 根据测试类型显示不同提示 if test_type == "screen_module": # 屏模组测试:提示 byPass All PQ message = f"开始屏模组性能测试,请 byPass All PQ" elif test_type == "sdr_movie": # SDR测试:提示设置正确图像模式 message = f"开始 SDR Movie 测试,请设置正确的图像模式" elif test_type == "hdr_movie": # HDR测试:提示设置正确图像模式 message = f"开始 HDR Movie 测试,请设置正确的图像模式" else: message = f"开始{self.get_test_type_name(test_type)}测试" confirm = messagebox.askyesno("确认测试", message) if not confirm: self.testing = False self.start_btn.config(state=tk.NORMAL) self.stop_btn.config(state=tk.DISABLED) self.clear_config_btn.config(state=tk.NORMAL) self.status_var.set("测试已取消") # 恢复配置项按钮 if hasattr(self, "config_panel_frame"): try: self.config_panel_frame.btn.configure(state="normal") except: pass return # 在新线程中执行测试 self.test_thread = threading.Thread( target=self.run_test, args=(test_type, test_items) ) self.test_thread.daemon = True self.test_thread.start() def stop_test(self): """停止测试 - 放弃本次所有数据(完全集成版)""" if not self.testing: return # ========== 1. 添加确认对话框 ========== confirm = messagebox.askyesno( "确认停止测试", "测试正在进行中,确定要停止吗?\n\n⚠️ 停止后将放弃本次测试的所有数据,无法保存。", icon="warning", ) if not confirm: self.log_gui.log("用户取消停止操作") return # ========== 2. 立即设置停止标志 ========== self.testing = False # ← 关键:先设置标志,让测试线程停止 self.log_gui.log("=" * 50) self.log_gui.log("⚠️ 正在停止测试...") self.log_gui.log("=" * 50) # ========== 3. 立即更新UI状态(让用户感知到停止)========== self.stop_btn.config(state=tk.DISABLED) self.status_var.set("正在停止测试,请稍候...") self.root.update() # 立即刷新界面 # ========== 4. 等待测试线程结束 ========== if self.test_thread and self.test_thread.is_alive(): self.log_gui.log("等待测试线程结束...") # 等待最多5秒 for i in range(50): # 50 * 0.1秒 = 5秒 if not self.test_thread.is_alive(): break time.sleep(0.1) self.root.update() # 保持界面响应 if self.test_thread.is_alive(): self.log_gui.log("⚠️ 测试线程未能正常结束,将在后台继续等待") else: self.log_gui.log("✓ 测试线程已结束") # ========== 5. 延迟1秒后执行清理(使用内部函数)========== def cleanup_and_finish(): """清理数据并完成停止操作""" # ========== 5.1 清理测试数据 ========== try: self.log_gui.log("清理测试数据...") # 清空测试结果对象 if hasattr(self, "results"): self.results = None self.log_gui.log(" ✓ 测试结果对象已清空") # 清空中间数据缓存 for attr in [ "gamut_results", "gamma_results", "cct_results", "contrast_results", "accuracy_results", ]: if hasattr(self, attr): setattr(self, attr, None) self.log_gui.log(" ✓ 所有中间数据已清空") except Exception as e: self.log_gui.log(f"⚠️ 清理数据时出错: {str(e)}") # ========== 5.2 清空图表显示 ========== try: self.clear_chart() self.log_gui.log("✓ 图表已清空") except Exception as e: self.log_gui.log(f"⚠️ 清空图表时出错: {str(e)}") try: self.clear_custom_template_results() self.log_gui.log("✓ 客户模板结果表格已清空") except Exception as e: self.log_gui.log(f"⚠️ 清空客户模板结果表格失败: {str(e)}") # ========== 5.2.5 跳转到色域图Tab(第一个Tab)========== try: if hasattr(self, "chart_notebook"): self.chart_notebook.select(self.gamut_chart_frame) self.root.update_idletasks() # ← 刷新界面 self.log_gui.log("✓ 已跳转到色域图界面") except Exception as e: self.log_gui.log(f"⚠️ 跳转到色域图失败: {str(e)}") # ========== 5.3 更新UI状态 ========== self.set_custom_result_table_locked(False) self.start_btn.config(state=tk.NORMAL) self.stop_btn.config(state=tk.DISABLED) self.save_btn.config(state=tk.DISABLED) self.clear_config_btn.config(state=tk.NORMAL) if hasattr(self, "custom_btn"): self.custom_btn.config(state=tk.NORMAL) self.status_var.set("测试已停止 - 数据已清空") self.log_gui.log("✓ UI状态已更新") # ========== 5.4 恢复配置项按钮 ========== if hasattr(self, "config_panel_frame"): try: self.config_panel_frame.btn.configure(state="normal") self.log_gui.log("✓ 配置项已恢复") except: pass # ========== 5.4.5 禁用色域参考标准下拉框 ========== try: if hasattr(self, "screen_gamut_combo"): self.screen_gamut_combo.configure(state="disabled") if hasattr(self, "sdr_gamut_combo"): self.sdr_gamut_combo.configure(state="disabled") if hasattr(self, "hdr_gamut_combo"): self.hdr_gamut_combo.configure(state="disabled") self.log_gui.log("✓ 色域参考标准已禁用") except Exception as e: self.log_gui.log(f"禁用色域参考标准失败: {str(e)}") # ========== 5.5 隐藏所有重新计算按钮 ========== try: button_hidden_count = 0 for btn_attr in [ "recalc_cct_btn", "sdr_recalc_cct_btn", "hdr_recalc_cct_btn", "recalc_gamut_btn", # ✅ 新增 "sdr_recalc_gamut_btn", # ✅ 新增 "hdr_recalc_gamut_btn", # ✅ 新增 ]: if hasattr(self, btn_attr): try: getattr(self, btn_attr).grid_remove() button_hidden_count += 1 except: pass if button_hidden_count > 0: self.log_gui.log(f"✓ 已隐藏 {button_hidden_count} 个重新计算按钮") except Exception as e: self.log_gui.log(f"⚠️ 隐藏按钮时出错: {str(e)}") # ========== 5.6 禁用并隐藏单步调试 ========== if hasattr(self, "debug_panel"): try: self.debug_panel.disable_all_debug() self.log_gui.log("✓ 单步调试已禁用") except Exception as e: self.log_gui.log(f"⚠️ 禁用单步调试失败: {str(e)}") # ✅ 隐藏调试面板 if hasattr(self, "debug_container"): try: self.debug_container.pack_forget() self.log_gui.log("✓ 单步调试面板已隐藏") except Exception as e: self.log_gui.log(f"⚠️ 隐藏调试面板失败: {str(e)}") # ========== 5.7 最终日志 ========== self.log_gui.log("=" * 50) self.log_gui.log("✓ 测试已停止,所有数据已清空") self.log_gui.log("=" * 50) # ========== 5.8 显示提示信息 ========== messagebox.showinfo( "测试已停止", "测试已停止,本次测试数据已清空。\n\n可以重新开始新的测试。", ) # ========== 延迟1秒后执行清理 ========== self.root.after(1000, cleanup_and_finish) def save_results(self): """保存测试结果(图片 + Excel)""" save_dir = filedialog.askdirectory(title="选择保存测试结果的目录") if not save_dir: return try: timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") test_type = self.get_test_type_name(self.test_type_var.get()) result_dir = os.path.join(save_dir, f"{test_type}_{timestamp}") os.makedirs(result_dir, exist_ok=True) # ========== ✅ 获取当前测试类型和已选测试项 ========== current_test_type = self.test_type_var.get() selected_items = self.get_selected_test_items() self.log_gui.log(f"保存测试类型: {current_test_type}") self.log_gui.log(f"已选测试项: {selected_items}") # ========== 保存图片 ========== if "gamut" in selected_items and hasattr(self, "gamut_fig"): gamut_path = os.path.join(result_dir, "色域测试结果.png") self.gamut_fig.savefig(gamut_path, dpi=300) self.log_gui.log(f"✓ 已保存: 色域测试结果.png") if current_test_type in ["screen_module", "sdr_movie"]: if "gamma" in selected_items and hasattr(self, "gamma_fig"): gamma_path = os.path.join(result_dir, "Gamma曲线测试结果.png") self.gamma_fig.savefig(gamma_path, dpi=300) self.log_gui.log(f"✓ 已保存: Gamma曲线测试结果.png") if current_test_type == "hdr_movie": if "eotf" in selected_items and hasattr(self, "eotf_fig"): eotf_path = os.path.join(result_dir, "EOTF曲线测试结果.png") self.eotf_fig.savefig(eotf_path, dpi=300) self.log_gui.log(f"✓ 已保存: EOTF曲线测试结果.png") if "cct" in selected_items and hasattr(self, "cct_fig"): cct_path = os.path.join(result_dir, "色度一致性测试结果.png") self.cct_fig.savefig(cct_path, dpi=300) self.log_gui.log(f"✓ 已保存: 色度一致性测试结果.png") if "contrast" in selected_items and hasattr(self, "contrast_fig"): contrast_path = os.path.join(result_dir, "对比度测试结果.png") self.contrast_fig.savefig(contrast_path, dpi=300, bbox_inches="tight") self.log_gui.log(f"✓ 已保存: 对比度测试结果.png") if current_test_type in ["sdr_movie", "hdr_movie"]: if "accuracy" in selected_items and hasattr(self, "accuracy_fig"): accuracy_path = os.path.join(result_dir, "色准测试结果.png") self.accuracy_fig.savefig(accuracy_path, dpi=300) self.log_gui.log(f"✓ 已保存: 色准测试结果.png") # ========== ✅ 屏模组测试 Excel 导出 ========== if ( current_test_type == "screen_module" and hasattr(self, "results") and self.results ): try: import openpyxl from openpyxl.styles import ( Font, Alignment, PatternFill, Border, Side, ) self.log_gui.log("=" * 60) self.log_gui.log("开始生成屏模组 Excel 数据报告...") wb = openpyxl.Workbook() ws = wb.active ws.title = "测试数据" # ========== 样式定义 ========== 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=Side(style="thin"), right=Side(style="thin"), top=Side(style="thin"), bottom=Side(style="thin"), ) # ========== 总标题 ========== ws.merge_cells("A1:G1") ws["A1"] = "屏模组性能测试数据报告" ws["A1"].font = title_font ws["A1"].fill = title_fill ws["A1"].alignment = title_alignment ws.row_dimensions[1].height = 35 # ========== 测试基本信息 ========== row = 3 ws.merge_cells(f"A{row}:B{row}") ws[f"A{row}"] = "📋 测试基本信息" ws[f"A{row}"].font = section_font ws[f"A{row}"].fill = section_fill ws[f"A{row}"].alignment = section_alignment ws.row_dimensions[row].height = 25 row += 1 info_items = [ ( "测试时间", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), ), ("测试类型", "屏模组"), ] for label, value in info_items: ws[f"A{row}"] = label ws[f"B{row}"] = value ws[f"A{row}"].font = label_font ws[f"B{row}"].font = data_font ws[f"A{row}"].border = thin_border ws[f"B{row}"].border = thin_border row += 1 row += 1 # 空行 # ========== 1. 色域数据 ========== if "gamut" in selected_items: 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 rgb_data and len(rgb_data) >= 3: # 分区标题 ws.merge_cells(f"A{row}:G{row}") ws[f"A{row}"] = "🎨 色域测试数据" ws[f"A{row}"].font = section_font ws[f"A{row}"].fill = section_fill ws[f"A{row}"].alignment = section_alignment ws.row_dimensions[row].height = 25 row += 1 if gamut_final_result: # 第一行:参考标准 ws[f"A{row}"] = "参考标准" ws[f"B{row}"] = gamut_final_result.get( "reference", "DCI-P3" ) ws[f"A{row}"].font = label_font ws[f"B{row}"].font = data_font ws[f"A{row}"].border = thin_border ws[f"B{row}"].border = thin_border row += 1 # 第二行:XY 覆盖率 | UV 覆盖率 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}"] = "XY 色域覆盖率" ws[f"B{row}"] = f"{xy_coverage:.2f}%" ws[f"C{row}"] = "UV 色域覆盖率" ws[f"D{row}"] = f"{uv_coverage:.2f}%" ws[f"A{row}"].font = label_font ws[f"B{row}"].font = data_font ws[f"C{row}"].font = label_font ws[f"D{row}"].font = data_font for col in ["A", "B", "C", "D"]: ws[f"{col}{row}"].border = thin_border row += 1 # RGB 数据表格 headers = [ "点位", "x 坐标", "y 坐标", "亮度 (cd/m²)", "", "", "", ] for col_idx, header in enumerate(headers, start=1): cell = ws.cell(row=row, column=col_idx) cell.value = header cell.font = header_font cell.fill = header_fill cell.alignment = header_alignment cell.border = thin_border row += 1 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"A{row}"].font = data_font ws[f"A{row}"].alignment = data_alignment ws[f"A{row}"].border = thin_border ws[f"B{row}"].number_format = "0.0000" ws[f"B{row}"].font = data_font ws[f"B{row}"].alignment = data_alignment ws[f"B{row}"].border = thin_border ws[f"C{row}"].number_format = "0.0000" ws[f"C{row}"].font = data_font ws[f"C{row}"].alignment = data_alignment ws[f"C{row}"].border = thin_border ws[f"D{row}"].number_format = "0.00" ws[f"D{row}"].font = data_font ws[f"D{row}"].alignment = data_alignment ws[f"D{row}"].border = thin_border row += 1 row += 1 # 空行 self.log_gui.log(" ✓ 添加色域数据") # ========== 2. Gamma 数据 ========== if "gamma" in selected_items: gray_data = self.results.get_intermediate_data("shared", "gray") if not gray_data: gray_data = self.results.get_intermediate_data( "gamma", "gray" ) gamma_final_result = None if "gamma" in self.results.test_items: gamma_final_result = self.results.test_items[ "gamma" ].final_result if gray_data and len(gray_data) > 0 and gamma_final_result: gamma_list = gamma_final_result.get("gamma", []) L_bar_list = gamma_final_result.get("L_bar", []) # 分区标题 ws.merge_cells(f"A{row}:G{row}") ws[f"A{row}"] = "📊 Gamma 曲线数据" ws[f"A{row}"].font = section_font ws[f"A{row}"].fill = section_fill ws[f"A{row}"].alignment = section_alignment ws.row_dimensions[row].height = 25 row += 1 # Gamma 统计信息 valid_gamma = [] if gamma_list: for item in gamma_list: if ( isinstance(item, (list, tuple)) and len(item) >= 4 ): gamma_val = item[3] if 0.5 < gamma_val < 5.0: valid_gamma.append(gamma_val) if valid_gamma: avg_gamma = sum(valid_gamma) / len(valid_gamma) max_gamma = max(valid_gamma) min_gamma = min(valid_gamma) ws[f"A{row}"] = "平均 Gamma" ws[f"B{row}"] = f"{avg_gamma:.3f}" ws[f"C{row}"] = "最大 Gamma" ws[f"D{row}"] = f"{max_gamma:.3f}" ws[f"E{row}"] = "最小 Gamma" ws[f"F{row}"] = f"{min_gamma:.3f}" for col in ["A", "B", "C", "D", "E", "F"]: ws[f"{col}{row}"].font = ( label_font if col in ["A", "C", "E"] else data_font ) ws[f"{col}{row}"].border = thin_border row += 1 # Gamma 数据表格 headers = [ "灰阶 (%)", "x 坐标", "y 坐标", "实测亮度\n(cd/m²)", "归一化亮度\n(L_bar)", "Gamma 值", "", ] for col_idx, header in enumerate(headers, start=1): cell = ws.cell(row=row, column=col_idx) cell.value = header cell.font = header_font cell.fill = header_fill cell.alignment = header_alignment cell.border = thin_border row += 1 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 gamma_val = None if ( i < len(gamma_list) and isinstance(gamma_list[i], (list, tuple)) and len(gamma_list[i]) >= 4 ): gamma_val = gamma_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 gamma_val is not None and 0.5 < gamma_val < 5.0: ws[f"F{row}"] = gamma_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" for col in ["A", "B", "C", "D", "E", "F"]: ws[f"{col}{row}"].font = data_font ws[f"{col}{row}"].alignment = data_alignment ws[f"{col}{row}"].border = thin_border row += 1 row += 1 self.log_gui.log(" ✓ 添加 Gamma 数据") # ========== 3. 色度一致性数据 ========== if "cct" in selected_items: gray_data = self.results.get_intermediate_data("shared", "gray") if not gray_data: gray_data = self.results.get_intermediate_data( "cct", "gray" ) if gray_data and len(gray_data) > 1: gray_data_no_black = gray_data[:-1] # 分区标题 ws.merge_cells(f"A{row}:G{row}") ws[f"A{row}"] = "🌈 色度一致性数据" ws[f"A{row}"].font = section_font ws[f"A{row}"].fill = section_fill ws[f"A{row}"].alignment = section_alignment ws.row_dimensions[row].height = 25 row += 1 # 色度波动信息 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}" for col in ["A", "B", "C", "D"]: ws[f"{col}{row}"].font = ( label_font if col in ["A", "C"] else data_font ) ws[f"{col}{row}"].border = thin_border row += 1 # 数据表格 headers = [ "灰阶 (%)", "x 坐标", "y 坐标", "亮度 (cd/m²)", "", "", "", ] for col_idx, header in enumerate(headers, start=1): cell = ws.cell(row=row, column=col_idx) cell.value = header cell.font = header_font cell.fill = header_fill cell.alignment = header_alignment cell.border = thin_border row += 1 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" for col in ["A", "B", "C", "D"]: ws[f"{col}{row}"].font = data_font ws[f"{col}{row}"].alignment = data_alignment ws[f"{col}{row}"].border = thin_border row += 1 row += 1 self.log_gui.log(" ✓ 添加色度一致性数据") # ========== 4. 对比度数据 ========== if "contrast" in selected_items: contrast_final_result = None if "contrast" in self.results.test_items: contrast_final_result = self.results.test_items[ "contrast" ].final_result if contrast_final_result: # 分区标题 ws.merge_cells(f"A{row}:G{row}") ws[f"A{row}"] = "⚫⚪ 对比度测试数据" ws[f"A{row}"].font = section_font ws[f"A{row}"].fill = section_fill ws[f"A{row}"].alignment = section_alignment ws.row_dimensions[row].height = 25 row += 1 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 = label_font ws[f"B{row}"].font = data_font ws[f"A{row}"].border = thin_border ws[f"B{row}"].border = thin_border row += 1 self.log_gui.log(" ✓ 添加对比度数据") # ========== 调整列宽 ========== ws.column_dimensions["A"].width = 18 ws.column_dimensions["B"].width = 18 ws.column_dimensions["C"].width = 18 ws.column_dimensions["D"].width = 18 ws.column_dimensions["E"].width = 18 ws.column_dimensions["F"].width = 15 ws.column_dimensions["G"].width = 15 # ========== 保存 Excel ========== excel_path = os.path.join(result_dir, "测试数据.xlsx") wb.save(excel_path) self.log_gui.log(f"✓ 已保存: 测试数据.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()) # ========== ✅ SDR Movie 测试 Excel 导出 ========== elif ( current_test_type == "sdr_movie" and hasattr(self, "results") and self.results ): try: import openpyxl from openpyxl.styles import ( Font, Alignment, PatternFill, Border, Side, ) self.log_gui.log("=" * 60) self.log_gui.log("开始生成 SDR Movie Excel 数据报告...") wb = openpyxl.Workbook() ws = wb.active ws.title = "测试数据" # ========== 样式定义 ========== 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=Side(style="thin"), right=Side(style="thin"), top=Side(style="thin"), bottom=Side(style="thin"), ) # ========== 总标题 ========== ws.merge_cells("A1:G1") ws["A1"] = "SDR Movie 性能测试数据报告" ws["A1"].font = title_font ws["A1"].fill = title_fill ws["A1"].alignment = title_alignment ws.row_dimensions[1].height = 35 # ========== 测试基本信息 ========== row = 3 ws.merge_cells(f"A{row}:B{row}") ws[f"A{row}"] = "📋 测试基本信息" ws[f"A{row}"].font = section_font ws[f"A{row}"].fill = section_fill ws[f"A{row}"].alignment = section_alignment ws.row_dimensions[row].height = 25 row += 1 info_items = [ ( "测试时间", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), ), ("测试类型", "SDR Movie"), ] for label, value in info_items: ws[f"A{row}"] = label ws[f"B{row}"] = value ws[f"A{row}"].font = label_font ws[f"B{row}"].font = data_font ws[f"A{row}"].border = thin_border ws[f"B{row}"].border = thin_border row += 1 row += 1 # 空行 # ========== 1. 色域数据 ========== if "gamut" in selected_items: 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 rgb_data and len(rgb_data) >= 3: ws.merge_cells(f"A{row}:G{row}") ws[f"A{row}"] = "🎨 色域测试数据" ws[f"A{row}"].font = section_font ws[f"A{row}"].fill = section_fill ws[f"A{row}"].alignment = section_alignment ws.row_dimensions[row].height = 25 row += 1 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 0 ) ws[f"A{row}"] = "参考标准" ws[f"B{row}"] = gamut_final_result.get( "reference", "DCI-P3" ) ws[f"A{row}"].font = label_font ws[f"B{row}"].font = data_font ws[f"A{row}"].border = thin_border ws[f"B{row}"].border = 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}%" for col in ["A", "B", "C", "D"]: ws[f"{col}{row}"].font = ( label_font if col in ["A", "C"] else data_font ) ws[f"{col}{row}"].border = thin_border row += 1 # RGB 数据表格 headers = [ "点位", "x 坐标", "y 坐标", "亮度 (cd/m²)", "", "", "", ] for col_idx, header in enumerate(headers, start=1): cell = ws.cell(row=row, column=col_idx) cell.value = header cell.font = header_font cell.fill = header_fill cell.alignment = header_alignment cell.border = thin_border row += 1 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" for col in ["A", "B", "C", "D"]: ws[f"{col}{row}"].font = data_font ws[f"{col}{row}"].alignment = data_alignment ws[f"{col}{row}"].border = thin_border row += 1 row += 1 self.log_gui.log(" ✓ 添加色域数据") # ========== 2. Gamma 数据 ========== if "gamma" in selected_items: gray_data = self.results.get_intermediate_data("shared", "gray") if not gray_data: gray_data = self.results.get_intermediate_data( "gamma", "gray" ) gamma_final_result = None if "gamma" in self.results.test_items: gamma_final_result = self.results.test_items[ "gamma" ].final_result if gray_data and gamma_final_result: gamma_list = gamma_final_result.get("gamma", []) L_bar_list = gamma_final_result.get("L_bar", []) ws.merge_cells(f"A{row}:G{row}") ws[f"A{row}"] = "📊 Gamma 曲线数据" ws[f"A{row}"].font = section_font ws[f"A{row}"].fill = section_fill ws[f"A{row}"].alignment = section_alignment ws.row_dimensions[row].height = 25 row += 1 # Gamma 统计 valid_gamma = [ item[3] for item in gamma_list if isinstance(item, (list, tuple)) and len(item) >= 4 and 0.5 < item[3] < 5.0 ] if valid_gamma: avg_gamma = sum(valid_gamma) / len(valid_gamma) ws[f"A{row}"] = "平均 Gamma" ws[f"B{row}"] = f"{avg_gamma:.3f}" ws[f"C{row}"] = "最大 Gamma" ws[f"D{row}"] = f"{max(valid_gamma):.3f}" ws[f"E{row}"] = "最小 Gamma" ws[f"F{row}"] = f"{min(valid_gamma):.3f}" for col in ["A", "B", "C", "D", "E", "F"]: ws[f"{col}{row}"].font = ( label_font if col in ["A", "C", "E"] else data_font ) ws[f"{col}{row}"].border = thin_border row += 1 # Gamma 数据表格 headers = [ "灰阶 (%)", "x 坐标", "y 坐标", "实测亮度\n(cd/m²)", "归一化亮度\n(L_bar)", "Gamma 值", "", ] for col_idx, header in enumerate(headers, start=1): cell = ws.cell(row=row, column=col_idx) cell.value = header cell.font = header_font cell.fill = header_fill cell.alignment = header_alignment cell.border = thin_border row += 1 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 gamma_val = None if ( i < len(gamma_list) and isinstance(gamma_list[i], (list, tuple)) and len(gamma_list[i]) >= 4 ): gamma_val = gamma_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 gamma_val is not None and 0.5 < gamma_val < 5.0: ws[f"F{row}"] = gamma_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" for col in ["A", "B", "C", "D", "E", "F"]: ws[f"{col}{row}"].font = data_font ws[f"{col}{row}"].alignment = data_alignment ws[f"{col}{row}"].border = thin_border row += 1 row += 1 self.log_gui.log(" ✓ 添加 Gamma 数据") # ========== 3. 色度一致性数据 ========== if "cct" in selected_items: gray_data = self.results.get_intermediate_data("shared", "gray") if not gray_data: gray_data = self.results.get_intermediate_data( "cct", "gray" ) if gray_data and len(gray_data) > 1: gray_data_no_black = gray_data[:-1] ws.merge_cells(f"A{row}:G{row}") ws[f"A{row}"] = "🌈 色度一致性数据" ws[f"A{row}"].font = section_font ws[f"A{row}"].fill = section_fill ws[f"A{row}"].alignment = section_alignment ws.row_dimensions[row].height = 25 row += 1 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}" for col in ["A", "B", "C", "D"]: ws[f"{col}{row}"].font = ( label_font if col in ["A", "C"] else data_font ) ws[f"{col}{row}"].border = thin_border row += 1 headers = [ "灰阶 (%)", "x 坐标", "y 坐标", "亮度 (cd/m²)", "", "", "", ] for col_idx, header in enumerate(headers, start=1): cell = ws.cell(row=row, column=col_idx) cell.value = header cell.font = header_font cell.fill = header_fill cell.alignment = header_alignment cell.border = thin_border row += 1 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" for col in ["A", "B", "C", "D"]: ws[f"{col}{row}"].font = data_font ws[f"{col}{row}"].alignment = data_alignment ws[f"{col}{row}"].border = thin_border row += 1 row += 1 self.log_gui.log(" ✓ 添加色度一致性数据") # ========== 4. 对比度数据 ========== if "contrast" in selected_items: contrast_final_result = None if "contrast" in self.results.test_items: contrast_final_result = self.results.test_items[ "contrast" ].final_result if contrast_final_result: ws.merge_cells(f"A{row}:G{row}") ws[f"A{row}"] = "⚫⚪ 对比度测试数据" ws[f"A{row}"].font = section_font ws[f"A{row}"].fill = section_fill ws[f"A{row}"].alignment = section_alignment ws.row_dimensions[row].height = 25 row += 1 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 = label_font ws[f"B{row}"].font = data_font ws[f"A{row}"].border = thin_border ws[f"B{row}"].border = thin_border row += 1 row += 1 self.log_gui.log(" ✓ 添加对比度数据") # ========== 5. 色准数据(SDR 特有)========== if "accuracy" in selected_items: accuracy_final_result = None if "accuracy" in self.results.test_items: accuracy_final_result = self.results.test_items[ "accuracy" ].final_result if accuracy_final_result: ws.merge_cells(f"A{row}:G{row}") ws[f"A{row}"] = "🎯 色准测试数据" ws[f"A{row}"].font = section_font ws[f"A{row}"].fill = section_fill ws[f"A{row}"].alignment = section_alignment ws.row_dimensions[row].height = 25 row += 1 # 色准统计信息 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}" for col in ["A", "B", "C", "D", "E", "F"]: ws[f"{col}{row}"].font = ( label_font if col in ["A", "C", "E"] else data_font ) ws[f"{col}{row}"].border = thin_border 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} 个" for col in ["A", "B", "C", "D", "E", "F"]: ws[f"{col}{row}"].font = ( label_font if col in ["A", "C", "E"] else data_font ) ws[f"{col}{row}"].border = thin_border row += 1 # ========== 色准详细数据表格(带 xy 坐标和亮度)========== color_patches = accuracy_final_result.get( "color_patches", [] ) delta_e_values = accuracy_final_result.get( "delta_e_values", [] ) # ✅ 获取原始测量数据(包含 xy 和亮度) color_measurements = accuracy_final_result.get( "color_measurements", [] ) if color_patches and delta_e_values: # 表头 headers = [ "序号", "颜色名称", "x 坐标", "y 坐标", "亮度 (cd/m²)", "ΔE 2000", "等级", ] for col_idx, header in enumerate(headers, start=1): cell = ws.cell(row=row, column=col_idx) cell.value = header cell.font = header_font cell.fill = header_fill cell.alignment = header_alignment cell.border = thin_border row += 1 # 数据行 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, y, 亮度) x_val = "N/A" y_val = "N/A" lv_val = "N/A" if color_measurements and idx - 1 < len( color_measurements ): measurement = color_measurements[idx - 1] if len(measurement) >= 3: x_val = measurement[0] y_val = measurement[1] lv_val = measurement[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" for col in ["A", "B", "C", "D", "E", "F", "G"]: ws[f"{col}{row}"].font = data_font ws[f"{col}{row}"].alignment = data_alignment ws[f"{col}{row}"].border = thin_border row += 1 row += 1 self.log_gui.log(" ✓ 添加色准数据(含 xy 坐标和亮度)") # ========== 调整列宽 ========== for col in ["A", "B", "C", "D", "E", "F", "G"]: ws.column_dimensions[col].width = 18 # ========== 保存 Excel ========== excel_path = os.path.join(result_dir, "测试数据.xlsx") wb.save(excel_path) self.log_gui.log(f"✓ 已保存: 测试数据.xlsx") self.log_gui.log("=" * 60) except ImportError: self.log_gui.log("⚠️ 未安装 openpyxl 库,跳过 Excel 导出") except Exception as e: self.log_gui.log(f"⚠️ Excel 导出失败: {str(e)}") import traceback self.log_gui.log(traceback.format_exc()) # ========== ✅ HDR Movie 测试 Excel 导出 ========== elif ( current_test_type == "hdr_movie" and hasattr(self, "results") and self.results ): try: import openpyxl from openpyxl.styles import ( Font, Alignment, PatternFill, Border, Side, ) self.log_gui.log("=" * 60) self.log_gui.log("开始生成 HDR Movie Excel 数据报告...") wb = openpyxl.Workbook() ws = wb.active ws.title = "测试数据" # ========== 样式定义 ========== 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=Side(style="thin"), right=Side(style="thin"), top=Side(style="thin"), bottom=Side(style="thin"), ) # ========== 总标题 ========== ws.merge_cells("A1:G1") ws["A1"] = "HDR Movie 性能测试数据报告" ws["A1"].font = title_font ws["A1"].fill = title_fill ws["A1"].alignment = title_alignment ws.row_dimensions[1].height = 35 # ========== 测试基本信息 ========== row = 3 ws.merge_cells(f"A{row}:B{row}") ws[f"A{row}"] = "📋 测试基本信息" ws[f"A{row}"].font = section_font ws[f"A{row}"].fill = section_fill ws[f"A{row}"].alignment = section_alignment ws.row_dimensions[row].height = 25 row += 1 info_items = [ ( "测试时间", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), ), ("测试类型", "HDR Movie"), ] for label, value in info_items: ws[f"A{row}"] = label ws[f"B{row}"] = value ws[f"A{row}"].font = label_font ws[f"B{row}"].font = data_font ws[f"A{row}"].border = thin_border ws[f"B{row}"].border = thin_border row += 1 row += 1 # ========== 1. 色域数据 ========== if "gamut" in selected_items: 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 rgb_data and len(rgb_data) >= 3: ws.merge_cells(f"A{row}:G{row}") ws[f"A{row}"] = "🎨 色域测试数据" ws[f"A{row}"].font = section_font ws[f"A{row}"].fill = section_fill ws[f"A{row}"].alignment = section_alignment ws.row_dimensions[row].height = 25 row += 1 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 0 ) ws[f"A{row}"] = "参考标准" ws[f"B{row}"] = gamut_final_result.get( "reference", "DCI-P3" ) ws[f"A{row}"].font = label_font ws[f"B{row}"].font = data_font ws[f"A{row}"].border = thin_border ws[f"B{row}"].border = 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}%" for col in ["A", "B", "C", "D"]: ws[f"{col}{row}"].font = ( label_font if col in ["A", "C"] else data_font ) ws[f"{col}{row}"].border = thin_border row += 1 # RGB 数据表格 headers = [ "点位", "x 坐标", "y 坐标", "亮度 (cd/m²)", "", "", "", ] for col_idx, header in enumerate(headers, start=1): cell = ws.cell(row=row, column=col_idx) cell.value = header cell.font = header_font cell.fill = header_fill cell.alignment = header_alignment cell.border = thin_border row += 1 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" for col in ["A", "B", "C", "D"]: ws[f"{col}{row}"].font = data_font ws[f"{col}{row}"].alignment = data_alignment ws[f"{col}{row}"].border = thin_border row += 1 row += 1 self.log_gui.log(" ✓ 添加色域数据") # ========== 2. EOTF 数据(HDR 特有)========== if "eotf" in selected_items: gray_data = self.results.get_intermediate_data("shared", "gray") if not gray_data: gray_data = self.results.get_intermediate_data( "eotf", "gray" ) eotf_final_result = None if "eotf" in self.results.test_items: eotf_final_result = self.results.test_items[ "eotf" ].final_result if gray_data and len(gray_data) > 0 and eotf_final_result: eotf_list = eotf_final_result.get("eotf", []) L_bar_list = eotf_final_result.get("L_bar", []) ws.merge_cells(f"A{row}:G{row}") ws[f"A{row}"] = "📊 EOTF 曲线数据" ws[f"A{row}"].font = section_font ws[f"A{row}"].fill = section_fill ws[f"A{row}"].alignment = section_alignment ws.row_dimensions[row].height = 25 row += 1 # ✅ EOTF 统计信息(类似 Gamma 统计) valid_eotf = [] if eotf_list: for item in eotf_list: if ( isinstance(item, (list, tuple)) and len(item) >= 4 ): eotf_val = item[3] if 0.5 < eotf_val < 5.0: valid_eotf.append(eotf_val) if valid_eotf: avg_eotf = sum(valid_eotf) / len(valid_eotf) max_eotf = max(valid_eotf) min_eotf = min(valid_eotf) ws[f"A{row}"] = "平均 EOTF" ws[f"B{row}"] = f"{avg_eotf:.3f}" ws[f"C{row}"] = "最大 EOTF" ws[f"D{row}"] = f"{max_eotf:.3f}" ws[f"E{row}"] = "最小 EOTF" ws[f"F{row}"] = f"{min_eotf:.3f}" for col in ["A", "B", "C", "D", "E", "F"]: ws[f"{col}{row}"].font = ( label_font if col in ["A", "C", "E"] else data_font ) ws[f"{col}{row}"].border = thin_border row += 1 # ✅ EOTF 数据表格(与 Gamma 表格完全一致) headers = [ "灰阶 (%)", "x 坐标", "y 坐标", "实测亮度\n(cd/m²)", "归一化亮度\n(L_bar)", "EOTF 值", "", ] for col_idx, header in enumerate(headers, start=1): cell = ws.cell(row=row, column=col_idx) cell.value = header cell.font = header_font cell.fill = header_fill cell.alignment = header_alignment cell.border = thin_border row += 1 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 eotf_val = None if ( i < len(eotf_list) and isinstance(eotf_list[i], (list, tuple)) and len(eotf_list[i]) >= 4 ): eotf_val = eotf_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 eotf_val is not None and 0.5 < eotf_val < 5.0: ws[f"F{row}"] = eotf_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" for col in ["A", "B", "C", "D", "E", "F"]: ws[f"{col}{row}"].font = data_font ws[f"{col}{row}"].alignment = data_alignment ws[f"{col}{row}"].border = thin_border row += 1 row += 1 self.log_gui.log(" ✓ 添加 EOTF 数据") # ========== 3. 色度一致性数据 ========== if "cct" in selected_items: gray_data = self.results.get_intermediate_data("shared", "gray") if not gray_data: gray_data = self.results.get_intermediate_data( "cct", "gray" ) if gray_data and len(gray_data) > 1: gray_data_no_black = gray_data[:-1] ws.merge_cells(f"A{row}:G{row}") ws[f"A{row}"] = "🌈 色度一致性数据" ws[f"A{row}"].font = section_font ws[f"A{row}"].fill = section_fill ws[f"A{row}"].alignment = section_alignment ws.row_dimensions[row].height = 25 row += 1 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}" for col in ["A", "B", "C", "D"]: ws[f"{col}{row}"].font = ( label_font if col in ["A", "C"] else data_font ) ws[f"{col}{row}"].border = thin_border row += 1 headers = [ "灰阶 (%)", "x 坐标", "y 坐标", "亮度 (cd/m²)", "", "", "", ] for col_idx, header in enumerate(headers, start=1): cell = ws.cell(row=row, column=col_idx) cell.value = header cell.font = header_font cell.fill = header_fill cell.alignment = header_alignment cell.border = thin_border row += 1 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" for col in ["A", "B", "C", "D"]: ws[f"{col}{row}"].font = data_font ws[f"{col}{row}"].alignment = data_alignment ws[f"{col}{row}"].border = thin_border row += 1 row += 1 self.log_gui.log(" ✓ 添加色度一致性数据") # ========== 4. 对比度数据 ========== if "contrast" in selected_items: contrast_final_result = None if "contrast" in self.results.test_items: contrast_final_result = self.results.test_items[ "contrast" ].final_result if contrast_final_result: ws.merge_cells(f"A{row}:G{row}") ws[f"A{row}"] = "⚫⚪ 对比度测试数据" ws[f"A{row}"].font = section_font ws[f"A{row}"].fill = section_fill ws[f"A{row}"].alignment = section_alignment ws.row_dimensions[row].height = 25 row += 1 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 = label_font ws[f"B{row}"].font = data_font ws[f"A{row}"].border = thin_border ws[f"B{row}"].border = thin_border row += 1 row += 1 self.log_gui.log(" ✓ 添加对比度数据") # ========== 5. 色准数据(HDR 特有)========== if "accuracy" in selected_items: accuracy_final_result = None if "accuracy" in self.results.test_items: accuracy_final_result = self.results.test_items[ "accuracy" ].final_result if accuracy_final_result: ws.merge_cells(f"A{row}:G{row}") ws[f"A{row}"] = "🎯 色准测试数据" ws[f"A{row}"].font = section_font ws[f"A{row}"].fill = section_fill ws[f"A{row}"].alignment = section_alignment ws.row_dimensions[row].height = 25 row += 1 # 色准统计信息 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}" for col in ["A", "B", "C", "D", "E", "F"]: ws[f"{col}{row}"].font = ( label_font if col in ["A", "C", "E"] else data_font ) ws[f"{col}{row}"].border = thin_border 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} 个" for col in ["A", "B", "C", "D", "E", "F"]: ws[f"{col}{row}"].font = ( label_font if col in ["A", "C", "E"] else data_font ) ws[f"{col}{row}"].border = thin_border row += 1 # ========== 色准详细数据表格(带 xy 坐标和亮度)========== color_patches = accuracy_final_result.get( "color_patches", [] ) delta_e_values = accuracy_final_result.get( "delta_e_values", [] ) # ✅ 获取原始测量数据(包含 xy 和亮度) color_measurements = accuracy_final_result.get( "color_measurements", [] ) if color_patches and delta_e_values: # 表头 headers = [ "序号", "颜色名称", "x 坐标", "y 坐标", "亮度 (cd/m²)", "ΔE 2000", "等级", ] for col_idx, header in enumerate(headers, start=1): cell = ws.cell(row=row, column=col_idx) cell.value = header cell.font = header_font cell.fill = header_fill cell.alignment = header_alignment cell.border = thin_border row += 1 # 数据行 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, y, 亮度) x_val = "N/A" y_val = "N/A" lv_val = "N/A" if color_measurements and idx - 1 < len( color_measurements ): measurement = color_measurements[idx - 1] if len(measurement) >= 3: x_val = measurement[0] y_val = measurement[1] lv_val = measurement[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" for col in ["A", "B", "C", "D", "E", "F", "G"]: ws[f"{col}{row}"].font = data_font ws[f"{col}{row}"].alignment = data_alignment ws[f"{col}{row}"].border = thin_border row += 1 row += 1 self.log_gui.log(" ✓ 添加色准数据(含 xy 坐标和亮度)") # ========== 调整列宽 ========== for col in ["A", "B", "C", "D", "E", "F", "G"]: ws.column_dimensions[col].width = 18 # ========== 保存 Excel ========== excel_path = os.path.join(result_dir, "测试数据.xlsx") wb.save(excel_path) self.log_gui.log(f"✓ 已保存: 测试数据.xlsx") self.log_gui.log("=" * 60) except ImportError: self.log_gui.log("⚠️ 未安装 openpyxl 库,跳过 Excel 导出") except Exception as e: self.log_gui.log(f"⚠️ Excel 导出失败: {str(e)}") import traceback self.log_gui.log(traceback.format_exc()) # ========== ✅ 统一的成功提示(在所有 Excel 代码之后)========== self.log_gui.log(f"=" * 50) self.log_gui.log(f"✅ 测试结果已保存到目录: {result_dir}") self.log_gui.log(f"=" * 50) messagebox.showinfo("成功", f"测试结果已保存到目录:\n{result_dir}") except Exception as e: self.log_gui.log(f"❌ 保存测试结果失败: {str(e)}") import traceback self.log_gui.log(traceback.format_exc()) messagebox.showerror("错误", f"保存测试结果失败: {str(e)}") new_pq_results = _run_new_pq_results run_test = _run_run_test run_screen_module_test = _run_run_screen_module_test run_custom_sdr_test = _run_run_custom_sdr_test run_sdr_movie_test = _run_run_sdr_movie_test run_hdr_movie_test = _run_run_hdr_movie_test send_fix_pattern = _run_send_fix_pattern test_custom_sdr = _run_test_custom_sdr test_gamut = _run_test_gamut test_gamma = _run_test_gamma test_eotf = _run_test_eotf test_cct = _run_test_cct test_contrast = _run_test_contrast calculate_delta_e_2000 = staticmethod(_calc_delta_e_2000) test_color_accuracy = _run_test_color_accuracy get_accuracy_color_standards = staticmethod(_get_accuracy_color_standards) calculate_gamut_coverage = staticmethod(_calc_gamut_coverage) calculate_gamma = staticmethod(_calc_gamma) calculate_color_accuracy = staticmethod(_calc_color_accuracy) plot_gamut = _plot_gamut plot_gamma = _plot_gamma plot_eotf = _plot_eotf calculate_pq_curve = staticmethod(_calc_pq_curve) plot_cct = _plot_cct plot_contrast = _plot_contrast plot_accuracy = _plot_accuracy on_test_completed = _run_on_test_completed on_custom_template_test_completed = _run_on_custom_template_test_completed get_current_test_result = _run_get_current_test_result on_test_error = _run_on_test_error def get_test_type_name(self, test_type): """获取测试类型的显示名称""" if test_type == "screen_module": return "屏模组性能测试" elif test_type == "sdr_movie": return "SDR Movie测试" elif test_type == "hdr_movie": return "HDR Movie测试" return test_type def get_selected_test_items(self): """获取当前选中的测试项""" selected_items = [] for var_name, var in self.test_vars.items(): if var.get(): selected_items.append(var_name.split("_")[-1]) return selected_items def update_config(self, event=None): """更新配置""" try: self.config.set_device_config( self.ca_com_var.get(), self.ucd_list_var.get(), self.ca_channel_var.get(), ) # 保存当前选中的测试项到配置 self.config.set_current_test_items(self.get_selected_test_items()) # 待修改为三种测试类型的timing值 self.config.set_current_timing(self.screen_module_timing_var.get()) # 自动保存配置到文件 self.save_pq_config() except Exception as e: self.log_gui.log(f"更新配置失败: {str(e)}") def update_config_and_tabs(self): """更新配置并同步Tab状态""" self.update_config() self.update_chart_tabs_state() # 根据当前测试类型保存对应参数 current_test_type = self.config.current_test_type selected_items = self.get_selected_test_items() if current_test_type == "screen_module": if "cct" in selected_items: self.save_cct_params() elif current_test_type == "sdr_movie": if "cct" in selected_items: self.save_sdr_cct_params() elif current_test_type == "hdr_movie": if "cct" in selected_items: if hasattr(self, "save_hdr_cct_params"): self.save_hdr_cct_params() # 控制参数框的显示 self.toggle_cct_params_frame() def toggle_cct_params_frame(self): """根据测试类型和测试项的选中状态显示对应参数框""" selected_items = self.get_selected_test_items() current_test_type = self.config.current_test_type # 默认隐藏所有参数框 self.cct_params_frame.pack_forget() self.sdr_cct_params_frame.pack_forget() if hasattr(self, "hdr_cct_params_frame"): self.hdr_cct_params_frame.pack_forget() # 根据测试类型和选中项显示对应参数框 if current_test_type == "screen_module": if "cct" in selected_items: self.cct_params_frame.pack(fill=tk.X, padx=5, pady=5) if hasattr(self, "log_gui"): self.log_gui.log("✓ 显示屏模组色度参数设置") elif current_test_type == "sdr_movie": if "cct" in selected_items: self.sdr_cct_params_frame.pack(fill=tk.X, padx=5, pady=5) if hasattr(self, "log_gui"): self.log_gui.log("✓ 显示 SDR 色度参数设置") elif current_test_type == "hdr_movie": if "cct" in selected_items: if hasattr(self, "hdr_cct_params_frame"): self.hdr_cct_params_frame.pack(fill=tk.X, padx=5, pady=5) if hasattr(self, "log_gui"): self.log_gui.log("✓ 显示 HDR 色度参数设置") else: if hasattr(self, "log_gui"): self.log_gui.log("⚠️ HDR 色度参数框尚未创建") def on_screen_module_timing_changed(self, event=None): """屏模组信号格式改变时的回调""" try: selected_timing = self.screen_module_timing_var.get() # 记录日志 self.log_gui.log(f"屏模组信号格式已更改为: {selected_timing}") # 解析分辨率和刷新率 import re match = re.search(r"(\d+)x(\d+)\s*@\s*(\d+)", selected_timing) if match: width = int(match.group(1)) height = int(match.group(2)) refresh_rate = int(match.group(3)) self.log_gui.log(f" ├─ 分辨率: {width}x{height}") self.log_gui.log(f" └─ 刷新率: {refresh_rate}Hz") # 根据分辨率给出提示 if width >= 3840: # 4K及以上 self.log_gui.log(" ℹ️ 检测到4K分辨率") if refresh_rate >= 120: self.log_gui.log(" ℹ️ 检测到高刷新率") # 更新配置 self.config.set_current_timing(selected_timing) # 如果正在测试,提示用户 if self.testing: self.log_gui.log("⚠️ 警告: 测试进行中,信号格式更改将在下次测试时生效") # 保存配置 self.save_pq_config() except Exception as e: self.log_gui.log(f"❌ 屏模组信号格式更改失败: {str(e)}") load_pq_config = _cfg_load_pq_config save_pq_config = _cfg_save_pq_config def on_closing(self): """窗口关闭时的处理""" try: # ✅ 检查是否清理了配置 if not self.config_cleared: # 保存配置 self.save_pq_config() else: print("配置已清理,不再保存") # 断开设备连接 if self.ucd.status: self.ucd.close() if self.ca is not None: self.ca.close() # 关闭窗口 self.root.destroy() except Exception as e: print(f"关闭窗口时出错: {str(e)}") self.root.destroy() def on_screen_gamut_ref_changed(self, event=None): """屏模组色域参考标准改变时的回调""" try: new_ref = self.screen_gamut_ref_var.get() self.log_gui.log(f"✓ 屏模组色域参考标准已更改为: {new_ref}") # 保存到配置 if "screen_module" not in self.config.current_test_types: self.config.current_test_types["screen_module"] = {} self.config.current_test_types["screen_module"]["gamut_reference"] = new_ref self.save_pq_config() except Exception as e: self.log_gui.log(f"保存屏模组色域参考标准失败: {str(e)}") def on_sdr_gamut_ref_changed(self, event=None): """SDR 色域参考标准改变时的回调""" try: new_ref = self.sdr_gamut_ref_var.get() self.log_gui.log(f"✓ SDR 色域参考标准已更改为: {new_ref}") # 保存到配置 if "sdr_movie" not in self.config.current_test_types: self.config.current_test_types["sdr_movie"] = {} self.config.current_test_types["sdr_movie"]["gamut_reference"] = new_ref self.save_pq_config() except Exception as e: self.log_gui.log(f"保存 SDR 色域参考标准失败: {str(e)}") def on_hdr_gamut_ref_changed(self, event=None): """HDR 色域参考标准改变时的回调""" try: new_ref = self.hdr_gamut_ref_var.get() self.log_gui.log(f"✓ HDR 色域参考标准已更改为: {new_ref}") # 保存到配置 if "hdr_movie" not in self.config.current_test_types: self.config.current_test_types["hdr_movie"] = {} self.config.current_test_types["hdr_movie"]["gamut_reference"] = new_ref self.save_pq_config() except Exception as e: self.log_gui.log(f"保存 HDR 色域参考标准失败: {str(e)}") toggle_screen_debug_panel = _side_toggle_screen_debug_panel toggle_sdr_debug_panel = _side_toggle_sdr_debug_panel toggle_hdr_debug_panel = _side_toggle_hdr_debug_panel clear_config_file = _cfg_clear_config_file start_local_dimming_test = _ld_start_local_dimming_test update_ld_results = _ld_update_ld_results stop_local_dimming_test = _ld_stop_local_dimming_test send_ld_window = _ld_send_ld_window measure_ld_luminance = _ld_measure_ld_luminance clear_ld_records = _ld_clear_ld_records save_local_dimming_results = _ld_save_local_dimming_results def main(): try: # root = tk.Tk() root = ttk.Window(themename="yeti") app = PQAutomationApp(root) root.mainloop() except Exception as e: print("程序发生错误:", e) traceback.print_exc() finally: pass if __name__ == "__main__": main()