重构移动utils文件夹
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
将 Full Range (0-255) 转换为 Limited Range (16-235)
|
将 Full Range (0-255) 转换为 Limited Range (16-235)
|
||||||
|
|
||||||
使用方法:
|
使用方法:
|
||||||
from utils.data_range_converter import DataRangeConverter
|
from app.data_range_converter import DataRangeConverter
|
||||||
|
|
||||||
converter = DataRangeConverter()
|
converter = DataRangeConverter()
|
||||||
converted_params = converter.convert(pattern_params, "Limited")
|
converted_params = converter.convert(pattern_params, "Limited")
|
||||||
@@ -187,7 +187,7 @@ def convert_pattern_params(pattern_params, data_range="Full", verbose=True):
|
|||||||
list: 转换后的图案参数列表
|
list: 转换后的图案参数列表
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
>>> from utils.data_range_converter import convert_pattern_params
|
>>> from app.data_range_converter import convert_pattern_params
|
||||||
>>> params = [[0,0,0], [255,255,255]]
|
>>> params = [[0,0,0], [255,255,255]]
|
||||||
>>> converted = convert_pattern_params(params, "Limited")
|
>>> converted = convert_pattern_params(params, "Limited")
|
||||||
[[16,16,16], [235,235,235]]
|
[[16,16,16], [235,235,235]]
|
||||||
@@ -208,7 +208,7 @@ def convert_single_rgb(r, g, b, data_range="Full"):
|
|||||||
tuple: 转换后的 RGB 值
|
tuple: 转换后的 RGB 值
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
>>> from utils.data_range_converter import convert_single_rgb
|
>>> from app.data_range_converter import convert_single_rgb
|
||||||
>>> r, g, b = convert_single_rgb(0, 0, 0, "Limited")
|
>>> r, g, b = convert_single_rgb(0, 0, 0, "Limited")
|
||||||
(16, 16, 16)
|
(16, 16, 16)
|
||||||
"""
|
"""
|
||||||
@@ -8,7 +8,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from tkinter import messagebox
|
from tkinter import messagebox
|
||||||
|
|
||||||
from utils.caSerail import CASerail
|
from drivers.caSerail import CASerail
|
||||||
|
|
||||||
def get_available_ucd_ports(self):
|
def get_available_ucd_ports(self):
|
||||||
"""获取可用的UCD端口列表"""
|
"""获取可用的UCD端口列表"""
|
||||||
|
|||||||
0
app/pq/__init__.py
Normal file
0
app/pq/__init__.py
Normal file
@@ -14,8 +14,8 @@ import colour
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
import algorithm.pq_algorithm as pq_algorithm
|
import algorithm.pq_algorithm as pq_algorithm
|
||||||
from utils.data_range_converter import convert_pattern_params
|
from app.data_range_converter import convert_pattern_params
|
||||||
from utils.pq.pq_result import PQResult
|
from app.pq.pq_result import PQResult
|
||||||
|
|
||||||
def new_pq_results(self, test_type, test_name):
|
def new_pq_results(self, test_type, test_name):
|
||||||
self.results = PQResult(test_type, test_name)
|
self.results = PQResult(test_type, test_name)
|
||||||
|
|||||||
@@ -1,127 +1,214 @@
|
|||||||
"""Local Dimming 测试逻辑(Step 4 重构)。
|
"""Local Dimming 测试逻辑(应用层)。
|
||||||
|
|
||||||
从 pqAutomationApp.PQAutomationApp 中搬迁。每个函数第一行 `self = app`
|
整合自原 drivers/local_dimming_test.py:窗口图片生成与测试主循环
|
||||||
以保留原有 `self.xxx` 属性访问不变。
|
直接落在本模块,UCD 通用操作下沉到 drivers.ucd_helpers。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import atexit
|
||||||
|
import csv
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import filedialog, messagebox
|
from tkinter import filedialog, messagebox
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from drivers.ucd_helpers import get_current_resolution, send_image_pattern
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# 模块级常量与窗口图片缓存
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
DEFAULT_WINDOW_PERCENTAGES = [1, 2, 5, 10, 18, 25, 50, 75, 100]
|
||||||
|
|
||||||
|
_TEMP_DIR = None
|
||||||
|
_IMAGE_CACHE = {} # {(width, height, percentage): file_path}
|
||||||
|
|
||||||
|
|
||||||
|
def _cleanup_temp_dir():
|
||||||
|
global _TEMP_DIR
|
||||||
|
if _TEMP_DIR and os.path.exists(_TEMP_DIR):
|
||||||
|
try:
|
||||||
|
shutil.rmtree(_TEMP_DIR)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
_TEMP_DIR = None
|
||||||
|
_IMAGE_CACHE.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_temp_dir():
|
||||||
|
global _TEMP_DIR
|
||||||
|
if _TEMP_DIR is None:
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
base = os.path.dirname(sys.executable)
|
||||||
|
else:
|
||||||
|
base = os.getcwd()
|
||||||
|
_TEMP_DIR = os.path.join(base, "temp_local_dimming")
|
||||||
|
os.makedirs(_TEMP_DIR, exist_ok=True)
|
||||||
|
atexit.register(_cleanup_temp_dir)
|
||||||
|
return _TEMP_DIR
|
||||||
|
|
||||||
|
|
||||||
|
def _make_window_image_array(width, height, percentage):
|
||||||
|
"""生成黑底+居中白窗的 numpy 图像,保持屏幕比例。"""
|
||||||
|
image = np.zeros((height, width, 3), dtype=np.uint8)
|
||||||
|
if percentage >= 100:
|
||||||
|
ww, wh = width, height
|
||||||
|
else:
|
||||||
|
scale = (percentage / 100.0) ** 0.5
|
||||||
|
ww = int(width * scale)
|
||||||
|
wh = int(height * scale)
|
||||||
|
x1 = (width - ww) // 2
|
||||||
|
y1 = (height - wh) // 2
|
||||||
|
image[y1:y1 + wh, x1:x1 + ww] = 255
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_window_image(width, height, percentage):
|
||||||
|
"""生成或复用缓存的窗口 PNG 文件,返回路径。"""
|
||||||
|
key = (width, height, percentage)
|
||||||
|
cached = _IMAGE_CACHE.get(key)
|
||||||
|
if cached and os.path.exists(cached):
|
||||||
|
return cached
|
||||||
|
arr = _make_window_image_array(width, height, percentage)
|
||||||
|
fname = f"window_{width}x{height}_{percentage:03d}percent.png"
|
||||||
|
path = os.path.join(_get_temp_dir(), fname)
|
||||||
|
Image.fromarray(arr, mode="RGB").save(path, format="PNG")
|
||||||
|
_IMAGE_CACHE[key] = path
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# GUI 入口(绑定为 PQAutomationApp 方法)
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
def start_local_dimming_test(self):
|
def start_local_dimming_test(self):
|
||||||
"""开始 Local Dimming 测试"""
|
"""开始 Local Dimming 测试。"""
|
||||||
# 检查设备连接
|
|
||||||
if not self.ca or not self.ucd.status:
|
if not self.ca or not self.ucd.status:
|
||||||
messagebox.showerror("错误", "请先连接 CA410 和 UCD323")
|
messagebox.showerror("错误", "请先连接 CA410 和 UCD323")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 禁用按钮
|
|
||||||
self.ld_start_btn.config(state=tk.DISABLED)
|
self.ld_start_btn.config(state=tk.DISABLED)
|
||||||
self.ld_stop_btn.config(state=tk.NORMAL)
|
self.ld_stop_btn.config(state=tk.NORMAL)
|
||||||
self.ld_save_btn.config(state=tk.DISABLED)
|
self.ld_save_btn.config(state=tk.DISABLED)
|
||||||
|
|
||||||
# 清空结果
|
|
||||||
for item in self.ld_tree.get_children():
|
for item in self.ld_tree.get_children():
|
||||||
self.ld_tree.delete(item)
|
self.ld_tree.delete(item)
|
||||||
|
|
||||||
# 获取配置
|
|
||||||
wait_time = float(self.ld_wait_time_var.get())
|
wait_time = float(self.ld_wait_time_var.get())
|
||||||
|
stop_event = threading.Event()
|
||||||
|
self.ld_stop_event = stop_event
|
||||||
|
|
||||||
# 在新线程中执行测试
|
def worker():
|
||||||
def run_test():
|
log = self.log_gui.log
|
||||||
from utils.local_dimming_test import LocalDimmingTest, LocalDimmingController
|
log("=" * 60)
|
||||||
|
log("开始 Local Dimming 测试")
|
||||||
|
log("=" * 60)
|
||||||
|
|
||||||
# 从设备当前 timing 获取分辨率
|
width, height = get_current_resolution(self.ucd)
|
||||||
ld_ctrl = LocalDimmingController(self.ucd)
|
total = len(DEFAULT_WINDOW_PERCENTAGES)
|
||||||
cur_w, cur_h = ld_ctrl.get_current_resolution()
|
log(f" 分辨率: {width}x{height}")
|
||||||
resolution = f"{cur_w}x{cur_h}"
|
log(f" 测试窗口: {DEFAULT_WINDOW_PERCENTAGES}")
|
||||||
|
log(f" 等待时间: {wait_time} 秒")
|
||||||
|
|
||||||
ld_test = LocalDimmingTest(
|
results = []
|
||||||
self.ucd,
|
for i, percentage in enumerate(DEFAULT_WINDOW_PERCENTAGES, 1):
|
||||||
self.ca,
|
if stop_event.is_set():
|
||||||
log_callback=self.log_gui.log,
|
log("⚠️ 测试已停止")
|
||||||
)
|
break
|
||||||
|
|
||||||
ld_test.wait_time = wait_time
|
log(f"[{i}/{total}] 测试 {percentage}% 窗口...")
|
||||||
|
try:
|
||||||
|
image_path = _ensure_window_image(width, height, percentage)
|
||||||
|
except Exception as e:
|
||||||
|
log(f" ❌ 图像生成失败: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
results = ld_test.run_test(resolution=resolution)
|
if not send_image_pattern(self.ucd, image_path):
|
||||||
|
log(f" ❌ {percentage}% 窗口发送失败,跳过")
|
||||||
|
continue
|
||||||
|
|
||||||
|
log(f" ⏳ 等待 {wait_time} 秒...")
|
||||||
|
time.sleep(wait_time)
|
||||||
|
|
||||||
|
try:
|
||||||
|
x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay()
|
||||||
|
except Exception as e:
|
||||||
|
log(f" ❌ 采集亮度异常: {e}")
|
||||||
|
continue
|
||||||
|
if lv is None:
|
||||||
|
log(f" ❌ {percentage}% 窗口采集失败")
|
||||||
|
continue
|
||||||
|
|
||||||
|
log(f" ✓ 采集亮度: {lv:.2f} cd/m²")
|
||||||
|
results.append((percentage, x, y, lv, _X, _Y, _Z))
|
||||||
|
|
||||||
|
log("=" * 60)
|
||||||
|
log(f"✅ Local Dimming 测试完成 ({len(results)}/{total})")
|
||||||
|
log("=" * 60)
|
||||||
|
|
||||||
# 保存到实例变量
|
|
||||||
self.ld_test_instance = ld_test
|
|
||||||
self.ld_test_results = results
|
self.ld_test_results = results
|
||||||
|
|
||||||
# 更新结果显示
|
|
||||||
self.root.after(0, lambda: self.update_ld_results(results))
|
self.root.after(0, lambda: self.update_ld_results(results))
|
||||||
|
|
||||||
# 清理临时文件
|
|
||||||
ld_test.cleanup()
|
|
||||||
|
|
||||||
# 恢复按钮状态
|
|
||||||
self.root.after(0, lambda: self.ld_start_btn.config(state=tk.NORMAL))
|
self.root.after(0, lambda: self.ld_start_btn.config(state=tk.NORMAL))
|
||||||
self.root.after(0, lambda: self.ld_stop_btn.config(state=tk.DISABLED))
|
self.root.after(0, lambda: self.ld_stop_btn.config(state=tk.DISABLED))
|
||||||
self.root.after(0, lambda: self.ld_save_btn.config(state=tk.NORMAL))
|
self.root.after(0, lambda: self.ld_save_btn.config(state=tk.NORMAL))
|
||||||
|
|
||||||
threading.Thread(target=run_test, daemon=True).start()
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
def update_ld_results(self, results):
|
def update_ld_results(self, results):
|
||||||
"""更新 Local Dimming 结果显示"""
|
"""把批量测试结果填入 Treeview。"""
|
||||||
for percentage, x, y, lv, X, Y, Z in results:
|
for percentage, x, y, lv, _X, _Y, _Z in results:
|
||||||
self.ld_tree.insert(
|
self.ld_tree.insert(
|
||||||
"",
|
"", tk.END,
|
||||||
tk.END,
|
|
||||||
values=(f"{percentage}%", f"{lv:.2f}", f"{x:.4f}", f"{y:.4f}"),
|
values=(f"{percentage}%", f"{lv:.2f}", f"{x:.4f}", f"{y:.4f}"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def stop_local_dimming_test(self):
|
def stop_local_dimming_test(self):
|
||||||
"""停止 Local Dimming 测试"""
|
"""请求停止当前 Local Dimming 测试。"""
|
||||||
if hasattr(self, "ld_test_instance"):
|
ev = getattr(self, "ld_stop_event", None)
|
||||||
self.ld_test_instance.stop()
|
if ev:
|
||||||
|
ev.set()
|
||||||
|
|
||||||
|
|
||||||
def send_ld_window(self, percentage):
|
def send_ld_window(self, percentage):
|
||||||
"""发送指定百分比的窗口"""
|
"""发送指定百分比的白色窗口(手动模式)。"""
|
||||||
if not self.ucd.status:
|
if not self.ucd.status:
|
||||||
messagebox.showwarning("警告", "请先连接 UCD323 设备")
|
messagebox.showwarning("警告", "请先连接 UCD323 设备")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.log_gui.log(f"🔆 发送 {percentage}% 窗口...")
|
self.log_gui.log(f"🔆 发送 {percentage}% 窗口...")
|
||||||
|
|
||||||
# 记录当前百分比(用于测量)
|
|
||||||
self.current_ld_percentage = percentage
|
self.current_ld_percentage = percentage
|
||||||
|
|
||||||
def send():
|
def send():
|
||||||
from utils.local_dimming_test import LocalDimmingController
|
width, height = get_current_resolution(self.ucd)
|
||||||
|
try:
|
||||||
ld_controller = LocalDimmingController(self.ucd)
|
image_path = _ensure_window_image(width, height, percentage)
|
||||||
|
except Exception as e:
|
||||||
# 从设备当前 timing 获取分辨率
|
self.root.after(0, lambda: self.log_gui.log(f"❌ 图像生成失败: {e}"))
|
||||||
width, height = ld_controller.get_current_resolution()
|
return
|
||||||
|
ok = send_image_pattern(self.ucd, image_path)
|
||||||
# 生成并发送图片
|
msg = (
|
||||||
success = ld_controller.send_window_pattern_with_resolution(
|
f"✅ {percentage}% 窗口已发送" if ok
|
||||||
percentage, width, height
|
else f"❌ {percentage}% 窗口发送失败"
|
||||||
)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
self.root.after(
|
|
||||||
0, lambda: self.log_gui.log(f"✅ {percentage}% 窗口已发送")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.root.after(
|
|
||||||
0, lambda: self.log_gui.log(f"❌ {percentage}% 窗口发送失败")
|
|
||||||
)
|
)
|
||||||
|
self.root.after(0, lambda: self.log_gui.log(msg))
|
||||||
|
|
||||||
threading.Thread(target=send, daemon=True).start()
|
threading.Thread(target=send, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
def measure_ld_luminance(self):
|
def measure_ld_luminance(self):
|
||||||
"""测量当前亮度"""
|
"""测量当前显示的亮度并追加一行到 Treeview。"""
|
||||||
if not self.ca:
|
if not self.ca:
|
||||||
messagebox.showwarning("警告", "请先连接 CA410 色度计")
|
messagebox.showwarning("警告", "请先连接 CA410 色度计")
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.current_ld_percentage is None:
|
if self.current_ld_percentage is None:
|
||||||
messagebox.showinfo("提示", "请先发送一个窗口图案")
|
messagebox.showinfo("提示", "请先发送一个窗口图案")
|
||||||
return
|
return
|
||||||
@@ -130,51 +217,31 @@ def measure_ld_luminance(self):
|
|||||||
|
|
||||||
def measure():
|
def measure():
|
||||||
try:
|
try:
|
||||||
x, y, lv, X, Y, Z = self.ca.readAllDisplay()
|
x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay()
|
||||||
|
|
||||||
if lv is not None:
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
|
||||||
|
|
||||||
# 更新显示
|
|
||||||
self.root.after(
|
|
||||||
0,
|
|
||||||
lambda: self.ld_result_label.config(
|
|
||||||
text=f"亮度: {lv:.2f} cd/m² | x: {x:.4f} | y: {y:.4f}"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# 添加到表格
|
|
||||||
self.root.after(
|
|
||||||
0,
|
|
||||||
lambda: self.ld_tree.insert(
|
|
||||||
"",
|
|
||||||
tk.END,
|
|
||||||
values=(
|
|
||||||
f"{self.current_ld_percentage}%",
|
|
||||||
f"{lv:.2f}",
|
|
||||||
f"{x:.4f}",
|
|
||||||
f"{y:.4f}",
|
|
||||||
timestamp,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.root.after(
|
|
||||||
0, lambda: self.log_gui.log(f"✅ 采集完成: {lv:.2f} cd/m²")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.root.after(0, lambda: self.log_gui.log("❌ 采集失败"))
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.root.after(0, lambda: self.log_gui.log(f"❌ 采集异常: {str(e)}"))
|
self.root.after(0, lambda: self.log_gui.log(f"❌ 采集异常: {str(e)}"))
|
||||||
|
return
|
||||||
|
if lv is None:
|
||||||
|
self.root.after(0, lambda: self.log_gui.log("❌ 采集失败"))
|
||||||
|
return
|
||||||
|
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
||||||
|
self.root.after(0, lambda: self.ld_result_label.config(
|
||||||
|
text=f"亮度: {lv:.2f} cd/m² | x: {x:.4f} | y: {y:.4f}"
|
||||||
|
))
|
||||||
|
self.root.after(0, lambda: self.ld_tree.insert(
|
||||||
|
"", tk.END,
|
||||||
|
values=(
|
||||||
|
f"{self.current_ld_percentage}%",
|
||||||
|
f"{lv:.2f}", f"{x:.4f}", f"{y:.4f}", timestamp,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
self.root.after(0, lambda: self.log_gui.log(f"✅ 采集完成: {lv:.2f} cd/m²"))
|
||||||
|
|
||||||
threading.Thread(target=measure, daemon=True).start()
|
threading.Thread(target=measure, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
def clear_ld_records(self):
|
def clear_ld_records(self):
|
||||||
"""清空测试记录"""
|
"""清空 Treeview 中的测试记录。"""
|
||||||
for item in self.ld_tree.get_children():
|
for item in self.ld_tree.get_children():
|
||||||
self.ld_tree.delete(item)
|
self.ld_tree.delete(item)
|
||||||
self.ld_result_label.config(text="亮度: -- cd/m² | x: -- | y: --")
|
self.ld_result_label.config(text="亮度: -- cd/m² | x: -- | y: --")
|
||||||
@@ -183,24 +250,20 @@ def clear_ld_records(self):
|
|||||||
|
|
||||||
|
|
||||||
def save_local_dimming_results(self):
|
def save_local_dimming_results(self):
|
||||||
"""保存 Local Dimming 结果"""
|
"""把 Treeview 中的全部记录导出为 CSV。"""
|
||||||
from tkinter import filedialog
|
|
||||||
import csv
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
if len(self.ld_tree.get_children()) == 0:
|
if len(self.ld_tree.get_children()) == 0:
|
||||||
messagebox.showinfo("提示", "没有可保存的数据")
|
messagebox.showinfo("提示", "没有可保存的数据")
|
||||||
return
|
return
|
||||||
|
|
||||||
default_name = f"LocalDimming_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
default_name = (
|
||||||
|
f"LocalDimming_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
||||||
|
)
|
||||||
save_path = filedialog.asksaveasfilename(
|
save_path = filedialog.asksaveasfilename(
|
||||||
title="保存测试结果",
|
title="保存测试结果",
|
||||||
initialfile=default_name,
|
initialfile=default_name,
|
||||||
defaultextension=".csv",
|
defaultextension=".csv",
|
||||||
filetypes=[("CSV 文件", "*.csv"), ("所有文件", "*.*")],
|
filetypes=[("CSV 文件", "*.csv"), ("所有文件", "*.*")],
|
||||||
)
|
)
|
||||||
|
|
||||||
if not save_path:
|
if not save_path:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -208,14 +271,10 @@ def save_local_dimming_results(self):
|
|||||||
with open(save_path, "w", newline="", encoding="utf-8-sig") as f:
|
with open(save_path, "w", newline="", encoding="utf-8-sig") as f:
|
||||||
writer = csv.writer(f)
|
writer = csv.writer(f)
|
||||||
writer.writerow(["窗口百分比", "亮度 (cd/m²)", "x", "y", "时间"])
|
writer.writerow(["窗口百分比", "亮度 (cd/m²)", "x", "y", "时间"])
|
||||||
|
|
||||||
for item in self.ld_tree.get_children():
|
for item in self.ld_tree.get_children():
|
||||||
values = self.ld_tree.item(item, "values")
|
writer.writerow(self.ld_tree.item(item, "values"))
|
||||||
writer.writerow(values)
|
|
||||||
|
|
||||||
self.log_gui.log(f"✓ 测试结果已保存: {save_path}")
|
self.log_gui.log(f"✓ 测试结果已保存: {save_path}")
|
||||||
messagebox.showinfo("成功", f"测试结果已保存到:\n{save_path}")
|
messagebox.showinfo("成功", f"测试结果已保存到:\n{save_path}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_gui.log(f"❌ 保存失败: {str(e)}")
|
self.log_gui.log(f"❌ 保存失败: {str(e)}")
|
||||||
messagebox.showerror("错误", f"保存失败: {str(e)}")
|
messagebox.showerror("错误", f"保存失败: {str(e)}")
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import tkinter as tk
|
|||||||
import ttkbootstrap as ttk
|
import ttkbootstrap as ttk
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||||
from views.pq_debug_panel import PQDebugPanel
|
from app.views.pq_debug_panel import PQDebugPanel
|
||||||
|
|
||||||
def init_gamut_chart(self):
|
def init_gamut_chart(self):
|
||||||
"""初始化色域图表 - 手动设置subplot位置,完全避免重叠"""
|
"""初始化色域图表 - 手动设置subplot位置,完全避免重叠"""
|
||||||
|
|||||||
@@ -22,11 +22,12 @@ def get_resource_path(relative_path):
|
|||||||
base_path = sys._MEIPASS
|
base_path = sys._MEIPASS
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# 开发环境:使用项目根目录
|
# 开发环境:使用项目根目录
|
||||||
# 当前文件: views/collapsing_frame.py
|
# 当前文件: app/views/collapsing_frame.py
|
||||||
# 项目根目录: views 的父目录
|
# 项目根目录: app/views 的祖父目录
|
||||||
current_file = os.path.abspath(__file__)
|
current_file = os.path.abspath(__file__)
|
||||||
views_dir = os.path.dirname(current_file)
|
views_dir = os.path.dirname(current_file)
|
||||||
base_path = os.path.dirname(views_dir)
|
app_dir = os.path.dirname(views_dir)
|
||||||
|
base_path = os.path.dirname(app_dir)
|
||||||
|
|
||||||
return os.path.join(base_path, relative_path)
|
return os.path.join(base_path, relative_path)
|
||||||
|
|
||||||
72
app/views/panel_manager.py
Normal file
72
app/views/panel_manager.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"""面板管理器(Step 6 重构)。
|
||||||
|
|
||||||
|
register_panel / show_panel / hide_all_panels —— 在右侧栏不同浮动面板间切换。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
|
||||||
|
def register_panel(self, panel_name, frame, button, visible_attr):
|
||||||
|
"""注册一个面板到管理系统"""
|
||||||
|
self.panels[panel_name] = {
|
||||||
|
"frame": frame,
|
||||||
|
"button": button,
|
||||||
|
"visible_attr": visible_attr,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def show_panel(self, panel_name):
|
||||||
|
"""显示指定面板,隐藏其他所有面板"""
|
||||||
|
if panel_name not in self.panels:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 如果当前面板就是要显示的面板,则隐藏它
|
||||||
|
if self.current_panel == panel_name:
|
||||||
|
self.hide_all_panels()
|
||||||
|
return
|
||||||
|
|
||||||
|
# 隐藏所有面板
|
||||||
|
self.hide_all_panels()
|
||||||
|
|
||||||
|
# 显示指定面板
|
||||||
|
panel_info = self.panels[panel_name]
|
||||||
|
|
||||||
|
# 隐藏主内容区域
|
||||||
|
self.control_frame_top.pack_forget()
|
||||||
|
self.control_frame_middle.pack_forget()
|
||||||
|
self.control_frame_bottom.pack_forget()
|
||||||
|
|
||||||
|
# 显示目标面板
|
||||||
|
panel_info["frame"].pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
# 更新按钮样式
|
||||||
|
if panel_info["button"]:
|
||||||
|
panel_info["button"].configure(style="SidebarSelected.TButton")
|
||||||
|
|
||||||
|
# 更新状态
|
||||||
|
setattr(self, panel_info["visible_attr"], True)
|
||||||
|
self.current_panel = panel_name
|
||||||
|
|
||||||
|
|
||||||
|
def hide_all_panels(self):
|
||||||
|
"""隐藏所有面板,显示主内容区域"""
|
||||||
|
# 隐藏所有注册的面板
|
||||||
|
for panel_name, panel_info in self.panels.items():
|
||||||
|
panel_info["frame"].pack_forget()
|
||||||
|
if panel_info["button"]:
|
||||||
|
panel_info["button"].configure(style="Sidebar.TButton")
|
||||||
|
setattr(self, panel_info["visible_attr"], False)
|
||||||
|
|
||||||
|
# 显示主内容区域
|
||||||
|
self.control_frame_top.pack(
|
||||||
|
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
|
||||||
|
)
|
||||||
|
self.control_frame_middle.pack(
|
||||||
|
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
|
||||||
|
)
|
||||||
|
self.control_frame_bottom.pack(
|
||||||
|
side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
|
||||||
|
)
|
||||||
|
|
||||||
|
self.current_panel = None
|
||||||
|
|
||||||
|
|
||||||
0
app/views/panels/__init__.py
Normal file
0
app/views/panels/__init__.py
Normal file
901
app/views/panels/cct_panel.py
Normal file
901
app/views/panels/cct_panel.py
Normal file
@@ -0,0 +1,901 @@
|
|||||||
|
"""CCT 参数面板及其处理函数(Step 6 重构)。"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from tkinter import messagebox
|
||||||
|
import tkinter as tk
|
||||||
|
import ttkbootstrap as ttk
|
||||||
|
|
||||||
|
import algorithm.pq_algorithm as pq_algorithm
|
||||||
|
|
||||||
|
def create_cct_params_frame(self):
|
||||||
|
"""创建色度参数设置区域 - 屏模组、SDR、HDR 独立(✅ 增加色域参考标准选择 + 单步调试按钮)"""
|
||||||
|
|
||||||
|
# ==================== 屏模组色度参数 Frame ====================
|
||||||
|
self.cct_params_frame = ttk.LabelFrame(
|
||||||
|
self.test_items_frame, text="色度参数设置(屏模组)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 默认值
|
||||||
|
self.DEFAULT_CCT_PARAMS = {
|
||||||
|
"x_ideal": 0.3127,
|
||||||
|
"x_tolerance": 0.003,
|
||||||
|
"y_ideal": 0.3290,
|
||||||
|
"y_tolerance": 0.003,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 从配置读取屏模组参数
|
||||||
|
saved_params = self.config.current_test_types.get("screen_module", {}).get(
|
||||||
|
"cct_params", self.DEFAULT_CCT_PARAMS.copy()
|
||||||
|
)
|
||||||
|
|
||||||
|
# 色域参考标准
|
||||||
|
saved_gamut_ref = self.config.current_test_types.get("screen_module", {}).get(
|
||||||
|
"gamut_reference", "DCI-P3"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建屏模组变量
|
||||||
|
self.cct_x_ideal_var = tk.StringVar(
|
||||||
|
value=str(saved_params.get("x_ideal", 0.3127))
|
||||||
|
)
|
||||||
|
self.cct_x_tolerance_var = tk.StringVar(
|
||||||
|
value=str(saved_params.get("x_tolerance", 0.003))
|
||||||
|
)
|
||||||
|
self.cct_y_ideal_var = tk.StringVar(
|
||||||
|
value=str(saved_params.get("y_ideal", 0.3290))
|
||||||
|
)
|
||||||
|
self.cct_y_tolerance_var = tk.StringVar(
|
||||||
|
value=str(saved_params.get("y_tolerance", 0.003))
|
||||||
|
)
|
||||||
|
self.screen_gamut_ref_var = tk.StringVar(value=saved_gamut_ref)
|
||||||
|
|
||||||
|
# 创建屏模组输入框(左侧:色度参数)
|
||||||
|
params = [
|
||||||
|
("x-ideal:", self.cct_x_ideal_var, "x_ideal"),
|
||||||
|
("x-tolerance:", self.cct_x_tolerance_var, "x_tolerance"),
|
||||||
|
("y-ideal:", self.cct_y_ideal_var, "y_ideal"),
|
||||||
|
("y-tolerance:", self.cct_y_tolerance_var, "y_tolerance"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, (label_text, var, key) in enumerate(params):
|
||||||
|
ttk.Label(self.cct_params_frame, text=label_text).grid(
|
||||||
|
row=i, column=0, sticky=tk.W, padx=5, pady=3
|
||||||
|
)
|
||||||
|
entry = ttk.Entry(self.cct_params_frame, textvariable=var, width=15)
|
||||||
|
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
|
||||||
|
|
||||||
|
# 绑定失去焦点事件
|
||||||
|
default_val = self.DEFAULT_CCT_PARAMS[key]
|
||||||
|
entry.bind(
|
||||||
|
"<FocusOut>",
|
||||||
|
lambda e, v=var, d=default_val: self.on_cct_param_focus_out(v, d),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 色域参考标准选择(右侧第一行)
|
||||||
|
ttk.Label(self.cct_params_frame, text="色域参考标准:").grid(
|
||||||
|
row=0, column=2, sticky=tk.W, padx=(20, 5), pady=3
|
||||||
|
)
|
||||||
|
screen_gamut_combo = ttk.Combobox(
|
||||||
|
self.cct_params_frame,
|
||||||
|
textvariable=self.screen_gamut_ref_var,
|
||||||
|
values=["BT.2020", "BT.709", "DCI-P3"],
|
||||||
|
state="disabled",
|
||||||
|
width=12,
|
||||||
|
)
|
||||||
|
screen_gamut_combo.grid(row=0, column=3, sticky=tk.W, padx=5, pady=3)
|
||||||
|
screen_gamut_combo.bind(
|
||||||
|
"<<ComboboxSelected>>", self.on_screen_gamut_ref_changed
|
||||||
|
)
|
||||||
|
self.screen_gamut_combo = screen_gamut_combo
|
||||||
|
|
||||||
|
# ==================== ✅ 单步调试按钮(右侧第二行)====================
|
||||||
|
ttk.Label(self.cct_params_frame, text="单步调试:").grid(
|
||||||
|
row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3
|
||||||
|
)
|
||||||
|
|
||||||
|
self.screen_debug_btn = ttk.Button(
|
||||||
|
self.cct_params_frame,
|
||||||
|
text="打开调试面板",
|
||||||
|
command=self.toggle_screen_debug_panel,
|
||||||
|
bootstyle="info-outline",
|
||||||
|
state=tk.DISABLED, # 初始禁用
|
||||||
|
width=15,
|
||||||
|
)
|
||||||
|
self.screen_debug_btn.grid(row=1, column=3, sticky=tk.W, padx=5, pady=3)
|
||||||
|
|
||||||
|
# 重新计算按钮(屏模组)
|
||||||
|
self.recalc_cct_btn = ttk.Button(
|
||||||
|
self.cct_params_frame,
|
||||||
|
text="应用新参数并重绘",
|
||||||
|
command=self.recalculate_cct,
|
||||||
|
bootstyle="success",
|
||||||
|
)
|
||||||
|
self.recalc_cct_btn.grid(
|
||||||
|
row=4, column=0, columnspan=2, pady=10, padx=5, sticky="ew"
|
||||||
|
)
|
||||||
|
self.recalc_cct_btn.grid_remove()
|
||||||
|
|
||||||
|
# 色域重新计算按钮
|
||||||
|
self.recalc_gamut_btn = ttk.Button(
|
||||||
|
self.cct_params_frame,
|
||||||
|
text="应用色域参考并重绘",
|
||||||
|
command=self.recalculate_gamut,
|
||||||
|
bootstyle="warning",
|
||||||
|
)
|
||||||
|
self.recalc_gamut_btn.grid(
|
||||||
|
row=4, column=2, columnspan=2, pady=10, padx=5, sticky="ew"
|
||||||
|
)
|
||||||
|
self.recalc_gamut_btn.grid_remove()
|
||||||
|
|
||||||
|
# 提示文字
|
||||||
|
ttk.Label(
|
||||||
|
self.cct_params_frame,
|
||||||
|
text="提示: 清空输入框将恢复默认值",
|
||||||
|
font=("SimHei", 8),
|
||||||
|
foreground="gray",
|
||||||
|
).grid(row=5, column=0, columnspan=4, sticky=tk.W, padx=5, pady=5)
|
||||||
|
|
||||||
|
# ==================== SDR 色度参数 Frame ====================
|
||||||
|
self.sdr_cct_params_frame = ttk.LabelFrame(
|
||||||
|
self.test_items_frame, text="色度参数设置(SDR)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# SDR 默认值
|
||||||
|
self.SDR_DEFAULT_CCT_PARAMS = {
|
||||||
|
"x_ideal": 0.3127,
|
||||||
|
"x_tolerance": 0.003,
|
||||||
|
"y_ideal": 0.3290,
|
||||||
|
"y_tolerance": 0.003,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 从配置读取 SDR 参数
|
||||||
|
sdr_saved_params = self.config.current_test_types.get("sdr_movie", {}).get(
|
||||||
|
"cct_params", self.SDR_DEFAULT_CCT_PARAMS.copy()
|
||||||
|
)
|
||||||
|
|
||||||
|
# 色域参考标准
|
||||||
|
sdr_saved_gamut_ref = self.config.current_test_types.get("sdr_movie", {}).get(
|
||||||
|
"gamut_reference", "BT.709"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建 SDR 变量
|
||||||
|
self.sdr_cct_x_ideal_var = tk.StringVar(
|
||||||
|
value=str(sdr_saved_params.get("x_ideal", 0.3127))
|
||||||
|
)
|
||||||
|
self.sdr_cct_x_tolerance_var = tk.StringVar(
|
||||||
|
value=str(sdr_saved_params.get("x_tolerance", 0.003))
|
||||||
|
)
|
||||||
|
self.sdr_cct_y_ideal_var = tk.StringVar(
|
||||||
|
value=str(sdr_saved_params.get("y_ideal", 0.3290))
|
||||||
|
)
|
||||||
|
self.sdr_cct_y_tolerance_var = tk.StringVar(
|
||||||
|
value=str(sdr_saved_params.get("y_tolerance", 0.003))
|
||||||
|
)
|
||||||
|
self.sdr_gamut_ref_var = tk.StringVar(value=sdr_saved_gamut_ref)
|
||||||
|
|
||||||
|
# 创建 SDR 输入框
|
||||||
|
sdr_params = [
|
||||||
|
("x-ideal:", self.sdr_cct_x_ideal_var, "x_ideal"),
|
||||||
|
("x-tolerance:", self.sdr_cct_x_tolerance_var, "x_tolerance"),
|
||||||
|
("y-ideal:", self.sdr_cct_y_ideal_var, "y_ideal"),
|
||||||
|
("y-tolerance:", self.sdr_cct_y_tolerance_var, "y_tolerance"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, (label_text, var, key) in enumerate(sdr_params):
|
||||||
|
ttk.Label(self.sdr_cct_params_frame, text=label_text).grid(
|
||||||
|
row=i, column=0, sticky=tk.W, padx=5, pady=3
|
||||||
|
)
|
||||||
|
entry = ttk.Entry(self.sdr_cct_params_frame, textvariable=var, width=15)
|
||||||
|
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
|
||||||
|
|
||||||
|
# 绑定失去焦点事件
|
||||||
|
default_val = self.SDR_DEFAULT_CCT_PARAMS[key]
|
||||||
|
entry.bind(
|
||||||
|
"<FocusOut>",
|
||||||
|
lambda e, v=var, d=default_val: self.on_sdr_cct_param_focus_out(v, d),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 色域参考标准选择(右侧第一行)
|
||||||
|
ttk.Label(self.sdr_cct_params_frame, text="色域参考标准:").grid(
|
||||||
|
row=0, column=2, sticky=tk.W, padx=(20, 5), pady=3
|
||||||
|
)
|
||||||
|
sdr_gamut_combo = ttk.Combobox(
|
||||||
|
self.sdr_cct_params_frame,
|
||||||
|
textvariable=self.sdr_gamut_ref_var,
|
||||||
|
values=["BT.2020", "BT.709", "DCI-P3"],
|
||||||
|
state="disabled",
|
||||||
|
width=12,
|
||||||
|
)
|
||||||
|
sdr_gamut_combo.grid(row=0, column=3, sticky=tk.W, padx=5, pady=3)
|
||||||
|
sdr_gamut_combo.bind("<<ComboboxSelected>>", self.on_sdr_gamut_ref_changed)
|
||||||
|
self.sdr_gamut_combo = sdr_gamut_combo
|
||||||
|
|
||||||
|
# ==================== ✅ SDR 单步调试按钮(右侧第二行)====================
|
||||||
|
ttk.Label(self.sdr_cct_params_frame, text="单步调试:").grid(
|
||||||
|
row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sdr_debug_btn = ttk.Button(
|
||||||
|
self.sdr_cct_params_frame,
|
||||||
|
text="打开调试面板",
|
||||||
|
command=self.toggle_sdr_debug_panel,
|
||||||
|
bootstyle="info-outline",
|
||||||
|
state=tk.DISABLED, # 初始禁用
|
||||||
|
width=15,
|
||||||
|
)
|
||||||
|
self.sdr_debug_btn.grid(row=1, column=3, sticky=tk.W, padx=5, pady=3)
|
||||||
|
|
||||||
|
# 重新计算按钮(SDR)
|
||||||
|
self.sdr_recalc_cct_btn = ttk.Button(
|
||||||
|
self.sdr_cct_params_frame,
|
||||||
|
text="应用新参数并重绘",
|
||||||
|
command=self.recalculate_cct,
|
||||||
|
bootstyle="success",
|
||||||
|
)
|
||||||
|
self.sdr_recalc_cct_btn.grid(
|
||||||
|
row=4, column=0, columnspan=2, pady=10, padx=5, sticky="ew"
|
||||||
|
)
|
||||||
|
self.sdr_recalc_cct_btn.grid_remove()
|
||||||
|
|
||||||
|
# 色域重新计算按钮(SDR)
|
||||||
|
self.sdr_recalc_gamut_btn = ttk.Button(
|
||||||
|
self.sdr_cct_params_frame,
|
||||||
|
text="应用色域参考并重绘",
|
||||||
|
command=self.recalculate_gamut,
|
||||||
|
bootstyle="warning",
|
||||||
|
)
|
||||||
|
self.sdr_recalc_gamut_btn.grid(
|
||||||
|
row=4, column=2, columnspan=2, pady=10, padx=5, sticky="ew"
|
||||||
|
)
|
||||||
|
self.sdr_recalc_gamut_btn.grid_remove()
|
||||||
|
|
||||||
|
# 提示文字
|
||||||
|
ttk.Label(
|
||||||
|
self.sdr_cct_params_frame,
|
||||||
|
text="提示: 清空输入框将恢复默认值",
|
||||||
|
font=("SimHei", 8),
|
||||||
|
foreground="gray",
|
||||||
|
).grid(row=5, column=0, columnspan=4, sticky=tk.W, padx=5, pady=5)
|
||||||
|
|
||||||
|
# ==================== HDR 色度参数 Frame ====================
|
||||||
|
self.hdr_cct_params_frame = ttk.LabelFrame(
|
||||||
|
self.test_items_frame, text="色度参数设置(HDR)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# HDR 默认值
|
||||||
|
self.HDR_DEFAULT_CCT_PARAMS = {
|
||||||
|
"x_ideal": 0.3127,
|
||||||
|
"x_tolerance": 0.003,
|
||||||
|
"y_ideal": 0.3290,
|
||||||
|
"y_tolerance": 0.003,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 从配置读取 HDR 参数
|
||||||
|
hdr_saved_params = self.config.current_test_types.get("hdr_movie", {}).get(
|
||||||
|
"cct_params", self.HDR_DEFAULT_CCT_PARAMS.copy()
|
||||||
|
)
|
||||||
|
|
||||||
|
# 色域参考标准
|
||||||
|
hdr_saved_gamut_ref = self.config.current_test_types.get("hdr_movie", {}).get(
|
||||||
|
"gamut_reference", "BT.2020"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建 HDR 变量
|
||||||
|
self.hdr_cct_x_ideal_var = tk.StringVar(
|
||||||
|
value=str(hdr_saved_params.get("x_ideal", 0.3127))
|
||||||
|
)
|
||||||
|
self.hdr_cct_x_tolerance_var = tk.StringVar(
|
||||||
|
value=str(hdr_saved_params.get("x_tolerance", 0.003))
|
||||||
|
)
|
||||||
|
self.hdr_cct_y_ideal_var = tk.StringVar(
|
||||||
|
value=str(hdr_saved_params.get("y_ideal", 0.3290))
|
||||||
|
)
|
||||||
|
self.hdr_cct_y_tolerance_var = tk.StringVar(
|
||||||
|
value=str(hdr_saved_params.get("y_tolerance", 0.003))
|
||||||
|
)
|
||||||
|
self.hdr_gamut_ref_var = tk.StringVar(value=hdr_saved_gamut_ref)
|
||||||
|
|
||||||
|
# 创建 HDR 输入框
|
||||||
|
hdr_params = [
|
||||||
|
("x-ideal:", self.hdr_cct_x_ideal_var, "x_ideal"),
|
||||||
|
("x-tolerance:", self.hdr_cct_x_tolerance_var, "x_tolerance"),
|
||||||
|
("y-ideal:", self.hdr_cct_y_ideal_var, "y_ideal"),
|
||||||
|
("y-tolerance:", self.hdr_cct_y_tolerance_var, "y_tolerance"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, (label_text, var, key) in enumerate(hdr_params):
|
||||||
|
ttk.Label(self.hdr_cct_params_frame, text=label_text).grid(
|
||||||
|
row=i, column=0, sticky=tk.W, padx=5, pady=3
|
||||||
|
)
|
||||||
|
entry = ttk.Entry(self.hdr_cct_params_frame, textvariable=var, width=15)
|
||||||
|
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
|
||||||
|
|
||||||
|
# 绑定失去焦点事件
|
||||||
|
default_val = self.HDR_DEFAULT_CCT_PARAMS[key]
|
||||||
|
entry.bind(
|
||||||
|
"<FocusOut>",
|
||||||
|
lambda e, v=var, d=default_val: self.on_hdr_cct_param_focus_out(v, d),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 色域参考标准选择(右侧第一行)
|
||||||
|
ttk.Label(self.hdr_cct_params_frame, text="色域参考标准:").grid(
|
||||||
|
row=0, column=2, sticky=tk.W, padx=(20, 5), pady=3
|
||||||
|
)
|
||||||
|
hdr_gamut_combo = ttk.Combobox(
|
||||||
|
self.hdr_cct_params_frame,
|
||||||
|
textvariable=self.hdr_gamut_ref_var,
|
||||||
|
values=["BT.2020", "BT.709", "DCI-P3"],
|
||||||
|
state="disabled",
|
||||||
|
width=12,
|
||||||
|
)
|
||||||
|
hdr_gamut_combo.grid(row=0, column=3, sticky=tk.W, padx=5, pady=3)
|
||||||
|
hdr_gamut_combo.bind("<<ComboboxSelected>>", self.on_hdr_gamut_ref_changed)
|
||||||
|
self.hdr_gamut_combo = hdr_gamut_combo
|
||||||
|
|
||||||
|
# ==================== ✅ HDR 单步调试按钮(右侧第二行)====================
|
||||||
|
ttk.Label(self.hdr_cct_params_frame, text="单步调试:").grid(
|
||||||
|
row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3
|
||||||
|
)
|
||||||
|
|
||||||
|
self.hdr_debug_btn = ttk.Button(
|
||||||
|
self.hdr_cct_params_frame,
|
||||||
|
text="打开调试面板",
|
||||||
|
command=self.toggle_hdr_debug_panel,
|
||||||
|
bootstyle="info-outline",
|
||||||
|
state=tk.DISABLED, # 初始禁用
|
||||||
|
width=15,
|
||||||
|
)
|
||||||
|
self.hdr_debug_btn.grid(row=1, column=3, sticky=tk.W, padx=5, pady=3)
|
||||||
|
|
||||||
|
# 重新计算按钮(HDR)
|
||||||
|
self.hdr_recalc_cct_btn = ttk.Button(
|
||||||
|
self.hdr_cct_params_frame,
|
||||||
|
text="应用新参数并重绘",
|
||||||
|
command=self.recalculate_cct,
|
||||||
|
bootstyle="success",
|
||||||
|
)
|
||||||
|
self.hdr_recalc_cct_btn.grid(
|
||||||
|
row=4, column=0, columnspan=2, pady=10, padx=5, sticky="ew"
|
||||||
|
)
|
||||||
|
self.hdr_recalc_cct_btn.grid_remove()
|
||||||
|
|
||||||
|
# 色域重新计算按钮(HDR)
|
||||||
|
self.hdr_recalc_gamut_btn = ttk.Button(
|
||||||
|
self.hdr_cct_params_frame,
|
||||||
|
text="应用色域参考并重绘",
|
||||||
|
command=self.recalculate_gamut,
|
||||||
|
bootstyle="warning",
|
||||||
|
)
|
||||||
|
self.hdr_recalc_gamut_btn.grid(
|
||||||
|
row=4, column=2, columnspan=2, pady=10, padx=5, sticky="ew"
|
||||||
|
)
|
||||||
|
self.hdr_recalc_gamut_btn.grid_remove()
|
||||||
|
|
||||||
|
# 提示文字
|
||||||
|
ttk.Label(
|
||||||
|
self.hdr_cct_params_frame,
|
||||||
|
text="提示: 清空输入框将恢复默认值",
|
||||||
|
font=("SimHei", 8),
|
||||||
|
foreground="gray",
|
||||||
|
).grid(row=5, column=0, columnspan=4, sticky=tk.W, padx=5, pady=5)
|
||||||
|
|
||||||
|
|
||||||
|
def on_sdr_cct_param_focus_out(self, var, default_value):
|
||||||
|
"""SDR 色度参数失去焦点时的处理"""
|
||||||
|
try:
|
||||||
|
value = var.get().strip()
|
||||||
|
|
||||||
|
if value == "":
|
||||||
|
var.set(str(default_value))
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log(f"✓ SDR 参数为空,恢复默认值: {default_value}")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
float_val = float(value)
|
||||||
|
if float_val < 0 or float_val > 1:
|
||||||
|
var.set(str(default_value))
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log(
|
||||||
|
f"⚠️ SDR 参数超出范围,恢复默认值: {default_value}"
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
var.set(str(default_value))
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log(f"⚠️ SDR 参数无效,恢复默认值: {default_value}")
|
||||||
|
|
||||||
|
self.save_sdr_cct_params()
|
||||||
|
except Exception as e:
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log(f"处理 SDR 参数失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def save_sdr_cct_params(self):
|
||||||
|
"""保存 SDR 色度参数"""
|
||||||
|
try:
|
||||||
|
|
||||||
|
def get_float(var, default):
|
||||||
|
try:
|
||||||
|
value = var.get().strip()
|
||||||
|
if value == "":
|
||||||
|
return default
|
||||||
|
return float(value)
|
||||||
|
except:
|
||||||
|
return default
|
||||||
|
|
||||||
|
sdr_cct_params = {
|
||||||
|
"x_ideal": get_float(
|
||||||
|
self.sdr_cct_x_ideal_var, self.SDR_DEFAULT_CCT_PARAMS["x_ideal"]
|
||||||
|
),
|
||||||
|
"x_tolerance": get_float(
|
||||||
|
self.sdr_cct_x_tolerance_var,
|
||||||
|
self.SDR_DEFAULT_CCT_PARAMS["x_tolerance"],
|
||||||
|
),
|
||||||
|
"y_ideal": get_float(
|
||||||
|
self.sdr_cct_y_ideal_var, self.SDR_DEFAULT_CCT_PARAMS["y_ideal"]
|
||||||
|
),
|
||||||
|
"y_tolerance": get_float(
|
||||||
|
self.sdr_cct_y_tolerance_var,
|
||||||
|
self.SDR_DEFAULT_CCT_PARAMS["y_tolerance"],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
if "sdr_movie" not in self.config.current_test_types:
|
||||||
|
self.config.current_test_types["sdr_movie"] = {}
|
||||||
|
|
||||||
|
self.config.current_test_types["sdr_movie"]["cct_params"] = sdr_cct_params
|
||||||
|
self.save_pq_config()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def on_hdr_cct_param_focus_out(self, var, default_value):
|
||||||
|
"""HDR 色度参数失去焦点时的处理"""
|
||||||
|
try:
|
||||||
|
value = var.get().strip()
|
||||||
|
|
||||||
|
if value == "":
|
||||||
|
var.set(str(default_value))
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log(f"✓ HDR 参数为空,恢复默认值: {default_value}")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
float_val = float(value)
|
||||||
|
if float_val < 0 or float_val > 1:
|
||||||
|
var.set(str(default_value))
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log(
|
||||||
|
f"⚠️ HDR 参数超出范围,恢复默认值: {default_value}"
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
var.set(str(default_value))
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log(f"⚠️ HDR 参数无效,恢复默认值: {default_value}")
|
||||||
|
|
||||||
|
self.save_hdr_cct_params()
|
||||||
|
except Exception as e:
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log(f"处理 HDR 参数失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def save_hdr_cct_params(self):
|
||||||
|
"""保存 HDR 色度参数"""
|
||||||
|
try:
|
||||||
|
|
||||||
|
def get_float(var, default):
|
||||||
|
try:
|
||||||
|
value = var.get().strip()
|
||||||
|
if value == "":
|
||||||
|
return default
|
||||||
|
return float(value)
|
||||||
|
except:
|
||||||
|
return default
|
||||||
|
|
||||||
|
hdr_cct_params = {
|
||||||
|
"x_ideal": get_float(
|
||||||
|
self.hdr_cct_x_ideal_var, self.HDR_DEFAULT_CCT_PARAMS["x_ideal"]
|
||||||
|
),
|
||||||
|
"x_tolerance": get_float(
|
||||||
|
self.hdr_cct_x_tolerance_var,
|
||||||
|
self.HDR_DEFAULT_CCT_PARAMS["x_tolerance"],
|
||||||
|
),
|
||||||
|
"y_ideal": get_float(
|
||||||
|
self.hdr_cct_y_ideal_var, self.HDR_DEFAULT_CCT_PARAMS["y_ideal"]
|
||||||
|
),
|
||||||
|
"y_tolerance": get_float(
|
||||||
|
self.hdr_cct_y_tolerance_var,
|
||||||
|
self.HDR_DEFAULT_CCT_PARAMS["y_tolerance"],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
if "hdr_movie" not in self.config.current_test_types:
|
||||||
|
self.config.current_test_types["hdr_movie"] = {}
|
||||||
|
|
||||||
|
self.config.current_test_types["hdr_movie"]["cct_params"] = hdr_cct_params
|
||||||
|
self.save_pq_config()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def recalculate_cct(self):
|
||||||
|
"""重新计算并绘制色度图"""
|
||||||
|
try:
|
||||||
|
# 1. 保存新参数
|
||||||
|
self.save_cct_params()
|
||||||
|
self.log_gui.log("✓ 色度参数已更新")
|
||||||
|
|
||||||
|
# 2. 收起配置项
|
||||||
|
if hasattr(self, "config_panel_frame"):
|
||||||
|
try:
|
||||||
|
if self.config_panel_frame.winfo_viewable():
|
||||||
|
self.config_panel_frame.btn.invoke()
|
||||||
|
self.root.update_idletasks()
|
||||||
|
time.sleep(0.1)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 3. 跳转到色度图Tab
|
||||||
|
self.chart_notebook.select(self.cct_chart_frame)
|
||||||
|
self.root.update_idletasks()
|
||||||
|
|
||||||
|
# 4. 检查是否有数据
|
||||||
|
if not hasattr(self, "results") or not self.results:
|
||||||
|
self.log_gui.log("⚠️ 没有测试数据,无法重新绘制")
|
||||||
|
messagebox.showwarning("警告", "请先完成测试后再重新计算")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 5. 获取保存的灰阶数据
|
||||||
|
gray_data = self.results.get_intermediate_data("shared", "gray")
|
||||||
|
if not gray_data:
|
||||||
|
gray_data = self.results.get_intermediate_data("cct", "gray")
|
||||||
|
|
||||||
|
if not gray_data or len(gray_data) < 2:
|
||||||
|
self.log_gui.log("⚠️ 没有可用的灰阶数据")
|
||||||
|
messagebox.showwarning("警告", "没有找到色度测试数据")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 6. 重新计算 CCT
|
||||||
|
self.log_gui.log("=" * 50)
|
||||||
|
self.log_gui.log("开始重新计算色度一致性...")
|
||||||
|
self.log_gui.log("=" * 50)
|
||||||
|
|
||||||
|
import algorithm.pq_algorithm as pq_algorithm
|
||||||
|
|
||||||
|
cct_values = pq_algorithm.calculate_cct_from_results(gray_data)
|
||||||
|
|
||||||
|
# 7. 更新结果
|
||||||
|
self.results.set_test_item_result("cct", {"cct_values": cct_values})
|
||||||
|
|
||||||
|
# 8. 重新绘制色度图
|
||||||
|
test_type = self.config.current_test_type
|
||||||
|
self.plot_cct(test_type)
|
||||||
|
|
||||||
|
self.log_gui.log("✓ 色度图已重新绘制")
|
||||||
|
self.log_gui.log("=" * 50)
|
||||||
|
|
||||||
|
messagebox.showinfo("成功", "色度图已根据新参数重新绘制!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log_gui.log(f"❌ 重新计算失败: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
self.log_gui.log(traceback.format_exc())
|
||||||
|
messagebox.showerror("错误", f"重新计算失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def recalculate_gamut(self):
|
||||||
|
"""重新计算并绘制色域图(使用新的参考标准)"""
|
||||||
|
try:
|
||||||
|
# 1. 收起配置项
|
||||||
|
if hasattr(self, "config_panel_frame"):
|
||||||
|
try:
|
||||||
|
if self.config_panel_frame.winfo_viewable():
|
||||||
|
self.config_panel_frame.btn.invoke()
|
||||||
|
self.root.update_idletasks()
|
||||||
|
time.sleep(0.1)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 2. 跳转到色域图Tab
|
||||||
|
self.chart_notebook.select(self.gamut_chart_frame)
|
||||||
|
self.root.update_idletasks()
|
||||||
|
|
||||||
|
# 3. 检查是否有数据
|
||||||
|
if not hasattr(self, "results") or not self.results:
|
||||||
|
self.log_gui.log("⚠️ 没有测试数据,无法重新绘制")
|
||||||
|
messagebox.showwarning("警告", "请先完成测试后再重新计算")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 4. 获取保存的色域数据
|
||||||
|
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
|
||||||
|
|
||||||
|
if not rgb_data or len(rgb_data) < 3:
|
||||||
|
self.log_gui.log("⚠️ 没有可用的色域数据")
|
||||||
|
messagebox.showwarning("警告", "没有找到色域测试数据")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 5. 获取当前测试类型
|
||||||
|
test_type = self.config.current_test_type
|
||||||
|
|
||||||
|
# 6. 获取用户选择的参考标准
|
||||||
|
if test_type == "screen_module":
|
||||||
|
reference_standard = self.screen_gamut_ref_var.get()
|
||||||
|
elif test_type == "sdr_movie":
|
||||||
|
reference_standard = self.sdr_gamut_ref_var.get()
|
||||||
|
elif test_type == "hdr_movie":
|
||||||
|
reference_standard = self.hdr_gamut_ref_var.get()
|
||||||
|
else:
|
||||||
|
reference_standard = "DCI-P3"
|
||||||
|
|
||||||
|
self.log_gui.log("=" * 50)
|
||||||
|
self.log_gui.log(f"开始重新计算色域(参考标准: {reference_standard})...")
|
||||||
|
self.log_gui.log("=" * 50)
|
||||||
|
|
||||||
|
# 7. 重新计算 XY 色域覆盖率
|
||||||
|
xy_points = [[result[0], result[1]] for result in rgb_data]
|
||||||
|
|
||||||
|
# 根据参考标准计算 XY 覆盖率
|
||||||
|
if reference_standard == "BT.2020":
|
||||||
|
area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_BT2020(
|
||||||
|
xy_points
|
||||||
|
)
|
||||||
|
elif reference_standard == "BT.709":
|
||||||
|
area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_BT709(
|
||||||
|
xy_points
|
||||||
|
)
|
||||||
|
elif reference_standard == "DCI-P3":
|
||||||
|
area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_DCIP3(
|
||||||
|
xy_points
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_DCIP3(
|
||||||
|
xy_points
|
||||||
|
)
|
||||||
|
reference_standard = "DCI-P3"
|
||||||
|
|
||||||
|
self.log_gui.log(f"✓ 参考标准: {reference_standard}")
|
||||||
|
self.log_gui.log(f"✓ XY 色域覆盖率: {coverage_xy:.1f}%")
|
||||||
|
|
||||||
|
# ========== ✅✅✅ 8. 重新计算 UV 色域覆盖率 ==========
|
||||||
|
# 将 XY 坐标转换为 UV 坐标
|
||||||
|
uv_points = []
|
||||||
|
for x, y in xy_points:
|
||||||
|
try:
|
||||||
|
# XY转UV公式
|
||||||
|
denom = -2 * x + 12 * y + 3
|
||||||
|
if abs(denom) < 1e-10:
|
||||||
|
u, v = 0, 0
|
||||||
|
else:
|
||||||
|
u = 4 * x / denom
|
||||||
|
v = 9 * y / denom
|
||||||
|
uv_points.append([u, v])
|
||||||
|
except ZeroDivisionError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.log_gui.log(f"✓ 转换后的 UV 点数量: {len(uv_points)}")
|
||||||
|
|
||||||
|
# 根据参考标准计算 UV 覆盖率
|
||||||
|
if reference_standard == "BT.2020":
|
||||||
|
area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_BT2020_uv(
|
||||||
|
uv_points
|
||||||
|
)
|
||||||
|
elif reference_standard == "BT.709":
|
||||||
|
area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_BT709_uv(
|
||||||
|
uv_points
|
||||||
|
)
|
||||||
|
elif reference_standard == "DCI-P3":
|
||||||
|
area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_DCIP3_uv(
|
||||||
|
uv_points
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_DCIP3_uv(
|
||||||
|
uv_points
|
||||||
|
)
|
||||||
|
|
||||||
|
self.log_gui.log(f"✓ UV 色域覆盖率: {coverage_uv:.1f}%")
|
||||||
|
# ========================================================
|
||||||
|
|
||||||
|
# 9. ✅ 更新结果(同时保存 XY 和 UV 覆盖率)
|
||||||
|
self.results.set_test_item_result(
|
||||||
|
"gamut",
|
||||||
|
{
|
||||||
|
"area": area_xy, # ← 兼容旧字段
|
||||||
|
"coverage": coverage_xy, # ← 兼容旧字段
|
||||||
|
"area_xy": area_xy, # ← XY 面积
|
||||||
|
"coverage_xy": coverage_xy, # ← XY 覆盖率
|
||||||
|
"area_uv": area_uv, # ← UV 面积
|
||||||
|
"coverage_uv": coverage_uv, # ← UV 覆盖率
|
||||||
|
"uv_coverage": coverage_uv, # ← 兼容字段(Excel 导出用)
|
||||||
|
"reference": reference_standard,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.log_gui.log("✓ 测试结果已更新到 results 对象")
|
||||||
|
|
||||||
|
# 10. 重新绘制色域图
|
||||||
|
self.plot_gamut(rgb_data, coverage_xy, test_type)
|
||||||
|
|
||||||
|
self.log_gui.log("✓ 色域图已重新绘制")
|
||||||
|
self.log_gui.log("=" * 50)
|
||||||
|
|
||||||
|
messagebox.showinfo(
|
||||||
|
"成功",
|
||||||
|
f"色域图已根据新参考标准 {reference_standard} 重新绘制!\n\n"
|
||||||
|
f"XY 覆盖率: {coverage_xy:.1f}%\n"
|
||||||
|
f"UV 覆盖率: {coverage_uv:.1f}%",
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log_gui.log(f"❌ 重新计算失败: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
self.log_gui.log(traceback.format_exc())
|
||||||
|
messagebox.showerror("错误", f"重新计算失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def on_cct_param_change(self, var, default_value):
|
||||||
|
"""色度参数改变时的处理 - 空值恢复默认"""
|
||||||
|
try:
|
||||||
|
value = var.get().strip()
|
||||||
|
|
||||||
|
if value == "":
|
||||||
|
# 空值:恢复默认值
|
||||||
|
var.set(str(default_value))
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log(f"输入框为空,恢复默认值: {default_value}")
|
||||||
|
else:
|
||||||
|
# 验证是否为有效数字
|
||||||
|
try:
|
||||||
|
float_val = float(value)
|
||||||
|
if float_val < 0 or float_val > 1:
|
||||||
|
var.set(str(default_value))
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log(
|
||||||
|
f"参数超出范围 [0, 1],恢复默认值: {default_value}"
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
var.set(str(default_value))
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log(f"无效的参数值,恢复默认值: {default_value}")
|
||||||
|
|
||||||
|
# 保存配置
|
||||||
|
self.save_cct_params()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log(f"处理参数变化失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def on_cct_param_focus_out(self, var, default_value):
|
||||||
|
"""色度参数失去焦点时的处理 - 空值恢复默认"""
|
||||||
|
try:
|
||||||
|
value = var.get().strip()
|
||||||
|
|
||||||
|
if value == "":
|
||||||
|
# 空值:恢复默认值
|
||||||
|
var.set(str(default_value))
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log(f"✓ 输入框为空,恢复默认值: {default_value}")
|
||||||
|
else:
|
||||||
|
# 验证是否为有效数字
|
||||||
|
try:
|
||||||
|
float_val = float(value)
|
||||||
|
if float_val < 0 or float_val > 1:
|
||||||
|
var.set(str(default_value))
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log(
|
||||||
|
f"⚠️ 参数超出范围 [0, 1],恢复默认值: {default_value}"
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
var.set(str(default_value))
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log(f"⚠️ 无效的参数值,恢复默认值: {default_value}")
|
||||||
|
|
||||||
|
# 保存配置
|
||||||
|
self.save_cct_params()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log(f"处理参数变化失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def save_cct_params(self):
|
||||||
|
"""保存色度参数 - 简化版"""
|
||||||
|
try:
|
||||||
|
current_type = self.config.current_test_type
|
||||||
|
|
||||||
|
def get_float(var, default):
|
||||||
|
try:
|
||||||
|
value = var.get().strip()
|
||||||
|
if value == "":
|
||||||
|
return default
|
||||||
|
return float(value)
|
||||||
|
except:
|
||||||
|
return default
|
||||||
|
|
||||||
|
cct_params = {
|
||||||
|
"x_ideal": get_float(
|
||||||
|
self.cct_x_ideal_var, self.DEFAULT_CCT_PARAMS["x_ideal"]
|
||||||
|
),
|
||||||
|
"x_tolerance": get_float(
|
||||||
|
self.cct_x_tolerance_var, self.DEFAULT_CCT_PARAMS["x_tolerance"]
|
||||||
|
),
|
||||||
|
"y_ideal": get_float(
|
||||||
|
self.cct_y_ideal_var, self.DEFAULT_CCT_PARAMS["y_ideal"]
|
||||||
|
),
|
||||||
|
"y_tolerance": get_float(
|
||||||
|
self.cct_y_tolerance_var, self.DEFAULT_CCT_PARAMS["y_tolerance"]
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
if current_type not in self.config.current_test_types:
|
||||||
|
self.config.current_test_types[current_type] = {}
|
||||||
|
|
||||||
|
self.config.current_test_types[current_type]["cct_params"] = cct_params
|
||||||
|
self.save_pq_config()
|
||||||
|
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def reload_cct_params(self):
|
||||||
|
"""切换测试类型时重新加载色度参数"""
|
||||||
|
try:
|
||||||
|
current_type = self.config.current_test_type
|
||||||
|
saved_params = self.config.current_test_types.get(current_type, {}).get(
|
||||||
|
"cct_params", None
|
||||||
|
)
|
||||||
|
|
||||||
|
if saved_params is None:
|
||||||
|
saved_params = self.DEFAULT_CCT_PARAMS.copy()
|
||||||
|
|
||||||
|
# 更新输入框的值
|
||||||
|
self.cct_x_ideal_var.set(str(saved_params["x_ideal"]))
|
||||||
|
self.cct_x_tolerance_var.set(str(saved_params["x_tolerance"]))
|
||||||
|
self.cct_y_ideal_var.set(str(saved_params["y_ideal"]))
|
||||||
|
self.cct_y_tolerance_var.set(str(saved_params["y_tolerance"]))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log(f"重新加载色度参数失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def toggle_cct_params_frame(self):
|
||||||
|
"""根据测试类型和测试项的选中状态显示对应参数框"""
|
||||||
|
selected_items = self.get_selected_test_items()
|
||||||
|
current_test_type = self.config.current_test_type
|
||||||
|
|
||||||
|
# ========== 默认隐藏所有参数框 ==========
|
||||||
|
self.cct_params_frame.pack_forget()
|
||||||
|
self.sdr_cct_params_frame.pack_forget()
|
||||||
|
|
||||||
|
# HDR 色度参数框(如果存在的话)
|
||||||
|
if hasattr(self, "hdr_cct_params_frame"):
|
||||||
|
self.hdr_cct_params_frame.pack_forget()
|
||||||
|
|
||||||
|
# ========== 根据测试类型和选中项显示对应参数框 ==========
|
||||||
|
if current_test_type == "screen_module":
|
||||||
|
# 屏模组:只有色度参数
|
||||||
|
if "cct" in selected_items:
|
||||||
|
self.cct_params_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log("✓ 显示屏模组色度参数设置")
|
||||||
|
|
||||||
|
elif current_test_type == "sdr_movie":
|
||||||
|
# SDR:只有色度参数(色准不需要参数设置框)
|
||||||
|
if "cct" in selected_items:
|
||||||
|
self.sdr_cct_params_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log("✓ 显示 SDR 色度参数设置")
|
||||||
|
|
||||||
|
elif current_test_type == "hdr_movie":
|
||||||
|
# HDR:只有色度参数(色准不需要参数设置框)
|
||||||
|
if "cct" in selected_items:
|
||||||
|
if hasattr(self, "hdr_cct_params_frame"):
|
||||||
|
self.hdr_cct_params_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log("✓ 显示 HDR 色度参数设置")
|
||||||
|
else:
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log("⚠️ HDR 色度参数框尚未创建")
|
||||||
|
|
||||||
|
|
||||||
609
app/views/panels/custom_template_panel.py
Normal file
609
app/views/panels/custom_template_panel.py
Normal file
@@ -0,0 +1,609 @@
|
|||||||
|
"""自定义模板结果面板(Step 6 重构)。"""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from tkinter import messagebox
|
||||||
|
import tkinter as tk
|
||||||
|
import ttkbootstrap as ttk
|
||||||
|
|
||||||
|
import colour
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from app.data_range_converter import convert_pattern_params
|
||||||
|
|
||||||
|
def create_custom_template_result_panel(self):
|
||||||
|
"""创建客户模板结果显示区域(黑底表格)"""
|
||||||
|
self.custom_result_frame = ttk.LabelFrame(
|
||||||
|
self.custom_template_tab_frame, text="客户模板结果显示"
|
||||||
|
)
|
||||||
|
self.custom_result_frame.pack(
|
||||||
|
side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5
|
||||||
|
)
|
||||||
|
|
||||||
|
table_container = tk.Frame(
|
||||||
|
self.custom_result_frame,
|
||||||
|
bg="#000000",
|
||||||
|
highlightthickness=1,
|
||||||
|
highlightbackground="#5a5a5a",
|
||||||
|
)
|
||||||
|
table_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
style = ttk.Style()
|
||||||
|
style.configure(
|
||||||
|
"CustomResult.Treeview",
|
||||||
|
background="#000000",
|
||||||
|
fieldbackground="#000000",
|
||||||
|
foreground="#ffffff",
|
||||||
|
rowheight=28,
|
||||||
|
borderwidth=0,
|
||||||
|
)
|
||||||
|
style.configure(
|
||||||
|
"CustomResult.Treeview.Heading",
|
||||||
|
background="#2f2f2f",
|
||||||
|
foreground="#f5f5f5",
|
||||||
|
font=("Microsoft YaHei", 10, "bold"),
|
||||||
|
relief="flat",
|
||||||
|
)
|
||||||
|
style.map(
|
||||||
|
"CustomResult.Treeview",
|
||||||
|
background=[("selected", "#1f4e79")],
|
||||||
|
foreground=[("selected", "#ffffff")],
|
||||||
|
)
|
||||||
|
style.map(
|
||||||
|
"CustomResult.Treeview.Heading",
|
||||||
|
background=[("active", "#3b3b3b")],
|
||||||
|
)
|
||||||
|
|
||||||
|
columns = (
|
||||||
|
"Pattern",
|
||||||
|
"No.",
|
||||||
|
"X",
|
||||||
|
"Y",
|
||||||
|
"Z",
|
||||||
|
"x",
|
||||||
|
"y",
|
||||||
|
"Lv",
|
||||||
|
"u'",
|
||||||
|
"v'",
|
||||||
|
"Tcp",
|
||||||
|
"duv",
|
||||||
|
"λd/λc",
|
||||||
|
"Pe"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.custom_result_tree = ttk.Treeview(
|
||||||
|
table_container,
|
||||||
|
columns=columns,
|
||||||
|
show="headings",
|
||||||
|
height=4,
|
||||||
|
style="CustomResult.Treeview",
|
||||||
|
)
|
||||||
|
|
||||||
|
column_widths = {
|
||||||
|
"Pattern": 90,
|
||||||
|
"No.": 60,
|
||||||
|
"X": 80,
|
||||||
|
"Y": 80,
|
||||||
|
"Z": 80,
|
||||||
|
"x": 80,
|
||||||
|
"y": 80,
|
||||||
|
"Lv": 80,
|
||||||
|
"u'": 80,
|
||||||
|
"v'": 80,
|
||||||
|
"Tcp": 90,
|
||||||
|
"duv": 80,
|
||||||
|
"λd/λc": 95,
|
||||||
|
"Pe": 80,
|
||||||
|
}
|
||||||
|
|
||||||
|
for col in columns:
|
||||||
|
self.custom_result_tree.heading(col, text=col)
|
||||||
|
self.custom_result_tree.column(
|
||||||
|
col,
|
||||||
|
width=column_widths.get(col, 80),
|
||||||
|
minwidth=60,
|
||||||
|
anchor=tk.CENTER,
|
||||||
|
stretch=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
y_scroll = ttk.Scrollbar(
|
||||||
|
table_container,
|
||||||
|
orient=tk.VERTICAL,
|
||||||
|
command=self.custom_result_tree.yview,
|
||||||
|
)
|
||||||
|
x_scroll = ttk.Scrollbar(
|
||||||
|
table_container,
|
||||||
|
orient=tk.HORIZONTAL,
|
||||||
|
command=self.custom_result_tree.xview,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.custom_result_tree.configure(
|
||||||
|
yscrollcommand=y_scroll.set,
|
||||||
|
xscrollcommand=x_scroll.set,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.custom_result_tree.grid(row=0, column=0, sticky="nsew")
|
||||||
|
y_scroll.grid(row=0, column=1, sticky="ns")
|
||||||
|
x_scroll.grid(row=1, column=0, sticky="ew")
|
||||||
|
|
||||||
|
# 右键菜单:复制全部数据(Excel 可直接按行列粘贴)
|
||||||
|
self.custom_result_menu = tk.Menu(self.root, tearoff=0)
|
||||||
|
self.custom_result_menu.add_command(
|
||||||
|
label="复制全部数据",
|
||||||
|
command=self.copy_custom_result_table,
|
||||||
|
)
|
||||||
|
self.custom_result_menu.add_command(
|
||||||
|
label="单步测试",
|
||||||
|
command=self.start_custom_row_single_step,
|
||||||
|
)
|
||||||
|
|
||||||
|
# self.custom_result_menu.add_separator()
|
||||||
|
# self.custom_result_menu.add_command(
|
||||||
|
# label="单步测试",
|
||||||
|
# command=self.fill_custom_result_test_data,
|
||||||
|
# )
|
||||||
|
self.custom_result_tree.bind("<Button-3>", self.show_custom_result_context_menu)
|
||||||
|
|
||||||
|
table_container.grid_rowconfigure(0, weight=1)
|
||||||
|
table_container.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
|
||||||
|
def show_custom_result_context_menu(self, event):
|
||||||
|
"""显示客户模板结果右键菜单"""
|
||||||
|
if not hasattr(self, "custom_result_tree") or not hasattr(
|
||||||
|
self, "custom_result_menu"
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.testing:
|
||||||
|
# 测试进行中锁定客户模板结果表,禁止右键菜单。
|
||||||
|
return
|
||||||
|
|
||||||
|
row_id = self.custom_result_tree.identify_row(event.y)
|
||||||
|
if row_id:
|
||||||
|
self.custom_result_tree.selection_set(row_id)
|
||||||
|
self.custom_result_tree.focus(row_id)
|
||||||
|
|
||||||
|
has_rows = len(self.custom_result_tree.get_children()) > 0
|
||||||
|
has_selection = len(self.custom_result_tree.selection()) > 0
|
||||||
|
can_single_step = (
|
||||||
|
has_selection
|
||||||
|
and self.ca is not None
|
||||||
|
and self.ucd is not None
|
||||||
|
and not self.testing
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self.custom_result_menu.entryconfigure(
|
||||||
|
0,
|
||||||
|
state=("normal" if has_rows else "disabled"),
|
||||||
|
)
|
||||||
|
self.custom_result_menu.entryconfigure(
|
||||||
|
1,
|
||||||
|
state=("normal" if can_single_step else "disabled"),
|
||||||
|
)
|
||||||
|
self.custom_result_menu.tk_popup(event.x_root, event.y_root)
|
||||||
|
finally:
|
||||||
|
self.custom_result_menu.grab_release()
|
||||||
|
|
||||||
|
|
||||||
|
def set_custom_result_table_locked(self, locked):
|
||||||
|
"""锁定/解锁客户模板结果表(测试期间禁选择、禁右键)"""
|
||||||
|
if not hasattr(self, "custom_result_tree"):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.custom_result_tree.configure(selectmode=("none" if locked else "browse"))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def start_custom_row_single_step(self):
|
||||||
|
"""单步测试当前选中行:发送该行 pattern 并覆盖该行测量结果"""
|
||||||
|
if not hasattr(self, "custom_result_tree"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.ca is None or self.ucd is None:
|
||||||
|
messagebox.showerror("错误", "请先连接CA410和信号发生器")
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.testing:
|
||||||
|
messagebox.showinfo("提示", "测试进行中,无法执行单步测试")
|
||||||
|
return
|
||||||
|
|
||||||
|
selected = self.custom_result_tree.selection()
|
||||||
|
if not selected:
|
||||||
|
messagebox.showinfo("提示", "请先选中一行再执行单步测试")
|
||||||
|
return
|
||||||
|
|
||||||
|
item_id = selected[0]
|
||||||
|
values = self.custom_result_tree.item(item_id, "values")
|
||||||
|
if not values:
|
||||||
|
messagebox.showinfo("提示", "选中行没有有效数据")
|
||||||
|
return
|
||||||
|
|
||||||
|
row_no = None
|
||||||
|
if len(values) > 1:
|
||||||
|
try:
|
||||||
|
row_no = int(float(values[1]))
|
||||||
|
except Exception:
|
||||||
|
row_no = None
|
||||||
|
|
||||||
|
if row_no is None or row_no <= 0:
|
||||||
|
children = list(self.custom_result_tree.get_children())
|
||||||
|
row_no = children.index(item_id) + 1 if item_id in children else 1
|
||||||
|
|
||||||
|
self._clear_custom_result_row(item_id, row_no)
|
||||||
|
|
||||||
|
threading.Thread(
|
||||||
|
target=self._run_custom_row_single_step,
|
||||||
|
args=(item_id, row_no),
|
||||||
|
daemon=True,
|
||||||
|
).start()
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_custom_result_row(self, item_id, row_no):
|
||||||
|
"""单步测试开始前清空指定行的测量数据"""
|
||||||
|
if not hasattr(self, "custom_result_tree"):
|
||||||
|
return
|
||||||
|
|
||||||
|
old_values = list(self.custom_result_tree.item(item_id, "values"))
|
||||||
|
pattern_name = old_values[0] if len(old_values) > 0 else f"P {row_no}"
|
||||||
|
|
||||||
|
cleared_values = (
|
||||||
|
pattern_name,
|
||||||
|
row_no,
|
||||||
|
"---",
|
||||||
|
"---",
|
||||||
|
"---",
|
||||||
|
"---",
|
||||||
|
"---",
|
||||||
|
"---",
|
||||||
|
"---",
|
||||||
|
"---",
|
||||||
|
"---",
|
||||||
|
"---",
|
||||||
|
"---",
|
||||||
|
"---",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.custom_result_tree.item(item_id, values=cleared_values)
|
||||||
|
self.custom_result_tree.see(item_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_custom_row_single_step(self, item_id, row_no):
|
||||||
|
"""后台执行客户模板单步测试"""
|
||||||
|
try:
|
||||||
|
self.root.after(0, lambda: self.status_var.set(f"单步测试第 {row_no} 行..."))
|
||||||
|
self.log_gui.log(f"开始单步测试第 {row_no} 行")
|
||||||
|
|
||||||
|
self.config.set_current_pattern("custom")
|
||||||
|
|
||||||
|
# 与批量 custom 测试保持一致:根据当前 SDR 配置转换 pattern 数据。
|
||||||
|
import copy
|
||||||
|
|
||||||
|
data_range = self.sdr_data_range_var.get()
|
||||||
|
original_params = copy.deepcopy(self.config.default_pattern_temp["pattern_params"])
|
||||||
|
converted_params = convert_pattern_params(
|
||||||
|
pattern_params=original_params,
|
||||||
|
data_range=data_range,
|
||||||
|
verbose=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
temp_config = self.config.get_temp_config_with_converted_params(
|
||||||
|
mode="custom",
|
||||||
|
converted_params=converted_params,
|
||||||
|
)
|
||||||
|
|
||||||
|
if row_no > len(converted_params):
|
||||||
|
self.log_gui.log(f"❌ 行号超出 pattern 范围: {row_no}/{len(converted_params)}")
|
||||||
|
self.root.after(0, lambda: self.status_var.set("单步测试失败:行号超范围"))
|
||||||
|
return
|
||||||
|
|
||||||
|
self.ucd.set_ucd_params(temp_config)
|
||||||
|
pattern_param = converted_params[row_no - 1]
|
||||||
|
self.ucd.set_pattern(self.ucd.current_pattern, pattern_param)
|
||||||
|
self.ucd.run()
|
||||||
|
|
||||||
|
time.sleep(self.pattern_settle_time)
|
||||||
|
|
||||||
|
# 测量:显示模式1读取 Tcp/duv/Lv,显示模式8读取 λd/Pe/Lv 与 XYZ。
|
||||||
|
self.ca.set_Display(1)
|
||||||
|
tcp, duv, lv, _, _, _ = self.ca.readAllDisplay()
|
||||||
|
|
||||||
|
self.ca.set_Display(8)
|
||||||
|
lambda_d, pe, lv, X, Y, Z = self.ca.readAllDisplay()
|
||||||
|
|
||||||
|
xy = colour.XYZ_to_xy(np.array([X, Y, Z]))
|
||||||
|
u_prime, v_prime, _ = colour.XYZ_to_CIE1976UCS(np.array([X, Y, Z]))
|
||||||
|
|
||||||
|
row_data = {
|
||||||
|
"X": X,
|
||||||
|
"Y": Y,
|
||||||
|
"Z": Z,
|
||||||
|
"x": xy[0],
|
||||||
|
"y": xy[1],
|
||||||
|
"Lv": lv,
|
||||||
|
"u_prime": u_prime,
|
||||||
|
"v_prime": v_prime,
|
||||||
|
"Tcp": tcp,
|
||||||
|
"duv": duv,
|
||||||
|
"lambda_d": lambda_d,
|
||||||
|
"Pe": pe,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.root.after(
|
||||||
|
0,
|
||||||
|
lambda: self._update_custom_result_row(item_id, row_no, row_data),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.log_gui.log(f"✓ 第 {row_no} 行单步测试完成并已覆盖")
|
||||||
|
self.root.after(0, lambda: self.status_var.set(f"第 {row_no} 行单步测试完成"))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log_gui.log(f"❌ 单步测试失败: {str(e)}")
|
||||||
|
self.root.after(0, lambda: self.status_var.set("单步测试失败"))
|
||||||
|
|
||||||
|
|
||||||
|
def _update_custom_result_row(self, item_id, row_no, result_data):
|
||||||
|
"""覆盖更新客户模板结果表中指定行"""
|
||||||
|
|
||||||
|
def fmt(value, digits=4):
|
||||||
|
if value is None:
|
||||||
|
return "--"
|
||||||
|
if isinstance(value, (int, float, np.floating)):
|
||||||
|
# CA 返回异常哨兵值(如 -99999999)时,显示为占位符。
|
||||||
|
if (not np.isfinite(value)) or value <= -99999998:
|
||||||
|
return "---"
|
||||||
|
return f"{value:.{digits}f}"
|
||||||
|
try:
|
||||||
|
numeric_value = float(value)
|
||||||
|
if (not np.isfinite(numeric_value)) or numeric_value <= -99999998:
|
||||||
|
return "---"
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
old_values = list(self.custom_result_tree.item(item_id, "values"))
|
||||||
|
pattern_name = old_values[0] if len(old_values) > 0 else f"P {row_no}"
|
||||||
|
|
||||||
|
new_values = (
|
||||||
|
pattern_name,
|
||||||
|
row_no,
|
||||||
|
fmt(result_data.get("X")),
|
||||||
|
fmt(result_data.get("Y")),
|
||||||
|
fmt(result_data.get("Z")),
|
||||||
|
fmt(result_data.get("x")),
|
||||||
|
fmt(result_data.get("y")),
|
||||||
|
fmt(result_data.get("Lv"), 3),
|
||||||
|
fmt(result_data.get("u_prime")),
|
||||||
|
fmt(result_data.get("v_prime")),
|
||||||
|
fmt(result_data.get("Tcp"), 1),
|
||||||
|
fmt(result_data.get("duv"), 5),
|
||||||
|
fmt(result_data.get("lambda_d"), 1),
|
||||||
|
fmt(result_data.get("Pe"), 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.custom_result_tree.item(item_id, values=new_values)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_custom_result_table(self):
|
||||||
|
"""复制客户模板结果表格到剪贴板(不含标题行/No./Pattern)"""
|
||||||
|
if not hasattr(self, "custom_result_tree"):
|
||||||
|
return
|
||||||
|
|
||||||
|
items = self.custom_result_tree.get_children()
|
||||||
|
if not items:
|
||||||
|
messagebox.showinfo("提示", "当前没有可复制的数据")
|
||||||
|
return
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
columns = tuple(self.custom_result_tree["columns"])
|
||||||
|
excluded_col_indexes = {
|
||||||
|
idx
|
||||||
|
for idx, col_name in enumerate(columns)
|
||||||
|
if col_name in ("No.", "Pattern")
|
||||||
|
}
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
values = self.custom_result_tree.item(item, "values")
|
||||||
|
# 跳过 No. 和 Pattern 两列,只保留测量数据列。
|
||||||
|
data_values = [
|
||||||
|
v for idx, v in enumerate(values) if idx not in excluded_col_indexes
|
||||||
|
]
|
||||||
|
row = [
|
||||||
|
str(v).replace("\t", " ").replace("\n", " ")
|
||||||
|
for v in data_values
|
||||||
|
]
|
||||||
|
lines.append("\t".join(row))
|
||||||
|
|
||||||
|
clipboard_text = "\n".join(lines)
|
||||||
|
self.root.clipboard_clear()
|
||||||
|
self.root.clipboard_append(clipboard_text)
|
||||||
|
self.root.update_idletasks()
|
||||||
|
|
||||||
|
if hasattr(self, "status_var"):
|
||||||
|
self.status_var.set(f"已复制 {len(items)} 行客户模板数据到剪贴板")
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log(f"✓ 已复制客户模板表格数据({len(items)} 行)")
|
||||||
|
|
||||||
|
|
||||||
|
def fill_custom_result_test_data(self):
|
||||||
|
"""填充 147 行客户模板测试数据(用于界面验证)"""
|
||||||
|
if not hasattr(self, "custom_result_tree"):
|
||||||
|
return
|
||||||
|
|
||||||
|
self.clear_custom_template_results()
|
||||||
|
|
||||||
|
pattern_names = []
|
||||||
|
if hasattr(self, "config") and hasattr(self.config, "get_temp_pattern_names"):
|
||||||
|
pattern_names = self.config.get_temp_pattern_names()
|
||||||
|
|
||||||
|
total_rows = 147
|
||||||
|
for i in range(1, total_rows + 1):
|
||||||
|
ratio = (i - 1) / (total_rows - 1) if total_rows > 1 else 0
|
||||||
|
row_data = {
|
||||||
|
"pattern_name": (
|
||||||
|
pattern_names[i - 1] if i - 1 < len(pattern_names) else f"P {i}"
|
||||||
|
),
|
||||||
|
"X": 0.8 + ratio * 120,
|
||||||
|
"Y": 0.9 + ratio * 135,
|
||||||
|
"Z": 1.1 + ratio * 145,
|
||||||
|
"x": 0.24 + ratio * 0.10,
|
||||||
|
"y": 0.26 + ratio * 0.10,
|
||||||
|
"Lv": 1.0 + ratio * 500,
|
||||||
|
"u_prime": 0.16 + ratio * 0.12,
|
||||||
|
"v_prime": 0.42 + ratio * 0.08,
|
||||||
|
"Tcp": 1800 + ratio * 12000,
|
||||||
|
"duv": -0.01 + ratio * 0.03,
|
||||||
|
"lambda_d": 430 + ratio * 200,
|
||||||
|
"Pe": 10 + ratio * 90,
|
||||||
|
}
|
||||||
|
self.append_custom_template_result(i, row_data)
|
||||||
|
|
||||||
|
if hasattr(self, "chart_notebook") and hasattr(self, "custom_template_tab_frame"):
|
||||||
|
self.chart_notebook.select(self.custom_template_tab_frame)
|
||||||
|
|
||||||
|
if hasattr(self, "status_var"):
|
||||||
|
self.status_var.set("已填充 147 行客户模板测试数据")
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log("✓ 已填充 147 行客户模板测试数据")
|
||||||
|
|
||||||
|
|
||||||
|
def clear_custom_template_results(self):
|
||||||
|
"""清空客户模板结果表格"""
|
||||||
|
if not hasattr(self, "custom_result_tree"):
|
||||||
|
return
|
||||||
|
for item in self.custom_result_tree.get_children():
|
||||||
|
self.custom_result_tree.delete(item)
|
||||||
|
|
||||||
|
|
||||||
|
def auto_expand_custom_result_view(self):
|
||||||
|
"""当客户模板表格有数据时,自动扩展窗口以尽量完整显示所有列"""
|
||||||
|
if not hasattr(self, "custom_result_tree"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(self.custom_result_tree.get_children()) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.root.update_idletasks()
|
||||||
|
|
||||||
|
columns = tuple(self.custom_result_tree["columns"])
|
||||||
|
columns_total_width = 0
|
||||||
|
for col in columns:
|
||||||
|
columns_total_width += int(self.custom_result_tree.column(col, "width"))
|
||||||
|
|
||||||
|
left_panel_width = self.left_frame.winfo_width() if hasattr(self, "left_frame") else 180
|
||||||
|
if left_panel_width <= 1:
|
||||||
|
left_panel_width = 180
|
||||||
|
|
||||||
|
# 列宽 + 左侧导航 + 滚动条/边框/外边距。
|
||||||
|
target_width = int(left_panel_width + columns_total_width + 120)
|
||||||
|
|
||||||
|
screen_max_width = max(900, self.root.winfo_screenwidth() - 40)
|
||||||
|
target_width = min(target_width, screen_max_width)
|
||||||
|
|
||||||
|
current_width = self.root.winfo_width()
|
||||||
|
current_height = self.root.winfo_height()
|
||||||
|
|
||||||
|
# 只扩不缩,避免用户窗口被反复改变。
|
||||||
|
if target_width > current_width:
|
||||||
|
self.root.geometry(f"{target_width}x{current_height}")
|
||||||
|
self.root.update_idletasks()
|
||||||
|
except Exception as e:
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log(f"⚠️ 自动扩展客户模板窗口失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def append_custom_template_result(self, row_no, result_data):
|
||||||
|
"""追加一条客户模板结果到表格"""
|
||||||
|
|
||||||
|
def fmt(value, digits=4):
|
||||||
|
if value is None:
|
||||||
|
return "--"
|
||||||
|
if isinstance(value, (int, float, np.floating)):
|
||||||
|
# CA 返回异常哨兵值(如 -99999999)时,显示为占位符。
|
||||||
|
if (not np.isfinite(value)) or value <= -99999998:
|
||||||
|
return "---"
|
||||||
|
return f"{value:.{digits}f}"
|
||||||
|
try:
|
||||||
|
numeric_value = float(value)
|
||||||
|
if (not np.isfinite(numeric_value)) or numeric_value <= -99999998:
|
||||||
|
return "---"
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
row_values = (
|
||||||
|
result_data.get("pattern_name", f"P {row_no}"),
|
||||||
|
row_no,
|
||||||
|
fmt(result_data.get("X")),
|
||||||
|
fmt(result_data.get("Y")),
|
||||||
|
fmt(result_data.get("Z")),
|
||||||
|
fmt(result_data.get("x")),
|
||||||
|
fmt(result_data.get("y")),
|
||||||
|
fmt(result_data.get("Lv"), 3),
|
||||||
|
fmt(result_data.get("u_prime")),
|
||||||
|
fmt(result_data.get("v_prime")),
|
||||||
|
fmt(result_data.get("Tcp"), 1),
|
||||||
|
fmt(result_data.get("duv"), 5),
|
||||||
|
fmt(result_data.get("lambda_d"), 1),
|
||||||
|
fmt(result_data.get("Pe"), 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
if hasattr(self, "custom_result_tree"):
|
||||||
|
item_id = self.custom_result_tree.insert("", tk.END, values=row_values)
|
||||||
|
# 新增数据后自动跳转到最新行。
|
||||||
|
self.custom_result_tree.see(item_id)
|
||||||
|
self.auto_expand_custom_result_view()
|
||||||
|
|
||||||
|
|
||||||
|
def start_custom_template_test(self):
|
||||||
|
"""开始客户模板测试(SDR)"""
|
||||||
|
|
||||||
|
if hasattr(self, "chart_notebook") and hasattr(self, "custom_template_tab_frame"):
|
||||||
|
self.chart_notebook.select(self.custom_template_tab_frame)
|
||||||
|
|
||||||
|
if self.ca is None or self.ucd is None:
|
||||||
|
messagebox.showerror("错误", "请先连接CA410和信号发生器")
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.testing:
|
||||||
|
messagebox.showinfo("提示", "测试已在进行中")
|
||||||
|
return
|
||||||
|
|
||||||
|
if hasattr(self, "debug_container"):
|
||||||
|
self.debug_container.pack_forget()
|
||||||
|
|
||||||
|
self.testing = True
|
||||||
|
self.start_btn.config(state=tk.DISABLED)
|
||||||
|
self.stop_btn.config(state=tk.NORMAL)
|
||||||
|
self.save_btn.config(state=tk.DISABLED)
|
||||||
|
self.clear_config_btn.config(state=tk.DISABLED)
|
||||||
|
self.custom_btn.config(state=tk.DISABLED)
|
||||||
|
self.status_var.set("客户模板测试进行中...")
|
||||||
|
|
||||||
|
self.log_gui.clear_log()
|
||||||
|
self.clear_custom_template_results()
|
||||||
|
|
||||||
|
confirm = messagebox.askyesno(
|
||||||
|
"确认测试", "开始客户模板测试(SDR)?\n\n将采集并显示客户模板格式结果。"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not confirm:
|
||||||
|
self.testing = False
|
||||||
|
self.start_btn.config(state=tk.NORMAL)
|
||||||
|
self.stop_btn.config(state=tk.DISABLED)
|
||||||
|
self.clear_config_btn.config(state=tk.NORMAL)
|
||||||
|
self.custom_btn.config(state=tk.NORMAL)
|
||||||
|
self.status_var.set("测试已取消")
|
||||||
|
self.set_custom_result_table_locked(False)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.set_custom_result_table_locked(True)
|
||||||
|
|
||||||
|
self.test_thread = threading.Thread(target=self.run_custom_sdr_test, args=([],))
|
||||||
|
self.test_thread.daemon = True
|
||||||
|
self.test_thread.start()
|
||||||
|
|
||||||
|
|
||||||
498
app/views/panels/main_layout.py
Normal file
498
app/views/panels/main_layout.py
Normal file
@@ -0,0 +1,498 @@
|
|||||||
|
"""主布局面板创建函数(Step 6 重构)。"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
import ttkbootstrap as ttk
|
||||||
|
|
||||||
|
from drivers.UCD323_Enum import UCDEnum
|
||||||
|
from app.views.collapsing_frame import CollapsingFrame
|
||||||
|
from app.resources import load_icon
|
||||||
|
|
||||||
|
def create_floating_config_panel(self):
|
||||||
|
"""创建右上角悬浮配置框"""
|
||||||
|
cf = CollapsingFrame(self.control_frame_top)
|
||||||
|
cf.pack(fill="both")
|
||||||
|
# 创建悬浮框主容器
|
||||||
|
self.config_panel_frame = ttk.Frame(cf)
|
||||||
|
cf.add(self.config_panel_frame, title="配置项")
|
||||||
|
|
||||||
|
# 创建一个统一的frame来替代选项卡控件
|
||||||
|
self.config_content_frame = ttk.Frame(self.config_panel_frame)
|
||||||
|
self.config_content_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
# 创建一个横向排列的Frame
|
||||||
|
config_row_frame = ttk.Frame(self.config_content_frame)
|
||||||
|
config_row_frame.pack(fill=tk.X, expand=False, padx=5, pady=5)
|
||||||
|
|
||||||
|
# 创建连接内容区域
|
||||||
|
self.connection_frame = ttk.LabelFrame(config_row_frame, text="设备连接")
|
||||||
|
self.connection_frame.pack(
|
||||||
|
side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建测试项目区域
|
||||||
|
self.test_items_frame = ttk.LabelFrame(config_row_frame, text="测试项目")
|
||||||
|
self.test_items_frame.pack(
|
||||||
|
side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建信号格式区域
|
||||||
|
self.signal_format_frame = ttk.LabelFrame(config_row_frame, text="信号格式")
|
||||||
|
self.signal_format_frame.pack(
|
||||||
|
side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建连接内容
|
||||||
|
self.create_connection_content()
|
||||||
|
# 创建测试项目内容
|
||||||
|
self.create_test_items_content()
|
||||||
|
# 创建信号格式内容
|
||||||
|
self.create_signal_format_content()
|
||||||
|
|
||||||
|
self.config_panel_frame.grid_remove()
|
||||||
|
self.config_panel_frame.btn.configure(image="closed")
|
||||||
|
|
||||||
|
|
||||||
|
def create_test_items_content(self):
|
||||||
|
"""创建测试项目选项卡内容"""
|
||||||
|
# 创建测试项目字典,用于管理不同测试类型的选项
|
||||||
|
self.test_items = {
|
||||||
|
"screen_module": {
|
||||||
|
"frame": ttk.Frame(self.test_items_frame),
|
||||||
|
"items": [
|
||||||
|
("色域", "gamut"),
|
||||||
|
("Gamma", "gamma"),
|
||||||
|
("色度", "cct"),
|
||||||
|
("对比度", "contrast"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"sdr_movie": {
|
||||||
|
"frame": ttk.Frame(self.test_items_frame),
|
||||||
|
"items": [
|
||||||
|
("色域", "gamut"),
|
||||||
|
("Gamma", "gamma"),
|
||||||
|
("色度", "cct"),
|
||||||
|
("对比度", "contrast"),
|
||||||
|
("色准", "accuracy"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"hdr_movie": {
|
||||||
|
"frame": ttk.Frame(self.test_items_frame),
|
||||||
|
"items": [
|
||||||
|
("色域", "gamut"),
|
||||||
|
("EOTF", "eotf"),
|
||||||
|
("色度", "cct"),
|
||||||
|
("对比度", "contrast"),
|
||||||
|
("色准", "accuracy"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# 根据当前测试类型创建复选框
|
||||||
|
self.test_vars = {}
|
||||||
|
self.update_test_items()
|
||||||
|
|
||||||
|
# 创建色度参数设置框架
|
||||||
|
self.create_cct_params_frame()
|
||||||
|
|
||||||
|
|
||||||
|
def create_signal_format_content(self):
|
||||||
|
"""创建信号格式选项卡内容"""
|
||||||
|
self.signal_tabs = ttk.Notebook(self.signal_format_frame)
|
||||||
|
self.signal_tabs.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# ==================== 屏模组格式设置 ====================
|
||||||
|
self.screen_module_signal_frame = ttk.Frame(self.signal_tabs)
|
||||||
|
self.screen_module_signal_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
self.signal_tabs.add(self.screen_module_signal_frame, text="屏模组测试")
|
||||||
|
|
||||||
|
self.screen_module_timing_var = tk.StringVar(
|
||||||
|
value=self.config.current_test_types[self.config.current_test_type][
|
||||||
|
"timing"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
screen_module_timing_combo = ttk.Combobox(
|
||||||
|
self.screen_module_signal_frame,
|
||||||
|
textvariable=self.screen_module_timing_var,
|
||||||
|
values=UCDEnum.TimingInfo.get_formatted_resolution_list(),
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
screen_module_timing_combo.bind(
|
||||||
|
"<<ComboboxSelected>>", self.on_screen_module_timing_changed
|
||||||
|
)
|
||||||
|
screen_module_timing_combo.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
|
||||||
|
|
||||||
|
# ==================== SDR信号格式设置 ====================
|
||||||
|
self.sdr_signal_frame = ttk.Frame(self.signal_tabs)
|
||||||
|
# 配置列权重
|
||||||
|
self.sdr_signal_frame.grid_columnconfigure(0, weight=0)
|
||||||
|
self.sdr_signal_frame.grid_columnconfigure(1, weight=1)
|
||||||
|
self.signal_tabs.add(self.sdr_signal_frame, text="SDR测试")
|
||||||
|
|
||||||
|
# 色彩空间
|
||||||
|
ttk.Label(self.sdr_signal_frame, text="色彩空间:").grid(
|
||||||
|
row=0, column=0, sticky=tk.W, padx=5, pady=2
|
||||||
|
)
|
||||||
|
self.sdr_color_space_var = tk.StringVar(value="BT.709")
|
||||||
|
sdr_color_space_combo = ttk.Combobox(
|
||||||
|
self.sdr_signal_frame,
|
||||||
|
textvariable=self.sdr_color_space_var,
|
||||||
|
values=["BT.709", "BT.601", "BT.2020"],
|
||||||
|
width=10,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
sdr_color_space_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
|
||||||
|
|
||||||
|
# Gamma
|
||||||
|
ttk.Label(self.sdr_signal_frame, text="Gamma:").grid(
|
||||||
|
row=1, column=0, sticky=tk.W, padx=5, pady=2
|
||||||
|
)
|
||||||
|
self.sdr_gamma_type_var = tk.StringVar(value="2.2")
|
||||||
|
sdr_gamma_combo = ttk.Combobox(
|
||||||
|
self.sdr_signal_frame,
|
||||||
|
textvariable=self.sdr_gamma_type_var,
|
||||||
|
values=["2.2", "2.4", "2.6"],
|
||||||
|
width=10,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
sdr_gamma_combo.grid(row=1, column=1, sticky=tk.W, padx=5, pady=2)
|
||||||
|
|
||||||
|
# 数据范围
|
||||||
|
ttk.Label(self.sdr_signal_frame, text="数据范围:").grid(
|
||||||
|
row=2, column=0, sticky=tk.W, padx=5, pady=2
|
||||||
|
)
|
||||||
|
self.sdr_data_range_var = tk.StringVar(value="Full")
|
||||||
|
sdr_range_combo = ttk.Combobox(
|
||||||
|
self.sdr_signal_frame,
|
||||||
|
textvariable=self.sdr_data_range_var,
|
||||||
|
values=["Full", "Limited"],
|
||||||
|
width=10,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
sdr_range_combo.grid(row=2, column=1, sticky=tk.W, padx=5, pady=2)
|
||||||
|
|
||||||
|
# 编码位深
|
||||||
|
ttk.Label(self.sdr_signal_frame, text="编码位深:").grid(
|
||||||
|
row=3, column=0, sticky=tk.W, padx=5, pady=2
|
||||||
|
)
|
||||||
|
self.sdr_bit_depth_var = tk.StringVar(value="8bit")
|
||||||
|
sdr_bit_depth_combo = ttk.Combobox(
|
||||||
|
self.sdr_signal_frame,
|
||||||
|
textvariable=self.sdr_bit_depth_var,
|
||||||
|
values=["8bit", "10bit", "12bit"],
|
||||||
|
width=10,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
sdr_bit_depth_combo.grid(row=3, column=1, sticky=tk.W, padx=5, pady=2)
|
||||||
|
|
||||||
|
# ==================== HDR信号格式设置 ====================
|
||||||
|
self.hdr_signal_frame = ttk.Frame(self.signal_tabs)
|
||||||
|
# 配置列权重
|
||||||
|
self.hdr_signal_frame.grid_columnconfigure(0, weight=0)
|
||||||
|
self.hdr_signal_frame.grid_columnconfigure(1, weight=1)
|
||||||
|
self.signal_tabs.add(self.hdr_signal_frame, text="HDR")
|
||||||
|
|
||||||
|
# 色彩空间
|
||||||
|
ttk.Label(self.hdr_signal_frame, text="色彩空间:").grid(
|
||||||
|
row=0, column=0, sticky=tk.W, padx=5, pady=2
|
||||||
|
)
|
||||||
|
self.hdr_color_space_var = tk.StringVar(value="BT.2020")
|
||||||
|
hdr_color_space_combo = ttk.Combobox(
|
||||||
|
self.hdr_signal_frame,
|
||||||
|
textvariable=self.hdr_color_space_var,
|
||||||
|
values=["BT.2020", "DCI-P3"],
|
||||||
|
width=10,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
hdr_color_space_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
|
||||||
|
|
||||||
|
# Metadata设置
|
||||||
|
ttk.Label(self.hdr_signal_frame, text="Metadata:").grid(
|
||||||
|
row=1, column=0, sticky=tk.W, padx=5, pady=2
|
||||||
|
)
|
||||||
|
self.hdr_metadata_frame = ttk.Frame(self.hdr_signal_frame)
|
||||||
|
self.hdr_metadata_frame.grid(
|
||||||
|
row=1, column=1, rowspan=2, sticky=tk.W, padx=5, pady=2
|
||||||
|
)
|
||||||
|
|
||||||
|
ttk.Label(self.hdr_metadata_frame, text="MaxCLL:").grid(
|
||||||
|
row=0, column=0, sticky=tk.W
|
||||||
|
)
|
||||||
|
self.hdr_maxcll_var = tk.StringVar(value="1000")
|
||||||
|
ttk.Entry(
|
||||||
|
self.hdr_metadata_frame, textvariable=self.hdr_maxcll_var, width=6
|
||||||
|
).grid(row=0, column=1, padx=2)
|
||||||
|
|
||||||
|
ttk.Label(self.hdr_metadata_frame, text="MaxFALL:").grid(
|
||||||
|
row=1, column=0, sticky=tk.W
|
||||||
|
)
|
||||||
|
self.hdr_maxfall_var = tk.StringVar(value="400")
|
||||||
|
ttk.Entry(
|
||||||
|
self.hdr_metadata_frame, textvariable=self.hdr_maxfall_var, width=6
|
||||||
|
).grid(row=1, column=1, padx=2)
|
||||||
|
|
||||||
|
# 数据范围
|
||||||
|
ttk.Label(self.hdr_signal_frame, text="数据范围:").grid(
|
||||||
|
row=3, column=0, sticky=tk.W, padx=5, pady=2
|
||||||
|
)
|
||||||
|
self.hdr_data_range_var = tk.StringVar(value="Full")
|
||||||
|
hdr_range_combo = ttk.Combobox(
|
||||||
|
self.hdr_signal_frame,
|
||||||
|
textvariable=self.hdr_data_range_var,
|
||||||
|
values=["Full", "Limited"],
|
||||||
|
width=10,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
hdr_range_combo.grid(row=3, column=1, sticky=tk.W, padx=5, pady=2)
|
||||||
|
|
||||||
|
# 编码位深
|
||||||
|
ttk.Label(self.hdr_signal_frame, text="编码位深:").grid(
|
||||||
|
row=4, column=0, sticky=tk.W, padx=5, pady=2
|
||||||
|
)
|
||||||
|
self.hdr_bit_depth_var = tk.StringVar(value="8bit")
|
||||||
|
hdr_bit_depth_combo = ttk.Combobox(
|
||||||
|
self.hdr_signal_frame,
|
||||||
|
textvariable=self.hdr_bit_depth_var,
|
||||||
|
values=["8bit", "10bit", "12bit"],
|
||||||
|
width=10,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
hdr_bit_depth_combo.grid(row=4, column=1, sticky=tk.W, padx=5, pady=2)
|
||||||
|
|
||||||
|
# ==================== 初始化:默认只启用屏模组 Tab ====================
|
||||||
|
self.signal_tabs.select(0) # 选中屏模组
|
||||||
|
self.signal_tabs.tab(1, state="disabled") # 禁用 SDR
|
||||||
|
self.signal_tabs.tab(2, state="disabled") # 禁用 HDR
|
||||||
|
|
||||||
|
|
||||||
|
def create_connection_content(self):
|
||||||
|
"""创建设备连接区域"""
|
||||||
|
# 创建设备连接区域的主框架
|
||||||
|
com_frame = ttk.Frame(self.connection_frame)
|
||||||
|
com_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
|
# 获取可用的COM端口列表
|
||||||
|
available_ports = self.get_available_com_ports()
|
||||||
|
|
||||||
|
# 使用网格布局,更整齐
|
||||||
|
ttk.Label(com_frame, text="UCD列表:").grid(
|
||||||
|
row=0, column=0, sticky=ttk.W, padx=5, pady=3
|
||||||
|
)
|
||||||
|
self.ucd_list_var = tk.StringVar(value=self.config.device_config["ucd_list"])
|
||||||
|
self.ucd_list_combo = ttk.Combobox(
|
||||||
|
com_frame,
|
||||||
|
textvariable=self.ucd_list_var,
|
||||||
|
values=available_ports,
|
||||||
|
width=10,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
self.ucd_list_combo.grid(row=0, column=1, sticky=ttk.W, padx=5, pady=3)
|
||||||
|
self.ucd_list_combo.bind("<<ComboboxSelected>>", self.update_config)
|
||||||
|
|
||||||
|
# 添加UCD连接状态指示器
|
||||||
|
self.ucd_status_indicator = tk.Canvas(
|
||||||
|
com_frame, width=15, height=15, bg="gray", highlightthickness=0
|
||||||
|
)
|
||||||
|
self.ucd_status_indicator.grid(row=0, column=2, padx=(10, 20))
|
||||||
|
self.ucd_status_indicator.config(bg="gray")
|
||||||
|
|
||||||
|
# 添加按钮框架
|
||||||
|
button_frame = ttk.Frame(com_frame)
|
||||||
|
button_frame.grid(row=3, column=0, columnspan=3, pady=3, sticky="w")
|
||||||
|
|
||||||
|
connect_icon = load_icon("assets/connect-svgrepo-com.png")
|
||||||
|
self.check_button = ttk.Button(
|
||||||
|
button_frame,
|
||||||
|
image=connect_icon,
|
||||||
|
bootstyle="link",
|
||||||
|
takefocus=False,
|
||||||
|
command=self.check_com_connections,
|
||||||
|
)
|
||||||
|
self.check_button.image = connect_icon
|
||||||
|
self.check_button.pack(side="left", padx=0, pady=3)
|
||||||
|
|
||||||
|
disconnect_icon = load_icon("assets/disconnect-svgrepo-com.png")
|
||||||
|
# 断开连接按钮
|
||||||
|
self.disconnect_button = ttk.Button(
|
||||||
|
button_frame,
|
||||||
|
image=disconnect_icon,
|
||||||
|
bootstyle="link",
|
||||||
|
takefocus=False,
|
||||||
|
command=self.disconnect_com_connections,
|
||||||
|
)
|
||||||
|
self.disconnect_button.image = disconnect_icon # 防止图标被垃圾回收
|
||||||
|
self.disconnect_button.pack(side="left", padx=0, pady=3)
|
||||||
|
|
||||||
|
refresh_icon = load_icon("assets/refresh-svgrepo-com.png")
|
||||||
|
self.refresh_button = ttk.Button(
|
||||||
|
button_frame,
|
||||||
|
image=refresh_icon,
|
||||||
|
bootstyle="link",
|
||||||
|
takefocus=False,
|
||||||
|
command=self.refresh_com_ports,
|
||||||
|
)
|
||||||
|
self.refresh_button.image = refresh_icon # 防止图标被垃圾回收
|
||||||
|
self.refresh_button.pack(side="left", padx=0, pady=3)
|
||||||
|
|
||||||
|
# CA端口
|
||||||
|
ttk.Label(com_frame, text="CA端口:").grid(
|
||||||
|
row=1, column=0, sticky=ttk.W, padx=5, pady=3
|
||||||
|
)
|
||||||
|
self.ca_com_var = tk.StringVar(value=self.config.device_config["ca_com"])
|
||||||
|
self.ca_com_combo = ttk.Combobox(
|
||||||
|
com_frame,
|
||||||
|
textvariable=self.ca_com_var,
|
||||||
|
values=available_ports,
|
||||||
|
width=10,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
self.ca_com_combo.grid(row=1, column=1, sticky=ttk.W, padx=5, pady=3)
|
||||||
|
self.ca_com_combo.bind("<<ComboboxSelected>>", self.update_config)
|
||||||
|
|
||||||
|
# 添加CA连接状态指示器
|
||||||
|
self.ca_status_indicator = tk.Canvas(
|
||||||
|
com_frame, width=15, height=15, bg="gray", highlightthickness=0
|
||||||
|
)
|
||||||
|
self.ca_status_indicator.grid(row=1, column=2, padx=(10, 20))
|
||||||
|
self.ca_status_indicator.config(bg="gray")
|
||||||
|
|
||||||
|
# 添加CA通道设置
|
||||||
|
ttk.Label(com_frame, text="CA通道:").grid(
|
||||||
|
row=2, column=0, sticky=tk.W, padx=5, pady=3
|
||||||
|
)
|
||||||
|
self.ca_channel_var = tk.StringVar(
|
||||||
|
value=self.config.device_config["ca_channel"]
|
||||||
|
)
|
||||||
|
ca_channel_combo = ttk.Combobox(
|
||||||
|
com_frame,
|
||||||
|
textvariable=self.ca_channel_var,
|
||||||
|
values=[str(i) for i in range(11)],
|
||||||
|
width=10,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
ca_channel_combo.grid(row=2, column=1, sticky=ttk.W, padx=5, pady=3)
|
||||||
|
ca_channel_combo.bind("<<ComboboxSelected>>", self.update_config)
|
||||||
|
|
||||||
|
|
||||||
|
def create_test_type_frame(self):
|
||||||
|
"""创建测试类型选择区域(侧边栏形式)"""
|
||||||
|
# 设置测试类型变量
|
||||||
|
self.test_type_var = tk.StringVar(value="screen_module")
|
||||||
|
|
||||||
|
# 创建测试类型按钮并放置在侧边栏
|
||||||
|
test_types = [
|
||||||
|
("屏模组性能测试", "screen_module"),
|
||||||
|
("SDR Movie测试", "sdr_movie"),
|
||||||
|
("HDR Movie测试", "hdr_movie"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for text, type_value in test_types:
|
||||||
|
btn = ttk.Button(
|
||||||
|
master=self.sidebar_frame,
|
||||||
|
text=text,
|
||||||
|
style="Sidebar.TButton",
|
||||||
|
padding=10,
|
||||||
|
command=lambda v=type_value: self.change_test_type(v),
|
||||||
|
takefocus=False,
|
||||||
|
)
|
||||||
|
btn.pack(fill=tk.X, padx=0, pady=1)
|
||||||
|
|
||||||
|
# 保存按钮引用以便后续更新样式
|
||||||
|
setattr(self, f"{type_value}_btn", btn)
|
||||||
|
|
||||||
|
# 添加分隔线
|
||||||
|
ttk.Separator(self.sidebar_frame, orient="horizontal").pack(
|
||||||
|
fill=tk.X, padx=10, pady=10
|
||||||
|
)
|
||||||
|
|
||||||
|
# ✅ 只保留日志按钮
|
||||||
|
self.log_btn = ttk.Button(
|
||||||
|
self.sidebar_frame,
|
||||||
|
text="测试日志",
|
||||||
|
style="Sidebar.TButton",
|
||||||
|
command=self.toggle_log_panel,
|
||||||
|
takefocus=False,
|
||||||
|
)
|
||||||
|
self.log_btn.pack(fill=tk.X, padx=0, pady=1)
|
||||||
|
|
||||||
|
# Local Dimming 测试按钮
|
||||||
|
self.local_dimming_btn = ttk.Button(
|
||||||
|
self.sidebar_frame,
|
||||||
|
text="Local Dimming",
|
||||||
|
style="Sidebar.TButton",
|
||||||
|
command=self.toggle_local_dimming_panel,
|
||||||
|
takefocus=False,
|
||||||
|
)
|
||||||
|
self.local_dimming_btn.pack(fill=tk.X, padx=0, pady=1)
|
||||||
|
|
||||||
|
# 注册面板按钮(只保留日志)
|
||||||
|
if hasattr(self, "panels"):
|
||||||
|
if "log" in self.panels:
|
||||||
|
self.panels["log"]["button"] = self.log_btn
|
||||||
|
if "local_dimming" in self.panels:
|
||||||
|
self.panels["local_dimming"]["button"] = self.local_dimming_btn
|
||||||
|
|
||||||
|
|
||||||
|
def update_config_info_display(self):
|
||||||
|
"""更新配置信息显示"""
|
||||||
|
if hasattr(self, "config") and hasattr(self.config, "get_current_config"):
|
||||||
|
current_config = self.config.get_current_config()
|
||||||
|
|
||||||
|
info_text = f"测试类型: {current_config.get('name', '未知')}\n"
|
||||||
|
info_text += (
|
||||||
|
f"测试项目: {', '.join(current_config.get('test_items', []))}\n"
|
||||||
|
)
|
||||||
|
info_text += f"信号格式: {current_config.get('signal_format', 'none')}\n"
|
||||||
|
info_text += f"色彩空间: {current_config.get('color_space', 'unknown')}\n"
|
||||||
|
info_text += f"位深度: {current_config.get('bit_depth', 'unknown')}"
|
||||||
|
|
||||||
|
# 高亮当前选中的测试类型
|
||||||
|
self.update_sidebar_selection()
|
||||||
|
|
||||||
|
|
||||||
|
def create_operation_frame(self):
|
||||||
|
"""创建操作按钮区域"""
|
||||||
|
operation_frame = ttk.Frame(self.control_frame_top)
|
||||||
|
operation_frame.pack(fill=tk.X, padx=5, pady=10)
|
||||||
|
|
||||||
|
self.start_btn = ttk.Button(
|
||||||
|
operation_frame,
|
||||||
|
text="开始测试",
|
||||||
|
command=self.start_test,
|
||||||
|
style="success.TButton",
|
||||||
|
)
|
||||||
|
self.start_btn.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
self.stop_btn = ttk.Button(
|
||||||
|
operation_frame,
|
||||||
|
text="停止测试",
|
||||||
|
command=self.stop_test,
|
||||||
|
style="danger.TButton",
|
||||||
|
state=tk.DISABLED,
|
||||||
|
)
|
||||||
|
self.stop_btn.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
self.save_btn = ttk.Button(
|
||||||
|
operation_frame,
|
||||||
|
text="保存结果",
|
||||||
|
command=self.save_results,
|
||||||
|
state=tk.DISABLED,
|
||||||
|
)
|
||||||
|
self.save_btn.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
self.clear_config_btn = ttk.Button(
|
||||||
|
operation_frame,
|
||||||
|
text="清理配置",
|
||||||
|
command=self.clear_config_file,
|
||||||
|
)
|
||||||
|
self.clear_config_btn.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
self.custom_btn = ttk.Button(
|
||||||
|
operation_frame,
|
||||||
|
text="客户模版",
|
||||||
|
command=self.start_custom_template_test,
|
||||||
|
style="info.TButton",
|
||||||
|
)
|
||||||
|
self.custom_btn.pack(side=tk.LEFT, padx=5)
|
||||||
|
self.update_custom_button_visibility()
|
||||||
|
|
||||||
|
|
||||||
412
app/views/panels/side_panels.py
Normal file
412
app/views/panels/side_panels.py
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
"""侧边面板(日志 / Local Dimming / 调试)(Step 6 重构)。"""
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
import tkinter as tk
|
||||||
|
import ttkbootstrap as ttk
|
||||||
|
|
||||||
|
from app.views.pq_log_gui import PQLogGUI
|
||||||
|
from app.views.pq_debug_panel import PQDebugPanel
|
||||||
|
|
||||||
|
def create_log_panel(self):
|
||||||
|
"""创建日志面板"""
|
||||||
|
self.log_frame = ttk.Frame(self.content_frame)
|
||||||
|
self.log_gui = PQLogGUI(self.log_frame)
|
||||||
|
self.log_gui.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
|
|
||||||
|
# 默认隐藏日志面板
|
||||||
|
self.log_visible = False
|
||||||
|
|
||||||
|
# 注册到面板管理系统
|
||||||
|
self.register_panel(
|
||||||
|
"log", self.log_frame, None, "log_visible"
|
||||||
|
) # button会在后面设置
|
||||||
|
|
||||||
|
|
||||||
|
def create_local_dimming_panel(self):
|
||||||
|
"""创建 Local Dimming 测试面板 - 手动控制版"""
|
||||||
|
self.local_dimming_frame = ttk.Frame(self.content_frame)
|
||||||
|
|
||||||
|
# 主容器
|
||||||
|
main_container = ttk.Frame(self.local_dimming_frame, padding=10)
|
||||||
|
main_container.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# ==================== 1. 标题 ====================
|
||||||
|
title_frame = ttk.Frame(main_container)
|
||||||
|
title_frame.pack(fill=tk.X, pady=(0, 10))
|
||||||
|
|
||||||
|
ttk.Label(
|
||||||
|
title_frame,
|
||||||
|
text="🔆 Local Dimming 窗口测试",
|
||||||
|
font=("微软雅黑", 14, "bold"),
|
||||||
|
).pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# ==================== 2. 窗口百分比按钮 ====================
|
||||||
|
window_frame = ttk.LabelFrame(
|
||||||
|
main_container, text="🔆 窗口百分比(点击发送)", padding=10
|
||||||
|
)
|
||||||
|
window_frame.pack(fill=tk.X, pady=(0, 10))
|
||||||
|
|
||||||
|
# 说明文字
|
||||||
|
ttk.Label(
|
||||||
|
window_frame,
|
||||||
|
text="点击按钮发送对应百分比的白色窗口(黑色背景 + 居中白色矩形)",
|
||||||
|
font=("", 9),
|
||||||
|
foreground="#28a745",
|
||||||
|
).pack(pady=(0, 8))
|
||||||
|
|
||||||
|
# 第一行:1%, 2%, 5%, 10%, 18%
|
||||||
|
row1 = ttk.Frame(window_frame)
|
||||||
|
row1.pack(fill=tk.X, pady=(0, 5))
|
||||||
|
|
||||||
|
percentages_row1 = [1, 2, 5, 10, 18]
|
||||||
|
for p in percentages_row1:
|
||||||
|
ttk.Button(
|
||||||
|
row1,
|
||||||
|
text=f"{p}%",
|
||||||
|
command=lambda p=p: self.send_ld_window(p),
|
||||||
|
bootstyle="success",
|
||||||
|
width=12,
|
||||||
|
).pack(side=tk.LEFT, padx=3)
|
||||||
|
|
||||||
|
# 第二行:25%, 50%, 75%, 100%
|
||||||
|
row2 = ttk.Frame(window_frame)
|
||||||
|
row2.pack(fill=tk.X)
|
||||||
|
|
||||||
|
percentages_row2 = [25, 50, 75, 100]
|
||||||
|
for p in percentages_row2:
|
||||||
|
ttk.Button(
|
||||||
|
row2,
|
||||||
|
text=f"{p}%",
|
||||||
|
command=lambda p=p: self.send_ld_window(p),
|
||||||
|
bootstyle="success",
|
||||||
|
width=12,
|
||||||
|
).pack(side=tk.LEFT, padx=3)
|
||||||
|
|
||||||
|
# ==================== 4. CA410 采集按钮 ====================
|
||||||
|
measure_frame = ttk.LabelFrame(main_container, text="📊 CA410 测量", padding=10)
|
||||||
|
measure_frame.pack(fill=tk.X, pady=(0, 10))
|
||||||
|
|
||||||
|
measure_btn_frame = ttk.Frame(measure_frame)
|
||||||
|
measure_btn_frame.pack(fill=tk.X)
|
||||||
|
|
||||||
|
self.ld_measure_btn = ttk.Button(
|
||||||
|
measure_btn_frame,
|
||||||
|
text="📏 采集当前亮度",
|
||||||
|
command=self.measure_ld_luminance,
|
||||||
|
bootstyle="primary",
|
||||||
|
width=15,
|
||||||
|
)
|
||||||
|
self.ld_measure_btn.pack(side=tk.LEFT, padx=(0, 5))
|
||||||
|
|
||||||
|
# 显示测量结果
|
||||||
|
self.ld_result_label = ttk.Label(
|
||||||
|
measure_btn_frame,
|
||||||
|
text="亮度: -- cd/m² | x: -- | y: --",
|
||||||
|
font=("Consolas", 10),
|
||||||
|
foreground="#007bff",
|
||||||
|
)
|
||||||
|
self.ld_result_label.pack(side=tk.LEFT, padx=(10, 0))
|
||||||
|
|
||||||
|
# ==================== 5. 测试结果表格 ====================
|
||||||
|
result_frame = ttk.LabelFrame(main_container, text="📋 测试记录", padding=10)
|
||||||
|
result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
||||||
|
|
||||||
|
# Treeview
|
||||||
|
columns = ("窗口百分比", "亮度 (cd/m²)", "x", "y", "时间")
|
||||||
|
self.ld_tree = ttk.Treeview(
|
||||||
|
result_frame, columns=columns, show="headings", height=10
|
||||||
|
)
|
||||||
|
|
||||||
|
for col in columns:
|
||||||
|
self.ld_tree.heading(col, text=col)
|
||||||
|
if col == "窗口百分比":
|
||||||
|
self.ld_tree.column(col, width=100, anchor=tk.CENTER)
|
||||||
|
elif col == "时间":
|
||||||
|
self.ld_tree.column(col, width=120, anchor=tk.CENTER)
|
||||||
|
else:
|
||||||
|
self.ld_tree.column(col, width=100, anchor=tk.CENTER)
|
||||||
|
|
||||||
|
self.ld_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# 滚动条
|
||||||
|
scrollbar = ttk.Scrollbar(
|
||||||
|
result_frame, orient=tk.VERTICAL, command=self.ld_tree.yview
|
||||||
|
)
|
||||||
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
self.ld_tree.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
|
# ==================== 6. 底部操作按钮 ====================
|
||||||
|
bottom_frame = ttk.Frame(main_container)
|
||||||
|
bottom_frame.pack(fill=tk.X)
|
||||||
|
|
||||||
|
self.ld_clear_btn = ttk.Button(
|
||||||
|
bottom_frame,
|
||||||
|
text="🗑️ 清空记录",
|
||||||
|
command=self.clear_ld_records,
|
||||||
|
bootstyle="danger-outline",
|
||||||
|
width=12,
|
||||||
|
)
|
||||||
|
self.ld_clear_btn.pack(side=tk.LEFT, padx=(0, 5))
|
||||||
|
|
||||||
|
self.ld_save_btn = ttk.Button(
|
||||||
|
bottom_frame,
|
||||||
|
text="💾 保存结果",
|
||||||
|
command=self.save_local_dimming_results,
|
||||||
|
bootstyle="info",
|
||||||
|
width=12,
|
||||||
|
)
|
||||||
|
self.ld_save_btn.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# 默认隐藏
|
||||||
|
self.local_dimming_visible = False
|
||||||
|
|
||||||
|
# 注册到面板管理系统
|
||||||
|
self.register_panel(
|
||||||
|
"local_dimming",
|
||||||
|
self.local_dimming_frame,
|
||||||
|
None,
|
||||||
|
"local_dimming_visible",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 初始化当前窗口百分比(用于记录)
|
||||||
|
self.current_ld_percentage = None
|
||||||
|
|
||||||
|
|
||||||
|
def toggle_local_dimming_panel(self):
|
||||||
|
"""切换 Local Dimming 面板显示"""
|
||||||
|
self.show_panel("local_dimming")
|
||||||
|
|
||||||
|
|
||||||
|
def toggle_log_panel(self):
|
||||||
|
"""切换日志面板的显示状态"""
|
||||||
|
self.show_panel("log")
|
||||||
|
|
||||||
|
|
||||||
|
def toggle_screen_debug_panel(self):
|
||||||
|
"""打开/关闭屏模组单步调试面板(独立窗口)"""
|
||||||
|
# 如果窗口已存在且可见,关闭它
|
||||||
|
if hasattr(self, "debug_window") and self.debug_window.winfo_exists():
|
||||||
|
self.debug_window.destroy()
|
||||||
|
self.screen_debug_btn.config(text="打开调试面板")
|
||||||
|
self.log_gui.log("✓ 单步调试面板已关闭")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 创建新窗口
|
||||||
|
self.debug_window = ttk.Toplevel(self.root)
|
||||||
|
self.debug_window.title("🔧 单步调试面板")
|
||||||
|
self.debug_window.geometry("900x400")
|
||||||
|
self.debug_window.transient(self.root)
|
||||||
|
|
||||||
|
# 创建调试面板容器
|
||||||
|
debug_container = ttk.Frame(self.debug_window, padding=10)
|
||||||
|
debug_container.pack(fill=tk.BOTH, expand=True) # ← 这个 pack 是对的
|
||||||
|
|
||||||
|
# 创建调试面板实例
|
||||||
|
from app.views.pq_debug_panel import PQDebugPanel
|
||||||
|
|
||||||
|
debug_panel_instance = PQDebugPanel(debug_container, self)
|
||||||
|
# ← 这里不应该有任何 pack 调用!
|
||||||
|
|
||||||
|
self.log_gui.log("✓ 单步调试面板实例已创建")
|
||||||
|
|
||||||
|
# 重新启用调试(如果有数据)
|
||||||
|
try:
|
||||||
|
test_type = self.config.current_test_type
|
||||||
|
selected_items = self.get_selected_test_items()
|
||||||
|
|
||||||
|
if test_type == "screen_module":
|
||||||
|
if "gamma" in selected_items:
|
||||||
|
gray_data = self.results.get_intermediate_data("shared", "gray")
|
||||||
|
if gray_data:
|
||||||
|
self.log_gui.log(f" → 加载 {len(gray_data)} 个灰阶数据点")
|
||||||
|
debug_panel_instance.enable_debug(
|
||||||
|
"screen_module", "gamma", gray_data
|
||||||
|
)
|
||||||
|
self.log_gui.log("✓ 屏模组 Gamma 单步调试已重新启用")
|
||||||
|
else:
|
||||||
|
self.log_gui.log(" ✗ 没有可用的灰阶数据")
|
||||||
|
|
||||||
|
if "gamut" in selected_items:
|
||||||
|
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
|
||||||
|
if rgb_data:
|
||||||
|
self.log_gui.log(f" → 加载 {len(rgb_data)} 个RGB数据点")
|
||||||
|
debug_panel_instance.enable_debug(
|
||||||
|
"screen_module", "rgb", rgb_data
|
||||||
|
)
|
||||||
|
self.log_gui.log("✓ 屏模组 RGB 单步调试已重新启用")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log_gui.log(f"⚠️ 加载调试数据失败: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
self.log_gui.log(traceback.format_exc())
|
||||||
|
|
||||||
|
# 更新按钮文字
|
||||||
|
self.screen_debug_btn.config(text="关闭调试面板")
|
||||||
|
|
||||||
|
# 窗口关闭时的回调
|
||||||
|
def on_closing():
|
||||||
|
self.screen_debug_btn.config(text="打开调试面板")
|
||||||
|
self.debug_window.destroy()
|
||||||
|
self.log_gui.log("✓ 单步调试窗口已关闭")
|
||||||
|
|
||||||
|
self.debug_window.protocol("WM_DELETE_WINDOW", on_closing)
|
||||||
|
self.debug_window.update_idletasks()
|
||||||
|
|
||||||
|
self.log_gui.log("✓ 单步调试面板已打开(独立窗口)")
|
||||||
|
|
||||||
|
|
||||||
|
def toggle_sdr_debug_panel(self):
|
||||||
|
"""打开/关闭 SDR 单步调试面板(独立窗口)"""
|
||||||
|
# 如果窗口已存在且可见,关闭它
|
||||||
|
if hasattr(self, "sdr_debug_window") and self.sdr_debug_window.winfo_exists():
|
||||||
|
self.sdr_debug_window.destroy()
|
||||||
|
self.sdr_debug_btn.config(text="打开调试面板")
|
||||||
|
self.log_gui.log("✓ SDR 单步调试面板已关闭")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 创建新窗口
|
||||||
|
self.sdr_debug_window = ttk.Toplevel(self.root)
|
||||||
|
self.sdr_debug_window.title("🔧 SDR 单步调试面板")
|
||||||
|
self.sdr_debug_window.geometry("900x400")
|
||||||
|
self.sdr_debug_window.transient(self.root)
|
||||||
|
|
||||||
|
# 创建调试面板容器
|
||||||
|
debug_container = ttk.Frame(self.sdr_debug_window, padding=10)
|
||||||
|
debug_container.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# ✅ 创建调试面板实例(不要对它调用 pack)
|
||||||
|
from app.views.pq_debug_panel import PQDebugPanel
|
||||||
|
|
||||||
|
debug_panel_instance = PQDebugPanel(debug_container, self)
|
||||||
|
# ← 删除:debug_panel_instance.pack(...)
|
||||||
|
|
||||||
|
self.log_gui.log("✓ SDR 单步调试面板实例已创建")
|
||||||
|
|
||||||
|
# ✅ 重新启用调试(如果有数据)
|
||||||
|
try:
|
||||||
|
selected_items = self.get_selected_test_items()
|
||||||
|
|
||||||
|
if "gamma" in selected_items:
|
||||||
|
gray_data = self.results.get_intermediate_data("shared", "gray")
|
||||||
|
if gray_data:
|
||||||
|
self.log_gui.log(f" → 加载 {len(gray_data)} 个灰阶数据点")
|
||||||
|
debug_panel_instance.enable_debug("sdr_movie", "gamma", gray_data)
|
||||||
|
self.log_gui.log("✓ SDR Gamma 单步调试已重新启用")
|
||||||
|
|
||||||
|
if "accuracy" in selected_items:
|
||||||
|
accuracy_data = self.results.get_intermediate_data(
|
||||||
|
"accuracy", "measured"
|
||||||
|
)
|
||||||
|
if accuracy_data:
|
||||||
|
self.log_gui.log(f" → 加载 {len(accuracy_data)} 个色准数据点")
|
||||||
|
debug_panel_instance.enable_debug(
|
||||||
|
"sdr_movie", "accuracy", accuracy_data
|
||||||
|
)
|
||||||
|
self.log_gui.log("✓ SDR 色准单步调试已重新启用")
|
||||||
|
|
||||||
|
if "gamut" in selected_items:
|
||||||
|
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
|
||||||
|
if rgb_data:
|
||||||
|
self.log_gui.log(f" → 加载 {len(rgb_data)} 个RGB数据点")
|
||||||
|
debug_panel_instance.enable_debug("sdr_movie", "rgb", rgb_data)
|
||||||
|
self.log_gui.log("✓ SDR RGB 单步调试已重新启用")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log_gui.log(f"⚠️ 加载 SDR 调试数据失败: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
self.log_gui.log(traceback.format_exc())
|
||||||
|
|
||||||
|
# 更新按钮文字
|
||||||
|
self.sdr_debug_btn.config(text="关闭调试面板")
|
||||||
|
|
||||||
|
# 窗口关闭时的回调
|
||||||
|
def on_closing():
|
||||||
|
self.sdr_debug_btn.config(text="打开调试面板")
|
||||||
|
self.sdr_debug_window.destroy()
|
||||||
|
self.log_gui.log("✓ SDR 单步调试窗口已关闭")
|
||||||
|
|
||||||
|
self.sdr_debug_window.protocol("WM_DELETE_WINDOW", on_closing)
|
||||||
|
self.sdr_debug_window.update_idletasks()
|
||||||
|
|
||||||
|
self.log_gui.log("✓ SDR 单步调试面板已打开(独立窗口)")
|
||||||
|
|
||||||
|
|
||||||
|
def toggle_hdr_debug_panel(self):
|
||||||
|
"""打开/关闭 HDR 单步调试面板(独立窗口)"""
|
||||||
|
# 如果窗口已存在且可见,关闭它
|
||||||
|
if hasattr(self, "hdr_debug_window") and self.hdr_debug_window.winfo_exists():
|
||||||
|
self.hdr_debug_window.destroy()
|
||||||
|
self.hdr_debug_btn.config(text="打开调试面板")
|
||||||
|
self.log_gui.log("✓ HDR 单步调试面板已关闭")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 创建新窗口
|
||||||
|
self.hdr_debug_window = ttk.Toplevel(self.root)
|
||||||
|
self.hdr_debug_window.title("🔧 HDR 单步调试面板")
|
||||||
|
self.hdr_debug_window.geometry("900x400")
|
||||||
|
self.hdr_debug_window.transient(self.root)
|
||||||
|
|
||||||
|
# 创建调试面板容器
|
||||||
|
debug_container = ttk.Frame(self.hdr_debug_window, padding=10)
|
||||||
|
debug_container.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# ✅ 创建调试面板实例(不要对它调用 pack)
|
||||||
|
from app.views.pq_debug_panel import PQDebugPanel
|
||||||
|
|
||||||
|
debug_panel_instance = PQDebugPanel(debug_container, self)
|
||||||
|
# ← 删除:debug_panel_instance.pack(...)
|
||||||
|
|
||||||
|
self.log_gui.log("✓ HDR 单步调试面板实例已创建")
|
||||||
|
|
||||||
|
# ✅ 重新启用调试(如果有数据)
|
||||||
|
try:
|
||||||
|
selected_items = self.get_selected_test_items()
|
||||||
|
|
||||||
|
if "eotf" in selected_items:
|
||||||
|
gray_data = self.results.get_intermediate_data("shared", "gray")
|
||||||
|
if gray_data:
|
||||||
|
self.log_gui.log(f" → 加载 {len(gray_data)} 个灰阶数据点")
|
||||||
|
debug_panel_instance.enable_debug("hdr_movie", "eotf", gray_data)
|
||||||
|
self.log_gui.log("✓ HDR EOTF 单步调试已重新启用")
|
||||||
|
|
||||||
|
if "accuracy" in selected_items:
|
||||||
|
accuracy_data = self.results.get_intermediate_data(
|
||||||
|
"accuracy", "measured"
|
||||||
|
)
|
||||||
|
if accuracy_data:
|
||||||
|
self.log_gui.log(f" → 加载 {len(accuracy_data)} 个色准数据点")
|
||||||
|
debug_panel_instance.enable_debug(
|
||||||
|
"hdr_movie", "accuracy", accuracy_data
|
||||||
|
)
|
||||||
|
self.log_gui.log("✓ HDR 色准单步调试已重新启用")
|
||||||
|
|
||||||
|
if "gamut" in selected_items:
|
||||||
|
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
|
||||||
|
if rgb_data:
|
||||||
|
self.log_gui.log(f" → 加载 {len(rgb_data)} 个RGB数据点")
|
||||||
|
debug_panel_instance.enable_debug("hdr_movie", "rgb", rgb_data)
|
||||||
|
self.log_gui.log("✓ HDR RGB 单步调试已重新启用")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log_gui.log(f"⚠️ 加载 HDR 调试数据失败: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
self.log_gui.log(traceback.format_exc())
|
||||||
|
|
||||||
|
# 更新按钮文字
|
||||||
|
self.hdr_debug_btn.config(text="关闭调试面板")
|
||||||
|
|
||||||
|
# 窗口关闭时的回调
|
||||||
|
def on_closing():
|
||||||
|
self.hdr_debug_btn.config(text="打开调试面板")
|
||||||
|
self.hdr_debug_window.destroy()
|
||||||
|
self.log_gui.log("✓ HDR 单步调试窗口已关闭")
|
||||||
|
|
||||||
|
self.hdr_debug_window.protocol("WM_DELETE_WINDOW", on_closing)
|
||||||
|
self.hdr_debug_window.update_idletasks()
|
||||||
|
|
||||||
|
self.log_gui.log("✓ HDR 单步调试面板已打开(独立窗口)")
|
||||||
|
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import UniTAP
|
import UniTAP
|
||||||
import time
|
import time
|
||||||
import gc
|
import gc
|
||||||
from utils.UCD323_Enum import UCDEnum
|
from drivers.UCD323_Enum import UCDEnum
|
||||||
|
|
||||||
|
|
||||||
class UCDController:
|
class UCDController:
|
||||||
0
drivers/__init__.py
Normal file
0
drivers/__init__.py
Normal file
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from utils.baseSerail import BaseSerial
|
from drivers.baseSerail import BaseSerial
|
||||||
# from baseSerail import BaseSerial
|
# from baseSerail import BaseSerial
|
||||||
import colour
|
import colour
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
import zlib
|
import zlib
|
||||||
from xmlrpc.client import Boolean
|
from xmlrpc.client import Boolean
|
||||||
from utils.baseSerail import BaseSerial
|
from drivers.baseSerail import BaseSerial
|
||||||
import binascii
|
import binascii
|
||||||
import utils.baseSerail as baseSerail
|
import drivers.baseSerail as baseSerail
|
||||||
|
|
||||||
# 包头码(包引导码)
|
# 包头码(包引导码)
|
||||||
PHeader = {
|
PHeader = {
|
||||||
80
drivers/ucd_helpers.py
Normal file
80
drivers/ucd_helpers.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
"""通用 UCD323/UCDController 辅助函数。
|
||||||
|
|
||||||
|
封装"按当前接口取 tx 模块"、"读取分辨率"、"发送图片 Pattern"等所有
|
||||||
|
测试模块共用的低层 UCD 操作,避免在多个业务模块中重复 if/else。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import UniTAP
|
||||||
|
|
||||||
|
|
||||||
|
def get_tx_modules(ucd):
|
||||||
|
"""根据当前接口返回 (pg, ag) 模块。
|
||||||
|
|
||||||
|
兼容 UCD323Controller(多接口,含 current_interface 属性)
|
||||||
|
与老的 UCDController(仅 HDMI)。
|
||||||
|
"""
|
||||||
|
interface = getattr(ucd, "current_interface", None)
|
||||||
|
if interface in (None, "HDMI"):
|
||||||
|
return ucd.role.hdtx.pg, ucd.role.hdtx.ag
|
||||||
|
if interface in ("DP", "Type-C"):
|
||||||
|
return ucd.role.dptx.pg, ucd.role.dptx.ag
|
||||||
|
raise ValueError(f"不支持的接口类型: {interface}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_resolution(ucd, default=(3840, 2160)):
|
||||||
|
"""从 UCD 当前 timing 获取 (width, height),失败时返回 default。"""
|
||||||
|
try:
|
||||||
|
pg, _ = get_tx_modules(ucd)
|
||||||
|
vm = pg.get_vm()
|
||||||
|
timing = getattr(vm, "timing", None)
|
||||||
|
if timing and hasattr(timing, "h_active") and hasattr(timing, "v_active"):
|
||||||
|
return timing.h_active, timing.v_active
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
timing = getattr(ucd, "current_timing", None)
|
||||||
|
if timing and hasattr(timing, "h_active") and hasattr(timing, "v_active"):
|
||||||
|
return timing.h_active, timing.v_active
|
||||||
|
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def send_image_pattern(ucd, image_path, *, bpc=8, color_format=None, colorimetry=None):
|
||||||
|
"""通过 UCD 发送一张本地图片作为显示 Pattern。
|
||||||
|
|
||||||
|
自动停止音频以避免蜂鸣声。颜色参数留空时使用 RGB / sRGB 默认值。
|
||||||
|
返回 True/False。
|
||||||
|
"""
|
||||||
|
if not getattr(ucd, "status", False):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
pg, ag = get_tx_modules(ucd)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
ag.stop_generate()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
color_mode = UniTAP.ColorInfo()
|
||||||
|
color_mode.color_format = color_format or UniTAP.ColorInfo.ColorFormat.CF_RGB
|
||||||
|
color_mode.bpc = bpc
|
||||||
|
color_mode.colorimetry = colorimetry or UniTAP.ColorInfo.Colorimetry.CM_sRGB
|
||||||
|
|
||||||
|
timing = None
|
||||||
|
try:
|
||||||
|
vm = pg.get_vm()
|
||||||
|
timing = getattr(vm, "timing", None)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
if timing:
|
||||||
|
pg.set_vm(vm=UniTAP.VideoMode(timing=timing, color_info=color_mode))
|
||||||
|
pg.set_pattern(pattern=image_path)
|
||||||
|
pg.apply()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
2476
pqAutomationApp.py
2476
pqAutomationApp.py
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"current_test_type": "sdr_movie",
|
"current_test_type": "screen_module",
|
||||||
"test_types": {
|
"test_types": {
|
||||||
"screen_module": {
|
"screen_module": {
|
||||||
"name": "屏模组性能测试",
|
"name": "屏模组性能测试",
|
||||||
|
|||||||
@@ -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}")
|
|
||||||
Reference in New Issue
Block a user