Files
pqAutomationApp/pqAutomationApp.py

5129 lines
213 KiB
Python
Raw Normal View History

2026-04-16 16:51:05 +08:00
import ttkbootstrap as ttk
import tkinter as tk
from tkinter import messagebox, filedialog
import sys
import threading
import time
import os
import datetime
2026-04-17 11:17:29 +08:00
import colour
2026-04-16 16:51:05 +08:00
import json
import traceback
import numpy as np
2026-04-17 11:17:29 +08:00
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
2026-04-16 16:51:05 +08:00
import algorithm.pq_algorithm as pq_algorithm
2026-04-17 11:17:29 +08:00
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
2026-04-16 16:51:05 +08:00
from app_version import APP_NAME, APP_VERSION, get_app_title
2026-04-20 11:48:38 +08:00
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
2026-04-16 16:51:05 +08:00
from PIL import Image, ImageTk
2026-04-20 11:48:38 +08:00
from app.views.collapsing_frame import CollapsingFrame
from app.views.pq_log_gui import PQLogGUI
2026-04-17 11:17:29 +08:00
from colormath.color_objects import xyYColor, LabColor
from colormath.color_conversions import convert_color
from colormath.color_diff import delta_e_cie2000
2026-04-20 11:48:38 +08:00
from app.views.pq_debug_panel import PQDebugPanel
2026-04-16 16:51:05 +08:00
2026-04-20 09:41:24 +08:00
# 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
2026-04-20 10:00:44 +08:00
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
2026-04-20 10:16:31 +08:00
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,
)
2026-04-20 10:54:47 +08:00
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,
)
2026-04-20 09:41:24 +08:00
2026-04-17 11:17:29 +08:00
plt.rcParams["font.family"] = ["sans-serif"]
plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"]
2026-04-16 16:51:05 +08:00
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(
2026-04-20 11:48:38 +08:00
side=tk.BOTTOM, fill=tk.BOTH, expand=True, padx=0, pady=5
2026-04-16 16:51:05 +08:00
)
2026-04-20 11:48:38 +08:00
# 创建日志显示区域
self.create_log_panel()
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
# 创建 Local Dimming 面板
self.create_local_dimming_panel()
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
# 创建测试类型选择区域
self.create_test_type_frame()
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
# 创建操作按钮区域
self.create_operation_frame()
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
# 创建结果图表区域
self.create_result_chart_frame()
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
# 创建客户模板结果显示区域(黑底表格)
self.create_custom_template_result_panel()
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
# 在所有控件创建完成后,统一初始化测试类型
self.root.after(100, self.initialize_default_test_type)
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
# 状态栏
self.status_var = tk.StringVar(value="就绪")
self.status_bar = ttk.Label(
root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W
2026-04-16 16:51:05 +08:00
)
2026-04-20 11:48:38 +08:00
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
2026-04-16 16:51:05 +08:00
2026-04-20 15:34:45 +08:00
def _dispatch_ui(self, fn, *args, **kwargs):
2026-04-20 16:57:30 +08:00
"""把 ``fn(*args, **kwargs)`` 调度到 Tk 主线程执行。
2026-04-20 15:34:45 +08:00
2026-04-20 16:57:30 +08:00
统一替代散落各处的 ``self.root.after(0, lambda: ...)`` 写法
2026-04-20 15:34:45 +08:00
- 自动捕获异常并记录日志避免工作线程静默丢失 UI 更新失败
2026-04-20 16:57:30 +08:00
- 参数用位置/关键字传入绕开 ``lambda`` 闭包捕获变量的常见坑
2026-04-20 15:34:45 +08:00
- 允许在 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
2026-04-20 11:48:38 +08:00
get_config_path = _cfg_get_config_path
def initialize_default_test_type(self):
"""初始化默认测试类型(在所有控件创建完成后调用)"""
try:
# 强制切换到屏模组
self.change_test_type("screen_module")
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
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)}")
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
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
2026-04-20 16:57:30 +08:00
def create_floating_config_panel(self):
"""创建右上角悬浮配置框"""
cf = CollapsingFrame(self.control_frame_top)
cf.pack(fill="both")
# 创建悬浮框主容器
self.config_panel_frame = ttk.Frame(cf)
cf.add(self.config_panel_frame, title="配置项")
# 创建一个统一的frame来替代选项卡控件
self.config_content_frame = ttk.Frame(self.config_panel_frame)
self.config_content_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 创建一个横向排列的Frame
config_row_frame = ttk.Frame(self.config_content_frame)
config_row_frame.pack(fill=tk.X, expand=False, padx=5, pady=5)
# 创建连接内容区域
self.connection_frame = ttk.LabelFrame(config_row_frame, text="设备连接")
self.connection_frame.pack(
side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5
)
# 创建测试项目区域
self.test_items_frame = ttk.LabelFrame(config_row_frame, text="测试项目")
self.test_items_frame.pack(
side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5
)
# 创建信号格式区域
self.signal_format_frame = ttk.LabelFrame(config_row_frame, text="信号格式")
self.signal_format_frame.pack(
side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5
)
# 创建连接内容
self.create_connection_content()
# 创建测试项目内容
self.create_test_items_content()
# 创建信号格式内容
self.create_signal_format_content()
self.config_panel_frame.grid_remove()
self.config_panel_frame.btn.configure(image="closed")
def create_test_items_content(self):
"""创建测试项目选项卡内容"""
# 创建测试项目字典,用于管理不同测试类型的选项
self.test_items = {
"screen_module": {
"frame": ttk.Frame(self.test_items_frame),
"items": [
("色域", "gamut"),
("Gamma", "gamma"),
("色度", "cct"),
("对比度", "contrast"),
],
},
"sdr_movie": {
"frame": ttk.Frame(self.test_items_frame),
"items": [
("色域", "gamut"),
("Gamma", "gamma"),
("色度", "cct"),
("对比度", "contrast"),
("色准", "accuracy"),
],
},
"hdr_movie": {
"frame": ttk.Frame(self.test_items_frame),
"items": [
("色域", "gamut"),
("EOTF", "eotf"),
("色度", "cct"),
("对比度", "contrast"),
("色准", "accuracy"),
],
},
}
# 根据当前测试类型创建复选框
self.test_vars = {}
self.update_test_items()
# 创建色度参数设置框架
self.create_cct_params_frame()
def create_cct_params_frame(self):
"""创建色度参数设置区域 - 屏模组、SDR、HDR 独立(✅ 增加色域参考标准选择 + 单步调试按钮)"""
# ==================== 屏模组色度参数 Frame ====================
self.cct_params_frame = ttk.LabelFrame(
self.test_items_frame, text="色度参数设置(屏模组)"
)
# 默认值
screen_default_cct_params = self.config.get_default_cct_params("screen_module")
# 从配置读取屏模组参数
saved_params = self.config.current_test_types.get("screen_module", {}).get(
"cct_params", screen_default_cct_params.copy()
)
# 色域参考标准
saved_gamut_ref = self.config.current_test_types.get("screen_module", {}).get(
"gamut_reference", self.config.get_default_gamut_reference("screen_module")
)
# 创建屏模组变量
self.cct_x_ideal_var = tk.StringVar(
value=str(saved_params.get("x_ideal", 0.3127))
)
self.cct_x_tolerance_var = tk.StringVar(
value=str(saved_params.get("x_tolerance", 0.003))
)
self.cct_y_ideal_var = tk.StringVar(
value=str(saved_params.get("y_ideal", 0.3290))
)
self.cct_y_tolerance_var = tk.StringVar(
value=str(saved_params.get("y_tolerance", 0.003))
)
self.screen_gamut_ref_var = tk.StringVar(value=saved_gamut_ref)
# 创建屏模组输入框(左侧:色度参数)
params = [
("x-ideal:", self.cct_x_ideal_var, "x_ideal"),
("x-tolerance:", self.cct_x_tolerance_var, "x_tolerance"),
("y-ideal:", self.cct_y_ideal_var, "y_ideal"),
("y-tolerance:", self.cct_y_tolerance_var, "y_tolerance"),
]
for i, (label_text, var, key) in enumerate(params):
ttk.Label(self.cct_params_frame, text=label_text).grid(
row=i, column=0, sticky=tk.W, padx=5, pady=3
)
entry = ttk.Entry(self.cct_params_frame, textvariable=var, width=15)
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
# 绑定失去焦点事件
default_val = screen_default_cct_params[key]
entry.bind(
"<FocusOut>",
lambda e, v=var, d=default_val: self.on_cct_param_focus_out(v, d),
)
# 色域参考标准选择(右侧第一行)
ttk.Label(self.cct_params_frame, text="色域参考标准:").grid(
row=0, column=2, sticky=tk.W, padx=(20, 5), pady=3
)
screen_gamut_combo = ttk.Combobox(
self.cct_params_frame,
textvariable=self.screen_gamut_ref_var,
values=["BT.2020", "BT.709", "DCI-P3"],
state="disabled",
width=12,
)
screen_gamut_combo.grid(row=0, column=3, sticky=tk.W, padx=5, pady=3)
screen_gamut_combo.bind(
"<<ComboboxSelected>>", self.on_screen_gamut_ref_changed
)
self.screen_gamut_combo = screen_gamut_combo
# ==================== ✅ 单步调试按钮(右侧第二行)====================
ttk.Label(self.cct_params_frame, text="单步调试:").grid(
row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3
)
self.screen_debug_btn = ttk.Button(
self.cct_params_frame,
text="打开调试面板",
command=self.toggle_screen_debug_panel,
bootstyle="info-outline",
state=tk.DISABLED, # 初始禁用
width=15,
)
self.screen_debug_btn.grid(row=1, column=3, sticky=tk.W, padx=5, pady=3)
# 重新计算按钮(屏模组)
self.recalc_cct_btn = ttk.Button(
self.cct_params_frame,
text="应用新参数并重绘",
command=self.recalculate_cct,
bootstyle="success",
)
self.recalc_cct_btn.grid(
row=4, column=0, columnspan=2, pady=10, padx=5, sticky="ew"
)
self.recalc_cct_btn.grid_remove()
# 色域重新计算按钮
self.recalc_gamut_btn = ttk.Button(
self.cct_params_frame,
text="应用色域参考并重绘",
command=self.recalculate_gamut,
bootstyle="warning",
)
self.recalc_gamut_btn.grid(
row=4, column=2, columnspan=2, pady=10, padx=5, sticky="ew"
)
self.recalc_gamut_btn.grid_remove()
# 提示文字
ttk.Label(
self.cct_params_frame,
text="提示: 清空输入框将恢复默认值",
font=("SimHei", 8),
foreground="gray",
).grid(row=5, column=0, columnspan=4, sticky=tk.W, padx=5, pady=5)
# ==================== SDR 色度参数 Frame ====================
self.sdr_cct_params_frame = ttk.LabelFrame(
self.test_items_frame, text="色度参数设置SDR"
)
# SDR 默认值
sdr_default_cct_params = self.config.get_default_cct_params("sdr_movie")
# 从配置读取 SDR 参数
sdr_saved_params = self.config.current_test_types.get("sdr_movie", {}).get(
"cct_params", sdr_default_cct_params.copy()
)
# 色域参考标准
sdr_saved_gamut_ref = self.config.current_test_types.get("sdr_movie", {}).get(
"gamut_reference", self.config.get_default_gamut_reference("sdr_movie")
)
# 创建 SDR 变量
self.sdr_cct_x_ideal_var = tk.StringVar(
value=str(sdr_saved_params.get("x_ideal", 0.3127))
)
self.sdr_cct_x_tolerance_var = tk.StringVar(
value=str(sdr_saved_params.get("x_tolerance", 0.003))
)
self.sdr_cct_y_ideal_var = tk.StringVar(
value=str(sdr_saved_params.get("y_ideal", 0.3290))
)
self.sdr_cct_y_tolerance_var = tk.StringVar(
value=str(sdr_saved_params.get("y_tolerance", 0.003))
)
self.sdr_gamut_ref_var = tk.StringVar(value=sdr_saved_gamut_ref)
# 创建 SDR 输入框
sdr_params = [
("x-ideal:", self.sdr_cct_x_ideal_var, "x_ideal"),
("x-tolerance:", self.sdr_cct_x_tolerance_var, "x_tolerance"),
("y-ideal:", self.sdr_cct_y_ideal_var, "y_ideal"),
("y-tolerance:", self.sdr_cct_y_tolerance_var, "y_tolerance"),
]
for i, (label_text, var, key) in enumerate(sdr_params):
ttk.Label(self.sdr_cct_params_frame, text=label_text).grid(
row=i, column=0, sticky=tk.W, padx=5, pady=3
)
entry = ttk.Entry(self.sdr_cct_params_frame, textvariable=var, width=15)
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
# 绑定失去焦点事件
default_val = sdr_default_cct_params[key]
entry.bind(
"<FocusOut>",
lambda e, v=var, d=default_val: self.on_sdr_cct_param_focus_out(v, d),
)
# 色域参考标准选择(右侧第一行)
ttk.Label(self.sdr_cct_params_frame, text="色域参考标准:").grid(
row=0, column=2, sticky=tk.W, padx=(20, 5), pady=3
)
sdr_gamut_combo = ttk.Combobox(
self.sdr_cct_params_frame,
textvariable=self.sdr_gamut_ref_var,
values=["BT.2020", "BT.709", "DCI-P3"],
state="disabled",
width=12,
)
sdr_gamut_combo.grid(row=0, column=3, sticky=tk.W, padx=5, pady=3)
sdr_gamut_combo.bind("<<ComboboxSelected>>", self.on_sdr_gamut_ref_changed)
self.sdr_gamut_combo = sdr_gamut_combo
# ==================== ✅ SDR 单步调试按钮(右侧第二行)====================
ttk.Label(self.sdr_cct_params_frame, text="单步调试:").grid(
row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3
)
self.sdr_debug_btn = ttk.Button(
self.sdr_cct_params_frame,
text="打开调试面板",
command=self.toggle_sdr_debug_panel,
bootstyle="info-outline",
state=tk.DISABLED, # 初始禁用
width=15,
)
self.sdr_debug_btn.grid(row=1, column=3, sticky=tk.W, padx=5, pady=3)
# 重新计算按钮SDR
self.sdr_recalc_cct_btn = ttk.Button(
self.sdr_cct_params_frame,
text="应用新参数并重绘",
command=self.recalculate_cct,
bootstyle="success",
)
self.sdr_recalc_cct_btn.grid(
row=4, column=0, columnspan=2, pady=10, padx=5, sticky="ew"
)
self.sdr_recalc_cct_btn.grid_remove()
# 色域重新计算按钮SDR
self.sdr_recalc_gamut_btn = ttk.Button(
self.sdr_cct_params_frame,
text="应用色域参考并重绘",
command=self.recalculate_gamut,
bootstyle="warning",
)
self.sdr_recalc_gamut_btn.grid(
row=4, column=2, columnspan=2, pady=10, padx=5, sticky="ew"
)
self.sdr_recalc_gamut_btn.grid_remove()
# 提示文字
ttk.Label(
self.sdr_cct_params_frame,
text="提示: 清空输入框将恢复默认值",
font=("SimHei", 8),
foreground="gray",
).grid(row=5, column=0, columnspan=4, sticky=tk.W, padx=5, pady=5)
# ==================== HDR 色度参数 Frame ====================
self.hdr_cct_params_frame = ttk.LabelFrame(
self.test_items_frame, text="色度参数设置HDR"
)
# HDR 默认值
hdr_default_cct_params = self.config.get_default_cct_params("hdr_movie")
# 从配置读取 HDR 参数
hdr_saved_params = self.config.current_test_types.get("hdr_movie", {}).get(
"cct_params", hdr_default_cct_params.copy()
)
# 色域参考标准
hdr_saved_gamut_ref = self.config.current_test_types.get("hdr_movie", {}).get(
"gamut_reference", self.config.get_default_gamut_reference("hdr_movie")
)
# 创建 HDR 变量
self.hdr_cct_x_ideal_var = tk.StringVar(
value=str(hdr_saved_params.get("x_ideal", 0.3127))
)
self.hdr_cct_x_tolerance_var = tk.StringVar(
value=str(hdr_saved_params.get("x_tolerance", 0.003))
)
self.hdr_cct_y_ideal_var = tk.StringVar(
value=str(hdr_saved_params.get("y_ideal", 0.3290))
)
self.hdr_cct_y_tolerance_var = tk.StringVar(
value=str(hdr_saved_params.get("y_tolerance", 0.003))
)
self.hdr_gamut_ref_var = tk.StringVar(value=hdr_saved_gamut_ref)
# 创建 HDR 输入框
hdr_params = [
("x-ideal:", self.hdr_cct_x_ideal_var, "x_ideal"),
("x-tolerance:", self.hdr_cct_x_tolerance_var, "x_tolerance"),
("y-ideal:", self.hdr_cct_y_ideal_var, "y_ideal"),
("y-tolerance:", self.hdr_cct_y_tolerance_var, "y_tolerance"),
]
for i, (label_text, var, key) in enumerate(hdr_params):
ttk.Label(self.hdr_cct_params_frame, text=label_text).grid(
row=i, column=0, sticky=tk.W, padx=5, pady=3
)
entry = ttk.Entry(self.hdr_cct_params_frame, textvariable=var, width=15)
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
# 绑定失去焦点事件
default_val = hdr_default_cct_params[key]
entry.bind(
"<FocusOut>",
lambda e, v=var, d=default_val: self.on_hdr_cct_param_focus_out(v, d),
)
# 色域参考标准选择(右侧第一行)
ttk.Label(self.hdr_cct_params_frame, text="色域参考标准:").grid(
row=0, column=2, sticky=tk.W, padx=(20, 5), pady=3
)
hdr_gamut_combo = ttk.Combobox(
self.hdr_cct_params_frame,
textvariable=self.hdr_gamut_ref_var,
values=["BT.2020", "BT.709", "DCI-P3"],
state="disabled",
width=12,
)
hdr_gamut_combo.grid(row=0, column=3, sticky=tk.W, padx=5, pady=3)
hdr_gamut_combo.bind("<<ComboboxSelected>>", self.on_hdr_gamut_ref_changed)
self.hdr_gamut_combo = hdr_gamut_combo
# ==================== ✅ HDR 单步调试按钮(右侧第二行)====================
ttk.Label(self.hdr_cct_params_frame, text="单步调试:").grid(
row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3
)
self.hdr_debug_btn = ttk.Button(
self.hdr_cct_params_frame,
text="打开调试面板",
command=self.toggle_hdr_debug_panel,
bootstyle="info-outline",
state=tk.DISABLED, # 初始禁用
width=15,
)
self.hdr_debug_btn.grid(row=1, column=3, sticky=tk.W, padx=5, pady=3)
# 重新计算按钮HDR
self.hdr_recalc_cct_btn = ttk.Button(
self.hdr_cct_params_frame,
text="应用新参数并重绘",
command=self.recalculate_cct,
bootstyle="success",
)
self.hdr_recalc_cct_btn.grid(
row=4, column=0, columnspan=2, pady=10, padx=5, sticky="ew"
)
self.hdr_recalc_cct_btn.grid_remove()
# 色域重新计算按钮HDR
self.hdr_recalc_gamut_btn = ttk.Button(
self.hdr_cct_params_frame,
text="应用色域参考并重绘",
command=self.recalculate_gamut,
bootstyle="warning",
)
self.hdr_recalc_gamut_btn.grid(
row=4, column=2, columnspan=2, pady=10, padx=5, sticky="ew"
)
self.hdr_recalc_gamut_btn.grid_remove()
# 提示文字
ttk.Label(
self.hdr_cct_params_frame,
text="提示: 清空输入框将恢复默认值",
font=("SimHei", 8),
foreground="gray",
).grid(row=5, column=0, columnspan=4, sticky=tk.W, padx=5, pady=5)
def _get_cct_var_dict(self, test_type):
"""按测试类型返回 CCT 变量映射。"""
if test_type == "sdr_movie":
return {
"x_ideal": self.sdr_cct_x_ideal_var,
"x_tolerance": self.sdr_cct_x_tolerance_var,
"y_ideal": self.sdr_cct_y_ideal_var,
"y_tolerance": self.sdr_cct_y_tolerance_var,
}
if test_type == "hdr_movie":
return {
"x_ideal": self.hdr_cct_x_ideal_var,
"x_tolerance": self.hdr_cct_x_tolerance_var,
"y_ideal": self.hdr_cct_y_ideal_var,
"y_tolerance": self.hdr_cct_y_tolerance_var,
}
return {
"x_ideal": self.cct_x_ideal_var,
"x_tolerance": self.cct_x_tolerance_var,
"y_ideal": self.cct_y_ideal_var,
"y_tolerance": self.cct_y_tolerance_var,
}
def _parse_cct_float(self, var, default):
"""读取并解析 CCT 输入值,失败时回落默认值。"""
try:
value = var.get().strip()
if value == "":
return default
return float(value)
except Exception:
return default
def _save_cct_params_for(self, test_type):
"""保存指定测试类型的 CCT 参数。"""
try:
default_params = self.config.get_default_cct_params(test_type)
var_dict = self._get_cct_var_dict(test_type)
cct_params = {
key: self._parse_cct_float(var_dict[key], default_params[key])
for key in default_params
}
if test_type not in self.config.current_test_types:
self.config.current_test_types[test_type] = {}
self.config.current_test_types[test_type]["cct_params"] = cct_params
self.save_pq_config()
except Exception:
pass
def _handle_cct_focus_out(self, var, default_value, save_func, label):
"""统一处理 CCT 参数失焦校验并保存。"""
try:
value = var.get().strip()
if value == "":
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"{label} 参数为空,恢复默认值: {default_value}")
else:
try:
float_val = float(value)
if float_val < 0 or float_val > 1:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(
f"⚠️ {label} 参数超出范围,恢复默认值: {default_value}"
)
except ValueError:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(
f"⚠️ {label} 参数无效,恢复默认值: {default_value}"
)
save_func()
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"处理 {label} 参数失败: {str(e)}")
def on_sdr_cct_param_focus_out(self, var, default_value):
"""SDR 色度参数失去焦点时的处理。"""
self._handle_cct_focus_out(var, default_value, self.save_sdr_cct_params, "SDR")
def save_sdr_cct_params(self):
"""保存 SDR 色度参数。"""
self._save_cct_params_for("sdr_movie")
def on_hdr_cct_param_focus_out(self, var, default_value):
"""HDR 色度参数失去焦点时的处理。"""
self._handle_cct_focus_out(var, default_value, self.save_hdr_cct_params, "HDR")
def save_hdr_cct_params(self):
"""保存 HDR 色度参数。"""
self._save_cct_params_for("hdr_movie")
def recalculate_cct(self):
"""重新计算并绘制色度图"""
try:
# 1. 保存新参数
self.save_cct_params()
self.log_gui.log("✓ 色度参数已更新")
# 2. 收起配置项
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.1)
except:
pass
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
# 3. 跳转到色度图Tab
self.chart_notebook.select(self.cct_chart_frame)
self.root.update_idletasks()
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
# 4. 检查是否有数据
if not hasattr(self, "results") or not self.results:
self.log_gui.log("⚠️ 没有测试数据,无法重新绘制")
messagebox.showwarning("警告", "请先完成测试后再重新计算")
return
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
# 5. 获取保存的灰阶数据
gray_data = self.results.get_intermediate_data("shared", "gray")
if not gray_data:
gray_data = self.results.get_intermediate_data("cct", "gray")
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
if not gray_data or len(gray_data) < 2:
self.log_gui.log("⚠️ 没有可用的灰阶数据")
messagebox.showwarning("警告", "没有找到色度测试数据")
return
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
# 6. 重新计算 CCT
self.log_gui.log("=" * 50)
self.log_gui.log("开始重新计算色度一致性...")
self.log_gui.log("=" * 50)
import algorithm.pq_algorithm as pq_algorithm
cct_values = pq_algorithm.calculate_cct_from_results(gray_data)
# 7. 更新结果
self.results.set_test_item_result("cct", {"cct_values": cct_values})
# 8. 重新绘制色度图
test_type = self.config.current_test_type
self.plot_cct(test_type)
self.log_gui.log("✓ 色度图已重新绘制")
self.log_gui.log("=" * 50)
messagebox.showinfo("成功", "色度图已根据新参数重新绘制!")
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)}")
def recalculate_gamut(self):
"""重新计算并绘制色域图(使用新的参考标准)"""
try:
# 1. 收起配置项
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.1)
except:
pass
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
# 2. 跳转到色域图Tab
self.chart_notebook.select(self.gamut_chart_frame)
self.root.update_idletasks()
# 3. 检查是否有数据
if not hasattr(self, "results") or not self.results:
self.log_gui.log("⚠️ 没有测试数据,无法重新绘制")
messagebox.showwarning("警告", "请先完成测试后再重新计算")
return
# 4. 获取保存的色域数据
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
if not rgb_data or len(rgb_data) < 3:
self.log_gui.log("⚠️ 没有可用的色域数据")
messagebox.showwarning("警告", "没有找到色域测试数据")
return
# 5. 获取当前测试类型
test_type = self.config.current_test_type
# 6. 获取用户选择的参考标准
if test_type == "screen_module":
reference_standard = self.screen_gamut_ref_var.get()
elif test_type == "sdr_movie":
reference_standard = self.sdr_gamut_ref_var.get()
elif test_type == "hdr_movie":
reference_standard = self.hdr_gamut_ref_var.get()
else:
reference_standard = "DCI-P3"
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
self.log_gui.log("=" * 50)
self.log_gui.log(f"开始重新计算色域(参考标准: {reference_standard}...")
self.log_gui.log("=" * 50)
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
# 7. 重新计算 XY 色域覆盖率
xy_points = [[result[0], result[1]] for result in rgb_data]
# 根据参考标准计算 XY 覆盖率
if reference_standard == "BT.2020":
area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_BT2020(
xy_points
)
elif reference_standard == "BT.709":
area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_BT709(
xy_points
)
elif reference_standard == "DCI-P3":
area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_DCIP3(
xy_points
)
else:
area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_DCIP3(
xy_points
)
reference_standard = "DCI-P3"
self.log_gui.log(f"✓ 参考标准: {reference_standard}")
self.log_gui.log(f"✓ XY 色域覆盖率: {coverage_xy:.1f}%")
# ========== ✅✅✅ 8. 重新计算 UV 色域覆盖率 ==========
# 将 XY 坐标转换为 UV 坐标
uv_points = []
for x, y in xy_points:
try:
# XY转UV公式
denom = -2 * x + 12 * y + 3
if abs(denom) < 1e-10:
u, v = 0, 0
else:
u = 4 * x / denom
v = 9 * y / denom
uv_points.append([u, v])
except ZeroDivisionError:
continue
self.log_gui.log(f"✓ 转换后的 UV 点数量: {len(uv_points)}")
# 根据参考标准计算 UV 覆盖率
if reference_standard == "BT.2020":
area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_BT2020_uv(
uv_points
)
elif reference_standard == "BT.709":
area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_BT709_uv(
uv_points
)
elif reference_standard == "DCI-P3":
area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_DCIP3_uv(
uv_points
)
else:
area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_DCIP3_uv(
uv_points
)
self.log_gui.log(f"✓ UV 色域覆盖率: {coverage_uv:.1f}%")
# ========================================================
# 9. ✅ 更新结果(同时保存 XY 和 UV 覆盖率)
self.results.set_test_item_result(
"gamut",
{
"area": area_xy, # ← 兼容旧字段
"coverage": coverage_xy, # ← 兼容旧字段
"area_xy": area_xy, # ← XY 面积
"coverage_xy": coverage_xy, # ← XY 覆盖率
"area_uv": area_uv, # ← UV 面积
"coverage_uv": coverage_uv, # ← UV 覆盖率
"uv_coverage": coverage_uv, # ← 兼容字段Excel 导出用)
"reference": reference_standard,
},
)
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
self.log_gui.log("✓ 测试结果已更新到 results 对象")
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
# 10. 重新绘制色域图
self.plot_gamut(rgb_data, coverage_xy, test_type)
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
self.log_gui.log("✓ 色域图已重新绘制")
self.log_gui.log("=" * 50)
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
messagebox.showinfo(
"成功",
f"色域图已根据新参考标准 {reference_standard} 重新绘制!\n\n"
f"XY 覆盖率: {coverage_xy:.1f}%\n"
f"UV 覆盖率: {coverage_uv:.1f}%",
)
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)}")
def on_cct_param_change(self, var, default_value):
"""色度参数改变时的处理 - 空值恢复默认"""
try:
value = var.get().strip()
if value == "":
# 空值:恢复默认值
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"输入框为空,恢复默认值: {default_value}")
else:
# 验证是否为有效数字
try:
float_val = float(value)
if float_val < 0 or float_val > 1:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(
f"参数超出范围 [0, 1],恢复默认值: {default_value}"
)
except ValueError:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"无效的参数值,恢复默认值: {default_value}")
# 保存配置
self.save_cct_params()
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"处理参数变化失败: {str(e)}")
def on_cct_param_focus_out(self, var, default_value):
"""色度参数失去焦点时的处理 - 空值恢复默认"""
self._handle_cct_focus_out(var, default_value, self.save_cct_params, "屏模组")
def save_cct_params(self):
"""保存色度参数 - 简化版"""
self._save_cct_params_for(self.config.current_test_type)
def reload_cct_params(self):
"""切换测试类型时重新加载色度参数"""
try:
current_type = self.config.current_test_type
saved_params = self.config.current_test_types.get(current_type, {}).get(
"cct_params", None
)
if saved_params is None:
saved_params = self.config.get_default_cct_params(current_type)
# 更新输入框的值
self.cct_x_ideal_var.set(str(saved_params["x_ideal"]))
self.cct_x_tolerance_var.set(str(saved_params["x_tolerance"]))
self.cct_y_ideal_var.set(str(saved_params["y_ideal"]))
self.cct_y_tolerance_var.set(str(saved_params["y_tolerance"]))
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"重新加载色度参数失败: {str(e)}")
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
def update_test_items(self):
"""根据当前测试类型更新测试项目复选框"""
# 先隐藏所有测试项目框架
for config in self.test_items.values():
config["frame"].pack_forget()
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
current_test_type = self.config.current_test_type
self.test_vars = {}
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
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)
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
# 添加测试类型标签
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)
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
# 从配置中读取保存的选择状态
saved_test_items = self.config.current_test_types[current_test_type].get(
"test_items", []
)
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
# 添加复选框
for i, (text, var_name) in enumerate(config["items"]):
# 修改:根据配置决定是否勾选
# 如果配置中有该测试项,则勾选;否则不勾选
is_checked = var_name in saved_test_items
var = tk.BooleanVar(value=is_checked)
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
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)
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
# 只有在 chart_notebook 已创建后才更新状态
if hasattr(self, "chart_notebook"):
self.update_chart_tabs_state()
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
# 更新色度参数框的显示状态
if hasattr(self, "cct_params_frame"):
self.toggle_cct_params_frame()
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
# ========== 新增方法: 更新配置并同步Tab状态 ==========
def update_config_and_tabs(self):
"""更新配置并同步图表Tab状态"""
self.update_config()
self.update_chart_tabs_state()
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
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测试",
2026-04-16 16:51:05 +08:00
}
2026-04-20 11:48:38 +08:00
return display_names.get(test_type, test_type)
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
def create_signal_format_content(self):
"""创建信号格式选项卡内容"""
self.signal_tabs = ttk.Notebook(self.signal_format_frame)
self.signal_tabs.pack(fill=tk.BOTH, expand=True)
# ==================== 屏模组格式设置 ====================
self.screen_module_signal_frame = ttk.Frame(self.signal_tabs)
self.screen_module_signal_frame.grid_columnconfigure(0, weight=1)
self.signal_tabs.add(self.screen_module_signal_frame, text="屏模组测试")
self.screen_module_timing_var = tk.StringVar(
value=self.config.current_test_types[self.config.current_test_type][
"timing"
]
)
screen_module_timing_combo = ttk.Combobox(
self.screen_module_signal_frame,
textvariable=self.screen_module_timing_var,
values=UCDEnum.TimingInfo.get_formatted_resolution_list(),
state="readonly",
)
screen_module_timing_combo.bind(
"<<ComboboxSelected>>", self.on_screen_module_timing_changed
)
screen_module_timing_combo.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
# ==================== SDR信号格式设置 ====================
self.sdr_signal_frame = ttk.Frame(self.signal_tabs)
# 配置列权重
self.sdr_signal_frame.grid_columnconfigure(0, weight=0)
self.sdr_signal_frame.grid_columnconfigure(1, weight=1)
self.signal_tabs.add(self.sdr_signal_frame, text="SDR测试")
# 色彩空间
ttk.Label(self.sdr_signal_frame, text="色彩空间:").grid(
row=0, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_color_space_var = tk.StringVar(value="BT.709")
sdr_color_space_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_color_space_var,
values=["BT.709", "BT.601", "BT.2020"],
width=10,
state="readonly",
)
sdr_color_space_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
# Gamma
ttk.Label(self.sdr_signal_frame, text="Gamma:").grid(
row=1, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_gamma_type_var = tk.StringVar(value="2.2")
sdr_gamma_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_gamma_type_var,
values=["2.2", "2.4", "2.6"],
width=10,
state="readonly",
)
sdr_gamma_combo.grid(row=1, column=1, sticky=tk.W, padx=5, pady=2)
# 数据范围
ttk.Label(self.sdr_signal_frame, text="数据范围:").grid(
row=2, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_data_range_var = tk.StringVar(value="Full")
sdr_range_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_data_range_var,
values=["Full", "Limited"],
width=10,
state="readonly",
)
sdr_range_combo.grid(row=2, column=1, sticky=tk.W, padx=5, pady=2)
# 编码位深
ttk.Label(self.sdr_signal_frame, text="编码位深:").grid(
row=3, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_bit_depth_var = tk.StringVar(value="8bit")
sdr_bit_depth_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_bit_depth_var,
values=["8bit", "10bit", "12bit"],
width=10,
state="readonly",
)
sdr_bit_depth_combo.grid(row=3, column=1, sticky=tk.W, padx=5, pady=2)
# ==================== HDR信号格式设置 ====================
self.hdr_signal_frame = ttk.Frame(self.signal_tabs)
# 配置列权重
self.hdr_signal_frame.grid_columnconfigure(0, weight=0)
self.hdr_signal_frame.grid_columnconfigure(1, weight=1)
self.signal_tabs.add(self.hdr_signal_frame, text="HDR")
# 色彩空间
ttk.Label(self.hdr_signal_frame, text="色彩空间:").grid(
row=0, column=0, sticky=tk.W, padx=5, pady=2
)
self.hdr_color_space_var = tk.StringVar(value="BT.2020")
hdr_color_space_combo = ttk.Combobox(
self.hdr_signal_frame,
textvariable=self.hdr_color_space_var,
values=["BT.2020", "DCI-P3"],
width=10,
state="readonly",
)
hdr_color_space_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
# Metadata设置
ttk.Label(self.hdr_signal_frame, text="Metadata:").grid(
row=1, column=0, sticky=tk.W, padx=5, pady=2
)
self.hdr_metadata_frame = ttk.Frame(self.hdr_signal_frame)
self.hdr_metadata_frame.grid(
row=1, column=1, rowspan=2, sticky=tk.W, padx=5, pady=2
)
ttk.Label(self.hdr_metadata_frame, text="MaxCLL:").grid(
row=0, column=0, sticky=tk.W
)
self.hdr_maxcll_var = tk.StringVar(value="1000")
ttk.Entry(
self.hdr_metadata_frame, textvariable=self.hdr_maxcll_var, width=6
).grid(row=0, column=1, padx=2)
ttk.Label(self.hdr_metadata_frame, text="MaxFALL:").grid(
row=1, column=0, sticky=tk.W
)
self.hdr_maxfall_var = tk.StringVar(value="400")
ttk.Entry(
self.hdr_metadata_frame, textvariable=self.hdr_maxfall_var, width=6
).grid(row=1, column=1, padx=2)
# 数据范围
ttk.Label(self.hdr_signal_frame, text="数据范围:").grid(
row=3, column=0, sticky=tk.W, padx=5, pady=2
)
self.hdr_data_range_var = tk.StringVar(value="Full")
hdr_range_combo = ttk.Combobox(
self.hdr_signal_frame,
textvariable=self.hdr_data_range_var,
values=["Full", "Limited"],
width=10,
state="readonly",
)
hdr_range_combo.grid(row=3, column=1, sticky=tk.W, padx=5, pady=2)
# 编码位深
ttk.Label(self.hdr_signal_frame, text="编码位深:").grid(
row=4, column=0, sticky=tk.W, padx=5, pady=2
)
self.hdr_bit_depth_var = tk.StringVar(value="8bit")
hdr_bit_depth_combo = ttk.Combobox(
self.hdr_signal_frame,
textvariable=self.hdr_bit_depth_var,
values=["8bit", "10bit", "12bit"],
width=10,
state="readonly",
)
hdr_bit_depth_combo.grid(row=4, column=1, sticky=tk.W, padx=5, pady=2)
# ==================== 初始化:默认只启用屏模组 Tab ====================
self.signal_tabs.select(0) # 选中屏模组
self.signal_tabs.tab(1, state="disabled") # 禁用 SDR
self.signal_tabs.tab(2, state="disabled") # 禁用 HDR
def create_connection_content(self):
"""创建设备连接区域"""
# 创建设备连接区域的主框架
com_frame = ttk.Frame(self.connection_frame)
com_frame.pack(fill=tk.X, pady=5)
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
# 获取可用的COM端口列表
available_ports = self.get_available_com_ports()
# 使用网格布局,更整齐
ttk.Label(com_frame, text="UCD列表:").grid(
row=0, column=0, sticky=ttk.W, padx=5, pady=3
)
self.ucd_list_var = tk.StringVar(value=self.config.device_config["ucd_list"])
self.ucd_list_combo = ttk.Combobox(
com_frame,
textvariable=self.ucd_list_var,
values=available_ports,
width=10,
state="readonly",
)
self.ucd_list_combo.grid(row=0, column=1, sticky=ttk.W, padx=5, pady=3)
self.ucd_list_combo.bind("<<ComboboxSelected>>", self.update_config)
# 添加UCD连接状态指示器
self.ucd_status_indicator = tk.Canvas(
com_frame, width=15, height=15, bg="gray", highlightthickness=0
)
self.ucd_status_indicator.grid(row=0, column=2, padx=(10, 20))
self.ucd_status_indicator.config(bg="gray")
# 添加按钮框架
button_frame = ttk.Frame(com_frame)
button_frame.grid(row=3, column=0, columnspan=3, pady=3, sticky="w")
connect_icon = load_icon("assets/connect-svgrepo-com.png")
self.check_button = ttk.Button(
button_frame,
image=connect_icon,
bootstyle="link",
takefocus=False,
command=self.check_com_connections,
)
self.check_button.image = connect_icon
self.check_button.pack(side="left", padx=0, pady=3)
disconnect_icon = load_icon("assets/disconnect-svgrepo-com.png")
# 断开连接按钮
self.disconnect_button = ttk.Button(
button_frame,
image=disconnect_icon,
bootstyle="link",
takefocus=False,
command=self.disconnect_com_connections,
)
self.disconnect_button.image = disconnect_icon # 防止图标被垃圾回收
self.disconnect_button.pack(side="left", padx=0, pady=3)
refresh_icon = load_icon("assets/refresh-svgrepo-com.png")
self.refresh_button = ttk.Button(
button_frame,
image=refresh_icon,
bootstyle="link",
takefocus=False,
command=self.refresh_com_ports,
)
self.refresh_button.image = refresh_icon # 防止图标被垃圾回收
self.refresh_button.pack(side="left", padx=0, pady=3)
# CA端口
ttk.Label(com_frame, text="CA端口:").grid(
row=1, column=0, sticky=ttk.W, padx=5, pady=3
)
self.ca_com_var = tk.StringVar(value=self.config.device_config["ca_com"])
self.ca_com_combo = ttk.Combobox(
com_frame,
textvariable=self.ca_com_var,
values=available_ports,
width=10,
state="readonly",
)
self.ca_com_combo.grid(row=1, column=1, sticky=ttk.W, padx=5, pady=3)
self.ca_com_combo.bind("<<ComboboxSelected>>", self.update_config)
# 添加CA连接状态指示器
self.ca_status_indicator = tk.Canvas(
com_frame, width=15, height=15, bg="gray", highlightthickness=0
)
self.ca_status_indicator.grid(row=1, column=2, padx=(10, 20))
self.ca_status_indicator.config(bg="gray")
# 添加CA通道设置
ttk.Label(com_frame, text="CA通道:").grid(
row=2, column=0, sticky=tk.W, padx=5, pady=3
)
self.ca_channel_var = tk.StringVar(
value=self.config.device_config["ca_channel"]
)
ca_channel_combo = ttk.Combobox(
com_frame,
textvariable=self.ca_channel_var,
values=[str(i) for i in range(11)],
width=10,
state="readonly",
)
ca_channel_combo.grid(row=2, column=1, sticky=ttk.W, padx=5, pady=3)
ca_channel_combo.bind("<<ComboboxSelected>>", self.update_config)
2026-04-16 16:51:05 +08:00
2026-04-20 11:48:38 +08:00
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
2026-04-20 16:57:30 +08:00
def create_test_type_frame(self):
"""创建测试类型选择区域(侧边栏形式)"""
# 设置测试类型变量
self.test_type_var = tk.StringVar(value="screen_module")
# 创建测试类型按钮并放置在侧边栏
test_types = [
("屏模组性能测试", "screen_module"),
("SDR Movie测试", "sdr_movie"),
("HDR Movie测试", "hdr_movie"),
]
for text, type_value in test_types:
btn = ttk.Button(
master=self.sidebar_frame,
text=text,
style="Sidebar.TButton",
padding=10,
command=lambda v=type_value: self.change_test_type(v),
takefocus=False,
)
btn.pack(fill=tk.X, padx=0, pady=1)
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
# 保存按钮引用以便后续更新样式
setattr(self, f"{type_value}_btn", btn)
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
# 添加分隔线
ttk.Separator(self.sidebar_frame, orient="horizontal").pack(
fill=tk.X, padx=10, pady=10
)
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
# ✅ 只保留日志按钮
self.log_btn = ttk.Button(
self.sidebar_frame,
text="测试日志",
style="Sidebar.TButton",
command=self.toggle_log_panel,
takefocus=False,
)
self.log_btn.pack(fill=tk.X, padx=0, pady=1)
# Local Dimming 测试按钮
self.local_dimming_btn = ttk.Button(
self.sidebar_frame,
text="Local Dimming",
style="Sidebar.TButton",
command=self.toggle_local_dimming_panel,
takefocus=False,
)
self.local_dimming_btn.pack(fill=tk.X, padx=0, pady=1)
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
# 注册面板按钮(只保留日志)
if hasattr(self, "panels"):
if "log" in self.panels:
self.panels["log"]["button"] = self.log_btn
if "local_dimming" in self.panels:
self.panels["local_dimming"]["button"] = self.local_dimming_btn
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
def update_config_info_display(self):
"""更新配置信息显示"""
if hasattr(self, "config") and hasattr(self.config, "get_current_config"):
current_config = self.config.get_current_config()
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
info_text = f"测试类型: {current_config.get('name', '未知')}\n"
info_text += (
f"测试项目: {', '.join(current_config.get('test_items', []))}\n"
)
info_text += f"信号格式: {current_config.get('signal_format', 'none')}\n"
info_text += f"色彩空间: {current_config.get('color_space', 'unknown')}\n"
info_text += f"位深度: {current_config.get('bit_depth', 'unknown')}"
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
# 高亮当前选中的测试类型
self.update_sidebar_selection()
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
def create_operation_frame(self):
"""创建操作按钮区域"""
operation_frame = ttk.Frame(self.control_frame_top)
operation_frame.pack(fill=tk.X, padx=5, pady=10)
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
self.start_btn = ttk.Button(
operation_frame,
text="开始测试",
command=self.start_test,
style="success.TButton",
)
self.start_btn.pack(side=tk.LEFT, padx=5)
self.stop_btn = ttk.Button(
operation_frame,
text="停止测试",
command=self.stop_test,
style="danger.TButton",
state=tk.DISABLED,
)
self.stop_btn.pack(side=tk.LEFT, padx=5)
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
self.save_btn = ttk.Button(
operation_frame,
text="保存结果",
command=self.save_results,
state=tk.DISABLED,
)
self.save_btn.pack(side=tk.LEFT, padx=5)
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
self.clear_config_btn = ttk.Button(
operation_frame,
text="清理配置",
command=self.clear_config_file,
)
self.clear_config_btn.pack(side=tk.LEFT, padx=5)
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
self.custom_btn = ttk.Button(
operation_frame,
text="客户模版",
command=self.start_custom_template_test,
style="info.TButton",
)
self.custom_btn.pack(side=tk.LEFT, padx=5)
self.update_custom_button_visibility()
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
def create_custom_template_result_panel(self):
"""创建客户模板结果显示区域(黑底表格)"""
self.custom_result_frame = ttk.LabelFrame(
self.custom_template_tab_frame, text="客户模板结果显示"
)
self.custom_result_frame.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5
)
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
table_container = tk.Frame(
self.custom_result_frame,
bg="#000000",
highlightthickness=1,
highlightbackground="#5a5a5a",
)
table_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
style = ttk.Style()
style.configure(
"CustomResult.Treeview",
background="#000000",
fieldbackground="#000000",
foreground="#ffffff",
rowheight=28,
borderwidth=0,
)
style.configure(
"CustomResult.Treeview.Heading",
background="#2f2f2f",
foreground="#f5f5f5",
font=("Microsoft YaHei", 10, "bold"),
relief="flat",
)
style.map(
"CustomResult.Treeview",
background=[("selected", "#1f4e79")],
foreground=[("selected", "#ffffff")],
)
style.map(
"CustomResult.Treeview.Heading",
background=[("active", "#3b3b3b")],
)
columns = (
"Pattern",
"No.",
"X",
"Y",
"Z",
"x",
"y",
"Lv",
"u'",
"v'",
"Tcp",
"duv",
"λd/λc",
"Pe"
)
self.custom_result_tree = ttk.Treeview(
table_container,
columns=columns,
show="headings",
height=4,
style="CustomResult.Treeview",
)
column_widths = {
"Pattern": 90,
"No.": 60,
"X": 80,
"Y": 80,
"Z": 80,
"x": 80,
"y": 80,
"Lv": 80,
"u'": 80,
"v'": 80,
"Tcp": 90,
"duv": 80,
"λd/λc": 95,
"Pe": 80,
}
for col in columns:
self.custom_result_tree.heading(col, text=col)
self.custom_result_tree.column(
col,
width=column_widths.get(col, 80),
minwidth=60,
anchor=tk.CENTER,
stretch=False,
)
y_scroll = ttk.Scrollbar(
table_container,
orient=tk.VERTICAL,
command=self.custom_result_tree.yview,
)
x_scroll = ttk.Scrollbar(
table_container,
orient=tk.HORIZONTAL,
command=self.custom_result_tree.xview,
)
self.custom_result_tree.configure(
yscrollcommand=y_scroll.set,
xscrollcommand=x_scroll.set,
)
self.custom_result_tree.grid(row=0, column=0, sticky="nsew")
y_scroll.grid(row=0, column=1, sticky="ns")
x_scroll.grid(row=1, column=0, sticky="ew")
# 右键菜单复制全部数据Excel 可直接按行列粘贴)
self.custom_result_menu = tk.Menu(self.root, tearoff=0)
self.custom_result_menu.add_command(
label="复制全部数据",
command=self.copy_custom_result_table,
)
self.custom_result_menu.add_command(
label="单步测试",
command=self.start_custom_row_single_step,
)
# self.custom_result_menu.add_separator()
# self.custom_result_menu.add_command(
# label="单步测试",
# command=self.fill_custom_result_test_data,
# )
self.custom_result_tree.bind("<Button-3>", self.show_custom_result_context_menu)
table_container.grid_rowconfigure(0, weight=1)
table_container.grid_columnconfigure(0, weight=1)
def show_custom_result_context_menu(self, event):
"""显示客户模板结果右键菜单"""
if not hasattr(self, "custom_result_tree") or not hasattr(
self, "custom_result_menu"
):
return
if self.testing:
# 测试进行中锁定客户模板结果表,禁止右键菜单。
return
row_id = self.custom_result_tree.identify_row(event.y)
if row_id:
self.custom_result_tree.selection_set(row_id)
self.custom_result_tree.focus(row_id)
has_rows = len(self.custom_result_tree.get_children()) > 0
has_selection = len(self.custom_result_tree.selection()) > 0
can_single_step = (
has_selection
and self.ca is not None
and self.ucd is not None
and not self.testing
)
try:
self.custom_result_menu.entryconfigure(
0,
state=("normal" if has_rows else "disabled"),
)
self.custom_result_menu.entryconfigure(
1,
state=("normal" if can_single_step else "disabled"),
)
self.custom_result_menu.tk_popup(event.x_root, event.y_root)
finally:
self.custom_result_menu.grab_release()
def set_custom_result_table_locked(self, locked):
"""锁定/解锁客户模板结果表(测试期间禁选择、禁右键)"""
if not hasattr(self, "custom_result_tree"):
return
try:
self.custom_result_tree.configure(selectmode=("none" if locked else "browse"))
except Exception:
pass
def start_custom_row_single_step(self):
"""单步测试当前选中行:发送该行 pattern 并覆盖该行测量结果"""
if not hasattr(self, "custom_result_tree"):
return
if self.ca is None or self.ucd is None:
messagebox.showerror("错误", "请先连接CA410和信号发生器")
return
if self.testing:
messagebox.showinfo("提示", "测试进行中,无法执行单步测试")
return
selected = self.custom_result_tree.selection()
if not selected:
messagebox.showinfo("提示", "请先选中一行再执行单步测试")
return
item_id = selected[0]
values = self.custom_result_tree.item(item_id, "values")
if not values:
messagebox.showinfo("提示", "选中行没有有效数据")
return
row_no = None
if len(values) > 1:
try:
row_no = int(float(values[1]))
except Exception:
row_no = None
if row_no is None or row_no <= 0:
children = list(self.custom_result_tree.get_children())
row_no = children.index(item_id) + 1 if item_id in children else 1
self._clear_custom_result_row(item_id, row_no)
threading.Thread(
target=self._run_custom_row_single_step,
args=(item_id, row_no),
daemon=True,
).start()
def _clear_custom_result_row(self, item_id, row_no):
"""单步测试开始前清空指定行的测量数据"""
if not hasattr(self, "custom_result_tree"):
return
old_values = list(self.custom_result_tree.item(item_id, "values"))
pattern_name = old_values[0] if len(old_values) > 0 else f"P {row_no}"
cleared_values = (
pattern_name,
row_no,
"---",
"---",
"---",
"---",
"---",
"---",
"---",
"---",
"---",
"---",
"---",
"---",
)
self.custom_result_tree.item(item_id, values=cleared_values)
self.custom_result_tree.see(item_id)
def _run_custom_row_single_step(self, item_id, row_no):
"""后台执行客户模板单步测试"""
try:
self._dispatch_ui(self.status_var.set, f"单步测试第 {row_no} 行...")
self.log_gui.log(f"开始单步测试第 {row_no}")
self.config.set_current_pattern("custom")
# 与批量 custom 测试保持一致:根据当前 SDR 配置转换 pattern 数据。
import copy
data_range = self.sdr_data_range_var.get()
original_params = copy.deepcopy(self.config.default_pattern_temp["pattern_params"])
converted_params = convert_pattern_params(
pattern_params=original_params,
data_range=data_range,
verbose=False,
)
temp_config = self.config.get_temp_config_with_converted_params(
mode="custom",
converted_params=converted_params,
)
if row_no > len(converted_params):
self.log_gui.log(f"❌ 行号超出 pattern 范围: {row_no}/{len(converted_params)}")
self._dispatch_ui(self.status_var.set, "单步测试失败:行号超范围")
return
self.ucd.set_ucd_params(temp_config)
pattern_param = converted_params[row_no - 1]
self.ucd.set_pattern(self.ucd.current_pattern, pattern_param)
self.ucd.run()
time.sleep(self.pattern_settle_time)
# 测量显示模式1读取 Tcp/duv/Lv显示模式8读取 λd/Pe/Lv 与 XYZ。
self.ca.set_Display(1)
tcp, duv, lv, _, _, _ = self.ca.readAllDisplay()
self.ca.set_Display(8)
lambda_d, pe, lv, X, Y, Z = self.ca.readAllDisplay()
xy = colour.XYZ_to_xy(np.array([X, Y, Z]))
u_prime, v_prime, _ = colour.XYZ_to_CIE1976UCS(np.array([X, Y, Z]))
row_data = {
"X": X,
"Y": Y,
"Z": Z,
"x": xy[0],
"y": xy[1],
"Lv": lv,
"u_prime": u_prime,
"v_prime": v_prime,
"Tcp": tcp,
"duv": duv,
"lambda_d": lambda_d,
"Pe": pe,
}
self._dispatch_ui(
self._update_custom_result_row, item_id, row_no, row_data
)
self.log_gui.log(f"✓ 第 {row_no} 行单步测试完成并已覆盖")
self._dispatch_ui(self.status_var.set, f"{row_no} 行单步测试完成")
except Exception as e:
self.log_gui.log(f"❌ 单步测试失败: {str(e)}")
self._dispatch_ui(self.status_var.set, "单步测试失败")
def _update_custom_result_row(self, item_id, row_no, result_data):
"""覆盖更新客户模板结果表中指定行"""
def fmt(value, digits=4):
if value is None:
return "--"
if isinstance(value, (int, float, np.floating)):
# CA 返回异常哨兵值(如 -99999999显示为占位符。
if (not np.isfinite(value)) or value <= -99999998:
return "---"
return f"{value:.{digits}f}"
try:
numeric_value = float(value)
if (not np.isfinite(numeric_value)) or numeric_value <= -99999998:
return "---"
except (TypeError, ValueError):
pass
return str(value)
old_values = list(self.custom_result_tree.item(item_id, "values"))
pattern_name = old_values[0] if len(old_values) > 0 else f"P {row_no}"
new_values = (
pattern_name,
row_no,
fmt(result_data.get("X")),
fmt(result_data.get("Y")),
fmt(result_data.get("Z")),
fmt(result_data.get("x")),
fmt(result_data.get("y")),
fmt(result_data.get("Lv"), 3),
fmt(result_data.get("u_prime")),
fmt(result_data.get("v_prime")),
fmt(result_data.get("Tcp"), 1),
fmt(result_data.get("duv"), 5),
fmt(result_data.get("lambda_d"), 1),
fmt(result_data.get("Pe"), 1),
)
self.custom_result_tree.item(item_id, values=new_values)
def copy_custom_result_table(self):
"""复制客户模板结果表格到剪贴板(不含标题行/No./Pattern"""
if not hasattr(self, "custom_result_tree"):
return
items = self.custom_result_tree.get_children()
if not items:
messagebox.showinfo("提示", "当前没有可复制的数据")
return
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
lines = []
columns = tuple(self.custom_result_tree["columns"])
excluded_col_indexes = {
idx
for idx, col_name in enumerate(columns)
if col_name in ("No.", "Pattern")
}
for item in items:
values = self.custom_result_tree.item(item, "values")
# 跳过 No. 和 Pattern 两列,只保留测量数据列。
data_values = [
v for idx, v in enumerate(values) if idx not in excluded_col_indexes
]
row = [
str(v).replace("\t", " ").replace("\n", " ")
for v in data_values
]
lines.append("\t".join(row))
clipboard_text = "\n".join(lines)
self.root.clipboard_clear()
self.root.clipboard_append(clipboard_text)
self.root.update_idletasks()
if hasattr(self, "status_var"):
self.status_var.set(f"已复制 {len(items)} 行客户模板数据到剪贴板")
if hasattr(self, "log_gui"):
self.log_gui.log(f"✓ 已复制客户模板表格数据({len(items)} 行)")
def fill_custom_result_test_data(self):
"""填充 147 行客户模板测试数据(用于界面验证)"""
if not hasattr(self, "custom_result_tree"):
return
self.clear_custom_template_results()
pattern_names = []
if hasattr(self, "config") and hasattr(self.config, "get_temp_pattern_names"):
pattern_names = self.config.get_temp_pattern_names()
total_rows = 147
for i in range(1, total_rows + 1):
ratio = (i - 1) / (total_rows - 1) if total_rows > 1 else 0
row_data = {
"pattern_name": (
pattern_names[i - 1] if i - 1 < len(pattern_names) else f"P {i}"
),
"X": 0.8 + ratio * 120,
"Y": 0.9 + ratio * 135,
"Z": 1.1 + ratio * 145,
"x": 0.24 + ratio * 0.10,
"y": 0.26 + ratio * 0.10,
"Lv": 1.0 + ratio * 500,
"u_prime": 0.16 + ratio * 0.12,
"v_prime": 0.42 + ratio * 0.08,
"Tcp": 1800 + ratio * 12000,
"duv": -0.01 + ratio * 0.03,
"lambda_d": 430 + ratio * 200,
"Pe": 10 + ratio * 90,
}
self.append_custom_template_result(i, row_data)
if hasattr(self, "chart_notebook") and hasattr(self, "custom_template_tab_frame"):
self.chart_notebook.select(self.custom_template_tab_frame)
if hasattr(self, "status_var"):
self.status_var.set("已填充 147 行客户模板测试数据")
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 已填充 147 行客户模板测试数据")
def clear_custom_template_results(self):
"""清空客户模板结果表格"""
if not hasattr(self, "custom_result_tree"):
return
for item in self.custom_result_tree.get_children():
self.custom_result_tree.delete(item)
def auto_expand_custom_result_view(self):
"""当客户模板表格有数据时,自动扩展窗口以尽量完整显示所有列"""
if not hasattr(self, "custom_result_tree"):
return
if len(self.custom_result_tree.get_children()) == 0:
return
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
try:
self.root.update_idletasks()
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
columns = tuple(self.custom_result_tree["columns"])
columns_total_width = 0
for col in columns:
columns_total_width += int(self.custom_result_tree.column(col, "width"))
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
left_panel_width = self.left_frame.winfo_width() if hasattr(self, "left_frame") else 180
if left_panel_width <= 1:
left_panel_width = 180
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
# 列宽 + 左侧导航 + 滚动条/边框/外边距。
target_width = int(left_panel_width + columns_total_width + 120)
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
screen_max_width = max(900, self.root.winfo_screenwidth() - 40)
target_width = min(target_width, screen_max_width)
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
current_width = self.root.winfo_width()
current_height = self.root.winfo_height()
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
# 只扩不缩,避免用户窗口被反复改变。
if target_width > current_width:
self.root.geometry(f"{target_width}x{current_height}")
self.root.update_idletasks()
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"⚠️ 自动扩展客户模板窗口失败: {str(e)}")
def append_custom_template_result(self, row_no, result_data):
"""追加一条客户模板结果到表格"""
def fmt(value, digits=4):
if value is None:
return "--"
if isinstance(value, (int, float, np.floating)):
# CA 返回异常哨兵值(如 -99999999显示为占位符。
if (not np.isfinite(value)) or value <= -99999998:
return "---"
return f"{value:.{digits}f}"
try:
numeric_value = float(value)
if (not np.isfinite(numeric_value)) or numeric_value <= -99999998:
return "---"
except (TypeError, ValueError):
pass
return str(value)
row_values = (
result_data.get("pattern_name", f"P {row_no}"),
row_no,
fmt(result_data.get("X")),
fmt(result_data.get("Y")),
fmt(result_data.get("Z")),
fmt(result_data.get("x")),
fmt(result_data.get("y")),
fmt(result_data.get("Lv"), 3),
fmt(result_data.get("u_prime")),
fmt(result_data.get("v_prime")),
fmt(result_data.get("Tcp"), 1),
fmt(result_data.get("duv"), 5),
fmt(result_data.get("lambda_d"), 1),
fmt(result_data.get("Pe"), 1)
)
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
if hasattr(self, "custom_result_tree"):
item_id = self.custom_result_tree.insert("", tk.END, values=row_values)
# 新增数据后自动跳转到最新行。
self.custom_result_tree.see(item_id)
self.auto_expand_custom_result_view()
def start_custom_template_test(self):
"""开始客户模板测试SDR"""
if hasattr(self, "chart_notebook") and hasattr(self, "custom_template_tab_frame"):
self.chart_notebook.select(self.custom_template_tab_frame)
if self.ca is None or self.ucd is None:
messagebox.showerror("错误", "请先连接CA410和信号发生器")
return
if self.testing:
messagebox.showinfo("提示", "测试已在进行中")
return
if hasattr(self, "debug_container"):
self.debug_container.pack_forget()
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.custom_btn.config(state=tk.DISABLED)
self.status_var.set("客户模板测试进行中...")
self.log_gui.clear_log()
self.clear_custom_template_results()
confirm = messagebox.askyesno(
"确认测试", "开始客户模板测试SDR\n\n将采集并显示客户模板格式结果。"
)
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.custom_btn.config(state=tk.NORMAL)
self.status_var.set("测试已取消")
self.set_custom_result_table_locked(False)
return
self.set_custom_result_table_locked(True)
self.test_thread = threading.Thread(target=self.run_custom_sdr_test, args=([],))
self.test_thread.daemon = True
self.test_thread.start()
def register_panel(self, panel_name, frame, button, visible_attr):
"""注册一个面板到管理系统"""
self.panels[panel_name] = {
"frame": frame,
"button": button,
"visible_attr": visible_attr,
}
def show_panel(self, panel_name):
"""显示指定面板,隐藏其他所有面板"""
if panel_name not in self.panels:
return
# 如果当前面板就是要显示的面板,则隐藏它
if self.current_panel == panel_name:
self.hide_all_panels()
return
# 隐藏所有面板
self.hide_all_panels()
# 显示指定面板
panel_info = self.panels[panel_name]
# 隐藏主内容区域
self.control_frame_top.pack_forget()
self.control_frame_middle.pack_forget()
self.control_frame_bottom.pack_forget()
# 显示目标面板
panel_info["frame"].pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5)
# 更新按钮样式
if panel_info["button"]:
panel_info["button"].configure(style="SidebarSelected.TButton")
# 更新状态
setattr(self, panel_info["visible_attr"], True)
self.current_panel = panel_name
def hide_all_panels(self):
"""隐藏所有面板,显示主内容区域"""
# 隐藏所有注册的面板
for panel_name, panel_info in self.panels.items():
panel_info["frame"].pack_forget()
if panel_info["button"]:
panel_info["button"].configure(style="Sidebar.TButton")
setattr(self, panel_info["visible_attr"], False)
# 显示主内容区域
self.control_frame_top.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
)
self.control_frame_middle.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
)
self.control_frame_bottom.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
)
self.current_panel = None
def create_log_panel(self):
"""创建日志面板"""
self.log_frame = ttk.Frame(self.content_frame)
self.log_gui = PQLogGUI(self.log_frame)
self.log_gui.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 默认隐藏日志面板
self.log_visible = False
# 注册到面板管理系统
self.register_panel(
"log", self.log_frame, None, "log_visible"
) # button会在后面设置
def create_local_dimming_panel(self):
"""创建 Local Dimming 测试面板 - 手动控制版"""
self.local_dimming_frame = ttk.Frame(self.content_frame)
# 主容器
main_container = ttk.Frame(self.local_dimming_frame, padding=10)
main_container.pack(fill=tk.BOTH, expand=True)
# ==================== 1. 标题 ====================
title_frame = ttk.Frame(main_container)
title_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(
title_frame,
text="🔆 Local Dimming 窗口测试",
font=("微软雅黑", 14, "bold"),
).pack(side=tk.LEFT)
# ==================== 2. 窗口百分比按钮 ====================
window_frame = ttk.LabelFrame(
main_container, text="🔆 窗口百分比(点击发送)", padding=10
)
window_frame.pack(fill=tk.X, pady=(0, 10))
# 说明文字
ttk.Label(
window_frame,
text="点击按钮发送对应百分比的白色窗口(黑色背景 + 居中白色矩形)",
font=("", 9),
foreground="#28a745",
).pack(pady=(0, 8))
# 第一行1%, 2%, 5%, 10%, 18%
row1 = ttk.Frame(window_frame)
row1.pack(fill=tk.X, pady=(0, 5))
percentages_row1 = [1, 2, 5, 10, 18]
for p in percentages_row1:
ttk.Button(
row1,
text=f"{p}%",
command=lambda p=p: self.send_ld_window(p),
bootstyle="success",
width=12,
).pack(side=tk.LEFT, padx=3)
# 第二行25%, 50%, 75%, 100%
row2 = ttk.Frame(window_frame)
row2.pack(fill=tk.X)
percentages_row2 = [25, 50, 75, 100]
for p in percentages_row2:
ttk.Button(
row2,
text=f"{p}%",
command=lambda p=p: self.send_ld_window(p),
bootstyle="success",
width=12,
).pack(side=tk.LEFT, padx=3)
# ==================== 4. CA410 采集按钮 ====================
measure_frame = ttk.LabelFrame(main_container, text="📊 CA410 测量", padding=10)
measure_frame.pack(fill=tk.X, pady=(0, 10))
measure_btn_frame = ttk.Frame(measure_frame)
measure_btn_frame.pack(fill=tk.X)
self.ld_measure_btn = ttk.Button(
measure_btn_frame,
text="📏 采集当前亮度",
command=self.measure_ld_luminance,
bootstyle="primary",
width=15,
)
self.ld_measure_btn.pack(side=tk.LEFT, padx=(0, 5))
# 显示测量结果
self.ld_result_label = ttk.Label(
measure_btn_frame,
text="亮度: -- cd/m² | x: -- | y: --",
font=("Consolas", 10),
foreground="#007bff",
)
self.ld_result_label.pack(side=tk.LEFT, padx=(10, 0))
# ==================== 5. 测试结果表格 ====================
result_frame = ttk.LabelFrame(main_container, text="📋 测试记录", padding=10)
result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# Treeview
columns = ("窗口百分比", "亮度 (cd/m²)", "x", "y", "时间")
self.ld_tree = ttk.Treeview(
result_frame, columns=columns, show="headings", height=10
)
for col in columns:
self.ld_tree.heading(col, text=col)
if col == "窗口百分比":
self.ld_tree.column(col, width=100, anchor=tk.CENTER)
elif col == "时间":
self.ld_tree.column(col, width=120, anchor=tk.CENTER)
else:
self.ld_tree.column(col, width=100, anchor=tk.CENTER)
self.ld_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 滚动条
scrollbar = ttk.Scrollbar(
result_frame, orient=tk.VERTICAL, command=self.ld_tree.yview
)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.ld_tree.configure(yscrollcommand=scrollbar.set)
# ==================== 6. 底部操作按钮 ====================
bottom_frame = ttk.Frame(main_container)
bottom_frame.pack(fill=tk.X)
self.ld_clear_btn = ttk.Button(
bottom_frame,
text="🗑️ 清空记录",
command=self.clear_ld_records,
bootstyle="danger-outline",
width=12,
)
self.ld_clear_btn.pack(side=tk.LEFT, padx=(0, 5))
self.ld_save_btn = ttk.Button(
bottom_frame,
text="💾 保存结果",
command=self.save_local_dimming_results,
bootstyle="info",
width=12,
)
self.ld_save_btn.pack(side=tk.LEFT)
# 默认隐藏
self.local_dimming_visible = False
# 注册到面板管理系统
self.register_panel(
"local_dimming",
self.local_dimming_frame,
None,
"local_dimming_visible",
)
# 初始化当前窗口百分比(用于记录)
self.current_ld_percentage = None
def toggle_local_dimming_panel(self):
"""切换 Local Dimming 面板显示"""
self.show_panel("local_dimming")
def toggle_log_panel(self):
"""切换日志面板的显示状态"""
self.show_panel("log")
2026-04-16 16:51:05 +08:00
create_result_chart_frame = _cf_create_result_chart_frame
on_chart_tab_changed = _cf_on_chart_tab_changed
2026-04-16 16:51:05 +08:00
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"):
2026-04-20 10:16:31 +08:00
self.chart_notebook.select(self.gamut_chart_frame)
2026-04-16 16:51:05 +08:00
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
2026-04-20 16:57:30 +08:00
2026-04-16 16:51:05 +08:00
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()
2026-04-20 16:44:46 +08:00
def toggle_cct_params_frame(self):
"""根据测试类型和测试项的选中状态显示对应参数框"""
selected_items = self.get_selected_test_items()
current_test_type = self.config.current_test_type
2026-04-20 16:57:30 +08:00
# ========== 默认隐藏所有参数框 ==========
2026-04-20 16:44:46 +08:00
self.cct_params_frame.pack_forget()
self.sdr_cct_params_frame.pack_forget()
2026-04-20 16:57:30 +08:00
# HDR 色度参数框(如果存在的话)
2026-04-20 16:44:46 +08:00
if hasattr(self, "hdr_cct_params_frame"):
self.hdr_cct_params_frame.pack_forget()
2026-04-20 16:57:30 +08:00
# ========== 根据测试类型和选中项显示对应参数框 ==========
2026-04-20 16:44:46 +08:00
if current_test_type == "screen_module":
2026-04-20 16:57:30 +08:00
# 屏模组:只有色度参数
2026-04-20 16:44:46 +08:00
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("✓ 显示屏模组色度参数设置")
2026-04-20 16:57:30 +08:00
2026-04-20 16:44:46 +08:00
elif current_test_type == "sdr_movie":
2026-04-20 16:57:30 +08:00
# SDR只有色度参数色准不需要参数设置框
2026-04-20 16:44:46 +08:00
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 色度参数设置")
2026-04-20 16:57:30 +08:00
2026-04-20 16:44:46 +08:00
elif current_test_type == "hdr_movie":
2026-04-20 16:57:30 +08:00
# HDR只有色度参数色准不需要参数设置框
2026-04-20 16:44:46 +08:00
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 色度参数框尚未创建")
2026-04-16 16:51:05 +08:00
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
2026-04-16 16:51:05 +08:00
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)}")
2026-04-20 16:57:30 +08:00
def toggle_screen_debug_panel(self):
"""打开/关闭屏模组单步调试面板(独立窗口)"""
# 如果窗口已存在且可见,关闭它
if hasattr(self, "debug_window") and self.debug_window.winfo_exists():
self.debug_window.destroy()
self.screen_debug_btn.config(text="打开调试面板")
self.log_gui.log("✓ 单步调试面板已关闭")
return
# 创建新窗口
self.debug_window = ttk.Toplevel(self.root)
self.debug_window.title("🔧 单步调试面板")
self.debug_window.geometry("900x400")
self.debug_window.transient(self.root)
# 创建调试面板容器
debug_container = ttk.Frame(self.debug_window, padding=10)
debug_container.pack(fill=tk.BOTH, expand=True) # ← 这个 pack 是对的
# 创建调试面板实例
debug_panel_instance = PQDebugPanel(debug_container, self)
# ← 这里不应该有任何 pack 调用!
self.log_gui.log("✓ 单步调试面板实例已创建")
# 重新启用调试(如果有数据)
try:
test_type = self.config.current_test_type
selected_items = self.get_selected_test_items()
if test_type == "screen_module":
if "gamma" in selected_items:
gray_data = self.results.get_intermediate_data("shared", "gray")
if gray_data:
self.log_gui.log(f" → 加载 {len(gray_data)} 个灰阶数据点")
debug_panel_instance.enable_debug(
"screen_module", "gamma", gray_data
)
self.log_gui.log("✓ 屏模组 Gamma 单步调试已重新启用")
else:
self.log_gui.log(" ✗ 没有可用的灰阶数据")
if "gamut" in selected_items:
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
if rgb_data:
self.log_gui.log(f" → 加载 {len(rgb_data)} 个RGB数据点")
debug_panel_instance.enable_debug(
"screen_module", "rgb", rgb_data
)
self.log_gui.log("✓ 屏模组 RGB 单步调试已重新启用")
except Exception as e:
self.log_gui.log(f"⚠️ 加载调试数据失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
# 更新按钮文字
self.screen_debug_btn.config(text="关闭调试面板")
# 窗口关闭时的回调
def on_closing():
self.screen_debug_btn.config(text="打开调试面板")
self.debug_window.destroy()
self.log_gui.log("✓ 单步调试窗口已关闭")
self.debug_window.protocol("WM_DELETE_WINDOW", on_closing)
self.debug_window.update_idletasks()
self.log_gui.log("✓ 单步调试面板已打开(独立窗口)")
def toggle_sdr_debug_panel(self):
"""打开/关闭 SDR 单步调试面板(独立窗口)"""
# 如果窗口已存在且可见,关闭它
if hasattr(self, "sdr_debug_window") and self.sdr_debug_window.winfo_exists():
self.sdr_debug_window.destroy()
self.sdr_debug_btn.config(text="打开调试面板")
self.log_gui.log("✓ SDR 单步调试面板已关闭")
return
# 创建新窗口
self.sdr_debug_window = ttk.Toplevel(self.root)
self.sdr_debug_window.title("🔧 SDR 单步调试面板")
self.sdr_debug_window.geometry("900x400")
self.sdr_debug_window.transient(self.root)
# 创建调试面板容器
debug_container = ttk.Frame(self.sdr_debug_window, padding=10)
debug_container.pack(fill=tk.BOTH, expand=True)
# ✅ 创建调试面板实例(不要对它调用 pack
debug_panel_instance = PQDebugPanel(debug_container, self)
# ← 删除debug_panel_instance.pack(...)
self.log_gui.log("✓ SDR 单步调试面板实例已创建")
# ✅ 重新启用调试(如果有数据)
try:
selected_items = self.get_selected_test_items()
if "gamma" in selected_items:
gray_data = self.results.get_intermediate_data("shared", "gray")
if gray_data:
self.log_gui.log(f" → 加载 {len(gray_data)} 个灰阶数据点")
debug_panel_instance.enable_debug("sdr_movie", "gamma", gray_data)
self.log_gui.log("✓ SDR Gamma 单步调试已重新启用")
if "accuracy" in selected_items:
accuracy_data = self.results.get_intermediate_data(
"accuracy", "measured"
)
if accuracy_data:
self.log_gui.log(f" → 加载 {len(accuracy_data)} 个色准数据点")
debug_panel_instance.enable_debug(
"sdr_movie", "accuracy", accuracy_data
)
self.log_gui.log("✓ SDR 色准单步调试已重新启用")
if "gamut" in selected_items:
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
if rgb_data:
self.log_gui.log(f" → 加载 {len(rgb_data)} 个RGB数据点")
debug_panel_instance.enable_debug("sdr_movie", "rgb", rgb_data)
self.log_gui.log("✓ SDR RGB 单步调试已重新启用")
except Exception as e:
self.log_gui.log(f"⚠️ 加载 SDR 调试数据失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
# 更新按钮文字
self.sdr_debug_btn.config(text="关闭调试面板")
# 窗口关闭时的回调
def on_closing():
self.sdr_debug_btn.config(text="打开调试面板")
self.sdr_debug_window.destroy()
self.log_gui.log("✓ SDR 单步调试窗口已关闭")
self.sdr_debug_window.protocol("WM_DELETE_WINDOW", on_closing)
self.sdr_debug_window.update_idletasks()
self.log_gui.log("✓ SDR 单步调试面板已打开(独立窗口)")
def toggle_hdr_debug_panel(self):
"""打开/关闭 HDR 单步调试面板(独立窗口)"""
# 如果窗口已存在且可见,关闭它
if hasattr(self, "hdr_debug_window") and self.hdr_debug_window.winfo_exists():
self.hdr_debug_window.destroy()
self.hdr_debug_btn.config(text="打开调试面板")
self.log_gui.log("✓ HDR 单步调试面板已关闭")
return
# 创建新窗口
self.hdr_debug_window = ttk.Toplevel(self.root)
self.hdr_debug_window.title("🔧 HDR 单步调试面板")
self.hdr_debug_window.geometry("900x400")
self.hdr_debug_window.transient(self.root)
# 创建调试面板容器
debug_container = ttk.Frame(self.hdr_debug_window, padding=10)
debug_container.pack(fill=tk.BOTH, expand=True)
# ✅ 创建调试面板实例(不要对它调用 pack
debug_panel_instance = PQDebugPanel(debug_container, self)
# ← 删除debug_panel_instance.pack(...)
self.log_gui.log("✓ HDR 单步调试面板实例已创建")
# ✅ 重新启用调试(如果有数据)
try:
selected_items = self.get_selected_test_items()
if "eotf" in selected_items:
gray_data = self.results.get_intermediate_data("shared", "gray")
if gray_data:
self.log_gui.log(f" → 加载 {len(gray_data)} 个灰阶数据点")
debug_panel_instance.enable_debug("hdr_movie", "eotf", gray_data)
self.log_gui.log("✓ HDR EOTF 单步调试已重新启用")
if "accuracy" in selected_items:
accuracy_data = self.results.get_intermediate_data(
"accuracy", "measured"
)
if accuracy_data:
self.log_gui.log(f" → 加载 {len(accuracy_data)} 个色准数据点")
debug_panel_instance.enable_debug(
"hdr_movie", "accuracy", accuracy_data
)
self.log_gui.log("✓ HDR 色准单步调试已重新启用")
if "gamut" in selected_items:
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
if rgb_data:
self.log_gui.log(f" → 加载 {len(rgb_data)} 个RGB数据点")
debug_panel_instance.enable_debug("hdr_movie", "rgb", rgb_data)
self.log_gui.log("✓ HDR RGB 单步调试已重新启用")
except Exception as e:
self.log_gui.log(f"⚠️ 加载 HDR 调试数据失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
# 更新按钮文字
self.hdr_debug_btn.config(text="关闭调试面板")
# 窗口关闭时的回调
def on_closing():
self.hdr_debug_btn.config(text="打开调试面板")
self.hdr_debug_window.destroy()
self.log_gui.log("✓ HDR 单步调试窗口已关闭")
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
self.hdr_debug_window.protocol("WM_DELETE_WINDOW", on_closing)
self.hdr_debug_window.update_idletasks()
2026-04-16 16:51:05 +08:00
2026-04-20 16:57:30 +08:00
self.log_gui.log("✓ HDR 单步调试面板已打开(独立窗口)")
2026-04-16 16:51:05 +08:00
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
2026-04-16 16:51:05 +08:00
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()