From 2e92b484968a757e5d5ab500aef8c4406427da75 Mon Sep 17 00:00:00 2001 From: "xinzhu.yin" Date: Mon, 20 Apr 2026 11:48:38 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E7=A7=BB=E5=8A=A8utils?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=A4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- {utils => app}/data_range_converter.py | 6 +- app/device/connection.py | 2 +- app/pq/__init__.py | 0 {utils => app}/pq/pq_config.py | 0 {utils => app}/pq/pq_result.py | 0 app/runner/test_runner.py | 4 +- app/tests/local_dimming.py | 287 ++- app/views/chart_frame.py | 2 +- {views => app/views}/collapsing_frame.py | 7 +- app/views/panel_manager.py | 72 + app/views/panels/__init__.py | 0 app/views/panels/cct_panel.py | 901 ++++++++ app/views/panels/custom_template_panel.py | 609 +++++ app/views/panels/main_layout.py | 498 +++++ app/views/panels/side_panels.py | 412 ++++ {views => app/views}/pq_debug_panel.py | 0 {views => app/views}/pq_log_gui.py | 0 {utils => drivers}/UCD323_Enum.py | 0 {utils => drivers}/UCD323_Function.py | 2 +- drivers/__init__.py | 0 {utils => drivers}/baseSerail.py | 0 {utils => drivers}/caSerail.py | 2 +- {utils => drivers}/tvSerail.py | 4 +- drivers/ucd_helpers.py | 80 + pqAutomationApp.py | 2476 +-------------------- settings/pq_config.json | 2 +- utils/local_dimming_test.py | 585 ----- 27 files changed, 2866 insertions(+), 3085 deletions(-) rename {utils => app}/data_range_converter.py (97%) create mode 100644 app/pq/__init__.py rename {utils => app}/pq/pq_config.py (100%) rename {utils => app}/pq/pq_result.py (100%) rename {views => app/views}/collapsing_frame.py (95%) create mode 100644 app/views/panel_manager.py create mode 100644 app/views/panels/__init__.py create mode 100644 app/views/panels/cct_panel.py create mode 100644 app/views/panels/custom_template_panel.py create mode 100644 app/views/panels/main_layout.py create mode 100644 app/views/panels/side_panels.py rename {views => app/views}/pq_debug_panel.py (100%) rename {views => app/views}/pq_log_gui.py (100%) rename {utils => drivers}/UCD323_Enum.py (100%) rename {utils => drivers}/UCD323_Function.py (99%) create mode 100644 drivers/__init__.py rename {utils => drivers}/baseSerail.py (100%) rename {utils => drivers}/caSerail.py (99%) rename {utils => drivers}/tvSerail.py (99%) create mode 100644 drivers/ucd_helpers.py delete mode 100644 utils/local_dimming_test.py diff --git a/utils/data_range_converter.py b/app/data_range_converter.py similarity index 97% rename from utils/data_range_converter.py rename to app/data_range_converter.py index 1bd16d1..787da5c 100644 --- a/utils/data_range_converter.py +++ b/app/data_range_converter.py @@ -4,7 +4,7 @@ 将 Full Range (0-255) 转换为 Limited Range (16-235) 使用方法: - from utils.data_range_converter import DataRangeConverter + from app.data_range_converter import DataRangeConverter converter = DataRangeConverter() converted_params = converter.convert(pattern_params, "Limited") @@ -187,7 +187,7 @@ def convert_pattern_params(pattern_params, data_range="Full", verbose=True): 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]] >>> converted = convert_pattern_params(params, "Limited") [[16,16,16], [235,235,235]] @@ -208,7 +208,7 @@ def convert_single_rgb(r, g, b, data_range="Full"): 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") (16, 16, 16) """ diff --git a/app/device/connection.py b/app/device/connection.py index 8536209..37aa885 100644 --- a/app/device/connection.py +++ b/app/device/connection.py @@ -8,7 +8,7 @@ import threading import time from tkinter import messagebox -from utils.caSerail import CASerail +from drivers.caSerail import CASerail def get_available_ucd_ports(self): """获取可用的UCD端口列表""" diff --git a/app/pq/__init__.py b/app/pq/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/pq/pq_config.py b/app/pq/pq_config.py similarity index 100% rename from utils/pq/pq_config.py rename to app/pq/pq_config.py diff --git a/utils/pq/pq_result.py b/app/pq/pq_result.py similarity index 100% rename from utils/pq/pq_result.py rename to app/pq/pq_result.py diff --git a/app/runner/test_runner.py b/app/runner/test_runner.py index 7548d28..c19a73c 100644 --- a/app/runner/test_runner.py +++ b/app/runner/test_runner.py @@ -14,8 +14,8 @@ import colour import numpy as np import algorithm.pq_algorithm as pq_algorithm -from utils.data_range_converter import convert_pattern_params -from utils.pq.pq_result import PQResult +from app.data_range_converter import convert_pattern_params +from app.pq.pq_result import PQResult def new_pq_results(self, test_type, test_name): self.results = PQResult(test_type, test_name) diff --git a/app/tests/local_dimming.py b/app/tests/local_dimming.py index 1d2622c..f2677b8 100644 --- a/app/tests/local_dimming.py +++ b/app/tests/local_dimming.py @@ -1,127 +1,214 @@ -"""Local Dimming 测试逻辑(Step 4 重构)。 +"""Local Dimming 测试逻辑(应用层)。 -从 pqAutomationApp.PQAutomationApp 中搬迁。每个函数第一行 `self = app` -以保留原有 `self.xxx` 属性访问不变。 +整合自原 drivers/local_dimming_test.py:窗口图片生成与测试主循环 +直接落在本模块,UCD 通用操作下沉到 drivers.ucd_helpers。 """ +import atexit +import csv +import datetime +import os +import shutil +import sys import threading +import time import tkinter as tk 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): - """开始 Local Dimming 测试""" - # 检查设备连接 + """开始 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()) + stop_event = threading.Event() + self.ld_stop_event = stop_event - # 在新线程中执行测试 - def run_test(): - from utils.local_dimming_test import LocalDimmingTest, LocalDimmingController + def worker(): + log = self.log_gui.log + log("=" * 60) + log("开始 Local Dimming 测试") + log("=" * 60) - # 从设备当前 timing 获取分辨率 - ld_ctrl = LocalDimmingController(self.ucd) - cur_w, cur_h = ld_ctrl.get_current_resolution() - resolution = f"{cur_w}x{cur_h}" + width, height = get_current_resolution(self.ucd) + total = len(DEFAULT_WINDOW_PERCENTAGES) + log(f" 分辨率: {width}x{height}") + log(f" 测试窗口: {DEFAULT_WINDOW_PERCENTAGES}") + log(f" 等待时间: {wait_time} 秒") - ld_test = LocalDimmingTest( - self.ucd, - self.ca, - log_callback=self.log_gui.log, - ) + results = [] + for i, percentage in enumerate(DEFAULT_WINDOW_PERCENTAGES, 1): + if stop_event.is_set(): + 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.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() + threading.Thread(target=worker, daemon=True).start() def update_ld_results(self, results): - """更新 Local Dimming 结果显示""" - for percentage, x, y, lv, X, Y, Z in results: + """把批量测试结果填入 Treeview。""" + for percentage, x, y, lv, _X, _Y, _Z in results: self.ld_tree.insert( - "", - tk.END, + "", 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() + """请求停止当前 Local Dimming 测试。""" + ev = getattr(self, "ld_stop_event", None) + if ev: + ev.set() 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 + width, height = get_current_resolution(self.ucd) + try: + image_path = _ensure_window_image(width, height, percentage) + except Exception as e: + self.root.after(0, lambda: self.log_gui.log(f"❌ 图像生成失败: {e}")) + return + ok = send_image_pattern(self.ucd, image_path) + msg = ( + f"✅ {percentage}% 窗口已发送" if ok + 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() def measure_ld_luminance(self): - """测量当前亮度""" + """测量当前显示的亮度并追加一行到 Treeview。""" if not self.ca: messagebox.showwarning("警告", "请先连接 CA410 色度计") return - if self.current_ld_percentage is None: messagebox.showinfo("提示", "请先发送一个窗口图案") return @@ -130,51 +217,31 @@ def measure_ld_luminance(self): 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("❌ 采集失败")) - + x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay() except Exception as 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() def clear_ld_records(self): - """清空测试记录""" + """清空 Treeview 中的测试记录。""" for item in self.ld_tree.get_children(): self.ld_tree.delete(item) 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): - """保存 Local Dimming 结果""" - from tkinter import filedialog - import csv - import datetime - + """把 Treeview 中的全部记录导出为 CSV。""" 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" - + 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 @@ -208,14 +271,10 @@ def save_local_dimming_results(self): 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) - + writer.writerow(self.ld_tree.item(item, "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)}") diff --git a/app/views/chart_frame.py b/app/views/chart_frame.py index 28904a2..d079e0c 100644 --- a/app/views/chart_frame.py +++ b/app/views/chart_frame.py @@ -8,7 +8,7 @@ import tkinter as tk import ttkbootstrap as ttk import matplotlib.pyplot as plt 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): """初始化色域图表 - 手动设置subplot位置,完全避免重叠""" diff --git a/views/collapsing_frame.py b/app/views/collapsing_frame.py similarity index 95% rename from views/collapsing_frame.py rename to app/views/collapsing_frame.py index 87ce11e..0e50c2a 100644 --- a/views/collapsing_frame.py +++ b/app/views/collapsing_frame.py @@ -22,11 +22,12 @@ def get_resource_path(relative_path): base_path = sys._MEIPASS except AttributeError: # 开发环境:使用项目根目录 - # 当前文件: views/collapsing_frame.py - # 项目根目录: views 的父目录 + # 当前文件: app/views/collapsing_frame.py + # 项目根目录: app/views 的祖父目录 current_file = os.path.abspath(__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) diff --git a/app/views/panel_manager.py b/app/views/panel_manager.py new file mode 100644 index 0000000..abada51 --- /dev/null +++ b/app/views/panel_manager.py @@ -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 + + diff --git a/app/views/panels/__init__.py b/app/views/panels/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/views/panels/cct_panel.py b/app/views/panels/cct_panel.py new file mode 100644 index 0000000..bd66943 --- /dev/null +++ b/app/views/panels/cct_panel.py @@ -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( + "", + 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( + "<>", 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( + "", + 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("<>", 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( + "", + 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("<>", 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 色度参数框尚未创建") + + diff --git a/app/views/panels/custom_template_panel.py b/app/views/panels/custom_template_panel.py new file mode 100644 index 0000000..c67a1de --- /dev/null +++ b/app/views/panels/custom_template_panel.py @@ -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("", 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() + + diff --git a/app/views/panels/main_layout.py b/app/views/panels/main_layout.py new file mode 100644 index 0000000..f5e3be8 --- /dev/null +++ b/app/views/panels/main_layout.py @@ -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( + "<>", 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("<>", 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("<>", 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("<>", 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() + + diff --git a/app/views/panels/side_panels.py b/app/views/panels/side_panels.py new file mode 100644 index 0000000..673a68f --- /dev/null +++ b/app/views/panels/side_panels.py @@ -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 单步调试面板已打开(独立窗口)") + + diff --git a/views/pq_debug_panel.py b/app/views/pq_debug_panel.py similarity index 100% rename from views/pq_debug_panel.py rename to app/views/pq_debug_panel.py diff --git a/views/pq_log_gui.py b/app/views/pq_log_gui.py similarity index 100% rename from views/pq_log_gui.py rename to app/views/pq_log_gui.py diff --git a/utils/UCD323_Enum.py b/drivers/UCD323_Enum.py similarity index 100% rename from utils/UCD323_Enum.py rename to drivers/UCD323_Enum.py diff --git a/utils/UCD323_Function.py b/drivers/UCD323_Function.py similarity index 99% rename from utils/UCD323_Function.py rename to drivers/UCD323_Function.py index 50026ef..19f6282 100644 --- a/utils/UCD323_Function.py +++ b/drivers/UCD323_Function.py @@ -2,7 +2,7 @@ import UniTAP import time import gc -from utils.UCD323_Enum import UCDEnum +from drivers.UCD323_Enum import UCDEnum class UCDController: diff --git a/drivers/__init__.py b/drivers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/baseSerail.py b/drivers/baseSerail.py similarity index 100% rename from utils/baseSerail.py rename to drivers/baseSerail.py diff --git a/utils/caSerail.py b/drivers/caSerail.py similarity index 99% rename from utils/caSerail.py rename to drivers/caSerail.py index 630a22a..1f4214f 100644 --- a/utils/caSerail.py +++ b/drivers/caSerail.py @@ -1,7 +1,7 @@ # -*- coding: UTF-8 -*- import re import time -from utils.baseSerail import BaseSerial +from drivers.baseSerail import BaseSerial # from baseSerail import BaseSerial import colour diff --git a/utils/tvSerail.py b/drivers/tvSerail.py similarity index 99% rename from utils/tvSerail.py rename to drivers/tvSerail.py index 2adf618..2ccf0e2 100644 --- a/utils/tvSerail.py +++ b/drivers/tvSerail.py @@ -1,9 +1,9 @@ # -*- coding: UTF-8 -*- import zlib from xmlrpc.client import Boolean -from utils.baseSerail import BaseSerial +from drivers.baseSerail import BaseSerial import binascii -import utils.baseSerail as baseSerail +import drivers.baseSerail as baseSerail # 包头码(包引导码) PHeader = { diff --git a/drivers/ucd_helpers.py b/drivers/ucd_helpers.py new file mode 100644 index 0000000..13609a1 --- /dev/null +++ b/drivers/ucd_helpers.py @@ -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 diff --git a/pqAutomationApp.py b/pqAutomationApp.py index d67c68f..74151b3 100644 --- a/pqAutomationApp.py +++ b/pqAutomationApp.py @@ -15,22 +15,22 @@ 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 drivers.caSerail import CASerail +from drivers.tvSerail import tvSerial +from drivers.UCD323_Function import UCDController +from drivers.UCD323_Enum import UCDEnum +from app.pq.pq_config import PQConfig +from app.pq.pq_result import PQResult +from app.data_range_converter import convert_pattern_params from PIL import Image, ImageTk -from views.collapsing_frame import CollapsingFrame +from app.views.collapsing_frame import CollapsingFrame -# from views.pq_history_gui import PQHistoryGUI -from views.pq_log_gui import PQLogGUI +# from app.views.pq_history_gui import PQHistoryGUI +from app.views.pq_log_gui import PQLogGUI from colormath.color_objects import xyYColor, LabColor from colormath.color_conversions import convert_color from colormath.color_diff import delta_e_cie2000 -from views.pq_debug_panel import PQDebugPanel +from app.views.pq_debug_panel import PQDebugPanel # Step 0/1 重构:资源工具和纯算法已迁移到 app/ 包,这里重新导入以保持 # 对原函数名/方法名的向后兼容(老代码内部仍用 self.calculate_* 调用)。 @@ -110,6 +110,58 @@ from app.runner.test_runner import ( test_gamma as _run_test_gamma, test_gamut as _run_test_gamut, ) +from app.views.panel_manager import ( + hide_all_panels as _pm_hide_all_panels, + register_panel as _pm_register_panel, + show_panel as _pm_show_panel, +) +from app.views.panels.side_panels import ( + create_local_dimming_panel as _side_create_local_dimming_panel, + create_log_panel as _side_create_log_panel, + toggle_hdr_debug_panel as _side_toggle_hdr_debug_panel, + toggle_local_dimming_panel as _side_toggle_local_dimming_panel, + toggle_log_panel as _side_toggle_log_panel, + toggle_screen_debug_panel as _side_toggle_screen_debug_panel, + toggle_sdr_debug_panel as _side_toggle_sdr_debug_panel, +) +from app.views.panels.custom_template_panel import ( + _clear_custom_result_row as _ctpl__clear_custom_result_row, + _run_custom_row_single_step as _ctpl__run_custom_row_single_step, + _update_custom_result_row as _ctpl__update_custom_result_row, + append_custom_template_result as _ctpl_append_custom_template_result, + auto_expand_custom_result_view as _ctpl_auto_expand_custom_result_view, + clear_custom_template_results as _ctpl_clear_custom_template_results, + copy_custom_result_table as _ctpl_copy_custom_result_table, + create_custom_template_result_panel as _ctpl_create_custom_template_result_panel, + fill_custom_result_test_data as _ctpl_fill_custom_result_test_data, + set_custom_result_table_locked as _ctpl_set_custom_result_table_locked, + show_custom_result_context_menu as _ctpl_show_custom_result_context_menu, + start_custom_row_single_step as _ctpl_start_custom_row_single_step, + start_custom_template_test as _ctpl_start_custom_template_test, +) +from app.views.panels.cct_panel import ( + create_cct_params_frame as _cct_create_cct_params_frame, + on_cct_param_change as _cct_on_cct_param_change, + on_cct_param_focus_out as _cct_on_cct_param_focus_out, + on_hdr_cct_param_focus_out as _cct_on_hdr_cct_param_focus_out, + on_sdr_cct_param_focus_out as _cct_on_sdr_cct_param_focus_out, + recalculate_cct as _cct_recalculate_cct, + recalculate_gamut as _cct_recalculate_gamut, + reload_cct_params as _cct_reload_cct_params, + save_cct_params as _cct_save_cct_params, + save_hdr_cct_params as _cct_save_hdr_cct_params, + save_sdr_cct_params as _cct_save_sdr_cct_params, + toggle_cct_params_frame as _cct_toggle_cct_params_frame, +) +from app.views.panels.main_layout import ( + create_connection_content as _layout_create_connection_content, + create_floating_config_panel as _layout_create_floating_config_panel, + create_operation_frame as _layout_create_operation_frame, + create_signal_format_content as _layout_create_signal_format_content, + create_test_items_content as _layout_create_test_items_content, + create_test_type_frame as _layout_create_test_type_frame, + update_config_info_display as _layout_update_config_info_display, +) plt.rcParams["font.family"] = ["sans-serif"] plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"] @@ -251,931 +303,31 @@ class PQAutomationApp: init_contrast_chart = _cf_init_contrast_chart init_accuracy_chart = _cf_init_accuracy_chart clear_chart = _cf_clear_chart - 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="配置项") + create_floating_config_panel = _layout_create_floating_config_panel - # 创建一个统一的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) + create_test_items_content = _layout_create_test_items_content - # 创建一个横向排列的Frame - config_row_frame = ttk.Frame(self.config_content_frame) - config_row_frame.pack(fill=tk.X, expand=False, padx=5, pady=5) + create_cct_params_frame = _cct_create_cct_params_frame - # 创建连接内容区域 - 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 - ) + on_sdr_cct_param_focus_out = _cct_on_sdr_cct_param_focus_out - # 创建测试项目区域 - 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 - ) + save_sdr_cct_params = _cct_save_sdr_cct_params - # 创建信号格式区域 - 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 - ) + on_hdr_cct_param_focus_out = _cct_on_hdr_cct_param_focus_out - # 创建连接内容 - self.create_connection_content() - # 创建测试项目内容 - self.create_test_items_content() - # 创建信号格式内容 - self.create_signal_format_content() + save_hdr_cct_params = _cct_save_hdr_cct_params - self.config_panel_frame.grid_remove() - self.config_panel_frame.btn.configure(image="closed") + recalculate_cct = _cct_recalculate_cct - 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"), - ], - }, - } + recalculate_gamut = _cct_recalculate_gamut - # 根据当前测试类型创建复选框 - self.test_vars = {} - self.update_test_items() + on_cct_param_change = _cct_on_cct_param_change - # 创建色度参数设置框架 - self.create_cct_params_frame() + on_cct_param_focus_out = _cct_on_cct_param_focus_out - def create_cct_params_frame(self): - """创建色度参数设置区域 - 屏模组、SDR、HDR 独立(✅ 增加色域参考标准选择 + 单步调试按钮)""" + save_cct_params = _cct_save_cct_params - # ==================== 屏模组色度参数 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( - "", - 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( - "<>", 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( - "", - 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("<>", 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( - "", - 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("<>", 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)}") + reload_cct_params = _cct_reload_cct_params def update_test_items(self): """根据当前测试类型更新测试项目复选框""" @@ -1244,281 +396,9 @@ class PQAutomationApp: } 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) + create_signal_format_content = _layout_create_signal_format_content - # ==================== 屏模组格式设置 ==================== - 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( - "<>", 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("<>", 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("<>", 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("<>", self.update_config) + create_connection_content = _layout_create_connection_content get_available_ucd_ports = _dev_get_available_ucd_ports get_available_com_ports = _dev_get_available_com_ports @@ -1528,942 +408,52 @@ class PQAutomationApp: check_port_connection = _dev_check_port_connection enable_com_widgets = _dev_enable_com_widgets disconnect_com_connections = _dev_disconnect_com_connections - def create_test_type_frame(self): - """创建测试类型选择区域(侧边栏形式)""" - # 设置测试类型变量 - self.test_type_var = tk.StringVar(value="screen_module") + create_test_type_frame = _layout_create_test_type_frame - # 创建测试类型按钮并放置在侧边栏 - test_types = [ - ("屏模组性能测试", "screen_module"), - ("SDR Movie测试", "sdr_movie"), - ("HDR Movie测试", "hdr_movie"), - ] + update_config_info_display = _layout_update_config_info_display - 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) + create_operation_frame = _layout_create_operation_frame - # 保存按钮引用以便后续更新样式 - setattr(self, f"{type_value}_btn", btn) + create_custom_template_result_panel = _ctpl_create_custom_template_result_panel - # 添加分隔线 - ttk.Separator(self.sidebar_frame, orient="horizontal").pack( - fill=tk.X, padx=10, pady=10 - ) + show_custom_result_context_menu = _ctpl_show_custom_result_context_menu - # ✅ 只保留日志按钮 - 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) + set_custom_result_table_locked = _ctpl_set_custom_result_table_locked - # 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) + start_custom_row_single_step = _ctpl_start_custom_row_single_step - # 注册面板按钮(只保留日志) - 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 + _clear_custom_result_row = _ctpl__clear_custom_result_row - def update_config_info_display(self): - """更新配置信息显示""" - if hasattr(self, "config") and hasattr(self.config, "get_current_config"): - current_config = self.config.get_current_config() + _run_custom_row_single_step = _ctpl__run_custom_row_single_step - 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')}" + _update_custom_result_row = _ctpl__update_custom_result_row - # 高亮当前选中的测试类型 - self.update_sidebar_selection() + copy_custom_result_table = _ctpl_copy_custom_result_table - def create_operation_frame(self): - """创建操作按钮区域""" - operation_frame = ttk.Frame(self.control_frame_top) - operation_frame.pack(fill=tk.X, padx=5, pady=10) + fill_custom_result_test_data = _ctpl_fill_custom_result_test_data - self.start_btn = ttk.Button( - operation_frame, - text="开始测试", - command=self.start_test, - style="success.TButton", - ) - self.start_btn.pack(side=tk.LEFT, padx=5) + clear_custom_template_results = _ctpl_clear_custom_template_results - 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) + auto_expand_custom_result_view = _ctpl_auto_expand_custom_result_view - self.save_btn = ttk.Button( - operation_frame, - text="保存结果", - command=self.save_results, - state=tk.DISABLED, - ) - self.save_btn.pack(side=tk.LEFT, padx=5) + append_custom_template_result = _ctpl_append_custom_template_result - self.clear_config_btn = ttk.Button( - operation_frame, - text="清理配置", - command=self.clear_config_file, - ) - self.clear_config_btn.pack(side=tk.LEFT, padx=5) + start_custom_template_test = _ctpl_start_custom_template_test - 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 - ) + register_panel = _pm_register_panel - 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) + show_panel = _pm_show_panel - 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")], - ) + hide_all_panels = _pm_hide_all_panels - columns = ( - "Pattern", - "No.", - "X", - "Y", - "Z", - "x", - "y", - "Lv", - "u'", - "v'", - "Tcp", - "duv", - "λd/λc", - "Pe" - ) + create_log_panel = _side_create_log_panel - self.custom_result_tree = ttk.Treeview( - table_container, - columns=columns, - show="headings", - height=4, - style="CustomResult.Treeview", - ) + create_local_dimming_panel = _side_create_local_dimming_panel - 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, - } + toggle_local_dimming_panel = _side_toggle_local_dimming_panel - 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("", 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") + toggle_log_panel = _side_toggle_log_panel create_result_chart_frame = _cf_create_result_chart_frame on_chart_tab_changed = _cf_on_chart_tab_changed @@ -4823,44 +2813,7 @@ class PQAutomationApp: # 控制参数框的显示 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 色度参数框尚未创建") + toggle_cct_params_frame = _cct_toggle_cct_params_frame def on_screen_module_timing_changed(self, event=None): """屏模组信号格式改变时的回调""" @@ -4975,230 +2928,11 @@ class PQAutomationApp: 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 + toggle_screen_debug_panel = _side_toggle_screen_debug_panel - # 创建新窗口 - self.debug_window = ttk.Toplevel(self.root) - self.debug_window.title("🔧 单步调试面板") - self.debug_window.geometry("900x400") - self.debug_window.transient(self.root) + toggle_sdr_debug_panel = _side_toggle_sdr_debug_panel - # 创建调试面板容器 - 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 单步调试面板已打开(独立窗口)") + toggle_hdr_debug_panel = _side_toggle_hdr_debug_panel clear_config_file = _cfg_clear_config_file start_local_dimming_test = _ld_start_local_dimming_test diff --git a/settings/pq_config.json b/settings/pq_config.json index 02b8611..d7b733a 100644 --- a/settings/pq_config.json +++ b/settings/pq_config.json @@ -1,5 +1,5 @@ { - "current_test_type": "sdr_movie", + "current_test_type": "screen_module", "test_types": { "screen_module": { "name": "屏模组性能测试", diff --git a/utils/local_dimming_test.py b/utils/local_dimming_test.py deleted file mode 100644 index 9b59a38..0000000 --- a/utils/local_dimming_test.py +++ /dev/null @@ -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}")