Compare commits
8 Commits
e9a591bf6e
...
38222ff002
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38222ff002 | ||
|
|
3206079c63 | ||
|
|
25be4b7f4a | ||
|
|
f33984affa | ||
|
|
8916f2fff0 | ||
|
|
9ad9cf9aa0 | ||
|
|
e4890d9d8d | ||
|
|
febbb28a4c |
@@ -160,8 +160,7 @@ class ConnectionController:
|
|||||||
def check_all_async(self) -> None:
|
def check_all_async(self) -> None:
|
||||||
"""异步并联检测 UCD + CA,通过 ``_dispatch_ui`` 回主线程更新 UI。"""
|
"""异步并联检测 UCD + CA,通过 ``_dispatch_ui`` 回主线程更新 UI。"""
|
||||||
app = self._app
|
app = self._app
|
||||||
app.check_button.configure(state="disabled")
|
self._set_connect_widgets_state("disabled")
|
||||||
app.refresh_button.configure(state="disabled")
|
|
||||||
app.status_var.set("正在检测连接...")
|
app.status_var.set("正在检测连接...")
|
||||||
app.root.update()
|
app.root.update()
|
||||||
|
|
||||||
@@ -185,6 +184,58 @@ class ConnectionController:
|
|||||||
|
|
||||||
threading.Thread(target=worker, daemon=True).start()
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
|
||||||
|
def check_ucd_async(self) -> None:
|
||||||
|
"""仅异步连接 UCD323。"""
|
||||||
|
app = self._app
|
||||||
|
self._set_connect_widgets_state("disabled")
|
||||||
|
app.status_var.set("正在连接 UCD323...")
|
||||||
|
app.root.update()
|
||||||
|
|
||||||
|
def worker():
|
||||||
|
try:
|
||||||
|
ucd_ok = self.connect_ucd(app.ucd_list_var.get())
|
||||||
|
app._dispatch_ui(
|
||||||
|
app.update_connection_indicator,
|
||||||
|
app.ucd_status_indicator,
|
||||||
|
ucd_ok,
|
||||||
|
)
|
||||||
|
app._dispatch_ui(
|
||||||
|
app.status_var.set,
|
||||||
|
"UCD323 连接成功" if ucd_ok else "UCD323 连接失败",
|
||||||
|
)
|
||||||
|
app._dispatch_ui(self._enable_widgets)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
app._dispatch_ui(app.log_gui.log, f"UCD323 连接出错: {exc}")
|
||||||
|
app._dispatch_ui(self._enable_widgets)
|
||||||
|
|
||||||
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
|
||||||
|
def check_ca_async(self) -> None:
|
||||||
|
"""仅异步连接 CA410。"""
|
||||||
|
app = self._app
|
||||||
|
self._set_connect_widgets_state("disabled")
|
||||||
|
app.status_var.set("正在连接 CA410...")
|
||||||
|
app.root.update()
|
||||||
|
|
||||||
|
def worker():
|
||||||
|
try:
|
||||||
|
ca_ok = self.connect_ca()
|
||||||
|
app._dispatch_ui(
|
||||||
|
app.update_connection_indicator,
|
||||||
|
app.ca_status_indicator,
|
||||||
|
ca_ok,
|
||||||
|
)
|
||||||
|
app._dispatch_ui(
|
||||||
|
app.status_var.set,
|
||||||
|
"CA410 连接成功" if ca_ok else "CA410 连接失败",
|
||||||
|
)
|
||||||
|
app._dispatch_ui(self._enable_widgets)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
app._dispatch_ui(app.log_gui.log, f"CA410 连接出错: {exc}")
|
||||||
|
app._dispatch_ui(self._enable_widgets)
|
||||||
|
|
||||||
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
|
||||||
def disconnect_all(self) -> None:
|
def disconnect_all(self) -> None:
|
||||||
try:
|
try:
|
||||||
self.disconnect_ucd()
|
self.disconnect_ucd()
|
||||||
@@ -196,6 +247,24 @@ class ConnectionController:
|
|||||||
self._log(f"断开连接时发生错误: {exc}", level="info")
|
self._log(f"断开连接时发生错误: {exc}", level="info")
|
||||||
messagebox.showerror("错误", f"断开连接失败: {exc}")
|
messagebox.showerror("错误", f"断开连接失败: {exc}")
|
||||||
|
|
||||||
|
def disconnect_ucd_only(self) -> None:
|
||||||
|
try:
|
||||||
|
self.disconnect_ucd()
|
||||||
|
self._app.refresh_connection_indicators()
|
||||||
|
self._app.status_var.set("UCD323 已断开")
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
self._log(f"断开 UCD323 时发生错误: {exc}", level="info")
|
||||||
|
messagebox.showerror("错误", f"断开 UCD323 失败: {exc}")
|
||||||
|
|
||||||
|
def disconnect_ca_only(self) -> None:
|
||||||
|
try:
|
||||||
|
self.disconnect_ca()
|
||||||
|
self._app.refresh_connection_indicators()
|
||||||
|
self._app.status_var.set("CA410 已断开")
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
self._log(f"断开 CA410 时发生错误: {exc}", level="info")
|
||||||
|
messagebox.showerror("错误", f"断开 CA410 失败: {exc}")
|
||||||
|
|
||||||
def refresh_ports(self) -> None:
|
def refresh_ports(self) -> None:
|
||||||
"""刷新 UCD + COM 端口下拉框;指示器复位。"""
|
"""刷新 UCD + COM 端口下拉框;指示器复位。"""
|
||||||
app = self._app
|
app = self._app
|
||||||
@@ -219,8 +288,21 @@ class ConnectionController:
|
|||||||
# -- 内部 ----------------------------------------------------
|
# -- 内部 ----------------------------------------------------
|
||||||
|
|
||||||
def _enable_widgets(self) -> None:
|
def _enable_widgets(self) -> None:
|
||||||
self._app.check_button.configure(state="normal")
|
self._set_connect_widgets_state("normal")
|
||||||
self._app.refresh_button.configure(state="normal")
|
|
||||||
|
def _set_connect_widgets_state(self, state: str) -> None:
|
||||||
|
for attr in (
|
||||||
|
"check_button",
|
||||||
|
"ucd_connect_button",
|
||||||
|
"ca_connect_button",
|
||||||
|
"refresh_button",
|
||||||
|
):
|
||||||
|
widget = getattr(self._app, attr, None)
|
||||||
|
if widget is not None:
|
||||||
|
try:
|
||||||
|
widget.configure(state=state)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
|
||||||
def _log(self, msg: str, *, level: str = "info") -> None:
|
def _log(self, msg: str, *, level: str = "info") -> None:
|
||||||
log_gui = getattr(self._app, "log_gui", None)
|
log_gui = getattr(self._app, "log_gui", None)
|
||||||
@@ -250,6 +332,14 @@ def check_com_connections(self: "PQAutomationApp"):
|
|||||||
self.connection.check_all_async()
|
self.connection.check_all_async()
|
||||||
|
|
||||||
|
|
||||||
|
def check_ucd_connection(self: "PQAutomationApp"):
|
||||||
|
self.connection.check_ucd_async()
|
||||||
|
|
||||||
|
|
||||||
|
def check_ca_connection(self: "PQAutomationApp"):
|
||||||
|
self.connection.check_ca_async()
|
||||||
|
|
||||||
|
|
||||||
def update_connection_indicator(self: "PQAutomationApp", indicator, connected):
|
def update_connection_indicator(self: "PQAutomationApp", indicator, connected):
|
||||||
_draw_connection_indicator(indicator, "green" if connected else "red")
|
_draw_connection_indicator(indicator, "green" if connected else "red")
|
||||||
|
|
||||||
@@ -307,6 +397,14 @@ def disconnect_com_connections(self: "PQAutomationApp"):
|
|||||||
self.connection.disconnect_all()
|
self.connection.disconnect_all()
|
||||||
|
|
||||||
|
|
||||||
|
def disconnect_ucd_connection(self: "PQAutomationApp"):
|
||||||
|
self.connection.disconnect_ucd_only()
|
||||||
|
|
||||||
|
|
||||||
|
def disconnect_ca_connection(self: "PQAutomationApp"):
|
||||||
|
self.connection.disconnect_ca_only()
|
||||||
|
|
||||||
|
|
||||||
def _get_ca_measure_lock(self: "PQAutomationApp"):
|
def _get_ca_measure_lock(self: "PQAutomationApp"):
|
||||||
lock = getattr(self, "_ca_measure_lock", None)
|
lock = getattr(self, "_ca_measure_lock", None)
|
||||||
if lock is None:
|
if lock is None:
|
||||||
@@ -357,11 +455,15 @@ __all__ = [
|
|||||||
"get_available_com_ports",
|
"get_available_com_ports",
|
||||||
"refresh_com_ports",
|
"refresh_com_ports",
|
||||||
"check_com_connections",
|
"check_com_connections",
|
||||||
|
"check_ucd_connection",
|
||||||
|
"check_ca_connection",
|
||||||
"update_connection_indicator",
|
"update_connection_indicator",
|
||||||
"refresh_connection_indicators",
|
"refresh_connection_indicators",
|
||||||
"check_port_connection",
|
"check_port_connection",
|
||||||
"enable_com_widgets",
|
"enable_com_widgets",
|
||||||
"disconnect_com_connections",
|
"disconnect_com_connections",
|
||||||
|
"disconnect_ucd_connection",
|
||||||
|
"disconnect_ca_connection",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -373,11 +475,15 @@ class DeviceConnectionMixin:
|
|||||||
get_available_com_ports = get_available_com_ports
|
get_available_com_ports = get_available_com_ports
|
||||||
refresh_com_ports = refresh_com_ports
|
refresh_com_ports = refresh_com_ports
|
||||||
check_com_connections = check_com_connections
|
check_com_connections = check_com_connections
|
||||||
|
check_ucd_connection = check_ucd_connection
|
||||||
|
check_ca_connection = check_ca_connection
|
||||||
update_connection_indicator = update_connection_indicator
|
update_connection_indicator = update_connection_indicator
|
||||||
refresh_connection_indicators = refresh_connection_indicators
|
refresh_connection_indicators = refresh_connection_indicators
|
||||||
check_port_connection = check_port_connection
|
check_port_connection = check_port_connection
|
||||||
enable_com_widgets = enable_com_widgets
|
enable_com_widgets = enable_com_widgets
|
||||||
disconnect_com_connections = disconnect_com_connections
|
disconnect_com_connections = disconnect_com_connections
|
||||||
|
disconnect_ucd_connection = disconnect_ucd_connection
|
||||||
|
disconnect_ca_connection = disconnect_ca_connection
|
||||||
_get_ca_measure_lock = _get_ca_measure_lock
|
_get_ca_measure_lock = _get_ca_measure_lock
|
||||||
_read_ca_display = _read_ca_display
|
_read_ca_display = _read_ca_display
|
||||||
read_ca_xyLv = read_ca_xyLv
|
read_ca_xyLv = read_ca_xyLv
|
||||||
|
|||||||
@@ -2,15 +2,14 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
_EXPORT_BG_COLOR = "#FFFFFF"
|
def _save_with_theme_background(fig, path, *, dpi=300, bbox_inches=None):
|
||||||
|
"""按图表当前主题背景导出,避免深色模式下被强制写成白底。"""
|
||||||
|
bg = fig.get_facecolor()
|
||||||
def _save_with_light_background(fig, path, *, dpi=300, bbox_inches=None):
|
|
||||||
"""导出统一浅色背景,避免深色主题下图片背景变暗。"""
|
|
||||||
kwargs = {
|
kwargs = {
|
||||||
"dpi": dpi,
|
"dpi": dpi,
|
||||||
"facecolor": _EXPORT_BG_COLOR,
|
"facecolor": bg,
|
||||||
"edgecolor": _EXPORT_BG_COLOR,
|
"edgecolor": bg,
|
||||||
|
"transparent": False,
|
||||||
}
|
}
|
||||||
if bbox_inches is not None:
|
if bbox_inches is not None:
|
||||||
kwargs["bbox_inches"] = bbox_inches
|
kwargs["bbox_inches"] = bbox_inches
|
||||||
@@ -85,7 +84,7 @@ def save_result_images(result_dir, current_test_type, selected_items,
|
|||||||
continue
|
continue
|
||||||
per_ref_name = f"色域测试结果_{ref}.png"
|
per_ref_name = f"色域测试结果_{ref}.png"
|
||||||
path = os.path.join(result_dir, per_ref_name)
|
path = os.path.join(result_dir, per_ref_name)
|
||||||
_save_with_light_background(fig, path, dpi=300)
|
_save_with_theme_background(fig, path, dpi=300)
|
||||||
log(f"已保存: {per_ref_name}")
|
log(f"已保存: {per_ref_name}")
|
||||||
finally:
|
finally:
|
||||||
ref_var.set(original_ref)
|
ref_var.set(original_ref)
|
||||||
@@ -97,7 +96,7 @@ def save_result_images(result_dir, current_test_type, selected_items,
|
|||||||
continue
|
continue
|
||||||
path = os.path.join(result_dir, filename)
|
path = os.path.join(result_dir, filename)
|
||||||
if default_bbox:
|
if default_bbox:
|
||||||
_save_with_light_background(fig, path, dpi=300)
|
_save_with_theme_background(fig, path, dpi=300)
|
||||||
else:
|
else:
|
||||||
_save_with_light_background(fig, path, dpi=300, bbox_inches="tight")
|
_save_with_theme_background(fig, path, dpi=300, bbox_inches="tight")
|
||||||
log(f"已保存: {filename}")
|
log(f"已保存: {filename}")
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
"""CIE 色度图底图渲染与缓存。
|
"""
|
||||||
|
CIE 色度图底图渲染与缓存(工业版)
|
||||||
|
|
||||||
将"重型图像渲染"(colour-science 的谱迹颜色填充)与"轻量框架数据层"
|
特点:
|
||||||
(参考/实测三角形、标签、覆盖率)解耦。
|
- colour-science 谱迹渲染
|
||||||
|
- numpy RGBA 缓存
|
||||||
|
- 内存 + 磁盘缓存
|
||||||
|
- 支持 light / dark UI
|
||||||
|
- 启动预热
|
||||||
|
- 线程安全
|
||||||
|
|
||||||
底图:
|
调用方式:
|
||||||
- 仅在首次调用或缓存失效时通过 colour-science 渲染一次;
|
|
||||||
- 渲染结果保存为 numpy RGBA 数组,同时落盘到 settings/cache/,
|
bg, bbox = get_cie1931_background(mode="dark")
|
||||||
下次启动直接 imread 加载,避免重新跑色彩科学计算。
|
ax.imshow(bg, extent=bbox)
|
||||||
|
|
||||||
调用方在每次绘图时只需 `ax.imshow(bg, extent=bbox)`,再叠加自己的矢量层。
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -20,91 +25,140 @@ from typing import Tuple
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
# 谱迹底图分辨率(边长,单位像素)。1024 对于 14 inch 画布足够细腻,
|
# ----------------------------
|
||||||
# 文件大小 ~1-2MB,单次渲染 ~0.5-1 s,缓存后毫秒级加载。
|
# 配置
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
|
# 渲染分辨率
|
||||||
_DIAGRAM_RES = 1024
|
_DIAGRAM_RES = 1024
|
||||||
|
|
||||||
# 缓存版本号:当渲染参数或风格调整时递增,强制重新生成。
|
# 缓存版本(风格变化时递增)
|
||||||
_CACHE_VERSION = "v1"
|
_CACHE_VERSION = "v2"
|
||||||
|
|
||||||
_BBox = Tuple[float, float, float, float] # (xmin, xmax, ymin, ymax)
|
# UI 颜色
|
||||||
|
_DARK_BG = "#0f1115"
|
||||||
|
_LIGHT_BG = "#ffffff"
|
||||||
|
|
||||||
|
_BBox = Tuple[float, float, float, float]
|
||||||
|
|
||||||
_CIE1931_BBOX: _BBox = (0.0, 0.8, 0.0, 0.9)
|
_CIE1931_BBOX: _BBox = (0.0, 0.8, 0.0, 0.9)
|
||||||
_CIE1976_BBOX: _BBox = (0.0, 0.65, 0.0, 0.6)
|
_CIE1976_BBOX: _BBox = (0.0, 0.65, 0.0, 0.6)
|
||||||
|
|
||||||
|
|
||||||
_memory_cache: dict[str, np.ndarray] = {}
|
_memory_cache: dict[str, np.ndarray] = {}
|
||||||
_lock = threading.Lock()
|
_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# cache path
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
def _cache_dir() -> str:
|
def _cache_dir() -> str:
|
||||||
# 项目根目录通过本文件位置反推:app/plots/ -> 项目根
|
|
||||||
here = os.path.dirname(os.path.abspath(__file__))
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
root = os.path.abspath(os.path.join(here, "..", ".."))
|
root = os.path.abspath(os.path.join(here, "..", ".."))
|
||||||
|
|
||||||
d = os.path.join(root, "settings", "cache")
|
d = os.path.join(root, "settings", "cache")
|
||||||
os.makedirs(d, exist_ok=True)
|
os.makedirs(d, exist_ok=True)
|
||||||
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
def _cache_key(kind: str, bbox: _BBox) -> str:
|
def _cache_key(kind: str, bbox: _BBox, mode: str) -> str:
|
||||||
sig = f"{kind}|{bbox}|{_DIAGRAM_RES}|{_CACHE_VERSION}"
|
sig = f"{kind}|{bbox}|{mode}|{_DIAGRAM_RES}|{_CACHE_VERSION}"
|
||||||
h = hashlib.md5(sig.encode("utf-8")).hexdigest()[:10]
|
h = hashlib.md5(sig.encode()).hexdigest()[:10]
|
||||||
return f"chromaticity_{kind}_{h}.npy"
|
|
||||||
|
return f"chromaticity_{kind}_{mode}_{h}.npy"
|
||||||
|
|
||||||
|
|
||||||
def _cache_path(kind: str, bbox: _BBox) -> str:
|
def _cache_path(kind: str, bbox: _BBox, mode: str) -> str:
|
||||||
return os.path.join(_cache_dir(), _cache_key(kind, bbox))
|
return os.path.join(_cache_dir(), _cache_key(kind, bbox, mode))
|
||||||
|
|
||||||
|
|
||||||
def _render_chromaticity(kind: str, bbox: _BBox) -> np.ndarray:
|
# ----------------------------
|
||||||
"""通过 colour-science 离屏渲染谱迹底图,返回 RGBA float 数组。"""
|
# 渲染
|
||||||
# 延迟导入:仅在缓存未命中时支付 colour.plotting 的加载开销。
|
# ----------------------------
|
||||||
|
|
||||||
|
def _render_chromaticity(kind: str, bbox: _BBox, mode: str) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
通过 colour-science 渲染 chromaticity 图。
|
||||||
|
"""
|
||||||
|
|
||||||
import matplotlib
|
import matplotlib
|
||||||
|
|
||||||
prev_backend = matplotlib.get_backend()
|
prev_backend = matplotlib.get_backend()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
matplotlib.use("Agg", force=True)
|
matplotlib.use("Agg", force=True)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
import colour
|
||||||
from colour.plotting import (
|
from colour.plotting import (
|
||||||
plot_chromaticity_diagram_CIE1931,
|
plot_chromaticity_diagram_CIE1931,
|
||||||
plot_chromaticity_diagram_CIE1976UCS,
|
plot_chromaticity_diagram_CIE1976UCS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if mode == "dark":
|
||||||
|
colour.plotting.colour_style("dark")
|
||||||
|
bg_color = _DARK_BG
|
||||||
|
else:
|
||||||
|
colour.plotting.colour_style("light")
|
||||||
|
bg_color = _LIGHT_BG
|
||||||
|
|
||||||
xmin, xmax, ymin, ymax = bbox
|
xmin, xmax, ymin, ymax = bbox
|
||||||
|
|
||||||
aspect = (xmax - xmin) / (ymax - ymin)
|
aspect = (xmax - xmin) / (ymax - ymin)
|
||||||
|
|
||||||
height = _DIAGRAM_RES
|
height = _DIAGRAM_RES
|
||||||
width = int(round(height * aspect))
|
width = int(round(height * aspect))
|
||||||
|
|
||||||
fig = plt.figure(figsize=(width / 100.0, height / 100.0), dpi=100)
|
fig = plt.figure(
|
||||||
ax = fig.add_axes([0.0, 0.0, 1.0, 1.0])
|
figsize=(width / 100.0, height / 100.0),
|
||||||
|
dpi=100
|
||||||
|
)
|
||||||
|
|
||||||
|
fig.patch.set_facecolor(bg_color)
|
||||||
|
|
||||||
|
ax = fig.add_axes([0, 0, 1, 1])
|
||||||
|
ax.set_facecolor(bg_color)
|
||||||
|
|
||||||
if kind == "cie1931":
|
if kind == "cie1931":
|
||||||
|
|
||||||
plot_chromaticity_diagram_CIE1931(
|
plot_chromaticity_diagram_CIE1931(
|
||||||
axes=ax, show=False, title=False,
|
axes=ax,
|
||||||
tight_layout=False, transparent_background=True,
|
show=False,
|
||||||
|
title=False,
|
||||||
|
tight_layout=False,
|
||||||
bounding_box=bbox,
|
bounding_box=bbox,
|
||||||
|
transparent_background=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
elif kind == "cie1976":
|
elif kind == "cie1976":
|
||||||
|
|
||||||
plot_chromaticity_diagram_CIE1976UCS(
|
plot_chromaticity_diagram_CIE1976UCS(
|
||||||
axes=ax, show=False, title=False,
|
axes=ax,
|
||||||
tight_layout=False, transparent_background=True,
|
show=False,
|
||||||
|
title=False,
|
||||||
|
tight_layout=False,
|
||||||
bounding_box=bbox,
|
bounding_box=bbox,
|
||||||
|
transparent_background=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
plt.close(fig)
|
plt.close(fig)
|
||||||
raise ValueError(f"unknown diagram kind: {kind!r}")
|
raise ValueError(f"unknown diagram kind: {kind}")
|
||||||
|
|
||||||
ax.set_xlim(xmin, xmax)
|
ax.set_xlim(xmin, xmax)
|
||||||
ax.set_ylim(ymin, ymax)
|
ax.set_ylim(ymin, ymax)
|
||||||
|
|
||||||
ax.set_axis_off()
|
ax.set_axis_off()
|
||||||
ax.set_position([0.0, 0.0, 1.0, 1.0])
|
ax.set_position([0, 0, 1, 1])
|
||||||
|
|
||||||
fig.canvas.draw()
|
fig.canvas.draw()
|
||||||
# 从 canvas 抓取 RGBA 数组
|
|
||||||
buf = np.asarray(fig.canvas.buffer_rgba()).copy()
|
buf = np.asarray(fig.canvas.buffer_rgba()).copy()
|
||||||
buf = np.flipud(buf)
|
buf = np.flipud(buf)
|
||||||
|
|
||||||
plt.close(fig)
|
plt.close(fig)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -115,52 +169,107 @@ def _render_chromaticity(kind: str, bbox: _BBox) -> np.ndarray:
|
|||||||
return buf
|
return buf
|
||||||
|
|
||||||
|
|
||||||
def _load_or_render(kind: str, bbox: _BBox) -> np.ndarray:
|
# ----------------------------
|
||||||
key = _cache_key(kind, bbox)
|
# load / render
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
|
def _load_or_render(kind: str, bbox: _BBox, mode: str) -> np.ndarray:
|
||||||
|
|
||||||
|
key = _cache_key(kind, bbox, mode)
|
||||||
|
|
||||||
with _lock:
|
with _lock:
|
||||||
|
|
||||||
if key in _memory_cache:
|
if key in _memory_cache:
|
||||||
return _memory_cache[key]
|
return _memory_cache[key]
|
||||||
|
|
||||||
disk = _cache_path(kind, bbox)
|
disk = _cache_path(kind, bbox, mode)
|
||||||
|
|
||||||
if os.path.isfile(disk):
|
if os.path.isfile(disk):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
arr = np.load(disk)
|
arr = np.load(disk)
|
||||||
|
|
||||||
_memory_cache[key] = arr
|
_memory_cache[key] = arr
|
||||||
|
|
||||||
return arr
|
return arr
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
# 缓存损坏则重新渲染
|
|
||||||
try:
|
try:
|
||||||
os.remove(disk)
|
os.remove(disk)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
arr = _render_chromaticity(kind, bbox)
|
arr = _render_chromaticity(kind, bbox, mode)
|
||||||
|
|
||||||
_memory_cache[key] = arr
|
_memory_cache[key] = arr
|
||||||
|
|
||||||
try:
|
try:
|
||||||
np.save(disk, arr)
|
np.save(disk, arr)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return arr
|
return arr
|
||||||
|
|
||||||
|
|
||||||
def get_cie1931_background() -> Tuple[np.ndarray, _BBox]:
|
# ----------------------------
|
||||||
"""返回 (RGBA 数组, bbox),可直接 ax.imshow(arr, extent=[*bbox])。"""
|
# public API
|
||||||
return _load_or_render("cie1931", _CIE1931_BBOX), _CIE1931_BBOX
|
# ----------------------------
|
||||||
|
|
||||||
|
def get_cie1931_background(mode: str = "dark") -> Tuple[np.ndarray, _BBox]:
|
||||||
|
"""
|
||||||
|
获取 CIE1931 背景图
|
||||||
|
|
||||||
|
mode:
|
||||||
|
"dark"
|
||||||
|
"light"
|
||||||
|
"""
|
||||||
|
|
||||||
|
return _load_or_render("cie1931", _CIE1931_BBOX, mode), _CIE1931_BBOX
|
||||||
|
|
||||||
|
|
||||||
def get_cie1976_background() -> Tuple[np.ndarray, _BBox]:
|
def get_cie1976_background(mode: str = "dark") -> Tuple[np.ndarray, _BBox]:
|
||||||
return _load_or_render("cie1976", _CIE1976_BBOX), _CIE1976_BBOX
|
|
||||||
|
|
||||||
|
return _load_or_render("cie1976", _CIE1976_BBOX, mode), _CIE1976_BBOX
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# cache control
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
def clear_cache(*, disk: bool = False) -> None:
|
def clear_cache(*, disk: bool = False) -> None:
|
||||||
"""清空内存缓存(可选连同磁盘)。供调试/样式调整时使用。"""
|
"""
|
||||||
|
清空缓存
|
||||||
|
"""
|
||||||
|
|
||||||
with _lock:
|
with _lock:
|
||||||
|
|
||||||
_memory_cache.clear()
|
_memory_cache.clear()
|
||||||
|
|
||||||
if disk:
|
if disk:
|
||||||
|
|
||||||
d = _cache_dir()
|
d = _cache_dir()
|
||||||
|
|
||||||
for name in os.listdir(d):
|
for name in os.listdir(d):
|
||||||
if name.startswith("chromaticity_") and name.endswith(".npy"):
|
|
||||||
|
if name.startswith("chromaticity_"):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.remove(os.path.join(d, name))
|
os.remove(os.path.join(d, name))
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# warmup
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
|
def warmup_cache(mode: str = "dark") -> None:
|
||||||
|
"""
|
||||||
|
启动预热缓存
|
||||||
|
|
||||||
|
可在软件启动时调用,避免首次绘图卡顿。
|
||||||
|
"""
|
||||||
|
|
||||||
|
get_cie1931_background(mode)
|
||||||
|
get_cie1976_background(mode)
|
||||||
@@ -9,15 +9,10 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from matplotlib.patches import Rectangle
|
from matplotlib.patches import Rectangle
|
||||||
from matplotlib.lines import Line2D
|
from matplotlib.lines import Line2D
|
||||||
import matplotlib.colors as mcolors
|
from matplotlib.ticker import MultipleLocator, AutoMinorLocator
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from app.views.modern_styles import get_theme_palette
|
from app.views.modern_styles import get_theme_palette
|
||||||
|
|
||||||
from app.plots.gamut_background import get_cie1976_background
|
from app.plots.gamut_background import get_cie1976_background
|
||||||
from app.tests.color_accuracy import get_accuracy_color_standards
|
from app.tests.color_accuracy import get_accuracy_color_standards
|
||||||
from app.pq.color_patch_map import get_patch_color
|
|
||||||
from app.pq.color_patch_map import get_patch_color_from_xy
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from pqAutomationApp import PQAutomationApp
|
from pqAutomationApp import PQAutomationApp
|
||||||
@@ -68,13 +63,13 @@ def _xy_to_uv(x: float, y: float):
|
|||||||
return 0.0, 0.0
|
return 0.0, 0.0
|
||||||
return (4.0 * x) / denom, (9.0 * y) / denom
|
return (4.0 * x) / denom, (9.0 * y) / denom
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 子图:左侧 Calman 风格面板
|
# 子图:左侧 Calman 风格面板
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def _draw_left_panel(ax, color_patches, delta_e_values, font_scale=1.0, dark_mode=False):
|
def _draw_left_panel(ax, color_patches, delta_e_values, font_scale=1.0, dark_mode=False):
|
||||||
"""左侧仅保留大条形图"""
|
"""左侧仅保留大条形图"""
|
||||||
|
|
||||||
ax.clear()
|
ax.clear()
|
||||||
|
|
||||||
n = len(color_patches)
|
n = len(color_patches)
|
||||||
@@ -85,37 +80,43 @@ def _draw_left_panel(ax, color_patches, delta_e_values, font_scale=1.0, dark_mod
|
|||||||
y_pos = list(range(n))
|
y_pos = list(range(n))
|
||||||
bar_colors = [_COLOR_MAP.get(name, "#888888") for name in color_patches]
|
bar_colors = [_COLOR_MAP.get(name, "#888888") for name in color_patches]
|
||||||
|
|
||||||
|
edgecolor = "#F3F5F7" if dark_mode else "#202020"
|
||||||
|
text_color = "#F3F5F7" if dark_mode else "#111111"
|
||||||
|
bg_color = "#0F1115" if dark_mode else "#FFFFFF"
|
||||||
|
|
||||||
ax.barh(
|
ax.barh(
|
||||||
y_pos,
|
y_pos,
|
||||||
delta_e_values,
|
delta_e_values,
|
||||||
height=0.72,
|
height=0.72,
|
||||||
color=bar_colors,
|
color=bar_colors,
|
||||||
edgecolor="#202020",
|
edgecolor=edgecolor,
|
||||||
linewidth=0.5,
|
linewidth=0.5,
|
||||||
zorder=3,
|
zorder=3,
|
||||||
)
|
)
|
||||||
|
|
||||||
text_color = "#F3F5F7" if dark_mode else "#111111"
|
|
||||||
bg_color = "#0F1115" if dark_mode else "#FFFFFF"
|
|
||||||
spine_color = "#8C8F94" if dark_mode else "#9A9A9A"
|
|
||||||
|
|
||||||
ax.set_yticks(y_pos)
|
ax.set_yticks(y_pos)
|
||||||
ax.set_yticklabels(color_patches, fontsize=max(5, 7 * font_scale), color=text_color)
|
ax.set_yticklabels(color_patches, fontsize=max(5, 7 * font_scale), color=text_color)
|
||||||
ax.invert_yaxis()
|
ax.invert_yaxis()
|
||||||
|
|
||||||
x_max = max(15.0, max(delta_e_values) * 1.15)
|
x_max = max(15.0, max(delta_e_values) * 1.15)
|
||||||
ax.set_xlim(0, x_max)
|
ax.set_xlim(0, x_max)
|
||||||
ax.tick_params(axis="x", labelsize=max(6, 8 * font_scale), colors=text_color)
|
|
||||||
ax.grid(axis="x", linestyle="-", linewidth=0.6, alpha=0.3, zorder=0)
|
ax.tick_params(
|
||||||
ax.grid(axis="y", linestyle=":", linewidth=0.35, alpha=0.15, zorder=0)
|
axis="x",
|
||||||
|
labelsize=max(6, 8 * font_scale),
|
||||||
|
colors=text_color
|
||||||
|
)
|
||||||
|
|
||||||
|
ax.tick_params(
|
||||||
|
axis="y",
|
||||||
|
labelsize=max(5, 7 * font_scale),
|
||||||
|
colors=text_color
|
||||||
|
)
|
||||||
|
|
||||||
ax.set_facecolor(bg_color)
|
ax.set_facecolor(bg_color)
|
||||||
for spine in ax.spines.values():
|
|
||||||
spine.set_color(spine_color)
|
|
||||||
spine.set_linewidth(0.9)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 自动 minor tick
|
||||||
|
ax.xaxis.set_minor_locator(AutoMinorLocator(2))
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -126,7 +127,7 @@ def _draw_uv_diagram(ax, color_patches, measurements, standards, font_scale=1.0,
|
|||||||
"""绘制 CIE 1976 u'v' 上的色准对比。"""
|
"""绘制 CIE 1976 u'v' 上的色准对比。"""
|
||||||
ax.clear()
|
ax.clear()
|
||||||
try:
|
try:
|
||||||
bg, bbox = get_cie1976_background()
|
bg, bbox = get_cie1976_background(mode="dark" if dark_mode else "light")
|
||||||
if bg.shape[-1] == 4:
|
if bg.shape[-1] == 4:
|
||||||
bg = bg[:, :, :3]
|
bg = bg[:, :, :3]
|
||||||
xmin, xmax, ymin, ymax = bbox
|
xmin, xmax, ymin, ymax = bbox
|
||||||
@@ -154,6 +155,11 @@ def _draw_uv_diagram(ax, color_patches, measurements, standards, font_scale=1.0,
|
|||||||
|
|
||||||
ax.set_facecolor("#000" if dark_mode else "#FFFFFF")
|
ax.set_facecolor("#000" if dark_mode else "#FFFFFF")
|
||||||
ax.set_aspect("equal", adjustable="box")
|
ax.set_aspect("equal", adjustable="box")
|
||||||
|
ax.xaxis.set_major_locator(MultipleLocator(0.1))
|
||||||
|
ax.yaxis.set_major_locator(MultipleLocator(0.1))
|
||||||
|
|
||||||
|
ax.xaxis.set_minor_locator(MultipleLocator(0.02))
|
||||||
|
ax.yaxis.set_minor_locator(MultipleLocator(0.02))
|
||||||
|
|
||||||
ax.set_title(
|
ax.set_title(
|
||||||
"CIE 1976 u'v'",
|
"CIE 1976 u'v'",
|
||||||
@@ -309,7 +315,7 @@ def _draw_result_judgement(ax, accuracy_data, font_scale=1.0, dark_mode=False):
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type):
|
def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type):
|
||||||
"""绘制色准测试结果 - Calman 风格(色块 + CIE 1976 u'v' + 统计)。"""
|
"""绘制色准测试结果"""
|
||||||
palette = get_theme_palette()
|
palette = get_theme_palette()
|
||||||
try:
|
try:
|
||||||
from app.views.theme_manager import is_dark
|
from app.views.theme_manager import is_dark
|
||||||
@@ -329,10 +335,14 @@ def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type):
|
|||||||
|
|
||||||
fig.patch.set_facecolor(palette["bg"])
|
fig.patch.set_facecolor(palette["bg"])
|
||||||
|
|
||||||
# 根据当前画布像素尺寸动态缩放字体,避免窗口缩小时文字挤压重叠。
|
# 先确保色准页签已激活,再读取真实画布尺寸进行动态缩放。
|
||||||
font_scale = 1.0
|
font_scale = 1.0
|
||||||
try:
|
try:
|
||||||
|
self.chart_notebook.select(self.accuracy_chart_frame)
|
||||||
|
self.chart_notebook.update_idletasks()
|
||||||
canvas_widget = self.accuracy_canvas.get_tk_widget()
|
canvas_widget = self.accuracy_canvas.get_tk_widget()
|
||||||
|
canvas_widget.update_idletasks()
|
||||||
|
canvas_widget.update()
|
||||||
cw = max(1, int(canvas_widget.winfo_width()))
|
cw = max(1, int(canvas_widget.winfo_width()))
|
||||||
ch = max(1, int(canvas_widget.winfo_height()))
|
ch = max(1, int(canvas_widget.winfo_height()))
|
||||||
font_scale = min(cw / 1000.0, ch / 600.0)
|
font_scale = min(cw / 1000.0, ch / 600.0)
|
||||||
@@ -417,11 +427,13 @@ def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Select the tab first so the canvas is visible and winfo_width/height
|
# 重新刷新布局并绘制,确保画布尺寸与 notebook tab 对齐。
|
||||||
# return real pixel dimensions before the figure is rendered.
|
|
||||||
self.chart_notebook.select(self.accuracy_chart_frame)
|
self.chart_notebook.select(self.accuracy_chart_frame)
|
||||||
self.accuracy_canvas.get_tk_widget().update_idletasks()
|
canvas_widget = self.accuracy_canvas.get_tk_widget()
|
||||||
|
canvas_widget.update_idletasks()
|
||||||
|
canvas_widget.update()
|
||||||
self.accuracy_canvas.draw()
|
self.accuracy_canvas.draw()
|
||||||
|
canvas_widget.update_idletasks()
|
||||||
|
|
||||||
|
|
||||||
class PlotAccuracyMixin:
|
class PlotAccuracyMixin:
|
||||||
|
|||||||
@@ -83,6 +83,15 @@ def plot_cct(self: "PQAutomationApp", test_type):
|
|||||||
self.log_gui.log(f" y范围: {min(y_measured):.6f} - {max(y_measured):.6f}", level="info")
|
self.log_gui.log(f" y范围: {min(y_measured):.6f} - {max(y_measured):.6f}", level="info")
|
||||||
|
|
||||||
# ========== 根据测试类型读取对应参数 ==========
|
# ========== 根据测试类型读取对应参数 ==========
|
||||||
|
# 屏模组中心坐标优先使用实测 100% 灰阶点(gray 数据第 1 个点)
|
||||||
|
screen_center_xy = None
|
||||||
|
if test_type == "screen_module":
|
||||||
|
try:
|
||||||
|
if gray_data and len(gray_data[0]) >= 2:
|
||||||
|
screen_center_xy = (float(gray_data[0][0]), float(gray_data[0][1]))
|
||||||
|
except Exception:
|
||||||
|
screen_center_xy = None
|
||||||
|
|
||||||
if test_type == "sdr_movie":
|
if test_type == "sdr_movie":
|
||||||
try:
|
try:
|
||||||
x_ideal = float(self.sdr_cct_x_ideal_var.get())
|
x_ideal = float(self.sdr_cct_x_ideal_var.get())
|
||||||
@@ -111,11 +120,23 @@ def plot_cct(self: "PQAutomationApp", test_type):
|
|||||||
self.log_gui.log("HDR 参数读取失败,使用默认值", level="error")
|
self.log_gui.log("HDR 参数读取失败,使用默认值", level="error")
|
||||||
else: # screen_module
|
else: # screen_module
|
||||||
try:
|
try:
|
||||||
x_ideal = float(self.cct_x_ideal_var.get())
|
|
||||||
x_tolerance = float(self.cct_x_tolerance_var.get())
|
x_tolerance = float(self.cct_x_tolerance_var.get())
|
||||||
y_ideal = float(self.cct_y_ideal_var.get())
|
|
||||||
y_tolerance = float(self.cct_y_tolerance_var.get())
|
y_tolerance = float(self.cct_y_tolerance_var.get())
|
||||||
self.log_gui.log("使用屏模组色度参数", level="success")
|
if screen_center_xy is not None:
|
||||||
|
x_ideal, y_ideal = screen_center_xy
|
||||||
|
# 同步到输入框,避免界面显示和实际计算不一致
|
||||||
|
try:
|
||||||
|
self.cct_x_ideal_var.set(f"{x_ideal:.6f}")
|
||||||
|
self.cct_y_ideal_var.set(f"{y_ideal:.6f}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.log_gui.log(
|
||||||
|
f"屏模组中心使用实测100%坐标: x={x_ideal:.6f}, y={y_ideal:.6f}"
|
||||||
|
, level="success")
|
||||||
|
else:
|
||||||
|
x_ideal = float(self.cct_x_ideal_var.get())
|
||||||
|
y_ideal = float(self.cct_y_ideal_var.get())
|
||||||
|
self.log_gui.log("未取到实测100%点,回退到屏模组色度参数", level="error")
|
||||||
except:
|
except:
|
||||||
x_ideal = 0.306
|
x_ideal = 0.306
|
||||||
x_tolerance = 0.003
|
x_tolerance = 0.003
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ def plot_gamut(self: "PQAutomationApp", results, coverage, test_type):
|
|||||||
# 左图:CIE 1931 xy
|
# 左图:CIE 1931 xy
|
||||||
# ============================================================
|
# ============================================================
|
||||||
try:
|
try:
|
||||||
bg_xy, bbox_xy = get_cie1931_background()
|
bg_xy, bbox_xy = get_cie1931_background(mode="dark" if dark_mode else "light")
|
||||||
_blit_background(ax_xy, bg_xy, bbox_xy)
|
_blit_background(ax_xy, bg_xy, bbox_xy)
|
||||||
_style_axes(
|
_style_axes(
|
||||||
ax_xy,
|
ax_xy,
|
||||||
@@ -343,7 +343,7 @@ def plot_gamut(self: "PQAutomationApp", results, coverage, test_type):
|
|||||||
# 右图:CIE 1976 u'v'
|
# 右图:CIE 1976 u'v'
|
||||||
# ============================================================
|
# ============================================================
|
||||||
try:
|
try:
|
||||||
bg_uv, bbox_uv = get_cie1976_background()
|
bg_uv, bbox_uv = get_cie1976_background(mode="dark" if dark_mode else "light")
|
||||||
_blit_background(ax_uv, bg_uv, bbox_uv)
|
_blit_background(ax_uv, bg_uv, bbox_uv)
|
||||||
_style_axes(
|
_style_axes(
|
||||||
ax_uv,
|
ax_uv,
|
||||||
|
|||||||
@@ -341,9 +341,9 @@ def send_fix_pattern(self: "PQAutomationApp", mode):
|
|||||||
self.pattern_service.send_session_pattern(session, 0)
|
self.pattern_service.send_session_pattern(session, 0)
|
||||||
|
|
||||||
if format_changed:
|
if format_changed:
|
||||||
signal_settle = max(1.0, float(getattr(self, "signal_settle_time", 5.0)))
|
signal_settle = max(1.0, float(getattr(self, "signal_settle_time", 4.0)))
|
||||||
self.log_gui.log(
|
self.log_gui.log(
|
||||||
f"信号格式已变化,等待电视重新锁定: {signal_settle:.1f}s(可通过 signal_settle_time 调整)",
|
f"信号格式已变化,等待电视重新锁定: {signal_settle:.1f}s",
|
||||||
level="info",
|
level="info",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -831,6 +831,20 @@ def test_cct(self: "PQAutomationApp", test_type, gray_data=None):
|
|||||||
|
|
||||||
self.log_gui.log(f"使用 {len(results)} 个灰阶数据点进行色度计算", level="info")
|
self.log_gui.log(f"使用 {len(results)} 个灰阶数据点进行色度计算", level="info")
|
||||||
|
|
||||||
|
# 屏模组测试:中心坐标直接使用本次灰阶 100% 实测值(第 1 个点)
|
||||||
|
if test_type == "screen_module":
|
||||||
|
try:
|
||||||
|
if results and len(results[0]) >= 2:
|
||||||
|
x_100 = float(results[0][0])
|
||||||
|
y_100 = float(results[0][1])
|
||||||
|
self.cct_x_ideal_var.set(f"{x_100:.6f}")
|
||||||
|
self.cct_y_ideal_var.set(f"{y_100:.6f}")
|
||||||
|
self.log_gui.log(
|
||||||
|
f"屏模组 CCT 中心采用 100% 实测值: x={x_100:.6f}, y={y_100:.6f}"
|
||||||
|
, level="success")
|
||||||
|
except Exception as e:
|
||||||
|
self.log_gui.log(f"同步屏模组100%中心坐标失败: {str(e)}", level="error")
|
||||||
|
|
||||||
# 提取色度坐标
|
# 提取色度坐标
|
||||||
cct_values = pq_algorithm.calculate_cct_from_results(results)
|
cct_values = pq_algorithm.calculate_cct_from_results(results)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import atexit
|
|||||||
import csv
|
import csv
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
@@ -15,6 +16,7 @@ import time
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import filedialog, messagebox
|
from tkinter import filedialog, messagebox
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
@@ -34,6 +36,9 @@ DEFAULT_WINDOW_PERCENTAGES = [1, 2, 5, 10, 18, 25, 50, 75, 100]
|
|||||||
DEFAULT_CHESSBOARD_GRID = 5
|
DEFAULT_CHESSBOARD_GRID = 5
|
||||||
INSTANT_PEAK_WINDOW_PERCENTAGE = 10
|
INSTANT_PEAK_WINDOW_PERCENTAGE = 10
|
||||||
INSTANT_PEAK_CAPTURE_DELAY = 0.5
|
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
|
_TEMP_DIR = None
|
||||||
_IMAGE_CACHE = {} # {(width, height, percentage): file_path}
|
_IMAGE_CACHE = {} # {(width, height, percentage): file_path}
|
||||||
@@ -63,8 +68,8 @@ def _get_temp_dir():
|
|||||||
return _TEMP_DIR
|
return _TEMP_DIR
|
||||||
|
|
||||||
|
|
||||||
def _make_window_image_array(width, height, percentage):
|
def _make_window_image_array(width, height, percentage, window_level=255):
|
||||||
"""生成黑底+居中白窗的 numpy 图像,保持屏幕比例。"""
|
"""生成黑底+居中窗口图像,保持屏幕比例。"""
|
||||||
image = np.zeros((height, width, 3), dtype=np.uint8)
|
image = np.zeros((height, width, 3), dtype=np.uint8)
|
||||||
if percentage >= 100:
|
if percentage >= 100:
|
||||||
ww, wh = width, height
|
ww, wh = width, height
|
||||||
@@ -74,18 +79,19 @@ def _make_window_image_array(width, height, percentage):
|
|||||||
wh = int(height * scale)
|
wh = int(height * scale)
|
||||||
x1 = (width - ww) // 2
|
x1 = (width - ww) // 2
|
||||||
y1 = (height - wh) // 2
|
y1 = (height - wh) // 2
|
||||||
image[y1:y1 + wh, x1:x1 + ww] = 255
|
image[y1:y1 + wh, x1:x1 + ww] = int(window_level)
|
||||||
return image
|
return image
|
||||||
|
|
||||||
|
|
||||||
def _ensure_window_image(width, height, percentage):
|
def _ensure_window_image(width, height, percentage, window_level=255):
|
||||||
"""生成或复用缓存的窗口 PNG 文件,返回路径。"""
|
"""生成或复用缓存的窗口 PNG 文件,返回路径。"""
|
||||||
key = (width, height, percentage)
|
level = max(0, min(255, int(window_level)))
|
||||||
|
key = (width, height, percentage, level)
|
||||||
cached = _IMAGE_CACHE.get(key)
|
cached = _IMAGE_CACHE.get(key)
|
||||||
if cached and os.path.exists(cached):
|
if cached and os.path.exists(cached):
|
||||||
return cached
|
return cached
|
||||||
arr = _make_window_image_array(width, height, percentage)
|
arr = _make_window_image_array(width, height, percentage, level)
|
||||||
fname = f"window_{width}x{height}_{percentage:03d}percent.png"
|
fname = f"window_{width}x{height}_{percentage:03d}percent_{level:03d}lv.png"
|
||||||
path = os.path.join(_get_temp_dir(), fname)
|
path = os.path.join(_get_temp_dir(), fname)
|
||||||
Image.fromarray(arr, mode="RGB").save(path, format="PNG")
|
Image.fromarray(arr, mode="RGB").save(path, format="PNG")
|
||||||
_IMAGE_CACHE[key] = path
|
_IMAGE_CACHE[key] = path
|
||||||
@@ -151,36 +157,6 @@ def _ensure_checkerboard_image(width, height, grid_size, center_white):
|
|||||||
_IMAGE_CACHE[key] = path
|
_IMAGE_CACHE[key] = path
|
||||||
return 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:
|
def _apply_ld_ucd_params(self: "PQAutomationApp") -> bool:
|
||||||
"""发送 Local Dimming 图案前,按当前测试类型写入 UCD 参数。"""
|
"""发送 Local Dimming 图案前,按当前测试类型写入 UCD 参数。"""
|
||||||
test_type = getattr(self.config, "current_test_type", "screen_module")
|
test_type = getattr(self.config, "current_test_type", "screen_module")
|
||||||
@@ -309,231 +285,445 @@ def _apply_ld_ucd_params(self: "PQAutomationApp") -> bool:
|
|||||||
return False
|
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):
|
def _set_current_ld_pattern(self: "PQAutomationApp", test_item, pattern_label, percentage=None):
|
||||||
self.current_ld_test_item = test_item
|
self.current_ld_test_item = test_item
|
||||||
self.current_ld_pattern_label = pattern_label
|
self.current_ld_pattern_label = pattern_label
|
||||||
self.current_ld_percentage = percentage
|
self.current_ld_percentage = percentage
|
||||||
|
|
||||||
|
def _send_ld_pattern_async(self: "PQAutomationApp", image_builder, success_msg, fail_msg):
|
||||||
|
"""统一的 Local Dimming 图案发送线程"""
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
def worker():
|
||||||
# GUI 入口(绑定为 PQAutomationApp 方法)
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
self.log_gui.log(f"🔆 发送 {percentage}% 窗口...", level="info")
|
|
||||||
_set_current_ld_pattern(self, "峰值亮度", f"{percentage}%窗口", percentage)
|
|
||||||
|
|
||||||
def send():
|
|
||||||
if not _apply_ld_ucd_params(self):
|
if not _apply_ld_ucd_params(self):
|
||||||
return
|
return
|
||||||
|
|
||||||
width, height = self.signal_service.current_resolution()
|
width, height = self.signal_service.current_resolution()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
image_path = _ensure_window_image(width, height, percentage)
|
image_path = image_builder(width, height)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._dispatch_ui(self.log_gui.log, f"图像生成失败: {e}")
|
self._dispatch_ui(self.log_gui.log, f"图像生成失败: {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.signal_service.send_image(image_path)
|
self.signal_service.send_image(image_path)
|
||||||
ok = True
|
ok = True
|
||||||
except Exception:
|
except Exception:
|
||||||
ok = False
|
ok = False
|
||||||
msg = (
|
|
||||||
f"{percentage}% 窗口已发送" if ok
|
msg = success_msg if ok else fail_msg
|
||||||
else f"{percentage}% 窗口发送失败"
|
|
||||||
)
|
|
||||||
self._dispatch_ui(self.log_gui.log, 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:
|
||||||
|
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):
|
def send_ld_checkerboard(self: "PQAutomationApp", center_white):
|
||||||
"""发送棋盘格图案(手动模式)。"""
|
|
||||||
if not self.signal_service.is_connected:
|
if not self.signal_service.is_connected:
|
||||||
messagebox.showwarning("警告", "请先连接 UCD323 设备")
|
messagebox.showwarning("警告", "请先连接 UCD323 设备")
|
||||||
return
|
return
|
||||||
|
|
||||||
pattern_label = "棋盘格(中心白)" if center_white else "棋盘格(中心黑)"
|
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)
|
_set_current_ld_pattern(self, "棋盘格对比度", pattern_label)
|
||||||
|
|
||||||
def send():
|
def builder(width, height):
|
||||||
if not _apply_ld_ucd_params(self):
|
return _ensure_checkerboard_image(
|
||||||
return
|
width,
|
||||||
width, height = self.signal_service.current_resolution()
|
height,
|
||||||
try:
|
DEFAULT_CHESSBOARD_GRID,
|
||||||
image_path = _ensure_checkerboard_image(
|
center_white,
|
||||||
width,
|
)
|
||||||
height,
|
|
||||||
DEFAULT_CHESSBOARD_GRID,
|
|
||||||
center_white,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
self._dispatch_ui(self.log_gui.log, f"图像生成失败: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
_send_ld_pattern_async(
|
||||||
self.signal_service.send_image(image_path)
|
self,
|
||||||
ok = True
|
builder,
|
||||||
except Exception:
|
f"{pattern_label} 已发送",
|
||||||
ok = False
|
f"{pattern_label} 发送失败",
|
||||||
|
)
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
def send_ld_black_pattern(self: "PQAutomationApp"):
|
def send_ld_black_pattern(self: "PQAutomationApp"):
|
||||||
"""发送全黑图案(手动模式)。"""
|
|
||||||
if not self.signal_service.is_connected:
|
if not self.signal_service.is_connected:
|
||||||
messagebox.showwarning("警告", "请先连接 UCD323 设备")
|
messagebox.showwarning("警告", "请先连接 UCD323 设备")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.log_gui.log("⚫ 发送全黑画面...", level="info")
|
self.log_gui.log("发送全黑画面...", level="info")
|
||||||
|
|
||||||
_set_current_ld_pattern(self, "黑电平", "全黑画面")
|
_set_current_ld_pattern(self, "黑电平", "全黑画面")
|
||||||
|
|
||||||
def send():
|
def builder(width, height):
|
||||||
if not _apply_ld_ucd_params(self):
|
return _ensure_solid_image(width, height, (0, 0, 0), "black")
|
||||||
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
|
|
||||||
|
|
||||||
try:
|
_send_ld_pattern_async(
|
||||||
self.signal_service.send_image(image_path)
|
self,
|
||||||
ok = True
|
builder,
|
||||||
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"):
|
def start_ld_instant_peak_tracking(self: "PQAutomationApp"):
|
||||||
"""发送瞬时峰值亮度图案:先黑场,再切到 10% 窗口并保持。"""
|
"""独立瞬时峰值测试:持续采样直到亮度回落或达到最长测量时长。"""
|
||||||
|
|
||||||
if not self.signal_service.is_connected:
|
if not self.signal_service.is_connected:
|
||||||
messagebox.showwarning("警告", "请先连接 UCD323 设备")
|
messagebox.showwarning("警告", "请先连接 UCD323 设备")
|
||||||
return
|
return
|
||||||
|
|
||||||
pattern_label = f"黑场后切 {INSTANT_PEAK_WINDOW_PERCENTAGE}%窗口"
|
if not self.ca:
|
||||||
self.log_gui.log(f"⚡ 发送瞬时峰值图案: {pattern_label}", level="info")
|
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(
|
_set_current_ld_pattern(
|
||||||
self,
|
self,
|
||||||
"瞬时峰值亮度",
|
"瞬时峰值亮度",
|
||||||
pattern_label,
|
pattern_label,
|
||||||
INSTANT_PEAK_WINDOW_PERCENTAGE,
|
window_percentage,
|
||||||
)
|
)
|
||||||
|
|
||||||
def send():
|
if hasattr(self, "ld_peak_start_btn"):
|
||||||
if not _apply_ld_ucd_params(self):
|
self.ld_peak_start_btn.configure(state="disabled")
|
||||||
return
|
|
||||||
width, height = self.signal_service.current_resolution()
|
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:
|
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")
|
black_image = _ensure_solid_image(width, height, (0, 0, 0), "black")
|
||||||
peak_image = _ensure_window_image(
|
peak_image = _ensure_window_image(
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
INSTANT_PEAK_WINDOW_PERCENTAGE,
|
window_percentage,
|
||||||
|
window_level,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
|
||||||
self._dispatch_ui(self.log_gui.log, f"图像生成失败: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
# 黑场预置
|
||||||
self.signal_service.send_image(black_image)
|
self.signal_service.send_image(black_image)
|
||||||
time.sleep(INSTANT_PEAK_CAPTURE_DELAY)
|
time.sleep(INSTANT_PEAK_CAPTURE_DELAY)
|
||||||
|
|
||||||
|
# 切窗口
|
||||||
self.signal_service.send_image(peak_image)
|
self.signal_service.send_image(peak_image)
|
||||||
ok = True
|
|
||||||
except Exception:
|
|
||||||
ok = False
|
|
||||||
|
|
||||||
msg = (
|
started = time.time()
|
||||||
f"瞬时峰值图案已发送,当前保持 {INSTANT_PEAK_WINDOW_PERCENTAGE}%窗口"
|
|
||||||
if ok else
|
|
||||||
"瞬时峰值图案发送失败"
|
|
||||||
)
|
|
||||||
self._dispatch_ui(self.log_gui.log, msg)
|
|
||||||
|
|
||||||
threading.Thread(target=send, daemon=True).start()
|
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"):
|
def measure_ld_luminance(self: "PQAutomationApp"):
|
||||||
@@ -545,8 +735,6 @@ def measure_ld_luminance(self: "PQAutomationApp"):
|
|||||||
messagebox.showinfo("提示", "请先发送一个窗口图案")
|
messagebox.showinfo("提示", "请先发送一个窗口图案")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.log_gui.log("📏 正在采集亮度...", level="info")
|
|
||||||
|
|
||||||
def measure():
|
def measure():
|
||||||
try:
|
try:
|
||||||
x, y, lv, _X, _Y, _Z = self.read_ca_xyLv()
|
x, y, lv, _X, _Y, _Z = self.read_ca_xyLv()
|
||||||
@@ -562,7 +750,7 @@ def measure_ld_luminance(self: "PQAutomationApp"):
|
|||||||
text=f"亮度: {lv:.2f} cd/m² | x: {x:.4f} | y: {y:.4f}",
|
text=f"亮度: {lv:.2f} cd/m² | x: {x:.4f} | y: {y:.4f}",
|
||||||
)
|
)
|
||||||
self._dispatch_ui(
|
self._dispatch_ui(
|
||||||
self.ld_tree.insert, "", tk.END,
|
self._insert_ld_tree_item,
|
||||||
values=(
|
values=(
|
||||||
getattr(self, "current_ld_test_item", "手动采集"),
|
getattr(self, "current_ld_test_item", "手动采集"),
|
||||||
self.current_ld_pattern_label,
|
self.current_ld_pattern_label,
|
||||||
@@ -585,6 +773,7 @@ def clear_ld_records(self: "PQAutomationApp"):
|
|||||||
self.current_ld_percentage = None
|
self.current_ld_percentage = None
|
||||||
self.current_ld_test_item = None
|
self.current_ld_test_item = None
|
||||||
self.current_ld_pattern_label = None
|
self.current_ld_pattern_label = None
|
||||||
|
self.ld_peak_tracking = False
|
||||||
self.log_gui.log("测试记录已清空", level="info")
|
self.log_gui.log("测试记录已清空", level="info")
|
||||||
|
|
||||||
|
|
||||||
@@ -619,17 +808,111 @@ def save_local_dimming_results(self: "PQAutomationApp"):
|
|||||||
messagebox.showerror("错误", f"保存失败: {str(e)}")
|
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:
|
class LocalDimmingMixin:
|
||||||
"""由 tools/refactor_to_mixins.py 自动生成。
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
把本模块的自由函数挂到 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_window = send_ld_window
|
||||||
|
send_ld_manual_window = send_ld_manual_window
|
||||||
send_ld_checkerboard = send_ld_checkerboard
|
send_ld_checkerboard = send_ld_checkerboard
|
||||||
send_ld_black_pattern = send_ld_black_pattern
|
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
|
measure_ld_luminance = measure_ld_luminance
|
||||||
clear_ld_records = clear_ld_records
|
clear_ld_records = clear_ld_records
|
||||||
save_local_dimming_results = save_local_dimming_results
|
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
|
||||||
|
|||||||
@@ -28,6 +28,16 @@ def show_panel(self: "PQAutomationApp", panel_name):
|
|||||||
# 如果当前面板就是要显示的面板,则隐藏它
|
# 如果当前面板就是要显示的面板,则隐藏它
|
||||||
if self.current_panel == panel_name:
|
if self.current_panel == panel_name:
|
||||||
self.hide_all_panels()
|
self.hide_all_panels()
|
||||||
|
# 如果当前测试类型是 Local Dimming,则在关闭日志等面板后自动恢复 Local Dimming 面板
|
||||||
|
try:
|
||||||
|
if (
|
||||||
|
getattr(self, "config", None)
|
||||||
|
and getattr(self.config, "current_test_type", None) == "local_dimming"
|
||||||
|
and panel_name != "local_dimming"
|
||||||
|
):
|
||||||
|
self.show_panel("local_dimming")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return
|
return
|
||||||
|
|
||||||
# 隐藏所有面板
|
# 隐藏所有面板
|
||||||
|
|||||||
@@ -415,9 +415,7 @@ def toggle_ai_image_panel(self: "PQAutomationApp"):
|
|||||||
self.show_panel("ai_image")
|
self.show_panel("ai_image")
|
||||||
_apply_ai_image_list_style(self)
|
_apply_ai_image_list_style(self)
|
||||||
if not getattr(self, "_ai_image_list_loaded", False):
|
if not getattr(self, "_ai_image_list_loaded", False):
|
||||||
logger.info("[AIImagePanel] 首次显示面板,开始加载列表")
|
_start_new_session(self)
|
||||||
reload_ai_image_list(self)
|
|
||||||
self._ai_image_list_loaded = True
|
|
||||||
|
|
||||||
|
|
||||||
def _get_app_base_dir(self: "PQAutomationApp") -> str:
|
def _get_app_base_dir(self: "PQAutomationApp") -> str:
|
||||||
@@ -618,7 +616,6 @@ def _on_list_select(self: "PQAutomationApp"):
|
|||||||
if getattr(self, "_ai_image_reloading", False):
|
if getattr(self, "_ai_image_reloading", False):
|
||||||
return
|
return
|
||||||
if getattr(self, "_ai_image_select_guard", False):
|
if getattr(self, "_ai_image_select_guard", False):
|
||||||
logger.debug("[AIImagePanel] 忽略重入选择事件")
|
|
||||||
return
|
return
|
||||||
sel = self.ai_image_tree.selection()
|
sel = self.ai_image_tree.selection()
|
||||||
if not sel:
|
if not sel:
|
||||||
@@ -631,7 +628,6 @@ def _on_list_select(self: "PQAutomationApp"):
|
|||||||
if ridx is None:
|
if ridx is None:
|
||||||
session_id = _session_id_for_item(self, item_id)
|
session_id = _session_id_for_item(self, item_id)
|
||||||
if session_id:
|
if session_id:
|
||||||
logger.info("[AIImagePanel] 选中会话头 sid=%s", session_id[:8])
|
|
||||||
_switch_to_session(self, session_id, show_message=False, refresh_list=False)
|
_switch_to_session(self, session_id, show_message=False, refresh_list=False)
|
||||||
return
|
return
|
||||||
if 0 <= ridx < len(self.ai_image_records):
|
if 0 <= ridx < len(self.ai_image_records):
|
||||||
@@ -848,7 +844,7 @@ def _switch_to_session(
|
|||||||
if sid == _svc.get_session_id():
|
if sid == _svc.get_session_id():
|
||||||
return
|
return
|
||||||
_svc.set_session_id(sid)
|
_svc.set_session_id(sid)
|
||||||
logger.info(
|
logger.debug(
|
||||||
"[AIImagePanel] 切换会话 sid=%s refresh=%s target=%s",
|
"[AIImagePanel] 切换会话 sid=%s refresh=%s target=%s",
|
||||||
sid[:8],
|
sid[:8],
|
||||||
refresh_list,
|
refresh_list,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ def create_cct_params_frame(self: "PQAutomationApp"):
|
|||||||
|
|
||||||
# 从配置读取屏模组参数
|
# 从配置读取屏模组参数
|
||||||
saved_params = self.config.current_test_types.get("screen_module", {}).get(
|
saved_params = self.config.current_test_types.get("screen_module", {}).get(
|
||||||
"cct_params", screen_default_cct_params.copy()
|
"cct_params", {}
|
||||||
)
|
)
|
||||||
|
|
||||||
# 色域参考标准
|
# 色域参考标准
|
||||||
@@ -45,15 +45,11 @@ def create_cct_params_frame(self: "PQAutomationApp"):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 创建屏模组变量
|
# 创建屏模组变量
|
||||||
self.cct_x_ideal_var = tk.StringVar(
|
self.cct_x_ideal_var = tk.StringVar(value="")
|
||||||
value=str(saved_params.get("x_ideal", 0.3127))
|
|
||||||
)
|
|
||||||
self.cct_x_tolerance_var = tk.StringVar(
|
self.cct_x_tolerance_var = tk.StringVar(
|
||||||
value=str(saved_params.get("x_tolerance", 0.003))
|
value=str(saved_params.get("x_tolerance", 0.003))
|
||||||
)
|
)
|
||||||
self.cct_y_ideal_var = tk.StringVar(
|
self.cct_y_ideal_var = tk.StringVar(value="")
|
||||||
value=str(saved_params.get("y_ideal", 0.3290))
|
|
||||||
)
|
|
||||||
self.cct_y_tolerance_var = tk.StringVar(
|
self.cct_y_tolerance_var = tk.StringVar(
|
||||||
value=str(saved_params.get("y_tolerance", 0.003))
|
value=str(saved_params.get("y_tolerance", 0.003))
|
||||||
)
|
)
|
||||||
@@ -74,12 +70,16 @@ def create_cct_params_frame(self: "PQAutomationApp"):
|
|||||||
entry = ttk.Entry(self.cct_params_frame, textvariable=var, width=15)
|
entry = ttk.Entry(self.cct_params_frame, textvariable=var, width=15)
|
||||||
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
|
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
|
||||||
|
|
||||||
# 绑定失去焦点事件
|
# 屏模组中心由实测 100% 点自动决定,避免手动误改。
|
||||||
default_val = screen_default_cct_params[key]
|
if key in ("x_ideal", "y_ideal"):
|
||||||
entry.bind(
|
entry.configure(state="readonly")
|
||||||
"<FocusOut>",
|
else:
|
||||||
lambda e, v=var, d=default_val: self.on_cct_param_focus_out(v, d),
|
# 绑定失去焦点事件
|
||||||
)
|
default_val = screen_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(
|
ttk.Label(self.cct_params_frame, text="色域参考标准:").grid(
|
||||||
@@ -665,15 +665,29 @@ def reload_cct_params(self: "PQAutomationApp"):
|
|||||||
saved_params = self.config.current_test_types.get(current_type, {}).get(
|
saved_params = self.config.current_test_types.get(current_type, {}).get(
|
||||||
"cct_params", None
|
"cct_params", None
|
||||||
)
|
)
|
||||||
|
default_params = self.config.get_default_cct_params(current_type)
|
||||||
|
|
||||||
if saved_params is None:
|
if saved_params is None:
|
||||||
saved_params = self.config.get_default_cct_params(current_type)
|
saved_params = {}
|
||||||
|
|
||||||
# 更新输入框的值
|
# 更新输入框的值
|
||||||
self.cct_x_ideal_var.set(str(saved_params["x_ideal"]))
|
if current_type == "screen_module":
|
||||||
self.cct_x_tolerance_var.set(str(saved_params["x_tolerance"]))
|
self.cct_x_ideal_var.set(
|
||||||
self.cct_y_ideal_var.set(str(saved_params["y_ideal"]))
|
str(saved_params["x_ideal"]) if "x_ideal" in saved_params else ""
|
||||||
self.cct_y_tolerance_var.set(str(saved_params["y_tolerance"]))
|
)
|
||||||
|
self.cct_y_ideal_var.set(
|
||||||
|
str(saved_params["y_ideal"]) if "y_ideal" in saved_params else ""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.cct_x_ideal_var.set(str(saved_params.get("x_ideal", default_params["x_ideal"])) )
|
||||||
|
self.cct_y_ideal_var.set(str(saved_params.get("y_ideal", default_params["y_ideal"])) )
|
||||||
|
|
||||||
|
self.cct_x_tolerance_var.set(
|
||||||
|
str(saved_params.get("x_tolerance", default_params["x_tolerance"]))
|
||||||
|
)
|
||||||
|
self.cct_y_tolerance_var.set(
|
||||||
|
str(saved_params.get("y_tolerance", default_params["y_tolerance"]))
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if hasattr(self, "log_gui"):
|
if hasattr(self, "log_gui"):
|
||||||
|
|||||||
@@ -194,12 +194,6 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"):
|
|||||||
"<<ComboboxSelected>>", lambda e: _on_preset_selected(self)
|
"<<ComboboxSelected>>", lambda e: _on_preset_selected(self)
|
||||||
)
|
)
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
preset_row1, text="加载",
|
|
||||||
bootstyle="info-outline", width=8,
|
|
||||||
command=lambda: _load_selected_preset(self),
|
|
||||||
).pack(side=tk.LEFT, padx=2)
|
|
||||||
|
|
||||||
ttk.Button(
|
ttk.Button(
|
||||||
preset_row1, text="应用为当前",
|
preset_row1, text="应用为当前",
|
||||||
bootstyle="success", width=12,
|
bootstyle="success", width=12,
|
||||||
@@ -274,7 +268,7 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"):
|
|||||||
right = ttk.Frame(mid)
|
right = ttk.Frame(mid)
|
||||||
right.grid(row=0, column=1, sticky=tk.NS)
|
right.grid(row=0, column=1, sticky=tk.NS)
|
||||||
|
|
||||||
edit_frame = ttk.LabelFrame(right, text="编辑选中点", padding=8)
|
edit_frame = ttk.LabelFrame(right, text="编辑选中点灯", padding=8)
|
||||||
edit_frame.pack(fill=tk.X)
|
edit_frame.pack(fill=tk.X)
|
||||||
|
|
||||||
self._gamma_edit_r_var = tk.StringVar()
|
self._gamma_edit_r_var = tk.StringVar()
|
||||||
@@ -358,27 +352,22 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"):
|
|||||||
command=lambda: _paste_from_clipboard(self),
|
command=lambda: _paste_from_clipboard(self),
|
||||||
).pack(fill=tk.X, pady=(6, 0))
|
).pack(fill=tk.X, pady=(6, 0))
|
||||||
|
|
||||||
# ===== 底部 =====
|
# ---- 右侧:校验与保存(与编辑区放在一起) ----
|
||||||
bottom = ttk.LabelFrame(root, text="校验与保存", padding=8)
|
save_box = ttk.LabelFrame(right, text="校验与保存", padding=8)
|
||||||
bottom.pack(fill=tk.X, pady=(10, 0))
|
save_box.pack(fill=tk.X, pady=(10, 0))
|
||||||
|
|
||||||
self._gamma_validate_label = ttk.Label(
|
self._gamma_validate_label = ttk.Label(
|
||||||
bottom, text="", style="Muted.TLabel", justify=tk.LEFT
|
save_box, text="", style="Muted.TLabel", justify=tk.LEFT
|
||||||
)
|
)
|
||||||
self._gamma_validate_label.pack(anchor=tk.W)
|
self._gamma_validate_label.pack(anchor=tk.W)
|
||||||
|
|
||||||
save_row = ttk.Frame(bottom)
|
save_row = ttk.Frame(save_box)
|
||||||
save_row.pack(fill=tk.X, pady=(6, 0))
|
save_row.pack(fill=tk.X, pady=(6, 0))
|
||||||
ttk.Button(
|
ttk.Button(
|
||||||
save_row, text="保存改动到当前预设",
|
save_row, text="保存改动到当前预设",
|
||||||
bootstyle="primary",
|
bootstyle="primary",
|
||||||
command=lambda: _save_to_current_preset(self),
|
command=lambda: _save_to_current_preset(self),
|
||||||
).pack(side=tk.LEFT)
|
).pack(side=tk.LEFT)
|
||||||
ttk.Button(
|
|
||||||
save_row, text="应用到运行时 (gray.json)",
|
|
||||||
bootstyle="success",
|
|
||||||
command=lambda: _apply_current_to_runtime(self),
|
|
||||||
).pack(side=tk.LEFT, padx=(6, 0))
|
|
||||||
ttk.Button(
|
ttk.Button(
|
||||||
save_row, text="另存为新预设...",
|
save_row, text="另存为新预设...",
|
||||||
bootstyle="info-outline",
|
bootstyle="info-outline",
|
||||||
|
|||||||
@@ -292,65 +292,9 @@ def create_signal_format_content(self: "PQAutomationApp"):
|
|||||||
self.sdr_signal_frame.grid_columnconfigure(1, weight=1)
|
self.sdr_signal_frame.grid_columnconfigure(1, weight=1)
|
||||||
self.signal_tabs.add(self.sdr_signal_frame, text="SDR测试")
|
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=["sRGB", "BT.709", "BT.601", "BT.2020", "DCI-P3"],
|
|
||||||
width=10,
|
|
||||||
state="readonly",
|
|
||||||
)
|
|
||||||
sdr_color_space_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
|
|
||||||
|
|
||||||
# Gamma(测试参考值,用于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=UCDEnum.SignalFormat.GammaType.GAMMA_22)
|
|
||||||
sdr_gamma_combo = ttk.Combobox(
|
|
||||||
self.sdr_signal_frame,
|
|
||||||
textvariable=self.sdr_gamma_type_var,
|
|
||||||
values=UCDEnum.SignalFormat.GammaType.get_list(),
|
|
||||||
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=UCDEnum.SignalFormat.DataRange.FULL)
|
|
||||||
sdr_range_combo = ttk.Combobox(
|
|
||||||
self.sdr_signal_frame,
|
|
||||||
textvariable=self.sdr_data_range_var,
|
|
||||||
values=UCDEnum.SignalFormat.DataRange.get_list(),
|
|
||||||
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=UCDEnum.SignalFormat.BitDepth.BIT_8)
|
|
||||||
sdr_bit_depth_combo = ttk.Combobox(
|
|
||||||
self.sdr_signal_frame,
|
|
||||||
textvariable=self.sdr_bit_depth_var,
|
|
||||||
values=UCDEnum.SignalFormat.BitDepth.get_list(),
|
|
||||||
width=10,
|
|
||||||
state="readonly",
|
|
||||||
)
|
|
||||||
sdr_bit_depth_combo.grid(row=3, column=1, sticky=tk.W, padx=5, pady=2)
|
|
||||||
|
|
||||||
# 分辨率
|
# 分辨率
|
||||||
ttk.Label(self.sdr_signal_frame, text="分辨率:").grid(
|
ttk.Label(self.sdr_signal_frame, text="分辨率:").grid(
|
||||||
row=4, column=0, sticky=tk.W, padx=5, pady=2
|
row=0, column=0, sticky=tk.W, padx=5, pady=2
|
||||||
)
|
)
|
||||||
self.sdr_timing_var = tk.StringVar(
|
self.sdr_timing_var = tk.StringVar(
|
||||||
value=self.config.current_test_types.get("sdr_movie", {}).get(
|
value=self.config.current_test_types.get("sdr_movie", {}).get(
|
||||||
@@ -365,7 +309,63 @@ def create_signal_format_content(self: "PQAutomationApp"):
|
|||||||
state="readonly",
|
state="readonly",
|
||||||
)
|
)
|
||||||
sdr_timing_combo.bind("<<ComboboxSelected>>", self.on_sdr_timing_changed)
|
sdr_timing_combo.bind("<<ComboboxSelected>>", self.on_sdr_timing_changed)
|
||||||
sdr_timing_combo.grid(row=4, column=1, sticky=tk.W, padx=5, pady=2)
|
sdr_timing_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
|
||||||
|
|
||||||
|
# 色彩空间
|
||||||
|
ttk.Label(self.sdr_signal_frame, text="色彩空间:").grid(
|
||||||
|
row=1, 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=["sRGB", "BT.709", "BT.601", "BT.2020", "DCI-P3"],
|
||||||
|
width=10,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
sdr_color_space_combo.grid(row=1, column=1, sticky=tk.W, padx=5, pady=2)
|
||||||
|
|
||||||
|
# Gamma(测试参考值,用于Gamma曲线绘制和色准计算)
|
||||||
|
ttk.Label(self.sdr_signal_frame, text="Gamma:").grid(
|
||||||
|
row=2, column=0, sticky=tk.W, padx=5, pady=2
|
||||||
|
)
|
||||||
|
self.sdr_gamma_type_var = tk.StringVar(value=UCDEnum.SignalFormat.GammaType.GAMMA_22)
|
||||||
|
sdr_gamma_combo = ttk.Combobox(
|
||||||
|
self.sdr_signal_frame,
|
||||||
|
textvariable=self.sdr_gamma_type_var,
|
||||||
|
values=UCDEnum.SignalFormat.GammaType.get_list(),
|
||||||
|
width=10,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
sdr_gamma_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_data_range_var = tk.StringVar(value=UCDEnum.SignalFormat.DataRange.FULL)
|
||||||
|
sdr_range_combo = ttk.Combobox(
|
||||||
|
self.sdr_signal_frame,
|
||||||
|
textvariable=self.sdr_data_range_var,
|
||||||
|
values=UCDEnum.SignalFormat.DataRange.get_list(),
|
||||||
|
width=10,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
sdr_range_combo.grid(row=3, column=1, sticky=tk.W, padx=5, pady=2)
|
||||||
|
|
||||||
|
# 编码位深
|
||||||
|
ttk.Label(self.sdr_signal_frame, text="编码位深:").grid(
|
||||||
|
row=4, column=0, sticky=tk.W, padx=5, pady=2
|
||||||
|
)
|
||||||
|
self.sdr_bit_depth_var = tk.StringVar(value=UCDEnum.SignalFormat.BitDepth.BIT_8)
|
||||||
|
sdr_bit_depth_combo = ttk.Combobox(
|
||||||
|
self.sdr_signal_frame,
|
||||||
|
textvariable=self.sdr_bit_depth_var,
|
||||||
|
values=UCDEnum.SignalFormat.BitDepth.get_list(),
|
||||||
|
width=10,
|
||||||
|
state="readonly",
|
||||||
|
)
|
||||||
|
sdr_bit_depth_combo.grid(row=4, column=1, sticky=tk.W, padx=5, pady=2)
|
||||||
|
|
||||||
# 色彩格式
|
# 色彩格式
|
||||||
ttk.Label(self.sdr_signal_frame, text="色彩格式:").grid(
|
ttk.Label(self.sdr_signal_frame, text="色彩格式:").grid(
|
||||||
@@ -607,13 +607,14 @@ def create_connection_content(self: "PQAutomationApp"):
|
|||||||
button_frame.grid_columnconfigure(0, weight=1)
|
button_frame.grid_columnconfigure(0, weight=1)
|
||||||
button_frame.grid_columnconfigure(1, weight=1)
|
button_frame.grid_columnconfigure(1, weight=1)
|
||||||
button_frame.grid_columnconfigure(2, weight=1)
|
button_frame.grid_columnconfigure(2, weight=1)
|
||||||
|
button_frame.grid_columnconfigure(3, weight=1)
|
||||||
|
|
||||||
# connect_icon = load_icon("assets/connect-svgrepo-com.png")
|
# connect_icon = load_icon("assets/connect-svgrepo-com.png")
|
||||||
self.check_button = ttk.Button(
|
self.check_button = ttk.Button(
|
||||||
button_frame,
|
button_frame,
|
||||||
# image=connect_icon,
|
# image=connect_icon,
|
||||||
# bootstyle="link",
|
# bootstyle="link",
|
||||||
text="连接",
|
text="全部连接",
|
||||||
bootstyle="success",
|
bootstyle="success",
|
||||||
takefocus=False,
|
takefocus=False,
|
||||||
command=self.check_com_connections,
|
command=self.check_com_connections,
|
||||||
@@ -621,6 +622,42 @@ def create_connection_content(self: "PQAutomationApp"):
|
|||||||
# self.check_button.image = connect_icon
|
# self.check_button.image = connect_icon
|
||||||
self.check_button.grid(row=0, column=0, padx=(0, 4), pady=3, sticky="ew")
|
self.check_button.grid(row=0, column=0, padx=(0, 4), pady=3, sticky="ew")
|
||||||
|
|
||||||
|
self.ucd_connect_button = ttk.Button(
|
||||||
|
button_frame,
|
||||||
|
text="连接UCD",
|
||||||
|
bootstyle="success-outline",
|
||||||
|
takefocus=False,
|
||||||
|
command=self.check_ucd_connection,
|
||||||
|
)
|
||||||
|
self.ucd_connect_button.grid(row=0, column=1, padx=4, pady=3, sticky="ew")
|
||||||
|
|
||||||
|
self.ca_connect_button = ttk.Button(
|
||||||
|
button_frame,
|
||||||
|
text="连接CA410",
|
||||||
|
bootstyle="success-outline",
|
||||||
|
takefocus=False,
|
||||||
|
command=self.check_ca_connection,
|
||||||
|
)
|
||||||
|
self.ca_connect_button.grid(row=0, column=2, padx=4, pady=3, sticky="ew")
|
||||||
|
|
||||||
|
self.ucd_disconnect_button = ttk.Button(
|
||||||
|
button_frame,
|
||||||
|
text="断开UCD",
|
||||||
|
bootstyle="danger-outline",
|
||||||
|
takefocus=False,
|
||||||
|
command=self.disconnect_ucd_connection,
|
||||||
|
)
|
||||||
|
self.ucd_disconnect_button.grid(row=1, column=1, padx=4, pady=3, sticky="ew")
|
||||||
|
|
||||||
|
self.ca_disconnect_button = ttk.Button(
|
||||||
|
button_frame,
|
||||||
|
text="断开CA410",
|
||||||
|
bootstyle="danger-outline",
|
||||||
|
takefocus=False,
|
||||||
|
command=self.disconnect_ca_connection,
|
||||||
|
)
|
||||||
|
self.ca_disconnect_button.grid(row=1, column=2, padx=4, pady=3, sticky="ew")
|
||||||
|
|
||||||
# disconnect_icon = load_icon("assets/disconnect-svgrepo-com.png")
|
# disconnect_icon = load_icon("assets/disconnect-svgrepo-com.png")
|
||||||
# 断开连接按钮
|
# 断开连接按钮
|
||||||
self.disconnect_button = ttk.Button(
|
self.disconnect_button = ttk.Button(
|
||||||
@@ -633,7 +670,7 @@ def create_connection_content(self: "PQAutomationApp"):
|
|||||||
command=self.disconnect_com_connections,
|
command=self.disconnect_com_connections,
|
||||||
)
|
)
|
||||||
# self.disconnect_button.image = disconnect_icon # 防止图标被垃圾回收
|
# self.disconnect_button.image = disconnect_icon # 防止图标被垃圾回收
|
||||||
self.disconnect_button.grid(row=0, column=1, padx=4, pady=3, sticky="ew")
|
self.disconnect_button.grid(row=1, column=0, padx=(0, 4), pady=3, sticky="ew")
|
||||||
|
|
||||||
# refresh_icon = load_icon("assets/refresh-svgrepo-com.png")
|
# refresh_icon = load_icon("assets/refresh-svgrepo-com.png")
|
||||||
self.refresh_button = ttk.Button(
|
self.refresh_button = ttk.Button(
|
||||||
@@ -646,7 +683,7 @@ def create_connection_content(self: "PQAutomationApp"):
|
|||||||
command=self.refresh_com_ports,
|
command=self.refresh_com_ports,
|
||||||
)
|
)
|
||||||
# self.refresh_button.image = refresh_icon # 防止图标被垃圾回收
|
# self.refresh_button.image = refresh_icon # 防止图标被垃圾回收
|
||||||
self.refresh_button.grid(row=0, column=2, padx=(4, 0), pady=3, sticky="ew")
|
self.refresh_button.grid(row=1, column=3, padx=(4, 0), pady=3, sticky="ew")
|
||||||
|
|
||||||
# CA端口
|
# CA端口
|
||||||
ttk.Label(com_frame, text="CA端口:").grid(
|
ttk.Label(com_frame, text="CA端口:").grid(
|
||||||
@@ -737,10 +774,9 @@ def create_test_type_frame(self: "PQAutomationApp"):
|
|||||||
).pack(fill=tk.X, padx=16, pady=(16, 6), anchor="w")
|
).pack(fill=tk.X, padx=16, pady=(16, 6), anchor="w")
|
||||||
|
|
||||||
panel_buttons = [
|
panel_buttons = [
|
||||||
("log_btn", "测试日志", self.toggle_log_panel),
|
|
||||||
("ai_image_btn", "AI 图片", self.toggle_ai_image_panel),
|
("ai_image_btn", "AI 图片", self.toggle_ai_image_panel),
|
||||||
("pantone_baseline_btn", "Pantone 摸底", self.toggle_pantone_baseline_panel),
|
("pantone_baseline_btn", "Pantone 摸底", self.toggle_pantone_baseline_panel),
|
||||||
("gamma_pattern_btn", "Gamma 图案", self.toggle_gamma_pattern_panel),
|
("gamma_pattern_btn", "Gamma Pattern编辑", self.toggle_gamma_pattern_panel),
|
||||||
("calman_btn", "CALMAN 灰阶", self.toggle_calman_panel),
|
("calman_btn", "CALMAN 灰阶", self.toggle_calman_panel),
|
||||||
]
|
]
|
||||||
for attr, text, cmd in panel_buttons:
|
for attr, text, cmd in panel_buttons:
|
||||||
@@ -764,6 +800,17 @@ def create_test_type_frame(self: "PQAutomationApp"):
|
|||||||
)
|
)
|
||||||
beta_lbl.pack(fill=tk.X, side=tk.BOTTOM, padx=4, pady=(6, 4))
|
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.theme_toggle_btn = ttk.Button(
|
||||||
self.sidebar_frame,
|
self.sidebar_frame,
|
||||||
@@ -777,8 +824,6 @@ def create_test_type_frame(self: "PQAutomationApp"):
|
|||||||
|
|
||||||
# 注册面板按钮
|
# 注册面板按钮
|
||||||
if hasattr(self, "panels"):
|
if hasattr(self, "panels"):
|
||||||
if "log" in self.panels:
|
|
||||||
self.panels["log"]["button"] = self.log_btn
|
|
||||||
if "ai_image" in self.panels:
|
if "ai_image" in self.panels:
|
||||||
self.panels["ai_image"]["button"] = self.ai_image_btn
|
self.panels["ai_image"]["button"] = self.ai_image_btn
|
||||||
if "single_step" in self.panels:
|
if "single_step" in self.panels:
|
||||||
@@ -804,6 +849,15 @@ def _refresh_theme_toggle_label(self: "PQAutomationApp") -> None:
|
|||||||
|
|
||||||
def _on_toggle_theme(self: "PQAutomationApp") -> None:
|
def _on_toggle_theme(self: "PQAutomationApp") -> None:
|
||||||
"""切换主题:重新应用 ttk 样式并刷新所有自定义样式相关的标签。"""
|
"""切换主题:重新应用 ttk 样式并刷新所有自定义样式相关的标签。"""
|
||||||
|
# 在测试进行时禁止切换主题,避免影响测量稳定性
|
||||||
|
if getattr(self, "testing", False):
|
||||||
|
try:
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log("警告: 测试进行中,禁止切换主题", level="error")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
from app.views.theme_manager import toggle_theme
|
from app.views.theme_manager import toggle_theme
|
||||||
toggle_theme()
|
toggle_theme()
|
||||||
# apply_modern_styles()
|
# apply_modern_styles()
|
||||||
@@ -974,10 +1028,10 @@ def on_screen_module_timing_changed(self: "PQAutomationApp", event=None):
|
|||||||
|
|
||||||
# 根据分辨率给出提示
|
# 根据分辨率给出提示
|
||||||
if width >= 3840: # 4K及以上
|
if width >= 3840: # 4K及以上
|
||||||
self.log_gui.log(" ℹ️ 检测到4K分辨率", level="info")
|
self.log_gui.log("检测到4K分辨率", level="info")
|
||||||
|
|
||||||
if refresh_rate >= 120:
|
if refresh_rate >= 120:
|
||||||
self.log_gui.log(" ℹ️ 检测到高刷新率", level="info")
|
self.log_gui.log("检测到高刷新率", level="info")
|
||||||
|
|
||||||
# 更新屏模组配置(独立于 current_test_type)
|
# 更新屏模组配置(独立于 current_test_type)
|
||||||
self.config.current_test_types.setdefault("screen_module", {})[
|
self.config.current_test_types.setdefault("screen_module", {})[
|
||||||
|
|||||||
@@ -42,13 +42,13 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
|
|||||||
|
|
||||||
ttk.Label(
|
ttk.Label(
|
||||||
title_frame,
|
title_frame,
|
||||||
text="🔆 Local Dimming 窗口测试",
|
text="Local Dimming 窗口测试",
|
||||||
font=("微软雅黑", 14, "bold"),
|
font=("微软雅黑", 14, "bold"),
|
||||||
).pack(side=tk.LEFT)
|
).pack(side=tk.LEFT)
|
||||||
|
|
||||||
# ==================== 2. 窗口百分比按钮 ====================
|
# ==================== 2. 窗口百分比按钮 ====================
|
||||||
window_frame = ttk.LabelFrame(
|
window_frame = ttk.LabelFrame(
|
||||||
main_container, text="🔆 窗口百分比(点击发送)", padding=10
|
main_container, text="窗口百分比", padding=10
|
||||||
)
|
)
|
||||||
window_frame.pack(fill=tk.X, pady=(0, 10))
|
window_frame.pack(fill=tk.X, pady=(0, 10))
|
||||||
|
|
||||||
@@ -60,6 +60,50 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
|
|||||||
style="SuccessState.TLabel",
|
style="SuccessState.TLabel",
|
||||||
).pack(pady=(0, 8))
|
).pack(pady=(0, 8))
|
||||||
|
|
||||||
|
window_level_row = ttk.Frame(window_frame)
|
||||||
|
window_level_row.pack(fill=tk.X, pady=(0, 8))
|
||||||
|
|
||||||
|
ttk.Label(window_level_row, text="窗口(%):").pack(side=tk.LEFT)
|
||||||
|
self.ld_window_percentage_var = tk.StringVar(value="10")
|
||||||
|
ld_window_percentage_entry = ttk.Entry(
|
||||||
|
window_level_row,
|
||||||
|
textvariable=self.ld_window_percentage_var,
|
||||||
|
width=8,
|
||||||
|
)
|
||||||
|
ld_window_percentage_entry.pack(side=tk.LEFT, padx=(6, 10))
|
||||||
|
|
||||||
|
ttk.Label(window_level_row, text="窗口亮度(%):").pack(side=tk.LEFT)
|
||||||
|
self.ld_window_luminance_var = tk.StringVar(value="100")
|
||||||
|
ld_window_luminance_entry = ttk.Entry(
|
||||||
|
window_level_row,
|
||||||
|
textvariable=self.ld_window_luminance_var,
|
||||||
|
width=8,
|
||||||
|
)
|
||||||
|
ld_window_luminance_entry.pack(side=tk.LEFT, padx=(6, 10))
|
||||||
|
|
||||||
|
ttk.Button(
|
||||||
|
window_level_row,
|
||||||
|
text="生成窗口",
|
||||||
|
command=self.send_ld_manual_window,
|
||||||
|
bootstyle="success-outline",
|
||||||
|
width=12,
|
||||||
|
).pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
ld_window_percentage_entry.bind(
|
||||||
|
"<Return>",
|
||||||
|
lambda _event: self.send_ld_manual_window(),
|
||||||
|
)
|
||||||
|
ld_window_luminance_entry.bind(
|
||||||
|
"<Return>",
|
||||||
|
lambda _event: self.send_ld_manual_window(),
|
||||||
|
)
|
||||||
|
|
||||||
|
ttk.Label(
|
||||||
|
window_level_row,
|
||||||
|
text="输入后可直接点生成或回车",
|
||||||
|
style="InfoState.TLabel",
|
||||||
|
).pack(side=tk.LEFT, padx=(8, 0))
|
||||||
|
|
||||||
# 第一行:1%, 2%, 5%, 10%, 18%
|
# 第一行:1%, 2%, 5%, 10%, 18%
|
||||||
row1 = ttk.Frame(window_frame)
|
row1 = ttk.Frame(window_frame)
|
||||||
row1.pack(fill=tk.X, pady=(0, 5))
|
row1.pack(fill=tk.X, pady=(0, 5))
|
||||||
@@ -89,16 +133,9 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
|
|||||||
).pack(side=tk.LEFT, padx=3)
|
).pack(side=tk.LEFT, padx=3)
|
||||||
|
|
||||||
# ==================== 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))
|
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 = ttk.Frame(pattern_frame)
|
||||||
pattern_row.pack(fill=tk.X)
|
pattern_row.pack(fill=tk.X)
|
||||||
|
|
||||||
@@ -118,14 +155,6 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
|
|||||||
width=14,
|
width=14,
|
||||||
).pack(side=tk.LEFT, padx=3)
|
).pack(side=tk.LEFT, padx=3)
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
pattern_row,
|
|
||||||
text="瞬时峰值",
|
|
||||||
command=self.send_ld_instant_peak,
|
|
||||||
bootstyle="warning",
|
|
||||||
width=12,
|
|
||||||
).pack(side=tk.LEFT, padx=3)
|
|
||||||
|
|
||||||
ttk.Button(
|
ttk.Button(
|
||||||
pattern_row,
|
pattern_row,
|
||||||
text="全黑画面",
|
text="全黑画面",
|
||||||
@@ -134,8 +163,92 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
|
|||||||
width=12,
|
width=12,
|
||||||
).pack(side=tk.LEFT, padx=3)
|
).pack(side=tk.LEFT, padx=3)
|
||||||
|
|
||||||
# ==================== 4. CA410 采集按钮 ====================
|
# ==================== 4. 独立瞬时峰值连续测试 ====================
|
||||||
measure_frame = ttk.LabelFrame(main_container, text="📊 CA410 测量", padding=10)
|
peak_frame = ttk.LabelFrame(main_container, text="瞬时峰值独立测试", padding=10)
|
||||||
|
peak_frame.pack(fill=tk.X, pady=(0, 10))
|
||||||
|
|
||||||
|
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(
|
||||||
|
row=1, column=1, sticky=tk.W, padx=(0, 10)
|
||||||
|
)
|
||||||
|
|
||||||
|
ttk.Label(peak_frame, text="窗口亮度(%):").grid(
|
||||||
|
row=1, column=2, sticky=tk.W, padx=(0, 4)
|
||||||
|
)
|
||||||
|
ttk.Entry(peak_frame, textvariable=self.ld_peak_window_luminance_var, width=8).grid(
|
||||||
|
row=1, column=3, sticky=tk.W, padx=(0, 10)
|
||||||
|
)
|
||||||
|
|
||||||
|
ttk.Label(peak_frame, text="连续时长(s):").grid(
|
||||||
|
row=1, column=4, sticky=tk.W, padx=(0, 4)
|
||||||
|
)
|
||||||
|
ttk.Entry(peak_frame, textvariable=self.ld_peak_duration_var, width=8).grid(
|
||||||
|
row=1, column=5, sticky=tk.W, padx=(0, 10)
|
||||||
|
)
|
||||||
|
|
||||||
|
ttk.Label(peak_frame, text="采样间隔(s):").grid(
|
||||||
|
row=1, column=6, sticky=tk.W, padx=(0, 4)
|
||||||
|
)
|
||||||
|
ttk.Entry(peak_frame, textvariable=self.ld_peak_sample_interval_var, width=8).grid(
|
||||||
|
row=1, column=7, sticky=tk.W
|
||||||
|
)
|
||||||
|
|
||||||
|
ttk.Checkbutton(
|
||||||
|
peak_frame,
|
||||||
|
text="记录曲线点到表格",
|
||||||
|
variable=self.ld_peak_record_curve_var,
|
||||||
|
bootstyle="round-toggle",
|
||||||
|
).grid(row=2, column=0, columnspan=3, sticky=tk.W, pady=(8, 0))
|
||||||
|
|
||||||
|
peak_btn_row = ttk.Frame(peak_frame)
|
||||||
|
peak_btn_row.grid(row=2, column=4, columnspan=4, sticky=tk.EW, pady=(8, 0))
|
||||||
|
|
||||||
|
self.ld_peak_start_btn = ttk.Button(
|
||||||
|
peak_btn_row,
|
||||||
|
text="开始峰值追踪",
|
||||||
|
command=self.start_ld_instant_peak_tracking,
|
||||||
|
bootstyle="warning",
|
||||||
|
width=14,
|
||||||
|
)
|
||||||
|
self.ld_peak_start_btn.pack(side=tk.LEFT, padx=(0, 5))
|
||||||
|
|
||||||
|
self.ld_peak_stop_btn = ttk.Button(
|
||||||
|
peak_btn_row,
|
||||||
|
text="停止",
|
||||||
|
command=self.stop_ld_instant_peak_tracking,
|
||||||
|
bootstyle="danger-outline",
|
||||||
|
width=10,
|
||||||
|
state="disabled",
|
||||||
|
)
|
||||||
|
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.pack(fill=tk.X, pady=(0, 10))
|
measure_frame.pack(fill=tk.X, pady=(0, 10))
|
||||||
|
|
||||||
measure_btn_frame = ttk.Frame(measure_frame)
|
measure_btn_frame = ttk.Frame(measure_frame)
|
||||||
@@ -143,7 +256,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
|
|||||||
|
|
||||||
self.ld_measure_btn = ttk.Button(
|
self.ld_measure_btn = ttk.Button(
|
||||||
measure_btn_frame,
|
measure_btn_frame,
|
||||||
text="📏 采集当前亮度",
|
text="采集当前亮度",
|
||||||
command=self.measure_ld_luminance,
|
command=self.measure_ld_luminance,
|
||||||
bootstyle="primary",
|
bootstyle="primary",
|
||||||
width=15,
|
width=15,
|
||||||
@@ -159,8 +272,8 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
|
|||||||
)
|
)
|
||||||
self.ld_result_label.pack(side=tk.LEFT, padx=(10, 0))
|
self.ld_result_label.pack(side=tk.LEFT, padx=(10, 0))
|
||||||
|
|
||||||
# ==================== 5. 测试结果表格 ====================
|
# ==================== 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))
|
result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
||||||
|
|
||||||
# Treeview
|
# Treeview
|
||||||
@@ -191,7 +304,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
|
|||||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
self.ld_tree.configure(yscrollcommand=scrollbar.set)
|
self.ld_tree.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
# ==================== 6. 底部操作按钮 ====================
|
# ==================== 7. 底部操作按钮 ====================
|
||||||
bottom_frame = ttk.Frame(main_container)
|
bottom_frame = ttk.Frame(main_container)
|
||||||
bottom_frame.pack(fill=tk.X)
|
bottom_frame.pack(fill=tk.X)
|
||||||
|
|
||||||
@@ -206,13 +319,22 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
|
|||||||
|
|
||||||
self.ld_save_btn = ttk.Button(
|
self.ld_save_btn = ttk.Button(
|
||||||
bottom_frame,
|
bottom_frame,
|
||||||
text="💾 保存结果",
|
text="保存结果",
|
||||||
command=self.save_local_dimming_results,
|
command=self.save_local_dimming_results,
|
||||||
bootstyle="info",
|
bootstyle="info",
|
||||||
width=12,
|
width=12,
|
||||||
)
|
)
|
||||||
self.ld_save_btn.pack(side=tk.LEFT)
|
self.ld_save_btn.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
self.ld_plot_btn = ttk.Button(
|
||||||
|
bottom_frame,
|
||||||
|
text="生成峰值曲线",
|
||||||
|
command=self.plot_ld_instant_peak_curve,
|
||||||
|
bootstyle="warning-outline",
|
||||||
|
width=14,
|
||||||
|
)
|
||||||
|
self.ld_plot_btn.pack(side=tk.LEFT, padx=(5, 0))
|
||||||
|
|
||||||
# 默认隐藏
|
# 默认隐藏
|
||||||
self.local_dimming_visible = False
|
self.local_dimming_visible = False
|
||||||
|
|
||||||
@@ -228,6 +350,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
|
|||||||
self.current_ld_percentage = None
|
self.current_ld_percentage = None
|
||||||
self.current_ld_test_item = None
|
self.current_ld_test_item = None
|
||||||
self.current_ld_pattern_label = None
|
self.current_ld_pattern_label = None
|
||||||
|
self.ld_peak_tracking = False
|
||||||
|
|
||||||
|
|
||||||
def toggle_local_dimming_panel(self: "PQAutomationApp"):
|
def toggle_local_dimming_panel(self: "PQAutomationApp"):
|
||||||
|
|||||||
@@ -612,13 +612,10 @@ class PQDebugPanel:
|
|||||||
|
|
||||||
if test_type == "screen_module":
|
if test_type == "screen_module":
|
||||||
self.screen_frame.pack(fill=tk.BOTH, expand=True)
|
self.screen_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
self.app.log_gui.log("显示屏模组调试面板", level="success")
|
|
||||||
elif test_type == "sdr_movie":
|
elif test_type == "sdr_movie":
|
||||||
self.sdr_frame.pack(fill=tk.BOTH, expand=True)
|
self.sdr_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
self.app.log_gui.log("显示 SDR 调试面板", level="success")
|
|
||||||
elif test_type == "hdr_movie":
|
elif test_type == "hdr_movie":
|
||||||
self.hdr_frame.pack(fill=tk.BOTH, expand=True)
|
self.hdr_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
self.app.log_gui.log("显示 HDR 调试面板", level="success")
|
|
||||||
|
|
||||||
# ==================== 启用/禁用控制 ====================
|
# ==================== 启用/禁用控制 ====================
|
||||||
|
|
||||||
@@ -643,39 +640,31 @@ class PQDebugPanel:
|
|||||||
if test_item == "gamma":
|
if test_item == "gamma":
|
||||||
self.screen_gray_combo.config(state="readonly")
|
self.screen_gray_combo.config(state="readonly")
|
||||||
self.screen_test_btn.config(state=tk.NORMAL)
|
self.screen_test_btn.config(state=tk.NORMAL)
|
||||||
self.app.log_gui.log("屏模组 Gamma 单步调试已启用", level="success")
|
|
||||||
elif test_item == "rgb":
|
elif test_item == "rgb":
|
||||||
self.screen_rgb_combo.config(state="readonly")
|
self.screen_rgb_combo.config(state="readonly")
|
||||||
self.screen_rgb_test_btn.config(state=tk.NORMAL)
|
self.screen_rgb_test_btn.config(state=tk.NORMAL)
|
||||||
self.app.log_gui.log("屏模组 RGB 单步调试已启用", level="success")
|
|
||||||
|
|
||||||
elif test_type == "sdr_movie":
|
elif test_type == "sdr_movie":
|
||||||
if test_item == "gamma":
|
if test_item == "gamma":
|
||||||
self.sdr_gray_combo.config(state="readonly")
|
self.sdr_gray_combo.config(state="readonly")
|
||||||
self.sdr_gamma_test_btn.config(state=tk.NORMAL)
|
self.sdr_gamma_test_btn.config(state=tk.NORMAL)
|
||||||
self.app.log_gui.log("SDR Gamma 单步调试已启用", level="success")
|
|
||||||
elif test_item == "accuracy":
|
elif test_item == "accuracy":
|
||||||
self.sdr_color_combo.config(state="readonly")
|
self.sdr_color_combo.config(state="readonly")
|
||||||
self.sdr_accuracy_test_btn.config(state=tk.NORMAL)
|
self.sdr_accuracy_test_btn.config(state=tk.NORMAL)
|
||||||
self.app.log_gui.log("SDR 色准单步调试已启用", level="success")
|
|
||||||
elif test_item == "rgb":
|
elif test_item == "rgb":
|
||||||
self.sdr_rgb_combo.config(state="readonly")
|
self.sdr_rgb_combo.config(state="readonly")
|
||||||
self.sdr_rgb_test_btn.config(state=tk.NORMAL)
|
self.sdr_rgb_test_btn.config(state=tk.NORMAL)
|
||||||
self.app.log_gui.log("SDR RGB 单步调试已启用", level="success")
|
|
||||||
|
|
||||||
elif test_type == "hdr_movie":
|
elif test_type == "hdr_movie":
|
||||||
if test_item == "eotf":
|
if test_item == "eotf":
|
||||||
self.hdr_gray_combo.config(state="readonly")
|
self.hdr_gray_combo.config(state="readonly")
|
||||||
self.hdr_eotf_test_btn.config(state=tk.NORMAL)
|
self.hdr_eotf_test_btn.config(state=tk.NORMAL)
|
||||||
self.app.log_gui.log("HDR EOTF 单步调试已启用", level="success")
|
|
||||||
elif test_item == "accuracy":
|
elif test_item == "accuracy":
|
||||||
self.hdr_color_combo.config(state="readonly")
|
self.hdr_color_combo.config(state="readonly")
|
||||||
self.hdr_accuracy_test_btn.config(state=tk.NORMAL)
|
self.hdr_accuracy_test_btn.config(state=tk.NORMAL)
|
||||||
self.app.log_gui.log("HDR 色准单步调试已启用", level="success")
|
|
||||||
elif test_item == "rgb":
|
elif test_item == "rgb":
|
||||||
self.hdr_rgb_combo.config(state="readonly")
|
self.hdr_rgb_combo.config(state="readonly")
|
||||||
self.hdr_rgb_test_btn.config(state=tk.NORMAL)
|
self.hdr_rgb_test_btn.config(state=tk.NORMAL)
|
||||||
self.app.log_gui.log("HDR RGB 单步调试已启用", level="success")
|
|
||||||
|
|
||||||
def disable_all_debug(self):
|
def disable_all_debug(self):
|
||||||
"""禁用所有单步调试(新测试开始时调用)"""
|
"""禁用所有单步调试(新测试开始时调用)"""
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ _LEGACY_LIGHT_THEMES = {"yeti"}
|
|||||||
|
|
||||||
_CALMAN_LIGHT_COLORS = {
|
_CALMAN_LIGHT_COLORS = {
|
||||||
"primary": "#1755a6",
|
"primary": "#1755a6",
|
||||||
"secondary": "#2B6CB0",
|
"secondary": "#3572B4",
|
||||||
"success": "#2F9E44",
|
"success": "#2F9E44",
|
||||||
"info": "#247BA0",
|
"info": "#247BA0",
|
||||||
"warning": "#C98700",
|
"warning": "#C98700",
|
||||||
|
|||||||
BIN
docs/PQ自动化工具使用指南SOP.pdf
Normal file
BIN
docs/PQ自动化工具使用指南SOP.pdf
Normal file
Binary file not shown.
@@ -393,7 +393,7 @@ class PQAutomationApp(
|
|||||||
if not hasattr(self, "_set_custom_template_tab_visible"):
|
if not hasattr(self, "_set_custom_template_tab_visible"):
|
||||||
return
|
return
|
||||||
|
|
||||||
# 客户模板结果 Tab 只属于 SDR Movie。
|
# 客户模板结果 Tab 只属于 SDR Movie
|
||||||
if test_type != "sdr_movie":
|
if test_type != "sdr_movie":
|
||||||
self._set_custom_template_tab_visible(False)
|
self._set_custom_template_tab_visible(False)
|
||||||
return
|
return
|
||||||
@@ -416,14 +416,9 @@ class PQAutomationApp(
|
|||||||
|
|
||||||
def change_test_type(self, test_type):
|
def change_test_type(self, test_type):
|
||||||
"""切换测试类型"""
|
"""切换测试类型"""
|
||||||
# 切换测试类型时,自动隐藏日志面板和 Local Dimming 面板
|
# 切换测试类型时,自动隐藏日志面板
|
||||||
if self.current_panel in (
|
if self.current_panel in (
|
||||||
"log",
|
"log",
|
||||||
"local_dimming",
|
|
||||||
"ai_image",
|
|
||||||
"single_step",
|
|
||||||
"pantone_baseline",
|
|
||||||
"gamma_pattern",
|
|
||||||
):
|
):
|
||||||
self.hide_all_panels()
|
self.hide_all_panels()
|
||||||
self._save_cct_params_before_test_type_switch()
|
self._save_cct_params_before_test_type_switch()
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
"y_ideal": 0.329,
|
"y_ideal": 0.329,
|
||||||
"y_tolerance": 0.003
|
"y_tolerance": 0.003
|
||||||
},
|
},
|
||||||
"gamut_reference": "BT.601"
|
"gamut_reference": "BT.709"
|
||||||
},
|
},
|
||||||
"hdr_movie": {
|
"hdr_movie": {
|
||||||
"name": "HDR Movie测试",
|
"name": "HDR Movie测试",
|
||||||
|
|||||||
@@ -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 |
Reference in New Issue
Block a user