9752 lines
378 KiB
Python
9752 lines
378 KiB
Python
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
|
||
|
||
plt.rcParams["font.family"] = ["sans-serif"]
|
||
plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"]
|
||
|
||
|
||
def get_resource_path(relative_path):
|
||
"""
|
||
获取资源文件的绝对路径(兼容开发环境和打包后)
|
||
|
||
Args:
|
||
relative_path: 相对路径,如 "assets/cie.png"
|
||
|
||
Returns:
|
||
str: 资源文件的绝对路径
|
||
"""
|
||
try:
|
||
# PyInstaller 打包后的临时文件夹路径
|
||
base_path = sys._MEIPASS
|
||
except AttributeError:
|
||
# 开发环境:使用脚本所在目录
|
||
base_path = os.path.dirname(os.path.abspath(__file__))
|
||
|
||
return os.path.join(base_path, relative_path)
|
||
|
||
|
||
def load_icon(png_path):
|
||
"""加载并调整图标大小为64x64"""
|
||
img = Image.open(get_resource_path(png_path))
|
||
img = img.resize((24, 24), Image.LANCZOS)
|
||
return ImageTk.PhotoImage(img)
|
||
|
||
|
||
def backgroud_style_set():
|
||
style = ttk.Style()
|
||
# 移除背景色设置,使用默认背景色
|
||
style.configure(
|
||
"SidebarSelected.TButton",
|
||
# anchor="w",
|
||
padding=10,
|
||
background="#005470",
|
||
)
|
||
|
||
|
||
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):
|
||
"""初始化色域图表 - 手动设置subplot位置,完全避免重叠"""
|
||
container = ttk.Frame(self.gamut_chart_frame)
|
||
container.pack(expand=True, fill=tk.BOTH)
|
||
|
||
self.gamut_fig = plt.Figure(figsize=(14, 6), dpi=100)
|
||
self.gamut_canvas = FigureCanvasTkAgg(self.gamut_fig, master=container)
|
||
|
||
canvas_widget = self.gamut_canvas.get_tk_widget()
|
||
canvas_widget.pack(expand=True, fill=tk.BOTH)
|
||
|
||
# ✅ 恢复原来的大尺寸:0.84 高度
|
||
self.gamut_ax_xy = self.gamut_fig.add_axes(
|
||
[0.02, 0.08, 0.46, 0.84]
|
||
) # ← 改回 0.84
|
||
self.gamut_ax_uv = self.gamut_fig.add_axes(
|
||
[0.52, 0.08, 0.46, 0.84]
|
||
) # ← 改回 0.84
|
||
|
||
# 初始化XY图
|
||
self.gamut_ax_xy.set_xlim(0, 600)
|
||
self.gamut_ax_xy.set_ylim(600, 0)
|
||
self.gamut_ax_xy.axis("off")
|
||
self.gamut_ax_xy.set_clip_on(False)
|
||
|
||
# 初始化UV图
|
||
self.gamut_ax_uv.set_xlim(0, 600)
|
||
self.gamut_ax_uv.set_ylim(600, 0)
|
||
self.gamut_ax_uv.axis("off")
|
||
self.gamut_ax_uv.set_clip_on(False)
|
||
|
||
# 调整标题位置:y=0.98
|
||
self.gamut_fig.suptitle("色域测试", fontsize=12, y=0.98)
|
||
|
||
self.gamut_canvas.draw()
|
||
|
||
def init_gamma_chart(self):
|
||
"""初始化Gamma曲线图表 - 左侧曲线 + 右侧表格(✅ 4列 + 通用说明)"""
|
||
container = ttk.Frame(self.gamma_chart_frame)
|
||
container.pack(expand=True, fill=tk.BOTH)
|
||
|
||
self.gamma_fig = plt.Figure(figsize=(12, 6), dpi=100, constrained_layout=False)
|
||
self.gamma_canvas = FigureCanvasTkAgg(self.gamma_fig, master=container)
|
||
|
||
canvas_widget = self.gamma_canvas.get_tk_widget()
|
||
canvas_widget.pack(expand=True, fill=tk.BOTH)
|
||
|
||
# 左侧:Gamma 曲线
|
||
self.gamma_ax = self.gamma_fig.add_axes([0.08, 0.12, 0.50, 0.78])
|
||
self.gamma_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
||
self.gamma_ax.set_ylabel("L_bar", fontsize=10)
|
||
self.gamma_ax.set_xlim(0, 105)
|
||
self.gamma_ax.set_ylim(0, 1.1)
|
||
self.gamma_ax.grid(True, linestyle="--", alpha=0.3)
|
||
self.gamma_ax.tick_params(labelsize=9)
|
||
|
||
# 左侧提示(通用说明,不显示具体 Gamma 值)
|
||
self.gamma_ax.text(
|
||
0.5,
|
||
0.5,
|
||
"等待测试数据...\n\n"
|
||
"将显示:\n"
|
||
"• 实测曲线 (蓝色)\n"
|
||
"• 理想 Gamma 曲线 (红色)\n\n"
|
||
"Gamma 值由测试配置决定",
|
||
ha="center",
|
||
va="center",
|
||
fontsize=10,
|
||
color="gray",
|
||
transform=self.gamma_ax.transAxes,
|
||
bbox=dict(
|
||
boxstyle="round,pad=1", facecolor="white", edgecolor="gray", alpha=0.8
|
||
),
|
||
)
|
||
|
||
# 右侧:数据表格
|
||
self.gamma_table_ax = self.gamma_fig.add_axes([0.62, 0.12, 0.35, 0.78])
|
||
self.gamma_table_ax.axis("off")
|
||
|
||
# 4列表格数据
|
||
table_data = [
|
||
["灰阶", "实测亮度\n(cd/m²)", "L_bar\n(计算)", "Gamma"],
|
||
["0%", "--", "--", "--"],
|
||
["10%", "--", "--", "--"],
|
||
["20%", "--", "--", "--"],
|
||
["30%", "--", "--", "--"],
|
||
["40%", "--", "--", "--"],
|
||
["50%", "--", "--", "--"],
|
||
["60%", "--", "--", "--"],
|
||
["70%", "--", "--", "--"],
|
||
["80%", "--", "--", "--"],
|
||
["90%", "--", "--", "--"],
|
||
["100%", "--", "--", "--"],
|
||
]
|
||
|
||
table = self.gamma_table_ax.table(
|
||
cellText=table_data,
|
||
cellLoc="center",
|
||
loc="center",
|
||
colWidths=[0.18, 0.28, 0.27, 0.27], # ← 4列宽度
|
||
)
|
||
|
||
table.auto_set_font_size(False)
|
||
table.set_fontsize(7.5)
|
||
table.scale(1, 1.5)
|
||
|
||
# 表头样式
|
||
for i in range(4):
|
||
cell = table[(0, i)]
|
||
cell.set_facecolor("#4472C4")
|
||
cell.set_text_props(weight="bold", color="white", fontsize=7)
|
||
|
||
# 数据行交替颜色
|
||
for i in range(1, len(table_data)):
|
||
for j in range(4):
|
||
cell = table[(i, j)]
|
||
if i % 2 == 0:
|
||
cell.set_facecolor("#E7E6E6")
|
||
else:
|
||
cell.set_facecolor("#FFFFFF")
|
||
|
||
# 底部说明
|
||
self.gamma_table_ax.text(
|
||
0.5,
|
||
0.02,
|
||
"表格说明:\n"
|
||
"• 实测亮度: 色度计测量值 (cd/m²)\n"
|
||
"• L_bar: 归一化亮度 (0-1)\n"
|
||
"• Gamma: 实际 Gamma 值",
|
||
ha="center",
|
||
va="bottom",
|
||
fontsize=7,
|
||
color="gray",
|
||
transform=self.gamma_table_ax.transAxes,
|
||
bbox=dict(
|
||
boxstyle="round,pad=0.5",
|
||
facecolor="lightyellow",
|
||
edgecolor="gray",
|
||
alpha=0.8,
|
||
),
|
||
)
|
||
|
||
self.gamma_fig.suptitle("Gamma曲线 + 数据表格", fontsize=12, y=0.98)
|
||
self.gamma_canvas.draw()
|
||
|
||
def init_eotf_chart(self):
|
||
"""初始化 EOTF 曲线图表(HDR 专用)- 左侧曲线 + 右侧表格(✅ 4列)"""
|
||
container = ttk.Frame(self.eotf_chart_frame)
|
||
container.pack(expand=True, fill=tk.BOTH)
|
||
|
||
self.eotf_fig = plt.Figure(figsize=(12, 6), dpi=100, constrained_layout=False)
|
||
self.eotf_canvas = FigureCanvasTkAgg(self.eotf_fig, master=container)
|
||
|
||
canvas_widget = self.eotf_canvas.get_tk_widget()
|
||
canvas_widget.pack(expand=True, fill=tk.BOTH)
|
||
|
||
# 左侧:EOTF 曲线
|
||
self.eotf_ax = self.eotf_fig.add_axes([0.08, 0.12, 0.50, 0.78])
|
||
self.eotf_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
||
self.eotf_ax.set_ylabel("L_bar (归一化亮度)", fontsize=10)
|
||
self.eotf_ax.set_xlim(0, 105)
|
||
self.eotf_ax.set_ylim(0, 1.1)
|
||
self.eotf_ax.grid(True, linestyle="--", alpha=0.3)
|
||
self.eotf_ax.tick_params(labelsize=9)
|
||
|
||
# 左侧提示
|
||
self.eotf_ax.text(
|
||
0.5,
|
||
0.5,
|
||
"等待测试数据...\n\n将显示:\n• 实测 EOTF 曲线 (蓝色)\n• 理想 PQ 曲线 (红色)",
|
||
ha="center",
|
||
va="center",
|
||
fontsize=11,
|
||
color="gray",
|
||
transform=self.eotf_ax.transAxes,
|
||
bbox=dict(
|
||
boxstyle="round,pad=1", facecolor="white", edgecolor="gray", alpha=0.8
|
||
),
|
||
)
|
||
|
||
# 右侧:数据表格
|
||
self.eotf_table_ax = self.eotf_fig.add_axes([0.62, 0.12, 0.35, 0.78])
|
||
self.eotf_table_ax.axis("off")
|
||
|
||
# 4列表格数据
|
||
table_data = [
|
||
["灰阶", "实测亮度\n(cd/m²)", "L_bar\n(计算)", "EOTF γ"],
|
||
["0%", "--", "--", "--"],
|
||
["10%", "--", "--", "--"],
|
||
["20%", "--", "--", "--"],
|
||
["30%", "--", "--", "--"],
|
||
["40%", "--", "--", "--"],
|
||
["50%", "--", "--", "--"],
|
||
["60%", "--", "--", "--"],
|
||
["70%", "--", "--", "--"],
|
||
["80%", "--", "--", "--"],
|
||
["90%", "--", "--", "--"],
|
||
["100%", "--", "--", "--"],
|
||
]
|
||
|
||
table = self.eotf_table_ax.table(
|
||
cellText=table_data,
|
||
cellLoc="center",
|
||
loc="center",
|
||
colWidths=[0.18, 0.28, 0.27, 0.27],
|
||
)
|
||
|
||
table.auto_set_font_size(False)
|
||
table.set_fontsize(7.5)
|
||
table.scale(1, 1.5)
|
||
|
||
# 表头样式
|
||
for i in range(4):
|
||
cell = table[(0, i)]
|
||
cell.set_facecolor("#4472C4")
|
||
cell.set_text_props(weight="bold", color="white", fontsize=7)
|
||
|
||
# 数据行交替颜色
|
||
for i in range(1, len(table_data)):
|
||
for j in range(4):
|
||
cell = table[(i, j)]
|
||
if i % 2 == 0:
|
||
cell.set_facecolor("#E7E6E6")
|
||
else:
|
||
cell.set_facecolor("#FFFFFF")
|
||
|
||
# 底部说明
|
||
self.eotf_table_ax.text(
|
||
0.5,
|
||
0.02,
|
||
"表格说明:\n"
|
||
"• 实测亮度: 色度计测量值 (cd/m²)\n"
|
||
"• L_bar: 归一化亮度 (0-1)\n"
|
||
"• EOTF γ: HDR 实际 Gamma 值",
|
||
ha="center",
|
||
va="bottom",
|
||
fontsize=7,
|
||
color="gray",
|
||
transform=self.eotf_table_ax.transAxes,
|
||
bbox=dict(
|
||
boxstyle="round,pad=0.5",
|
||
facecolor="lightyellow",
|
||
edgecolor="gray",
|
||
alpha=0.8,
|
||
),
|
||
)
|
||
|
||
self.eotf_fig.suptitle("EOTF 曲线 + 数据表格", fontsize=12, y=0.98)
|
||
self.eotf_canvas.draw()
|
||
|
||
def init_cct_chart(self):
|
||
"""初始化色度坐标图表 - 正向横坐标,标题居中最上方"""
|
||
container = ttk.Frame(self.cct_chart_frame)
|
||
container.pack(expand=True)
|
||
|
||
self.cct_fig = plt.Figure(figsize=(8, 6), dpi=100, tight_layout=False)
|
||
self.cct_canvas = FigureCanvasTkAgg(self.cct_fig, master=container)
|
||
|
||
canvas_widget = self.cct_canvas.get_tk_widget()
|
||
canvas_widget.pack()
|
||
canvas_widget.config(width=800, height=600)
|
||
canvas_widget.pack_propagate(False)
|
||
|
||
self.cct_ax1 = self.cct_fig.add_subplot(211)
|
||
self.cct_ax2 = self.cct_fig.add_subplot(212)
|
||
|
||
# 上图:x coordinates
|
||
self.cct_ax1.set_xlabel("灰阶 (%)", fontsize=9)
|
||
self.cct_ax1.set_ylabel("CIE x", fontsize=9)
|
||
self.cct_ax1.set_xlim(0, 105)
|
||
self.cct_ax1.set_ylim(0.25, 0.35)
|
||
self.cct_ax1.grid(True, linestyle="--", alpha=0.3)
|
||
self.cct_ax1.tick_params(labelsize=8)
|
||
|
||
# 下图:y coordinates
|
||
self.cct_ax2.set_xlabel("灰阶 (%)", fontsize=9)
|
||
self.cct_ax2.set_ylabel("CIE y", fontsize=9)
|
||
self.cct_ax2.set_xlim(0, 105)
|
||
self.cct_ax2.set_ylim(0.25, 0.35)
|
||
self.cct_ax2.grid(True, linestyle="--", alpha=0.3)
|
||
self.cct_ax2.tick_params(labelsize=8)
|
||
|
||
# 调整标题位置:y=0.985(比色域/Gamma略高)
|
||
self.cct_fig.suptitle("色度一致性测试", fontsize=12, y=0.985)
|
||
|
||
self.cct_fig.subplots_adjust(
|
||
left=0.12,
|
||
right=0.88,
|
||
top=0.90,
|
||
bottom=0.08,
|
||
hspace=0.25,
|
||
)
|
||
|
||
self.cct_canvas.draw()
|
||
|
||
def init_contrast_chart(self):
|
||
"""初始化对比度图表 - 固定大小,居中显示"""
|
||
container = ttk.Frame(self.contrast_chart_frame)
|
||
container.pack(expand=True)
|
||
|
||
self.contrast_fig = plt.Figure(
|
||
figsize=(6, 6),
|
||
dpi=100,
|
||
tight_layout=False,
|
||
)
|
||
self.contrast_canvas = FigureCanvasTkAgg(self.contrast_fig, master=container)
|
||
|
||
canvas_widget = self.contrast_canvas.get_tk_widget()
|
||
canvas_widget.pack()
|
||
|
||
canvas_widget.config(width=600, height=600)
|
||
canvas_widget.pack_propagate(False)
|
||
|
||
self.contrast_ax = self.contrast_fig.add_subplot(111)
|
||
self.contrast_ax.set_xlim(0, 1)
|
||
self.contrast_ax.set_ylim(0, 1)
|
||
self.contrast_ax.axis("off")
|
||
|
||
# 调整标题位置:y=0.985
|
||
self.contrast_fig.suptitle("对比度测试", fontsize=12, y=0.985)
|
||
|
||
self.contrast_fig.subplots_adjust(
|
||
left=0.02,
|
||
right=0.98,
|
||
top=0.90,
|
||
bottom=0.02,
|
||
)
|
||
|
||
self.contrast_canvas.draw()
|
||
|
||
def init_accuracy_chart(self):
|
||
"""初始化色准图表 - 固定大小,居中显示"""
|
||
container = ttk.Frame(self.accuracy_chart_frame)
|
||
container.pack(expand=True)
|
||
|
||
self.accuracy_fig = plt.Figure(
|
||
figsize=(10, 6),
|
||
dpi=100,
|
||
tight_layout=False,
|
||
)
|
||
self.accuracy_canvas = FigureCanvasTkAgg(self.accuracy_fig, master=container)
|
||
|
||
canvas_widget = self.accuracy_canvas.get_tk_widget()
|
||
canvas_widget.pack()
|
||
|
||
canvas_widget.config(width=1000, height=600)
|
||
canvas_widget.pack_propagate(False)
|
||
|
||
self.accuracy_ax = self.accuracy_fig.add_subplot(111)
|
||
self.accuracy_ax.set_xlim(0, 1)
|
||
self.accuracy_ax.set_ylim(0, 1)
|
||
self.accuracy_ax.axis("off")
|
||
|
||
# 调整标题位置
|
||
self.accuracy_fig.suptitle("色准测试", fontsize=12, y=0.985)
|
||
|
||
self.accuracy_fig.subplots_adjust(
|
||
left=0.05,
|
||
right=0.95,
|
||
top=0.90,
|
||
bottom=0.05,
|
||
)
|
||
|
||
self.accuracy_canvas.draw()
|
||
|
||
def clear_chart(self):
|
||
"""清空所有图表"""
|
||
|
||
# ========== 1. 清空色域图表 ==========
|
||
if hasattr(self, "gamut_ax_xy") and hasattr(self, "gamut_ax_uv"):
|
||
# 清空XY图
|
||
self.gamut_ax_xy.clear()
|
||
self.gamut_ax_xy.set_xlim(0, 600)
|
||
self.gamut_ax_xy.set_ylim(600, 0)
|
||
self.gamut_ax_xy.axis("off")
|
||
self.gamut_ax_xy.set_clip_on(False)
|
||
|
||
# 清空UV图
|
||
self.gamut_ax_uv.clear()
|
||
self.gamut_ax_uv.set_xlim(0, 600)
|
||
self.gamut_ax_uv.set_ylim(600, 0)
|
||
self.gamut_ax_uv.axis("off")
|
||
self.gamut_ax_uv.set_clip_on(False)
|
||
|
||
self.gamut_fig.suptitle("色域测试", fontsize=12, y=0.98)
|
||
self.gamut_canvas.draw()
|
||
|
||
# ========== 2. 清空Gamma图表(4列 + 通用说明)==========
|
||
if hasattr(self, "gamma_ax") and hasattr(self, "gamma_table_ax"):
|
||
# 清空左侧曲线
|
||
self.gamma_ax.clear()
|
||
self.gamma_ax.set_xlim(0, 105)
|
||
self.gamma_ax.set_ylim(0, 1.1)
|
||
self.gamma_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
||
self.gamma_ax.set_ylabel("L_bar", fontsize=10)
|
||
self.gamma_ax.grid(True, linestyle="--", alpha=0.3)
|
||
self.gamma_ax.tick_params(labelsize=9)
|
||
|
||
# 左侧提示
|
||
self.gamma_ax.text(
|
||
0.5,
|
||
0.5,
|
||
"等待测试数据...\n\n"
|
||
"将显示:\n"
|
||
"• 实测曲线 (蓝色)\n"
|
||
"• 理想 Gamma 曲线 (红色)\n\n"
|
||
"Gamma 值由测试配置决定",
|
||
ha="center",
|
||
va="center",
|
||
fontsize=10,
|
||
color="gray",
|
||
transform=self.gamma_ax.transAxes,
|
||
bbox=dict(
|
||
boxstyle="round,pad=1",
|
||
facecolor="white",
|
||
edgecolor="gray",
|
||
alpha=0.8,
|
||
),
|
||
)
|
||
|
||
# 清空右侧表格
|
||
self.gamma_table_ax.clear()
|
||
self.gamma_table_ax.axis("off")
|
||
|
||
# 4列表格
|
||
table_data = [
|
||
["灰阶", "实测亮度\n(cd/m²)", "L_bar\n(计算)", "Gamma"],
|
||
["0%", "--", "--", "--"],
|
||
["10%", "--", "--", "--"],
|
||
["20%", "--", "--", "--"],
|
||
["30%", "--", "--", "--"],
|
||
["40%", "--", "--", "--"],
|
||
["50%", "--", "--", "--"],
|
||
["60%", "--", "--", "--"],
|
||
["70%", "--", "--", "--"],
|
||
["80%", "--", "--", "--"],
|
||
["90%", "--", "--", "--"],
|
||
["100%", "--", "--", "--"],
|
||
]
|
||
|
||
table = self.gamma_table_ax.table(
|
||
cellText=table_data,
|
||
cellLoc="center",
|
||
loc="center",
|
||
colWidths=[0.18, 0.28, 0.27, 0.27],
|
||
)
|
||
|
||
table.auto_set_font_size(False)
|
||
table.set_fontsize(7.5)
|
||
table.scale(1, 1.5)
|
||
|
||
# 表头样式
|
||
for i in range(4):
|
||
cell = table[(0, i)]
|
||
cell.set_facecolor("#4472C4")
|
||
cell.set_text_props(weight="bold", color="white", fontsize=7)
|
||
|
||
# 数据行交替颜色
|
||
for i in range(1, len(table_data)):
|
||
for j in range(4):
|
||
cell = table[(i, j)]
|
||
if i % 2 == 0:
|
||
cell.set_facecolor("#E7E6E6")
|
||
else:
|
||
cell.set_facecolor("#FFFFFF")
|
||
|
||
# 底部说明
|
||
self.gamma_table_ax.text(
|
||
0.5,
|
||
0.02,
|
||
"表格说明:\n"
|
||
"• 实测亮度: 色度计测量值 (cd/m²)\n"
|
||
"• L_bar: 归一化亮度 (0-1)\n"
|
||
"• Gamma: 实际 Gamma 值",
|
||
ha="center",
|
||
va="bottom",
|
||
fontsize=7,
|
||
color="gray",
|
||
transform=self.gamma_table_ax.transAxes,
|
||
bbox=dict(
|
||
boxstyle="round,pad=0.5",
|
||
facecolor="lightyellow",
|
||
edgecolor="gray",
|
||
alpha=0.8,
|
||
),
|
||
)
|
||
|
||
self.gamma_fig.suptitle("Gamma曲线 + 数据表格", fontsize=12, y=0.98)
|
||
self.gamma_canvas.draw()
|
||
|
||
# ========== 3. 清空EOTF图表(4列)==========
|
||
if hasattr(self, "eotf_ax") and hasattr(self, "eotf_table_ax"):
|
||
# 清空左侧曲线
|
||
self.eotf_ax.clear()
|
||
self.eotf_ax.set_xlim(0, 105)
|
||
self.eotf_ax.set_ylim(0, 1.1)
|
||
self.eotf_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
||
self.eotf_ax.set_ylabel("L_bar (归一化亮度)", fontsize=10)
|
||
self.eotf_ax.grid(True, linestyle="--", alpha=0.3)
|
||
self.eotf_ax.tick_params(labelsize=9)
|
||
|
||
# 左侧提示
|
||
self.eotf_ax.text(
|
||
0.5,
|
||
0.5,
|
||
"等待测试数据...\n\n将显示:\n• 实测 EOTF 曲线 (蓝色)\n• 理想 PQ 曲线 (红色)",
|
||
ha="center",
|
||
va="center",
|
||
fontsize=11,
|
||
color="gray",
|
||
transform=self.eotf_ax.transAxes,
|
||
bbox=dict(
|
||
boxstyle="round,pad=1",
|
||
facecolor="white",
|
||
edgecolor="gray",
|
||
alpha=0.8,
|
||
),
|
||
)
|
||
|
||
# 清空右侧表格
|
||
self.eotf_table_ax.clear()
|
||
self.eotf_table_ax.axis("off")
|
||
|
||
# 4列表格
|
||
table_data = [
|
||
["灰阶", "实测亮度\n(cd/m²)", "L_bar\n(计算)", "EOTF γ"],
|
||
["0%", "--", "--", "--"],
|
||
["10%", "--", "--", "--"],
|
||
["20%", "--", "--", "--"],
|
||
["30%", "--", "--", "--"],
|
||
["40%", "--", "--", "--"],
|
||
["50%", "--", "--", "--"],
|
||
["60%", "--", "--", "--"],
|
||
["70%", "--", "--", "--"],
|
||
["80%", "--", "--", "--"],
|
||
["90%", "--", "--", "--"],
|
||
["100%", "--", "--", "--"],
|
||
]
|
||
|
||
table = self.eotf_table_ax.table(
|
||
cellText=table_data,
|
||
cellLoc="center",
|
||
loc="center",
|
||
colWidths=[0.18, 0.28, 0.27, 0.27],
|
||
)
|
||
|
||
table.auto_set_font_size(False)
|
||
table.set_fontsize(7.5)
|
||
table.scale(1, 1.5)
|
||
|
||
# 表头样式
|
||
for i in range(4):
|
||
cell = table[(0, i)]
|
||
cell.set_facecolor("#4472C4")
|
||
cell.set_text_props(weight="bold", color="white", fontsize=7)
|
||
|
||
# 数据行交替颜色
|
||
for i in range(1, len(table_data)):
|
||
for j in range(4):
|
||
cell = table[(i, j)]
|
||
if i % 2 == 0:
|
||
cell.set_facecolor("#E7E6E6")
|
||
else:
|
||
cell.set_facecolor("#FFFFFF")
|
||
|
||
# 底部说明
|
||
self.eotf_table_ax.text(
|
||
0.5,
|
||
0.02,
|
||
"表格说明:\n"
|
||
"• 实测亮度: 色度计测量值 (cd/m²)\n"
|
||
"• L_bar: 归一化亮度 (0-1)\n"
|
||
"• EOTF γ: HDR 实际 Gamma 值",
|
||
ha="center",
|
||
va="bottom",
|
||
fontsize=7,
|
||
color="gray",
|
||
transform=self.eotf_table_ax.transAxes,
|
||
bbox=dict(
|
||
boxstyle="round,pad=0.5",
|
||
facecolor="lightyellow",
|
||
edgecolor="gray",
|
||
alpha=0.8,
|
||
),
|
||
)
|
||
|
||
self.eotf_fig.suptitle("EOTF 曲线 + 数据表格", fontsize=12, y=0.98)
|
||
self.eotf_canvas.draw()
|
||
|
||
# ========== 4. 清空色度图表 ==========
|
||
if hasattr(self, "cct_ax1") and hasattr(self, "cct_ax2"):
|
||
# 上图:x coordinates
|
||
self.cct_ax1.clear()
|
||
self.cct_ax1.set_xlabel("灰阶 (%)", fontsize=9)
|
||
self.cct_ax1.set_ylabel("CIE x", fontsize=9)
|
||
self.cct_ax1.set_xlim(0, 105)
|
||
self.cct_ax1.set_ylim(0.25, 0.35)
|
||
self.cct_ax1.grid(True, linestyle="--", alpha=0.3)
|
||
self.cct_ax1.tick_params(labelsize=8)
|
||
|
||
# 下图:y coordinates
|
||
self.cct_ax2.clear()
|
||
self.cct_ax2.set_xlabel("灰阶 (%)", fontsize=9)
|
||
self.cct_ax2.set_ylabel("CIE y", fontsize=9)
|
||
self.cct_ax2.set_xlim(0, 105)
|
||
self.cct_ax2.set_ylim(0.25, 0.35)
|
||
self.cct_ax2.grid(True, linestyle="--", alpha=0.3)
|
||
self.cct_ax2.tick_params(labelsize=8)
|
||
|
||
self.cct_fig.suptitle("色度一致性测试", fontsize=12, y=0.985)
|
||
|
||
# 重置布局
|
||
self.cct_fig.subplots_adjust(
|
||
left=0.12,
|
||
right=0.88,
|
||
top=0.90,
|
||
bottom=0.08,
|
||
hspace=0.25,
|
||
)
|
||
|
||
self.cct_canvas.draw()
|
||
|
||
# ========== 5. 清空对比度图表 ==========
|
||
if hasattr(self, "contrast_ax"):
|
||
self.contrast_ax.clear()
|
||
self.contrast_ax.set_xlim(0, 1)
|
||
self.contrast_ax.set_ylim(0, 1)
|
||
self.contrast_ax.axis("off")
|
||
|
||
self.contrast_fig.suptitle("对比度测试", fontsize=12, y=0.985)
|
||
|
||
# 重置布局
|
||
self.contrast_fig.subplots_adjust(
|
||
left=0.02,
|
||
right=0.98,
|
||
top=0.90,
|
||
bottom=0.02,
|
||
)
|
||
|
||
self.contrast_canvas.draw()
|
||
|
||
# ========== 6. 清空色准图表 ==========
|
||
if hasattr(self, "accuracy_ax"):
|
||
self.accuracy_ax.clear()
|
||
self.accuracy_ax.set_xlim(0, 1)
|
||
self.accuracy_ax.set_ylim(0, 1)
|
||
self.accuracy_ax.axis("off")
|
||
|
||
# 标题
|
||
self.accuracy_fig.suptitle("色准测试", fontsize=12, y=0.985)
|
||
|
||
# 重置布局
|
||
self.accuracy_fig.subplots_adjust(
|
||
left=0.05,
|
||
right=0.95,
|
||
top=0.90,
|
||
bottom=0.05,
|
||
)
|
||
|
||
self.accuracy_canvas.draw()
|
||
|
||
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(2) # ← 色度图是第3个Tab(索引2)
|
||
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(0) # 色域图是第1个Tab
|
||
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):
|
||
"""根据测试项目复选框状态更新图表Tab的启用/禁用"""
|
||
if not hasattr(self, "chart_notebook"):
|
||
return
|
||
|
||
selected_items = self.get_selected_test_items()
|
||
current_test_type = self.config.current_test_type
|
||
|
||
# 检查5个独立图表的选中状态
|
||
gamut_selected = "gamut" in selected_items
|
||
gamma_selected = "gamma" in selected_items or "eotf" in selected_items
|
||
cct_selected = "cct" in selected_items
|
||
contrast_selected = "contrast" in selected_items
|
||
accuracy_selected = "accuracy" in selected_items
|
||
|
||
# 屏模组测试时,强制隐藏色准 Tab
|
||
if current_test_type == "screen_module":
|
||
accuracy_selected = False
|
||
|
||
try:
|
||
# 获取当前所有 Tab
|
||
current_tabs = self.chart_notebook.tabs()
|
||
current_tab_set = set(current_tabs)
|
||
|
||
gamut_tab_id = str(self.gamut_chart_frame)
|
||
gamma_tab_id = str(self.gamma_chart_frame)
|
||
eotf_tab_id = str(self.eotf_chart_frame)
|
||
cct_tab_id = str(self.cct_chart_frame)
|
||
contrast_tab_id = str(self.contrast_chart_frame)
|
||
accuracy_tab_id = str(self.accuracy_chart_frame)
|
||
|
||
# ========== 控制前4个固定 Tab ==========
|
||
if gamut_tab_id in current_tab_set:
|
||
self.chart_notebook.tab(
|
||
gamut_tab_id, state="normal" if gamut_selected else "disabled"
|
||
)
|
||
|
||
if gamma_tab_id in current_tab_set:
|
||
self.chart_notebook.tab(
|
||
gamma_tab_id, state="normal" if gamma_selected else "disabled"
|
||
)
|
||
elif eotf_tab_id in current_tab_set:
|
||
self.chart_notebook.tab(
|
||
eotf_tab_id, state="normal" if gamma_selected else "disabled"
|
||
)
|
||
|
||
if cct_tab_id in current_tab_set:
|
||
self.chart_notebook.tab(
|
||
cct_tab_id, state="normal" if cct_selected else "disabled"
|
||
)
|
||
|
||
if contrast_tab_id in current_tab_set:
|
||
self.chart_notebook.tab(
|
||
contrast_tab_id,
|
||
state="normal" if contrast_selected else "disabled",
|
||
)
|
||
|
||
# ========== 控制色准 Tab(动态添加/移除)==========
|
||
accuracy_tab_exists = accuracy_tab_id in current_tab_set
|
||
|
||
if accuracy_selected and not accuracy_tab_exists:
|
||
# 需要显示色准,但当前没有 → 添加
|
||
self.chart_notebook.add(self.accuracy_chart_frame, text="色准")
|
||
self.chart_notebook.tab(accuracy_tab_id, state="normal")
|
||
self.log_gui.log("✓ 色准 Tab 已显示")
|
||
|
||
elif accuracy_selected and accuracy_tab_exists:
|
||
# 需要显示色准,且已存在 → 启用
|
||
self.chart_notebook.tab(accuracy_tab_id, state="normal")
|
||
|
||
elif not accuracy_selected and accuracy_tab_exists:
|
||
# 不需要显示色准,但存在 → 移除
|
||
self.chart_notebook.forget(self.accuracy_chart_frame)
|
||
self.log_gui.log("✓ 色准 Tab 已隐藏")
|
||
|
||
except Exception as e:
|
||
self.log_gui.log(f"更新Tab状态失败: {str(e)}")
|
||
|
||
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):
|
||
"""创建结果图表区域 - 6个独立Tab(Gamma 和 EOTF 分离)"""
|
||
# 创建Notebook用于图表切换
|
||
self.chart_notebook = ttk.Notebook(self.result_frame)
|
||
self.chart_notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||
|
||
# ========== 创建6个独立的Tab页面 ==========
|
||
# 1. 色域图页面
|
||
self.gamut_chart_frame = ttk.Frame(self.chart_notebook)
|
||
|
||
# 2. Gamma图页面(SDR/屏模组使用)
|
||
self.gamma_chart_frame = ttk.Frame(self.chart_notebook)
|
||
|
||
# 3. EOTF图页面(HDR专用)
|
||
self.eotf_chart_frame = ttk.Frame(self.chart_notebook)
|
||
|
||
# 4. 色度一致性页面
|
||
self.cct_chart_frame = ttk.Frame(self.chart_notebook)
|
||
|
||
# 5. 对比度页面
|
||
self.contrast_chart_frame = ttk.Frame(self.chart_notebook)
|
||
|
||
# 6. 色准页面
|
||
self.accuracy_chart_frame = ttk.Frame(self.chart_notebook)
|
||
|
||
# 7. 客户模板结果页面
|
||
self.custom_template_tab_frame = ttk.Frame(self.chart_notebook)
|
||
|
||
# ========== 添加到Notebook(初始只添加前5个)==========
|
||
self.chart_notebook.add(self.gamut_chart_frame, text="色域图")
|
||
self.chart_notebook.add(self.gamma_chart_frame, text="Gamma曲线")
|
||
# ← EOTF 不添加,由 change_test_type() 动态控制
|
||
self.chart_notebook.add(self.cct_chart_frame, text="色度一致性")
|
||
self.chart_notebook.add(self.contrast_chart_frame, text="对比度")
|
||
self.chart_notebook.add(self.accuracy_chart_frame, text="色准")
|
||
|
||
# 初始化六个独立的图表
|
||
self.init_gamut_chart()
|
||
self.init_gamma_chart()
|
||
self.init_eotf_chart()
|
||
self.init_cct_chart()
|
||
self.init_contrast_chart()
|
||
self.init_accuracy_chart()
|
||
|
||
# 绑定Tab切换事件
|
||
self.chart_notebook.bind("<<NotebookTabChanged>>", self.on_chart_tab_changed)
|
||
|
||
# ==================== ✅ 在图表下方创建单步调试面板 ====================
|
||
self.debug_container = ttk.LabelFrame(
|
||
self.result_frame, # ← 放在 result_frame 内,图表正下方
|
||
text="🔧 单步调试",
|
||
padding=10,
|
||
)
|
||
# 默认不显示
|
||
|
||
# 创建单步调试面板实例
|
||
self.debug_panel = PQDebugPanel(self.debug_container, self)
|
||
|
||
self.log_gui.log("✓ 单步调试面板已创建(放在测试结果图表下方)")
|
||
|
||
def on_chart_tab_changed(self, event):
|
||
"""Tab切换时的事件处理"""
|
||
try:
|
||
self._last_tab_index = self.chart_notebook.index(
|
||
self.chart_notebook.select()
|
||
)
|
||
except Exception as e:
|
||
self.log_gui.log(f"Tab切换事件处理失败: {str(e)}")
|
||
|
||
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(0) # ← 选中第一个Tab(色域图)
|
||
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 Range,RGB 保持不变")
|
||
|
||
# ✅ 创建临时配置对象(不修改 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 Range,RGB 保持不变")
|
||
|
||
# ✅ 创建临时配置对象
|
||
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
|
||
):
|
||
"""
|
||
计算 ΔE 2000 色差(修正版)
|
||
|
||
Args:
|
||
measured_x, measured_y: 测量的 xy 坐标
|
||
measured_lv: 测量的亮度(cd/m²) # ← 新增
|
||
standard_x, standard_y: 标准的 xy 坐标
|
||
|
||
Returns:
|
||
float: ΔE 2000 色差值
|
||
"""
|
||
import math
|
||
|
||
# ========== 1. xy → XYZ(使用实际亮度)==========
|
||
def xy_to_XYZ(x, y, Y): # ← 修改:接收 Y 参数
|
||
if y == 0:
|
||
return 0, 0, 0
|
||
X = x * Y / y
|
||
Z = (1 - x - y) * Y / y
|
||
return X, Y, Z
|
||
|
||
# 修复:使用实际测量的亮度
|
||
X1, Y1, Z1 = xy_to_XYZ(measured_x, measured_y, measured_lv)
|
||
|
||
# 修复:标准值使用相同的参考亮度(只比较色度差异)
|
||
X2, Y2, Z2 = xy_to_XYZ(standard_x, standard_y, measured_lv)
|
||
|
||
# ========== 2. XYZ → Lab(D65 白点)==========
|
||
def XYZ_to_Lab(X, Y, Z):
|
||
# D65 白点
|
||
Xn, Yn, Zn = 95.047, 100.000, 108.883
|
||
|
||
# 归一化
|
||
xr = X / Xn
|
||
yr = Y / Yn
|
||
zr = Z / Zn
|
||
|
||
# f(t) 函数
|
||
def f(t):
|
||
delta = 6.0 / 29.0
|
||
if t > delta**3:
|
||
return t ** (1.0 / 3.0)
|
||
else:
|
||
return t / (3 * delta**2) + 4.0 / 29.0
|
||
|
||
fx = f(xr)
|
||
fy = f(yr)
|
||
fz = f(zr)
|
||
|
||
L = 116 * fy - 16
|
||
a = 500 * (fx - fy)
|
||
b = 200 * (fy - fz)
|
||
|
||
return L, a, b
|
||
|
||
L1, a1, b1 = XYZ_to_Lab(X1, Y1, Z1)
|
||
L2, a2, b2 = XYZ_to_Lab(X2, Y2, Z2)
|
||
|
||
# ========== 3. ΔE 2000 公式(保持不变)==========
|
||
L_bar = (L1 + L2) / 2.0
|
||
C1 = math.sqrt(a1**2 + b1**2)
|
||
C2 = math.sqrt(a2**2 + b2**2)
|
||
C_bar = (C1 + C2) / 2.0
|
||
|
||
G = 0.5 * (1 - math.sqrt(C_bar**7 / (C_bar**7 + 25**7)))
|
||
|
||
a1_prime = a1 * (1 + G)
|
||
a2_prime = a2 * (1 + G)
|
||
|
||
C1_prime = math.sqrt(a1_prime**2 + b1**2)
|
||
C2_prime = math.sqrt(a2_prime**2 + b2**2)
|
||
C_bar_prime = (C1_prime + C2_prime) / 2.0
|
||
|
||
def calc_hue(a_prime, b):
|
||
if a_prime == 0 and b == 0:
|
||
return 0
|
||
h = math.atan2(b, a_prime) * 180 / math.pi
|
||
if h < 0:
|
||
h += 360
|
||
return h
|
||
|
||
h1_prime = calc_hue(a1_prime, b1)
|
||
h2_prime = calc_hue(a2_prime, b2)
|
||
|
||
if C1_prime == 0 or C2_prime == 0:
|
||
delta_h_prime = 0
|
||
else:
|
||
delta_h = h2_prime - h1_prime
|
||
if abs(delta_h) <= 180:
|
||
delta_h_prime = delta_h
|
||
elif delta_h > 180:
|
||
delta_h_prime = delta_h - 360
|
||
else:
|
||
delta_h_prime = delta_h + 360
|
||
|
||
if C1_prime == 0 or C2_prime == 0:
|
||
H_bar_prime = h1_prime + h2_prime
|
||
else:
|
||
if abs(h1_prime - h2_prime) <= 180:
|
||
H_bar_prime = (h1_prime + h2_prime) / 2.0
|
||
elif h1_prime + h2_prime < 360:
|
||
H_bar_prime = (h1_prime + h2_prime + 360) / 2.0
|
||
else:
|
||
H_bar_prime = (h1_prime + h2_prime - 360) / 2.0
|
||
|
||
delta_L_prime = L2 - L1
|
||
delta_C_prime = C2_prime - C1_prime
|
||
delta_H_prime = (
|
||
2
|
||
* math.sqrt(C1_prime * C2_prime)
|
||
* math.sin(math.radians(delta_h_prime / 2.0))
|
||
)
|
||
|
||
S_L = 1 + (0.015 * (L_bar - 50) ** 2) / math.sqrt(20 + (L_bar - 50) ** 2)
|
||
S_C = 1 + 0.045 * C_bar_prime
|
||
|
||
T = (
|
||
1
|
||
- 0.17 * math.cos(math.radians(H_bar_prime - 30))
|
||
+ 0.24 * math.cos(math.radians(2 * H_bar_prime))
|
||
+ 0.32 * math.cos(math.radians(3 * H_bar_prime + 6))
|
||
- 0.20 * math.cos(math.radians(4 * H_bar_prime - 63))
|
||
)
|
||
|
||
S_H = 1 + 0.015 * C_bar_prime * T
|
||
|
||
delta_theta = 30 * math.exp(-(((H_bar_prime - 275) / 25) ** 2))
|
||
R_C = 2 * math.sqrt(C_bar_prime**7 / (C_bar_prime**7 + 25**7))
|
||
R_T = -R_C * math.sin(math.radians(2 * delta_theta))
|
||
|
||
kL = 1.0
|
||
kC = 1.0
|
||
kH = 1.0
|
||
|
||
delta_E = math.sqrt(
|
||
(delta_L_prime / (kL * S_L)) ** 2
|
||
+ (delta_C_prime / (kC * S_C)) ** 2
|
||
+ (delta_H_prime / (kH * S_H)) ** 2
|
||
+ R_T * (delta_C_prime / (kC * S_C)) * (delta_H_prime / (kH * S_H))
|
||
)
|
||
|
||
return delta_E
|
||
|
||
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 2000,Gamma {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):
|
||
"""
|
||
获取色准测试的标准 xy 色度坐标(动态计算)
|
||
|
||
Args:
|
||
test_type: 测试类型 ("sdr_movie" 或 "hdr_movie")
|
||
|
||
Returns:
|
||
dict: {color_name: (x, y), ...}
|
||
"""
|
||
|
||
# ========== RGB → xy 转换函数 ==========
|
||
def rgb_to_xy_srgb(r, g, b):
|
||
"""sRGB (8bit) → CIE 1931 xy"""
|
||
# 1. 归一化到 0-1
|
||
r, g, b = r / 255.0, g / 255.0, b / 255.0
|
||
|
||
# 2. sRGB Gamma 解码
|
||
def gamma_decode(c):
|
||
if c <= 0.04045:
|
||
return c / 12.92
|
||
else:
|
||
return ((c + 0.055) / 1.055) ** 2.4
|
||
|
||
r_linear = gamma_decode(r)
|
||
g_linear = gamma_decode(g)
|
||
b_linear = gamma_decode(b)
|
||
|
||
# 3. sRGB → XYZ(D65 白点,IEC 61966-2-1 标准)
|
||
X = r_linear * 0.4124564 + g_linear * 0.3575761 + b_linear * 0.1804375
|
||
Y = r_linear * 0.2126729 + g_linear * 0.7151522 + b_linear * 0.0721750
|
||
Z = r_linear * 0.0193339 + g_linear * 0.1191920 + b_linear * 0.9503041
|
||
|
||
# 4. XYZ → xy
|
||
total = X + Y + Z
|
||
if total == 0:
|
||
return 0.3127, 0.3290 # D65 白点
|
||
|
||
x = X / total
|
||
y = Y / total
|
||
return x, y
|
||
|
||
# ========== 你的 RGB 定义(29色)==========
|
||
SDR_COLOR_PATTERNS = [
|
||
("White", 255, 255, 255),
|
||
("Gray 80", 230, 230, 230),
|
||
("Gray 65", 209, 209, 209),
|
||
("Gray 50", 186, 186, 186),
|
||
("Gray 35", 158, 158, 158),
|
||
("Dark Skin", 115, 82, 66),
|
||
("Light Skin", 194, 150, 130),
|
||
("Blue Sky", 94, 122, 156),
|
||
("Foliage", 89, 107, 66),
|
||
("Blue Flower", 130, 128, 176),
|
||
("Bluish Green", 99, 189, 168),
|
||
("Orange", 217, 120, 41),
|
||
("Purplish Blue", 74, 92, 163),
|
||
("Moderate Red", 194, 84, 97),
|
||
("Purple", 92, 61, 107),
|
||
("Yellow Green", 158, 186, 64),
|
||
("Orange Yellow", 230, 161, 46),
|
||
("Blue (Legacy)", 51, 61, 150),
|
||
("Green (Legacy)", 71, 148, 71),
|
||
("Red (Legacy)", 176, 48, 59),
|
||
("Yellow (Legacy)", 237, 199, 33),
|
||
("Magenta (Legacy)", 186, 84, 145),
|
||
("Cyan (Legacy)", 0, 133, 163),
|
||
("100% Red", 255, 0, 0),
|
||
("100% Green", 0, 255, 0),
|
||
("100% Blue", 0, 0, 255),
|
||
("100% Cyan", 0, 255, 255),
|
||
("100% Magenta", 255, 0, 255),
|
||
("100% Yellow", 255, 255, 0),
|
||
]
|
||
|
||
# ========== 动态计算 xy 坐标 ==========
|
||
standards = {}
|
||
for name, r, g, b in SDR_COLOR_PATTERNS:
|
||
x, y = rgb_to_xy_srgb(r, g, b)
|
||
standards[name] = (x, y)
|
||
|
||
return standards
|
||
|
||
def calculate_gamut_coverage(self, results):
|
||
"""计算色域覆盖率"""
|
||
area, coverage = pq_algorithm.calculate_gamut_coverage_DCIP3(results)
|
||
return area, coverage
|
||
|
||
def calculate_gamma(self, results, max_index_fix, pattern_params=None):
|
||
"""计算Gamma值,返回results + gamma
|
||
|
||
Args:
|
||
results: 测量结果列表
|
||
max_index_fix: 最大灰阶索引
|
||
pattern_params: 8bit pattern参数,用于计算input_level(22293 Gamma数据对齐)
|
||
"""
|
||
results_with_gamma_list = pq_algorithm.calculate_gamma(
|
||
results, max_index_fix, pattern_params
|
||
)
|
||
L_bar = pq_algorithm.calculate_L_bar(results)
|
||
return results_with_gamma_list, L_bar
|
||
|
||
def calculate_color_accuracy(self, measured, standard):
|
||
"""计算色差"""
|
||
# 使用简化的色差计算方法
|
||
delta_E = {}
|
||
|
||
for color in measured.keys():
|
||
# 计算欧氏距离作为简化的色差
|
||
dx = measured[color][0] - standard[color][0]
|
||
dy = measured[color][1] - standard[color][1]
|
||
delta_E[color] = np.sqrt(dx * dx + dy * dy) * 1000 # 放大1000倍便于显示
|
||
|
||
return delta_E
|
||
|
||
def plot_gamut(self, results, coverage, test_type):
|
||
"""绘制色域图 - 根据用户选择的参考标准动态计算覆盖率"""
|
||
|
||
self.gamut_ax_xy.clear()
|
||
self.gamut_ax_uv.clear()
|
||
|
||
# ==================== XY 图校准参数 ====================
|
||
XY_ORIGIN_X = 20.55
|
||
XY_ORIGIN_Y = 378.00
|
||
XY_PIXELS_PER_X = 510.6818
|
||
XY_PIXELS_PER_Y = 429.8844
|
||
|
||
# ==================== UV 图校准参数 ====================
|
||
UV_ORIGIN_U = 26.91
|
||
UV_ORIGIN_V = 377.16
|
||
UV_PIXELS_PER_U = 615.7260
|
||
UV_PIXELS_PER_V = 599.8432
|
||
|
||
# ========== ✅ 读取用户选择的参考标准 ==========
|
||
if test_type == "screen_module":
|
||
current_ref = self.screen_gamut_ref_var.get()
|
||
elif test_type == "sdr_movie":
|
||
current_ref = self.sdr_gamut_ref_var.get()
|
||
elif test_type == "hdr_movie":
|
||
current_ref = self.hdr_gamut_ref_var.get()
|
||
else:
|
||
current_ref = "DCI-P3"
|
||
|
||
# ========== ✅✅✅ 根据参考标准重新计算覆盖率(XY 空间)==========
|
||
xy_coverage = coverage # 默认使用传入的值
|
||
uv_coverage = 0.0
|
||
|
||
try:
|
||
# 提取前 3 个 RGB 点的 xy 坐标
|
||
if len(results) >= 3:
|
||
xy_points = [[result[0], result[1]] for result in results[:3]]
|
||
|
||
# 根据参考标准计算 XY 覆盖率
|
||
if current_ref == "BT.2020":
|
||
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_BT2020(
|
||
xy_points
|
||
)
|
||
elif current_ref == "BT.709":
|
||
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_BT709(
|
||
xy_points
|
||
)
|
||
elif current_ref == "DCI-P3":
|
||
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_DCIP3(
|
||
xy_points
|
||
)
|
||
elif current_ref == "BT.601":
|
||
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_BT601(
|
||
xy_points
|
||
)
|
||
else:
|
||
self.log_gui.log(f"⚠️ 未知参考标准 '{current_ref}',使用 DCI-P3")
|
||
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_DCIP3(
|
||
xy_points
|
||
)
|
||
current_ref = "DCI-P3"
|
||
|
||
self.log_gui.log(
|
||
f"✓ XY 空间覆盖率({current_ref}): {xy_coverage:.1f}%"
|
||
)
|
||
|
||
except Exception as e:
|
||
self.log_gui.log(f"⚠️ 重新计算 XY 覆盖率失败: {str(e)}")
|
||
xy_coverage = coverage # 回退到传入值
|
||
# =================================================
|
||
|
||
# ========== 左图:CIE 1931 xy ==========
|
||
try:
|
||
img_xy = mpimg.imread(get_resource_path("assets/cie.png"))
|
||
h_xy, w_xy = img_xy.shape[:2]
|
||
|
||
self.log_gui.log(f"加载 XY 色域图: {w_xy}x{h_xy}")
|
||
|
||
self.gamut_ax_xy.imshow(img_xy, extent=[0, w_xy, h_xy, 0], aspect="equal")
|
||
self.gamut_ax_xy.set_xlim(0, w_xy)
|
||
self.gamut_ax_xy.set_ylim(h_xy, 0)
|
||
self.gamut_ax_xy.axis("off")
|
||
self.gamut_ax_xy.set_clip_on(False)
|
||
|
||
def cie_xy_to_pixel(x, y):
|
||
"""CIE xy → 像素坐标"""
|
||
px = XY_ORIGIN_X + x * XY_PIXELS_PER_X
|
||
py = XY_ORIGIN_Y - y * XY_PIXELS_PER_Y
|
||
return px, py
|
||
|
||
if len(results) >= 3:
|
||
red_x, red_y = results[0][0], results[0][1]
|
||
green_x, green_y = results[1][0], results[1][1]
|
||
blue_x, blue_y = results[2][0], results[2][1]
|
||
|
||
self.log_gui.log(
|
||
f"测量色域: R({red_x:.4f},{red_y:.4f}) "
|
||
f"G({green_x:.4f},{green_y:.4f}) B({blue_x:.4f},{blue_y:.4f})"
|
||
)
|
||
|
||
# ========== 绘制测量三角形 ==========
|
||
points = [
|
||
cie_xy_to_pixel(red_x, red_y),
|
||
cie_xy_to_pixel(green_x, green_y),
|
||
cie_xy_to_pixel(blue_x, blue_y),
|
||
cie_xy_to_pixel(red_x, red_y),
|
||
]
|
||
|
||
xs = [p[0] for p in points]
|
||
ys = [p[1] for p in points]
|
||
|
||
self.gamut_ax_xy.plot(
|
||
xs,
|
||
ys,
|
||
color="red",
|
||
linewidth=2.5,
|
||
marker="o",
|
||
markersize=10,
|
||
markerfacecolor="red",
|
||
markeredgecolor="white",
|
||
markeredgewidth=2,
|
||
label="测量色域",
|
||
zorder=10,
|
||
)
|
||
|
||
# ========== 标注 RGB 点 ==========
|
||
labels = ["R", "G", "B"]
|
||
coords = [(red_x, red_y), (green_x, green_y), (blue_x, blue_y)]
|
||
|
||
for (x_cie, y_cie), label in zip(coords, labels):
|
||
px, py = cie_xy_to_pixel(x_cie, y_cie)
|
||
|
||
# 自适应偏移
|
||
if label == "R":
|
||
offset = (-60, -40) if x_cie > 0.6 else (0, -60)
|
||
elif label == "G":
|
||
offset = (0, -60)
|
||
else: # B
|
||
offset = (60, 40)
|
||
|
||
self.gamut_ax_xy.annotate(
|
||
f"{label}\n({x_cie:.3f},{y_cie:.3f})",
|
||
xy=(px, py),
|
||
xytext=offset,
|
||
textcoords="offset points",
|
||
fontsize=9,
|
||
color="white",
|
||
fontweight="bold",
|
||
bbox=dict(
|
||
boxstyle="round,pad=0.5",
|
||
facecolor="red",
|
||
alpha=0.9,
|
||
edgecolor="white",
|
||
linewidth=2,
|
||
),
|
||
arrowprops=dict(arrowstyle="->", color="red", lw=2),
|
||
zorder=11,
|
||
clip_on=False,
|
||
)
|
||
|
||
# ========== 绘制所有参考标准 ==========
|
||
# DCI-P3
|
||
dcip3 = [
|
||
(0.6800, 0.3200),
|
||
(0.2650, 0.6900),
|
||
(0.1500, 0.0600),
|
||
(0.6800, 0.3200),
|
||
]
|
||
dcip3_px = [cie_xy_to_pixel(x, y) for x, y in dcip3]
|
||
self.gamut_ax_xy.plot(
|
||
[p[0] for p in dcip3_px],
|
||
[p[1] for p in dcip3_px],
|
||
color="blue",
|
||
linewidth=1.5,
|
||
linestyle="--",
|
||
marker="s",
|
||
markersize=6,
|
||
alpha=0.7,
|
||
label="DCI-P3",
|
||
zorder=5,
|
||
)
|
||
|
||
# BT.2020
|
||
bt2020 = [
|
||
(0.7080, 0.2920),
|
||
(0.1700, 0.7970),
|
||
(0.1310, 0.0460),
|
||
(0.7080, 0.2920),
|
||
]
|
||
bt2020_px = [cie_xy_to_pixel(x, y) for x, y in bt2020]
|
||
self.gamut_ax_xy.plot(
|
||
[p[0] for p in bt2020_px],
|
||
[p[1] for p in bt2020_px],
|
||
color="green",
|
||
linewidth=1.5,
|
||
linestyle="-.",
|
||
marker="D",
|
||
markersize=5,
|
||
alpha=0.7,
|
||
label="BT.2020",
|
||
zorder=4,
|
||
)
|
||
|
||
# BT.709
|
||
bt709 = [
|
||
(0.6400, 0.3300),
|
||
(0.3000, 0.6000),
|
||
(0.1500, 0.0600),
|
||
(0.6400, 0.3300),
|
||
]
|
||
bt709_px = [cie_xy_to_pixel(x, y) for x, y in bt709]
|
||
self.gamut_ax_xy.plot(
|
||
[p[0] for p in bt709_px],
|
||
[p[1] for p in bt709_px],
|
||
color="gray",
|
||
linewidth=1.2,
|
||
linestyle=":",
|
||
marker="^",
|
||
markersize=5,
|
||
alpha=0.6,
|
||
label="BT.709",
|
||
zorder=3,
|
||
)
|
||
|
||
# BT.601(仅 SDR 测试)
|
||
if test_type == "sdr_movie":
|
||
bt601 = [
|
||
(0.6300, 0.3400),
|
||
(0.3100, 0.5950),
|
||
(0.1550, 0.0700),
|
||
(0.6300, 0.3400),
|
||
]
|
||
bt601_px = [cie_xy_to_pixel(x, y) for x, y in bt601]
|
||
self.gamut_ax_xy.plot(
|
||
[p[0] for p in bt601_px],
|
||
[p[1] for p in bt601_px],
|
||
color="purple",
|
||
linewidth=1.2,
|
||
linestyle="-",
|
||
marker="o",
|
||
markersize=5,
|
||
alpha=0.6,
|
||
label="BT.601",
|
||
zorder=3,
|
||
)
|
||
|
||
# ========== ✅ XY 覆盖率标注(使用重新计算的值)==========
|
||
self.gamut_ax_xy.text(
|
||
w_xy * 0.85,
|
||
h_xy * 0.92,
|
||
f"参考: {current_ref}\n覆盖率: {xy_coverage:.1f}%",
|
||
ha="right",
|
||
va="bottom",
|
||
fontsize=11,
|
||
fontweight="bold",
|
||
color="red",
|
||
bbox=dict(
|
||
boxstyle="round,pad=0.5",
|
||
facecolor="white",
|
||
alpha=0.95,
|
||
edgecolor="red",
|
||
linewidth=2,
|
||
),
|
||
zorder=12,
|
||
)
|
||
|
||
# 图例
|
||
self.gamut_ax_xy.legend(
|
||
loc="upper right",
|
||
fontsize=7,
|
||
framealpha=0.95,
|
||
edgecolor="black",
|
||
fancybox=True,
|
||
)
|
||
|
||
except Exception as e:
|
||
self.log_gui.log(f"XY 图绘制失败: {str(e)}")
|
||
import traceback
|
||
|
||
self.log_gui.log(traceback.format_exc())
|
||
|
||
# ========== 右图:CIE 1976 u'v' ==========
|
||
try:
|
||
img_uv = mpimg.imread(get_resource_path("assets/cie_uv.png"))
|
||
h_uv, w_uv = img_uv.shape[:2]
|
||
|
||
self.log_gui.log(f"加载 UV 色域图: {w_uv}x{h_uv}")
|
||
|
||
self.gamut_ax_uv.imshow(img_uv, extent=[0, w_uv, h_uv, 0], aspect="equal")
|
||
self.gamut_ax_uv.set_xlim(0, w_uv)
|
||
self.gamut_ax_uv.set_ylim(h_uv, 0)
|
||
self.gamut_ax_uv.axis("off")
|
||
self.gamut_ax_uv.set_clip_on(False)
|
||
|
||
def cie_uv_to_pixel(u, v):
|
||
"""CIE u'v' → 像素坐标"""
|
||
px = UV_ORIGIN_U + u * UV_PIXELS_PER_U
|
||
py = UV_ORIGIN_V - v * UV_PIXELS_PER_V
|
||
return px, py
|
||
|
||
if len(results) >= 3:
|
||
# 只取前 3 个 RGB 点
|
||
rgb_results = results[:3]
|
||
|
||
# 转换为 u'v' 坐标
|
||
def xy_to_uv(x, y):
|
||
"""xy → u'v' 转换"""
|
||
denom = -2 * x + 12 * y + 3
|
||
if abs(denom) < 1e-10:
|
||
return 0, 0
|
||
u = (4 * x) / denom
|
||
v = (9 * y) / denom
|
||
return u, v
|
||
|
||
uv_coords = [
|
||
[u, v] for u, v in [xy_to_uv(r[0], r[1]) for r in rgb_results]
|
||
]
|
||
|
||
self.log_gui.log(f"UV 坐标: {uv_coords}")
|
||
|
||
# ========== ✅✅✅ 计算 u'v' 覆盖率(使用参考标准)==========
|
||
try:
|
||
uv_coverage = pq_algorithm.calculate_uv_gamut_coverage(
|
||
uv_coords, reference=current_ref # ← 传入用户选择的参考标准
|
||
)
|
||
self.log_gui.log(
|
||
f"✓ UV 空间覆盖率({current_ref}): {uv_coverage:.1f}%"
|
||
)
|
||
except Exception as e:
|
||
self.log_gui.log(f"⚠️ 计算 UV 覆盖率失败: {str(e)}")
|
||
uv_coverage = 0.0
|
||
# =================================================
|
||
|
||
# ========== 绘制测量三角形 ==========
|
||
uv_coords_plot = uv_coords + [uv_coords[0]]
|
||
points_uv = [cie_uv_to_pixel(u, v) for u, v in uv_coords_plot]
|
||
xs_uv = [p[0] for p in points_uv]
|
||
ys_uv = [p[1] for p in points_uv]
|
||
|
||
self.gamut_ax_uv.plot(
|
||
xs_uv,
|
||
ys_uv,
|
||
color="red",
|
||
linewidth=2.5,
|
||
marker="o",
|
||
markersize=10,
|
||
markerfacecolor="red",
|
||
markeredgecolor="white",
|
||
markeredgewidth=2,
|
||
label="测量色域",
|
||
zorder=10,
|
||
)
|
||
|
||
# ========== 标注 RGB 点 ==========
|
||
labels = ["R", "G", "B"]
|
||
for (u, v), label in zip(uv_coords, labels):
|
||
px, py = cie_uv_to_pixel(u, v)
|
||
|
||
# 自适应偏移
|
||
if label == "R":
|
||
if u > 0.42 and v > 0.50:
|
||
offset = (-70, 20)
|
||
elif u > 0.45:
|
||
offset = (30, 50)
|
||
else:
|
||
offset = (50, 45)
|
||
elif label == "G":
|
||
offset = (0, -60)
|
||
else: # B
|
||
offset = (60, 40)
|
||
|
||
self.gamut_ax_uv.annotate(
|
||
f"{label}\n({u:.3f},{v:.3f})",
|
||
xy=(px, py),
|
||
xytext=offset,
|
||
textcoords="offset points",
|
||
fontsize=9,
|
||
color="white",
|
||
fontweight="bold",
|
||
bbox=dict(
|
||
boxstyle="round,pad=0.5",
|
||
facecolor="red",
|
||
alpha=0.9,
|
||
edgecolor="white",
|
||
linewidth=2,
|
||
),
|
||
arrowprops=dict(arrowstyle="->", color="red", lw=2),
|
||
zorder=11,
|
||
clip_on=False,
|
||
)
|
||
|
||
# ========== DCI-P3 参考(蓝色)==========
|
||
dcip3_uv = [
|
||
[0.4970, 0.5260], # Red
|
||
[0.0999, 0.5780], # Green
|
||
[0.1754, 0.1576], # Blue
|
||
[0.4970, 0.5260], # 闭合
|
||
]
|
||
dcip3_uv_px = [cie_uv_to_pixel(u, v) for u, v in dcip3_uv]
|
||
|
||
self.gamut_ax_uv.plot(
|
||
[p[0] for p in dcip3_uv_px],
|
||
[p[1] for p in dcip3_uv_px],
|
||
color="blue",
|
||
linewidth=1.5,
|
||
linestyle="--",
|
||
marker="s",
|
||
markersize=6,
|
||
alpha=0.7,
|
||
label="DCI-P3",
|
||
zorder=5,
|
||
)
|
||
|
||
# ========== BT.2020 参考(绿色)==========
|
||
bt2020_uv = [
|
||
[0.5566, 0.5165], # Red
|
||
[0.0556, 0.5868], # Green
|
||
[0.1593, 0.1258], # Blue
|
||
[0.5566, 0.5165], # 闭合
|
||
]
|
||
bt2020_uv_px = [cie_uv_to_pixel(u, v) for u, v in bt2020_uv]
|
||
|
||
self.gamut_ax_uv.plot(
|
||
[p[0] for p in bt2020_uv_px],
|
||
[p[1] for p in bt2020_uv_px],
|
||
color="green",
|
||
linewidth=1.5,
|
||
linestyle="-.",
|
||
marker="D",
|
||
markersize=5,
|
||
alpha=0.7,
|
||
label="BT.2020",
|
||
zorder=4,
|
||
)
|
||
|
||
# ========== BT.709 参考(灰色)==========
|
||
bt709_uv = [
|
||
[0.4507, 0.5229], # Red
|
||
[0.1250, 0.5625], # Green
|
||
[0.1754, 0.1576], # Blue
|
||
[0.4507, 0.5229], # 闭合
|
||
]
|
||
bt709_uv_px = [cie_uv_to_pixel(u, v) for u, v in bt709_uv]
|
||
|
||
self.gamut_ax_uv.plot(
|
||
[p[0] for p in bt709_uv_px],
|
||
[p[1] for p in bt709_uv_px],
|
||
color="gray",
|
||
linewidth=1.2,
|
||
linestyle=":",
|
||
marker="^",
|
||
markersize=5,
|
||
alpha=0.6,
|
||
label="BT.709",
|
||
zorder=3,
|
||
)
|
||
|
||
# ========== BT.601 参考(紫色)- 仅 SDR 测试显示 ==========
|
||
if test_type == "sdr_movie":
|
||
bt601_uv = [
|
||
[0.4510, 0.5236], # Red
|
||
[0.1291, 0.5606], # Green
|
||
[0.1787, 0.1610], # Blue
|
||
[0.4510, 0.5236], # 闭合
|
||
]
|
||
bt601_uv_px = [cie_uv_to_pixel(u, v) for u, v in bt601_uv]
|
||
|
||
self.gamut_ax_uv.plot(
|
||
[p[0] for p in bt601_uv_px],
|
||
[p[1] for p in bt601_uv_px],
|
||
color="purple",
|
||
linewidth=1.2,
|
||
linestyle="-",
|
||
marker="o",
|
||
markersize=5,
|
||
alpha=0.6,
|
||
label="BT.601",
|
||
zorder=3,
|
||
)
|
||
|
||
# ========== ✅ UV 覆盖率标注(使用动态计算的值)==========
|
||
self.gamut_ax_uv.text(
|
||
w_uv * 0.85,
|
||
h_uv * 0.92,
|
||
f"参考: {current_ref}\n覆盖率: {uv_coverage:.1f}%", # ← 使用动态计算的值
|
||
ha="right",
|
||
va="bottom",
|
||
fontsize=11,
|
||
fontweight="bold",
|
||
color="red",
|
||
bbox=dict(
|
||
boxstyle="round,pad=0.5",
|
||
facecolor="white",
|
||
alpha=0.95,
|
||
edgecolor="red",
|
||
linewidth=2,
|
||
),
|
||
zorder=12,
|
||
)
|
||
|
||
# 图例
|
||
self.gamut_ax_uv.legend(
|
||
loc="upper right",
|
||
fontsize=7,
|
||
framealpha=0.95,
|
||
edgecolor="black",
|
||
fancybox=True,
|
||
)
|
||
|
||
except Exception as e:
|
||
self.log_gui.log(f"UV 图绘制失败: {str(e)}")
|
||
import traceback
|
||
|
||
self.log_gui.log(traceback.format_exc())
|
||
|
||
# ========== 总标题 ==========
|
||
test_type_name = self.get_test_type_name(test_type)
|
||
self.gamut_fig.suptitle(
|
||
f"{test_type_name} - 色域测试", fontsize=12, y=0.98, fontweight="bold"
|
||
)
|
||
|
||
self.gamut_canvas.draw()
|
||
self.chart_notebook.select(0)
|
||
|
||
self.log_gui.log("色域图绘制完成")
|
||
|
||
def plot_gamma(self, L_bar, results_with_gamma_list, target_gamma, test_type):
|
||
"""绘制Gamma曲线 + 数据表格(包含实测亮度)"""
|
||
# ========== 1. 清空并重置左侧曲线 ==========
|
||
self.gamma_ax.clear()
|
||
self.gamma_ax.set_xlim(0, 105)
|
||
self.gamma_ax.set_ylim(0, 1.1)
|
||
self.gamma_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
||
self.gamma_ax.set_ylabel("L_bar", fontsize=10)
|
||
self.gamma_ax.grid(True, linestyle="--", alpha=0.3)
|
||
self.gamma_ax.tick_params(labelsize=9)
|
||
|
||
# 生成横坐标(灰阶百分比)
|
||
x_values = np.linspace(0, 100, len(L_bar))
|
||
|
||
# 反转 L_bar(确保从左到右是 0% → 100%)
|
||
if len(L_bar) > 1 and L_bar[0] > L_bar[-1]:
|
||
L_bar = L_bar[::-1]
|
||
results_with_gamma_list = results_with_gamma_list[::-1]
|
||
|
||
# 计算平均Gamma
|
||
gamma_values = []
|
||
for item in results_with_gamma_list:
|
||
if isinstance(item, (list, tuple)) and len(item) >= 4:
|
||
gamma = item[3]
|
||
if 0.5 < gamma < 5.0:
|
||
gamma_values.append(gamma)
|
||
|
||
avg_gamma = np.mean(gamma_values) if gamma_values else target_gamma
|
||
|
||
# 绘制实测曲线
|
||
self.gamma_ax.plot(
|
||
x_values,
|
||
L_bar,
|
||
"b-o",
|
||
label=f"实测 (平均γ={avg_gamma:.2f})",
|
||
linewidth=2,
|
||
markersize=4,
|
||
zorder=5,
|
||
)
|
||
|
||
# 绘制理想曲线(使用 target_gamma)
|
||
ideal_L_bar = [(x / 100) ** target_gamma for x in x_values]
|
||
self.gamma_ax.plot(
|
||
x_values,
|
||
ideal_L_bar,
|
||
"r--",
|
||
label=f"理想 (γ={target_gamma})", # ← 显示实际的 target_gamma
|
||
linewidth=2,
|
||
alpha=0.7,
|
||
zorder=3,
|
||
)
|
||
|
||
# 图例
|
||
self.gamma_ax.legend(fontsize=9, loc="upper left", framealpha=0.95)
|
||
|
||
# ========== 2. 清空并绘制右侧表格 ==========
|
||
self.gamma_table_ax.clear()
|
||
self.gamma_table_ax.axis("off")
|
||
|
||
# 构建表格数据(4列)
|
||
table_data = [["灰阶", "实测亮度\n(cd/m²)", "L_bar", "Gamma"]]
|
||
|
||
for i, (x_val, L_val, result) in enumerate(
|
||
zip(x_values, L_bar, results_with_gamma_list)
|
||
):
|
||
# 提取实测亮度
|
||
if isinstance(result, (list, tuple)) and len(result) >= 3:
|
||
measured_lv = result[2]
|
||
measured_lv_str = f"{measured_lv:.2f}"
|
||
else:
|
||
measured_lv_str = "--"
|
||
|
||
# 提取 Gamma
|
||
if isinstance(result, (list, tuple)) and len(result) >= 4:
|
||
gamma = result[3]
|
||
if gamma < 0.5 or gamma > 5.0:
|
||
gamma_str = "--"
|
||
else:
|
||
gamma_str = f"{gamma:.2f}"
|
||
else:
|
||
gamma_str = "--"
|
||
|
||
table_data.append(
|
||
[
|
||
f"{x_val:.0f}%",
|
||
measured_lv_str, # ← 实测亮度
|
||
f"{L_val:.3f}",
|
||
gamma_str,
|
||
]
|
||
)
|
||
|
||
# 绘制表格(4列)
|
||
table = self.gamma_table_ax.table(
|
||
cellText=table_data,
|
||
cellLoc="center",
|
||
loc="center",
|
||
colWidths=[0.18, 0.28, 0.27, 0.27],
|
||
)
|
||
|
||
# 美化表格
|
||
table.auto_set_font_size(False)
|
||
table.set_fontsize(7.5)
|
||
table.scale(1, 1.5)
|
||
|
||
# 表头样式
|
||
for i in range(4): # ← 4列
|
||
cell = table[(0, i)]
|
||
cell.set_facecolor("#4472C4")
|
||
cell.set_text_props(weight="bold", color="white")
|
||
|
||
# 数据行交替颜色
|
||
for i in range(1, len(table_data)):
|
||
for j in range(4): # ← 4列
|
||
cell = table[(i, j)]
|
||
if i % 2 == 0:
|
||
cell.set_facecolor("#E7E6E6")
|
||
else:
|
||
cell.set_facecolor("#FFFFFF")
|
||
|
||
# ========== 3. 总标题 ==========
|
||
test_type_name = self.get_test_type_name(test_type)
|
||
self.gamma_fig.suptitle(
|
||
f"{test_type_name} - Gamma曲线",
|
||
fontsize=12,
|
||
y=0.98,
|
||
fontweight="bold",
|
||
)
|
||
|
||
# ========== 4. 绘制到画布 ==========
|
||
self.gamma_canvas.draw()
|
||
self.chart_notebook.select(1)
|
||
|
||
self.log_gui.log("Gamma曲线 + 数据表格绘制完成")
|
||
|
||
def plot_eotf(self, L_bar, results_with_eotf_list, test_type):
|
||
"""绘制 EOTF 曲线 + 数据表格(HDR 专用,包含实测亮度)"""
|
||
# ========== 1. 清空并重置左侧曲线 ==========
|
||
self.eotf_ax.clear()
|
||
self.eotf_ax.set_xlim(0, 105)
|
||
self.eotf_ax.set_ylim(0, 1.1)
|
||
self.eotf_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
||
self.eotf_ax.set_ylabel("L_bar", fontsize=10)
|
||
self.eotf_ax.grid(True, linestyle="--", alpha=0.3)
|
||
self.eotf_ax.tick_params(labelsize=9)
|
||
|
||
# 生成横坐标(灰阶百分比)
|
||
x_values = np.linspace(0, 100, len(L_bar))
|
||
|
||
# 反转 L_bar
|
||
if len(L_bar) > 1 and L_bar[0] > L_bar[-1]:
|
||
L_bar = L_bar[::-1]
|
||
results_with_eotf_list = results_with_eotf_list[::-1]
|
||
|
||
# 计算平均 EOTF Gamma
|
||
eotf_values = []
|
||
for item in results_with_eotf_list:
|
||
if isinstance(item, (list, tuple)) and len(item) >= 4:
|
||
eotf = item[3]
|
||
if 0.5 < eotf < 5.0:
|
||
eotf_values.append(eotf)
|
||
|
||
avg_eotf = np.mean(eotf_values) if eotf_values else 2.2
|
||
|
||
# 绘制实测曲线
|
||
self.eotf_ax.plot(
|
||
x_values,
|
||
L_bar,
|
||
"b-o",
|
||
label=f"实测 (平均γ={avg_eotf:.2f})",
|
||
linewidth=2,
|
||
markersize=4,
|
||
zorder=5,
|
||
)
|
||
|
||
# 绘制 PQ (ST.2084) 理想曲线
|
||
pq_L_bar = self.calculate_pq_curve(x_values)
|
||
self.eotf_ax.plot(
|
||
x_values,
|
||
pq_L_bar,
|
||
"r--",
|
||
label="理想 PQ (ST.2084)",
|
||
linewidth=2,
|
||
alpha=0.7,
|
||
zorder=3,
|
||
)
|
||
|
||
# 图例
|
||
self.eotf_ax.legend(fontsize=9, loc="upper left", framealpha=0.95)
|
||
|
||
# ========== 2. 清空并绘制右侧表格 ==========
|
||
self.eotf_table_ax.clear()
|
||
self.eotf_table_ax.axis("off")
|
||
|
||
# 构建表格数据(4列)
|
||
table_data = [["灰阶", "实测亮度\n(cd/m²)", "L_bar", "EOTF γ"]]
|
||
|
||
for i, (x_val, L_val, result) in enumerate(
|
||
zip(x_values, L_bar, results_with_eotf_list)
|
||
):
|
||
# 提取实测亮度
|
||
if isinstance(result, (list, tuple)) and len(result) >= 3:
|
||
measured_lv = result[2]
|
||
measured_lv_str = f"{measured_lv:.2f}"
|
||
else:
|
||
measured_lv_str = "--"
|
||
|
||
# 提取 EOTF
|
||
if isinstance(result, (list, tuple)) and len(result) >= 4:
|
||
eotf = result[3]
|
||
if eotf < 0.5 or eotf > 5.0:
|
||
eotf_str = "--"
|
||
else:
|
||
eotf_str = f"{eotf:.2f}"
|
||
else:
|
||
eotf_str = "--"
|
||
|
||
table_data.append(
|
||
[
|
||
f"{x_val:.0f}%",
|
||
measured_lv_str, # ← 实测亮度
|
||
f"{L_val:.3f}",
|
||
eotf_str,
|
||
]
|
||
)
|
||
|
||
# 绘制表格(4列)
|
||
table = self.eotf_table_ax.table(
|
||
cellText=table_data,
|
||
cellLoc="center",
|
||
loc="center",
|
||
colWidths=[0.18, 0.28, 0.27, 0.27],
|
||
)
|
||
|
||
# 美化表格
|
||
table.auto_set_font_size(False)
|
||
table.set_fontsize(7.5)
|
||
table.scale(1, 1.5)
|
||
|
||
# 表头样式
|
||
for i in range(4): # ← 4列
|
||
cell = table[(0, i)]
|
||
cell.set_facecolor("#4472C4")
|
||
cell.set_text_props(weight="bold", color="white")
|
||
|
||
# 数据行交替颜色
|
||
for i in range(1, len(table_data)):
|
||
for j in range(4): # ← 4列
|
||
cell = table[(i, j)]
|
||
if i % 2 == 0:
|
||
cell.set_facecolor("#E7E6E6")
|
||
else:
|
||
cell.set_facecolor("#FFFFFF")
|
||
|
||
# ========== 3. 总标题 ==========
|
||
test_type_name = self.get_test_type_name(test_type)
|
||
self.eotf_fig.suptitle(
|
||
f"{test_type_name} - EOTF 曲线(PQ ST.2084)",
|
||
fontsize=12,
|
||
y=0.98,
|
||
fontweight="bold",
|
||
)
|
||
|
||
# ========== 4. 绘制到画布 ==========
|
||
self.eotf_canvas.draw()
|
||
|
||
# 选中 EOTF Tab
|
||
try:
|
||
eotf_tab_id = str(self.eotf_chart_frame)
|
||
current_tabs = list(self.chart_notebook.tabs())
|
||
if eotf_tab_id in current_tabs:
|
||
eotf_index = current_tabs.index(eotf_tab_id)
|
||
self.chart_notebook.select(eotf_index)
|
||
except:
|
||
pass
|
||
|
||
self.log_gui.log("EOTF 曲线 + 数据表格绘制完成")
|
||
|
||
def calculate_pq_curve(self, gray_levels):
|
||
"""计算 PQ (ST.2084) EOTF 理想曲线
|
||
|
||
Args:
|
||
gray_levels: 灰阶百分比数组 (0-100)
|
||
|
||
Returns:
|
||
L_bar: 归一化亮度数组 (0-1)
|
||
"""
|
||
# PQ 曲线参数(ITU-R BT.2100 标准)
|
||
m1 = 0.1593017578125 # = 2610 / 16384
|
||
m2 = 78.84375 # = 78.84375
|
||
c1 = 0.8359375 # = 3424 / 4096
|
||
c2 = 18.8515625 # = 2413 / 128
|
||
c3 = 18.6875 # = 2392 / 128
|
||
|
||
L_bar = []
|
||
for gray in gray_levels:
|
||
# 归一化灰阶(0-1)
|
||
V = gray / 100.0
|
||
|
||
if V <= 0:
|
||
L_bar.append(0)
|
||
else:
|
||
# PQ 反向 EOTF 计算
|
||
V_pow = np.power(V, 1 / m2)
|
||
numerator = max(V_pow - c1, 0)
|
||
denominator = c2 - c3 * V_pow
|
||
|
||
if denominator > 0:
|
||
L = np.power(numerator / denominator, 1 / m1)
|
||
else:
|
||
L = 0
|
||
|
||
L_bar.append(L)
|
||
|
||
return np.array(L_bar)
|
||
|
||
def plot_cct(self, test_type):
|
||
"""绘制 x 和 y 坐标分离图 - 每个点标注纵坐标值"""
|
||
self.cct_fig.clear()
|
||
|
||
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("⚠️ 无 xy 数据可用")
|
||
ax = self.cct_fig.add_subplot(111)
|
||
ax.text(
|
||
0.5,
|
||
0.5,
|
||
"无可用数据",
|
||
ha="center",
|
||
va="center",
|
||
fontsize=14,
|
||
color="red",
|
||
)
|
||
ax.axis("off")
|
||
self.cct_canvas.draw()
|
||
return
|
||
|
||
x_measured = [data[0] for data in gray_data]
|
||
y_measured = [data[1] for data in gray_data]
|
||
|
||
# 反转数据顺序(从暗到亮)
|
||
x_measured = x_measured[::-1]
|
||
y_measured = y_measured[::-1]
|
||
|
||
# 去掉第一个点
|
||
x_measured = x_measured[1:]
|
||
y_measured = y_measured[1:]
|
||
|
||
# 重新生成灰阶坐标
|
||
total_points = len(gray_data)
|
||
grayscale = np.linspace(100 / total_points, 100, len(x_measured))
|
||
|
||
self.log_gui.log(f"✓ 已移除第一个数据点,当前数据点数: {len(x_measured)}")
|
||
self.log_gui.log(f" x范围: {min(x_measured):.6f} - {max(x_measured):.6f}")
|
||
self.log_gui.log(f" y范围: {min(y_measured):.6f} - {max(y_measured):.6f}")
|
||
|
||
# ========== 根据测试类型读取对应参数 ==========
|
||
if test_type == "sdr_movie":
|
||
try:
|
||
x_ideal = float(self.sdr_cct_x_ideal_var.get())
|
||
x_tolerance = float(self.sdr_cct_x_tolerance_var.get())
|
||
y_ideal = float(self.sdr_cct_y_ideal_var.get())
|
||
y_tolerance = float(self.sdr_cct_y_tolerance_var.get())
|
||
self.log_gui.log("✓ 使用 SDR 色度参数")
|
||
except:
|
||
x_ideal = 0.3127
|
||
x_tolerance = 0.003
|
||
y_ideal = 0.3290
|
||
y_tolerance = 0.003
|
||
self.log_gui.log("⚠️ SDR 参数读取失败,使用默认值")
|
||
elif test_type == "hdr_movie":
|
||
try:
|
||
x_ideal = float(self.hdr_cct_x_ideal_var.get())
|
||
x_tolerance = float(self.hdr_cct_x_tolerance_var.get())
|
||
y_ideal = float(self.hdr_cct_y_ideal_var.get())
|
||
y_tolerance = float(self.hdr_cct_y_tolerance_var.get())
|
||
self.log_gui.log("✓ 使用 HDR 色度参数")
|
||
except:
|
||
x_ideal = 0.3127
|
||
x_tolerance = 0.003
|
||
y_ideal = 0.3290
|
||
y_tolerance = 0.003
|
||
self.log_gui.log("⚠️ HDR 参数读取失败,使用默认值")
|
||
else: # screen_module
|
||
try:
|
||
x_ideal = float(self.cct_x_ideal_var.get())
|
||
x_tolerance = float(self.cct_x_tolerance_var.get())
|
||
y_ideal = float(self.cct_y_ideal_var.get())
|
||
y_tolerance = float(self.cct_y_tolerance_var.get())
|
||
self.log_gui.log("✓ 使用屏模组色度参数")
|
||
except:
|
||
x_ideal = 0.306
|
||
x_tolerance = 0.003
|
||
y_ideal = 0.318
|
||
y_tolerance = 0.003
|
||
self.log_gui.log("⚠️ 屏模组参数读取失败,使用默认值")
|
||
|
||
x_low = x_ideal - x_tolerance
|
||
x_high = x_ideal + x_tolerance
|
||
y_low = y_ideal - y_tolerance
|
||
y_high = y_ideal + y_tolerance
|
||
|
||
self.log_gui.log(f"✓ 用户设置参数:")
|
||
self.log_gui.log(f" x-ideal={x_ideal:.4f}, tolerance={x_tolerance:.4f}")
|
||
self.log_gui.log(f" x范围: [{x_low:.4f}, {x_high:.4f}]")
|
||
self.log_gui.log(f" y-ideal={y_ideal:.4f}, tolerance={y_tolerance:.4f}")
|
||
self.log_gui.log(f" y范围: [{y_low:.4f}, {y_high:.4f}]")
|
||
|
||
# 为所有测试类型创建子图
|
||
ax1 = self.cct_fig.add_subplot(211)
|
||
ax2 = self.cct_fig.add_subplot(212)
|
||
|
||
# ========== 上图:x coordinates ==========
|
||
ax1.plot(
|
||
grayscale,
|
||
x_measured,
|
||
"b-o",
|
||
label="屏本体",
|
||
linewidth=2,
|
||
markersize=4,
|
||
zorder=5,
|
||
)
|
||
|
||
# 为每个点添加数值标注(x 坐标)
|
||
for i, (gs, x_val) in enumerate(zip(grayscale, x_measured)):
|
||
ax1.annotate(
|
||
f"{x_val:.5f}", # 显示 5 位小数
|
||
xy=(gs, x_val),
|
||
xytext=(0, 8), # 向上偏移 8 点
|
||
textcoords="offset points",
|
||
ha="center",
|
||
va="bottom",
|
||
fontsize=7,
|
||
color="blue",
|
||
bbox=dict(
|
||
boxstyle="round,pad=0.2",
|
||
facecolor="white",
|
||
edgecolor="blue",
|
||
alpha=0.8,
|
||
linewidth=0.5,
|
||
),
|
||
)
|
||
|
||
# 绘制完整的参考线
|
||
full_grayscale = np.linspace(0, 100, 100)
|
||
ax1.axhline(
|
||
y=x_ideal,
|
||
color="green",
|
||
linestyle="--",
|
||
linewidth=1.5,
|
||
label=f"x-ideal ({x_ideal:.4f})",
|
||
zorder=3,
|
||
)
|
||
ax1.axhline(
|
||
y=x_low,
|
||
color="red",
|
||
linestyle=":",
|
||
linewidth=1,
|
||
alpha=0.7,
|
||
label=f"x-low ({x_low:.4f})",
|
||
zorder=2,
|
||
)
|
||
ax1.axhline(
|
||
y=x_high,
|
||
color="red",
|
||
linestyle=":",
|
||
linewidth=1,
|
||
alpha=0.7,
|
||
label=f"x-high ({x_high:.4f})",
|
||
zorder=2,
|
||
)
|
||
ax1.fill_between(
|
||
full_grayscale, x_low, x_high, alpha=0.15, color="blue", zorder=1
|
||
)
|
||
|
||
ax1.set_xlabel("灰阶 (%)", fontsize=9)
|
||
ax1.set_ylabel("CIE x", fontsize=9)
|
||
ax1.grid(True, linestyle="--", alpha=0.3)
|
||
ax1.tick_params(labelsize=8)
|
||
ax1.set_xlim(0, 105)
|
||
|
||
# 纵坐标范围由用户参数控制
|
||
x_min_data = min(x_measured)
|
||
x_max_data = max(x_measured)
|
||
data_range_x = x_max_data - x_min_data
|
||
|
||
self.log_gui.log(f" x数据波动: {data_range_x:.6f}")
|
||
|
||
range_span = x_tolerance * 2
|
||
margin_ratio = 0.20 # ← 增大边距以容纳标注
|
||
extra_margin = range_span * margin_ratio
|
||
|
||
final_y_min = min(x_min_data, x_low) - extra_margin
|
||
final_y_max = max(x_max_data, x_high) + extra_margin
|
||
|
||
if x_min_data >= x_low and x_max_data <= x_high:
|
||
self.log_gui.log(f" x数据在tolerance范围内,使用tolerance范围显示")
|
||
final_y_min = x_low - extra_margin
|
||
final_y_max = x_high + extra_margin
|
||
else:
|
||
self.log_gui.log(f" x数据超出tolerance范围,扩展显示范围")
|
||
|
||
ax1.set_ylim(final_y_min, final_y_max)
|
||
self.log_gui.log(
|
||
f" x轴显示范围: {final_y_min:.6f} - {final_y_max:.6f} (跨度: {final_y_max - final_y_min:.6f})"
|
||
)
|
||
|
||
# ========== 下图:y coordinates ==========
|
||
ax2.plot(
|
||
grayscale,
|
||
y_measured,
|
||
"r-o",
|
||
label="屏本体",
|
||
linewidth=2,
|
||
markersize=4,
|
||
zorder=5,
|
||
)
|
||
|
||
# 为每个点添加数值标注(y 坐标)
|
||
for i, (gs, y_val) in enumerate(zip(grayscale, y_measured)):
|
||
ax2.annotate(
|
||
f"{y_val:.5f}", # 显示 5 位小数
|
||
xy=(gs, y_val),
|
||
xytext=(0, 8), # 向上偏移 8 点
|
||
textcoords="offset points",
|
||
ha="center",
|
||
va="bottom",
|
||
fontsize=7,
|
||
color="red",
|
||
bbox=dict(
|
||
boxstyle="round,pad=0.2",
|
||
facecolor="white",
|
||
edgecolor="red",
|
||
alpha=0.8,
|
||
linewidth=0.5,
|
||
),
|
||
)
|
||
|
||
ax2.axhline(
|
||
y=y_ideal,
|
||
color="green",
|
||
linestyle="--",
|
||
linewidth=1.5,
|
||
label=f"y-ideal ({y_ideal:.4f})",
|
||
zorder=3,
|
||
)
|
||
ax2.axhline(
|
||
y=y_low,
|
||
color="orange",
|
||
linestyle=":",
|
||
linewidth=1,
|
||
alpha=0.7,
|
||
label=f"y-low ({y_low:.4f})",
|
||
zorder=2,
|
||
)
|
||
ax2.axhline(
|
||
y=y_high,
|
||
color="orange",
|
||
linestyle=":",
|
||
linewidth=1,
|
||
alpha=0.7,
|
||
label=f"y-high ({y_high:.4f})",
|
||
zorder=2,
|
||
)
|
||
ax2.fill_between(
|
||
full_grayscale, y_low, y_high, alpha=0.15, color="orange", zorder=1
|
||
)
|
||
|
||
ax2.set_xlabel("灰阶 (%)", fontsize=9)
|
||
ax2.set_ylabel("CIE y", fontsize=9)
|
||
ax2.grid(True, linestyle="--", alpha=0.3)
|
||
ax2.tick_params(labelsize=8)
|
||
ax2.set_xlim(0, 105)
|
||
|
||
# 纵坐标范围由用户参数控制
|
||
y_min_data = min(y_measured)
|
||
y_max_data = max(y_measured)
|
||
data_range_y = y_max_data - y_min_data
|
||
|
||
self.log_gui.log(f" y数据波动: {data_range_y:.6f}")
|
||
|
||
range_span = y_tolerance * 2
|
||
extra_margin = range_span * margin_ratio
|
||
|
||
final_y_min = min(y_min_data, y_low) - extra_margin
|
||
final_y_max = max(y_max_data, y_high) + extra_margin
|
||
|
||
if y_min_data >= y_low and y_max_data <= y_high:
|
||
self.log_gui.log(f" y数据在tolerance范围内,使用tolerance范围显示")
|
||
final_y_min = y_low - extra_margin
|
||
final_y_max = y_high + extra_margin
|
||
else:
|
||
self.log_gui.log(f" y数据超出tolerance范围,扩展显示范围")
|
||
|
||
ax2.set_ylim(final_y_min, final_y_max)
|
||
self.log_gui.log(
|
||
f" y轴显示范围: {final_y_min:.6f} - {final_y_max:.6f} (跨度: {final_y_max - final_y_min:.6f})"
|
||
)
|
||
|
||
# ========== 总标题 - 统一格式(去掉统计信息)==========
|
||
test_type_name = self.get_test_type_name(test_type)
|
||
|
||
self.cct_fig.suptitle(
|
||
f"{test_type_name} - 色度一致性测试",
|
||
fontsize=12,
|
||
y=0.98,
|
||
fontweight="bold",
|
||
)
|
||
|
||
self.cct_fig.subplots_adjust(
|
||
left=0.12,
|
||
right=0.82,
|
||
top=0.92,
|
||
bottom=0.08,
|
||
hspace=0.30,
|
||
)
|
||
|
||
ax1.legend(
|
||
fontsize=7, loc="center left", bbox_to_anchor=(1.05, 0.5), framealpha=1.0
|
||
)
|
||
ax2.legend(
|
||
fontsize=7, loc="center left", bbox_to_anchor=(1.05, 0.5), framealpha=1.0
|
||
)
|
||
|
||
self.cct_canvas.draw()
|
||
self.chart_notebook.select(2)
|
||
|
||
self.log_gui.log("✓ xy 色度坐标图绘制完成")
|
||
|
||
def plot_contrast(self, contrast_data, test_type):
|
||
"""绘制对比度测试结果 - 固定布局版本"""
|
||
# 清空并重置
|
||
self.contrast_ax.clear()
|
||
self.contrast_ax.set_xlim(0, 1)
|
||
self.contrast_ax.set_ylim(0, 1)
|
||
self.contrast_ax.axis("off")
|
||
|
||
# 强制重置布局
|
||
self.contrast_fig.subplots_adjust(
|
||
left=0.02,
|
||
right=0.98,
|
||
top=0.90,
|
||
bottom=0.02,
|
||
)
|
||
|
||
max_lum = contrast_data["max_luminance"]
|
||
min_lum = contrast_data["min_luminance"]
|
||
contrast = contrast_data["contrast_ratio"]
|
||
|
||
# 确定等级和颜色
|
||
if contrast >= 5000:
|
||
grade, grade_color = "优秀", "#4CAF50"
|
||
elif contrast >= 3000:
|
||
grade, grade_color = "良好", "#8BC34A"
|
||
elif contrast >= 1000:
|
||
grade, grade_color = "合格", "#FFC107"
|
||
else:
|
||
grade, grade_color = "不合格", "#F44336"
|
||
|
||
test_type_name = self.get_test_type_name(test_type)
|
||
|
||
# ========== 顶部标题 - 统一格式 ==========
|
||
self.contrast_fig.suptitle(
|
||
f"{test_type_name} - 对比度测试",
|
||
fontsize=12,
|
||
y=0.98,
|
||
fontweight="bold",
|
||
)
|
||
|
||
# ========== 中央大对比度卡片 ==========
|
||
from matplotlib.patches import Rectangle
|
||
|
||
center_card = Rectangle(
|
||
(0.15, 0.48),
|
||
0.70,
|
||
0.32,
|
||
transform=self.contrast_ax.transAxes,
|
||
facecolor=grade_color,
|
||
edgecolor="black",
|
||
linewidth=2.5,
|
||
alpha=0.15,
|
||
)
|
||
self.contrast_ax.add_patch(center_card)
|
||
|
||
# 对比度数值
|
||
self.contrast_ax.text(
|
||
0.5,
|
||
0.65,
|
||
f"{contrast:.0f} : 1",
|
||
ha="center",
|
||
va="center",
|
||
fontsize=36,
|
||
fontweight="bold",
|
||
color=grade_color,
|
||
transform=self.contrast_ax.transAxes,
|
||
)
|
||
|
||
# 等级标签
|
||
self.contrast_ax.text(
|
||
0.5,
|
||
0.51,
|
||
f"等级: {grade}",
|
||
ha="center",
|
||
va="center",
|
||
fontsize=12,
|
||
fontweight="bold",
|
||
color=grade_color,
|
||
transform=self.contrast_ax.transAxes,
|
||
)
|
||
|
||
# ========== 两个信息卡片(缩小)==========
|
||
card_width = 0.32
|
||
card_height = 0.22
|
||
card_y = 0.12
|
||
|
||
gap = 0.05
|
||
total_width = card_width * 2 + gap
|
||
start_x = (1 - total_width) / 2
|
||
|
||
cards_data = [
|
||
{
|
||
"x": start_x,
|
||
"title": "白场亮度",
|
||
"value": f"{max_lum:.2f}",
|
||
"unit": "cd/m²",
|
||
"color": "#E3F2FD",
|
||
"edge_color": "#2196F3",
|
||
},
|
||
{
|
||
"x": start_x + card_width + gap,
|
||
"title": "黑场亮度",
|
||
"value": f"{min_lum:.4f}",
|
||
"unit": "cd/m²",
|
||
"color": "#F3E5F5",
|
||
"edge_color": "#9C27B0",
|
||
},
|
||
]
|
||
|
||
for card in cards_data:
|
||
# 绘制卡片背景
|
||
rect = Rectangle(
|
||
(card["x"], card_y),
|
||
card_width,
|
||
card_height,
|
||
transform=self.contrast_ax.transAxes,
|
||
facecolor=card["color"],
|
||
edgecolor=card["edge_color"],
|
||
linewidth=2,
|
||
)
|
||
self.contrast_ax.add_patch(rect)
|
||
|
||
# 标题
|
||
self.contrast_ax.text(
|
||
card["x"] + card_width / 2,
|
||
card_y + card_height - 0.03,
|
||
card["title"],
|
||
ha="center",
|
||
va="top",
|
||
fontsize=10,
|
||
fontweight="bold",
|
||
transform=self.contrast_ax.transAxes,
|
||
)
|
||
|
||
# 数值
|
||
self.contrast_ax.text(
|
||
card["x"] + card_width / 2,
|
||
card_y + card_height / 2,
|
||
card["value"],
|
||
ha="center",
|
||
va="center",
|
||
fontsize=16,
|
||
fontweight="bold",
|
||
transform=self.contrast_ax.transAxes,
|
||
)
|
||
|
||
# 单位
|
||
self.contrast_ax.text(
|
||
card["x"] + card_width / 2,
|
||
card_y + 0.03,
|
||
card["unit"],
|
||
ha="center",
|
||
va="bottom",
|
||
fontsize=9,
|
||
color="gray",
|
||
transform=self.contrast_ax.transAxes,
|
||
)
|
||
|
||
self.contrast_canvas.draw()
|
||
self.chart_notebook.select(3)
|
||
|
||
def plot_accuracy(self, accuracy_data, test_type):
|
||
"""绘制色准测试结果 - 29色显示 - 简洁版布局(显示 Gamma)"""
|
||
self.accuracy_ax.clear()
|
||
self.accuracy_ax.set_xlim(0, 1)
|
||
self.accuracy_ax.set_ylim(0, 1)
|
||
self.accuracy_ax.axis("off")
|
||
|
||
self.accuracy_fig.subplots_adjust(
|
||
left=0.05,
|
||
right=0.95,
|
||
top=0.95,
|
||
bottom=0.02,
|
||
)
|
||
|
||
# 获取色准数据
|
||
color_patches = accuracy_data.get("color_patches", [])
|
||
delta_e_values = accuracy_data.get("delta_e_values", [])
|
||
avg_delta_e = accuracy_data.get("avg_delta_e", 0)
|
||
max_delta_e = accuracy_data.get("max_delta_e", 0)
|
||
min_delta_e = accuracy_data.get("min_delta_e", 0)
|
||
excellent_count = accuracy_data.get("excellent_count", 0)
|
||
good_count = accuracy_data.get("good_count", 0)
|
||
poor_count = accuracy_data.get("poor_count", 0)
|
||
|
||
# 获取 Gamma 值
|
||
target_gamma = accuracy_data.get("target_gamma", 2.2)
|
||
|
||
test_type_name = self.get_test_type_name(test_type)
|
||
|
||
# ========== 标题(动态显示 Gamma)==========
|
||
if test_type == "sdr_movie":
|
||
title = f"{test_type_name} - 色准测试(全 29色 | Gamma {target_gamma})"
|
||
elif test_type == "hdr_movie":
|
||
title = f"{test_type_name} - 色准测试(全 29色 | PQ EOTF)"
|
||
else: # screen_module
|
||
title = f"{test_type_name} - 色准测试(全 29色 | Gamma {target_gamma})"
|
||
|
||
self.accuracy_fig.suptitle(
|
||
title,
|
||
fontsize=11,
|
||
y=0.98,
|
||
fontweight="bold",
|
||
)
|
||
|
||
# ========== 29色:6行5列布局 ==========
|
||
cols = 5
|
||
rows = 6
|
||
|
||
patch_width = 0.135
|
||
patch_height = 0.085
|
||
x_start = 0.08
|
||
y_start = 0.90
|
||
x_gap = 0.035
|
||
y_gap = 0.050
|
||
|
||
from matplotlib.patches import Rectangle
|
||
|
||
# ========== 绘制色块 ==========
|
||
for i, (color_name, delta_e) in enumerate(zip(color_patches, delta_e_values)):
|
||
row = i // cols
|
||
col = i % cols
|
||
|
||
x = x_start + col * (patch_width + x_gap)
|
||
y = y_start - row * (patch_height + y_gap)
|
||
|
||
# 颜色映射
|
||
color_map = {
|
||
# 灰阶
|
||
"White": "#FFFFFF",
|
||
"Gray 80": "#E6E6E6",
|
||
"Gray 65": "#D1D1D1",
|
||
"Gray 50": "#BABABA",
|
||
"Gray 35": "#9E9E9E",
|
||
# 饱和色
|
||
"100% Red": "#FF0000",
|
||
"100% Green": "#00FF00",
|
||
"100% Blue": "#0000FF",
|
||
"100% Cyan": "#00FFFF",
|
||
"100% Magenta": "#FF00FF",
|
||
"100% Yellow": "#FFFF00",
|
||
# ColorChecker 颜色
|
||
"Dark Skin": "#735242",
|
||
"Light Skin": "#C29682",
|
||
"Blue Sky": "#5E7A9C",
|
||
"Foliage": "#596B42",
|
||
"Blue Flower": "#8280B0",
|
||
"Bluish Green": "#63BDA8",
|
||
"Orange": "#D97829",
|
||
"Purplish Blue": "#4A5CA3",
|
||
"Moderate Red": "#C25461",
|
||
"Purple": "#5C3D6B",
|
||
"Yellow Green": "#9EBA40",
|
||
"Orange Yellow": "#E6A12E",
|
||
"Blue (Legacy)": "#333D96",
|
||
"Green (Legacy)": "#479447",
|
||
"Red (Legacy)": "#B0303B",
|
||
"Yellow (Legacy)": "#EDC721",
|
||
"Magenta (Legacy)": "#BA5491",
|
||
"Cyan (Legacy)": "#0085A3",
|
||
}
|
||
|
||
patch_color = color_map.get(color_name, "#808080")
|
||
|
||
# ΔE 等级颜色
|
||
if delta_e < 3:
|
||
edge_color = "green"
|
||
elif delta_e < 5:
|
||
edge_color = "orange"
|
||
else:
|
||
edge_color = "red"
|
||
|
||
# 绘制色块
|
||
rect = Rectangle(
|
||
(x, y),
|
||
patch_width,
|
||
patch_height,
|
||
transform=self.accuracy_ax.transAxes,
|
||
facecolor=patch_color,
|
||
edgecolor=edge_color,
|
||
linewidth=1.8,
|
||
)
|
||
self.accuracy_ax.add_patch(rect)
|
||
|
||
# ========== 标注色块名称(上方)==========
|
||
self.accuracy_ax.text(
|
||
x + patch_width / 2,
|
||
y + patch_height + 0.015,
|
||
color_name,
|
||
ha="center",
|
||
va="bottom",
|
||
fontsize=5.5,
|
||
fontweight="bold",
|
||
transform=self.accuracy_ax.transAxes,
|
||
clip_on=False,
|
||
)
|
||
|
||
# ========== 标注 ΔE 值(中心)==========
|
||
dark_colors = [
|
||
"100% Red",
|
||
"100% Green",
|
||
"100% Blue",
|
||
"Gray 35",
|
||
"Dark Skin",
|
||
"Foliage",
|
||
"Purple",
|
||
"Purplish Blue",
|
||
"Blue (Legacy)",
|
||
"Green (Legacy)",
|
||
"Red (Legacy)",
|
||
"Magenta (Legacy)",
|
||
"Cyan (Legacy)",
|
||
]
|
||
|
||
text_color = "white" if color_name in dark_colors else "black"
|
||
|
||
self.accuracy_ax.text(
|
||
x + patch_width / 2,
|
||
y + patch_height / 2,
|
||
f"ΔE\n{delta_e:.2f}",
|
||
ha="center",
|
||
va="center",
|
||
fontsize=5.2,
|
||
fontweight="bold",
|
||
color=text_color,
|
||
transform=self.accuracy_ax.transAxes,
|
||
bbox=dict(
|
||
boxstyle="round,pad=0.22",
|
||
facecolor="white" if text_color == "black" else "black",
|
||
alpha=0.75,
|
||
edgecolor=edge_color,
|
||
linewidth=1.0,
|
||
),
|
||
)
|
||
|
||
# ========== 统计信息卡片(只保留外框)==========
|
||
card_width = 0.84
|
||
card_height = 0.15
|
||
card_x = 0.08
|
||
card_y = 0.01
|
||
|
||
info_card = Rectangle(
|
||
(card_x, card_y),
|
||
card_width,
|
||
card_height,
|
||
transform=self.accuracy_ax.transAxes,
|
||
facecolor="#F0F0F0",
|
||
edgecolor="black",
|
||
linewidth=1.5,
|
||
)
|
||
self.accuracy_ax.add_patch(info_card)
|
||
|
||
# ========== 标题(带说明)==========
|
||
self.accuracy_ax.text(
|
||
card_x + card_width / 2,
|
||
card_y + card_height - 0.008,
|
||
"色准统计(5灰阶 + 18 ColorChecker + 6饱和色 | ΔE 2000 标准)",
|
||
ha="center",
|
||
va="top",
|
||
fontsize=7.5,
|
||
fontweight="bold",
|
||
transform=self.accuracy_ax.transAxes,
|
||
)
|
||
|
||
# ========== 统计内容(无内部框)==========
|
||
stats_y = card_y + card_height * 0.55
|
||
|
||
# 左侧:ΔE 统计
|
||
left_x = card_x + 0.02
|
||
stats_text = [
|
||
f"平均 ΔE: {avg_delta_e:.2f}",
|
||
f"最大 ΔE: {max_delta_e:.2f}",
|
||
f"最小 ΔE: {min_delta_e:.2f}",
|
||
]
|
||
|
||
for i, text in enumerate(stats_text):
|
||
self.accuracy_ax.text(
|
||
left_x,
|
||
stats_y - i * 0.030,
|
||
text,
|
||
ha="left",
|
||
va="center",
|
||
fontsize=7,
|
||
fontweight="bold",
|
||
transform=self.accuracy_ax.transAxes,
|
||
)
|
||
|
||
# 中间:色块统计
|
||
middle_x = card_x + card_width * 0.32
|
||
|
||
self.accuracy_ax.text(
|
||
middle_x,
|
||
stats_y,
|
||
f"优秀 (ΔE<3): {excellent_count} 个",
|
||
ha="left",
|
||
va="center",
|
||
fontsize=7,
|
||
color="green",
|
||
fontweight="bold",
|
||
transform=self.accuracy_ax.transAxes,
|
||
)
|
||
|
||
self.accuracy_ax.text(
|
||
middle_x,
|
||
stats_y - 0.030,
|
||
f"良好 (3≤ΔE<5): {good_count} 个",
|
||
ha="left",
|
||
va="center",
|
||
fontsize=7,
|
||
color="orange",
|
||
fontweight="bold",
|
||
transform=self.accuracy_ax.transAxes,
|
||
)
|
||
|
||
self.accuracy_ax.text(
|
||
middle_x,
|
||
stats_y - 0.060,
|
||
f"偏差 (ΔE≥5): {poor_count} 个",
|
||
ha="left",
|
||
va="center",
|
||
fontsize=7,
|
||
color="red",
|
||
fontweight="bold",
|
||
transform=self.accuracy_ax.transAxes,
|
||
)
|
||
|
||
# 右侧:总体评价
|
||
right_x = card_x + card_width - 0.02
|
||
|
||
if avg_delta_e < 2:
|
||
grade = "专业级"
|
||
grade_icon = "★★★"
|
||
grade_color = "darkgreen"
|
||
elif avg_delta_e < 3:
|
||
grade = "优秀"
|
||
grade_icon = "✓✓"
|
||
grade_color = "green"
|
||
elif avg_delta_e < 5:
|
||
grade = "良好"
|
||
grade_icon = "✓"
|
||
grade_color = "orange"
|
||
else:
|
||
grade = "需要校准"
|
||
grade_icon = "✗"
|
||
grade_color = "red"
|
||
|
||
self.accuracy_ax.text(
|
||
right_x,
|
||
stats_y + 0.020,
|
||
"总体评价:",
|
||
ha="right",
|
||
va="bottom",
|
||
fontsize=7,
|
||
fontweight="bold",
|
||
transform=self.accuracy_ax.transAxes,
|
||
)
|
||
|
||
self.accuracy_ax.text(
|
||
right_x,
|
||
stats_y - 0.025,
|
||
f"{grade} {grade_icon}",
|
||
ha="right",
|
||
va="top",
|
||
fontsize=11,
|
||
fontweight="bold",
|
||
color=grade_color,
|
||
transform=self.accuracy_ax.transAxes,
|
||
)
|
||
|
||
self.accuracy_canvas.draw()
|
||
self.chart_notebook.select(4)
|
||
|
||
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()
|