重构移动utils文件夹

This commit is contained in:
xinzhu.yin
2026-04-20 11:48:38 +08:00
parent b6c1c2ab93
commit 2e92b48496
27 changed files with 2866 additions and 3085 deletions

View File

@@ -4,7 +4,7 @@
Full Range (0-255) 转换为 Limited Range (16-235) Full Range (0-255) 转换为 Limited Range (16-235)
使用方法: 使用方法:
from utils.data_range_converter import DataRangeConverter from app.data_range_converter import DataRangeConverter
converter = DataRangeConverter() converter = DataRangeConverter()
converted_params = converter.convert(pattern_params, "Limited") converted_params = converter.convert(pattern_params, "Limited")
@@ -187,7 +187,7 @@ def convert_pattern_params(pattern_params, data_range="Full", verbose=True):
list: 转换后的图案参数列表 list: 转换后的图案参数列表
示例: 示例:
>>> from utils.data_range_converter import convert_pattern_params >>> from app.data_range_converter import convert_pattern_params
>>> params = [[0,0,0], [255,255,255]] >>> params = [[0,0,0], [255,255,255]]
>>> converted = convert_pattern_params(params, "Limited") >>> converted = convert_pattern_params(params, "Limited")
[[16,16,16], [235,235,235]] [[16,16,16], [235,235,235]]
@@ -208,7 +208,7 @@ def convert_single_rgb(r, g, b, data_range="Full"):
tuple: 转换后的 RGB tuple: 转换后的 RGB
示例: 示例:
>>> from utils.data_range_converter import convert_single_rgb >>> from app.data_range_converter import convert_single_rgb
>>> r, g, b = convert_single_rgb(0, 0, 0, "Limited") >>> r, g, b = convert_single_rgb(0, 0, 0, "Limited")
(16, 16, 16) (16, 16, 16)
""" """

View File

@@ -8,7 +8,7 @@ import threading
import time import time
from tkinter import messagebox from tkinter import messagebox
from utils.caSerail import CASerail from drivers.caSerail import CASerail
def get_available_ucd_ports(self): def get_available_ucd_ports(self):
"""获取可用的UCD端口列表""" """获取可用的UCD端口列表"""

0
app/pq/__init__.py Normal file
View File

View File

@@ -14,8 +14,8 @@ import colour
import numpy as np import numpy as np
import algorithm.pq_algorithm as pq_algorithm import algorithm.pq_algorithm as pq_algorithm
from utils.data_range_converter import convert_pattern_params from app.data_range_converter import convert_pattern_params
from utils.pq.pq_result import PQResult from app.pq.pq_result import PQResult
def new_pq_results(self, test_type, test_name): def new_pq_results(self, test_type, test_name):
self.results = PQResult(test_type, test_name) self.results = PQResult(test_type, test_name)

View File

@@ -1,127 +1,214 @@
"""Local Dimming 测试逻辑(Step 4 重构)。 """Local Dimming 测试逻辑(应用层)。
从 pqAutomationApp.PQAutomationApp 中搬迁。每个函数第一行 `self = app` 整合自原 drivers/local_dimming_test.py窗口图片生成与测试主循环
以保留原有 `self.xxx` 属性访问不变 直接落在本模块UCD 通用操作下沉到 drivers.ucd_helpers
""" """
import atexit
import csv
import datetime
import os
import shutil
import sys
import threading import threading
import time
import tkinter as tk import tkinter as tk
from tkinter import filedialog, messagebox from tkinter import filedialog, messagebox
import numpy as np
from PIL import Image
from drivers.ucd_helpers import get_current_resolution, send_image_pattern
# --------------------------------------------------------------------------
# 模块级常量与窗口图片缓存
# --------------------------------------------------------------------------
DEFAULT_WINDOW_PERCENTAGES = [1, 2, 5, 10, 18, 25, 50, 75, 100]
_TEMP_DIR = None
_IMAGE_CACHE = {} # {(width, height, percentage): file_path}
def _cleanup_temp_dir():
global _TEMP_DIR
if _TEMP_DIR and os.path.exists(_TEMP_DIR):
try:
shutil.rmtree(_TEMP_DIR)
except Exception:
pass
_TEMP_DIR = None
_IMAGE_CACHE.clear()
def _get_temp_dir():
global _TEMP_DIR
if _TEMP_DIR is None:
if getattr(sys, "frozen", False):
base = os.path.dirname(sys.executable)
else:
base = os.getcwd()
_TEMP_DIR = os.path.join(base, "temp_local_dimming")
os.makedirs(_TEMP_DIR, exist_ok=True)
atexit.register(_cleanup_temp_dir)
return _TEMP_DIR
def _make_window_image_array(width, height, percentage):
"""生成黑底+居中白窗的 numpy 图像,保持屏幕比例。"""
image = np.zeros((height, width, 3), dtype=np.uint8)
if percentage >= 100:
ww, wh = width, height
else:
scale = (percentage / 100.0) ** 0.5
ww = int(width * scale)
wh = int(height * scale)
x1 = (width - ww) // 2
y1 = (height - wh) // 2
image[y1:y1 + wh, x1:x1 + ww] = 255
return image
def _ensure_window_image(width, height, percentage):
"""生成或复用缓存的窗口 PNG 文件,返回路径。"""
key = (width, height, percentage)
cached = _IMAGE_CACHE.get(key)
if cached and os.path.exists(cached):
return cached
arr = _make_window_image_array(width, height, percentage)
fname = f"window_{width}x{height}_{percentage:03d}percent.png"
path = os.path.join(_get_temp_dir(), fname)
Image.fromarray(arr, mode="RGB").save(path, format="PNG")
_IMAGE_CACHE[key] = path
return path
# --------------------------------------------------------------------------
# GUI 入口(绑定为 PQAutomationApp 方法)
# --------------------------------------------------------------------------
def start_local_dimming_test(self): def start_local_dimming_test(self):
"""开始 Local Dimming 测试""" """开始 Local Dimming 测试"""
# 检查设备连接
if not self.ca or not self.ucd.status: if not self.ca or not self.ucd.status:
messagebox.showerror("错误", "请先连接 CA410 和 UCD323") messagebox.showerror("错误", "请先连接 CA410 和 UCD323")
return return
# 禁用按钮
self.ld_start_btn.config(state=tk.DISABLED) self.ld_start_btn.config(state=tk.DISABLED)
self.ld_stop_btn.config(state=tk.NORMAL) self.ld_stop_btn.config(state=tk.NORMAL)
self.ld_save_btn.config(state=tk.DISABLED) self.ld_save_btn.config(state=tk.DISABLED)
# 清空结果
for item in self.ld_tree.get_children(): for item in self.ld_tree.get_children():
self.ld_tree.delete(item) self.ld_tree.delete(item)
# 获取配置
wait_time = float(self.ld_wait_time_var.get()) wait_time = float(self.ld_wait_time_var.get())
stop_event = threading.Event()
self.ld_stop_event = stop_event
# 在新线程中执行测试 def worker():
def run_test(): log = self.log_gui.log
from utils.local_dimming_test import LocalDimmingTest, LocalDimmingController log("=" * 60)
log("开始 Local Dimming 测试")
log("=" * 60)
# 从设备当前 timing 获取分辨率 width, height = get_current_resolution(self.ucd)
ld_ctrl = LocalDimmingController(self.ucd) total = len(DEFAULT_WINDOW_PERCENTAGES)
cur_w, cur_h = ld_ctrl.get_current_resolution() log(f" 分辨率: {width}x{height}")
resolution = f"{cur_w}x{cur_h}" log(f" 测试窗口: {DEFAULT_WINDOW_PERCENTAGES}")
log(f" 等待时间: {wait_time}")
ld_test = LocalDimmingTest( results = []
self.ucd, for i, percentage in enumerate(DEFAULT_WINDOW_PERCENTAGES, 1):
self.ca, if stop_event.is_set():
log_callback=self.log_gui.log, log("⚠️ 测试已停止")
) break
ld_test.wait_time = wait_time log(f"[{i}/{total}] 测试 {percentage}% 窗口...")
try:
image_path = _ensure_window_image(width, height, percentage)
except Exception as e:
log(f" ❌ 图像生成失败: {e}")
continue
results = ld_test.run_test(resolution=resolution) if not send_image_pattern(self.ucd, image_path):
log(f"{percentage}% 窗口发送失败,跳过")
continue
log(f" ⏳ 等待 {wait_time} 秒...")
time.sleep(wait_time)
try:
x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay()
except Exception as e:
log(f" ❌ 采集亮度异常: {e}")
continue
if lv is None:
log(f"{percentage}% 窗口采集失败")
continue
log(f" ✓ 采集亮度: {lv:.2f} cd/m²")
results.append((percentage, x, y, lv, _X, _Y, _Z))
log("=" * 60)
log(f"✅ Local Dimming 测试完成 ({len(results)}/{total})")
log("=" * 60)
# 保存到实例变量
self.ld_test_instance = ld_test
self.ld_test_results = results self.ld_test_results = results
# 更新结果显示
self.root.after(0, lambda: self.update_ld_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_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_stop_btn.config(state=tk.DISABLED))
self.root.after(0, lambda: self.ld_save_btn.config(state=tk.NORMAL)) self.root.after(0, lambda: self.ld_save_btn.config(state=tk.NORMAL))
threading.Thread(target=run_test, daemon=True).start() threading.Thread(target=worker, daemon=True).start()
def update_ld_results(self, results): def update_ld_results(self, results):
"""更新 Local Dimming 结果显示""" """把批量测试结果填入 Treeview。"""
for percentage, x, y, lv, X, Y, Z in results: for percentage, x, y, lv, _X, _Y, _Z in results:
self.ld_tree.insert( self.ld_tree.insert(
"", "", tk.END,
tk.END,
values=(f"{percentage}%", f"{lv:.2f}", f"{x:.4f}", f"{y:.4f}"), values=(f"{percentage}%", f"{lv:.2f}", f"{x:.4f}", f"{y:.4f}"),
) )
def stop_local_dimming_test(self): def stop_local_dimming_test(self):
"""停止 Local Dimming 测试""" """请求停止当前 Local Dimming 测试"""
if hasattr(self, "ld_test_instance"): ev = getattr(self, "ld_stop_event", None)
self.ld_test_instance.stop() if ev:
ev.set()
def send_ld_window(self, percentage): def send_ld_window(self, percentage):
"""发送指定百分比的窗口""" """发送指定百分比的白色窗口(手动模式)。"""
if not self.ucd.status: if not self.ucd.status:
messagebox.showwarning("警告", "请先连接 UCD323 设备") messagebox.showwarning("警告", "请先连接 UCD323 设备")
return return
self.log_gui.log(f"🔆 发送 {percentage}% 窗口...") self.log_gui.log(f"🔆 发送 {percentage}% 窗口...")
# 记录当前百分比(用于测量)
self.current_ld_percentage = percentage self.current_ld_percentage = percentage
def send(): def send():
from utils.local_dimming_test import LocalDimmingController width, height = get_current_resolution(self.ucd)
try:
ld_controller = LocalDimmingController(self.ucd) image_path = _ensure_window_image(width, height, percentage)
except Exception as e:
# 从设备当前 timing 获取分辨率 self.root.after(0, lambda: self.log_gui.log(f"❌ 图像生成失败: {e}"))
width, height = ld_controller.get_current_resolution() return
ok = send_image_pattern(self.ucd, image_path)
# 生成并发送图片 msg = (
success = ld_controller.send_window_pattern_with_resolution( f"{percentage}% 窗口已发送" if ok
percentage, width, height else f"{percentage}% 窗口发送失败"
)
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}% 窗口发送失败")
) )
self.root.after(0, lambda: self.log_gui.log(msg))
threading.Thread(target=send, daemon=True).start() threading.Thread(target=send, daemon=True).start()
def measure_ld_luminance(self): def measure_ld_luminance(self):
"""测量当前亮度""" """测量当前显示的亮度并追加一行到 Treeview。"""
if not self.ca: if not self.ca:
messagebox.showwarning("警告", "请先连接 CA410 色度计") messagebox.showwarning("警告", "请先连接 CA410 色度计")
return return
if self.current_ld_percentage is None: if self.current_ld_percentage is None:
messagebox.showinfo("提示", "请先发送一个窗口图案") messagebox.showinfo("提示", "请先发送一个窗口图案")
return return
@@ -130,51 +217,31 @@ def measure_ld_luminance(self):
def measure(): def measure():
try: try:
x, y, lv, X, Y, Z = self.ca.readAllDisplay() 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: except Exception as e:
self.root.after(0, lambda: self.log_gui.log(f"❌ 采集异常: {str(e)}")) self.root.after(0, lambda: self.log_gui.log(f"❌ 采集异常: {str(e)}"))
return
if lv is None:
self.root.after(0, lambda: self.log_gui.log("❌ 采集失败"))
return
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²"))
threading.Thread(target=measure, daemon=True).start() threading.Thread(target=measure, daemon=True).start()
def clear_ld_records(self): def clear_ld_records(self):
"""清空测试记录""" """清空 Treeview 中的测试记录"""
for item in self.ld_tree.get_children(): for item in self.ld_tree.get_children():
self.ld_tree.delete(item) self.ld_tree.delete(item)
self.ld_result_label.config(text="亮度: -- cd/m² | x: -- | y: --") self.ld_result_label.config(text="亮度: -- cd/m² | x: -- | y: --")
@@ -183,24 +250,20 @@ def clear_ld_records(self):
def save_local_dimming_results(self): def save_local_dimming_results(self):
"""保存 Local Dimming 结果""" """把 Treeview 中的全部记录导出为 CSV。"""
from tkinter import filedialog
import csv
import datetime
if len(self.ld_tree.get_children()) == 0: if len(self.ld_tree.get_children()) == 0:
messagebox.showinfo("提示", "没有可保存的数据") messagebox.showinfo("提示", "没有可保存的数据")
return return
default_name = f"LocalDimming_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" default_name = (
f"LocalDimming_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
)
save_path = filedialog.asksaveasfilename( save_path = filedialog.asksaveasfilename(
title="保存测试结果", title="保存测试结果",
initialfile=default_name, initialfile=default_name,
defaultextension=".csv", defaultextension=".csv",
filetypes=[("CSV 文件", "*.csv"), ("所有文件", "*.*")], filetypes=[("CSV 文件", "*.csv"), ("所有文件", "*.*")],
) )
if not save_path: if not save_path:
return return
@@ -208,14 +271,10 @@ def save_local_dimming_results(self):
with open(save_path, "w", newline="", encoding="utf-8-sig") as f: with open(save_path, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f) writer = csv.writer(f)
writer.writerow(["窗口百分比", "亮度 (cd/m²)", "x", "y", "时间"]) writer.writerow(["窗口百分比", "亮度 (cd/m²)", "x", "y", "时间"])
for item in self.ld_tree.get_children(): for item in self.ld_tree.get_children():
values = self.ld_tree.item(item, "values") writer.writerow(self.ld_tree.item(item, "values"))
writer.writerow(values)
self.log_gui.log(f"✓ 测试结果已保存: {save_path}") self.log_gui.log(f"✓ 测试结果已保存: {save_path}")
messagebox.showinfo("成功", f"测试结果已保存到:\n{save_path}") messagebox.showinfo("成功", f"测试结果已保存到:\n{save_path}")
except Exception as e: except Exception as e:
self.log_gui.log(f"❌ 保存失败: {str(e)}") self.log_gui.log(f"❌ 保存失败: {str(e)}")
messagebox.showerror("错误", f"保存失败: {str(e)}") messagebox.showerror("错误", f"保存失败: {str(e)}")

View File

@@ -8,7 +8,7 @@ import tkinter as tk
import ttkbootstrap as ttk import ttkbootstrap as ttk
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from views.pq_debug_panel import PQDebugPanel from app.views.pq_debug_panel import PQDebugPanel
def init_gamut_chart(self): def init_gamut_chart(self):
"""初始化色域图表 - 手动设置subplot位置完全避免重叠""" """初始化色域图表 - 手动设置subplot位置完全避免重叠"""

View File

@@ -22,11 +22,12 @@ def get_resource_path(relative_path):
base_path = sys._MEIPASS base_path = sys._MEIPASS
except AttributeError: except AttributeError:
# 开发环境:使用项目根目录 # 开发环境:使用项目根目录
# 当前文件: views/collapsing_frame.py # 当前文件: app/views/collapsing_frame.py
# 项目根目录: views 的父目录 # 项目根目录: app/views 的父目录
current_file = os.path.abspath(__file__) current_file = os.path.abspath(__file__)
views_dir = os.path.dirname(current_file) views_dir = os.path.dirname(current_file)
base_path = os.path.dirname(views_dir) app_dir = os.path.dirname(views_dir)
base_path = os.path.dirname(app_dir)
return os.path.join(base_path, relative_path) return os.path.join(base_path, relative_path)

View File

@@ -0,0 +1,72 @@
"""面板管理器Step 6 重构)。
register_panel / show_panel / hide_all_panels —— 在右侧栏不同浮动面板间切换。
"""
import tkinter as tk
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

View File

View File

@@ -0,0 +1,901 @@
"""CCT 参数面板及其处理函数Step 6 重构)。"""
import time
import traceback
from tkinter import messagebox
import tkinter as tk
import ttkbootstrap as ttk
import algorithm.pq_algorithm as pq_algorithm
def create_cct_params_frame(self):
"""创建色度参数设置区域 - 屏模组、SDR、HDR 独立(✅ 增加色域参考标准选择 + 单步调试按钮)"""
# ==================== 屏模组色度参数 Frame ====================
self.cct_params_frame = ttk.LabelFrame(
self.test_items_frame, text="色度参数设置(屏模组)"
)
# 默认值
self.DEFAULT_CCT_PARAMS = {
"x_ideal": 0.3127,
"x_tolerance": 0.003,
"y_ideal": 0.3290,
"y_tolerance": 0.003,
}
# 从配置读取屏模组参数
saved_params = self.config.current_test_types.get("screen_module", {}).get(
"cct_params", self.DEFAULT_CCT_PARAMS.copy()
)
# 色域参考标准
saved_gamut_ref = self.config.current_test_types.get("screen_module", {}).get(
"gamut_reference", "DCI-P3"
)
# 创建屏模组变量
self.cct_x_ideal_var = tk.StringVar(
value=str(saved_params.get("x_ideal", 0.3127))
)
self.cct_x_tolerance_var = tk.StringVar(
value=str(saved_params.get("x_tolerance", 0.003))
)
self.cct_y_ideal_var = tk.StringVar(
value=str(saved_params.get("y_ideal", 0.3290))
)
self.cct_y_tolerance_var = tk.StringVar(
value=str(saved_params.get("y_tolerance", 0.003))
)
self.screen_gamut_ref_var = tk.StringVar(value=saved_gamut_ref)
# 创建屏模组输入框(左侧:色度参数)
params = [
("x-ideal:", self.cct_x_ideal_var, "x_ideal"),
("x-tolerance:", self.cct_x_tolerance_var, "x_tolerance"),
("y-ideal:", self.cct_y_ideal_var, "y_ideal"),
("y-tolerance:", self.cct_y_tolerance_var, "y_tolerance"),
]
for i, (label_text, var, key) in enumerate(params):
ttk.Label(self.cct_params_frame, text=label_text).grid(
row=i, column=0, sticky=tk.W, padx=5, pady=3
)
entry = ttk.Entry(self.cct_params_frame, textvariable=var, width=15)
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
# 绑定失去焦点事件
default_val = self.DEFAULT_CCT_PARAMS[key]
entry.bind(
"<FocusOut>",
lambda e, v=var, d=default_val: self.on_cct_param_focus_out(v, d),
)
# 色域参考标准选择(右侧第一行)
ttk.Label(self.cct_params_frame, text="色域参考标准:").grid(
row=0, column=2, sticky=tk.W, padx=(20, 5), pady=3
)
screen_gamut_combo = ttk.Combobox(
self.cct_params_frame,
textvariable=self.screen_gamut_ref_var,
values=["BT.2020", "BT.709", "DCI-P3"],
state="disabled",
width=12,
)
screen_gamut_combo.grid(row=0, column=3, sticky=tk.W, padx=5, pady=3)
screen_gamut_combo.bind(
"<<ComboboxSelected>>", self.on_screen_gamut_ref_changed
)
self.screen_gamut_combo = screen_gamut_combo
# ==================== ✅ 单步调试按钮(右侧第二行)====================
ttk.Label(self.cct_params_frame, text="单步调试:").grid(
row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3
)
self.screen_debug_btn = ttk.Button(
self.cct_params_frame,
text="打开调试面板",
command=self.toggle_screen_debug_panel,
bootstyle="info-outline",
state=tk.DISABLED, # 初始禁用
width=15,
)
self.screen_debug_btn.grid(row=1, column=3, sticky=tk.W, padx=5, pady=3)
# 重新计算按钮(屏模组)
self.recalc_cct_btn = ttk.Button(
self.cct_params_frame,
text="应用新参数并重绘",
command=self.recalculate_cct,
bootstyle="success",
)
self.recalc_cct_btn.grid(
row=4, column=0, columnspan=2, pady=10, padx=5, sticky="ew"
)
self.recalc_cct_btn.grid_remove()
# 色域重新计算按钮
self.recalc_gamut_btn = ttk.Button(
self.cct_params_frame,
text="应用色域参考并重绘",
command=self.recalculate_gamut,
bootstyle="warning",
)
self.recalc_gamut_btn.grid(
row=4, column=2, columnspan=2, pady=10, padx=5, sticky="ew"
)
self.recalc_gamut_btn.grid_remove()
# 提示文字
ttk.Label(
self.cct_params_frame,
text="提示: 清空输入框将恢复默认值",
font=("SimHei", 8),
foreground="gray",
).grid(row=5, column=0, columnspan=4, sticky=tk.W, padx=5, pady=5)
# ==================== SDR 色度参数 Frame ====================
self.sdr_cct_params_frame = ttk.LabelFrame(
self.test_items_frame, text="色度参数设置SDR"
)
# SDR 默认值
self.SDR_DEFAULT_CCT_PARAMS = {
"x_ideal": 0.3127,
"x_tolerance": 0.003,
"y_ideal": 0.3290,
"y_tolerance": 0.003,
}
# 从配置读取 SDR 参数
sdr_saved_params = self.config.current_test_types.get("sdr_movie", {}).get(
"cct_params", self.SDR_DEFAULT_CCT_PARAMS.copy()
)
# 色域参考标准
sdr_saved_gamut_ref = self.config.current_test_types.get("sdr_movie", {}).get(
"gamut_reference", "BT.709"
)
# 创建 SDR 变量
self.sdr_cct_x_ideal_var = tk.StringVar(
value=str(sdr_saved_params.get("x_ideal", 0.3127))
)
self.sdr_cct_x_tolerance_var = tk.StringVar(
value=str(sdr_saved_params.get("x_tolerance", 0.003))
)
self.sdr_cct_y_ideal_var = tk.StringVar(
value=str(sdr_saved_params.get("y_ideal", 0.3290))
)
self.sdr_cct_y_tolerance_var = tk.StringVar(
value=str(sdr_saved_params.get("y_tolerance", 0.003))
)
self.sdr_gamut_ref_var = tk.StringVar(value=sdr_saved_gamut_ref)
# 创建 SDR 输入框
sdr_params = [
("x-ideal:", self.sdr_cct_x_ideal_var, "x_ideal"),
("x-tolerance:", self.sdr_cct_x_tolerance_var, "x_tolerance"),
("y-ideal:", self.sdr_cct_y_ideal_var, "y_ideal"),
("y-tolerance:", self.sdr_cct_y_tolerance_var, "y_tolerance"),
]
for i, (label_text, var, key) in enumerate(sdr_params):
ttk.Label(self.sdr_cct_params_frame, text=label_text).grid(
row=i, column=0, sticky=tk.W, padx=5, pady=3
)
entry = ttk.Entry(self.sdr_cct_params_frame, textvariable=var, width=15)
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
# 绑定失去焦点事件
default_val = self.SDR_DEFAULT_CCT_PARAMS[key]
entry.bind(
"<FocusOut>",
lambda e, v=var, d=default_val: self.on_sdr_cct_param_focus_out(v, d),
)
# 色域参考标准选择(右侧第一行)
ttk.Label(self.sdr_cct_params_frame, text="色域参考标准:").grid(
row=0, column=2, sticky=tk.W, padx=(20, 5), pady=3
)
sdr_gamut_combo = ttk.Combobox(
self.sdr_cct_params_frame,
textvariable=self.sdr_gamut_ref_var,
values=["BT.2020", "BT.709", "DCI-P3"],
state="disabled",
width=12,
)
sdr_gamut_combo.grid(row=0, column=3, sticky=tk.W, padx=5, pady=3)
sdr_gamut_combo.bind("<<ComboboxSelected>>", self.on_sdr_gamut_ref_changed)
self.sdr_gamut_combo = sdr_gamut_combo
# ==================== ✅ SDR 单步调试按钮(右侧第二行)====================
ttk.Label(self.sdr_cct_params_frame, text="单步调试:").grid(
row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3
)
self.sdr_debug_btn = ttk.Button(
self.sdr_cct_params_frame,
text="打开调试面板",
command=self.toggle_sdr_debug_panel,
bootstyle="info-outline",
state=tk.DISABLED, # 初始禁用
width=15,
)
self.sdr_debug_btn.grid(row=1, column=3, sticky=tk.W, padx=5, pady=3)
# 重新计算按钮SDR
self.sdr_recalc_cct_btn = ttk.Button(
self.sdr_cct_params_frame,
text="应用新参数并重绘",
command=self.recalculate_cct,
bootstyle="success",
)
self.sdr_recalc_cct_btn.grid(
row=4, column=0, columnspan=2, pady=10, padx=5, sticky="ew"
)
self.sdr_recalc_cct_btn.grid_remove()
# 色域重新计算按钮SDR
self.sdr_recalc_gamut_btn = ttk.Button(
self.sdr_cct_params_frame,
text="应用色域参考并重绘",
command=self.recalculate_gamut,
bootstyle="warning",
)
self.sdr_recalc_gamut_btn.grid(
row=4, column=2, columnspan=2, pady=10, padx=5, sticky="ew"
)
self.sdr_recalc_gamut_btn.grid_remove()
# 提示文字
ttk.Label(
self.sdr_cct_params_frame,
text="提示: 清空输入框将恢复默认值",
font=("SimHei", 8),
foreground="gray",
).grid(row=5, column=0, columnspan=4, sticky=tk.W, padx=5, pady=5)
# ==================== HDR 色度参数 Frame ====================
self.hdr_cct_params_frame = ttk.LabelFrame(
self.test_items_frame, text="色度参数设置HDR"
)
# HDR 默认值
self.HDR_DEFAULT_CCT_PARAMS = {
"x_ideal": 0.3127,
"x_tolerance": 0.003,
"y_ideal": 0.3290,
"y_tolerance": 0.003,
}
# 从配置读取 HDR 参数
hdr_saved_params = self.config.current_test_types.get("hdr_movie", {}).get(
"cct_params", self.HDR_DEFAULT_CCT_PARAMS.copy()
)
# 色域参考标准
hdr_saved_gamut_ref = self.config.current_test_types.get("hdr_movie", {}).get(
"gamut_reference", "BT.2020"
)
# 创建 HDR 变量
self.hdr_cct_x_ideal_var = tk.StringVar(
value=str(hdr_saved_params.get("x_ideal", 0.3127))
)
self.hdr_cct_x_tolerance_var = tk.StringVar(
value=str(hdr_saved_params.get("x_tolerance", 0.003))
)
self.hdr_cct_y_ideal_var = tk.StringVar(
value=str(hdr_saved_params.get("y_ideal", 0.3290))
)
self.hdr_cct_y_tolerance_var = tk.StringVar(
value=str(hdr_saved_params.get("y_tolerance", 0.003))
)
self.hdr_gamut_ref_var = tk.StringVar(value=hdr_saved_gamut_ref)
# 创建 HDR 输入框
hdr_params = [
("x-ideal:", self.hdr_cct_x_ideal_var, "x_ideal"),
("x-tolerance:", self.hdr_cct_x_tolerance_var, "x_tolerance"),
("y-ideal:", self.hdr_cct_y_ideal_var, "y_ideal"),
("y-tolerance:", self.hdr_cct_y_tolerance_var, "y_tolerance"),
]
for i, (label_text, var, key) in enumerate(hdr_params):
ttk.Label(self.hdr_cct_params_frame, text=label_text).grid(
row=i, column=0, sticky=tk.W, padx=5, pady=3
)
entry = ttk.Entry(self.hdr_cct_params_frame, textvariable=var, width=15)
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
# 绑定失去焦点事件
default_val = self.HDR_DEFAULT_CCT_PARAMS[key]
entry.bind(
"<FocusOut>",
lambda e, v=var, d=default_val: self.on_hdr_cct_param_focus_out(v, d),
)
# 色域参考标准选择(右侧第一行)
ttk.Label(self.hdr_cct_params_frame, text="色域参考标准:").grid(
row=0, column=2, sticky=tk.W, padx=(20, 5), pady=3
)
hdr_gamut_combo = ttk.Combobox(
self.hdr_cct_params_frame,
textvariable=self.hdr_gamut_ref_var,
values=["BT.2020", "BT.709", "DCI-P3"],
state="disabled",
width=12,
)
hdr_gamut_combo.grid(row=0, column=3, sticky=tk.W, padx=5, pady=3)
hdr_gamut_combo.bind("<<ComboboxSelected>>", self.on_hdr_gamut_ref_changed)
self.hdr_gamut_combo = hdr_gamut_combo
# ==================== ✅ HDR 单步调试按钮(右侧第二行)====================
ttk.Label(self.hdr_cct_params_frame, text="单步调试:").grid(
row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3
)
self.hdr_debug_btn = ttk.Button(
self.hdr_cct_params_frame,
text="打开调试面板",
command=self.toggle_hdr_debug_panel,
bootstyle="info-outline",
state=tk.DISABLED, # 初始禁用
width=15,
)
self.hdr_debug_btn.grid(row=1, column=3, sticky=tk.W, padx=5, pady=3)
# 重新计算按钮HDR
self.hdr_recalc_cct_btn = ttk.Button(
self.hdr_cct_params_frame,
text="应用新参数并重绘",
command=self.recalculate_cct,
bootstyle="success",
)
self.hdr_recalc_cct_btn.grid(
row=4, column=0, columnspan=2, pady=10, padx=5, sticky="ew"
)
self.hdr_recalc_cct_btn.grid_remove()
# 色域重新计算按钮HDR
self.hdr_recalc_gamut_btn = ttk.Button(
self.hdr_cct_params_frame,
text="应用色域参考并重绘",
command=self.recalculate_gamut,
bootstyle="warning",
)
self.hdr_recalc_gamut_btn.grid(
row=4, column=2, columnspan=2, pady=10, padx=5, sticky="ew"
)
self.hdr_recalc_gamut_btn.grid_remove()
# 提示文字
ttk.Label(
self.hdr_cct_params_frame,
text="提示: 清空输入框将恢复默认值",
font=("SimHei", 8),
foreground="gray",
).grid(row=5, column=0, columnspan=4, sticky=tk.W, padx=5, pady=5)
def on_sdr_cct_param_focus_out(self, var, default_value):
"""SDR 色度参数失去焦点时的处理"""
try:
value = var.get().strip()
if value == "":
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"✓ SDR 参数为空,恢复默认值: {default_value}")
else:
try:
float_val = float(value)
if float_val < 0 or float_val > 1:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(
f"⚠️ SDR 参数超出范围,恢复默认值: {default_value}"
)
except ValueError:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"⚠️ SDR 参数无效,恢复默认值: {default_value}")
self.save_sdr_cct_params()
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"处理 SDR 参数失败: {str(e)}")
def save_sdr_cct_params(self):
"""保存 SDR 色度参数"""
try:
def get_float(var, default):
try:
value = var.get().strip()
if value == "":
return default
return float(value)
except:
return default
sdr_cct_params = {
"x_ideal": get_float(
self.sdr_cct_x_ideal_var, self.SDR_DEFAULT_CCT_PARAMS["x_ideal"]
),
"x_tolerance": get_float(
self.sdr_cct_x_tolerance_var,
self.SDR_DEFAULT_CCT_PARAMS["x_tolerance"],
),
"y_ideal": get_float(
self.sdr_cct_y_ideal_var, self.SDR_DEFAULT_CCT_PARAMS["y_ideal"]
),
"y_tolerance": get_float(
self.sdr_cct_y_tolerance_var,
self.SDR_DEFAULT_CCT_PARAMS["y_tolerance"],
),
}
if "sdr_movie" not in self.config.current_test_types:
self.config.current_test_types["sdr_movie"] = {}
self.config.current_test_types["sdr_movie"]["cct_params"] = sdr_cct_params
self.save_pq_config()
except:
pass
def on_hdr_cct_param_focus_out(self, var, default_value):
"""HDR 色度参数失去焦点时的处理"""
try:
value = var.get().strip()
if value == "":
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"✓ HDR 参数为空,恢复默认值: {default_value}")
else:
try:
float_val = float(value)
if float_val < 0 or float_val > 1:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(
f"⚠️ HDR 参数超出范围,恢复默认值: {default_value}"
)
except ValueError:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"⚠️ HDR 参数无效,恢复默认值: {default_value}")
self.save_hdr_cct_params()
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"处理 HDR 参数失败: {str(e)}")
def save_hdr_cct_params(self):
"""保存 HDR 色度参数"""
try:
def get_float(var, default):
try:
value = var.get().strip()
if value == "":
return default
return float(value)
except:
return default
hdr_cct_params = {
"x_ideal": get_float(
self.hdr_cct_x_ideal_var, self.HDR_DEFAULT_CCT_PARAMS["x_ideal"]
),
"x_tolerance": get_float(
self.hdr_cct_x_tolerance_var,
self.HDR_DEFAULT_CCT_PARAMS["x_tolerance"],
),
"y_ideal": get_float(
self.hdr_cct_y_ideal_var, self.HDR_DEFAULT_CCT_PARAMS["y_ideal"]
),
"y_tolerance": get_float(
self.hdr_cct_y_tolerance_var,
self.HDR_DEFAULT_CCT_PARAMS["y_tolerance"],
),
}
if "hdr_movie" not in self.config.current_test_types:
self.config.current_test_types["hdr_movie"] = {}
self.config.current_test_types["hdr_movie"]["cct_params"] = hdr_cct_params
self.save_pq_config()
except:
pass
def recalculate_cct(self):
"""重新计算并绘制色度图"""
try:
# 1. 保存新参数
self.save_cct_params()
self.log_gui.log("✓ 色度参数已更新")
# 2. 收起配置项
if hasattr(self, "config_panel_frame"):
try:
if self.config_panel_frame.winfo_viewable():
self.config_panel_frame.btn.invoke()
self.root.update_idletasks()
time.sleep(0.1)
except:
pass
# 3. 跳转到色度图Tab
self.chart_notebook.select(self.cct_chart_frame)
self.root.update_idletasks()
# 4. 检查是否有数据
if not hasattr(self, "results") or not self.results:
self.log_gui.log("⚠️ 没有测试数据,无法重新绘制")
messagebox.showwarning("警告", "请先完成测试后再重新计算")
return
# 5. 获取保存的灰阶数据
gray_data = self.results.get_intermediate_data("shared", "gray")
if not gray_data:
gray_data = self.results.get_intermediate_data("cct", "gray")
if not gray_data or len(gray_data) < 2:
self.log_gui.log("⚠️ 没有可用的灰阶数据")
messagebox.showwarning("警告", "没有找到色度测试数据")
return
# 6. 重新计算 CCT
self.log_gui.log("=" * 50)
self.log_gui.log("开始重新计算色度一致性...")
self.log_gui.log("=" * 50)
import algorithm.pq_algorithm as pq_algorithm
cct_values = pq_algorithm.calculate_cct_from_results(gray_data)
# 7. 更新结果
self.results.set_test_item_result("cct", {"cct_values": cct_values})
# 8. 重新绘制色度图
test_type = self.config.current_test_type
self.plot_cct(test_type)
self.log_gui.log("✓ 色度图已重新绘制")
self.log_gui.log("=" * 50)
messagebox.showinfo("成功", "色度图已根据新参数重新绘制!")
except Exception as e:
self.log_gui.log(f"❌ 重新计算失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
messagebox.showerror("错误", f"重新计算失败: {str(e)}")
def recalculate_gamut(self):
"""重新计算并绘制色域图(使用新的参考标准)"""
try:
# 1. 收起配置项
if hasattr(self, "config_panel_frame"):
try:
if self.config_panel_frame.winfo_viewable():
self.config_panel_frame.btn.invoke()
self.root.update_idletasks()
time.sleep(0.1)
except:
pass
# 2. 跳转到色域图Tab
self.chart_notebook.select(self.gamut_chart_frame)
self.root.update_idletasks()
# 3. 检查是否有数据
if not hasattr(self, "results") or not self.results:
self.log_gui.log("⚠️ 没有测试数据,无法重新绘制")
messagebox.showwarning("警告", "请先完成测试后再重新计算")
return
# 4. 获取保存的色域数据
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
if not rgb_data or len(rgb_data) < 3:
self.log_gui.log("⚠️ 没有可用的色域数据")
messagebox.showwarning("警告", "没有找到色域测试数据")
return
# 5. 获取当前测试类型
test_type = self.config.current_test_type
# 6. 获取用户选择的参考标准
if test_type == "screen_module":
reference_standard = self.screen_gamut_ref_var.get()
elif test_type == "sdr_movie":
reference_standard = self.sdr_gamut_ref_var.get()
elif test_type == "hdr_movie":
reference_standard = self.hdr_gamut_ref_var.get()
else:
reference_standard = "DCI-P3"
self.log_gui.log("=" * 50)
self.log_gui.log(f"开始重新计算色域(参考标准: {reference_standard}...")
self.log_gui.log("=" * 50)
# 7. 重新计算 XY 色域覆盖率
xy_points = [[result[0], result[1]] for result in rgb_data]
# 根据参考标准计算 XY 覆盖率
if reference_standard == "BT.2020":
area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_BT2020(
xy_points
)
elif reference_standard == "BT.709":
area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_BT709(
xy_points
)
elif reference_standard == "DCI-P3":
area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_DCIP3(
xy_points
)
else:
area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_DCIP3(
xy_points
)
reference_standard = "DCI-P3"
self.log_gui.log(f"✓ 参考标准: {reference_standard}")
self.log_gui.log(f"✓ XY 色域覆盖率: {coverage_xy:.1f}%")
# ========== ✅✅✅ 8. 重新计算 UV 色域覆盖率 ==========
# 将 XY 坐标转换为 UV 坐标
uv_points = []
for x, y in xy_points:
try:
# XY转UV公式
denom = -2 * x + 12 * y + 3
if abs(denom) < 1e-10:
u, v = 0, 0
else:
u = 4 * x / denom
v = 9 * y / denom
uv_points.append([u, v])
except ZeroDivisionError:
continue
self.log_gui.log(f"✓ 转换后的 UV 点数量: {len(uv_points)}")
# 根据参考标准计算 UV 覆盖率
if reference_standard == "BT.2020":
area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_BT2020_uv(
uv_points
)
elif reference_standard == "BT.709":
area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_BT709_uv(
uv_points
)
elif reference_standard == "DCI-P3":
area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_DCIP3_uv(
uv_points
)
else:
area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_DCIP3_uv(
uv_points
)
self.log_gui.log(f"✓ UV 色域覆盖率: {coverage_uv:.1f}%")
# ========================================================
# 9. ✅ 更新结果(同时保存 XY 和 UV 覆盖率)
self.results.set_test_item_result(
"gamut",
{
"area": area_xy, # ← 兼容旧字段
"coverage": coverage_xy, # ← 兼容旧字段
"area_xy": area_xy, # ← XY 面积
"coverage_xy": coverage_xy, # ← XY 覆盖率
"area_uv": area_uv, # ← UV 面积
"coverage_uv": coverage_uv, # ← UV 覆盖率
"uv_coverage": coverage_uv, # ← 兼容字段Excel 导出用)
"reference": reference_standard,
},
)
self.log_gui.log("✓ 测试结果已更新到 results 对象")
# 10. 重新绘制色域图
self.plot_gamut(rgb_data, coverage_xy, test_type)
self.log_gui.log("✓ 色域图已重新绘制")
self.log_gui.log("=" * 50)
messagebox.showinfo(
"成功",
f"色域图已根据新参考标准 {reference_standard} 重新绘制!\n\n"
f"XY 覆盖率: {coverage_xy:.1f}%\n"
f"UV 覆盖率: {coverage_uv:.1f}%",
)
except Exception as e:
self.log_gui.log(f"❌ 重新计算失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
messagebox.showerror("错误", f"重新计算失败: {str(e)}")
def on_cct_param_change(self, var, default_value):
"""色度参数改变时的处理 - 空值恢复默认"""
try:
value = var.get().strip()
if value == "":
# 空值:恢复默认值
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"输入框为空,恢复默认值: {default_value}")
else:
# 验证是否为有效数字
try:
float_val = float(value)
if float_val < 0 or float_val > 1:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(
f"参数超出范围 [0, 1],恢复默认值: {default_value}"
)
except ValueError:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"无效的参数值,恢复默认值: {default_value}")
# 保存配置
self.save_cct_params()
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"处理参数变化失败: {str(e)}")
def on_cct_param_focus_out(self, var, default_value):
"""色度参数失去焦点时的处理 - 空值恢复默认"""
try:
value = var.get().strip()
if value == "":
# 空值:恢复默认值
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"✓ 输入框为空,恢复默认值: {default_value}")
else:
# 验证是否为有效数字
try:
float_val = float(value)
if float_val < 0 or float_val > 1:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(
f"⚠️ 参数超出范围 [0, 1],恢复默认值: {default_value}"
)
except ValueError:
var.set(str(default_value))
if hasattr(self, "log_gui"):
self.log_gui.log(f"⚠️ 无效的参数值,恢复默认值: {default_value}")
# 保存配置
self.save_cct_params()
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"处理参数变化失败: {str(e)}")
def save_cct_params(self):
"""保存色度参数 - 简化版"""
try:
current_type = self.config.current_test_type
def get_float(var, default):
try:
value = var.get().strip()
if value == "":
return default
return float(value)
except:
return default
cct_params = {
"x_ideal": get_float(
self.cct_x_ideal_var, self.DEFAULT_CCT_PARAMS["x_ideal"]
),
"x_tolerance": get_float(
self.cct_x_tolerance_var, self.DEFAULT_CCT_PARAMS["x_tolerance"]
),
"y_ideal": get_float(
self.cct_y_ideal_var, self.DEFAULT_CCT_PARAMS["y_ideal"]
),
"y_tolerance": get_float(
self.cct_y_tolerance_var, self.DEFAULT_CCT_PARAMS["y_tolerance"]
),
}
if current_type not in self.config.current_test_types:
self.config.current_test_types[current_type] = {}
self.config.current_test_types[current_type]["cct_params"] = cct_params
self.save_pq_config()
except:
pass
def reload_cct_params(self):
"""切换测试类型时重新加载色度参数"""
try:
current_type = self.config.current_test_type
saved_params = self.config.current_test_types.get(current_type, {}).get(
"cct_params", None
)
if saved_params is None:
saved_params = self.DEFAULT_CCT_PARAMS.copy()
# 更新输入框的值
self.cct_x_ideal_var.set(str(saved_params["x_ideal"]))
self.cct_x_tolerance_var.set(str(saved_params["x_tolerance"]))
self.cct_y_ideal_var.set(str(saved_params["y_ideal"]))
self.cct_y_tolerance_var.set(str(saved_params["y_tolerance"]))
except Exception as e:
if hasattr(self, "log_gui"):
self.log_gui.log(f"重新加载色度参数失败: {str(e)}")
def 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 色度参数框尚未创建")

View File

@@ -0,0 +1,609 @@
"""自定义模板结果面板Step 6 重构)。"""
import threading
import time
from tkinter import messagebox
import tkinter as tk
import ttkbootstrap as ttk
import colour
import numpy as np
from app.data_range_converter import convert_pattern_params
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()

View File

@@ -0,0 +1,498 @@
"""主布局面板创建函数Step 6 重构)。"""
import tkinter as tk
import ttkbootstrap as ttk
from drivers.UCD323_Enum import UCDEnum
from app.views.collapsing_frame import CollapsingFrame
from app.resources import load_icon
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_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 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()

View File

@@ -0,0 +1,412 @@
"""侧边面板(日志 / Local Dimming / 调试Step 6 重构)。"""
import traceback
import tkinter as tk
import ttkbootstrap as ttk
from app.views.pq_log_gui import PQLogGUI
from app.views.pq_debug_panel import PQDebugPanel
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 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 app.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 app.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 app.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 单步调试面板已打开(独立窗口)")

View File

@@ -2,7 +2,7 @@
import UniTAP import UniTAP
import time import time
import gc import gc
from utils.UCD323_Enum import UCDEnum from drivers.UCD323_Enum import UCDEnum
class UCDController: class UCDController:

0
drivers/__init__.py Normal file
View File

View File

@@ -1,7 +1,7 @@
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
import re import re
import time import time
from utils.baseSerail import BaseSerial from drivers.baseSerail import BaseSerial
# from baseSerail import BaseSerial # from baseSerail import BaseSerial
import colour import colour

View File

@@ -1,9 +1,9 @@
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
import zlib import zlib
from xmlrpc.client import Boolean from xmlrpc.client import Boolean
from utils.baseSerail import BaseSerial from drivers.baseSerail import BaseSerial
import binascii import binascii
import utils.baseSerail as baseSerail import drivers.baseSerail as baseSerail
# 包头码(包引导码) # 包头码(包引导码)
PHeader = { PHeader = {

80
drivers/ucd_helpers.py Normal file
View File

@@ -0,0 +1,80 @@
"""通用 UCD323/UCDController 辅助函数。
封装"按当前接口取 tx 模块""读取分辨率""发送图片 Pattern"等所有
测试模块共用的低层 UCD 操作,避免在多个业务模块中重复 if/else。
"""
import UniTAP
def get_tx_modules(ucd):
"""根据当前接口返回 (pg, ag) 模块。
兼容 UCD323Controller多接口含 current_interface 属性)
与老的 UCDController仅 HDMI
"""
interface = getattr(ucd, "current_interface", None)
if interface in (None, "HDMI"):
return ucd.role.hdtx.pg, ucd.role.hdtx.ag
if interface in ("DP", "Type-C"):
return ucd.role.dptx.pg, ucd.role.dptx.ag
raise ValueError(f"不支持的接口类型: {interface}")
def get_current_resolution(ucd, default=(3840, 2160)):
"""从 UCD 当前 timing 获取 (width, height),失败时返回 default。"""
try:
pg, _ = get_tx_modules(ucd)
vm = pg.get_vm()
timing = getattr(vm, "timing", None)
if timing and hasattr(timing, "h_active") and hasattr(timing, "v_active"):
return timing.h_active, timing.v_active
except Exception:
pass
timing = getattr(ucd, "current_timing", None)
if timing and hasattr(timing, "h_active") and hasattr(timing, "v_active"):
return timing.h_active, timing.v_active
return default
def send_image_pattern(ucd, image_path, *, bpc=8, color_format=None, colorimetry=None):
"""通过 UCD 发送一张本地图片作为显示 Pattern。
自动停止音频以避免蜂鸣声。颜色参数留空时使用 RGB / sRGB 默认值。
返回 True/False。
"""
if not getattr(ucd, "status", False):
return False
try:
pg, ag = get_tx_modules(ucd)
except Exception:
return False
try:
ag.stop_generate()
except Exception:
pass
color_mode = UniTAP.ColorInfo()
color_mode.color_format = color_format or UniTAP.ColorInfo.ColorFormat.CF_RGB
color_mode.bpc = bpc
color_mode.colorimetry = colorimetry or UniTAP.ColorInfo.Colorimetry.CM_sRGB
timing = None
try:
vm = pg.get_vm()
timing = getattr(vm, "timing", None)
except Exception:
pass
try:
if timing:
pg.set_vm(vm=UniTAP.VideoMode(timing=timing, color_info=color_mode))
pg.set_pattern(pattern=image_path)
pg.apply()
return True
except Exception:
return False

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{ {
"current_test_type": "sdr_movie", "current_test_type": "screen_module",
"test_types": { "test_types": {
"screen_module": { "screen_module": {
"name": "屏模组性能测试", "name": "屏模组性能测试",

View File

@@ -1,585 +0,0 @@
"""
Local Dimming 测试模块
功能:
- 生成不同百分比的白色窗口图片
- 通过 UCD 发送图片到显示器
- 自动采集 CA410 亮度数据
- 记录并导出测试结果
"""
import os
import sys
import time
import atexit
import shutil
import numpy as np
from PIL import Image, ImageDraw
import UniTAP
class LocalDimmingController:
"""Local Dimming 控制器 - 用于发送不同百分比窗口 Pattern"""
def __init__(self, ucd_controller):
"""
初始化 Local Dimming 控制器
Args:
ucd_controller: UCD323 控制器实例
"""
self.ucd = ucd_controller
# 兼容打包后的路径
if getattr(sys, "frozen", False):
base_dir = os.path.dirname(sys.executable)
else:
base_dir = os.getcwd()
self.temp_dir = os.path.join(base_dir, "temp_local_dimming")
# 创建临时目录
if not os.path.exists(self.temp_dir):
os.makedirs(self.temp_dir)
print(f"[LD] 创建临时目录: {self.temp_dir}")
self.cached_images = {} # 缓存已生成的图片 {(分辨率, 百分比): 文件路径}
# 注册退出时自动清理
atexit.register(self.cleanup)
print("[LD] Local Dimming 控制器已初始化")
def send_window_pattern_with_resolution(self, percentage, width, height):
"""
发送指定百分比和分辨率的白色窗口 Pattern
Args:
percentage: 窗口面积百分比 (1-100)
width: 图像宽度
height: 图像高度
Returns:
bool: 是否成功
"""
try:
# 检查设备连接状态
if not self.ucd.status:
print("[LD 错误] 设备未连接")
return False
print(f"\n[LD] 开始发送 {percentage}% 窗口 Pattern")
print(f"[LD] 使用分辨率: {width}x{height}")
# 获取 Pattern Generator 和 Audio Generator
# 兼容 UCDController仅 HDMI和 UCD323Controller多接口
if hasattr(self.ucd, 'current_interface'):
# UCD323Controller多接口支持
interface = self.ucd.current_interface
if interface == "HDMI":
pg = self.ucd.role.hdtx.pg
ag = self.ucd.role.hdtx.ag
elif interface == "Type-C" or interface == "DP":
pg = self.ucd.role.dptx.pg
ag = self.ucd.role.dptx.ag
else:
print(f"[LD 错误] 不支持的接口类型: {interface}")
return False
else:
# UCDController仅 HDMI
pg = self.ucd.role.hdtx.pg
ag = self.ucd.role.hdtx.ag
# 先停止音频,避免蜂鸣声
try:
ag.stop_generate()
print("[LD] 已停止音频生成")
except Exception as e:
print(f"[LD 警告] 停止音频失败: {e}")
# 检查缓存
cache_key = (f"{width}x{height}", percentage)
if cache_key in self.cached_images:
image_path = self.cached_images[cache_key]
if os.path.exists(image_path):
print(f"[LD] 使用缓存图片: {image_path}")
else:
print(f"[LD] 缓存图片不存在,重新生成...")
image_path = self._generate_and_save_image(
width, height, percentage, cache_key
)
else:
print(f"[LD] 正在生成 {percentage}% 窗口图像...")
image_path = self._generate_and_save_image(
width, height, percentage, cache_key
)
# 发送图像到设备
print(f"[LD] 正在发送图像到设备...")
# 设置 ColorInfo
color_mode = UniTAP.ColorInfo()
color_mode.color_format = UniTAP.ColorInfo.ColorFormat.CF_RGB
color_mode.bpc = 8
color_mode.colorimetry = UniTAP.ColorInfo.Colorimetry.CM_sRGB
# 获取当前 timing
try:
current_vm = pg.get_vm()
timing = (
current_vm.timing
if current_vm and hasattr(current_vm, "timing")
else None
)
except:
timing = None
# 如果有 timing设置 VideoMode
if timing:
video_mode = UniTAP.VideoMode(timing=timing, color_info=color_mode)
pg.set_vm(vm=video_mode)
# 设置图片 Pattern
pg.set_pattern(pattern=image_path)
# 应用
pg.apply()
print(f"[LD] {percentage}% 窗口 Pattern 已发送到设备")
return True
except Exception as e:
print(f"[LD 异常] 发送 {percentage}% 窗口失败: {e}")
import traceback
traceback.print_exc()
return False
def send_window_pattern(self, percentage):
"""
发送指定百分比的白色窗口 Pattern从 GUI 获取分辨率)
Args:
percentage: 窗口面积百分比 (1-100)
Returns:
bool: 是否成功
"""
# 从设备当前 timing 获取分辨率
width, height = self.get_current_resolution()
return self.send_window_pattern_with_resolution(percentage, width, height)
def get_current_resolution(self):
"""
从设备当前 timing 获取显示器分辨率
Returns:
tuple: (width, height)
"""
try:
# 方式1从 Pattern Generator 的当前 VideoMode 获取
if hasattr(self.ucd, 'current_interface'):
interface = self.ucd.current_interface
if interface == "HDMI":
pg = self.ucd.role.hdtx.pg
elif interface == "Type-C" or interface == "DP":
pg = self.ucd.role.dptx.pg
else:
pg = None
else:
pg = self.ucd.role.hdtx.pg
if pg:
current_vm = pg.get_vm()
if current_vm and hasattr(current_vm, "timing") and current_vm.timing:
timing = current_vm.timing
if hasattr(timing, "h_active") and hasattr(timing, "v_active"):
width = timing.h_active
height = timing.v_active
print(f"[LD] 从当前 timing 获取分辨率: {width}x{height}")
return width, height
# 方式2从 current_timing 属性获取
if hasattr(self.ucd, "current_timing") and self.ucd.current_timing:
timing = self.ucd.current_timing
if hasattr(timing, "h_active") and hasattr(timing, "v_active"):
width = timing.h_active
height = timing.v_active
print(f"[LD] 从 current_timing 获取分辨率: {width}x{height}")
return width, height
except Exception as e:
print(f"[LD 警告] 获取分辨率失败: {e}")
print("[LD 警告] 使用默认分辨率 3840x2160")
return 3840, 2160
def _generate_and_save_image(self, width, height, percentage, cache_key):
"""
生成并保存窗口图像
Args:
width: 图像宽度
height: 图像高度
percentage: 窗口面积百分比
cache_key: 缓存键
Returns:
str: 图像文件路径
"""
# 生成图像
image_array = self._create_window_image(width, height, percentage)
# 保存到项目目录
filename = f"window_{width}x{height}_{percentage:03d}percent.png"
image_path = os.path.join(self.temp_dir, filename)
image = Image.fromarray(image_array, mode="RGB")
image.save(image_path, format="PNG")
# 缓存
self.cached_images[cache_key] = image_path
print(f"[LD] 图像已保存: {image_path}")
return image_path
def _create_window_image(self, width, height, percentage):
"""
创建窗口图像
黑色背景 + 居中白色矩形窗口(保持屏幕比例)
Args:
width: 图像宽度
height: 图像高度
percentage: 窗口面积百分比 (1-100)
Returns:
numpy.ndarray: RGB 图像数组 (height, width, 3)
"""
# 创建黑色背景
image = np.zeros((height, width, 3), dtype=np.uint8)
# 计算窗口尺寸(保持屏幕比例)
scale_factor = (percentage / 100.0) ** 0.5
window_width = int(width * scale_factor)
window_height = int(height * scale_factor)
# 100% 时强制全屏
if percentage == 100:
window_width = width
window_height = height
# 计算居中位置
x1 = (width - window_width) // 2
y1 = (height - window_height) // 2
x2 = x1 + window_width
y2 = y1 + window_height
# 绘制白色窗口
image[y1:y2, x1:x2] = [255, 255, 255]
print(
f"[LD] 图像生成完成: {width}x{height}, 窗口 {window_width}x{window_height}"
)
return image
def cleanup(self):
"""清理临时文件夹"""
if os.path.exists(self.temp_dir):
try:
shutil.rmtree(self.temp_dir)
print(f"[LD] 临时文件夹已删除: {self.temp_dir}")
except Exception as e:
print(f"[LD 警告] 删除临时文件夹失败: {e}")
def __del__(self):
"""析构函数:清理临时文件(备用机制)"""
try:
self.cleanup()
except:
pass
class LocalDimmingTest:
def __init__(self, ucd_controller, ca_serial, log_callback=None):
"""
初始化 Local Dimming 测试
Args:
ucd_controller: UCD323 控制器实例
ca_serial: CA410 串口实例
log_callback: 日志回调函数
"""
self.ucd = ucd_controller
self.ca = ca_serial
self.log = log_callback if log_callback else print
# 临时图片目录
self.temp_dir = self._init_temp_dir()
# 测试结果
self.test_results = []
# 测试配置
self.window_percentages = [1, 2, 5, 10, 18, 25, 50, 75, 100]
self.wait_time = 2.0 # 每次切换后等待时间(秒)
# 停止标志
self.stop_flag = False
self.log("✓ Local Dimming 测试模块已初始化")
def _init_temp_dir(self):
"""初始化临时目录"""
if getattr(sys, "frozen", False):
base_dir = os.path.dirname(sys.executable)
else:
base_dir = os.getcwd()
temp_dir = os.path.join(base_dir, "temp_local_dimming")
os.makedirs(temp_dir, exist_ok=True)
return temp_dir
def generate_window_image(self, width, height, percentage):
"""
生成窗口图片(黑色背景 + 居中白色矩形窗口)
Args:
width: 图像宽度
height: 图像高度
percentage: 窗口面积百分比 (1-100)
Returns:
str: 图片文件路径
"""
# 计算窗口尺寸(保持屏幕比例)
scale_factor = (percentage / 100.0) ** 0.5
window_width = int(width * scale_factor)
window_height = int(height * scale_factor)
# 100% 时强制全屏
if percentage == 100:
window_width = width
window_height = height
# 创建黑色背景
image = np.zeros((height, width, 3), dtype=np.uint8)
# 计算居中位置
x1 = (width - window_width) // 2
y1 = (height - window_height) // 2
x2 = x1 + window_width
y2 = y1 + window_height
# 绘制白色窗口
image[y1:y2, x1:x2] = [255, 255, 255]
# 保存图片
filename = f"window_{width}x{height}_{percentage:03d}percent.png"
image_path = os.path.join(self.temp_dir, filename)
pil_image = Image.fromarray(image, mode="RGB")
pil_image.save(image_path, format="PNG")
self.log(f" ✓ 图片已生成: {window_width}×{window_height} px")
return image_path
def send_image_to_ucd(self, image_path):
"""
通过 UCD 发送图片到显示器
Args:
image_path: 图片文件路径
Returns:
bool: 是否成功
"""
try:
# 获取 Pattern Generator 和 Audio Generator
# 兼容 UCDController仅 HDMI和 UCD323Controller多接口
if hasattr(self.ucd, 'current_interface'):
interface = self.ucd.current_interface
if interface == "HDMI":
pg = self.ucd.role.hdtx.pg
ag = self.ucd.role.hdtx.ag
elif interface == "Type-C" or interface == "DP":
pg = self.ucd.role.dptx.pg
ag = self.ucd.role.dptx.ag
else:
self.log(f" ❌ 不支持的接口类型: {interface}")
return False
else:
# UCDController仅 HDMI
pg = self.ucd.role.hdtx.pg
ag = self.ucd.role.hdtx.ag
# 停止音频
try:
ag.stop_generate()
except:
pass
# 设置 ColorInfo
color_mode = UniTAP.ColorInfo()
color_mode.color_format = UniTAP.ColorInfo.ColorFormat.CF_RGB
color_mode.bpc = 8
color_mode.colorimetry = UniTAP.ColorInfo.Colorimetry.CM_sRGB
# 获取当前 timing
try:
current_vm = pg.get_vm()
timing = (
current_vm.timing
if current_vm and hasattr(current_vm, "timing")
else None
)
except:
timing = None
# 设置 VideoMode
if timing:
video_mode = UniTAP.VideoMode(timing=timing, color_info=color_mode)
pg.set_vm(vm=video_mode)
# 设置图片 Pattern
pg.set_pattern(pattern=image_path)
# 应用
pg.apply()
return True
except Exception as e:
self.log(f" ❌ 发送图片失败: {str(e)}")
import traceback
traceback.print_exc()
return False
def measure_luminance(self):
"""
使用 CA410 采集亮度
Returns:
tuple: (x, y, lv, X, Y, Z) 或 None
"""
try:
if not self.ca:
self.log(" ❌ CA410 未连接")
return None
# 采集数据
x, y, lv, X, Y, Z = self.ca.readAllDisplay()
if x is not None and y is not None and lv is not None:
self.log(f" ✓ 采集亮度: {lv:.2f} cd/m²")
return (x, y, lv, X, Y, Z)
else:
self.log(" ❌ 采集数据失败")
return None
except Exception as e:
self.log(f" ❌ 采集亮度异常: {str(e)}")
return None
def run_test(self, resolution="3840x2160"):
"""
执行完整的 Local Dimming 测试
Args:
resolution: 分辨率字符串,如 "3840x2160"
Returns:
list: 测试结果 [(百分比, x, y, lv, X, Y, Z), ...]
"""
self.log("=" * 60)
self.log("开始 Local Dimming 测试")
self.log("=" * 60)
# 重置停止标志
self.stop_flag = False
# 解析分辨率
try:
width, height = map(int, resolution.split("x"))
except:
width, height = 3840, 2160
self.log(f" ⚠️ 分辨率解析失败,使用默认值: {width}x{height}")
self.log(f" 分辨率: {width}x{height}")
self.log(f" 测试窗口: {self.window_percentages}")
self.log(f" 等待时间: {self.wait_time}")
self.log("")
self.test_results = []
for i, percentage in enumerate(self.window_percentages, start=1):
# 检查停止标志
if self.stop_flag:
self.log("⚠️ 测试已停止")
break
self.log(f"[{i}/{len(self.window_percentages)}] 测试 {percentage}% 窗口...")
# 1. 生成图片
image_path = self.generate_window_image(width, height, percentage)
# 2. 发送到 UCD
if not self.send_image_to_ucd(image_path):
self.log(f"{percentage}% 窗口发送失败,跳过")
continue
# 3. 等待稳定
self.log(f" ⏳ 等待 {self.wait_time} 秒...")
time.sleep(self.wait_time)
# 4. 采集亮度
result = self.measure_luminance()
if result:
x, y, lv, X, Y, Z = result
self.test_results.append((percentage, x, y, lv, X, Y, Z))
self.log(f"{percentage}% 窗口测试完成")
else:
self.log(f"{percentage}% 窗口采集失败")
self.log("")
self.log("=" * 60)
self.log("✅ Local Dimming 测试完成")
self.log(
f" 成功测试: {len(self.test_results)}/{len(self.window_percentages)} 个窗口"
)
self.log("=" * 60)
return self.test_results
def stop(self):
"""停止测试"""
self.stop_flag = True
self.log("⚠️ 正在停止测试...")
def get_results_summary(self):
"""获取测试结果摘要"""
if not self.test_results:
return None
luminances = [lv for _, _, _, lv, _, _, _ in self.test_results]
return {
"data_points": self.test_results,
"max_luminance": max(luminances),
"min_luminance": min(luminances),
"avg_luminance": sum(luminances) / len(luminances),
}
def cleanup(self):
"""清理临时文件"""
try:
import shutil
if os.path.exists(self.temp_dir):
shutil.rmtree(self.temp_dir)
self.log(f"✓ 临时文件已清理: {self.temp_dir}")
except Exception as e:
self.log(f"⚠️ 清理临时文件失败: {e}")