Files
pqAutomationApp/app/tests/local_dimming.py
2026-06-11 15:53:41 +08:00

1030 lines
34 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Local Dimming 测试逻辑(应用层)。
整合自原 drivers/local_dimming_test.py窗口图片生成与测试主循环
直接落在本模块UCD 通用操作通过 SignalService 完成。
"""
import atexit
import csv
import datetime
import os
import re
import shutil
import sys
import threading
import time
import tkinter as tk
from tkinter import filedialog, messagebox
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp
# --------------------------------------------------------------------------
# 模块级常量与窗口图片缓存
# --------------------------------------------------------------------------
DEFAULT_WINDOW_PERCENTAGES = [1, 2, 5, 10, 18, 25, 50, 75, 100]
DEFAULT_CHESSBOARD_GRID = 5
INSTANT_PEAK_WINDOW_PERCENTAGE = 10
INSTANT_PEAK_CAPTURE_DELAY = 0.5
INSTANT_PEAK_DROP_RATIO = 0.97
INSTANT_PEAK_MIN_DROP_NITS = 2.0
INSTANT_PEAK_SAMPLE_INTERVAL = 0.3
_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, window_level=255):
"""生成黑底+居中窗口图像,保持屏幕比例。"""
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] = int(window_level)
return image
def _ensure_window_image(width, height, percentage, window_level=255):
"""生成或复用缓存的窗口 PNG 文件,返回路径。"""
level = max(0, min(255, int(window_level)))
key = (width, height, percentage, level)
cached = _IMAGE_CACHE.get(key)
if cached and os.path.exists(cached):
return cached
arr = _make_window_image_array(width, height, percentage, level)
fname = f"window_{width}x{height}_{percentage:03d}percent_{level:03d}lv.png"
path = os.path.join(_get_temp_dir(), fname)
Image.fromarray(arr, mode="RGB").save(path, format="PNG")
_IMAGE_CACHE[key] = path
return path
def _ensure_solid_image(width, height, rgb, name):
"""生成或复用纯色 PNG 文件,返回路径。"""
rgb = tuple(int(v) for v in rgb)
key = ("solid", width, height, rgb)
cached = _IMAGE_CACHE.get(key)
if cached and os.path.exists(cached):
return cached
arr = np.zeros((height, width, 3), dtype=np.uint8)
arr[:, :] = rgb
path = os.path.join(_get_temp_dir(), f"{name}_{width}x{height}.png")
Image.fromarray(arr, mode="RGB").save(path, format="PNG")
_IMAGE_CACHE[key] = path
return path
def _make_checkerboard_image_array(width, height, grid_size, center_white):
"""生成棋盘格图像,保证中心块可切换黑/白。"""
image = np.zeros((height, width, 3), dtype=np.uint8)
y_edges = np.linspace(0, height, grid_size + 1, dtype=int)
x_edges = np.linspace(0, width, grid_size + 1, dtype=int)
center_index = grid_size // 2
for row in range(grid_size):
for col in range(grid_size):
block_is_white = (row + col) % 2 == 0
if not center_white:
block_is_white = not block_is_white
value = 255 if block_is_white else 0
image[
y_edges[row]:y_edges[row + 1],
x_edges[col]:x_edges[col + 1],
] = value
center_value = 255 if center_white else 0
image[
y_edges[center_index]:y_edges[center_index + 1],
x_edges[center_index]:x_edges[center_index + 1],
] = center_value
return image
def _ensure_checkerboard_image(width, height, grid_size, center_white):
"""生成或复用棋盘格 PNG 文件,返回路径。"""
key = ("checkerboard", width, height, grid_size, center_white)
cached = _IMAGE_CACHE.get(key)
if cached and os.path.exists(cached):
return cached
arr = _make_checkerboard_image_array(width, height, grid_size, center_white)
center_name = "white_center" if center_white else "black_center"
path = os.path.join(
_get_temp_dir(),
f"checkerboard_{grid_size}x{grid_size}_{center_name}_{width}x{height}.png",
)
Image.fromarray(arr, mode="RGB").save(path, format="PNG")
_IMAGE_CACHE[key] = path
return path
def _ld_ucd_params_signature(self: "PQAutomationApp") -> tuple:
"""Local Dimming 发图前 UCD 参数签名,用于跳过未变化的重复配置。"""
test_type = getattr(self.config, "current_test_type", "screen_module")
cfg = self.config.current_test_types.get(test_type, {})
timing = cfg.get("timing", "")
if test_type == "screen_module":
color_space = (
self.screen_module_color_space_var.get()
if hasattr(self, "screen_module_color_space_var")
else cfg.get("colorimetry", "sRGB")
)
data_range = (
self.screen_module_data_range_var.get()
if hasattr(self, "screen_module_data_range_var")
else cfg.get("data_range", "Full")
)
bit_depth = (
self.screen_module_bit_depth_var.get()
if hasattr(self, "screen_module_bit_depth_var")
else f"{int(cfg.get('bpc', 8))}bit"
)
output_format = (
self.screen_module_output_format_var.get()
if hasattr(self, "screen_module_output_format_var")
else cfg.get("color_format", "RGB")
)
elif test_type == "sdr_movie":
color_space = (
self.sdr_color_space_var.get()
if hasattr(self, "sdr_color_space_var")
else cfg.get("colorimetry", "sRGB")
)
data_range = (
self.sdr_data_range_var.get()
if hasattr(self, "sdr_data_range_var")
else cfg.get("data_range", "Full")
)
bit_depth = (
self.sdr_bit_depth_var.get()
if hasattr(self, "sdr_bit_depth_var")
else f"{int(cfg.get('bpc', 8))}bit"
)
output_format = (
self.sdr_output_format_var.get()
if hasattr(self, "sdr_output_format_var")
else cfg.get("color_format", "RGB")
)
elif test_type == "hdr_movie":
color_space = (
self.hdr_color_space_var.get()
if hasattr(self, "hdr_color_space_var")
else cfg.get("colorimetry", "sRGB")
)
data_range = (
self.hdr_data_range_var.get()
if hasattr(self, "hdr_data_range_var")
else cfg.get("data_range", "Full")
)
bit_depth = (
self.hdr_bit_depth_var.get()
if hasattr(self, "hdr_bit_depth_var")
else f"{int(cfg.get('bpc', 8))}bit"
)
output_format = (
self.hdr_output_format_var.get()
if hasattr(self, "hdr_output_format_var")
else cfg.get("color_format", "RGB")
)
max_cll = self.hdr_maxcll_var.get() if hasattr(self, "hdr_maxcll_var") else None
max_fall = self.hdr_maxfall_var.get() if hasattr(self, "hdr_maxfall_var") else None
return (test_type, timing, color_space, data_range, bit_depth, output_format, max_cll, max_fall)
elif test_type == "local_dimming":
color_space = (
self.local_dimming_color_space_var.get()
if hasattr(self, "local_dimming_color_space_var")
else cfg.get("colorimetry", "sRGB")
)
data_range = (
self.local_dimming_data_range_var.get()
if hasattr(self, "local_dimming_data_range_var")
else cfg.get("data_range", "Full")
)
bit_depth = (
self.local_dimming_bit_depth_var.get()
if hasattr(self, "local_dimming_bit_depth_var")
else f"{int(cfg.get('bpc', 8))}bit"
)
output_format = (
self.local_dimming_output_format_var.get()
if hasattr(self, "local_dimming_output_format_var")
else cfg.get("color_format", "RGB")
)
else:
return (test_type,)
return (test_type, timing, color_space, data_range, bit_depth, output_format)
def invalidate_ld_ucd_params_cache(self: "PQAutomationApp") -> None:
"""信号格式或分辨率变更后,强制下次发图重新写入 UCD 参数。"""
self._last_ld_ucd_signature = None
def _apply_ld_ucd_params(self: "PQAutomationApp") -> bool:
"""发送 Local Dimming 图案前,按当前测试类型写入 UCD 参数。"""
signature = _ld_ucd_params_signature(self)
if getattr(self, "_last_ld_ucd_signature", None) == signature:
return True
test_type = signature[0]
cfg = self.config.current_test_types.get(test_type, {})
try:
self.signal_service.apply_config(self.config)
if test_type == "screen_module":
ok = self.signal_service.update_signal_format(
color_space=(
self.screen_module_color_space_var.get()
if hasattr(self, "screen_module_color_space_var")
else cfg.get("colorimetry", "sRGB")
),
data_range=(
self.screen_module_data_range_var.get()
if hasattr(self, "screen_module_data_range_var")
else cfg.get("data_range", "Full")
),
bit_depth=(
self.screen_module_bit_depth_var.get()
if hasattr(self, "screen_module_bit_depth_var")
else f"{int(cfg.get('bpc', 8))}bit"
),
output_format=(
self.screen_module_output_format_var.get()
if hasattr(self, "screen_module_output_format_var")
else cfg.get("color_format", "RGB")
),
)
elif test_type == "sdr_movie":
ok = self.signal_service.update_signal_format(
color_space=(
self.sdr_color_space_var.get()
if hasattr(self, "sdr_color_space_var")
else cfg.get("colorimetry", "sRGB")
),
data_range=(
self.sdr_data_range_var.get()
if hasattr(self, "sdr_data_range_var")
else cfg.get("data_range", "Full")
),
bit_depth=(
self.sdr_bit_depth_var.get()
if hasattr(self, "sdr_bit_depth_var")
else f"{int(cfg.get('bpc', 8))}bit"
),
output_format=(
self.sdr_output_format_var.get()
if hasattr(self, "sdr_output_format_var")
else cfg.get("color_format", "RGB")
),
)
elif test_type == "hdr_movie":
ok = self.signal_service.update_signal_format(
color_space=(
self.hdr_color_space_var.get()
if hasattr(self, "hdr_color_space_var")
else cfg.get("colorimetry", "sRGB")
),
data_range=(
self.hdr_data_range_var.get()
if hasattr(self, "hdr_data_range_var")
else cfg.get("data_range", "Full")
),
bit_depth=(
self.hdr_bit_depth_var.get()
if hasattr(self, "hdr_bit_depth_var")
else f"{int(cfg.get('bpc', 8))}bit"
),
output_format=(
self.hdr_output_format_var.get()
if hasattr(self, "hdr_output_format_var")
else cfg.get("color_format", "RGB")
),
max_cll=(
self.hdr_maxcll_var.get()
if hasattr(self, "hdr_maxcll_var")
else None
),
max_fall=(
self.hdr_maxfall_var.get()
if hasattr(self, "hdr_maxfall_var")
else None
),
)
elif test_type == "local_dimming":
ok = self.signal_service.update_signal_format(
color_space=(
self.local_dimming_color_space_var.get()
if hasattr(self, "local_dimming_color_space_var")
else cfg.get("colorimetry", "sRGB")
),
data_range=(
self.local_dimming_data_range_var.get()
if hasattr(self, "local_dimming_data_range_var")
else cfg.get("data_range", "Full")
),
bit_depth=(
self.local_dimming_bit_depth_var.get()
if hasattr(self, "local_dimming_bit_depth_var")
else f"{int(cfg.get('bpc', 8))}bit"
),
output_format=(
self.local_dimming_output_format_var.get()
if hasattr(self, "local_dimming_output_format_var")
else cfg.get("color_format", "RGB")
),
)
else:
self._dispatch_ui(
self.log_gui.log,
f"Local Dimming 不支持的测试类型: {test_type}",
"error",
)
return False
if not ok:
self._dispatch_ui(self.log_gui.log, "Local Dimming UCD 参数设置失败", "error")
return False
self._last_ld_ucd_signature = signature
return True
except Exception as e:
self._dispatch_ui(self.log_gui.log, f"Local Dimming UCD 参数设置异常: {e}", "error")
return False
def _set_current_ld_pattern(self: "PQAutomationApp", test_item, pattern_label, percentage=None):
self.current_ld_test_item = test_item
self.current_ld_pattern_label = pattern_label
self.current_ld_percentage = percentage
def _send_ld_pattern_async(self: "PQAutomationApp", image_builder, success_msg, fail_msg):
"""统一的 Local Dimming 图案发送线程"""
def worker():
if not _apply_ld_ucd_params(self):
return
width, height = self.signal_service.current_resolution()
try:
image_path = image_builder(width, height)
except Exception as e:
self._dispatch_ui(self.log_gui.log, f"图像生成失败: {e}")
return
try:
self.signal_service.send_image(image_path)
ok = True
except Exception:
ok = False
msg = success_msg if ok else fail_msg
self._dispatch_ui(self.log_gui.log, msg)
threading.Thread(target=worker, daemon=True).start()
# --------------------------------------------------------------------------
# GUI 入口(绑定为 PQAutomationApp 方法)
# --------------------------------------------------------------------------
def send_ld_window(self: "PQAutomationApp", percentage):
FIXED_WINDOW_PERCENTAGE = 40
try:
luminance_percent = float(percentage)
if luminance_percent < 1 or luminance_percent > 100:
raise ValueError
except Exception:
messagebox.showwarning("参数错误", "亮度范围应为 1-100")
return
if not self.signal_service.is_connected:
messagebox.showwarning("警告", "请先连接 UCD323 设备")
return
window_level = int(round(luminance_percent / 100 * 255))
self.log_gui.log(
f"发送 {FIXED_WINDOW_PERCENTAGE}%窗口(亮度{luminance_percent:.0f}%...",
level="info",
)
_set_current_ld_pattern(
self,
"峰值亮度",
f"{FIXED_WINDOW_PERCENTAGE}%窗口({luminance_percent:.0f}%亮度)",
FIXED_WINDOW_PERCENTAGE,
)
def builder(width, height):
return _ensure_window_image(
width,
height,
FIXED_WINDOW_PERCENTAGE,
window_level,
)
_send_ld_pattern_async(
self,
builder,
f"{FIXED_WINDOW_PERCENTAGE}%窗口({luminance_percent:.0f}%亮度)已发送",
f"{FIXED_WINDOW_PERCENTAGE}%窗口({luminance_percent:.0f}%亮度)发送失败",
)
def send_ld_manual_window(self: "PQAutomationApp"):
"""按手动输入的窗口大小和亮度发送窗口图案。"""
if not self.signal_service.is_connected:
messagebox.showwarning("警告", "请先连接 UCD323 设备")
return
try:
percentage = int(float(self.ld_window_percentage_var.get()))
if percentage < 1 or percentage > 100:
raise ValueError("窗口范围应为 1-100")
except Exception as e:
messagebox.showwarning("参数错误", f"窗口百分比无效: {e}")
return
try:
luminance_percent = float(self.ld_window_luminance_var.get())
if luminance_percent < 1 or luminance_percent > 100:
raise ValueError("亮度范围应为 1-100")
except Exception as e:
messagebox.showwarning("参数错误", f"窗口亮度无效: {e}")
return
window_level = int(round(luminance_percent / 100.0 * 255.0))
self.log_gui.log(
f"发送 {percentage}%窗口(亮度{luminance_percent:.0f}%...",
level="info",
)
_set_current_ld_pattern(
self,
"峰值亮度",
f"{percentage}%窗口({luminance_percent:.0f}%亮度)",
percentage,
)
def builder(width, height):
return _ensure_window_image(
width,
height,
percentage,
window_level,
)
_send_ld_pattern_async(
self,
builder,
f"{percentage}%窗口({luminance_percent:.0f}%亮度)已发送",
f"{percentage}%窗口({luminance_percent:.0f}%亮度)发送失败",
)
def send_ld_checkerboard(self: "PQAutomationApp", center_white):
if not self.signal_service.is_connected:
messagebox.showwarning("警告", "请先连接 UCD323 设备")
return
pattern_label = "棋盘格(中心白)" if center_white else "棋盘格(中心黑)"
self.log_gui.log(f"发送 {pattern_label}...", level="info")
_set_current_ld_pattern(self, "棋盘格对比度", pattern_label)
def builder(width, height):
return _ensure_checkerboard_image(
width,
height,
DEFAULT_CHESSBOARD_GRID,
center_white,
)
_send_ld_pattern_async(
self,
builder,
f"{pattern_label} 已发送",
f"{pattern_label} 发送失败",
)
def send_ld_black_pattern(self: "PQAutomationApp"):
if not self.signal_service.is_connected:
messagebox.showwarning("警告", "请先连接 UCD323 设备")
return
self.log_gui.log("发送全黑画面...", level="info")
_set_current_ld_pattern(self, "黑电平", "全黑画面")
def builder(width, height):
return _ensure_solid_image(width, height, (0, 0, 0), "black")
_send_ld_pattern_async(
self,
builder,
"全黑画面已发送",
"全黑画面发送失败",
)
def start_ld_instant_peak_tracking(self: "PQAutomationApp"):
"""独立瞬时峰值测试:持续采样直到亮度回落或达到最长测量时长。"""
if not self.signal_service.is_connected:
messagebox.showwarning("警告", "请先连接 UCD323 设备")
return
if not self.ca:
messagebox.showwarning("警告", "请先连接 CA410 色度计")
return
if getattr(self, "ld_peak_tracking", False):
messagebox.showinfo("提示", "瞬时峰值测试正在进行中")
return
try:
window_percentage = int(float(self.ld_peak_window_size_var.get()))
if window_percentage < 1 or window_percentage > 100:
raise ValueError("窗口百分比超出范围")
window_luminance_percent = float(self.ld_peak_window_luminance_var.get())
if window_luminance_percent < 1 or window_luminance_percent > 100:
raise ValueError("窗口亮度超出范围")
sample_interval = float(
self.ld_peak_sample_interval_var.get()
if hasattr(self, "ld_peak_sample_interval_var")
else INSTANT_PEAK_SAMPLE_INTERVAL
)
if sample_interval <= 0:
raise ValueError("采样间隔必须大于 0")
# 无限模式
no_limit = bool(
self.ld_peak_no_limit_var.get()
if hasattr(self, "ld_peak_no_limit_var")
else False
)
if not no_limit:
max_duration = float(self.ld_peak_duration_var.get())
if max_duration <= 0:
raise ValueError("测量时长必须大于 0")
else:
max_duration = None
# 回落百分比
drop_percent = float(
self.ld_peak_drop_percent_var.get()
if hasattr(self, "ld_peak_drop_percent_var")
else 3
)
if drop_percent <= 0 or drop_percent >= 50:
raise ValueError("回落百分比建议 1~50")
except Exception as e:
messagebox.showwarning("参数错误", f"请检查瞬时峰值参数: {e}")
return
record_curve = bool(self.ld_peak_record_curve_var.get())
window_level = int(round(window_luminance_percent / 100.0 * 255.0))
pattern_label = f"黑场后切 {window_percentage}%窗口({window_luminance_percent:.0f}%亮度)"
duration_text = "直到亮度回落" if no_limit else f"最长 {max_duration:.1f}s"
self.ld_peak_tracking = True
self.log_gui.log(
f"开始瞬时峰值测试: {pattern_label}{duration_text},回落阈值 {drop_percent:.1f}%",
level="info",
)
_set_current_ld_pattern(
self,
"瞬时峰值亮度",
pattern_label,
window_percentage,
)
if hasattr(self, "ld_peak_start_btn"):
self.ld_peak_start_btn.configure(state="disabled")
if hasattr(self, "ld_peak_stop_btn"):
self.ld_peak_stop_btn.configure(state="normal")
def run():
peak_lv = None
peak_time = None
drop_time = None
curve_count = 0
try:
if not _apply_ld_ucd_params(self):
return
width, height = self.signal_service.current_resolution()
black_image = _ensure_solid_image(width, height, (0, 0, 0), "black")
peak_image = _ensure_window_image(
width,
height,
window_percentage,
window_level,
)
# 黑场预置
self.signal_service.send_image(black_image)
time.sleep(INSTANT_PEAK_CAPTURE_DELAY)
# 切窗口
self.signal_service.send_image(peak_image)
started = time.time()
while self.ld_peak_tracking:
elapsed = time.time() - started
# 固定时长模式
if max_duration is not None:
if elapsed > max_duration:
break
# 安全保护30分钟
if elapsed > 1800:
self._dispatch_ui(
self.log_gui.log,
"安全超时停止(30分钟)",
"warning",
)
break
x, y, lv, _X, _Y, _Z = self.read_ca_xyLv()
if lv is None:
time.sleep(sample_interval)
continue
lv = float(lv)
# 更新峰值
if peak_lv is None or lv > peak_lv:
peak_lv = lv
peak_time = elapsed
# 曲线记录
if record_curve:
curve_count += 1
self._dispatch_ui(
self._insert_ld_tree_item,
values=(
"瞬时峰值曲线",
f"{window_percentage}%窗口@{window_luminance_percent:.0f}% t={elapsed:.2f}s",
f"{lv:.4f}",
f"{x:.4f}",
f"{y:.4f}",
datetime.datetime.now().strftime("%H:%M:%S"),
),
)
# 回落检测
if peak_lv is not None:
drop_threshold = peak_lv * (1 - drop_percent / 100.0)
if lv < drop_threshold and elapsed > (peak_time or 0):
drop_time = elapsed
break
self._dispatch_ui(
self.ld_result_label.config,
text=f"亮度:{lv:.2f} cd/m² | 峰值:{(peak_lv or lv):.2f} cd/m² | t:{elapsed:.2f}s",
)
time.sleep(sample_interval)
if peak_lv is None:
self._dispatch_ui(
self.log_gui.log,
"瞬时峰值测试未采到有效亮度",
"warning",
)
return
end_time = drop_time if drop_time is not None else (time.time() - started)
sustain_time = max(0.0, end_time - (peak_time or 0))
result_label = (
f"峰值={peak_lv:.2f} cd/m², 持续={sustain_time:.2f}s"
if drop_time is not None
else f"峰值={peak_lv:.2f} cd/m², 持续>{sustain_time:.2f}s"
)
self._dispatch_ui(
self._insert_ld_tree_item,
values=(
"瞬时峰值亮度",
pattern_label,
result_label,
"--",
"--",
datetime.datetime.now().strftime("%H:%M:%S"),
),
)
self._dispatch_ui(
self.log_gui.log,
f"瞬时峰值测试完成: {result_label},曲线点 {curve_count}",
"success",
)
except Exception as e:
self._dispatch_ui(
self.log_gui.log,
f"瞬时峰值测试异常: {e}",
"error",
)
finally:
self.ld_peak_tracking = False
if hasattr(self, "ld_peak_start_btn"):
self._dispatch_ui(
self.ld_peak_start_btn.configure,
state="normal",
)
if hasattr(self, "ld_peak_stop_btn"):
self._dispatch_ui(
self.ld_peak_stop_btn.configure,
state="disabled",
)
threading.Thread(target=run, daemon=True).start()
def stop_ld_instant_peak_tracking(self: "PQAutomationApp"):
"""停止独立瞬时峰值连续采样"""
if getattr(self, "ld_peak_tracking", False):
self.ld_peak_tracking = False
self.log_gui.log("已请求停止瞬时峰值测试", level="info")
def _insert_ld_tree_item(self, parent="", index=tk.END, **kwargs):
item = self.ld_tree.insert(parent, index, **kwargs)
try:
self.ld_tree.see(item)
except Exception:
pass
return item
def measure_ld_luminance(self: "PQAutomationApp"):
"""测量当前显示的亮度并追加一行到 Treeview。"""
if not self.ca:
messagebox.showwarning("警告", "请先连接 CA410 色度计")
return
if getattr(self, "current_ld_pattern_label", None) is None:
messagebox.showinfo("提示", "请先发送一个窗口图案")
return
def measure():
try:
x, y, lv, _X, _Y, _Z = self.read_ca_xyLv()
except Exception as e:
self._dispatch_ui(self.log_gui.log, f"采集异常: {str(e)}")
return
if lv is None:
self._dispatch_ui(self.log_gui.log, "采集失败")
return
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
self._dispatch_ui(
self.ld_result_label.config,
text=f"亮度: {lv:.2f} cd/m² | x: {x:.4f} | y: {y:.4f}",
)
self._dispatch_ui(
self._insert_ld_tree_item,
values=(
getattr(self, "current_ld_test_item", "手动采集"),
self.current_ld_pattern_label,
f"{lv:.4f}",
f"{x:.4f}",
f"{y:.4f}",
timestamp,
),
)
self._dispatch_ui(self.log_gui.log, f"采集完成: {lv:.2f} cd/m²")
threading.Thread(target=measure, daemon=True).start()
def clear_ld_records(self: "PQAutomationApp"):
"""清空 Treeview 中的测试记录。"""
for item in self.ld_tree.get_children():
self.ld_tree.delete(item)
self.ld_result_label.config(text="亮度: -- cd/m² | x: -- | y: --")
self.current_ld_percentage = None
self.current_ld_test_item = None
self.current_ld_pattern_label = None
self.ld_peak_tracking = False
self.log_gui.log("测试记录已清空", level="info")
def save_local_dimming_results(self: "PQAutomationApp"):
"""把 Treeview 中的全部记录导出为 CSV。"""
if len(self.ld_tree.get_children()) == 0:
messagebox.showinfo("提示", "没有可保存的数据")
return
default_name = (
f"LocalDimming_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
)
save_path = filedialog.asksaveasfilename(
title="保存测试结果",
initialfile=default_name,
defaultextension=".csv",
filetypes=[("CSV 文件", "*.csv"), ("所有文件", "*.*")],
)
if not save_path:
return
try:
with open(save_path, "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerow(["测试项目", "图案", "亮度/结果", "x", "y", "时间"])
for item in self.ld_tree.get_children():
writer.writerow(self.ld_tree.item(item, "values"))
self.log_gui.log(f"测试结果已保存: {save_path}", level="success")
messagebox.showinfo("成功", f"测试结果已保存到:\n{save_path}")
except Exception as e:
self.log_gui.log(f"保存失败: {str(e)}", level="error")
messagebox.showerror("错误", f"保存失败: {str(e)}")
def plot_ld_instant_peak_curve(self: "PQAutomationApp"):
"""绘制最近一次瞬时峰值测试的亮度-时间曲线"""
pattern = re.compile(r"t\s*=\s*([0-9]+(?:\.[0-9]+)?)s")
curve_points = []
# 从表格底部向上找最近一次曲线
items = list(self.ld_tree.get_children())[::-1]
collecting = False
for item in items:
values = self.ld_tree.item(item, "values")
if len(values) < 3:
continue
test_item = str(values[0])
pattern_text = str(values[1])
lv_text = str(values[2])
if test_item == "瞬时峰值曲线":
collecting = True
else:
if collecting:
break
continue
match = pattern.search(pattern_text)
if not match:
continue
try:
t_sec = float(match.group(1))
lv = float(lv_text)
except Exception:
continue
curve_points.append((t_sec, lv))
if not curve_points:
messagebox.showinfo("提示", "没有可绘制的瞬时峰值曲线数据")
return
# 时间排序
curve_points.sort(key=lambda x: x[0])
t_data = [p[0] for p in curve_points]
lv_data = [p[1] for p in curve_points]
fig = plt.figure(figsize=(8.6, 4.6))
ax = fig.add_subplot(111)
ax.plot(
t_data,
lv_data,
"-o",
linewidth=1.8,
markersize=3.5,
color="#2a9d8f",
)
ax.set_title("Instant Peak Luminance Curve")
ax.set_xlabel("Time (s)")
ax.set_ylabel("Luminance (cd/m²)")
ax.grid(True, linestyle="--", alpha=0.35)
# 标记峰值
peak_idx = int(np.argmax(lv_data))
ax.scatter(
[t_data[peak_idx]],
[lv_data[peak_idx]],
color="#e76f51",
zorder=3,
)
ax.annotate(
f"Peak: {lv_data[peak_idx]:.2f} cd/m² @ {t_data[peak_idx]:.2f}s",
(t_data[peak_idx], lv_data[peak_idx]),
xytext=(8, 10),
textcoords="offset points",
fontsize=9,
color="#333333",
)
fig.tight_layout()
plt.show(block=False)
self.log_gui.log("已生成本次瞬时峰值曲线图", level="success")
class LocalDimmingMixin:
"""由 tools/refactor_to_mixins.py 自动生成。
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
"""
send_ld_window = send_ld_window
send_ld_manual_window = send_ld_manual_window
send_ld_checkerboard = send_ld_checkerboard
send_ld_black_pattern = send_ld_black_pattern
start_ld_instant_peak_tracking = start_ld_instant_peak_tracking
stop_ld_instant_peak_tracking = stop_ld_instant_peak_tracking
measure_ld_luminance = measure_ld_luminance
clear_ld_records = clear_ld_records
save_local_dimming_results = save_local_dimming_results
plot_ld_instant_peak_curve = plot_ld_instant_peak_curve
invalidate_ld_ucd_params_cache = invalidate_ld_ucd_params_cache
_insert_ld_tree_item = _insert_ld_tree_item