Files
pqAutomationApp/pqAutomationApp.py
2026-04-20 10:16:31 +08:00

7127 lines
292 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import ttkbootstrap as ttk
import tkinter as tk
from tkinter import messagebox, filedialog
import sys
import threading
import time
import os
import datetime
import colour
import json
import traceback
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import algorithm.pq_algorithm as pq_algorithm
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from app_version import APP_NAME, APP_VERSION, get_app_title
from utils.caSerail import CASerail
from utils.tvSerail import tvSerial
from utils.UCD323_Function import UCDController
from utils.UCD323_Enum import UCDEnum
from utils.pq.pq_config import PQConfig
from utils.pq.pq_result import PQResult
from utils.data_range_converter import convert_pattern_params
from PIL import Image, ImageTk
from views.collapsing_frame import CollapsingFrame
# from views.pq_history_gui import PQHistoryGUI
from views.pq_log_gui import PQLogGUI
from colormath.color_objects import xyYColor, LabColor
from colormath.color_conversions import convert_color
from colormath.color_diff import delta_e_cie2000
from views.pq_debug_panel import PQDebugPanel
# Step 0/1 重构:资源工具和纯算法已迁移到 app/ 包,这里重新导入以保持
# 对原函数名/方法名的向后兼容(老代码内部仍用 self.calculate_* 调用)。
from app.resources import (
backgroud_style_set,
get_resource_path,
load_icon,
)
from app.tests.color_accuracy import (
calculate_color_accuracy as _calc_color_accuracy,
calculate_delta_e_2000 as _calc_delta_e_2000,
get_accuracy_color_standards as _get_accuracy_color_standards,
)
from app.tests.eotf import calculate_pq_curve as _calc_pq_curve
from app.tests.gamma import calculate_gamma as _calc_gamma
from app.tests.gamut import calculate_gamut_coverage as _calc_gamut_coverage
from app.plots.plot_accuracy import plot_accuracy as _plot_accuracy
from app.plots.plot_cct import plot_cct as _plot_cct
from app.plots.plot_contrast import plot_contrast as _plot_contrast
from app.plots.plot_eotf import plot_eotf as _plot_eotf
from app.plots.plot_gamma import plot_gamma as _plot_gamma
from app.plots.plot_gamut import plot_gamut as _plot_gamut
from app.views.chart_frame import (
clear_chart as _cf_clear_chart,
create_result_chart_frame as _cf_create_result_chart_frame,
init_accuracy_chart as _cf_init_accuracy_chart,
init_cct_chart as _cf_init_cct_chart,
init_contrast_chart as _cf_init_contrast_chart,
init_eotf_chart as _cf_init_eotf_chart,
init_gamma_chart as _cf_init_gamma_chart,
init_gamut_chart as _cf_init_gamut_chart,
on_chart_tab_changed as _cf_on_chart_tab_changed,
update_chart_tabs_state as _cf_update_chart_tabs_state,
)
plt.rcParams["font.family"] = ["sans-serif"]
plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"]
class PQAutomationApp:
def __init__(self, root):
self.root = root
self.root.title(get_app_title())
self.root.geometry("900x650")
self.root.minsize(900, 650)
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
self.app_name = APP_NAME
self.app_version = APP_VERSION
self.config_cleared = False
# 初始化设备连接状态
self.ca = None # CA410色度计
self.ucd = UCDController() # 信号发生器
# 初始化测试状态
self.testing = False
self.test_thread = None
# 采集节奏参数:默认在稳定性与速度之间取平衡,可按现场情况再微调。
self.pattern_settle_time = 0.4
self.pattern_progress_log_step = 5
# 创建主框架
self.main_frame = ttk.Frame(root)
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
backgroud_style_set()
# 创建配置对象
self.config = PQConfig()
self.results = None
# 加载上次保存的设置
self.config_file = self.get_config_path()
self.load_pq_config()
# 如果加载的配置不是屏模组,强制切换为屏模组
if self.config.current_test_type != "screen_module":
self.config.set_current_test_type("screen_module")
# 初始化侧边栏功能显示状态 - 使用统一的页面管理
self.current_panel = None # 当前显示的面板名称
self.panels = {} # 存储所有面板的信息
self.log_visible = False
# 创建左侧面板
self.left_frame = ttk.Frame(self.main_frame, width=180)
self.left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=0, pady=5)
self.left_frame.pack_propagate(False)
# 创建左侧导航栏
self.sidebar_frame = ttk.Frame(self.left_frame, bootstyle="primary")
self.sidebar_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=5)
# self.sidebar_frame.pack_propagate(False)
# 创建右侧内容区域
self.content_frame = ttk.Frame(self.main_frame)
self.content_frame.pack(
side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5
)
# 创建右侧内容区域的上中下三个分区
self.control_frame_top = ttk.Frame(self.content_frame)
self.control_frame_top.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
)
self.control_frame_middle = ttk.Frame(self.content_frame)
self.control_frame_middle.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
)
self.control_frame_bottom = ttk.Frame(self.content_frame)
self.control_frame_bottom.pack(
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
)
# 创建右上角悬浮配置框
self.create_floating_config_panel()
# 创建右侧结果显示区域
self.result_frame = ttk.LabelFrame(self.control_frame_middle, text="测试结果")
self.result_frame.pack(
side=tk.BOTTOM, fill=tk.BOTH, expand=True, padx=0, pady=5
)
# 创建日志显示区域
self.create_log_panel()
# 创建 Local Dimming 面板
self.create_local_dimming_panel()
# 创建测试类型选择区域
self.create_test_type_frame()
# 创建操作按钮区域
self.create_operation_frame()
# 创建结果图表区域
self.create_result_chart_frame()
# 创建客户模板结果显示区域(黑底表格)
self.create_custom_template_result_panel()
# 在所有控件创建完成后,统一初始化测试类型
self.root.after(100, self.initialize_default_test_type)
# 状态栏
self.status_var = tk.StringVar(value="就绪")
self.status_bar = ttk.Label(
root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W
)
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
def get_config_path(self):
"""获取配置文件的完整路径(兼容打包后的程序)"""
import os
import sys
# 判断是否是打包后的程序
if getattr(sys, "frozen", False):
# 打包后:使用可执行文件所在目录
base_path = os.path.dirname(sys.executable)
else:
# 开发环境:使用脚本所在目录
base_path = os.path.dirname(os.path.abspath(__file__))
# 构建配置文件路径
config_dir = os.path.join(base_path, "settings")
config_file = os.path.join(config_dir, "pq_config.json")
# 确保 settings 目录存在
if not os.path.exists(config_dir):
os.makedirs(config_dir)
return config_file
def initialize_default_test_type(self):
"""初始化默认测试类型(在所有控件创建完成后调用)"""
try:
# 强制切换到屏模组
self.change_test_type("screen_module")
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 默认测试类型已设置为屏模组")
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"初始化默认测试类型失败: {str(e)}")
def init_gamut_chart(self):
"""转发到 app.views.chart_frame.init_gamut_chartStep 3 重构)"""
return _cf_init_gamut_chart(self)
def init_gamma_chart(self):
"""转发到 app.views.chart_frame.init_gamma_chartStep 3 重构)"""
return _cf_init_gamma_chart(self)
def init_eotf_chart(self):
"""转发到 app.views.chart_frame.init_eotf_chartStep 3 重构)"""
return _cf_init_eotf_chart(self)
def init_cct_chart(self):
"""转发到 app.views.chart_frame.init_cct_chartStep 3 重构)"""
return _cf_init_cct_chart(self)
def init_contrast_chart(self):
"""转发到 app.views.chart_frame.init_contrast_chartStep 3 重构)"""
return _cf_init_contrast_chart(self)
def init_accuracy_chart(self):
"""转发到 app.views.chart_frame.init_accuracy_chartStep 3 重构)"""
return _cf_init_accuracy_chart(self)
def clear_chart(self):
"""转发到 app.views.chart_frame.clear_chartStep 3 重构)"""
return _cf_clear_chart(self)
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="色度参数设置(屏模组)"
)
# 默认值
self.DEFAULT_CCT_PARAMS = {
"x_ideal": 0.3127,
"x_tolerance": 0.003,
"y_ideal": 0.3290,
"y_tolerance": 0.003,
}
# 从配置读取屏模组参数
saved_params = self.config.current_test_types.get("screen_module", {}).get(
"cct_params", self.DEFAULT_CCT_PARAMS.copy()
)
# 色域参考标准
saved_gamut_ref = self.config.current_test_types.get("screen_module", {}).get(
"gamut_reference", "DCI-P3"
)
# 创建屏模组变量
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 = self.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 默认值
self.SDR_DEFAULT_CCT_PARAMS = {
"x_ideal": 0.3127,
"x_tolerance": 0.003,
"y_ideal": 0.3290,
"y_tolerance": 0.003,
}
# 从配置读取 SDR 参数
sdr_saved_params = self.config.current_test_types.get("sdr_movie", {}).get(
"cct_params", self.SDR_DEFAULT_CCT_PARAMS.copy()
)
# 色域参考标准
sdr_saved_gamut_ref = self.config.current_test_types.get("sdr_movie", {}).get(
"gamut_reference", "BT.709"
)
# 创建 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 = self.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 默认值
self.HDR_DEFAULT_CCT_PARAMS = {
"x_ideal": 0.3127,
"x_tolerance": 0.003,
"y_ideal": 0.3290,
"y_tolerance": 0.003,
}
# 从配置读取 HDR 参数
hdr_saved_params = self.config.current_test_types.get("hdr_movie", {}).get(
"cct_params", self.HDR_DEFAULT_CCT_PARAMS.copy()
)
# 色域参考标准
hdr_saved_gamut_ref = self.config.current_test_types.get("hdr_movie", {}).get(
"gamut_reference", "BT.2020"
)
# 创建 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 = self.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 on_sdr_cct_param_focus_out(self, var, default_value):
"""SDR 色度参数失去焦点时的处理"""
try:
value = var.get().strip()
if value == "":
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"✓ SDR 参数为空,恢复默认值: {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"⚠️ SDR 参数超出范围,恢复默认值: {default_value}"
)
except ValueError:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"⚠️ SDR 参数无效,恢复默认值: {default_value}")
self.save_sdr_cct_params()
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"处理 SDR 参数失败: {str(e)}")
def save_sdr_cct_params(self):
"""保存 SDR 色度参数"""
try:
def get_float(var, default):
try:
value = var.get().strip()
if value == "":
return default
return float(value)
except:
return default
sdr_cct_params = {
"x_ideal": get_float(
self.sdr_cct_x_ideal_var, self.SDR_DEFAULT_CCT_PARAMS["x_ideal"]
),
"x_tolerance": get_float(
self.sdr_cct_x_tolerance_var,
self.SDR_DEFAULT_CCT_PARAMS["x_tolerance"],
),
"y_ideal": get_float(
self.sdr_cct_y_ideal_var, self.SDR_DEFAULT_CCT_PARAMS["y_ideal"]
),
"y_tolerance": get_float(
self.sdr_cct_y_tolerance_var,
self.SDR_DEFAULT_CCT_PARAMS["y_tolerance"],
),
}
if "sdr_movie" not in self.config.current_test_types:
self.config.current_test_types["sdr_movie"] = {}
self.config.current_test_types["sdr_movie"]["cct_params"] = sdr_cct_params
self.save_pq_config()
except:
pass
def on_hdr_cct_param_focus_out(self, var, default_value):
"""HDR 色度参数失去焦点时的处理"""
try:
value = var.get().strip()
if value == "":
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"✓ HDR 参数为空,恢复默认值: {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"⚠️ HDR 参数超出范围,恢复默认值: {default_value}"
)
except ValueError:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"⚠️ HDR 参数无效,恢复默认值: {default_value}")
self.save_hdr_cct_params()
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"处理 HDR 参数失败: {str(e)}")
def save_hdr_cct_params(self):
"""保存 HDR 色度参数"""
try:
def get_float(var, default):
try:
value = var.get().strip()
if value == "":
return default
return float(value)
except:
return default
hdr_cct_params = {
"x_ideal": get_float(
self.hdr_cct_x_ideal_var, self.HDR_DEFAULT_CCT_PARAMS["x_ideal"]
),
"x_tolerance": get_float(
self.hdr_cct_x_tolerance_var,
self.HDR_DEFAULT_CCT_PARAMS["x_tolerance"],
),
"y_ideal": get_float(
self.hdr_cct_y_ideal_var, self.HDR_DEFAULT_CCT_PARAMS["y_ideal"]
),
"y_tolerance": get_float(
self.hdr_cct_y_tolerance_var,
self.HDR_DEFAULT_CCT_PARAMS["y_tolerance"],
),
}
if "hdr_movie" not in self.config.current_test_types:
self.config.current_test_types["hdr_movie"] = {}
self.config.current_test_types["hdr_movie"]["cct_params"] = hdr_cct_params
self.save_pq_config()
except:
pass
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
# 3. 跳转到色度图Tab
self.chart_notebook.select(self.cct_chart_frame)
self.root.update_idletasks()
# 4. 检查是否有数据
if not hasattr(self, "results") or not self.results:
self.log_gui.log("⚠️ 没有测试数据,无法重新绘制")
messagebox.showwarning("警告", "请先完成测试后再重新计算")
return
# 5. 获取保存的灰阶数据
gray_data = self.results.get_intermediate_data("shared", "gray")
if not gray_data:
gray_data = self.results.get_intermediate_data("cct", "gray")
if not gray_data or len(gray_data) < 2:
self.log_gui.log("⚠️ 没有可用的灰阶数据")
messagebox.showwarning("警告", "没有找到色度测试数据")
return
# 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
# 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"
self.log_gui.log("=" * 50)
self.log_gui.log(f"开始重新计算色域(参考标准: {reference_standard}...")
self.log_gui.log("=" * 50)
# 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,
},
)
self.log_gui.log("✓ 测试结果已更新到 results 对象")
# 10. 重新绘制色域图
self.plot_gamut(rgb_data, coverage_xy, test_type)
self.log_gui.log("✓ 色域图已重新绘制")
self.log_gui.log("=" * 50)
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):
"""色度参数失去焦点时的处理 - 空值恢复默认"""
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 save_cct_params(self):
"""保存色度参数 - 简化版"""
try:
current_type = self.config.current_test_type
def get_float(var, default):
try:
value = var.get().strip()
if value == "":
return default
return float(value)
except:
return default
cct_params = {
"x_ideal": get_float(
self.cct_x_ideal_var, self.DEFAULT_CCT_PARAMS["x_ideal"]
),
"x_tolerance": get_float(
self.cct_x_tolerance_var, self.DEFAULT_CCT_PARAMS["x_tolerance"]
),
"y_ideal": get_float(
self.cct_y_ideal_var, self.DEFAULT_CCT_PARAMS["y_ideal"]
),
"y_tolerance": get_float(
self.cct_y_tolerance_var, self.DEFAULT_CCT_PARAMS["y_tolerance"]
),
}
if current_type not in self.config.current_test_types:
self.config.current_test_types[current_type] = {}
self.config.current_test_types[current_type]["cct_params"] = cct_params
self.save_pq_config()
except:
pass
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.DEFAULT_CCT_PARAMS.copy()
# 更新输入框的值
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)}")
def update_test_items(self):
"""根据当前测试类型更新测试项目复选框"""
# 先隐藏所有测试项目框架
for config in self.test_items.values():
config["frame"].pack_forget()
current_test_type = self.config.current_test_type
self.test_vars = {}
if current_test_type in self.test_items:
config = self.test_items[current_test_type]
frame = config["frame"]
frame.pack(fill=tk.X, padx=5, pady=5)
# 添加测试类型标签
type_label = ttk.Label(
frame,
text=self.get_test_type_display_name(current_test_type),
style="primary.TLabel",
)
type_label.grid(row=0, column=0, columnspan=2, sticky=tk.W, padx=5, pady=3)
# 从配置中读取保存的选择状态
saved_test_items = self.config.current_test_types[current_test_type].get(
"test_items", []
)
# 添加复选框
for i, (text, var_name) in enumerate(config["items"]):
# 修改:根据配置决定是否勾选
# 如果配置中有该测试项,则勾选;否则不勾选
is_checked = var_name in saved_test_items
var = tk.BooleanVar(value=is_checked)
self.test_vars[f"{current_test_type}_{var_name}"] = var
ttk.Checkbutton(
frame,
text=text,
variable=var,
bootstyle="round-toggle",
command=self.update_config_and_tabs,
).grid(row=i // 2 + 1, column=i % 2, sticky=tk.W, padx=10, pady=5)
# 只有在 chart_notebook 已创建后才更新状态
if hasattr(self, "chart_notebook"):
self.update_chart_tabs_state()
# 更新色度参数框的显示状态
if hasattr(self, "cct_params_frame"):
self.toggle_cct_params_frame()
# ========== 新增方法: 更新配置并同步Tab状态 ==========
def update_config_and_tabs(self):
"""更新配置并同步图表Tab状态"""
self.update_config()
self.update_chart_tabs_state()
def update_chart_tabs_state(self):
"""转发到 app.views.chart_frame.update_chart_tabs_stateStep 3 重构)"""
return _cf_update_chart_tabs_state(self)
def get_test_type_display_name(self, test_type):
"""获取测试类型的显示名称"""
display_names = {
"screen_module": "屏模组性能测试",
"sdr_movie": "SDR Movie测试",
"hdr_movie": "HDR Movie测试",
}
return display_names.get(test_type, test_type)
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)
# 获取可用的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)
def get_available_ucd_ports(self):
"""获取可用的UCD端口列表"""
return self.ucd.search_device()
def get_available_com_ports(self):
"""获取可用的COM端口列表"""
try:
import serial.tools.list_ports
ports = serial.tools.list_ports.comports()
return [port.device for port in ports]
except Exception as e:
self.log_gui.log(f"获取COM端口列表出错: {e}")
return []
def refresh_com_ports(self):
"""刷新COM端口列表"""
available_ports = self.get_available_com_ports()
available_list = self.get_available_ucd_ports()
# 更新UCD列表的下拉框选项
ucd_list_current = self.ucd_list_var.get()
if ucd_list_current not in available_list:
self.ucd_list_var.set(available_list[0] if available_list else "")
self.ucd_list_combo.config(values=available_list)
# 更新CA端口的下拉框选项
ca_com_current = self.ca_com_var.get()
if ca_com_current not in available_ports:
self.ca_com_var.set(
available_ports[1]
if len(available_ports) > 1
else (available_ports[0] if available_ports else "")
)
self.ca_com_combo.config(values=available_ports)
# 重置连接状态指示器为灰色
if hasattr(self, "ucd_status_indicator"):
self.ucd_status_indicator.config(bg="gray")
if hasattr(self, "ca_status_indicator"):
self.ca_status_indicator.config(bg="gray")
self.update_config()
def check_com_connections(self):
"""检测COM端口连接状态"""
# 禁用连接按钮,防止重复点击
self.check_button.configure(state="disabled")
self.refresh_button.configure(state="disabled")
# 更新状态栏
self.status_var.set("正在检测连接...")
self.root.update()
# 使用线程进行连接检测
def check_connections():
try:
# 检测TV连接
ucd_connected = self.check_port_connection(is_ucd=True)
self.root.after(
0,
lambda: self.update_connection_indicator(
self.ucd_status_indicator, ucd_connected
),
)
# 检测CA连接
ca_connected = self.check_port_connection(is_ucd=False)
self.root.after(
0,
lambda: self.update_connection_indicator(
self.ca_status_indicator, ca_connected
),
)
# 更新状态栏
self.root.after(0, lambda: self.status_var.set("连接检测完成"))
# 重新启用所有控件
self.root.after(0, self.enable_com_widgets)
except Exception as e:
self.root.after(0, lambda: self.log_gui.log(f"连接检测出错: {e}"))
self.root.after(0, self.enable_com_widgets)
# 启动线程
threading.Thread(target=check_connections, daemon=True).start()
def update_connection_indicator(self, indicator, connected):
"""更新连接状态指示器颜色"""
if connected:
indicator.config(bg="green")
else:
indicator.config(bg="red")
def check_port_connection(self, is_ucd=True):
"""检测指定端口是否可以连接"""
try:
if is_ucd:
if self.ucd.status:
try:
self.ucd.close()
except:
pass
if not self.ucd.open(self.ucd_list_var.get()):
self.log_gui.log(
f"设备 {self.ucd_list_var.get()} 异常UCD323连接失败"
)
return False
else:
return True
else:
# 如果CA对象已存在先关闭
if self.ca is not None:
try:
self.ca.close()
except:
pass
channel_value = self.ca_channel_var.get()
str_channel = f"{int(channel_value):02d}"
self.ca = CASerail()
self.ca.open(self.config.device_config["ca_com"], 19200, 7, "E", 2)
# data = self.ca.set_xyLv_Display()
data = self.ca.set_all_Display()
if data:
data = self.ca.setSynchMode(3)
data = self.ca.setMeasureSpeed(1)
if True:
time.sleep(0.5)
data = self.ca.setZeroCalibration()
channel_value = self.ca_channel_var.get()
str_channel = f"{int(channel_value):02d}"
data = self.ca.setChannel(str_channel)
return True
else:
self.log_gui.log(
f"端口 {self.config.device_config["ca_com"]} 异常,色温仪连接失败"
)
self.ca.close()
self.ca = None
return False
except Exception as e:
self.log_gui.log(f"端口连接失败: {e}")
return False
def enable_com_widgets(self):
"""重新启用所有控件"""
self.check_button.configure(state="normal")
self.refresh_button.configure(state="normal")
def disconnect_com_connections(self):
"""断开所有串口连接"""
try:
# 断开TV连接
if self.ucd.status:
try:
self.ucd.close()
except:
pass
finally:
self.ucd.status = False
self.log_gui.log("UCD连接已断开")
# 断开CA连接
if self.ca is not None:
try:
self.ca.close()
except:
pass
finally:
self.ca = None
self.log_gui.log("CA连接已断开")
# 重新启用相关控件
self.enable_com_widgets()
self.ucd_status_indicator.config(bg="gray")
self.ca_status_indicator.config(bg="gray")
self.status_var.set("串口连接已断开")
except Exception as e:
self.log_gui.log(f"断开连接时发生错误: {str(e)}")
messagebox.showerror("错误", f"断开连接失败: {str(e)}")
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)
# 保存按钮引用以便后续更新样式
setattr(self, f"{type_value}_btn", btn)
# 添加分隔线
ttk.Separator(self.sidebar_frame, orient="horizontal").pack(
fill=tk.X, padx=10, pady=10
)
# ✅ 只保留日志按钮
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)
# 注册面板按钮(只保留日志)
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
def update_config_info_display(self):
"""更新配置信息显示"""
if hasattr(self, "config") and hasattr(self.config, "get_current_config"):
current_config = self.config.get_current_config()
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')}"
# 高亮当前选中的测试类型
self.update_sidebar_selection()
def create_operation_frame(self):
"""创建操作按钮区域"""
operation_frame = ttk.Frame(self.control_frame_top)
operation_frame.pack(fill=tk.X, padx=5, pady=10)
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)
self.save_btn = ttk.Button(
operation_frame,
text="保存结果",
command=self.save_results,
state=tk.DISABLED,
)
self.save_btn.pack(side=tk.LEFT, padx=5)
self.clear_config_btn = ttk.Button(
operation_frame,
text="清理配置",
command=self.clear_config_file,
)
self.clear_config_btn.pack(side=tk.LEFT, padx=5)
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()
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
)
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.root.after(0, lambda: 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.root.after(0, lambda: 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.root.after(
0,
lambda: self._update_custom_result_row(item_id, row_no, row_data),
)
self.log_gui.log(f"✓ 第 {row_no} 行单步测试完成并已覆盖")
self.root.after(0, lambda: self.status_var.set(f"{row_no} 行单步测试完成"))
except Exception as e:
self.log_gui.log(f"❌ 单步测试失败: {str(e)}")
self.root.after(0, lambda: 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
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
try:
self.root.update_idletasks()
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"))
left_panel_width = self.left_frame.winfo_width() if hasattr(self, "left_frame") else 180
if left_panel_width <= 1:
left_panel_width = 180
# 列宽 + 左侧导航 + 滚动条/边框/外边距。
target_width = int(left_panel_width + columns_total_width + 120)
screen_max_width = max(900, self.root.winfo_screenwidth() - 40)
target_width = min(target_width, screen_max_width)
current_width = self.root.winfo_width()
current_height = self.root.winfo_height()
# 只扩不缩,避免用户窗口被反复改变。
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)
)
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")
def create_result_chart_frame(self):
"""转发到 app.views.chart_frame.create_result_chart_frameStep 3 重构)"""
return _cf_create_result_chart_frame(self)
def on_chart_tab_changed(self, event):
"""转发到 app.views.chart_frame.on_chart_tab_changedStep 3 重构)"""
return _cf_on_chart_tab_changed(self, event)
def change_test_type(self, test_type):
"""切换测试类型"""
# 切换测试类型时,自动隐藏日志面板和 Local Dimming 面板
if self.current_panel in ("log", "local_dimming"):
self.hide_all_panels()
# 先保存当前测试类型的色度参数
if hasattr(self, "cct_x_ideal_var"):
try:
current_type = self.config.current_test_type
if current_type == "screen_module":
self.save_cct_params()
elif current_type == "sdr_movie":
self.save_sdr_cct_params()
elif current_type == "hdr_movie":
if hasattr(self, "save_hdr_cct_params"):
self.save_hdr_cct_params()
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"保存参数失败: {str(e)}")
# 更新测试类型
self.test_type_var.set(test_type)
if hasattr(self, "config") and hasattr(self.config, "set_current_test_type"):
success = self.config.set_current_test_type(test_type)
if not success and hasattr(self, "log_gui"):
self.log_gui.log(f"切换测试类型失败: {test_type}")
# 更新测试项目和侧边栏
self.update_test_items()
self.update_sidebar_selection()
self.on_test_type_change()
# ========== ✅ 1. 切换信号格式 Tab ==========
if hasattr(self, "signal_tabs"):
try:
# 定义测试类型与信号格式 Tab 的映射
tab_mapping = {
"screen_module": 0, # 屏模组测试
"sdr_movie": 1, # SDR测试
"hdr_movie": 2, # HDR
}
target_tab = tab_mapping.get(test_type, 0)
# 先启用所有 Tab
for i in range(3):
self.signal_tabs.tab(i, state="normal")
# 切换到目标 Tab
self.signal_tabs.select(target_tab)
# 强制刷新显示
self.signal_tabs.update()
self.root.update_idletasks()
# 强制显示对应的 Frame
if target_tab == 0:
self.screen_module_signal_frame.tkraise()
elif target_tab == 1:
self.sdr_signal_frame.tkraise()
elif target_tab == 2:
self.hdr_signal_frame.tkraise()
# 禁用其他 Tab
for i in range(3):
if i != target_tab:
self.signal_tabs.tab(i, state="disabled")
# 日志记录
if hasattr(self, "log_gui"):
tab_names = ["屏模组测试", "SDR测试", "HDR"]
self.log_gui.log(f"✓ 已切换到 {tab_names[target_tab]} 信号格式")
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"切换信号格式失败: {str(e)}")
else:
if hasattr(self, "log_gui"):
self.log_gui.log("⚠️ signal_tabs 尚未创建")
# ========== 2. 动态切换 Gamma/EOTF Tab ==========
if hasattr(self, "chart_notebook"):
try:
current_tabs = list(self.chart_notebook.tabs())
# 获取当前 Tab 的索引
gamma_tab_id = str(self.gamma_chart_frame)
eotf_tab_id = str(self.eotf_chart_frame)
if test_type == "hdr_movie":
# ========== HDR 测试:移除 Gamma添加 EOTF ==========
# 1. 如果 Gamma Tab 存在,移除它
if gamma_tab_id in current_tabs:
gamma_index = current_tabs.index(gamma_tab_id)
self.chart_notebook.forget(gamma_index)
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 已隐藏 Gamma 曲线 Tab")
# 2. 如果 EOTF Tab 不存在,添加它(在色域图之后)
if eotf_tab_id not in current_tabs:
self.chart_notebook.insert(
1, self.eotf_chart_frame, text="EOTF 曲线"
)
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 已显示 EOTF 曲线 Tab")
else:
# ========== SDR/屏模组测试:移除 EOTF添加 Gamma ==========
# 1. 如果 EOTF Tab 存在,移除它
if eotf_tab_id in current_tabs:
eotf_index = current_tabs.index(eotf_tab_id)
self.chart_notebook.forget(eotf_index)
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 已隐藏 EOTF 曲线 Tab")
# 2. 如果 Gamma Tab 不存在,添加它(在色域图之后)
if gamma_tab_id not in current_tabs:
self.chart_notebook.insert(
1, self.gamma_chart_frame, text="Gamma 曲线"
)
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 已显示 Gamma 曲线 Tab")
# ========== 3. 仅在 SDR 测试显示客户模板结果 Tab ==========
custom_tab_id = str(self.custom_template_tab_frame)
current_tabs = list(self.chart_notebook.tabs())
if test_type == "sdr_movie":
if custom_tab_id not in current_tabs:
self.chart_notebook.add(
self.custom_template_tab_frame,
text="客户模板结果显示",
)
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 已显示客户模板结果 Tab")
else:
if custom_tab_id in current_tabs:
self.chart_notebook.forget(self.custom_template_tab_frame)
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 已隐藏客户模板结果 Tab")
# 刷新显示
self.chart_notebook.update_idletasks()
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"切换 Gamma/EOTF Tab 失败: {str(e)}")
def update_sidebar_selection(self):
"""更新侧边栏按钮的选中状态"""
# 重置所有按钮样式为默认
self.screen_module_btn.configure(style="Sidebar.TButton")
self.sdr_movie_btn.configure(style="Sidebar.TButton")
self.hdr_movie_btn.configure(style="Sidebar.TButton")
# 设置当前选中按钮的样式
current_type = self.test_type_var.get()
if current_type == "screen_module":
self.screen_module_btn.configure(style="SidebarSelected.TButton")
elif current_type == "sdr_movie":
self.sdr_movie_btn.configure(style="SidebarSelected.TButton")
elif current_type == "hdr_movie":
self.hdr_movie_btn.configure(style="SidebarSelected.TButton")
def on_test_type_change(self):
"""根据测试类型更新内容区域"""
test_type = self.test_type_var.get()
# 获取当前测试类型的配置
if hasattr(self, "config") and hasattr(self.config, "get_current_config"):
current_config = self.config.get_current_config()
# 更新配置信息显示
self.update_config_info_display()
# SDR 选中时显示客户模版按钮
self.update_custom_button_visibility()
def update_custom_button_visibility(self):
"""只在 SDR 测试时显示客户模版按钮"""
if not hasattr(self, "custom_btn") or not hasattr(self, "test_type_var"):
return
if self.test_type_var.get() == "sdr_movie":
if not self.custom_btn.winfo_manager():
self.custom_btn.pack(side=tk.LEFT, padx=5)
else:
if self.custom_btn.winfo_manager():
self.custom_btn.pack_forget()
def start_test(self):
"""开始测试"""
# 检查设备连接状态
if self.ca is None or self.ucd is None:
messagebox.showerror("错误", "请先连接CA410和信号发生器")
return
# 检查是否已经在测试中
if self.testing:
messagebox.showinfo("提示", "测试已在进行中")
return
# ✅ 禁用并隐藏单步调试
if hasattr(self, "debug_panel"):
self.debug_panel.disable_all_debug()
self.log_gui.log("✓ 单步调试已禁用")
if hasattr(self, "debug_container"):
self.debug_container.pack_forget()
self.log_gui.log("✓ 单步调试面板已隐藏")
# 获取测试类型和测试项目
test_type = self.test_type_var.get()
test_items = self.get_selected_test_items()
if not test_items:
messagebox.showinfo("提示", "请至少选择一个测试项目")
return
# 自动收起配置项
if hasattr(self, "config_panel_frame"):
try:
if self.config_panel_frame.winfo_viewable():
self.config_panel_frame.btn.invoke()
self.root.update_idletasks()
time.sleep(0.2)
except:
pass
# 禁用配置项按钮
try:
self.config_panel_frame.btn.configure(state="disabled")
except:
pass
# ✅ 新增:禁用色域参考标准下拉框
try:
if hasattr(self, "screen_gamut_combo"):
self.screen_gamut_combo.configure(state="disabled")
if hasattr(self, "sdr_gamut_combo"):
self.sdr_gamut_combo.configure(state="disabled")
if hasattr(self, "hdr_gamut_combo"):
self.hdr_gamut_combo.configure(state="disabled")
except Exception as e:
self.log_gui.log(f"禁用色域参考标准失败: {str(e)}")
# 隐藏所有重新计算按钮
if hasattr(self, "recalc_cct_btn"):
try:
self.recalc_cct_btn.grid_remove()
except:
pass
if hasattr(self, "sdr_recalc_cct_btn"):
try:
self.sdr_recalc_cct_btn.grid_remove()
except:
pass
if hasattr(self, "hdr_recalc_cct_btn"):
try:
self.hdr_recalc_cct_btn.grid_remove()
except:
pass
# 更新UI状态
self.testing = True
self.start_btn.config(state=tk.DISABLED)
self.stop_btn.config(state=tk.NORMAL)
self.save_btn.config(state=tk.DISABLED)
self.clear_config_btn.config(state=tk.DISABLED)
self.status_var.set("测试进行中...")
# 清空日志和图表
self.log_gui.clear_log()
self.clear_chart()
# 根据测试类型显示不同提示
if test_type == "screen_module":
# 屏模组测试:提示 byPass All PQ
message = f"开始屏模组性能测试,请 byPass All PQ"
elif test_type == "sdr_movie":
# SDR测试提示设置正确图像模式
message = f"开始 SDR Movie 测试,请设置正确的图像模式"
elif test_type == "hdr_movie":
# HDR测试提示设置正确图像模式
message = f"开始 HDR Movie 测试,请设置正确的图像模式"
else:
message = f"开始{self.get_test_type_name(test_type)}测试"
confirm = messagebox.askyesno("确认测试", message)
if not confirm:
self.testing = False
self.start_btn.config(state=tk.NORMAL)
self.stop_btn.config(state=tk.DISABLED)
self.clear_config_btn.config(state=tk.NORMAL)
self.status_var.set("测试已取消")
# 恢复配置项按钮
if hasattr(self, "config_panel_frame"):
try:
self.config_panel_frame.btn.configure(state="normal")
except:
pass
return
# 在新线程中执行测试
self.test_thread = threading.Thread(
target=self.run_test, args=(test_type, test_items)
)
self.test_thread.daemon = True
self.test_thread.start()
def stop_test(self):
"""停止测试 - 放弃本次所有数据(完全集成版)"""
if not self.testing:
return
# ========== 1. 添加确认对话框 ==========
confirm = messagebox.askyesno(
"确认停止测试",
"测试正在进行中,确定要停止吗?\n\n⚠️ 停止后将放弃本次测试的所有数据,无法保存。",
icon="warning",
)
if not confirm:
self.log_gui.log("用户取消停止操作")
return
# ========== 2. 立即设置停止标志 ==========
self.testing = False # ← 关键:先设置标志,让测试线程停止
self.log_gui.log("=" * 50)
self.log_gui.log("⚠️ 正在停止测试...")
self.log_gui.log("=" * 50)
# ========== 3. 立即更新UI状态让用户感知到停止==========
self.stop_btn.config(state=tk.DISABLED)
self.status_var.set("正在停止测试,请稍候...")
self.root.update() # 立即刷新界面
# ========== 4. 等待测试线程结束 ==========
if self.test_thread and self.test_thread.is_alive():
self.log_gui.log("等待测试线程结束...")
# 等待最多5秒
for i in range(50): # 50 * 0.1秒 = 5秒
if not self.test_thread.is_alive():
break
time.sleep(0.1)
self.root.update() # 保持界面响应
if self.test_thread.is_alive():
self.log_gui.log("⚠️ 测试线程未能正常结束,将在后台继续等待")
else:
self.log_gui.log("✓ 测试线程已结束")
# ========== 5. 延迟1秒后执行清理使用内部函数==========
def cleanup_and_finish():
"""清理数据并完成停止操作"""
# ========== 5.1 清理测试数据 ==========
try:
self.log_gui.log("清理测试数据...")
# 清空测试结果对象
if hasattr(self, "results"):
self.results = None
self.log_gui.log(" ✓ 测试结果对象已清空")
# 清空中间数据缓存
for attr in [
"gamut_results",
"gamma_results",
"cct_results",
"contrast_results",
"accuracy_results",
]:
if hasattr(self, attr):
setattr(self, attr, None)
self.log_gui.log(" ✓ 所有中间数据已清空")
except Exception as e:
self.log_gui.log(f"⚠️ 清理数据时出错: {str(e)}")
# ========== 5.2 清空图表显示 ==========
try:
self.clear_chart()
self.log_gui.log("✓ 图表已清空")
except Exception as e:
self.log_gui.log(f"⚠️ 清空图表时出错: {str(e)}")
try:
self.clear_custom_template_results()
self.log_gui.log("✓ 客户模板结果表格已清空")
except Exception as e:
self.log_gui.log(f"⚠️ 清空客户模板结果表格失败: {str(e)}")
# ========== 5.2.5 跳转到色域图Tab第一个Tab==========
try:
if hasattr(self, "chart_notebook"):
self.chart_notebook.select(self.gamut_chart_frame)
self.root.update_idletasks() # ← 刷新界面
self.log_gui.log("✓ 已跳转到色域图界面")
except Exception as e:
self.log_gui.log(f"⚠️ 跳转到色域图失败: {str(e)}")
# ========== 5.3 更新UI状态 ==========
self.set_custom_result_table_locked(False)
self.start_btn.config(state=tk.NORMAL)
self.stop_btn.config(state=tk.DISABLED)
self.save_btn.config(state=tk.DISABLED)
self.clear_config_btn.config(state=tk.NORMAL)
if hasattr(self, "custom_btn"):
self.custom_btn.config(state=tk.NORMAL)
self.status_var.set("测试已停止 - 数据已清空")
self.log_gui.log("✓ UI状态已更新")
# ========== 5.4 恢复配置项按钮 ==========
if hasattr(self, "config_panel_frame"):
try:
self.config_panel_frame.btn.configure(state="normal")
self.log_gui.log("✓ 配置项已恢复")
except:
pass
# ========== 5.4.5 禁用色域参考标准下拉框 ==========
try:
if hasattr(self, "screen_gamut_combo"):
self.screen_gamut_combo.configure(state="disabled")
if hasattr(self, "sdr_gamut_combo"):
self.sdr_gamut_combo.configure(state="disabled")
if hasattr(self, "hdr_gamut_combo"):
self.hdr_gamut_combo.configure(state="disabled")
self.log_gui.log("✓ 色域参考标准已禁用")
except Exception as e:
self.log_gui.log(f"禁用色域参考标准失败: {str(e)}")
# ========== 5.5 隐藏所有重新计算按钮 ==========
try:
button_hidden_count = 0
for btn_attr in [
"recalc_cct_btn",
"sdr_recalc_cct_btn",
"hdr_recalc_cct_btn",
"recalc_gamut_btn", # ✅ 新增
"sdr_recalc_gamut_btn", # ✅ 新增
"hdr_recalc_gamut_btn", # ✅ 新增
]:
if hasattr(self, btn_attr):
try:
getattr(self, btn_attr).grid_remove()
button_hidden_count += 1
except:
pass
if button_hidden_count > 0:
self.log_gui.log(f"✓ 已隐藏 {button_hidden_count} 个重新计算按钮")
except Exception as e:
self.log_gui.log(f"⚠️ 隐藏按钮时出错: {str(e)}")
# ========== 5.6 禁用并隐藏单步调试 ==========
if hasattr(self, "debug_panel"):
try:
self.debug_panel.disable_all_debug()
self.log_gui.log("✓ 单步调试已禁用")
except Exception as e:
self.log_gui.log(f"⚠️ 禁用单步调试失败: {str(e)}")
# ✅ 隐藏调试面板
if hasattr(self, "debug_container"):
try:
self.debug_container.pack_forget()
self.log_gui.log("✓ 单步调试面板已隐藏")
except Exception as e:
self.log_gui.log(f"⚠️ 隐藏调试面板失败: {str(e)}")
# ========== 5.7 最终日志 ==========
self.log_gui.log("=" * 50)
self.log_gui.log("✓ 测试已停止,所有数据已清空")
self.log_gui.log("=" * 50)
# ========== 5.8 显示提示信息 ==========
messagebox.showinfo(
"测试已停止",
"测试已停止,本次测试数据已清空。\n\n可以重新开始新的测试。",
)
# ========== 延迟1秒后执行清理 ==========
self.root.after(1000, cleanup_and_finish)
def save_results(self):
"""保存测试结果(图片 + Excel"""
save_dir = filedialog.askdirectory(title="选择保存测试结果的目录")
if not save_dir:
return
try:
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
test_type = self.get_test_type_name(self.test_type_var.get())
result_dir = os.path.join(save_dir, f"{test_type}_{timestamp}")
os.makedirs(result_dir, exist_ok=True)
# ========== ✅ 获取当前测试类型和已选测试项 ==========
current_test_type = self.test_type_var.get()
selected_items = self.get_selected_test_items()
self.log_gui.log(f"保存测试类型: {current_test_type}")
self.log_gui.log(f"已选测试项: {selected_items}")
# ========== 保存图片 ==========
if "gamut" in selected_items and hasattr(self, "gamut_fig"):
gamut_path = os.path.join(result_dir, "色域测试结果.png")
self.gamut_fig.savefig(gamut_path, dpi=300)
self.log_gui.log(f"✓ 已保存: 色域测试结果.png")
if current_test_type in ["screen_module", "sdr_movie"]:
if "gamma" in selected_items and hasattr(self, "gamma_fig"):
gamma_path = os.path.join(result_dir, "Gamma曲线测试结果.png")
self.gamma_fig.savefig(gamma_path, dpi=300)
self.log_gui.log(f"✓ 已保存: Gamma曲线测试结果.png")
if current_test_type == "hdr_movie":
if "eotf" in selected_items and hasattr(self, "eotf_fig"):
eotf_path = os.path.join(result_dir, "EOTF曲线测试结果.png")
self.eotf_fig.savefig(eotf_path, dpi=300)
self.log_gui.log(f"✓ 已保存: EOTF曲线测试结果.png")
if "cct" in selected_items and hasattr(self, "cct_fig"):
cct_path = os.path.join(result_dir, "色度一致性测试结果.png")
self.cct_fig.savefig(cct_path, dpi=300)
self.log_gui.log(f"✓ 已保存: 色度一致性测试结果.png")
if "contrast" in selected_items and hasattr(self, "contrast_fig"):
contrast_path = os.path.join(result_dir, "对比度测试结果.png")
self.contrast_fig.savefig(contrast_path, dpi=300, bbox_inches="tight")
self.log_gui.log(f"✓ 已保存: 对比度测试结果.png")
if current_test_type in ["sdr_movie", "hdr_movie"]:
if "accuracy" in selected_items and hasattr(self, "accuracy_fig"):
accuracy_path = os.path.join(result_dir, "色准测试结果.png")
self.accuracy_fig.savefig(accuracy_path, dpi=300)
self.log_gui.log(f"✓ 已保存: 色准测试结果.png")
# ========== ✅ 屏模组测试 Excel 导出 ==========
if (
current_test_type == "screen_module"
and hasattr(self, "results")
and self.results
):
try:
import openpyxl
from openpyxl.styles import (
Font,
Alignment,
PatternFill,
Border,
Side,
)
self.log_gui.log("=" * 60)
self.log_gui.log("开始生成屏模组 Excel 数据报告...")
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "测试数据"
# ========== 样式定义 ==========
title_font = Font(
name="微软雅黑", size=16, bold=True, color="FFFFFF"
)
title_fill = PatternFill(
start_color="4472C4", end_color="4472C4", fill_type="solid"
)
title_alignment = Alignment(horizontal="center", vertical="center")
section_font = Font(
name="微软雅黑", size=13, bold=True, color="FFFFFF"
)
section_fill = PatternFill(
start_color="5B9BD5", end_color="5B9BD5", fill_type="solid"
)
section_alignment = Alignment(
horizontal="center", vertical="center"
)
header_font = Font(
name="微软雅黑", size=10, bold=True, color="FFFFFF"
)
header_fill = PatternFill(
start_color="70AD47", end_color="70AD47", fill_type="solid"
)
header_alignment = Alignment(
horizontal="center", vertical="center", wrap_text=True
)
data_font = Font(name="微软雅黑", size=10)
data_alignment = Alignment(horizontal="center", vertical="center")
label_font = Font(name="微软雅黑", size=10, bold=True)
thin_border = Border(
left=Side(style="thin"),
right=Side(style="thin"),
top=Side(style="thin"),
bottom=Side(style="thin"),
)
# ========== 总标题 ==========
ws.merge_cells("A1:G1")
ws["A1"] = "屏模组性能测试数据报告"
ws["A1"].font = title_font
ws["A1"].fill = title_fill
ws["A1"].alignment = title_alignment
ws.row_dimensions[1].height = 35
# ========== 测试基本信息 ==========
row = 3
ws.merge_cells(f"A{row}:B{row}")
ws[f"A{row}"] = "📋 测试基本信息"
ws[f"A{row}"].font = section_font
ws[f"A{row}"].fill = section_fill
ws[f"A{row}"].alignment = section_alignment
ws.row_dimensions[row].height = 25
row += 1
info_items = [
(
"测试时间",
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
),
("测试类型", "屏模组"),
]
for label, value in info_items:
ws[f"A{row}"] = label
ws[f"B{row}"] = value
ws[f"A{row}"].font = label_font
ws[f"B{row}"].font = data_font
ws[f"A{row}"].border = thin_border
ws[f"B{row}"].border = thin_border
row += 1
row += 1 # 空行
# ========== 1. 色域数据 ==========
if "gamut" in selected_items:
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
gamut_final_result = None
if "gamut" in self.results.test_items:
gamut_final_result = self.results.test_items[
"gamut"
].final_result
if rgb_data and len(rgb_data) >= 3:
# 分区标题
ws.merge_cells(f"A{row}:G{row}")
ws[f"A{row}"] = "🎨 色域测试数据"
ws[f"A{row}"].font = section_font
ws[f"A{row}"].fill = section_fill
ws[f"A{row}"].alignment = section_alignment
ws.row_dimensions[row].height = 25
row += 1
if gamut_final_result:
# 第一行:参考标准
ws[f"A{row}"] = "参考标准"
ws[f"B{row}"] = gamut_final_result.get(
"reference", "DCI-P3"
)
ws[f"A{row}"].font = label_font
ws[f"B{row}"].font = data_font
ws[f"A{row}"].border = thin_border
ws[f"B{row}"].border = thin_border
row += 1
# 第二行XY 覆盖率 | UV 覆盖率
xy_coverage = gamut_final_result.get("coverage", 0)
uv_coverage = (
gamut_final_result.get("uv_coverage", 0)
or gamut_final_result.get("uv_space_coverage", 0)
or gamut_final_result.get("coverage_uv", 0)
or 0
)
ws[f"A{row}"] = "XY 色域覆盖率"
ws[f"B{row}"] = f"{xy_coverage:.2f}%"
ws[f"C{row}"] = "UV 色域覆盖率"
ws[f"D{row}"] = f"{uv_coverage:.2f}%"
ws[f"A{row}"].font = label_font
ws[f"B{row}"].font = data_font
ws[f"C{row}"].font = label_font
ws[f"D{row}"].font = data_font
for col in ["A", "B", "C", "D"]:
ws[f"{col}{row}"].border = thin_border
row += 1
# RGB 数据表格
headers = [
"点位",
"x 坐标",
"y 坐标",
"亮度 (cd/m²)",
"",
"",
"",
]
for col_idx, header in enumerate(headers, start=1):
cell = ws.cell(row=row, column=col_idx)
cell.value = header
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
row += 1
rgb_labels = ["Red", "Green", "Blue"]
for i, result in enumerate(rgb_data[:3]):
x, y, lv = result[0], result[1], result[2]
ws[f"A{row}"] = rgb_labels[i]
ws[f"B{row}"] = x
ws[f"C{row}"] = y
ws[f"D{row}"] = lv
ws[f"A{row}"].font = data_font
ws[f"A{row}"].alignment = data_alignment
ws[f"A{row}"].border = thin_border
ws[f"B{row}"].number_format = "0.0000"
ws[f"B{row}"].font = data_font
ws[f"B{row}"].alignment = data_alignment
ws[f"B{row}"].border = thin_border
ws[f"C{row}"].number_format = "0.0000"
ws[f"C{row}"].font = data_font
ws[f"C{row}"].alignment = data_alignment
ws[f"C{row}"].border = thin_border
ws[f"D{row}"].number_format = "0.00"
ws[f"D{row}"].font = data_font
ws[f"D{row}"].alignment = data_alignment
ws[f"D{row}"].border = thin_border
row += 1
row += 1 # 空行
self.log_gui.log(" ✓ 添加色域数据")
# ========== 2. Gamma 数据 ==========
if "gamma" in selected_items:
gray_data = self.results.get_intermediate_data("shared", "gray")
if not gray_data:
gray_data = self.results.get_intermediate_data(
"gamma", "gray"
)
gamma_final_result = None
if "gamma" in self.results.test_items:
gamma_final_result = self.results.test_items[
"gamma"
].final_result
if gray_data and len(gray_data) > 0 and gamma_final_result:
gamma_list = gamma_final_result.get("gamma", [])
L_bar_list = gamma_final_result.get("L_bar", [])
# 分区标题
ws.merge_cells(f"A{row}:G{row}")
ws[f"A{row}"] = "📊 Gamma 曲线数据"
ws[f"A{row}"].font = section_font
ws[f"A{row}"].fill = section_fill
ws[f"A{row}"].alignment = section_alignment
ws.row_dimensions[row].height = 25
row += 1
# Gamma 统计信息
valid_gamma = []
if gamma_list:
for item in gamma_list:
if (
isinstance(item, (list, tuple))
and len(item) >= 4
):
gamma_val = item[3]
if 0.5 < gamma_val < 5.0:
valid_gamma.append(gamma_val)
if valid_gamma:
avg_gamma = sum(valid_gamma) / len(valid_gamma)
max_gamma = max(valid_gamma)
min_gamma = min(valid_gamma)
ws[f"A{row}"] = "平均 Gamma"
ws[f"B{row}"] = f"{avg_gamma:.3f}"
ws[f"C{row}"] = "最大 Gamma"
ws[f"D{row}"] = f"{max_gamma:.3f}"
ws[f"E{row}"] = "最小 Gamma"
ws[f"F{row}"] = f"{min_gamma:.3f}"
for col in ["A", "B", "C", "D", "E", "F"]:
ws[f"{col}{row}"].font = (
label_font
if col in ["A", "C", "E"]
else data_font
)
ws[f"{col}{row}"].border = thin_border
row += 1
# Gamma 数据表格
headers = [
"灰阶 (%)",
"x 坐标",
"y 坐标",
"实测亮度\n(cd/m²)",
"归一化亮度\n(L_bar)",
"Gamma 值",
"",
]
for col_idx, header in enumerate(headers, start=1):
cell = ws.cell(row=row, column=col_idx)
cell.value = header
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
row += 1
total_points = len(gray_data)
for i in range(total_points - 1, -1, -1):
gray_level = (
100 - int(i * 100 / (total_points - 1))
if total_points > 1
else 0
)
x, y, lv = (
gray_data[i][0],
gray_data[i][1],
gray_data[i][2],
)
L_bar_val = L_bar_list[i] if i < len(L_bar_list) else 0
gamma_val = None
if (
i < len(gamma_list)
and isinstance(gamma_list[i], (list, tuple))
and len(gamma_list[i]) >= 4
):
gamma_val = gamma_list[i][3]
ws[f"A{row}"] = gray_level
ws[f"B{row}"] = x
ws[f"C{row}"] = y
ws[f"D{row}"] = lv
ws[f"E{row}"] = L_bar_val
if gamma_val is not None and 0.5 < gamma_val < 5.0:
ws[f"F{row}"] = gamma_val
ws[f"F{row}"].number_format = "0.000"
else:
ws[f"F{row}"] = "N/A"
ws[f"A{row}"].number_format = "0"
ws[f"B{row}"].number_format = "0.0000"
ws[f"C{row}"].number_format = "0.0000"
ws[f"D{row}"].number_format = "0.00"
ws[f"E{row}"].number_format = "0.0000"
for col in ["A", "B", "C", "D", "E", "F"]:
ws[f"{col}{row}"].font = data_font
ws[f"{col}{row}"].alignment = data_alignment
ws[f"{col}{row}"].border = thin_border
row += 1
row += 1
self.log_gui.log(" ✓ 添加 Gamma 数据")
# ========== 3. 色度一致性数据 ==========
if "cct" in selected_items:
gray_data = self.results.get_intermediate_data("shared", "gray")
if not gray_data:
gray_data = self.results.get_intermediate_data(
"cct", "gray"
)
if gray_data and len(gray_data) > 1:
gray_data_no_black = gray_data[:-1]
# 分区标题
ws.merge_cells(f"A{row}:G{row}")
ws[f"A{row}"] = "🌈 色度一致性数据"
ws[f"A{row}"].font = section_font
ws[f"A{row}"].fill = section_fill
ws[f"A{row}"].alignment = section_alignment
ws.row_dimensions[row].height = 25
row += 1
# 色度波动信息
x_coords = [d[0] for d in gray_data_no_black]
y_coords = [d[1] for d in gray_data_no_black]
ws[f"A{row}"] = "x 坐标范围"
ws[f"B{row}"] = f"{min(x_coords):.4f} ~ {max(x_coords):.4f}"
ws[f"C{row}"] = "y 坐标范围"
ws[f"D{row}"] = f"{min(y_coords):.4f} ~ {max(y_coords):.4f}"
for col in ["A", "B", "C", "D"]:
ws[f"{col}{row}"].font = (
label_font if col in ["A", "C"] else data_font
)
ws[f"{col}{row}"].border = thin_border
row += 1
# 数据表格
headers = [
"灰阶 (%)",
"x 坐标",
"y 坐标",
"亮度 (cd/m²)",
"",
"",
"",
]
for col_idx, header in enumerate(headers, start=1):
cell = ws.cell(row=row, column=col_idx)
cell.value = header
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
row += 1
total_points = len(gray_data)
for i in range(len(gray_data_no_black) - 1, -1, -1):
x, y, lv = (
gray_data_no_black[i][0],
gray_data_no_black[i][1],
gray_data_no_black[i][2],
)
gray_level = (
100 - int(i * 100 / (total_points - 1))
if total_points > 1
else 0
)
ws[f"A{row}"] = gray_level
ws[f"B{row}"] = x
ws[f"C{row}"] = y
ws[f"D{row}"] = lv
ws[f"A{row}"].number_format = "0"
ws[f"B{row}"].number_format = "0.0000"
ws[f"C{row}"].number_format = "0.0000"
ws[f"D{row}"].number_format = "0.00"
for col in ["A", "B", "C", "D"]:
ws[f"{col}{row}"].font = data_font
ws[f"{col}{row}"].alignment = data_alignment
ws[f"{col}{row}"].border = thin_border
row += 1
row += 1
self.log_gui.log(" ✓ 添加色度一致性数据")
# ========== 4. 对比度数据 ==========
if "contrast" in selected_items:
contrast_final_result = None
if "contrast" in self.results.test_items:
contrast_final_result = self.results.test_items[
"contrast"
].final_result
if contrast_final_result:
# 分区标题
ws.merge_cells(f"A{row}:G{row}")
ws[f"A{row}"] = "⚫⚪ 对比度测试数据"
ws[f"A{row}"].font = section_font
ws[f"A{row}"].fill = section_fill
ws[f"A{row}"].alignment = section_alignment
ws.row_dimensions[row].height = 25
row += 1
max_lv = contrast_final_result.get("max_luminance", 0)
min_lv = contrast_final_result.get("min_luminance", 0)
contrast_ratio = contrast_final_result.get(
"contrast_ratio", 0
)
info_items = [
("最大亮度(白场)", f"{max_lv:.2f} cd/m²"),
("最小亮度(黑场)", f"{min_lv:.4f} cd/m²"),
("对比度", f"{contrast_ratio:.0f}:1"),
]
for label, value in info_items:
ws[f"A{row}"] = label
ws[f"B{row}"] = value
ws[f"A{row}"].font = label_font
ws[f"B{row}"].font = data_font
ws[f"A{row}"].border = thin_border
ws[f"B{row}"].border = thin_border
row += 1
self.log_gui.log(" ✓ 添加对比度数据")
# ========== 调整列宽 ==========
ws.column_dimensions["A"].width = 18
ws.column_dimensions["B"].width = 18
ws.column_dimensions["C"].width = 18
ws.column_dimensions["D"].width = 18
ws.column_dimensions["E"].width = 18
ws.column_dimensions["F"].width = 15
ws.column_dimensions["G"].width = 15
# ========== 保存 Excel ==========
excel_path = os.path.join(result_dir, "测试数据.xlsx")
wb.save(excel_path)
self.log_gui.log(f"✓ 已保存: 测试数据.xlsx")
self.log_gui.log("=" * 60)
except ImportError:
self.log_gui.log("⚠️ 未安装 openpyxl 库,跳过 Excel 导出")
self.log_gui.log(" 安装方法: pip install openpyxl")
except Exception as e:
self.log_gui.log(f"⚠️ Excel 导出失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
# ========== ✅ SDR Movie 测试 Excel 导出 ==========
elif (
current_test_type == "sdr_movie"
and hasattr(self, "results")
and self.results
):
try:
import openpyxl
from openpyxl.styles import (
Font,
Alignment,
PatternFill,
Border,
Side,
)
self.log_gui.log("=" * 60)
self.log_gui.log("开始生成 SDR Movie Excel 数据报告...")
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "测试数据"
# ========== 样式定义 ==========
title_font = Font(
name="微软雅黑", size=16, bold=True, color="FFFFFF"
)
title_fill = PatternFill(
start_color="4472C4", end_color="4472C4", fill_type="solid"
)
title_alignment = Alignment(horizontal="center", vertical="center")
section_font = Font(
name="微软雅黑", size=13, bold=True, color="FFFFFF"
)
section_fill = PatternFill(
start_color="5B9BD5", end_color="5B9BD5", fill_type="solid"
)
section_alignment = Alignment(
horizontal="center", vertical="center"
)
header_font = Font(
name="微软雅黑", size=10, bold=True, color="FFFFFF"
)
header_fill = PatternFill(
start_color="70AD47", end_color="70AD47", fill_type="solid"
)
header_alignment = Alignment(
horizontal="center", vertical="center", wrap_text=True
)
data_font = Font(name="微软雅黑", size=10)
data_alignment = Alignment(horizontal="center", vertical="center")
label_font = Font(name="微软雅黑", size=10, bold=True)
thin_border = Border(
left=Side(style="thin"),
right=Side(style="thin"),
top=Side(style="thin"),
bottom=Side(style="thin"),
)
# ========== 总标题 ==========
ws.merge_cells("A1:G1")
ws["A1"] = "SDR Movie 性能测试数据报告"
ws["A1"].font = title_font
ws["A1"].fill = title_fill
ws["A1"].alignment = title_alignment
ws.row_dimensions[1].height = 35
# ========== 测试基本信息 ==========
row = 3
ws.merge_cells(f"A{row}:B{row}")
ws[f"A{row}"] = "📋 测试基本信息"
ws[f"A{row}"].font = section_font
ws[f"A{row}"].fill = section_fill
ws[f"A{row}"].alignment = section_alignment
ws.row_dimensions[row].height = 25
row += 1
info_items = [
(
"测试时间",
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
),
("测试类型", "SDR Movie"),
]
for label, value in info_items:
ws[f"A{row}"] = label
ws[f"B{row}"] = value
ws[f"A{row}"].font = label_font
ws[f"B{row}"].font = data_font
ws[f"A{row}"].border = thin_border
ws[f"B{row}"].border = thin_border
row += 1
row += 1 # 空行
# ========== 1. 色域数据 ==========
if "gamut" in selected_items:
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
gamut_final_result = None
if "gamut" in self.results.test_items:
gamut_final_result = self.results.test_items[
"gamut"
].final_result
if rgb_data and len(rgb_data) >= 3:
ws.merge_cells(f"A{row}:G{row}")
ws[f"A{row}"] = "🎨 色域测试数据"
ws[f"A{row}"].font = section_font
ws[f"A{row}"].fill = section_fill
ws[f"A{row}"].alignment = section_alignment
ws.row_dimensions[row].height = 25
row += 1
if gamut_final_result:
xy_coverage = gamut_final_result.get("coverage", 0)
uv_coverage = (
gamut_final_result.get("uv_coverage", 0)
or gamut_final_result.get("uv_space_coverage", 0)
or 0
)
ws[f"A{row}"] = "参考标准"
ws[f"B{row}"] = gamut_final_result.get(
"reference", "DCI-P3"
)
ws[f"A{row}"].font = label_font
ws[f"B{row}"].font = data_font
ws[f"A{row}"].border = thin_border
ws[f"B{row}"].border = thin_border
row += 1
ws[f"A{row}"] = "XY 色域覆盖率"
ws[f"B{row}"] = f"{xy_coverage:.2f}%"
ws[f"C{row}"] = "UV 色域覆盖率"
ws[f"D{row}"] = f"{uv_coverage:.2f}%"
for col in ["A", "B", "C", "D"]:
ws[f"{col}{row}"].font = (
label_font if col in ["A", "C"] else data_font
)
ws[f"{col}{row}"].border = thin_border
row += 1
# RGB 数据表格
headers = [
"点位",
"x 坐标",
"y 坐标",
"亮度 (cd/m²)",
"",
"",
"",
]
for col_idx, header in enumerate(headers, start=1):
cell = ws.cell(row=row, column=col_idx)
cell.value = header
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
row += 1
rgb_labels = ["Red", "Green", "Blue"]
for i, result in enumerate(rgb_data[:3]):
x, y, lv = result[0], result[1], result[2]
ws[f"A{row}"] = rgb_labels[i]
ws[f"B{row}"] = x
ws[f"C{row}"] = y
ws[f"D{row}"] = lv
ws[f"B{row}"].number_format = "0.0000"
ws[f"C{row}"].number_format = "0.0000"
ws[f"D{row}"].number_format = "0.00"
for col in ["A", "B", "C", "D"]:
ws[f"{col}{row}"].font = data_font
ws[f"{col}{row}"].alignment = data_alignment
ws[f"{col}{row}"].border = thin_border
row += 1
row += 1
self.log_gui.log(" ✓ 添加色域数据")
# ========== 2. Gamma 数据 ==========
if "gamma" in selected_items:
gray_data = self.results.get_intermediate_data("shared", "gray")
if not gray_data:
gray_data = self.results.get_intermediate_data(
"gamma", "gray"
)
gamma_final_result = None
if "gamma" in self.results.test_items:
gamma_final_result = self.results.test_items[
"gamma"
].final_result
if gray_data and gamma_final_result:
gamma_list = gamma_final_result.get("gamma", [])
L_bar_list = gamma_final_result.get("L_bar", [])
ws.merge_cells(f"A{row}:G{row}")
ws[f"A{row}"] = "📊 Gamma 曲线数据"
ws[f"A{row}"].font = section_font
ws[f"A{row}"].fill = section_fill
ws[f"A{row}"].alignment = section_alignment
ws.row_dimensions[row].height = 25
row += 1
# Gamma 统计
valid_gamma = [
item[3]
for item in gamma_list
if isinstance(item, (list, tuple))
and len(item) >= 4
and 0.5 < item[3] < 5.0
]
if valid_gamma:
avg_gamma = sum(valid_gamma) / len(valid_gamma)
ws[f"A{row}"] = "平均 Gamma"
ws[f"B{row}"] = f"{avg_gamma:.3f}"
ws[f"C{row}"] = "最大 Gamma"
ws[f"D{row}"] = f"{max(valid_gamma):.3f}"
ws[f"E{row}"] = "最小 Gamma"
ws[f"F{row}"] = f"{min(valid_gamma):.3f}"
for col in ["A", "B", "C", "D", "E", "F"]:
ws[f"{col}{row}"].font = (
label_font
if col in ["A", "C", "E"]
else data_font
)
ws[f"{col}{row}"].border = thin_border
row += 1
# Gamma 数据表格
headers = [
"灰阶 (%)",
"x 坐标",
"y 坐标",
"实测亮度\n(cd/m²)",
"归一化亮度\n(L_bar)",
"Gamma 值",
"",
]
for col_idx, header in enumerate(headers, start=1):
cell = ws.cell(row=row, column=col_idx)
cell.value = header
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
row += 1
total_points = len(gray_data)
for i in range(total_points - 1, -1, -1):
gray_level = (
100 - int(i * 100 / (total_points - 1))
if total_points > 1
else 0
)
x, y, lv = (
gray_data[i][0],
gray_data[i][1],
gray_data[i][2],
)
L_bar_val = L_bar_list[i] if i < len(L_bar_list) else 0
gamma_val = None
if (
i < len(gamma_list)
and isinstance(gamma_list[i], (list, tuple))
and len(gamma_list[i]) >= 4
):
gamma_val = gamma_list[i][3]
ws[f"A{row}"] = gray_level
ws[f"B{row}"] = x
ws[f"C{row}"] = y
ws[f"D{row}"] = lv
ws[f"E{row}"] = L_bar_val
if gamma_val is not None and 0.5 < gamma_val < 5.0:
ws[f"F{row}"] = gamma_val
ws[f"F{row}"].number_format = "0.000"
else:
ws[f"F{row}"] = "N/A"
ws[f"A{row}"].number_format = "0"
ws[f"B{row}"].number_format = "0.0000"
ws[f"C{row}"].number_format = "0.0000"
ws[f"D{row}"].number_format = "0.00"
ws[f"E{row}"].number_format = "0.0000"
for col in ["A", "B", "C", "D", "E", "F"]:
ws[f"{col}{row}"].font = data_font
ws[f"{col}{row}"].alignment = data_alignment
ws[f"{col}{row}"].border = thin_border
row += 1
row += 1
self.log_gui.log(" ✓ 添加 Gamma 数据")
# ========== 3. 色度一致性数据 ==========
if "cct" in selected_items:
gray_data = self.results.get_intermediate_data("shared", "gray")
if not gray_data:
gray_data = self.results.get_intermediate_data(
"cct", "gray"
)
if gray_data and len(gray_data) > 1:
gray_data_no_black = gray_data[:-1]
ws.merge_cells(f"A{row}:G{row}")
ws[f"A{row}"] = "🌈 色度一致性数据"
ws[f"A{row}"].font = section_font
ws[f"A{row}"].fill = section_fill
ws[f"A{row}"].alignment = section_alignment
ws.row_dimensions[row].height = 25
row += 1
x_coords = [d[0] for d in gray_data_no_black]
y_coords = [d[1] for d in gray_data_no_black]
ws[f"A{row}"] = "x 坐标范围"
ws[f"B{row}"] = f"{min(x_coords):.4f} ~ {max(x_coords):.4f}"
ws[f"C{row}"] = "y 坐标范围"
ws[f"D{row}"] = f"{min(y_coords):.4f} ~ {max(y_coords):.4f}"
for col in ["A", "B", "C", "D"]:
ws[f"{col}{row}"].font = (
label_font if col in ["A", "C"] else data_font
)
ws[f"{col}{row}"].border = thin_border
row += 1
headers = [
"灰阶 (%)",
"x 坐标",
"y 坐标",
"亮度 (cd/m²)",
"",
"",
"",
]
for col_idx, header in enumerate(headers, start=1):
cell = ws.cell(row=row, column=col_idx)
cell.value = header
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
row += 1
total_points = len(gray_data)
for i in range(len(gray_data_no_black) - 1, -1, -1):
x, y, lv = (
gray_data_no_black[i][0],
gray_data_no_black[i][1],
gray_data_no_black[i][2],
)
gray_level = (
100 - int(i * 100 / (total_points - 1))
if total_points > 1
else 0
)
ws[f"A{row}"] = gray_level
ws[f"B{row}"] = x
ws[f"C{row}"] = y
ws[f"D{row}"] = lv
ws[f"A{row}"].number_format = "0"
ws[f"B{row}"].number_format = "0.0000"
ws[f"C{row}"].number_format = "0.0000"
ws[f"D{row}"].number_format = "0.00"
for col in ["A", "B", "C", "D"]:
ws[f"{col}{row}"].font = data_font
ws[f"{col}{row}"].alignment = data_alignment
ws[f"{col}{row}"].border = thin_border
row += 1
row += 1
self.log_gui.log(" ✓ 添加色度一致性数据")
# ========== 4. 对比度数据 ==========
if "contrast" in selected_items:
contrast_final_result = None
if "contrast" in self.results.test_items:
contrast_final_result = self.results.test_items[
"contrast"
].final_result
if contrast_final_result:
ws.merge_cells(f"A{row}:G{row}")
ws[f"A{row}"] = "⚫⚪ 对比度测试数据"
ws[f"A{row}"].font = section_font
ws[f"A{row}"].fill = section_fill
ws[f"A{row}"].alignment = section_alignment
ws.row_dimensions[row].height = 25
row += 1
max_lv = contrast_final_result.get("max_luminance", 0)
min_lv = contrast_final_result.get("min_luminance", 0)
contrast_ratio = contrast_final_result.get(
"contrast_ratio", 0
)
info_items = [
("最大亮度(白场)", f"{max_lv:.2f} cd/m²"),
("最小亮度(黑场)", f"{min_lv:.4f} cd/m²"),
("对比度", f"{contrast_ratio:.0f}:1"),
]
for label, value in info_items:
ws[f"A{row}"] = label
ws[f"B{row}"] = value
ws[f"A{row}"].font = label_font
ws[f"B{row}"].font = data_font
ws[f"A{row}"].border = thin_border
ws[f"B{row}"].border = thin_border
row += 1
row += 1
self.log_gui.log(" ✓ 添加对比度数据")
# ========== 5. 色准数据SDR 特有)==========
if "accuracy" in selected_items:
accuracy_final_result = None
if "accuracy" in self.results.test_items:
accuracy_final_result = self.results.test_items[
"accuracy"
].final_result
if accuracy_final_result:
ws.merge_cells(f"A{row}:G{row}")
ws[f"A{row}"] = "🎯 色准测试数据"
ws[f"A{row}"].font = section_font
ws[f"A{row}"].fill = section_fill
ws[f"A{row}"].alignment = section_alignment
ws.row_dimensions[row].height = 25
row += 1
# 色准统计信息
avg_delta_e = accuracy_final_result.get("avg_delta_e", 0)
max_delta_e = accuracy_final_result.get("max_delta_e", 0)
min_delta_e = accuracy_final_result.get("min_delta_e", 0)
excellent_count = accuracy_final_result.get(
"excellent_count", 0
)
good_count = accuracy_final_result.get("good_count", 0)
poor_count = accuracy_final_result.get("poor_count", 0)
ws[f"A{row}"] = "平均 ΔE"
ws[f"B{row}"] = f"{avg_delta_e:.2f}"
ws[f"C{row}"] = "最大 ΔE"
ws[f"D{row}"] = f"{max_delta_e:.2f}"
ws[f"E{row}"] = "最小 ΔE"
ws[f"F{row}"] = f"{min_delta_e:.2f}"
for col in ["A", "B", "C", "D", "E", "F"]:
ws[f"{col}{row}"].font = (
label_font if col in ["A", "C", "E"] else data_font
)
ws[f"{col}{row}"].border = thin_border
row += 1
# 第二行统计
ws[f"A{row}"] = "优秀 (ΔE<3)"
ws[f"B{row}"] = f"{excellent_count}"
ws[f"C{row}"] = "良好 (3≤ΔE<5)"
ws[f"D{row}"] = f"{good_count}"
ws[f"E{row}"] = "偏差 (ΔE≥5)"
ws[f"F{row}"] = f"{poor_count}"
for col in ["A", "B", "C", "D", "E", "F"]:
ws[f"{col}{row}"].font = (
label_font if col in ["A", "C", "E"] else data_font
)
ws[f"{col}{row}"].border = thin_border
row += 1
# ========== 色准详细数据表格(带 xy 坐标和亮度)==========
color_patches = accuracy_final_result.get(
"color_patches", []
)
delta_e_values = accuracy_final_result.get(
"delta_e_values", []
)
# ✅ 获取原始测量数据(包含 xy 和亮度)
color_measurements = accuracy_final_result.get(
"color_measurements", []
)
if color_patches and delta_e_values:
# 表头
headers = [
"序号",
"颜色名称",
"x 坐标",
"y 坐标",
"亮度 (cd/m²)",
"ΔE 2000",
"等级",
]
for col_idx, header in enumerate(headers, start=1):
cell = ws.cell(row=row, column=col_idx)
cell.value = header
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
row += 1
# 数据行
for idx, (color_name, delta_e) in enumerate(
zip(color_patches, delta_e_values), start=1
):
# 判断等级
if delta_e < 3:
grade = "优秀"
elif delta_e < 5:
grade = "良好"
else:
grade = "偏差"
# ✅ 获取测量数据x, y, 亮度)
x_val = "N/A"
y_val = "N/A"
lv_val = "N/A"
if color_measurements and idx - 1 < len(
color_measurements
):
measurement = color_measurements[idx - 1]
if len(measurement) >= 3:
x_val = measurement[0]
y_val = measurement[1]
lv_val = measurement[2]
ws[f"A{row}"] = idx
ws[f"B{row}"] = color_name
ws[f"C{row}"] = x_val
ws[f"D{row}"] = y_val
ws[f"E{row}"] = lv_val
ws[f"F{row}"] = delta_e
ws[f"G{row}"] = grade
# 数字格式
ws[f"A{row}"].number_format = "0"
if isinstance(x_val, (int, float)):
ws[f"C{row}"].number_format = "0.0000"
if isinstance(y_val, (int, float)):
ws[f"D{row}"].number_format = "0.0000"
if isinstance(lv_val, (int, float)):
ws[f"E{row}"].number_format = "0.00"
ws[f"F{row}"].number_format = "0.00"
for col in ["A", "B", "C", "D", "E", "F", "G"]:
ws[f"{col}{row}"].font = data_font
ws[f"{col}{row}"].alignment = data_alignment
ws[f"{col}{row}"].border = thin_border
row += 1
row += 1
self.log_gui.log(" ✓ 添加色准数据(含 xy 坐标和亮度)")
# ========== 调整列宽 ==========
for col in ["A", "B", "C", "D", "E", "F", "G"]:
ws.column_dimensions[col].width = 18
# ========== 保存 Excel ==========
excel_path = os.path.join(result_dir, "测试数据.xlsx")
wb.save(excel_path)
self.log_gui.log(f"✓ 已保存: 测试数据.xlsx")
self.log_gui.log("=" * 60)
except ImportError:
self.log_gui.log("⚠️ 未安装 openpyxl 库,跳过 Excel 导出")
except Exception as e:
self.log_gui.log(f"⚠️ Excel 导出失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
# ========== ✅ HDR Movie 测试 Excel 导出 ==========
elif (
current_test_type == "hdr_movie"
and hasattr(self, "results")
and self.results
):
try:
import openpyxl
from openpyxl.styles import (
Font,
Alignment,
PatternFill,
Border,
Side,
)
self.log_gui.log("=" * 60)
self.log_gui.log("开始生成 HDR Movie Excel 数据报告...")
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "测试数据"
# ========== 样式定义 ==========
title_font = Font(
name="微软雅黑", size=16, bold=True, color="FFFFFF"
)
title_fill = PatternFill(
start_color="4472C4", end_color="4472C4", fill_type="solid"
)
title_alignment = Alignment(horizontal="center", vertical="center")
section_font = Font(
name="微软雅黑", size=13, bold=True, color="FFFFFF"
)
section_fill = PatternFill(
start_color="5B9BD5", end_color="5B9BD5", fill_type="solid"
)
section_alignment = Alignment(
horizontal="center", vertical="center"
)
header_font = Font(
name="微软雅黑", size=10, bold=True, color="FFFFFF"
)
header_fill = PatternFill(
start_color="70AD47", end_color="70AD47", fill_type="solid"
)
header_alignment = Alignment(
horizontal="center", vertical="center", wrap_text=True
)
data_font = Font(name="微软雅黑", size=10)
data_alignment = Alignment(horizontal="center", vertical="center")
label_font = Font(name="微软雅黑", size=10, bold=True)
thin_border = Border(
left=Side(style="thin"),
right=Side(style="thin"),
top=Side(style="thin"),
bottom=Side(style="thin"),
)
# ========== 总标题 ==========
ws.merge_cells("A1:G1")
ws["A1"] = "HDR Movie 性能测试数据报告"
ws["A1"].font = title_font
ws["A1"].fill = title_fill
ws["A1"].alignment = title_alignment
ws.row_dimensions[1].height = 35
# ========== 测试基本信息 ==========
row = 3
ws.merge_cells(f"A{row}:B{row}")
ws[f"A{row}"] = "📋 测试基本信息"
ws[f"A{row}"].font = section_font
ws[f"A{row}"].fill = section_fill
ws[f"A{row}"].alignment = section_alignment
ws.row_dimensions[row].height = 25
row += 1
info_items = [
(
"测试时间",
datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
),
("测试类型", "HDR Movie"),
]
for label, value in info_items:
ws[f"A{row}"] = label
ws[f"B{row}"] = value
ws[f"A{row}"].font = label_font
ws[f"B{row}"].font = data_font
ws[f"A{row}"].border = thin_border
ws[f"B{row}"].border = thin_border
row += 1
row += 1
# ========== 1. 色域数据 ==========
if "gamut" in selected_items:
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
gamut_final_result = None
if "gamut" in self.results.test_items:
gamut_final_result = self.results.test_items[
"gamut"
].final_result
if rgb_data and len(rgb_data) >= 3:
ws.merge_cells(f"A{row}:G{row}")
ws[f"A{row}"] = "🎨 色域测试数据"
ws[f"A{row}"].font = section_font
ws[f"A{row}"].fill = section_fill
ws[f"A{row}"].alignment = section_alignment
ws.row_dimensions[row].height = 25
row += 1
if gamut_final_result:
xy_coverage = gamut_final_result.get("coverage", 0)
uv_coverage = (
gamut_final_result.get("uv_coverage", 0)
or gamut_final_result.get("uv_space_coverage", 0)
or 0
)
ws[f"A{row}"] = "参考标准"
ws[f"B{row}"] = gamut_final_result.get(
"reference", "DCI-P3"
)
ws[f"A{row}"].font = label_font
ws[f"B{row}"].font = data_font
ws[f"A{row}"].border = thin_border
ws[f"B{row}"].border = thin_border
row += 1
ws[f"A{row}"] = "XY 色域覆盖率"
ws[f"B{row}"] = f"{xy_coverage:.2f}%"
ws[f"C{row}"] = "UV 色域覆盖率"
ws[f"D{row}"] = f"{uv_coverage:.2f}%"
for col in ["A", "B", "C", "D"]:
ws[f"{col}{row}"].font = (
label_font if col in ["A", "C"] else data_font
)
ws[f"{col}{row}"].border = thin_border
row += 1
# RGB 数据表格
headers = [
"点位",
"x 坐标",
"y 坐标",
"亮度 (cd/m²)",
"",
"",
"",
]
for col_idx, header in enumerate(headers, start=1):
cell = ws.cell(row=row, column=col_idx)
cell.value = header
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
row += 1
rgb_labels = ["Red", "Green", "Blue"]
for i, result in enumerate(rgb_data[:3]):
x, y, lv = result[0], result[1], result[2]
ws[f"A{row}"] = rgb_labels[i]
ws[f"B{row}"] = x
ws[f"C{row}"] = y
ws[f"D{row}"] = lv
ws[f"B{row}"].number_format = "0.0000"
ws[f"C{row}"].number_format = "0.0000"
ws[f"D{row}"].number_format = "0.00"
for col in ["A", "B", "C", "D"]:
ws[f"{col}{row}"].font = data_font
ws[f"{col}{row}"].alignment = data_alignment
ws[f"{col}{row}"].border = thin_border
row += 1
row += 1
self.log_gui.log(" ✓ 添加色域数据")
# ========== 2. EOTF 数据HDR 特有)==========
if "eotf" in selected_items:
gray_data = self.results.get_intermediate_data("shared", "gray")
if not gray_data:
gray_data = self.results.get_intermediate_data(
"eotf", "gray"
)
eotf_final_result = None
if "eotf" in self.results.test_items:
eotf_final_result = self.results.test_items[
"eotf"
].final_result
if gray_data and len(gray_data) > 0 and eotf_final_result:
eotf_list = eotf_final_result.get("eotf", [])
L_bar_list = eotf_final_result.get("L_bar", [])
ws.merge_cells(f"A{row}:G{row}")
ws[f"A{row}"] = "📊 EOTF 曲线数据"
ws[f"A{row}"].font = section_font
ws[f"A{row}"].fill = section_fill
ws[f"A{row}"].alignment = section_alignment
ws.row_dimensions[row].height = 25
row += 1
# ✅ EOTF 统计信息(类似 Gamma 统计)
valid_eotf = []
if eotf_list:
for item in eotf_list:
if (
isinstance(item, (list, tuple))
and len(item) >= 4
):
eotf_val = item[3]
if 0.5 < eotf_val < 5.0:
valid_eotf.append(eotf_val)
if valid_eotf:
avg_eotf = sum(valid_eotf) / len(valid_eotf)
max_eotf = max(valid_eotf)
min_eotf = min(valid_eotf)
ws[f"A{row}"] = "平均 EOTF"
ws[f"B{row}"] = f"{avg_eotf:.3f}"
ws[f"C{row}"] = "最大 EOTF"
ws[f"D{row}"] = f"{max_eotf:.3f}"
ws[f"E{row}"] = "最小 EOTF"
ws[f"F{row}"] = f"{min_eotf:.3f}"
for col in ["A", "B", "C", "D", "E", "F"]:
ws[f"{col}{row}"].font = (
label_font
if col in ["A", "C", "E"]
else data_font
)
ws[f"{col}{row}"].border = thin_border
row += 1
# ✅ EOTF 数据表格(与 Gamma 表格完全一致)
headers = [
"灰阶 (%)",
"x 坐标",
"y 坐标",
"实测亮度\n(cd/m²)",
"归一化亮度\n(L_bar)",
"EOTF 值",
"",
]
for col_idx, header in enumerate(headers, start=1):
cell = ws.cell(row=row, column=col_idx)
cell.value = header
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
row += 1
total_points = len(gray_data)
for i in range(total_points - 1, -1, -1):
gray_level = (
100 - int(i * 100 / (total_points - 1))
if total_points > 1
else 0
)
x, y, lv = (
gray_data[i][0],
gray_data[i][1],
gray_data[i][2],
)
L_bar_val = L_bar_list[i] if i < len(L_bar_list) else 0
eotf_val = None
if (
i < len(eotf_list)
and isinstance(eotf_list[i], (list, tuple))
and len(eotf_list[i]) >= 4
):
eotf_val = eotf_list[i][3]
ws[f"A{row}"] = gray_level
ws[f"B{row}"] = x
ws[f"C{row}"] = y
ws[f"D{row}"] = lv
ws[f"E{row}"] = L_bar_val
if eotf_val is not None and 0.5 < eotf_val < 5.0:
ws[f"F{row}"] = eotf_val
ws[f"F{row}"].number_format = "0.000"
else:
ws[f"F{row}"] = "N/A"
ws[f"A{row}"].number_format = "0"
ws[f"B{row}"].number_format = "0.0000"
ws[f"C{row}"].number_format = "0.0000"
ws[f"D{row}"].number_format = "0.00"
ws[f"E{row}"].number_format = "0.0000"
for col in ["A", "B", "C", "D", "E", "F"]:
ws[f"{col}{row}"].font = data_font
ws[f"{col}{row}"].alignment = data_alignment
ws[f"{col}{row}"].border = thin_border
row += 1
row += 1
self.log_gui.log(" ✓ 添加 EOTF 数据")
# ========== 3. 色度一致性数据 ==========
if "cct" in selected_items:
gray_data = self.results.get_intermediate_data("shared", "gray")
if not gray_data:
gray_data = self.results.get_intermediate_data(
"cct", "gray"
)
if gray_data and len(gray_data) > 1:
gray_data_no_black = gray_data[:-1]
ws.merge_cells(f"A{row}:G{row}")
ws[f"A{row}"] = "🌈 色度一致性数据"
ws[f"A{row}"].font = section_font
ws[f"A{row}"].fill = section_fill
ws[f"A{row}"].alignment = section_alignment
ws.row_dimensions[row].height = 25
row += 1
x_coords = [d[0] for d in gray_data_no_black]
y_coords = [d[1] for d in gray_data_no_black]
ws[f"A{row}"] = "x 坐标范围"
ws[f"B{row}"] = f"{min(x_coords):.4f} ~ {max(x_coords):.4f}"
ws[f"C{row}"] = "y 坐标范围"
ws[f"D{row}"] = f"{min(y_coords):.4f} ~ {max(y_coords):.4f}"
for col in ["A", "B", "C", "D"]:
ws[f"{col}{row}"].font = (
label_font if col in ["A", "C"] else data_font
)
ws[f"{col}{row}"].border = thin_border
row += 1
headers = [
"灰阶 (%)",
"x 坐标",
"y 坐标",
"亮度 (cd/m²)",
"",
"",
"",
]
for col_idx, header in enumerate(headers, start=1):
cell = ws.cell(row=row, column=col_idx)
cell.value = header
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
row += 1
total_points = len(gray_data)
for i in range(len(gray_data_no_black) - 1, -1, -1):
x, y, lv = (
gray_data_no_black[i][0],
gray_data_no_black[i][1],
gray_data_no_black[i][2],
)
gray_level = (
100 - int(i * 100 / (total_points - 1))
if total_points > 1
else 0
)
ws[f"A{row}"] = gray_level
ws[f"B{row}"] = x
ws[f"C{row}"] = y
ws[f"D{row}"] = lv
ws[f"A{row}"].number_format = "0"
ws[f"B{row}"].number_format = "0.0000"
ws[f"C{row}"].number_format = "0.0000"
ws[f"D{row}"].number_format = "0.00"
for col in ["A", "B", "C", "D"]:
ws[f"{col}{row}"].font = data_font
ws[f"{col}{row}"].alignment = data_alignment
ws[f"{col}{row}"].border = thin_border
row += 1
row += 1
self.log_gui.log(" ✓ 添加色度一致性数据")
# ========== 4. 对比度数据 ==========
if "contrast" in selected_items:
contrast_final_result = None
if "contrast" in self.results.test_items:
contrast_final_result = self.results.test_items[
"contrast"
].final_result
if contrast_final_result:
ws.merge_cells(f"A{row}:G{row}")
ws[f"A{row}"] = "⚫⚪ 对比度测试数据"
ws[f"A{row}"].font = section_font
ws[f"A{row}"].fill = section_fill
ws[f"A{row}"].alignment = section_alignment
ws.row_dimensions[row].height = 25
row += 1
max_lv = contrast_final_result.get("max_luminance", 0)
min_lv = contrast_final_result.get("min_luminance", 0)
contrast_ratio = contrast_final_result.get(
"contrast_ratio", 0
)
info_items = [
("最大亮度(白场)", f"{max_lv:.2f} cd/m²"),
("最小亮度(黑场)", f"{min_lv:.4f} cd/m²"),
("对比度", f"{contrast_ratio:.0f}:1"),
]
for label, value in info_items:
ws[f"A{row}"] = label
ws[f"B{row}"] = value
ws[f"A{row}"].font = label_font
ws[f"B{row}"].font = data_font
ws[f"A{row}"].border = thin_border
ws[f"B{row}"].border = thin_border
row += 1
row += 1
self.log_gui.log(" ✓ 添加对比度数据")
# ========== 5. 色准数据HDR 特有)==========
if "accuracy" in selected_items:
accuracy_final_result = None
if "accuracy" in self.results.test_items:
accuracy_final_result = self.results.test_items[
"accuracy"
].final_result
if accuracy_final_result:
ws.merge_cells(f"A{row}:G{row}")
ws[f"A{row}"] = "🎯 色准测试数据"
ws[f"A{row}"].font = section_font
ws[f"A{row}"].fill = section_fill
ws[f"A{row}"].alignment = section_alignment
ws.row_dimensions[row].height = 25
row += 1
# 色准统计信息
avg_delta_e = accuracy_final_result.get("avg_delta_e", 0)
max_delta_e = accuracy_final_result.get("max_delta_e", 0)
min_delta_e = accuracy_final_result.get("min_delta_e", 0)
excellent_count = accuracy_final_result.get(
"excellent_count", 0
)
good_count = accuracy_final_result.get("good_count", 0)
poor_count = accuracy_final_result.get("poor_count", 0)
ws[f"A{row}"] = "平均 ΔE"
ws[f"B{row}"] = f"{avg_delta_e:.2f}"
ws[f"C{row}"] = "最大 ΔE"
ws[f"D{row}"] = f"{max_delta_e:.2f}"
ws[f"E{row}"] = "最小 ΔE"
ws[f"F{row}"] = f"{min_delta_e:.2f}"
for col in ["A", "B", "C", "D", "E", "F"]:
ws[f"{col}{row}"].font = (
label_font if col in ["A", "C", "E"] else data_font
)
ws[f"{col}{row}"].border = thin_border
row += 1
# 第二行统计
ws[f"A{row}"] = "优秀 (ΔE<3)"
ws[f"B{row}"] = f"{excellent_count}"
ws[f"C{row}"] = "良好 (3≤ΔE<5)"
ws[f"D{row}"] = f"{good_count}"
ws[f"E{row}"] = "偏差 (ΔE≥5)"
ws[f"F{row}"] = f"{poor_count}"
for col in ["A", "B", "C", "D", "E", "F"]:
ws[f"{col}{row}"].font = (
label_font if col in ["A", "C", "E"] else data_font
)
ws[f"{col}{row}"].border = thin_border
row += 1
# ========== 色准详细数据表格(带 xy 坐标和亮度)==========
color_patches = accuracy_final_result.get(
"color_patches", []
)
delta_e_values = accuracy_final_result.get(
"delta_e_values", []
)
# ✅ 获取原始测量数据(包含 xy 和亮度)
color_measurements = accuracy_final_result.get(
"color_measurements", []
)
if color_patches and delta_e_values:
# 表头
headers = [
"序号",
"颜色名称",
"x 坐标",
"y 坐标",
"亮度 (cd/m²)",
"ΔE 2000",
"等级",
]
for col_idx, header in enumerate(headers, start=1):
cell = ws.cell(row=row, column=col_idx)
cell.value = header
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_alignment
cell.border = thin_border
row += 1
# 数据行
for idx, (color_name, delta_e) in enumerate(
zip(color_patches, delta_e_values), start=1
):
# 判断等级
if delta_e < 3:
grade = "优秀"
elif delta_e < 5:
grade = "良好"
else:
grade = "偏差"
# ✅ 获取测量数据x, y, 亮度)
x_val = "N/A"
y_val = "N/A"
lv_val = "N/A"
if color_measurements and idx - 1 < len(
color_measurements
):
measurement = color_measurements[idx - 1]
if len(measurement) >= 3:
x_val = measurement[0]
y_val = measurement[1]
lv_val = measurement[2]
ws[f"A{row}"] = idx
ws[f"B{row}"] = color_name
ws[f"C{row}"] = x_val
ws[f"D{row}"] = y_val
ws[f"E{row}"] = lv_val
ws[f"F{row}"] = delta_e
ws[f"G{row}"] = grade
# 数字格式
ws[f"A{row}"].number_format = "0"
if isinstance(x_val, (int, float)):
ws[f"C{row}"].number_format = "0.0000"
if isinstance(y_val, (int, float)):
ws[f"D{row}"].number_format = "0.0000"
if isinstance(lv_val, (int, float)):
ws[f"E{row}"].number_format = "0.00"
ws[f"F{row}"].number_format = "0.00"
for col in ["A", "B", "C", "D", "E", "F", "G"]:
ws[f"{col}{row}"].font = data_font
ws[f"{col}{row}"].alignment = data_alignment
ws[f"{col}{row}"].border = thin_border
row += 1
row += 1
self.log_gui.log(" ✓ 添加色准数据(含 xy 坐标和亮度)")
# ========== 调整列宽 ==========
for col in ["A", "B", "C", "D", "E", "F", "G"]:
ws.column_dimensions[col].width = 18
# ========== 保存 Excel ==========
excel_path = os.path.join(result_dir, "测试数据.xlsx")
wb.save(excel_path)
self.log_gui.log(f"✓ 已保存: 测试数据.xlsx")
self.log_gui.log("=" * 60)
except ImportError:
self.log_gui.log("⚠️ 未安装 openpyxl 库,跳过 Excel 导出")
except Exception as e:
self.log_gui.log(f"⚠️ Excel 导出失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
# ========== ✅ 统一的成功提示(在所有 Excel 代码之后)==========
self.log_gui.log(f"=" * 50)
self.log_gui.log(f"✅ 测试结果已保存到目录: {result_dir}")
self.log_gui.log(f"=" * 50)
messagebox.showinfo("成功", f"测试结果已保存到目录:\n{result_dir}")
except Exception as e:
self.log_gui.log(f"❌ 保存测试结果失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
messagebox.showerror("错误", f"保存测试结果失败: {str(e)}")
def new_pq_results(self, test_type, test_name):
self.results = PQResult(test_type, test_name)
# 设置配置
config = {
"test_type": test_type,
"test_name": test_name,
"test_items": self.config.current_test_types[test_type]["test_items"],
"test_items_chinese": self.config.get_test_item_chinese_names(
self.config.current_test_types[test_type]["test_items"]
),
}
self.results.set_test_config(config)
# 添加测试项
for item in config["test_items"]:
self.results.add_test_item(
item, config["test_items_chinese"][config["test_items"].index(item)]
)
def run_test(self, test_type, test_items):
"""执行测试"""
try:
self.log_gui.log(f"开始执行{self.get_test_type_name(test_type)}测试")
self.log_gui.log(
f"测试项目: {', '.join(self.config.get_test_item_chinese_names(test_items))}"
)
# 根据测试类型执行不同的测试流程
if test_type == "screen_module":
self.run_screen_module_test(test_items)
elif test_type == "sdr_movie":
self.run_sdr_movie_test(test_items)
elif test_type == "hdr_movie":
self.run_hdr_movie_test(test_items)
# 测试完成后更新UI状态
if self.testing: # 如果没有被中途停止
self.root.after(0, self.on_test_completed)
except Exception as e:
self.log_gui.log(f"测试过程中发生错误: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
self.root.after(0, self.on_test_error)
def run_screen_module_test(self, test_items):
"""执行屏模组性能测试 - 优化版"""
self.log_gui.log("执行屏模组性能测试...")
if test_items:
self.new_pq_results("screen_module", "屏模组性能测试")
else:
self.log_gui.log("未选择任何测试项目")
return
# 判断是否需要灰阶数据
needs_gray_data = any(
item in test_items for item in ["gamma", "cct", "contrast"]
)
shared_gray_data = None # 共享的灰阶数据
# 计算总测试项数量
total_items = len(test_items)
current_item = 0
for item in test_items:
if not self.testing: # 检查是否被停止
return
current_item += 1
self.status_var.set(f"测试进行中... ({current_item}/{total_items})")
# ==================== 色域测试 ====================
if item == "gamut":
self.test_gamut("screen_module")
# ==================== 灰阶数据采集 ====================
# 如果是第一个需要灰阶数据的测试项,统一采集数据
elif (
item in ["gamma", "cct", "contrast"]
and shared_gray_data is None
and needs_gray_data
):
self.log_gui.log("=" * 50)
self.log_gui.log("开始统一采集灰阶数据(用于 Gamma/CCT/对比度测试)")
self.log_gui.log("=" * 50)
shared_gray_data = self.send_fix_pattern("gray")
if not shared_gray_data or len(shared_gray_data) < 2:
self.log_gui.log("灰阶数据采集失败或数据不足,跳过相关测试")
return
self.log_gui.log(
f"✓ 灰阶数据采集完成,共 {len(shared_gray_data)} 个数据点"
)
# 保存到 results 对象,供所有灰阶测试使用
self.results.add_intermediate_data("shared", "gray", shared_gray_data)
# 执行当前测试项
if item == "gamma":
self.test_gamma("screen_module", shared_gray_data)
elif item == "cct":
self.test_cct("screen_module", shared_gray_data)
elif item == "contrast":
self.test_contrast("screen_module", shared_gray_data)
# ==================== 后续灰阶测试(复用数据) ====================
elif item in ["gamma", "cct", "contrast"] and shared_gray_data is not None:
self.log_gui.log(f"复用已采集的灰阶数据进行 {item} 测试")
if item == "gamma":
self.test_gamma("screen_module", shared_gray_data)
elif item == "cct":
self.test_cct("screen_module", shared_gray_data)
elif item == "contrast":
self.test_contrast("screen_module", shared_gray_data)
def run_custom_sdr_test(self, test_items):
"""执行客户定制 SDR 测试 - 升级版"""
self.log_gui.log("执行客户定制 SDR 测试...")
# 获取信号格式设置
color_space = self.sdr_color_space_var.get() # BT.709/BT.601/BT.2020
gamma_type = self.sdr_gamma_type_var.get() # 2.2/2.4/2.6
data_range = self.sdr_data_range_var.get() # Full/Limited
bit_depth = self.sdr_bit_depth_var.get() # 8bit/10bit/12bit
self.log_gui.log(f"信号格式: 色彩空间={color_space}, Gamma={gamma_type}")
self.log_gui.log(f" 数据范围={data_range}, 编码位深={bit_depth}")
self.log_gui.log("开始统一采集灰阶数据(用于 Gamma/CCT/对比度测试)")
self.test_custom_sdr()
if self.testing:
self.root.after(0, self.on_custom_template_test_completed)
def run_sdr_movie_test(self, test_items):
"""执行SDR Movie测试"""
self.log_gui.log("执行SDR Movie测试...")
if test_items:
self.new_pq_results("sdr_movie", "SDR Movie测试")
else:
self.log_gui.log("未选择任何测试项目")
return
# 获取信号格式设置
color_space = self.sdr_color_space_var.get() # BT.709/BT.601/BT.2020
gamma_type = self.sdr_gamma_type_var.get() # 2.2/2.4/2.6
data_range = self.sdr_data_range_var.get() # Full/Limited
bit_depth = self.sdr_bit_depth_var.get() # 8bit/10bit/12bit
self.log_gui.log(f"信号格式: 色彩空间={color_space}, Gamma={gamma_type}")
self.log_gui.log(f" 数据范围={data_range}, 编码位深={bit_depth}")
# 判断是否需要灰阶数据
needs_gray_data = any(
item in test_items for item in ["gamma", "cct", "contrast"]
)
shared_gray_data = None
# 计算总测试项数量
total_items = len(test_items)
current_item = 0
for item in test_items:
if not self.testing:
return
current_item += 1
self.status_var.set(f"测试进行中... ({current_item}/{total_items})")
if item == "gamut":
self.test_gamut("sdr_movie")
elif (
item in ["gamma", "cct", "contrast"]
and shared_gray_data is None
and needs_gray_data
):
self.log_gui.log("开始统一采集灰阶数据(用于 Gamma/CCT/对比度测试)")
shared_gray_data = self.send_fix_pattern("gray")
if not shared_gray_data or len(shared_gray_data) < 2:
self.log_gui.log("灰阶数据采集失败或数据不足")
return
self.results.add_intermediate_data("shared", "gray", shared_gray_data)
if item == "gamma":
self.test_gamma("sdr_movie", shared_gray_data)
elif item == "cct":
self.test_cct("sdr_movie", shared_gray_data)
elif item == "contrast":
self.test_contrast("sdr_movie", shared_gray_data)
elif item in ["gamma", "cct", "contrast"] and shared_gray_data is not None:
self.log_gui.log(f"复用已采集的灰阶数据进行 {item} 测试")
if item == "gamma":
self.test_gamma("sdr_movie", shared_gray_data)
elif item == "cct":
self.test_cct("sdr_movie", shared_gray_data)
elif item == "contrast":
self.test_contrast("sdr_movie", shared_gray_data)
elif item == "accuracy":
self.test_color_accuracy("sdr_movie")
def run_hdr_movie_test(self, test_items):
"""执行HDR Movie测试"""
self.log_gui.log("执行HDR Movie测试...")
if test_items:
self.new_pq_results("hdr_movie", "HDR Movie测试")
else:
self.log_gui.log("未选择任何测试项目")
return
# 获取信号格式设置
color_space = self.hdr_color_space_var.get()
max_cll = self.hdr_maxcll_var.get()
max_fall = self.hdr_maxfall_var.get()
data_range = self.hdr_data_range_var.get()
bit_depth = self.hdr_bit_depth_var.get()
self.log_gui.log(f"信号格式: 色彩空间={color_space}")
self.log_gui.log(f" MaxCLL={max_cll}, MaxFALL={max_fall}")
self.log_gui.log(f" 数据范围={data_range}, 编码位深={bit_depth}")
# 判断是否需要灰阶数据
needs_gray_data = any(
item in test_items for item in ["eotf", "cct", "contrast"]
)
shared_gray_data = None
# 计算总测试项数量
total_items = len(test_items)
current_item = 0
for item in test_items:
if not self.testing:
return
current_item += 1
self.status_var.set(f"测试进行中... ({current_item}/{total_items})")
if item == "gamut":
self.test_gamut("hdr_movie")
elif (
item in ["eotf", "cct", "contrast"]
and shared_gray_data is None
and needs_gray_data
):
self.log_gui.log("开始统一采集灰阶数据(用于 EOTF/CCT/对比度测试)")
shared_gray_data = self.send_fix_pattern("gray")
if not shared_gray_data or len(shared_gray_data) < 2:
self.log_gui.log("灰阶数据采集失败或数据不足")
return
self.results.add_intermediate_data("shared", "gray", shared_gray_data)
if item == "eotf":
self.test_eotf("hdr_movie", shared_gray_data)
elif item == "cct":
self.test_cct("hdr_movie", shared_gray_data)
elif item == "contrast":
self.test_contrast("hdr_movie", shared_gray_data)
elif item in ["eotf", "cct", "contrast"] and shared_gray_data is not None:
self.log_gui.log(f"复用已采集的灰阶数据进行 {item} 测试")
if item == "eotf":
self.test_eotf("hdr_movie", shared_gray_data)
elif item == "cct":
self.test_cct("hdr_movie", shared_gray_data)
elif item == "contrast":
self.test_contrast("hdr_movie", shared_gray_data)
elif item == "accuracy":
self.test_color_accuracy("hdr_movie")
def send_fix_pattern(self, mode):
"""发送固定图案并采集数据 - 支持不同测试类型的信号格式"""
results = []
try:
# 1. 设置图案模式
if mode == "rgb":
self.config.set_current_pattern("rgb")
elif mode == "gray":
self.config.set_current_pattern("gray")
elif mode == "accuracy": # 色准模式SDR 和 HDR 通用 29色
self.config.set_current_pattern("accuracy")
elif mode == "custom":
self.config.set_current_pattern("custom")
else:
self.log_gui.log(f"❌ 未知的图案模式: {mode}")
return None
# 2. 获取当前测试类型
test_type = self.config.current_test_type
# 3. 根据测试类型设置信号格式和图案
if test_type == "screen_module":
# 屏模组测试:使用 Timing
self.log_gui.log("=" * 50)
self.log_gui.log("设置屏模组信号格式:")
self.log_gui.log("=" * 50)
timing_str = self.config.current_test_types[test_type]["timing"]
self.log_gui.log(f" Timing: {timing_str}")
# ✅ 屏模组测试:直接使用原始配置
self.ucd.set_ucd_params(self.config)
elif test_type == "sdr_movie":
# SDR 测试设置色彩空间、Gamma 等
self.log_gui.log("=" * 50)
self.log_gui.log("设置 SDR 信号格式:")
self.log_gui.log("=" * 50)
color_space = self.sdr_color_space_var.get()
gamma = self.sdr_gamma_type_var.get()
data_range = self.sdr_data_range_var.get()
bit_depth = self.sdr_bit_depth_var.get()
self.log_gui.log(f" 色彩空间: {color_space}")
self.log_gui.log(f" Gamma: {gamma}")
self.log_gui.log(f" 数据范围: {data_range}")
self.log_gui.log(f" 编码位深: {bit_depth}")
success = self.ucd.set_sdr_format(
color_space=color_space,
gamma=gamma,
data_range=data_range,
bit_depth=bit_depth,
)
if success:
self.log_gui.log("✓ SDR 信号格式设置成功")
else:
self.log_gui.log("✗ SDR 信号格式设置失败")
# 设置图案参数
if mode == "accuracy":
self.log_gui.log(f"设置 SDR 29色色准测试图案...")
else:
self.log_gui.log(f"设置 SDR 测试图案({mode} 模式)...")
# ========== ✅✅✅ 修改:使用临时配置对象 ==========
import copy
# 从原始配置获取参数(每次都是干净的)
if mode == "rgb":
original_params = copy.deepcopy(
self.config.default_pattern_rgb["pattern_params"]
)
elif mode == "gray":
original_params = copy.deepcopy(
self.config.default_pattern_gray["pattern_params"]
)
elif mode == "accuracy":
original_params = copy.deepcopy(
self.config.default_pattern_accuracy["pattern_params"]
)
elif mode == "custom":
original_params = copy.deepcopy(
self.config.default_pattern_temp["pattern_params"]
)
self.log_gui.log(f"🔍 使用原始 RGB 参数(前 3 个):")
for i in range(min(3, len(original_params))):
self.log_gui.log(f" [{i+1}] {original_params[i]}")
# 根据 data_range 转换
converted_params = convert_pattern_params(
pattern_params=original_params, data_range=data_range, verbose=False
)
if data_range == "Limited":
self.log_gui.log("🔧 转换为 Limited Range (16-235):")
for i in range(min(3, len(converted_params))):
self.log_gui.log(
f" {original_params[i]}{converted_params[i]}"
)
else:
self.log_gui.log("✓ Full RangeRGB 保持不变")
# ✅ 创建临时配置对象(不修改 self.config
temp_config = self.config.get_temp_config_with_converted_params(
mode=mode, converted_params=converted_params
)
# ✅ 使用临时配置设置参数
self.ucd.set_ucd_params(temp_config)
self.log_gui.log(f"✓ 图案参数已设置,共 {len(converted_params)} 个图案")
# ========== 修改结束 ==========
elif test_type == "hdr_movie":
# HDR 测试设置色彩空间、Metadata 等
self.log_gui.log("=" * 50)
self.log_gui.log("设置 HDR 信号格式:")
self.log_gui.log("=" * 50)
color_space = self.hdr_color_space_var.get()
data_range = self.hdr_data_range_var.get()
bit_depth = self.hdr_bit_depth_var.get()
max_cll = self.hdr_maxcll_var.get()
max_fall = self.hdr_maxfall_var.get()
self.log_gui.log(f" 色彩空间: {color_space}")
self.log_gui.log(f" 数据范围: {data_range}")
self.log_gui.log(f" 编码位深: {bit_depth}")
self.log_gui.log(f" MaxCLL: {max_cll}")
self.log_gui.log(f" MaxFALL: {max_fall}")
success = self.ucd.set_hdr_format(
color_space=color_space,
data_range=data_range,
bit_depth=bit_depth,
max_cll=max_cll,
max_fall=max_fall,
)
if success:
self.log_gui.log("✓ HDR 信号格式设置成功")
else:
self.log_gui.log("✗ HDR 信号格式设置失败")
# 设置图案参数
if mode == "accuracy":
self.log_gui.log(f"设置 HDR 29色色准测试图案...")
else:
self.log_gui.log(f"设置 HDR 测试图案({mode} 模式)...")
# ========== ✅✅✅ 修改:使用临时配置对象 ==========
import copy
# 从原始配置获取参数
if mode == "rgb":
original_params = copy.deepcopy(
self.config.default_pattern_rgb["pattern_params"]
)
elif mode == "gray":
original_params = copy.deepcopy(
self.config.default_pattern_gray["pattern_params"]
)
elif mode == "accuracy":
original_params = copy.deepcopy(
self.config.default_pattern_accuracy["pattern_params"]
)
self.log_gui.log(f"🔍 使用原始 RGB 参数(前 3 个):")
for i in range(min(3, len(original_params))):
self.log_gui.log(f" [{i+1}] {original_params[i]}")
# 根据 data_range 转换
converted_params = convert_pattern_params(
pattern_params=original_params, data_range=data_range, verbose=False
)
if data_range == "Limited":
self.log_gui.log("🔧 转换为 Limited Range (16-235):")
for i in range(min(3, len(converted_params))):
self.log_gui.log(
f" {original_params[i]}{converted_params[i]}"
)
else:
self.log_gui.log("✓ Full RangeRGB 保持不变")
# ✅ 创建临时配置对象
temp_config = self.config.get_temp_config_with_converted_params(
mode=mode, converted_params=converted_params
)
self.ucd.set_ucd_params(temp_config)
self.log_gui.log(f"✓ 图案参数已设置,共 {len(converted_params)} 个图案")
# ========== 修改结束 ==========
self.log_gui.log("=" * 50)
# 4. 循环发送图案并采集数据(使用原始配置的数量)
total_patterns = len(self.config.current_pattern["pattern_params"])
self.log_gui.log(f"开始采集数据,共 {total_patterns} 个图案")
settle_time = max(0.2, float(getattr(self, "pattern_settle_time", 1.0)))
progress_step = max(
1, int(getattr(self, "pattern_progress_log_step", 5))
)
self.log_gui.log(
f"采集等待时间: {settle_time:.2f}s可通过 pattern_settle_time 调整)"
)
# 获取颜色名称列表(用于日志显示)
color_names = None
if mode == "accuracy":
color_names = self.config.get_accuracy_color_names()
custom_pattern_names = []
if mode == "custom" and hasattr(self.config, "get_temp_pattern_names"):
custom_pattern_names = self.config.get_temp_pattern_names()
for i in range(total_patterns):
if not self.testing:
self.log_gui.log("⚠️ 测试已停止")
return results
should_log_detail = (
i == 0
or (i + 1) == total_patterns
or ((i + 1) % progress_step == 0)
)
# 设置下一个图案(显示颜色名称)
if should_log_detail:
if color_names and i < len(color_names):
self.log_gui.log(
f"发送第 {i+1}/{total_patterns} 个图案: {color_names[i]}..."
)
else:
self.log_gui.log(f"发送第 {i+1}/{total_patterns} 个图案...")
self.ucd.set_next_pattern()
self.ucd.run()
time.sleep(settle_time)
# 测量数据
if mode == "custom":
result = []
self.ca.set_Display(1)
tcp, duv, lv, X, Y, Z = self.ca.readAllDisplay()
if should_log_detail:
self.log_gui.log(
f" ✓ 测量完成: TCP={tcp:.4f}, DUV={duv:.4f}, lv={lv:.2f}, "
f"X={X:.4f}, Y={Y:.4f}, Z={Z:.4f}"
)
self.ca.set_Display(8)
lambda_, Pe, lv, X, Y, Z = self.ca.readAllDisplay()
if should_log_detail:
self.log_gui.log(
f" ✓ 测量完成: λ={lambda_:.4f}, Pe={Pe:.4f}, lv={lv:.2f}, "
f"X={X:.4f}, Y={Y:.4f}, Z={Z:.4f}"
)
result = [tcp, duv, lv, lambda_, Pe, lv, X, Y, Z]
results.append(result)
# 每完成一个 pattern实时写入客户模板结果表。
try:
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 = {
"pattern_name": (
custom_pattern_names[i]
if i < len(custom_pattern_names)
else f"P {i + 1}"
),
"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_,
"Pe": Pe,
}
self.root.after(
0,
lambda row_no=i + 1, data=row_data: self.append_custom_template_result(
row_no, data
),
)
except Exception as e:
self.log_gui.log(f"⚠️ 第 {i+1} 行实时结果写入失败: {str(e)}")
else:
self.ca.set_xyLv_Display()
x, y, lv, X, Y, Z = self.ca.readAllDisplay()
results.append([x, y, lv, X, Y, Z])
if should_log_detail:
self.log_gui.log(
f" ✓ 测量完成: x={x:.4f}, y={y:.4f}, lv={lv:.2f}, "
f"X={X:.4f}, Y={Y:.4f}, Z={Z:.4f}"
)
self.log_gui.log(f"✓ 数据采集完成,共 {len(results)} 组数据")
return results
except Exception as e:
self.log_gui.log(f"❌ 发送图案失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
return None
def test_custom_sdr(self):
"""执行客户定制 SDR 测试 - 升级版"""
self.log_gui.log("执行客户定制 SDR 测试...")
results = self.send_fix_pattern("custom")
if not results:
self.log_gui.log("客户模板SDR测试被中断")
return
self.log_gui.log(f"客户模板采集完成,共 {len(results)} 组数据")
def test_gamut(self, test_type):
"""测试色域"""
self.log_gui.log("开始测试色域...")
self.results.start_test_item("gamut")
try:
# 存储测量结果
results = self.send_fix_pattern("rgb")
# 检查结果是否为空
if not results:
self.log_gui.log("色域测试被中断")
return
self.results.add_intermediate_data("gamut", "rgb", results)
# 计算色域覆盖率
self.log_gui.log("计算色域覆盖率...")
# 提取 x, y 坐标用于计算色域
xy_points = [[result[0], result[1]] for result in results]
# ========== ✅ 测试时:使用色彩空间的值作为参考标准 ==========
reference_standard = None
area = None
coverage = None
if test_type == "screen_module":
# 屏模组测试:固定使用 DCI-P3因为没有色彩空间设置
reference_standard = "DCI-P3"
# ✅ 同步更新到色域参考标准变量(供后续重绘使用)
self.screen_gamut_ref_var.set(reference_standard)
elif test_type == "sdr_movie":
# SDR 测试:使用色彩空间设置
color_space = self.sdr_color_space_var.get()
if color_space == "BT.709":
reference_standard = "BT.709"
elif color_space == "BT.601":
reference_standard = "BT.601"
elif color_space == "BT.2020":
reference_standard = "BT.2020"
else:
reference_standard = "BT.709"
self.log_gui.log(
f"⚠️ 未识别的色彩空间 '{color_space}',使用默认标准 BT.709"
)
# ✅ 同步更新到色域参考标准变量
self.sdr_gamut_ref_var.set(reference_standard)
elif test_type == "hdr_movie":
# HDR 测试:使用色彩空间设置
color_space = self.hdr_color_space_var.get()
if color_space == "BT.2020":
reference_standard = "BT.2020"
elif color_space == "DCI-P3":
reference_standard = "DCI-P3"
else:
reference_standard = "BT.2020"
self.log_gui.log(
f"⚠️ 未识别的色彩空间 '{color_space}',使用默认标准 BT.2020"
)
# ✅ 同步更新到色域参考标准变量
self.hdr_gamut_ref_var.set(reference_standard)
else:
# 未知测试类型,使用 DCI-P3 作为后备
reference_standard = "DCI-P3"
self.log_gui.log(
f"⚠️ 未识别的测试类型 '{test_type}',使用默认标准 DCI-P3"
)
# ========== 根据参考标准计算 XY 覆盖率 ==========
if reference_standard == "BT.2020":
area, coverage = pq_algorithm.calculate_gamut_coverage_BT2020(xy_points)
elif reference_standard == "BT.709":
area, coverage = pq_algorithm.calculate_gamut_coverage_BT709(xy_points)
elif reference_standard == "DCI-P3":
area, coverage = pq_algorithm.calculate_gamut_coverage_DCIP3(xy_points)
elif reference_standard == "BT.601":
area, coverage = pq_algorithm.calculate_gamut_coverage_BT601(xy_points)
else:
# 默认使用 DCI-P3
area, coverage = pq_algorithm.calculate_gamut_coverage_DCIP3(xy_points)
reference_standard = "DCI-P3"
self.log_gui.log(
f"⚠️ 未识别的参考标准 '{reference_standard}',使用默认标准 DCI-P3"
)
# ========== ✅✅✅ 新增:计算 UV 覆盖率 ==========
uv_coverage = 0
try:
# 将 XY 转换为 UV
uv_points = []
for x, y in xy_points:
u, v = pq_algorithm.xy_to_uv_1976(x, y)
uv_points.append([u, v])
# 根据参考标准计算 UV 覆盖率
if len(uv_points) >= 3:
if reference_standard == "BT.2020":
_, uv_coverage = (
pq_algorithm.calculate_gamut_coverage_BT2020_uv(uv_points)
)
elif reference_standard == "BT.709":
_, uv_coverage = pq_algorithm.calculate_gamut_coverage_BT709_uv(
uv_points
)
elif reference_standard == "DCI-P3":
_, uv_coverage = pq_algorithm.calculate_gamut_coverage_DCIP3_uv(
uv_points
)
elif reference_standard == "BT.601":
_, uv_coverage = pq_algorithm.calculate_gamut_coverage_BT601_uv(
uv_points
)
else:
_, uv_coverage = pq_algorithm.calculate_gamut_coverage_DCIP3_uv(
uv_points
)
self.log_gui.log(
f"✓ XY 覆盖率: {coverage:.1f}% | UV 覆盖率: {uv_coverage:.1f}%"
)
except:
uv_coverage = 0
# ========== 保存结果时包含 XY 和 UV 覆盖率 ==========
self.results.set_test_item_result(
"gamut",
{
"area": area,
"coverage": coverage,
"uv_coverage": uv_coverage, # ✅ 新增 UV 覆盖率
"reference": reference_standard,
},
)
# 传递完整的 results 用于绘图
self.plot_gamut(results, coverage, test_type)
self.log_gui.log("色域测试完成")
except Exception as e:
self.log_gui.log(f"色域测试失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
raise
def test_gamma(self, test_type, gray_data=None):
"""测试Gamma曲线
Args:
test_type: 测阶数据,如果提供则使用,否则重新采集
"""
self.log_gui.log("开始测试Gamma曲线...")
self.results.start_test_item("gamma")
try:
# 使用传入的灰阶数据或独立采集
if gray_data is not None:
self.log_gui.log("使用共享的灰阶数据")
results = gray_data
else:
self.log_gui.log("独立采集灰阶数据")
results = self.send_fix_pattern("gray")
if not results or len(results) < 2:
self.log_gui.log("Gamma测试被中断或数据不足")
return
self.results.add_intermediate_data("gamma", "gray", results)
self.log_gui.log(f"使用 {len(results)} 个灰阶数据点进行计算")
self.log_gui.log("计算Gamma值...")
# ========== ✅ 修复:正确获取 max_index_fix ==========
# 获取配置中的值
config_max_value = self.config.current_pattern.get(
"measurement_max_value", 10
)
# 强制转换为整数
try:
max_index_fix = int(config_max_value)
except (ValueError, TypeError):
self.log_gui.log(f"警告: measurement_max_value 转换失败,使用默认值 10")
max_index_fix = 10
self.log_gui.log(f"配置中的 max_index_fix = {max_index_fix}")
# 关键修复:验证并调整 max_index_fix
# max_index_fix 应该是数据点的最大索引从0开始所以是 len - 1
actual_max_index = len(results) - 1
if max_index_fix > actual_max_index:
self.log_gui.log(
f"警告: 配置的 max_index_fix({max_index_fix}) > 实际最大索引({actual_max_index})"
)
self.log_gui.log(f"自动调整为: {actual_max_index}")
max_index_fix = actual_max_index
self.log_gui.log(f"最终使用的 max_index_fix = {max_index_fix}")
# ========================================================
# 获取灰阶 pattern 参数用于22293 Gamma数据对齐
pattern_params = self.config.default_pattern_gray.get(
"pattern_params", None
)
# 计算Gamma值使用修正后的 max_index_fix 和 8bit pattern参数
results_with_gamma_list, L_bar = self.calculate_gamma(
results, max_index_fix, pattern_params
)
self.results.set_test_item_result(
"gamma", {"gamma": results_with_gamma_list, "L_bar": L_bar}
)
# 绘制Gamma曲线
if test_type == "sdr_movie":
try:
target_gamma = float(self.sdr_gamma_type_var.get())
except (ValueError, AttributeError):
target_gamma = 2.2
else:
target_gamma = 2.2
self.plot_gamma(L_bar, results_with_gamma_list, target_gamma, test_type)
self.log_gui.log("Gamma测试完成")
except Exception as e:
self.log_gui.log(f"Gamma测试失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
raise
def test_eotf(self, test_type, gray_data=None):
"""测试 EOTF 曲线HDR 专用)
Args:
test_type: 测试类型阶数据,如果提供则使用,否则重新采集
"""
self.log_gui.log("开始测试 EOTF 曲线HDR...")
self.results.start_test_item("eotf")
try:
# 使用传入的灰阶数据或独立采集
if gray_data is not None:
self.log_gui.log("使用共享的灰阶数据")
results = gray_data
else:
self.log_gui.log("独立采集灰阶数据")
results = self.send_fix_pattern("gray")
if not results or len(results) < 2:
self.log_gui.log("EOTF 测试被中断或数据不足")
return
self.results.add_intermediate_data("eotf", "gray", results)
self.log_gui.log(f"使用 {len(results)} 个灰阶数据点进行计算")
self.log_gui.log("计算 EOTF 值...")
# ========== 获取 max_index_fix ==========
config_max_value = self.config.current_pattern.get(
"measurement_max_value", 10
)
try:
max_index_fix = int(config_max_value)
except (ValueError, TypeError):
self.log_gui.log(f"警告: measurement_max_value 转换失败,使用默认值 10")
max_index_fix = 10
self.log_gui.log(f"配置中的 max_index_fix = {max_index_fix}")
# 验证并调整 max_index_fix
actual_max_index = len(results) - 1
if max_index_fix > actual_max_index:
self.log_gui.log(
f"警告: 配置的 max_index_fix({max_index_fix}) > 实际最大索引({actual_max_index})"
)
self.log_gui.log(f"自动调整为: {actual_max_index}")
max_index_fix = actual_max_index
self.log_gui.log(f"最终使用的 max_index_fix = {max_index_fix}")
# 获取灰阶 pattern 参数用于22293 Gamma数据对齐
pattern_params = self.config.default_pattern_gray.get(
"pattern_params", None
)
# ========== 计算 EOTF复用 Gamma 计算逻辑使用8bit pattern参数==========
results_with_eotf_list, L_bar = self.calculate_gamma(
results, max_index_fix, pattern_params
)
# 保存结果
self.results.set_test_item_result(
"eotf", {"eotf": results_with_eotf_list, "L_bar": L_bar}
)
# ========== 绘制 EOTF 曲线 ==========
# HDR 使用 PQ 曲线,目标 gamma 设为 None不使用传统 gamma
self.plot_eotf(L_bar, results_with_eotf_list, test_type)
self.log_gui.log("EOTF 测试完成")
except Exception as e:
self.log_gui.log(f"EOTF 测试失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
raise
def test_cct(self, test_type, gray_data=None):
"""测试色度一致性"""
self.log_gui.log("开始测试色度一致性...")
self.results.start_test_item("cct")
try:
if gray_data is not None:
self.log_gui.log("使用共享的灰阶数据")
results = gray_data
else:
self.log_gui.log("独立采集灰阶数据")
results = self.send_fix_pattern("gray")
if not results:
self.log_gui.log("色度一致性测试被中断")
return
self.results.add_intermediate_data("cct", "gray", results)
self.log_gui.log(f"使用 {len(results)} 个灰阶数据点进行色度计算")
# 提取色度坐标
cct_values = pq_algorithm.calculate_cct_from_results(results)
# 保存到结果
self.results.set_test_item_result("cct", {"cct_values": cct_values})
# 绘制图表
self.plot_cct(test_type)
self.log_gui.log("色度一致性测试完成")
except Exception as e:
self.log_gui.log(f"色度一致性测试失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
raise
def test_contrast(self, test_type, gray_data=None):
"""测试对比度
Args:
test_type: 阶数据,如果提供则使用,否则重新采集
"""
self.log_gui.log("开始测试对比度...")
self.results.start_test_item("contrast")
try:
# ✅ 优先使用传入的灰阶数据
if gray_data is not None:
self.log_gui.log("使用共享的灰阶数据")
results = gray_data
else:
self.log_gui.log("独立采集灰阶数据")
results = self.send_fix_pattern("gray")
if not results:
self.log_gui.log("对比度测试被中断")
return
self.results.add_intermediate_data("contrast", "gray", results)
# 获取最亮和最暗的亮度值
luminance_values = [result[2] for result in results] # 提取lv值
max_luminance = max(luminance_values) # 最大亮度(白)
min_luminance = min(luminance_values) # 最小亮度(黑)
# 防止除以0
if min_luminance < 0.001:
min_luminance = 0.001
# 计算对比度
contrast_ratio = max_luminance / min_luminance
# 保存结果
contrast_data = {
"max_luminance": max_luminance,
"min_luminance": min_luminance,
"contrast_ratio": contrast_ratio,
"luminance_values": luminance_values,
}
self.results.set_test_item_result("contrast", contrast_data)
# 显示对比度结果到日志
self.log_gui.log(f"最大亮度 (白场): {max_luminance:.2f} cd/m²")
self.log_gui.log(f"最小亮度 (黑场): {min_luminance:.4f} cd/m²")
self.log_gui.log(f"对比度: {contrast_ratio:.0f}:1")
# 绘制对比度图表
self.plot_contrast(contrast_data, test_type)
self.log_gui.log("对比度测试完成")
except Exception as e:
self.log_gui.log(f"对比度测试失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
raise
def calculate_delta_e_2000(
self, measured_x, measured_y, measured_lv, standard_x, standard_y
):
"""转发到 app.tests.color_accuracy.calculate_delta_e_2000Step 1 重构)"""
return _calc_delta_e_2000(
measured_x, measured_y, measured_lv, standard_x, standard_y
)
def test_color_accuracy(self, test_type):
"""测试色准 - 使用手工实现的 ΔE 2000应用 Gamma"""
# ========== 读取用户选择的 Gamma ==========
if test_type == "sdr_movie":
try:
target_gamma = float(self.sdr_gamma_type_var.get())
except (ValueError, AttributeError):
target_gamma = 2.2
self.log_gui.log("=" * 50)
self.log_gui.log(f"开始测试色准SDR Movie 标准 - 29色")
self.log_gui.log(f"✓ 使用 Gamma: {target_gamma}") # ← 新增
self.log_gui.log("=" * 50)
elif test_type == "hdr_movie":
target_gamma = 2.4 # HDR 使用 PQ但保留参考值
self.log_gui.log("=" * 50)
self.log_gui.log(f"开始测试色准HDR Movie 标准 - 29色")
self.log_gui.log(f"✓ 使用 Gamma: PQ (参考γ={target_gamma})") # ← 新增
self.log_gui.log("=" * 50)
else: # screen_module
target_gamma = 2.2
self.log_gui.log("=" * 50)
self.log_gui.log(f"开始测试色准(屏模组 标准 - 29色")
self.log_gui.log(f"✓ 使用 Gamma: {target_gamma}")
self.log_gui.log("=" * 50)
# 获取 29色名称
color_names = self.config.get_accuracy_color_names()
self.log_gui.log(f"✓ 将测试 {len(color_names)} 个色块")
self.log_gui.log(f" 色块分组:")
self.log_gui.log(f" 灰阶 (5个): {', '.join(color_names[:5])}")
self.log_gui.log(f" ColorChecker (18个): {', '.join(color_names[5:23])}")
self.log_gui.log(f" 饱和色 (6个): {', '.join(color_names[23:])}")
self.log_gui.log("=" * 50)
self.log_gui.log("开始发送色准图案并采集数据...")
self.log_gui.log("=" * 50)
# 发送 29色图案
measured_data_list = self.send_fix_pattern("accuracy")
if measured_data_list is None or len(measured_data_list) != 29:
self.log_gui.log(f"❌ 数据数量不匹配")
self.log_gui.log(f" 期望: 29 个")
self.log_gui.log(
f" 实际: {len(measured_data_list) if measured_data_list else 0}"
)
return
# 保存原始测量数据供单步调试使用
self.results.add_intermediate_data("accuracy", "measured", measured_data_list)
# ========== 计算 ΔE 2000显示 Gamma==========
self.log_gui.log("=" * 50)
self.log_gui.log(f"计算色准ΔE 2000Gamma {target_gamma}...")
self.log_gui.log("=" * 50)
# 获取标准 xy 坐标
standards = self.get_accuracy_color_standards(test_type)
delta_e_values = []
color_patches = []
for i, (name, measured_data) in enumerate(zip(color_names, measured_data_list)):
measured_x = measured_data[0]
measured_y = measured_data[1]
measured_lv = measured_data[2]
standard_x, standard_y = standards.get(name, (0.3127, 0.3290))
delta_e = self.calculate_delta_e_2000(
measured_x,
measured_y,
measured_lv,
standard_x,
standard_y,
)
delta_e_values.append(delta_e)
color_patches.append(name)
if delta_e < 3:
grade, icon = "优秀", ""
elif delta_e < 5:
grade, icon = "良好", ""
else:
grade, icon = "偏差", ""
self.log_gui.log(
f" [{i+1:2d}] {name:20s} ΔE={delta_e:5.2f} {icon} {grade}"
)
# ========== 统计 ==========
avg_delta_e_all = sum(delta_e_values) / len(delta_e_values)
max_delta_e_all = max(delta_e_values)
min_delta_e_all = min(delta_e_values)
excellent_count_all = sum(1 for de in delta_e_values if de < 3)
good_count_all = sum(1 for de in delta_e_values if 3 <= de < 5)
poor_count_all = sum(1 for de in delta_e_values if de >= 5)
delta_e_gray = delta_e_values[0:5]
avg_delta_e_gray = sum(delta_e_gray) / len(delta_e_gray)
delta_e_colorchecker = delta_e_values[5:23]
avg_delta_e_colorchecker = sum(delta_e_colorchecker) / len(delta_e_colorchecker)
delta_e_saturated = delta_e_values[23:29]
avg_delta_e_saturated = sum(delta_e_saturated) / len(delta_e_saturated)
self.log_gui.log("=" * 50)
self.log_gui.log("色准统计(全 29色:")
self.log_gui.log("=" * 50)
self.log_gui.log(f" 平均 ΔE: {avg_delta_e_all:.2f}")
self.log_gui.log(f" 最大 ΔE: {max_delta_e_all:.2f}")
self.log_gui.log(f" 最小 ΔE: {min_delta_e_all:.2f}")
self.log_gui.log(f" 优秀 (ΔE<3): {excellent_count_all}")
self.log_gui.log(f" 良好 (3≤ΔE<5): {good_count_all}")
self.log_gui.log(f" 偏差 (ΔE≥5): {poor_count_all}")
self.log_gui.log("")
self.log_gui.log("分组统计:")
self.log_gui.log(f" 灰阶 (5个): 平均 ΔE = {avg_delta_e_gray:.2f}")
self.log_gui.log(
f" ColorChecker (18个): 平均 ΔE = {avg_delta_e_colorchecker:.2f}"
)
self.log_gui.log(f" 饱和色 (6个): 平均 ΔE = {avg_delta_e_saturated:.2f}")
# ========== 保存测试结果 ==========
accuracy_data = {
"color_patches": color_patches,
"delta_e_values": delta_e_values,
"color_measurements": measured_data_list,
"avg_delta_e": avg_delta_e_all,
"max_delta_e": max_delta_e_all,
"min_delta_e": min_delta_e_all,
"excellent_count": excellent_count_all,
"good_count": good_count_all,
"poor_count": poor_count_all,
"avg_delta_e_gray": avg_delta_e_gray,
"avg_delta_e_colorchecker": avg_delta_e_colorchecker,
"avg_delta_e_saturated": avg_delta_e_saturated,
"target_gamma": target_gamma,
}
self.results.set_test_item_result("accuracy", accuracy_data)
# ========== 绘制图表 ==========
self.plot_accuracy(accuracy_data, test_type)
self.log_gui.log("色准测试完成")
def get_accuracy_color_standards(self, test_type):
"""转发到 app.tests.color_accuracy.get_accuracy_color_standardsStep 1 重构)"""
return _get_accuracy_color_standards(test_type)
def calculate_gamut_coverage(self, results):
"""转发到 app.tests.gamut.calculate_gamut_coverageStep 1 重构)"""
return _calc_gamut_coverage(results)
def calculate_gamma(self, results, max_index_fix, pattern_params=None):
"""转发到 app.tests.gamma.calculate_gammaStep 1 重构)"""
return _calc_gamma(results, max_index_fix, pattern_params)
def calculate_color_accuracy(self, measured, standard):
"""转发到 app.tests.color_accuracy.calculate_color_accuracyStep 1 重构)"""
return _calc_color_accuracy(measured, standard)
def plot_gamut(self, results, coverage, test_type):
"""转发到 app.plots.plot_gamut.plot_gamutStep 2 重构)"""
return _plot_gamut(self, results, coverage, test_type)
def plot_gamma(self, L_bar, results_with_gamma_list, target_gamma, test_type):
"""转发到 app.plots.plot_gamma.plot_gammaStep 2 重构)"""
return _plot_gamma(self, L_bar, results_with_gamma_list, target_gamma, test_type)
def plot_eotf(self, L_bar, results_with_eotf_list, test_type):
"""转发到 app.plots.plot_eotf.plot_eotfStep 2 重构)"""
return _plot_eotf(self, L_bar, results_with_eotf_list, test_type)
def calculate_pq_curve(self, gray_levels):
"""转发到 app.tests.eotf.calculate_pq_curveStep 1 重构)"""
return _calc_pq_curve(gray_levels)
def plot_cct(self, test_type):
"""转发到 app.plots.plot_cct.plot_cctStep 2 重构)"""
return _plot_cct(self, test_type)
def plot_contrast(self, contrast_data, test_type):
"""转发到 app.plots.plot_contrast.plot_contrastStep 2 重构)"""
return _plot_contrast(self, contrast_data, test_type)
def plot_accuracy(self, accuracy_data, test_type):
"""转发到 app.plots.plot_accuracy.plot_accuracyStep 2 重构)"""
return _plot_accuracy(self, accuracy_data, test_type)
def on_test_completed(self):
"""测试完成后的UI更新"""
self.testing = False
self.start_btn.config(state=tk.NORMAL)
self.stop_btn.config(state=tk.DISABLED)
self.save_btn.config(state=tk.NORMAL)
self.clear_config_btn.config(state=tk.NORMAL)
self.status_var.set("测试完成")
self.log_gui.log("测试完成")
# 恢复配置项按钮
if hasattr(self, "config_panel_frame"):
try:
self.config_panel_frame.btn.configure(state="normal")
except:
pass
# 启用色域参考标准下拉框
try:
test_type = self.config.current_test_type
if test_type == "screen_module" and hasattr(self, "screen_gamut_combo"):
self.screen_gamut_combo.configure(state="readonly")
self.log_gui.log("✓ 屏模组色域参考标准已启用")
elif test_type == "sdr_movie" and hasattr(self, "sdr_gamut_combo"):
self.sdr_gamut_combo.configure(state="readonly")
self.log_gui.log("✓ SDR 色域参考标准已启用")
elif test_type == "hdr_movie" and hasattr(self, "hdr_gamut_combo"):
self.hdr_gamut_combo.configure(state="readonly")
self.log_gui.log("✓ HDR 色域参考标准已启用")
except Exception as e:
self.log_gui.log(f"启用色域参考标准失败: {str(e)}")
# 获取当前测试类型和选中的测试项
selected_items = self.get_selected_test_items()
test_type = self.config.current_test_type
# ==================== ✅ 启用单步调试按钮 ====================
if hasattr(self, "debug_panel"):
try:
# 屏模组:启用 Gamma 和 RGB 单步调试
if test_type == "screen_module":
if "gamma" in selected_items:
gray_data = self.results.get_intermediate_data("shared", "gray")
if gray_data:
self.debug_panel.enable_debug(
"screen_module", "gamma", gray_data
)
# 启用 RGB 单步调试(色域测试完成后)
if "gamut" in selected_items:
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
if rgb_data:
self.debug_panel.enable_debug(
"screen_module", "rgb", rgb_data
)
# ✅ 启用单步调试按钮
if hasattr(self, "screen_debug_btn"):
self.screen_debug_btn.config(state=tk.NORMAL)
self.log_gui.log("✓ 屏模组单步调试按钮已启用")
# SDR启用 Gamma、色准和 RGB 单步调试
elif test_type == "sdr_movie":
if "gamma" in selected_items:
gray_data = self.results.get_intermediate_data("shared", "gray")
if gray_data:
self.debug_panel.enable_debug(
"sdr_movie", "gamma", gray_data
)
if "accuracy" in selected_items:
accuracy_data = self.results.get_intermediate_data(
"accuracy", "measured"
)
if accuracy_data:
self.debug_panel.enable_debug(
"sdr_movie", "accuracy", accuracy_data
)
# 启用 RGB 单步调试(色域测试完成后)
if "gamut" in selected_items:
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
if rgb_data:
self.debug_panel.enable_debug("sdr_movie", "rgb", rgb_data)
# ✅ 启用单步调试按钮
if hasattr(self, "sdr_debug_btn"):
self.sdr_debug_btn.config(state=tk.NORMAL)
self.log_gui.log("✓ SDR 单步调试按钮已启用")
# HDR启用 EOTF、色准和 RGB 单步调试
elif test_type == "hdr_movie":
if "eotf" in selected_items:
gray_data = self.results.get_intermediate_data("shared", "gray")
if gray_data:
self.debug_panel.enable_debug(
"hdr_movie", "eotf", gray_data
)
if "accuracy" in selected_items:
accuracy_data = self.results.get_intermediate_data(
"accuracy", "measured"
)
if accuracy_data:
self.debug_panel.enable_debug(
"hdr_movie", "accuracy", accuracy_data
)
# 启用 RGB 单步调试(色域测试完成后)
if "gamut" in selected_items:
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
if rgb_data:
self.debug_panel.enable_debug("hdr_movie", "rgb", rgb_data)
# ✅ 启用单步调试按钮
if hasattr(self, "hdr_debug_btn"):
self.hdr_debug_btn.config(state=tk.NORMAL)
self.log_gui.log("✓ HDR 单步调试按钮已启用")
except Exception as e:
self.log_gui.log(f"启用单步调试失败: {str(e)}")
# ==================== 显示色度/色域重新计算按钮 ====================
if "cct" in selected_items:
try:
if test_type == "screen_module" and hasattr(self, "recalc_cct_btn"):
self.recalc_cct_btn.grid()
self.log_gui.log("✓ 屏模组色度参数调整按钮已启用")
elif test_type == "sdr_movie" and hasattr(self, "sdr_recalc_cct_btn"):
self.sdr_recalc_cct_btn.grid()
self.log_gui.log("✓ SDR 色度参数调整按钮已启用")
elif test_type == "hdr_movie" and hasattr(self, "hdr_recalc_cct_btn"):
self.hdr_recalc_cct_btn.grid()
self.log_gui.log("✓ HDR 色度参数调整按钮已启用")
except Exception as e:
self.log_gui.log(f"显示色度重新计算按钮失败: {str(e)}")
if "gamut" in selected_items:
try:
if test_type == "screen_module" and hasattr(self, "recalc_gamut_btn"):
self.recalc_gamut_btn.grid()
self.log_gui.log("✓ 屏模组色域参考调整按钮已启用")
elif test_type == "sdr_movie" and hasattr(self, "sdr_recalc_gamut_btn"):
self.sdr_recalc_gamut_btn.grid()
self.log_gui.log("✓ SDR 色域参考调整按钮已启用")
elif test_type == "hdr_movie" and hasattr(self, "hdr_recalc_gamut_btn"):
self.hdr_recalc_gamut_btn.grid()
self.log_gui.log("✓ HDR 色域参考调整按钮已启用")
except Exception as e:
self.log_gui.log(f"显示色域重新计算按钮失败: {str(e)}")
messagebox.showinfo("完成", "测试已完成!")
def on_custom_template_test_completed(self):
"""客户模板测试完成后的UI更新"""
self.testing = False
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)
self.custom_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
self.log_gui.log("客户模板测试完成")
messagebox.showinfo("完成", "客户模板测试已完成!")
def get_current_test_result(self):
"""获取当前测试结果"""
test_type = self.test_type_var.get()
test_items = self.get_selected_test_items()
# 构建测试结果字典
result = {
"test_type": test_type,
"test_type_name": self.get_test_type_name(test_type),
"test_items": test_items,
"test_items_names": self.config.get_test_item_chinese_names(test_items),
"timestamp": datetime.datetime.now(),
"status": "完成",
"results": {},
}
# 根据测试项目收集结果数据
for item in test_items:
if item == "gamut" and hasattr(self, "gamut_results"):
result["results"]["gamut"] = getattr(self, "gamut_results", {})
elif item in ["gamma", "eotf"] and hasattr(self, "gamma_results"):
result["results"][item] = getattr(self, "gamma_results", {})
elif item == "cct" and hasattr(self, "cct_results"):
result["results"]["cct"] = getattr(self, "cct_results", {})
elif item == "contrast" and hasattr(self, "contrast_results"):
result["results"]["contrast"] = getattr(self, "contrast_results", {})
elif item == "accuracy" and hasattr(self, "accuracy_results"):
result["results"]["accuracy"] = getattr(self, "accuracy_results", {})
return result
def on_test_error(self):
"""测试出错后的UI更新"""
self.testing = False
self.set_custom_result_table_locked(False)
self.start_btn.config(state=tk.NORMAL)
self.stop_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("测试出错")
# 恢复配置项按钮
if hasattr(self, "config_panel_frame"):
try:
self.config_panel_frame.btn.configure(state="normal")
except:
pass
messagebox.showerror("错误", "测试过程中发生错误,请查看日志")
def get_test_type_name(self, test_type):
"""获取测试类型的显示名称"""
if test_type == "screen_module":
return "屏模组性能测试"
elif test_type == "sdr_movie":
return "SDR Movie测试"
elif test_type == "hdr_movie":
return "HDR Movie测试"
return test_type
def get_selected_test_items(self):
"""获取当前选中的测试项"""
selected_items = []
for var_name, var in self.test_vars.items():
if var.get():
selected_items.append(var_name.split("_")[-1])
return selected_items
def update_config(self, event=None):
"""更新配置"""
try:
self.config.set_device_config(
self.ca_com_var.get(),
self.ucd_list_var.get(),
self.ca_channel_var.get(),
)
# 保存当前选中的测试项到配置
self.config.set_current_test_items(self.get_selected_test_items())
# 待修改为三种测试类型的timing值
self.config.set_current_timing(self.screen_module_timing_var.get())
# 自动保存配置到文件
self.save_pq_config()
except Exception as e:
self.log_gui.log(f"更新配置失败: {str(e)}")
def update_config_and_tabs(self):
"""更新配置并同步Tab状态"""
self.update_config()
self.update_chart_tabs_state()
# 根据当前测试类型保存对应参数
current_test_type = self.config.current_test_type
selected_items = self.get_selected_test_items()
if current_test_type == "screen_module":
if "cct" in selected_items:
self.save_cct_params()
elif current_test_type == "sdr_movie":
if "cct" in selected_items:
self.save_sdr_cct_params()
elif current_test_type == "hdr_movie":
if "cct" in selected_items:
if hasattr(self, "save_hdr_cct_params"):
self.save_hdr_cct_params()
# 控制参数框的显示
self.toggle_cct_params_frame()
def toggle_cct_params_frame(self):
"""根据测试类型和测试项的选中状态显示对应参数框"""
selected_items = self.get_selected_test_items()
current_test_type = self.config.current_test_type
# ========== 默认隐藏所有参数框 ==========
self.cct_params_frame.pack_forget()
self.sdr_cct_params_frame.pack_forget()
# HDR 色度参数框(如果存在的话)
if hasattr(self, "hdr_cct_params_frame"):
self.hdr_cct_params_frame.pack_forget()
# ========== 根据测试类型和选中项显示对应参数框 ==========
if current_test_type == "screen_module":
# 屏模组:只有色度参数
if "cct" in selected_items:
self.cct_params_frame.pack(fill=tk.X, padx=5, pady=5)
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 显示屏模组色度参数设置")
elif current_test_type == "sdr_movie":
# SDR只有色度参数色准不需要参数设置框
if "cct" in selected_items:
self.sdr_cct_params_frame.pack(fill=tk.X, padx=5, pady=5)
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 显示 SDR 色度参数设置")
elif current_test_type == "hdr_movie":
# HDR只有色度参数色准不需要参数设置框
if "cct" in selected_items:
if hasattr(self, "hdr_cct_params_frame"):
self.hdr_cct_params_frame.pack(fill=tk.X, padx=5, pady=5)
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 显示 HDR 色度参数设置")
else:
if hasattr(self, "log_gui"):
self.log_gui.log("⚠️ HDR 色度参数框尚未创建")
def on_screen_module_timing_changed(self, event=None):
"""屏模组信号格式改变时的回调"""
try:
selected_timing = self.screen_module_timing_var.get()
# 记录日志
self.log_gui.log(f"屏模组信号格式已更改为: {selected_timing}")
# 解析分辨率和刷新率
import re
match = re.search(r"(\d+)x(\d+)\s*@\s*(\d+)", selected_timing)
if match:
width = int(match.group(1))
height = int(match.group(2))
refresh_rate = int(match.group(3))
self.log_gui.log(f" ├─ 分辨率: {width}x{height}")
self.log_gui.log(f" └─ 刷新率: {refresh_rate}Hz")
# 根据分辨率给出提示
if width >= 3840: # 4K及以上
self.log_gui.log(" 检测到4K分辨率")
if refresh_rate >= 120:
self.log_gui.log(" 检测到高刷新率")
# 更新配置
self.config.set_current_timing(selected_timing)
# 如果正在测试,提示用户
if self.testing:
self.log_gui.log("⚠️ 警告: 测试进行中,信号格式更改将在下次测试时生效")
# 保存配置
self.save_pq_config()
except Exception as e:
self.log_gui.log(f"❌ 屏模组信号格式更改失败: {str(e)}")
def load_pq_config(self):
"""加载PQ配置兼容打包后的程序"""
try:
# ✅ 使用 self.config_file已经是动态路径
if os.path.exists(self.config_file):
with open(self.config_file, "r", encoding="utf-8") as f:
config_dict = json.load(f)
self.config.from_dict(config_dict)
if hasattr(self, "log_gui"):
self.log_gui.log("✓ 配置文件加载成功")
else:
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)},使用默认配置")
def save_pq_config(self):
"""保存PQ配置兼容打包后的程序"""
try:
# 确保目录存在
os.makedirs(os.path.dirname(self.config_file), exist_ok=True)
# 保存配置
self.config.save_to_file(self.config_file)
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"保存配置文件失败: {str(e)}")
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)}")
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 是对的
# 创建调试面板实例
from views.pq_debug_panel import PQDebugPanel
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
from views.pq_debug_panel import PQDebugPanel
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
from views.pq_debug_panel import PQDebugPanel
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 单步调试窗口已关闭")
self.hdr_debug_window.protocol("WM_DELETE_WINDOW", on_closing)
self.hdr_debug_window.update_idletasks()
self.log_gui.log("✓ HDR 单步调试面板已打开(独立窗口)")
def clear_config_file(self):
"""清理配置文件(兼容打包后的程序)"""
from tkinter import messagebox
config_file = self.get_config_path()
try:
if os.path.exists(config_file):
os.remove(config_file)
self.config_cleared = True
messagebox.showinfo("提示", "✓ 清理成功")
self.log_gui.log("✓ 配置文件清理成功")
else:
messagebox.showinfo("提示", "配置文件不存在")
self.log_gui.log("⚠️ 配置文件不存在")
except Exception as e:
messagebox.showerror("错误", "❌ 清理失败")
self.log_gui.log(f"❌ 配置文件清理失败: {str(e)}")
def start_local_dimming_test(self):
"""开始 Local Dimming 测试"""
# 检查设备连接
if not self.ca or not self.ucd.status:
messagebox.showerror("错误", "请先连接 CA410 和 UCD323")
return
# 禁用按钮
self.ld_start_btn.config(state=tk.DISABLED)
self.ld_stop_btn.config(state=tk.NORMAL)
self.ld_save_btn.config(state=tk.DISABLED)
# 清空结果
for item in self.ld_tree.get_children():
self.ld_tree.delete(item)
# 获取配置
wait_time = float(self.ld_wait_time_var.get())
# 在新线程中执行测试
def run_test():
from utils.local_dimming_test import LocalDimmingTest, LocalDimmingController
# 从设备当前 timing 获取分辨率
ld_ctrl = LocalDimmingController(self.ucd)
cur_w, cur_h = ld_ctrl.get_current_resolution()
resolution = f"{cur_w}x{cur_h}"
ld_test = LocalDimmingTest(
self.ucd,
self.ca,
log_callback=self.log_gui.log,
)
ld_test.wait_time = wait_time
results = ld_test.run_test(resolution=resolution)
# 保存到实例变量
self.ld_test_instance = ld_test
self.ld_test_results = results
# 更新结果显示
self.root.after(0, lambda: self.update_ld_results(results))
# 清理临时文件
ld_test.cleanup()
# 恢复按钮状态
self.root.after(0, lambda: self.ld_start_btn.config(state=tk.NORMAL))
self.root.after(0, lambda: self.ld_stop_btn.config(state=tk.DISABLED))
self.root.after(0, lambda: self.ld_save_btn.config(state=tk.NORMAL))
threading.Thread(target=run_test, daemon=True).start()
def update_ld_results(self, results):
"""更新 Local Dimming 结果显示"""
for percentage, x, y, lv, X, Y, Z in results:
self.ld_tree.insert(
"",
tk.END,
values=(f"{percentage}%", f"{lv:.2f}", f"{x:.4f}", f"{y:.4f}"),
)
def stop_local_dimming_test(self):
"""停止 Local Dimming 测试"""
if hasattr(self, "ld_test_instance"):
self.ld_test_instance.stop()
def save_local_dimming_results(self):
"""保存 Local Dimming 结果"""
from tkinter import filedialog
import csv
import datetime
default_name = f"LocalDimming_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
save_path = filedialog.asksaveasfilename(
title="保存测试结果",
initialfile=default_name,
defaultextension=".csv",
filetypes=[("CSV 文件", "*.csv"), ("所有文件", "*.*")],
)
if not save_path:
return
try:
with open(save_path, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerow(["窗口百分比", "x", "y", "亮度 (cd/m²)", "X", "Y", "Z"])
for item in self.ld_tree.get_children():
values = self.ld_tree.item(item, "values")
# 从 self.ld_test_results 获取完整数据
percentage_str = values[0]
percentage = int(percentage_str.replace("%", ""))
# 找到对应的完整数据
for p, x, y, lv, X, Y, Z in self.ld_test_results:
if p == percentage:
writer.writerow([f"{p}%", x, y, lv, X, Y, Z])
break
self.log_gui.log(f"✓ 测试结果已保存: {save_path}")
messagebox.showinfo("成功", f"测试结果已保存到:\n{save_path}")
except Exception as e:
self.log_gui.log(f"❌ 保存失败: {str(e)}")
messagebox.showerror("错误", f"保存失败: {str(e)}")
def send_ld_window(self, percentage):
"""发送指定百分比的窗口"""
if not self.ucd.status:
messagebox.showwarning("警告", "请先连接 UCD323 设备")
return
self.log_gui.log(f"🔆 发送 {percentage}% 窗口...")
# 记录当前百分比(用于测量)
self.current_ld_percentage = percentage
def send():
from utils.local_dimming_test import LocalDimmingController
ld_controller = LocalDimmingController(self.ucd)
# 从设备当前 timing 获取分辨率
width, height = ld_controller.get_current_resolution()
# 生成并发送图片
success = ld_controller.send_window_pattern_with_resolution(
percentage, width, height
)
if success:
self.root.after(
0, lambda: self.log_gui.log(f"{percentage}% 窗口已发送")
)
else:
self.root.after(
0, lambda: self.log_gui.log(f"{percentage}% 窗口发送失败")
)
threading.Thread(target=send, daemon=True).start()
def measure_ld_luminance(self):
"""测量当前亮度"""
if not self.ca:
messagebox.showwarning("警告", "请先连接 CA410 色度计")
return
if self.current_ld_percentage is None:
messagebox.showinfo("提示", "请先发送一个窗口图案")
return
self.log_gui.log("📏 正在采集亮度...")
def measure():
try:
x, y, lv, X, Y, Z = self.ca.readAllDisplay()
if lv is not None:
import datetime
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
# 更新显示
self.root.after(
0,
lambda: self.ld_result_label.config(
text=f"亮度: {lv:.2f} cd/m² | x: {x:.4f} | y: {y:.4f}"
),
)
# 添加到表格
self.root.after(
0,
lambda: self.ld_tree.insert(
"",
tk.END,
values=(
f"{self.current_ld_percentage}%",
f"{lv:.2f}",
f"{x:.4f}",
f"{y:.4f}",
timestamp,
),
),
)
self.root.after(
0, lambda: self.log_gui.log(f"✅ 采集完成: {lv:.2f} cd/m²")
)
else:
self.root.after(0, lambda: self.log_gui.log("❌ 采集失败"))
except Exception as e:
self.root.after(0, lambda: self.log_gui.log(f"❌ 采集异常: {str(e)}"))
threading.Thread(target=measure, daemon=True).start()
def clear_ld_records(self):
"""清空测试记录"""
for item in self.ld_tree.get_children():
self.ld_tree.delete(item)
self.ld_result_label.config(text="亮度: -- cd/m² | x: -- | y: --")
self.current_ld_percentage = None
self.log_gui.log("🗑️ 测试记录已清空")
def save_local_dimming_results(self):
"""保存 Local Dimming 结果"""
from tkinter import filedialog
import csv
import datetime
if len(self.ld_tree.get_children()) == 0:
messagebox.showinfo("提示", "没有可保存的数据")
return
default_name = f"LocalDimming_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
save_path = filedialog.asksaveasfilename(
title="保存测试结果",
initialfile=default_name,
defaultextension=".csv",
filetypes=[("CSV 文件", "*.csv"), ("所有文件", "*.*")],
)
if not save_path:
return
try:
with open(save_path, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerow(["窗口百分比", "亮度 (cd/m²)", "x", "y", "时间"])
for item in self.ld_tree.get_children():
values = self.ld_tree.item(item, "values")
writer.writerow(values)
self.log_gui.log(f"✓ 测试结果已保存: {save_path}")
messagebox.showinfo("成功", f"测试结果已保存到:\n{save_path}")
except Exception as e:
self.log_gui.log(f"❌ 保存失败: {str(e)}")
messagebox.showerror("错误", f"保存失败: {str(e)}")
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()