修复LocalDimming测试错误

This commit is contained in:
xinzhu.yin
2026-06-09 15:22:20 +08:00
parent 8916f2fff0
commit f33984affa
7 changed files with 331 additions and 509 deletions

View File

@@ -157,36 +157,6 @@ def _ensure_checkerboard_image(width, height, grid_size, center_white):
_IMAGE_CACHE[key] = path
return path
def _build_ld_result_row(test_item, pattern_label, value, x="--", y="--"):
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
if isinstance(value, (int, float, np.floating)):
display_value = f"{float(value):.4f}"
else:
display_value = str(value)
return {
"test_item": test_item,
"pattern": pattern_label,
"value": display_value,
"x": x if isinstance(x, str) else f"{x:.4f}",
"y": y if isinstance(y, str) else f"{y:.4f}",
"time": timestamp,
}
def _measure_ld_row(self: "PQAutomationApp", test_item, pattern_label):
"""读取一次 CA410 数据并包装为表格行。"""
x, y, lv, _X, _Y, _Z = self.read_ca_xyLv()
if lv is None:
raise RuntimeError(f"{pattern_label} 采集失败")
return _build_ld_result_row(test_item, pattern_label, lv, x, y), lv
def _send_ld_image(self: "PQAutomationApp", image_path):
self.signal_service.send_image(image_path)
def _apply_ld_ucd_params(self: "PQAutomationApp") -> bool:
"""发送 Local Dimming 图案前,按当前测试类型写入 UCD 参数。"""
test_type = getattr(self.config, "current_test_type", "screen_module")
@@ -315,146 +285,95 @@ def _apply_ld_ucd_params(self: "PQAutomationApp") -> bool:
return False
def _run_ld_measurement_step(self: "PQAutomationApp", width, height, wait_time, step, log):
label = step["label"]
test_item = step["test_item"]
kind = step["kind"]
if kind == "window":
percentage = step["percentage"]
image_path = _ensure_window_image(width, height, percentage)
_send_ld_image(self, image_path)
settle_time = wait_time
elif kind == "black":
image_path = _ensure_solid_image(width, height, (0, 0, 0), "black")
_send_ld_image(self, image_path)
settle_time = wait_time
elif kind == "checkerboard":
image_path = _ensure_checkerboard_image(
width,
height,
DEFAULT_CHESSBOARD_GRID,
step["center_white"],
)
_send_ld_image(self, image_path)
settle_time = wait_time
elif kind == "instant_peak":
black_image = _ensure_solid_image(width, height, (0, 0, 0), "black")
peak_image = _ensure_window_image(
width,
height,
step["percentage"],
)
_send_ld_image(self, black_image)
log(f" 黑场预置 {wait_time:.1f}", level="info")
time.sleep(wait_time)
_send_ld_image(self, peak_image)
settle_time = min(wait_time, INSTANT_PEAK_CAPTURE_DELAY)
else:
raise ValueError(f"未知 Local Dimming 测试步骤: {kind}")
log(f" 等待 {settle_time:.1f} 秒后采集...", level="info")
time.sleep(settle_time)
return _measure_ld_row(self, test_item, label)
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 图案发送线程"""
# --------------------------------------------------------------------------
# GUI 入口(绑定为 PQAutomationApp 方法)
# --------------------------------------------------------------------------
def worker():
def start_local_dimming_test(self: "PQAutomationApp"):
"""Local Dimming 不再提供自动测试,保留接口仅提示用户使用手动模式。"""
messagebox.showinfo("提示", "Local Dimming 请使用手动发送图案后再采集亮度")
def update_ld_results(self: "PQAutomationApp", results):
"""把批量测试结果填入 Treeview。"""
for row in results:
self.ld_tree.insert(
"", tk.END,
values=(
row["test_item"],
row["pattern"],
row["value"],
row["x"],
row["y"],
row["time"],
),
)
def stop_local_dimming_test(self: "PQAutomationApp"):
"""兼容旧接口,无操作。"""
return
def send_ld_window(self: "PQAutomationApp", percentage):
"""发送指定百分比的白色窗口(手动模式)。"""
if not self.signal_service.is_connected:
messagebox.showwarning("警告", "请先连接 UCD323 设备")
return
try:
luminance_percent = float(
self.ld_window_luminance_var.get()
if hasattr(self, "ld_window_luminance_var")
else 100
)
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 send():
if not _apply_ld_ucd_params(self):
return
width, height = self.signal_service.current_resolution()
try:
image_path = _ensure_window_image(
width,
height,
percentage,
window_level,
)
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 = (
f"{percentage}% 窗口({luminance_percent:.0f}%亮度)已发送" if ok
else f"{percentage}% 窗口({luminance_percent:.0f}%亮度)发送失败"
)
msg = success_msg if ok else fail_msg
self._dispatch_ui(self.log_gui.log, msg)
threading.Thread(target=send, daemon=True).start()
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:
@@ -463,133 +382,104 @@ def send_ld_manual_window(self: "PQAutomationApp"):
messagebox.showwarning("参数错误", f"窗口百分比无效: {e}")
return
self.send_ld_window(percentage)
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")
self.log_gui.log(f"发送 {pattern_label}...", level="info")
_set_current_ld_pattern(self, "棋盘格对比度", pattern_label)
def send():
if not _apply_ld_ucd_params(self):
return
width, height = self.signal_service.current_resolution()
try:
image_path = _ensure_checkerboard_image(
def builder(width, height):
return _ensure_checkerboard_image(
width,
height,
DEFAULT_CHESSBOARD_GRID,
center_white,
)
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 = f"{pattern_label} 已发送" if ok else f"{pattern_label} 发送失败"
self._dispatch_ui(self.log_gui.log, msg)
threading.Thread(target=send, daemon=True).start()
_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")
self.log_gui.log("发送全黑画面...", level="info")
_set_current_ld_pattern(self, "黑电平", "全黑画面")
def send():
if not _apply_ld_ucd_params(self):
return
width, height = self.signal_service.current_resolution()
try:
image_path = _ensure_solid_image(width, height, (0, 0, 0), "black")
except Exception as e:
self._dispatch_ui(self.log_gui.log, f"图像生成失败: {e}")
return
def builder(width, height):
return _ensure_solid_image(width, height, (0, 0, 0), "black")
try:
self.signal_service.send_image(image_path)
ok = True
except Exception:
ok = False
msg = "全黑画面已发送" if ok else "全黑画面发送失败"
self._dispatch_ui(self.log_gui.log, msg)
threading.Thread(target=send, daemon=True).start()
def send_ld_instant_peak(self: "PQAutomationApp"):
"""发送瞬时峰值亮度图案:先黑场,再切到 10% 窗口并保持。"""
if not self.signal_service.is_connected:
messagebox.showwarning("警告", "请先连接 UCD323 设备")
return
pattern_label = f"黑场后切 {INSTANT_PEAK_WINDOW_PERCENTAGE}%窗口"
self.log_gui.log(f"⚡ 发送瞬时峰值图案: {pattern_label}", level="info")
_set_current_ld_pattern(
_send_ld_pattern_async(
self,
"瞬时峰值亮度",
pattern_label,
INSTANT_PEAK_WINDOW_PERCENTAGE,
builder,
"全黑画面已发送",
"全黑画面发送失败",
)
def send():
if not _apply_ld_ucd_params(self):
return
width, height = self.signal_service.current_resolution()
try:
black_image = _ensure_solid_image(width, height, (0, 0, 0), "black")
peak_image = _ensure_window_image(
width,
height,
INSTANT_PEAK_WINDOW_PERCENTAGE,
)
except Exception as e:
self._dispatch_ui(self.log_gui.log, f"图像生成失败: {e}")
return
try:
self.signal_service.send_image(black_image)
time.sleep(INSTANT_PEAK_CAPTURE_DELAY)
self.signal_service.send_image(peak_image)
ok = True
except Exception:
ok = False
msg = (
f"瞬时峰值图案已发送,当前保持 {INSTANT_PEAK_WINDOW_PERCENTAGE}%窗口"
if ok else
"瞬时峰值图案发送失败"
)
self._dispatch_ui(self.log_gui.log, msg)
threading.Thread(target=send, daemon=True).start()
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
@@ -603,38 +493,72 @@ def start_ld_instant_peak_tracking(self: "PQAutomationApp"):
if window_luminance_percent < 1 or window_luminance_percent > 100:
raise ValueError("窗口亮度超出范围")
max_duration = float(self.ld_peak_duration_var.get())
if max_duration <= 0:
raise ValueError("测量时长必须大于 0")
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}最长 {max_duration:.1f}s",
f"开始瞬时峰值测试: {pattern_label}{duration_text},回落阈值 {drop_percent:.1f}%",
level="info",
)
_set_current_ld_pattern(self, "瞬时峰值亮度", pattern_label, window_percentage)
_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
@@ -645,6 +569,7 @@ def start_ld_instant_peak_tracking(self: "PQAutomationApp"):
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,
@@ -653,75 +578,97 @@ def start_ld_instant_peak_tracking(self: "PQAutomationApp"):
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 = float(lv)
peak_lv = lv
peak_time = elapsed
# 曲线记录
if record_curve:
curve_count += 1
self._dispatch_ui(
self.ld_tree.insert,
"",
tk.END,
self._insert_ld_tree_item,
values=(
"瞬时峰值曲线",
f"{window_percentage}%窗口@{window_luminance_percent:.0f}% t={elapsed:.2f}s",
f"{float(lv):.4f}",
f"{lv:.4f}",
f"{x:.4f}",
f"{y:.4f}",
datetime.datetime.now().strftime("%H:%M:%S"),
),
)
# 回落检测
if peak_lv is not None:
drop_threshold = max(
peak_lv * INSTANT_PEAK_DROP_RATIO,
peak_lv - INSTANT_PEAK_MIN_DROP_NITS,
)
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²"
f" | t: {elapsed:.2f}s"
),
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")
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.0))
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(未检测到回落)"
else f"峰值={peak_lv:.2f} cd/m², 持续>{sustain_time:.2f}s"
)
self._dispatch_ui(
self.ld_tree.insert,
"",
tk.END,
self._insert_ld_tree_item,
values=(
"瞬时峰值亮度",
pattern_label,
@@ -731,30 +678,54 @@ def start_ld_instant_peak_tracking(self: "PQAutomationApp"):
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")
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")
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")
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:
@@ -764,8 +735,6 @@ def measure_ld_luminance(self: "PQAutomationApp"):
messagebox.showinfo("提示", "请先发送一个窗口图案")
return
self.log_gui.log("📏 正在采集亮度...", level="info")
def measure():
try:
x, y, lv, _X, _Y, _Z = self.read_ca_xyLv()
@@ -781,7 +750,7 @@ def measure_ld_luminance(self: "PQAutomationApp"):
text=f"亮度: {lv:.2f} cd/m² | x: {x:.4f} | y: {y:.4f}",
)
self._dispatch_ui(
self.ld_tree.insert, "", tk.END,
self._insert_ld_tree_item,
values=(
getattr(self, "current_ld_test_item", "手动采集"),
self.current_ld_pattern_label,
@@ -840,48 +809,82 @@ def save_local_dimming_results(self: "PQAutomationApp"):
def plot_ld_instant_peak_curve(self: "PQAutomationApp"):
"""从测试表格提取瞬时峰值曲线点并生成亮度-时间曲线图。"""
curve_points = []
pattern = re.compile(r"t\s*=\s*([0-9]+(?:\.[0-9]+)?)s")
"""绘制最近一次瞬时峰值测试的亮度-时间曲线"""
for item in self.ld_tree.get_children():
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 != "瞬时峰值曲线":
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.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.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]),
@@ -893,24 +896,23 @@ def plot_ld_instant_peak_curve(self: "PQAutomationApp"):
fig.tight_layout()
plt.show(block=False)
self.log_gui.log("已生成瞬时峰值曲线图", level="success")
self.log_gui.log("已生成本次瞬时峰值曲线图", level="success")
class LocalDimmingMixin:
"""由 tools/refactor_to_mixins.py 自动生成。
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
"""
start_local_dimming_test = start_local_dimming_test
update_ld_results = update_ld_results
stop_local_dimming_test = stop_local_dimming_test
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
send_ld_instant_peak = send_ld_instant_peak
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
_insert_ld_tree_item = _insert_ld_tree_item

View File

@@ -774,7 +774,6 @@ def create_test_type_frame(self: "PQAutomationApp"):
).pack(fill=tk.X, padx=16, pady=(16, 6), anchor="w")
panel_buttons = [
("log_btn", "测试日志", self.toggle_log_panel),
("ai_image_btn", "AI 图片", self.toggle_ai_image_panel),
("pantone_baseline_btn", "Pantone 摸底", self.toggle_pantone_baseline_panel),
("gamma_pattern_btn", "Gamma Pattern编辑", self.toggle_gamma_pattern_panel),
@@ -801,6 +800,17 @@ def create_test_type_frame(self: "PQAutomationApp"):
)
beta_lbl.pack(fill=tk.X, side=tk.BOTTOM, padx=4, pady=(6, 4))
# ---------- 测试日志(底部固定) ----------
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=(0, 2), side=tk.BOTTOM)
# ---------- 主题切换(底部固定) ----------
self.theme_toggle_btn = ttk.Button(
self.sidebar_frame,
@@ -814,8 +824,6 @@ def create_test_type_frame(self: "PQAutomationApp"):
# 注册面板按钮
if hasattr(self, "panels"):
if "log" in self.panels:
self.panels["log"]["button"] = self.log_btn
if "ai_image" in self.panels:
self.panels["ai_image"]["button"] = self.ai_image_btn
if "single_step" in self.panels:
@@ -1020,10 +1028,10 @@ def on_screen_module_timing_changed(self: "PQAutomationApp", event=None):
# 根据分辨率给出提示
if width >= 3840: # 4K及以上
self.log_gui.log(" 检测到4K分辨率", level="info")
self.log_gui.log("检测到4K分辨率", level="info")
if refresh_rate >= 120:
self.log_gui.log(" 检测到高刷新率", level="info")
self.log_gui.log("检测到高刷新率", level="info")
# 更新屏模组配置(独立于 current_test_type
self.config.current_test_types.setdefault("screen_module", {})[

View File

@@ -42,13 +42,13 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
ttk.Label(
title_frame,
text="🔆 Local Dimming 窗口测试",
text="Local Dimming 窗口测试",
font=("微软雅黑", 14, "bold"),
).pack(side=tk.LEFT)
# ==================== 2. 窗口百分比按钮 ====================
window_frame = ttk.LabelFrame(
main_container, text="🔆 窗口百分比(点击发送)", padding=10
main_container, text="窗口百分比", padding=10
)
window_frame.pack(fill=tk.X, pady=(0, 10))
@@ -133,16 +133,9 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
).pack(side=tk.LEFT, padx=3)
# ==================== 3. 其他手动图案 ====================
pattern_frame = ttk.LabelFrame(main_container, text="🧩 其他测试图案", padding=10)
pattern_frame = ttk.LabelFrame(main_container, text="其他测试图案", padding=10)
pattern_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(
pattern_frame,
text="手动发送棋盘格、瞬时峰值、黑场图案,再点击采集当前亮度",
font=("", 9),
style="SuccessState.TLabel",
).pack(pady=(0, 8))
pattern_row = ttk.Frame(pattern_frame)
pattern_row.pack(fill=tk.X)
@@ -171,21 +164,16 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
).pack(side=tk.LEFT, padx=3)
# ==================== 4. 独立瞬时峰值连续测试 ====================
peak_frame = ttk.LabelFrame(main_container, text="瞬时峰值独立测试", padding=10)
peak_frame = ttk.LabelFrame(main_container, text="瞬时峰值独立测试", padding=10)
peak_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(
peak_frame,
text="先黑场切窗口后连续测亮度,直到回落或到达最长测量时间",
font=("", 9),
style="WarningState.TLabel",
).grid(row=0, column=0, columnspan=8, sticky=tk.W, pady=(0, 8))
self.ld_peak_window_size_var = tk.StringVar(value="10")
self.ld_peak_window_luminance_var = tk.StringVar(value="100")
self.ld_peak_duration_var = tk.StringVar(value="20")
self.ld_peak_sample_interval_var = tk.StringVar(value="0.3")
self.ld_peak_record_curve_var = tk.BooleanVar(value=True)
self.ld_peak_no_limit_var = tk.BooleanVar(value=False)
self.ld_peak_drop_percent_var = tk.StringVar(value="3")
ttk.Label(peak_frame, text="窗口(%):").grid(row=1, column=0, sticky=tk.W, padx=(0, 4))
ttk.Entry(peak_frame, textvariable=self.ld_peak_window_size_var, width=8).grid(
@@ -242,8 +230,25 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
)
self.ld_peak_stop_btn.pack(side=tk.LEFT)
ttk.Label(peak_frame, text="亮度回落(%):").grid(
row=2, column=0, sticky=tk.W, padx=(0, 4), pady=(6, 0)
)
ttk.Entry(
peak_frame,
textvariable=self.ld_peak_drop_percent_var,
width=8
).grid(row=2, column=1, sticky=tk.W, pady=(6, 0))
ttk.Checkbutton(
peak_frame,
text="不固定测试时间",
variable=self.ld_peak_no_limit_var,
bootstyle="round-toggle",
).grid(row=2, column=2, columnspan=3, sticky=tk.W, pady=(6, 0))
# ==================== 5. CA410 采集按钮 ====================
measure_frame = ttk.LabelFrame(main_container, text="📊 CA410 测量", padding=10)
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)
@@ -251,7 +256,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
self.ld_measure_btn = ttk.Button(
measure_btn_frame,
text="📏 采集当前亮度",
text="采集当前亮度",
command=self.measure_ld_luminance,
bootstyle="primary",
width=15,
@@ -268,7 +273,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
self.ld_result_label.pack(side=tk.LEFT, padx=(10, 0))
# ==================== 6. 测试结果表格 ====================
result_frame = ttk.LabelFrame(main_container, text="📋 测试记录", padding=10)
result_frame = ttk.LabelFrame(main_container, text="测试记录", padding=10)
result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# Treeview
@@ -314,7 +319,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
self.ld_save_btn = ttk.Button(
bottom_frame,
text="💾 保存结果",
text="保存结果",
command=self.save_local_dimming_results,
bootstyle="info",
width=12,
@@ -323,7 +328,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
self.ld_plot_btn = ttk.Button(
bottom_frame,
text="📈 生成峰值曲线",
text="生成峰值曲线",
command=self.plot_ld_instant_peak_curve,
bootstyle="warning-outline",
width=14,

View File

@@ -393,7 +393,7 @@ class PQAutomationApp(
if not hasattr(self, "_set_custom_template_tab_visible"):
return
# 客户模板结果 Tab 只属于 SDR Movie
# 客户模板结果 Tab 只属于 SDR Movie
if test_type != "sdr_movie":
self._set_custom_template_tab_visible(False)
return
@@ -416,14 +416,9 @@ class PQAutomationApp(
def change_test_type(self, test_type):
"""切换测试类型"""
# 切换测试类型时,自动隐藏日志面板和 Local Dimming 面板
# 切换测试类型时,自动隐藏日志面板
if self.current_panel in (
"log",
"local_dimming",
"ai_image",
"single_step",
"pantone_baseline",
"gamma_pattern",
):
self.hide_all_panels()
self._save_cct_params_before_test_type_switch()

View File

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

View File

@@ -1,188 +0,0 @@
"""离线色准图 Demo。
运行后会在 tools/demo_outputs/ 下生成一张 PNG
用于在没有 UCD 设备时预览当前色准图表的 Calman 风格布局。
"""
from __future__ import annotations
import argparse
import math
import sys
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
plt.rcParams["font.family"] = ["sans-serif"]
plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei", "DejaVu Sans"]
plt.rcParams["axes.unicode_minus"] = False
REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from app.plots.plot_accuracy import plot_accuracy
from app.tests.color_accuracy import (
calculate_delta_e_2000,
get_accuracy_color_standards,
)
COLOR_NAMES = [
"White",
"Gray 80",
"Gray 65",
"Gray 50",
"Gray 35",
"Dark Skin",
"Light Skin",
"Blue Sky",
"Foliage",
"Blue Flower",
"Bluish Green",
"Orange",
"Purplish Blue",
"Moderate Red",
"Purple",
"Yellow Green",
"Orange Yellow",
"Blue (Legacy)",
"Green (Legacy)",
"Red (Legacy)",
"Yellow (Legacy)",
"Magenta (Legacy)",
"Cyan (Legacy)",
"100% Red",
"100% Green",
"100% Blue",
"100% Cyan",
"100% Magenta",
"100% Yellow",
]
class _DummyNotebook:
def select(self, *_args, **_kwargs):
return None
class _DummyCanvas:
def draw(self):
return None
class _DemoApp:
def __init__(self, fig):
self.accuracy_fig = fig
self.accuracy_canvas = _DummyCanvas()
self.chart_notebook = _DummyNotebook()
self.accuracy_chart_frame = object()
def get_test_type_name(self, test_type):
mapping = {
"sdr_movie": "SDR Movie",
"hdr_movie": "HDR Movie",
"screen_module": "屏模组",
}
return mapping.get(test_type, str(test_type))
def _build_demo_data(test_type: str = "sdr_movie"):
standards = get_accuracy_color_standards(test_type)
rng = np.random.default_rng(20260527)
measured = []
color_patches = []
delta_e_values = []
for idx, name in enumerate(COLOR_NAMES):
sx, sy = standards[name]
# 构造一些“看起来像真实测量”的偏移:
# 大部分点轻微偏移,少数点更明显,便于看出方向和等级差异。
if idx < 5:
offset_scale = 0.0012
elif idx < 23:
offset_scale = 0.0028
else:
offset_scale = 0.0045
angle = rng.uniform(0, 2 * math.pi)
radius = offset_scale * (0.55 + 0.85 * rng.random())
dx = math.cos(angle) * radius
dy = math.sin(angle) * radius
# 为了让图上连线不完全随机,给部分饱和色再加一点定向偏移。
if idx >= 23:
dx += 0.002 * (1 if idx % 2 == 0 else -1)
dy += 0.0015 * (1 if idx % 3 == 0 else -1)
mx = min(max(sx + dx, 0.0), 0.8)
my = min(max(sy + dy, 0.0), 0.9)
# 亮度也做一点微小变化,避免所有点完全同一层。
measured_lv = 70.0 + rng.normal(0, 4.0)
measured_lv = max(measured_lv, 1.0)
delta_e = calculate_delta_e_2000(mx, my, measured_lv, sx, sy)
measured.append((mx, my, measured_lv))
color_patches.append(name)
delta_e_values.append(delta_e)
avg_delta_e = float(np.mean(delta_e_values))
max_delta_e = float(np.max(delta_e_values))
min_delta_e = float(np.min(delta_e_values))
return {
"color_patches": color_patches,
"delta_e_values": delta_e_values,
"color_measurements": measured,
"avg_delta_e": avg_delta_e,
"max_delta_e": max_delta_e,
"min_delta_e": min_delta_e,
"excellent_count": sum(1 for value in delta_e_values if value < 3),
"good_count": sum(1 for value in delta_e_values if 3 <= value < 5),
"poor_count": sum(1 for value in delta_e_values if value >= 5),
"avg_delta_e_gray": float(np.mean(delta_e_values[0:5])),
"avg_delta_e_colorchecker": float(np.mean(delta_e_values[5:23])),
"avg_delta_e_saturated": float(np.mean(delta_e_values[23:29])),
"target_gamma": 2.2,
}
def main():
parser = argparse.ArgumentParser(description="Generate an offline color accuracy demo PNG.")
parser.add_argument(
"--output",
type=Path,
default=Path(__file__).resolve().parent / "demo_outputs" / "accuracy_demo.png",
help="Output PNG path.",
)
parser.add_argument(
"--test-type",
choices=["sdr_movie", "hdr_movie", "screen_module"],
default="sdr_movie",
help="Test type used for the title and standard color set.",
)
args = parser.parse_args()
args.output.parent.mkdir(parents=True, exist_ok=True)
fig = plt.Figure(figsize=(14, 8), dpi=120, tight_layout=False)
app = _DemoApp(fig)
accuracy_data = _build_demo_data(args.test_type)
plot_accuracy(app, accuracy_data, args.test_type)
fig.savefig(args.output, dpi=220)
print(f"Saved demo image to: {args.output}")
if __name__ == "__main__":
main()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 545 KiB