2026-04-21 15:31:48 +08:00
|
|
|
|
"""Local Dimming 测试逻辑(应用层)。
|
2026-04-20 10:54:47 +08:00
|
|
|
|
|
2026-04-20 11:48:38 +08:00
|
|
|
|
整合自原 drivers/local_dimming_test.py:窗口图片生成与测试主循环
|
|
|
|
|
|
直接落在本模块,UCD 通用操作下沉到 drivers.ucd_helpers。
|
2026-04-20 10:54:47 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
2026-04-20 11:48:38 +08:00
|
|
|
|
import atexit
|
|
|
|
|
|
import csv
|
|
|
|
|
|
import datetime
|
|
|
|
|
|
import os
|
|
|
|
|
|
import shutil
|
|
|
|
|
|
import sys
|
2026-04-20 10:54:47 +08:00
|
|
|
|
import threading
|
2026-04-20 11:48:38 +08:00
|
|
|
|
import time
|
2026-04-20 10:54:47 +08:00
|
|
|
|
import tkinter as tk
|
|
|
|
|
|
from tkinter import filedialog, messagebox
|
|
|
|
|
|
|
2026-04-20 11:48:38 +08:00
|
|
|
|
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 方法)
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
|
2026-04-20 11:13:57 +08:00
|
|
|
|
def start_local_dimming_test(self):
|
2026-04-20 11:48:38 +08:00
|
|
|
|
"""开始 Local Dimming 测试。"""
|
2026-04-20 10:54:47 +08:00
|
|
|
|
if not self.ca or not self.ucd.status:
|
|
|
|
|
|
messagebox.showerror("错误", "请先连接 CA410 和 UCD323")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self.ld_start_btn.config(state=tk.DISABLED)
|
|
|
|
|
|
self.ld_stop_btn.config(state=tk.NORMAL)
|
|
|
|
|
|
self.ld_save_btn.config(state=tk.DISABLED)
|
|
|
|
|
|
|
|
|
|
|
|
for item in self.ld_tree.get_children():
|
|
|
|
|
|
self.ld_tree.delete(item)
|
|
|
|
|
|
|
|
|
|
|
|
wait_time = float(self.ld_wait_time_var.get())
|
2026-04-20 11:48:38 +08:00
|
|
|
|
stop_event = threading.Event()
|
|
|
|
|
|
self.ld_stop_event = stop_event
|
|
|
|
|
|
|
|
|
|
|
|
def worker():
|
|
|
|
|
|
log = self.log_gui.log
|
2026-04-21 15:31:48 +08:00
|
|
|
|
log("=" * 60, level="separator")
|
|
|
|
|
|
log("开始 Local Dimming 测试", level="info")
|
|
|
|
|
|
log("=" * 60, level="separator")
|
2026-04-20 11:48:38 +08:00
|
|
|
|
|
|
|
|
|
|
width, height = get_current_resolution(self.ucd)
|
|
|
|
|
|
total = len(DEFAULT_WINDOW_PERCENTAGES)
|
2026-04-21 15:31:48 +08:00
|
|
|
|
log(f" 分辨率: {width}x{height}", level="info")
|
|
|
|
|
|
log(f" 测试窗口: {DEFAULT_WINDOW_PERCENTAGES}", level="info")
|
|
|
|
|
|
log(f" 等待时间: {wait_time} 秒", level="info")
|
2026-04-20 11:48:38 +08:00
|
|
|
|
|
|
|
|
|
|
results = []
|
|
|
|
|
|
for i, percentage in enumerate(DEFAULT_WINDOW_PERCENTAGES, 1):
|
|
|
|
|
|
if stop_event.is_set():
|
2026-04-21 15:31:48 +08:00
|
|
|
|
log("测试已停止", level="error")
|
2026-04-20 11:48:38 +08:00
|
|
|
|
break
|
|
|
|
|
|
|
2026-04-21 15:31:48 +08:00
|
|
|
|
log(f"[{i}/{total}] 测试 {percentage}% 窗口...", level="info")
|
2026-04-20 11:48:38 +08:00
|
|
|
|
try:
|
|
|
|
|
|
image_path = _ensure_window_image(width, height, percentage)
|
|
|
|
|
|
except Exception as e:
|
2026-04-21 16:03:11 +08:00
|
|
|
|
log(f" 图像生成失败: {e}", level="error")
|
2026-04-20 11:48:38 +08:00
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
if not send_image_pattern(self.ucd, image_path):
|
2026-04-21 16:03:11 +08:00
|
|
|
|
log(f" {percentage}% 窗口发送失败,跳过", level="error")
|
2026-04-20 11:48:38 +08:00
|
|
|
|
continue
|
|
|
|
|
|
|
2026-04-21 15:31:48 +08:00
|
|
|
|
log(f"等待 {wait_time} 秒...", level="info")
|
2026-04-20 11:48:38 +08:00
|
|
|
|
time.sleep(wait_time)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay()
|
|
|
|
|
|
except Exception as e:
|
2026-04-21 16:03:11 +08:00
|
|
|
|
log(f" 采集亮度异常: {e}", level="error")
|
2026-04-20 11:48:38 +08:00
|
|
|
|
continue
|
|
|
|
|
|
if lv is None:
|
2026-04-21 16:03:11 +08:00
|
|
|
|
log(f" {percentage}% 窗口采集失败", level="error")
|
2026-04-20 11:48:38 +08:00
|
|
|
|
continue
|
|
|
|
|
|
|
2026-04-21 15:31:48 +08:00
|
|
|
|
log(f"采集亮度: {lv:.2f} cd/m²", level="info")
|
2026-04-20 11:48:38 +08:00
|
|
|
|
results.append((percentage, x, y, lv, _X, _Y, _Z))
|
|
|
|
|
|
|
2026-04-21 15:31:48 +08:00
|
|
|
|
log("=" * 60, level="separator")
|
|
|
|
|
|
log(f"Local Dimming 测试完成 ({len(results)}/{total})", level="success")
|
|
|
|
|
|
log("=" * 60, level="separator")
|
2026-04-20 10:54:47 +08:00
|
|
|
|
|
|
|
|
|
|
self.ld_test_results = results
|
2026-04-20 15:34:45 +08:00
|
|
|
|
self._dispatch_ui(self.update_ld_results, results)
|
|
|
|
|
|
self._dispatch_ui(self.ld_start_btn.config, state=tk.NORMAL)
|
|
|
|
|
|
self._dispatch_ui(self.ld_stop_btn.config, state=tk.DISABLED)
|
|
|
|
|
|
self._dispatch_ui(self.ld_save_btn.config, state=tk.NORMAL)
|
2026-04-20 10:54:47 +08:00
|
|
|
|
|
2026-04-20 11:48:38 +08:00
|
|
|
|
threading.Thread(target=worker, daemon=True).start()
|
2026-04-20 10:54:47 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-04-20 11:13:57 +08:00
|
|
|
|
def update_ld_results(self, results):
|
2026-04-20 11:48:38 +08:00
|
|
|
|
"""把批量测试结果填入 Treeview。"""
|
|
|
|
|
|
for percentage, x, y, lv, _X, _Y, _Z in results:
|
2026-04-20 10:54:47 +08:00
|
|
|
|
self.ld_tree.insert(
|
2026-04-20 11:48:38 +08:00
|
|
|
|
"", tk.END,
|
2026-04-20 10:54:47 +08:00
|
|
|
|
values=(f"{percentage}%", f"{lv:.2f}", f"{x:.4f}", f"{y:.4f}"),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-20 11:13:57 +08:00
|
|
|
|
def stop_local_dimming_test(self):
|
2026-04-20 11:48:38 +08:00
|
|
|
|
"""请求停止当前 Local Dimming 测试。"""
|
|
|
|
|
|
ev = getattr(self, "ld_stop_event", None)
|
|
|
|
|
|
if ev:
|
|
|
|
|
|
ev.set()
|
2026-04-20 10:54:47 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-04-20 11:13:57 +08:00
|
|
|
|
def send_ld_window(self, percentage):
|
2026-04-20 11:48:38 +08:00
|
|
|
|
"""发送指定百分比的白色窗口(手动模式)。"""
|
2026-04-20 10:54:47 +08:00
|
|
|
|
if not self.ucd.status:
|
|
|
|
|
|
messagebox.showwarning("警告", "请先连接 UCD323 设备")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-04-21 15:31:48 +08:00
|
|
|
|
self.log_gui.log(f"🔆 发送 {percentage}% 窗口...", level="info")
|
2026-04-20 10:54:47 +08:00
|
|
|
|
self.current_ld_percentage = percentage
|
|
|
|
|
|
|
|
|
|
|
|
def send():
|
2026-04-20 11:48:38 +08:00
|
|
|
|
width, height = get_current_resolution(self.ucd)
|
|
|
|
|
|
try:
|
|
|
|
|
|
image_path = _ensure_window_image(width, height, percentage)
|
|
|
|
|
|
except Exception as e:
|
2026-04-21 16:03:11 +08:00
|
|
|
|
self._dispatch_ui(self.log_gui.log, f"图像生成失败: {e}")
|
2026-04-20 11:48:38 +08:00
|
|
|
|
return
|
|
|
|
|
|
ok = send_image_pattern(self.ucd, image_path)
|
|
|
|
|
|
msg = (
|
2026-04-21 15:31:48 +08:00
|
|
|
|
f"{percentage}% 窗口已发送" if ok
|
2026-04-21 16:03:11 +08:00
|
|
|
|
else f"{percentage}% 窗口发送失败"
|
2026-04-20 10:54:47 +08:00
|
|
|
|
)
|
2026-04-20 15:34:45 +08:00
|
|
|
|
self._dispatch_ui(self.log_gui.log, msg)
|
2026-04-20 10:54:47 +08:00
|
|
|
|
|
|
|
|
|
|
threading.Thread(target=send, daemon=True).start()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-20 11:13:57 +08:00
|
|
|
|
def measure_ld_luminance(self):
|
2026-04-20 11:48:38 +08:00
|
|
|
|
"""测量当前显示的亮度并追加一行到 Treeview。"""
|
2026-04-20 10:54:47 +08:00
|
|
|
|
if not self.ca:
|
|
|
|
|
|
messagebox.showwarning("警告", "请先连接 CA410 色度计")
|
|
|
|
|
|
return
|
|
|
|
|
|
if self.current_ld_percentage is None:
|
|
|
|
|
|
messagebox.showinfo("提示", "请先发送一个窗口图案")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-04-21 15:31:48 +08:00
|
|
|
|
self.log_gui.log("📏 正在采集亮度...", level="info")
|
2026-04-20 10:54:47 +08:00
|
|
|
|
|
|
|
|
|
|
def measure():
|
|
|
|
|
|
try:
|
2026-04-20 11:48:38 +08:00
|
|
|
|
x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay()
|
2026-04-20 10:54:47 +08:00
|
|
|
|
except Exception as e:
|
2026-04-21 16:03:11 +08:00
|
|
|
|
self._dispatch_ui(self.log_gui.log, f"采集异常: {str(e)}")
|
2026-04-20 11:48:38 +08:00
|
|
|
|
return
|
|
|
|
|
|
if lv is None:
|
2026-04-21 16:03:11 +08:00
|
|
|
|
self._dispatch_ui(self.log_gui.log, "采集失败")
|
2026-04-20 11:48:38 +08:00
|
|
|
|
return
|
|
|
|
|
|
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
2026-04-20 15:34:45 +08:00
|
|
|
|
self._dispatch_ui(
|
|
|
|
|
|
self.ld_result_label.config,
|
|
|
|
|
|
text=f"亮度: {lv:.2f} cd/m² | x: {x:.4f} | y: {y:.4f}",
|
|
|
|
|
|
)
|
|
|
|
|
|
self._dispatch_ui(
|
|
|
|
|
|
self.ld_tree.insert, "", tk.END,
|
2026-04-20 11:48:38 +08:00
|
|
|
|
values=(
|
|
|
|
|
|
f"{self.current_ld_percentage}%",
|
|
|
|
|
|
f"{lv:.2f}", f"{x:.4f}", f"{y:.4f}", timestamp,
|
|
|
|
|
|
),
|
2026-04-20 15:34:45 +08:00
|
|
|
|
)
|
2026-04-21 15:31:48 +08:00
|
|
|
|
self._dispatch_ui(self.log_gui.log, f"采集完成: {lv:.2f} cd/m²")
|
2026-04-20 10:54:47 +08:00
|
|
|
|
|
|
|
|
|
|
threading.Thread(target=measure, daemon=True).start()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-20 11:13:57 +08:00
|
|
|
|
def clear_ld_records(self):
|
2026-04-20 11:48:38 +08:00
|
|
|
|
"""清空 Treeview 中的测试记录。"""
|
2026-04-20 10:54:47 +08:00
|
|
|
|
for item in self.ld_tree.get_children():
|
|
|
|
|
|
self.ld_tree.delete(item)
|
|
|
|
|
|
self.ld_result_label.config(text="亮度: -- cd/m² | x: -- | y: --")
|
|
|
|
|
|
self.current_ld_percentage = None
|
2026-04-21 15:31:48 +08:00
|
|
|
|
self.log_gui.log("测试记录已清空", level="info")
|
2026-04-20 10:54:47 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-04-20 11:13:57 +08:00
|
|
|
|
def save_local_dimming_results(self):
|
2026-04-20 11:48:38 +08:00
|
|
|
|
"""把 Treeview 中的全部记录导出为 CSV。"""
|
2026-04-20 10:54:47 +08:00
|
|
|
|
if len(self.ld_tree.get_children()) == 0:
|
|
|
|
|
|
messagebox.showinfo("提示", "没有可保存的数据")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-04-20 11:48:38 +08:00
|
|
|
|
default_name = (
|
|
|
|
|
|
f"LocalDimming_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
|
|
|
|
|
)
|
2026-04-20 10:54:47 +08:00
|
|
|
|
save_path = filedialog.asksaveasfilename(
|
|
|
|
|
|
title="保存测试结果",
|
|
|
|
|
|
initialfile=default_name,
|
|
|
|
|
|
defaultextension=".csv",
|
|
|
|
|
|
filetypes=[("CSV 文件", "*.csv"), ("所有文件", "*.*")],
|
|
|
|
|
|
)
|
|
|
|
|
|
if not save_path:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(save_path, "w", newline="", encoding="utf-8-sig") as f:
|
|
|
|
|
|
writer = csv.writer(f)
|
|
|
|
|
|
writer.writerow(["窗口百分比", "亮度 (cd/m²)", "x", "y", "时间"])
|
|
|
|
|
|
for item in self.ld_tree.get_children():
|
2026-04-20 11:48:38 +08:00
|
|
|
|
writer.writerow(self.ld_tree.item(item, "values"))
|
2026-04-21 15:31:48 +08:00
|
|
|
|
self.log_gui.log(f"测试结果已保存: {save_path}", level="success")
|
2026-04-20 10:54:47 +08:00
|
|
|
|
messagebox.showinfo("成功", f"测试结果已保存到:\n{save_path}")
|
|
|
|
|
|
except Exception as e:
|
2026-04-21 16:03:11 +08:00
|
|
|
|
self.log_gui.log(f"保存失败: {str(e)}", level="error")
|
2026-04-20 10:54:47 +08:00
|
|
|
|
messagebox.showerror("错误", f"保存失败: {str(e)}")
|