Compare commits
13 Commits
85ac47e8de
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46a97d6ae7 | ||
|
|
cc7218411c | ||
|
|
38222ff002 | ||
|
|
3206079c63 | ||
|
|
25be4b7f4a | ||
|
|
f33984affa | ||
|
|
8916f2fff0 | ||
|
|
9ad9cf9aa0 | ||
|
|
e4890d9d8d | ||
|
|
febbb28a4c | ||
|
|
e9a591bf6e | ||
|
|
49d82da8b9 | ||
|
|
3aa975c4d3 |
@@ -6,9 +6,8 @@
|
|||||||
"把模块级函数当类方法装到 PQAutomationApp 上" 的写法。
|
"把模块级函数当类方法装到 PQAutomationApp 上" 的写法。
|
||||||
- 拆掉 ``check_port_connection(is_ucd)`` 的布尔旗参数反模式,
|
- 拆掉 ``check_port_connection(is_ucd)`` 的布尔旗参数反模式,
|
||||||
分离为 ``connect_ucd`` / ``connect_ca`` 两条独立路径。
|
分离为 ``connect_ucd`` / ``connect_ca`` 两条独立路径。
|
||||||
- UCD 这一侧不再直接调用旧 ``UCDController``,而是通过
|
- UCD 经由 :class:`UCD323Device` + :class:`EventBus` 管理;
|
||||||
:class:`UCD323Device` + :class:`EventBus`;指示器更新由订阅
|
指示灯由 GUI 订阅带 :class:`DeviceKind` 的 :class:`ConnectionChanged` 事件更新。
|
||||||
:class:`ConnectionChanged` 事件触发,与 GUI 解耦。
|
|
||||||
|
|
||||||
模块底部仍保留 7 个旧名字函数作为薄兼容层(``check_com_connections``
|
模块底部仍保留 7 个旧名字函数作为薄兼容层(``check_com_connections``
|
||||||
等),供 ``pqAutomationApp.PQAutomationApp`` 类体继续以同名属性挂接,
|
等),供 ``pqAutomationApp.PQAutomationApp`` 类体继续以同名属性挂接,
|
||||||
@@ -22,9 +21,9 @@ import time
|
|||||||
from tkinter import messagebox
|
from tkinter import messagebox
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from app.ucd_domain import ConnectionChanged, UcdError
|
from app.ucd import ConnectionChanged, DeviceKind, DeviceInfo, UCD323Device, UcdError
|
||||||
from drivers.caSerail import CASerail
|
from drivers.caSerail import CASerail
|
||||||
from drivers.ucd_driver import DeviceInfo
|
from app.views.modern_styles import get_theme_palette
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -33,8 +32,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from app.ucd_domain import EventBus
|
from app.ucd import EventBus
|
||||||
from drivers.ucd_driver import UCD323Device
|
|
||||||
|
|
||||||
|
|
||||||
# ─── ConnectionController ────────────────────────────────────────
|
# ─── ConnectionController ────────────────────────────────────────
|
||||||
@@ -64,7 +62,7 @@ class ConnectionController:
|
|||||||
def list_ucd_devices(self) -> list[str]:
|
def list_ucd_devices(self) -> list[str]:
|
||||||
"""返回 SDK 给出的设备显示字符串列表。"""
|
"""返回 SDK 给出的设备显示字符串列表。"""
|
||||||
try:
|
try:
|
||||||
return self._device.raw_controller.search_device() or []
|
return self._device.search_devices()
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
self._log(f"枚举 UCD 设备失败: {exc}", level="error")
|
self._log(f"枚举 UCD 设备失败: {exc}", level="error")
|
||||||
return []
|
return []
|
||||||
@@ -102,11 +100,6 @@ class ConnectionController:
|
|||||||
self._device.close()
|
self._device.close()
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
pass
|
pass
|
||||||
# 旧 controller.status 也要清零,兼容仍读取它的代码
|
|
||||||
try:
|
|
||||||
self._app.ucd.status = False
|
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
pass
|
|
||||||
self._log("UCD连接已断开", level="info")
|
self._log("UCD连接已断开", level="info")
|
||||||
|
|
||||||
# -- CA 连接 -------------------------------------------------
|
# -- CA 连接 -------------------------------------------------
|
||||||
@@ -137,7 +130,7 @@ class ConnectionController:
|
|||||||
channel_value = self._app.ca_channel_var.get()
|
channel_value = self._app.ca_channel_var.get()
|
||||||
ca.setChannel(f"{int(channel_value):02d}")
|
ca.setChannel(f"{int(channel_value):02d}")
|
||||||
self._app.ca = ca
|
self._app.ca = ca
|
||||||
self._bus.publish(ConnectionChanged(True, None))
|
self._bus.publish(ConnectionChanged(DeviceKind.CA, True, None))
|
||||||
return True
|
return True
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
self._log(f"CA410 连接失败: {exc}", level="error")
|
self._log(f"CA410 连接失败: {exc}", level="error")
|
||||||
@@ -151,7 +144,7 @@ class ConnectionController:
|
|||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
pass
|
pass
|
||||||
self._app.ca = None
|
self._app.ca = None
|
||||||
self._bus.publish(ConnectionChanged(False, None))
|
self._bus.publish(ConnectionChanged(DeviceKind.CA, False, None))
|
||||||
self._log("CA连接已断开", level="info")
|
self._log("CA连接已断开", level="info")
|
||||||
|
|
||||||
# -- 一次性入口 ----------------------------------------------
|
# -- 一次性入口 ----------------------------------------------
|
||||||
@@ -159,8 +152,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()
|
||||||
|
|
||||||
@@ -184,18 +176,87 @@ 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()
|
||||||
self.disconnect_ca()
|
self.disconnect_ca()
|
||||||
self._enable_widgets()
|
self._enable_widgets()
|
||||||
self._app.ucd_status_indicator.config(bg="gray")
|
self._app.refresh_connection_indicators()
|
||||||
self._app.ca_status_indicator.config(bg="gray")
|
|
||||||
self._app.status_var.set("串口连接已断开")
|
self._app.status_var.set("串口连接已断开")
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
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
|
||||||
@@ -212,18 +273,28 @@ class ConnectionController:
|
|||||||
)
|
)
|
||||||
app.ca_com_combo.config(values=com_ports)
|
app.ca_com_combo.config(values=com_ports)
|
||||||
|
|
||||||
if hasattr(app, "ucd_status_indicator"):
|
app.refresh_connection_indicators()
|
||||||
app.ucd_status_indicator.config(bg="gray")
|
|
||||||
if hasattr(app, "ca_status_indicator"):
|
|
||||||
app.ca_status_indicator.config(bg="gray")
|
|
||||||
|
|
||||||
app.update_config()
|
app.update_config()
|
||||||
|
|
||||||
# -- 内部 ----------------------------------------------------
|
# -- 内部 ----------------------------------------------------
|
||||||
|
|
||||||
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)
|
||||||
@@ -253,8 +324,54 @@ 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):
|
||||||
indicator.config(bg="green" if connected else "red")
|
_draw_connection_indicator(indicator, "green" if connected else "red")
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_connection_indicators(self: "PQAutomationApp"):
|
||||||
|
"""根据当前设备状态重画 UCD / CA 指示灯。"""
|
||||||
|
if hasattr(self, "ucd_status_indicator"):
|
||||||
|
ucd_connected = self.signal_service.is_connected
|
||||||
|
_draw_connection_indicator(
|
||||||
|
self.ucd_status_indicator,
|
||||||
|
"green" if ucd_connected else "gray",
|
||||||
|
)
|
||||||
|
if hasattr(self, "ca_status_indicator"):
|
||||||
|
ca_connected = getattr(self, "ca", None) is not None
|
||||||
|
_draw_connection_indicator(
|
||||||
|
self.ca_status_indicator,
|
||||||
|
"green" if ca_connected else "gray",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_connection_indicator(canvas, state: str) -> None:
|
||||||
|
palette = get_theme_palette()
|
||||||
|
color_map = {
|
||||||
|
"green": "#2ECC71",
|
||||||
|
"red": "#E74C3C",
|
||||||
|
"gray": "#9AA3AD",
|
||||||
|
}
|
||||||
|
fill = color_map.get(state, state)
|
||||||
|
border = palette["border"]
|
||||||
|
bg = palette["card_bg"]
|
||||||
|
try:
|
||||||
|
canvas.configure(bg=bg, highlightbackground=border, highlightcolor=border)
|
||||||
|
canvas.delete("all")
|
||||||
|
# 保持原有视觉:方形状态灯(红/绿/灰)
|
||||||
|
canvas.create_rectangle(0, 0, 15, 15, fill=fill, outline=border, width=1)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
canvas.config(bg=fill)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def check_port_connection(self: "PQAutomationApp", is_ucd=True):
|
def check_port_connection(self: "PQAutomationApp", is_ucd=True):
|
||||||
@@ -272,6 +389,57 @@ 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"):
|
||||||
|
lock = getattr(self, "_ca_measure_lock", None)
|
||||||
|
if lock is None:
|
||||||
|
lock = threading.RLock()
|
||||||
|
self._ca_measure_lock = lock
|
||||||
|
return lock
|
||||||
|
|
||||||
|
|
||||||
|
def _read_ca_display(self: "PQAutomationApp", mode: int):
|
||||||
|
"""在锁内切换 CA410 Display 模式并立即读取,避免模式串扰。"""
|
||||||
|
if getattr(self, "ca", None) is None:
|
||||||
|
raise RuntimeError("请先连接 CA410 色度计")
|
||||||
|
|
||||||
|
with _get_ca_measure_lock(self):
|
||||||
|
self.ca.set_Display(mode)
|
||||||
|
return self.ca.readAllDisplay()
|
||||||
|
|
||||||
|
|
||||||
|
def read_ca_xyLv(self: "PQAutomationApp"):
|
||||||
|
"""读取 xy/Lv/XYZ(Display 0)。"""
|
||||||
|
return _read_ca_display(self, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def read_ca_tcp_duv(self: "PQAutomationApp"):
|
||||||
|
"""读取 Tcp/duv/Lv/XYZ(Display 1)。"""
|
||||||
|
return _read_ca_display(self, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def read_ca_uvLv(self: "PQAutomationApp"):
|
||||||
|
"""读取 u'/v'/Lv/XYZ(Display 5)。"""
|
||||||
|
return _read_ca_display(self, 5)
|
||||||
|
|
||||||
|
|
||||||
|
def read_ca_xyz(self: "PQAutomationApp"):
|
||||||
|
"""读取 XYZ(Display 7)。"""
|
||||||
|
return _read_ca_display(self, 7)
|
||||||
|
|
||||||
|
|
||||||
|
def read_ca_lambda_pe(self: "PQAutomationApp"):
|
||||||
|
"""读取 λd/Pe/Lv/XYZ(Display 8)。"""
|
||||||
|
return _read_ca_display(self, 8)
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ConnectionController",
|
"ConnectionController",
|
||||||
# 兼容层
|
# 兼容层
|
||||||
@@ -279,10 +447,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",
|
||||||
"check_port_connection",
|
"check_port_connection",
|
||||||
"enable_com_widgets",
|
"enable_com_widgets",
|
||||||
"disconnect_com_connections",
|
"disconnect_com_connections",
|
||||||
|
"disconnect_ucd_connection",
|
||||||
|
"disconnect_ca_connection",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -294,7 +467,19 @@ 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
|
||||||
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
|
||||||
|
_read_ca_display = _read_ca_display
|
||||||
|
read_ca_xyLv = read_ca_xyLv
|
||||||
|
read_ca_tcp_duv = read_ca_tcp_duv
|
||||||
|
read_ca_uvLv = read_ca_uvLv
|
||||||
|
read_ca_xyz = read_ca_xyz
|
||||||
|
read_ca_lambda_pe = read_ca_lambda_pe
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import threading
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -93,6 +94,11 @@ class TkLogHandler(logging.Handler):
|
|||||||
def emit(self, record: logging.LogRecord) -> None: # noqa: D401
|
def emit(self, record: logging.LogRecord) -> None: # noqa: D401
|
||||||
if getattr(record, _FROM_GUI_FLAG, False):
|
if getattr(record, _FROM_GUI_FLAG, False):
|
||||||
return
|
return
|
||||||
|
# Tkinter widgets are not thread-safe. Forwarding background-thread logs
|
||||||
|
# into GUI controls may block/hang the worker thread. Keep those logs in
|
||||||
|
# file handlers only, and only mirror main-thread logs to GUI.
|
||||||
|
if threading.current_thread() is not threading.main_thread():
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
message = self.format(record)
|
message = self.format(record)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -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,90 +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)
|
||||||
|
|
||||||
plt.close(fig)
|
plt.close(fig)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -114,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,7 +9,8 @@ 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
|
||||||
|
from matplotlib.ticker import MultipleLocator, AutoMinorLocator
|
||||||
|
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
|
||||||
|
|
||||||
@@ -55,14 +56,6 @@ _COLOR_MAP = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _grade_color(delta_e: float) -> str:
|
|
||||||
if delta_e < 3:
|
|
||||||
return "#1FAE45" # 绿
|
|
||||||
if delta_e < 5:
|
|
||||||
return "#E08A00" # 橙
|
|
||||||
return "#D81B1B" # 红
|
|
||||||
|
|
||||||
|
|
||||||
def _xy_to_uv(x: float, y: float):
|
def _xy_to_uv(x: float, y: float):
|
||||||
"""CIE 1931 xy → CIE 1976 u'v'"""
|
"""CIE 1931 xy → CIE 1976 u'v'"""
|
||||||
denom = -2.0 * x + 12.0 * y + 3.0
|
denom = -2.0 * x + 12.0 * y + 3.0
|
||||||
@@ -70,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)
|
||||||
@@ -86,36 +79,44 @@ 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]
|
||||||
edge_colors = [_grade_color(dE) for dE in delta_e_values]
|
|
||||||
|
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=edge_colors,
|
edgecolor=edgecolor,
|
||||||
linewidth=1.0,
|
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)
|
# 自动 minor tick
|
||||||
spine.set_linewidth(0.9)
|
ax.xaxis.set_minor_locator(AutoMinorLocator(2))
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -126,12 +127,17 @@ 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:
|
||||||
|
bg = bg[:, :, :3]
|
||||||
xmin, xmax, ymin, ymax = bbox
|
xmin, xmax, ymin, ymax = bbox
|
||||||
ax.imshow(
|
ax.imshow(
|
||||||
bg, extent=(xmin, xmax, ymin, ymax),
|
bg,
|
||||||
origin="upper", interpolation="bicubic",
|
extent=(xmin, xmax, ymin, ymax),
|
||||||
zorder=0, aspect="auto",
|
origin="lower",
|
||||||
|
interpolation="bilinear",
|
||||||
|
zorder=0,
|
||||||
|
aspect="auto",
|
||||||
)
|
)
|
||||||
ax.set_xlim(xmin, xmax)
|
ax.set_xlim(xmin, xmax)
|
||||||
ax.set_ylim(ymin, ymax)
|
ax.set_ylim(ymin, ymax)
|
||||||
@@ -145,73 +151,129 @@ def _draw_uv_diagram(ax, color_patches, measurements, standards, font_scale=1.0,
|
|||||||
legend_label_color = "#FFF" if dark_mode else "#111"
|
legend_label_color = "#FFF" if dark_mode else "#111"
|
||||||
legend_bg = "#111" if dark_mode else "#FFFFFF"
|
legend_bg = "#111" if dark_mode else "#FFFFFF"
|
||||||
legend_edge = "#FFF" if dark_mode else "#333"
|
legend_edge = "#FFF" if dark_mode else "#333"
|
||||||
outer_edge = "#FFFFFF" if dark_mode else "#333333"
|
outer_edge = "#FFFFFF" if dark_mode else "#222222"
|
||||||
|
|
||||||
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.set_title("CIE 1976 u'v'", fontsize=max(8, 11 * font_scale), fontweight="bold",
|
ax.xaxis.set_major_locator(MultipleLocator(0.1))
|
||||||
color=text_color, pad=4)
|
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(
|
||||||
|
"CIE 1976 u'v'",
|
||||||
|
fontsize=max(8, 11 * font_scale),
|
||||||
|
fontweight="bold",
|
||||||
|
color=text_color,
|
||||||
|
pad=4,
|
||||||
|
)
|
||||||
|
|
||||||
ax.set_xlabel("u'", fontsize=max(7, 9 * font_scale), color=sub_text_color, labelpad=1)
|
ax.set_xlabel("u'", fontsize=max(7, 9 * font_scale), color=sub_text_color, labelpad=1)
|
||||||
ax.set_ylabel("v'", fontsize=max(7, 9 * font_scale), color=sub_text_color, labelpad=1)
|
ax.set_ylabel("v'", fontsize=max(7, 9 * font_scale), color=sub_text_color, labelpad=1)
|
||||||
|
|
||||||
ax.tick_params(axis="both", labelsize=max(6, 8 * font_scale), colors=tick_color)
|
ax.tick_params(axis="both", labelsize=max(6, 8 * font_scale), colors=tick_color)
|
||||||
|
|
||||||
for sp in ax.spines.values():
|
for sp in ax.spines.values():
|
||||||
sp.set_color(outer_edge)
|
sp.set_color(outer_edge)
|
||||||
sp.set_linewidth(0.9)
|
sp.set_linewidth(0.9)
|
||||||
|
|
||||||
for name, meas in zip(color_patches, measurements):
|
for name, meas in zip(color_patches, measurements):
|
||||||
|
|
||||||
if meas is None or len(meas) < 2:
|
if meas is None or len(meas) < 2:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
mx, my = meas[0], meas[1]
|
mx, my = meas[0], meas[1]
|
||||||
sxy = standards.get(name)
|
sxy = standards.get(name)
|
||||||
|
|
||||||
if sxy is None:
|
if sxy is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
sx, sy = sxy
|
sx, sy = sxy
|
||||||
|
|
||||||
m_u, m_v = _xy_to_uv(mx, my)
|
m_u, m_v = _xy_to_uv(mx, my)
|
||||||
s_u, s_v = _xy_to_uv(sx, sy)
|
s_u, s_v = _xy_to_uv(sx, sy)
|
||||||
|
|
||||||
|
# face = get_patch_color_from_xy(name, (sx, sy)).strip().upper()
|
||||||
face = _COLOR_MAP.get(name, "#FFFFFF")
|
face = _COLOR_MAP.get(name, "#FFFFFF")
|
||||||
|
# face = get_patch_color_from_xy(name, (mx, my))
|
||||||
|
# face = "#FF0000"
|
||||||
|
|
||||||
# 目标点:仅空心方框(不填充标准颜色)
|
|
||||||
|
# 目标点(Target) 空心方框
|
||||||
ax.scatter(
|
ax.scatter(
|
||||||
[s_u], [s_v],
|
s_u,
|
||||||
s=56, marker="s",
|
s_v,
|
||||||
facecolors="none", edgecolors=outer_edge,
|
s=90,
|
||||||
linewidths=1.25, zorder=18,
|
marker="s",
|
||||||
|
facecolors="none",
|
||||||
|
edgecolors=outer_edge,
|
||||||
|
linewidths=1.6,
|
||||||
|
zorder=18,
|
||||||
)
|
)
|
||||||
# 实测点:白色外圈 + 内层圆点
|
|
||||||
|
# 实测点(Actual) 彩色实心 + 白色描边
|
||||||
ax.scatter(
|
ax.scatter(
|
||||||
[m_u], [m_v],
|
[m_u],
|
||||||
s=52, marker="o",
|
[m_v],
|
||||||
facecolors="none", edgecolors=outer_edge,
|
s=80,
|
||||||
linewidths=1.0, zorder=19,
|
marker="o",
|
||||||
)
|
color=face,
|
||||||
ax.scatter(
|
edgecolors=outer_edge,
|
||||||
[m_u], [m_v],
|
linewidths=1.2,
|
||||||
s=24, marker="o",
|
zorder=20,
|
||||||
facecolors=face, edgecolors="#111111",
|
|
||||||
linewidths=0.85, zorder=20,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# # Δu'v' 偏差连线
|
||||||
|
# ax.plot(
|
||||||
|
# [s_u, m_u],
|
||||||
|
# [s_v, m_v],
|
||||||
|
# color=face,
|
||||||
|
# linewidth=1.0,
|
||||||
|
# alpha=0.8,
|
||||||
|
# zorder=15,
|
||||||
|
# )
|
||||||
|
|
||||||
legend_handles = [
|
legend_handles = [
|
||||||
Line2D([0], [0], marker="s", linestyle="none",
|
Line2D(
|
||||||
markerfacecolor="#CCCCCC", markeredgecolor=outer_edge,
|
[0],
|
||||||
markersize=7, label="目标 (Target)"),
|
[0],
|
||||||
Line2D([0], [0], marker="o", linestyle="none",
|
marker="s",
|
||||||
markerfacecolor="#CCCCCC", markeredgecolor="#000000",
|
linestyle="none",
|
||||||
markersize=7, label="实测 (Actual)"),
|
markerfacecolor="none",
|
||||||
|
markeredgecolor=outer_edge,
|
||||||
|
markersize=9,
|
||||||
|
markeredgewidth=1.4,
|
||||||
|
label="目标 (Target)",
|
||||||
|
),
|
||||||
|
Line2D(
|
||||||
|
[0],
|
||||||
|
[0],
|
||||||
|
marker="o",
|
||||||
|
linestyle="none",
|
||||||
|
markerfacecolor="#AAAAAA",
|
||||||
|
markeredgecolor=outer_edge,
|
||||||
|
markersize=9,
|
||||||
|
markeredgewidth=1.2,
|
||||||
|
label="实测 (Actual)",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
leg = ax.legend(
|
leg = ax.legend(
|
||||||
handles=legend_handles,
|
handles=legend_handles,
|
||||||
loc="lower right", fontsize=max(6, 8 * font_scale),
|
loc="lower right",
|
||||||
framealpha=0.88, labelcolor=legend_label_color,
|
fontsize=max(6, 8 * font_scale),
|
||||||
|
framealpha=0.9,
|
||||||
|
labelcolor=legend_label_color,
|
||||||
)
|
)
|
||||||
if leg is not None:
|
|
||||||
|
if leg:
|
||||||
leg.get_frame().set_facecolor(legend_bg)
|
leg.get_frame().set_facecolor(legend_bg)
|
||||||
leg.get_frame().set_edgecolor(legend_edge)
|
leg.get_frame().set_edgecolor(legend_edge)
|
||||||
leg.set_zorder(50)
|
leg.set_zorder(50)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _draw_result_judgement(ax, accuracy_data, font_scale=1.0, dark_mode=False):
|
def _draw_result_judgement(ax, accuracy_data, font_scale=1.0, dark_mode=False):
|
||||||
"""底部结果条"""
|
"""底部结果条"""
|
||||||
ax.clear()
|
ax.clear()
|
||||||
@@ -253,23 +315,34 @@ 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()
|
||||||
fig = self.accuracy_fig
|
|
||||||
fig.clear()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from app.views.theme_manager import is_dark
|
from app.views.theme_manager import is_dark
|
||||||
dark_mode = is_dark()
|
dark_mode = is_dark()
|
||||||
except Exception:
|
except Exception:
|
||||||
dark_mode = False
|
dark_mode = False
|
||||||
|
|
||||||
fig.patch.set_facecolor("#1B1F24" if dark_mode else "#FFFFFF")
|
fig = self.accuracy_fig
|
||||||
|
fig.clear()
|
||||||
|
try:
|
||||||
|
fig.set_layout_engine(None)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
fig.set_tight_layout(False)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# 根据当前画布像素尺寸动态缩放字体,避免窗口缩小时文字挤压重叠。
|
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)
|
||||||
@@ -295,13 +368,12 @@ def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type):
|
|||||||
else:
|
else:
|
||||||
title = f"{test_type_name} - 色准测试(全 29色 | Gamma {target_gamma})"
|
title = f"{test_type_name} - 色准测试(全 29色 | Gamma {target_gamma})"
|
||||||
|
|
||||||
title_color = "#F3F5F7" if dark_mode else "#111"
|
|
||||||
fig.suptitle(
|
fig.suptitle(
|
||||||
title,
|
title,
|
||||||
fontsize=max(8, 11 * font_scale),
|
fontsize=max(8, 11 * font_scale),
|
||||||
y=0.975,
|
y=0.975,
|
||||||
fontweight="bold",
|
fontweight="bold",
|
||||||
color=title_color,
|
color=palette["fg"],
|
||||||
)
|
)
|
||||||
|
|
||||||
gs = fig.add_gridspec(
|
gs = fig.add_gridspec(
|
||||||
@@ -316,6 +388,8 @@ def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type):
|
|||||||
ax_left = fig.add_subplot(gs[0, 0])
|
ax_left = fig.add_subplot(gs[0, 0])
|
||||||
ax_uv = fig.add_subplot(gs[0, 1])
|
ax_uv = fig.add_subplot(gs[0, 1])
|
||||||
ax_judge = fig.add_subplot(gs[1, :])
|
ax_judge = fig.add_subplot(gs[1, :])
|
||||||
|
for ax in (ax_left, ax_uv, ax_judge):
|
||||||
|
ax.set_facecolor(palette["card_bg"])
|
||||||
|
|
||||||
# 兼容外部对 self.accuracy_ax 的引用
|
# 兼容外部对 self.accuracy_ax 的引用
|
||||||
self.accuracy_ax = ax_judge
|
self.accuracy_ax = ax_judge
|
||||||
@@ -353,8 +427,13 @@ def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.accuracy_canvas.draw()
|
# 重新刷新布局并绘制,确保画布尺寸与 notebook tab 对齐。
|
||||||
self.chart_notebook.select(self.accuracy_chart_frame)
|
self.chart_notebook.select(self.accuracy_chart_frame)
|
||||||
|
canvas_widget = self.accuracy_canvas.get_tk_widget()
|
||||||
|
canvas_widget.update_idletasks()
|
||||||
|
canvas_widget.update()
|
||||||
|
self.accuracy_canvas.draw()
|
||||||
|
canvas_widget.update_idletasks()
|
||||||
|
|
||||||
|
|
||||||
class PlotAccuracyMixin:
|
class PlotAccuracyMixin:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_cct 原样搬迁。
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from app.views.modern_styles import get_theme_palette
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -11,11 +12,36 @@ if TYPE_CHECKING:
|
|||||||
from pqAutomationApp import PQAutomationApp
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
def _is_dark_palette(palette: dict[str, str]) -> bool:
|
||||||
|
"""根据主题背景色亮度判断是否深色主题。"""
|
||||||
|
bg = palette.get("bg", "#FFFFFF").lstrip("#")
|
||||||
|
try:
|
||||||
|
r = int(bg[0:2], 16)
|
||||||
|
g = int(bg[2:4], 16)
|
||||||
|
b = int(bg[4:6], 16)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return (r * 299 + g * 587 + b * 114) / 1000 < 128
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def plot_cct(self: "PQAutomationApp", test_type):
|
def plot_cct(self: "PQAutomationApp", test_type):
|
||||||
"""绘制 x 和 y 坐标分离图 - 每个点标注纵坐标值"""
|
"""绘制 x 和 y 坐标分离图 - 每个点标注纵坐标值"""
|
||||||
|
palette = get_theme_palette()
|
||||||
|
dark_mode = _is_dark_palette(palette)
|
||||||
|
x_line_color = "#2F8BFF" if dark_mode else "#0A4BFF"
|
||||||
|
y_line_color = "#FF4D4D" if dark_mode else "#D90429"
|
||||||
|
ideal_line_color = "#00C853" if dark_mode else "#198754"
|
||||||
|
x_tol_color = "#FF7070" if dark_mode else "#C0392B"
|
||||||
|
y_tol_color = "#FFB74D" if dark_mode else "#D68910"
|
||||||
|
grid_color = "#566070" if dark_mode else "#B8BDC3"
|
||||||
|
axis_text = "#EDF2FA" if dark_mode else palette["fg"]
|
||||||
|
axis_sub_text = "#C8D2E0" if dark_mode else "#222222"
|
||||||
|
legend_bg = "#131821" if dark_mode else "#FFFFFF"
|
||||||
|
legend_edge = "#A4B2C6" if dark_mode else palette["border"]
|
||||||
|
|
||||||
self.cct_fig.clear()
|
self.cct_fig.clear()
|
||||||
|
self.cct_fig.patch.set_facecolor(palette["bg"])
|
||||||
|
|
||||||
gray_data = self.results.get_intermediate_data("shared", "gray")
|
gray_data = self.results.get_intermediate_data("shared", "gray")
|
||||||
if not gray_data:
|
if not gray_data:
|
||||||
@@ -31,7 +57,7 @@ def plot_cct(self: "PQAutomationApp", test_type):
|
|||||||
ha="center",
|
ha="center",
|
||||||
va="center",
|
va="center",
|
||||||
fontsize=14,
|
fontsize=14,
|
||||||
color="red",
|
color=palette["danger"],
|
||||||
)
|
)
|
||||||
ax.axis("off")
|
ax.axis("off")
|
||||||
self.cct_canvas.draw()
|
self.cct_canvas.draw()
|
||||||
@@ -57,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())
|
||||||
@@ -85,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
|
||||||
@@ -111,12 +158,20 @@ def plot_cct(self: "PQAutomationApp", test_type):
|
|||||||
# 为所有测试类型创建子图
|
# 为所有测试类型创建子图
|
||||||
ax1 = self.cct_fig.add_subplot(211)
|
ax1 = self.cct_fig.add_subplot(211)
|
||||||
ax2 = self.cct_fig.add_subplot(212)
|
ax2 = self.cct_fig.add_subplot(212)
|
||||||
|
for ax in (ax1, ax2):
|
||||||
|
ax.set_facecolor(palette["card_bg"])
|
||||||
|
for spine in ax.spines.values():
|
||||||
|
spine.set_color(palette["border"])
|
||||||
|
ax.tick_params(labelsize=8, colors=axis_sub_text)
|
||||||
|
ax.xaxis.label.set_color(axis_text)
|
||||||
|
ax.yaxis.label.set_color(axis_text)
|
||||||
|
|
||||||
# ========== 上图:x coordinates ==========
|
# ========== 上图:x coordinates ==========
|
||||||
ax1.plot(
|
ax1.plot(
|
||||||
grayscale,
|
grayscale,
|
||||||
x_measured,
|
x_measured,
|
||||||
"b-o",
|
color=x_line_color,
|
||||||
|
marker="o",
|
||||||
label="屏本体",
|
label="屏本体",
|
||||||
linewidth=2,
|
linewidth=2,
|
||||||
markersize=4,
|
markersize=4,
|
||||||
@@ -133,13 +188,13 @@ def plot_cct(self: "PQAutomationApp", test_type):
|
|||||||
ha="center",
|
ha="center",
|
||||||
va="bottom",
|
va="bottom",
|
||||||
fontsize=7,
|
fontsize=7,
|
||||||
color="blue",
|
color=x_line_color,
|
||||||
bbox=dict(
|
bbox=dict(
|
||||||
boxstyle="round,pad=0.2",
|
boxstyle="round,pad=0.2",
|
||||||
facecolor="white",
|
facecolor=palette["card_bg"],
|
||||||
edgecolor="blue",
|
edgecolor=x_line_color,
|
||||||
alpha=0.8,
|
alpha=0.92 if dark_mode else 0.85,
|
||||||
linewidth=0.5,
|
linewidth=0.8,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -147,7 +202,7 @@ def plot_cct(self: "PQAutomationApp", test_type):
|
|||||||
full_grayscale = np.linspace(0, 100, 100)
|
full_grayscale = np.linspace(0, 100, 100)
|
||||||
ax1.axhline(
|
ax1.axhline(
|
||||||
y=x_ideal,
|
y=x_ideal,
|
||||||
color="green",
|
color=ideal_line_color,
|
||||||
linestyle="--",
|
linestyle="--",
|
||||||
linewidth=1.5,
|
linewidth=1.5,
|
||||||
label=f"x-ideal ({x_ideal:.4f})",
|
label=f"x-ideal ({x_ideal:.4f})",
|
||||||
@@ -155,30 +210,34 @@ def plot_cct(self: "PQAutomationApp", test_type):
|
|||||||
)
|
)
|
||||||
ax1.axhline(
|
ax1.axhline(
|
||||||
y=x_low,
|
y=x_low,
|
||||||
color="red",
|
color=x_tol_color,
|
||||||
linestyle=":",
|
linestyle=":",
|
||||||
linewidth=1,
|
linewidth=1,
|
||||||
alpha=0.7,
|
alpha=0.95 if dark_mode else 0.7,
|
||||||
label=f"x-low ({x_low:.4f})",
|
label=f"x-low ({x_low:.4f})",
|
||||||
zorder=2,
|
zorder=2,
|
||||||
)
|
)
|
||||||
ax1.axhline(
|
ax1.axhline(
|
||||||
y=x_high,
|
y=x_high,
|
||||||
color="red",
|
color=x_tol_color,
|
||||||
linestyle=":",
|
linestyle=":",
|
||||||
linewidth=1,
|
linewidth=1,
|
||||||
alpha=0.7,
|
alpha=0.95 if dark_mode else 0.7,
|
||||||
label=f"x-high ({x_high:.4f})",
|
label=f"x-high ({x_high:.4f})",
|
||||||
zorder=2,
|
zorder=2,
|
||||||
)
|
)
|
||||||
ax1.fill_between(
|
ax1.fill_between(
|
||||||
full_grayscale, x_low, x_high, alpha=0.15, color="blue", zorder=1
|
full_grayscale,
|
||||||
|
x_low,
|
||||||
|
x_high,
|
||||||
|
alpha=0.22 if dark_mode else 0.15,
|
||||||
|
color=x_line_color,
|
||||||
|
zorder=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
ax1.set_xlabel("灰阶 (%)", fontsize=9)
|
ax1.set_xlabel("灰阶 (%)", fontsize=9, color=axis_text)
|
||||||
ax1.set_ylabel("CIE x", fontsize=9)
|
ax1.set_ylabel("CIE x", fontsize=9, color=axis_text)
|
||||||
ax1.grid(True, linestyle="--", alpha=0.3)
|
ax1.grid(True, linestyle="--", alpha=0.45 if dark_mode else 0.3, color=grid_color)
|
||||||
ax1.tick_params(labelsize=8)
|
|
||||||
ax1.set_xlim(0, 105)
|
ax1.set_xlim(0, 105)
|
||||||
|
|
||||||
# 纵坐标范围由用户参数控制
|
# 纵坐标范围由用户参数控制
|
||||||
@@ -211,7 +270,8 @@ def plot_cct(self: "PQAutomationApp", test_type):
|
|||||||
ax2.plot(
|
ax2.plot(
|
||||||
grayscale,
|
grayscale,
|
||||||
y_measured,
|
y_measured,
|
||||||
"r-o",
|
color=y_line_color,
|
||||||
|
marker="o",
|
||||||
label="屏本体",
|
label="屏本体",
|
||||||
linewidth=2,
|
linewidth=2,
|
||||||
markersize=4,
|
markersize=4,
|
||||||
@@ -228,19 +288,19 @@ def plot_cct(self: "PQAutomationApp", test_type):
|
|||||||
ha="center",
|
ha="center",
|
||||||
va="bottom",
|
va="bottom",
|
||||||
fontsize=7,
|
fontsize=7,
|
||||||
color="red",
|
color=y_line_color,
|
||||||
bbox=dict(
|
bbox=dict(
|
||||||
boxstyle="round,pad=0.2",
|
boxstyle="round,pad=0.2",
|
||||||
facecolor="white",
|
facecolor=palette["card_bg"],
|
||||||
edgecolor="red",
|
edgecolor=y_line_color,
|
||||||
alpha=0.8,
|
alpha=0.92 if dark_mode else 0.85,
|
||||||
linewidth=0.5,
|
linewidth=0.8,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
ax2.axhline(
|
ax2.axhline(
|
||||||
y=y_ideal,
|
y=y_ideal,
|
||||||
color="green",
|
color=ideal_line_color,
|
||||||
linestyle="--",
|
linestyle="--",
|
||||||
linewidth=1.5,
|
linewidth=1.5,
|
||||||
label=f"y-ideal ({y_ideal:.4f})",
|
label=f"y-ideal ({y_ideal:.4f})",
|
||||||
@@ -248,30 +308,34 @@ def plot_cct(self: "PQAutomationApp", test_type):
|
|||||||
)
|
)
|
||||||
ax2.axhline(
|
ax2.axhline(
|
||||||
y=y_low,
|
y=y_low,
|
||||||
color="orange",
|
color=y_tol_color,
|
||||||
linestyle=":",
|
linestyle=":",
|
||||||
linewidth=1,
|
linewidth=1,
|
||||||
alpha=0.7,
|
alpha=0.95 if dark_mode else 0.7,
|
||||||
label=f"y-low ({y_low:.4f})",
|
label=f"y-low ({y_low:.4f})",
|
||||||
zorder=2,
|
zorder=2,
|
||||||
)
|
)
|
||||||
ax2.axhline(
|
ax2.axhline(
|
||||||
y=y_high,
|
y=y_high,
|
||||||
color="orange",
|
color=y_tol_color,
|
||||||
linestyle=":",
|
linestyle=":",
|
||||||
linewidth=1,
|
linewidth=1,
|
||||||
alpha=0.7,
|
alpha=0.95 if dark_mode else 0.7,
|
||||||
label=f"y-high ({y_high:.4f})",
|
label=f"y-high ({y_high:.4f})",
|
||||||
zorder=2,
|
zorder=2,
|
||||||
)
|
)
|
||||||
ax2.fill_between(
|
ax2.fill_between(
|
||||||
full_grayscale, y_low, y_high, alpha=0.15, color="orange", zorder=1
|
full_grayscale,
|
||||||
|
y_low,
|
||||||
|
y_high,
|
||||||
|
alpha=0.22 if dark_mode else 0.15,
|
||||||
|
color=y_tol_color,
|
||||||
|
zorder=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
ax2.set_xlabel("灰阶 (%)", fontsize=9)
|
ax2.set_xlabel("灰阶 (%)", fontsize=9, color=axis_text)
|
||||||
ax2.set_ylabel("CIE y", fontsize=9)
|
ax2.set_ylabel("CIE y", fontsize=9, color=axis_text)
|
||||||
ax2.grid(True, linestyle="--", alpha=0.3)
|
ax2.grid(True, linestyle="--", alpha=0.45 if dark_mode else 0.3, color=grid_color)
|
||||||
ax2.tick_params(labelsize=8)
|
|
||||||
ax2.set_xlim(0, 105)
|
ax2.set_xlim(0, 105)
|
||||||
|
|
||||||
# 纵坐标范围由用户参数控制
|
# 纵坐标范围由用户参数控制
|
||||||
@@ -307,6 +371,7 @@ def plot_cct(self: "PQAutomationApp", test_type):
|
|||||||
fontsize=12,
|
fontsize=12,
|
||||||
y=0.98,
|
y=0.98,
|
||||||
fontweight="bold",
|
fontweight="bold",
|
||||||
|
color=palette["fg"],
|
||||||
)
|
)
|
||||||
|
|
||||||
self.cct_fig.subplots_adjust(
|
self.cct_fig.subplots_adjust(
|
||||||
@@ -317,12 +382,25 @@ def plot_cct(self: "PQAutomationApp", test_type):
|
|||||||
hspace=0.30,
|
hspace=0.30,
|
||||||
)
|
)
|
||||||
|
|
||||||
ax1.legend(
|
legend1 = ax1.legend(
|
||||||
fontsize=7, loc="center left", bbox_to_anchor=(1.05, 0.5), framealpha=1.0
|
fontsize=7,
|
||||||
|
loc="center left",
|
||||||
|
bbox_to_anchor=(1.05, 0.5),
|
||||||
|
framealpha=0.92 if dark_mode else 1.0,
|
||||||
|
facecolor=legend_bg,
|
||||||
|
edgecolor=legend_edge,
|
||||||
)
|
)
|
||||||
ax2.legend(
|
legend2 = ax2.legend(
|
||||||
fontsize=7, loc="center left", bbox_to_anchor=(1.05, 0.5), framealpha=1.0
|
fontsize=7,
|
||||||
|
loc="center left",
|
||||||
|
bbox_to_anchor=(1.05, 0.5),
|
||||||
|
framealpha=0.92 if dark_mode else 1.0,
|
||||||
|
facecolor=legend_bg,
|
||||||
|
edgecolor=legend_edge,
|
||||||
)
|
)
|
||||||
|
for legend in (legend1, legend2):
|
||||||
|
for text in legend.get_texts():
|
||||||
|
text.set_color(axis_text)
|
||||||
|
|
||||||
self.cct_canvas.draw()
|
self.cct_canvas.draw()
|
||||||
self.chart_notebook.select(self.cct_chart_frame)
|
self.chart_notebook.select(self.cct_chart_frame)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_contrast 原样搬迁
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from matplotlib.patches import Rectangle
|
from matplotlib.patches import Rectangle
|
||||||
|
from app.views.modern_styles import get_theme_palette
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -14,9 +15,12 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
def plot_contrast(self: "PQAutomationApp", contrast_data, test_type):
|
def plot_contrast(self: "PQAutomationApp", contrast_data, test_type):
|
||||||
"""绘制对比度测试结果 - 固定布局版本"""
|
"""绘制对比度测试结果 - 固定布局版本"""
|
||||||
|
palette = get_theme_palette()
|
||||||
|
|
||||||
# 清空并重置
|
# 清空并重置
|
||||||
self.contrast_ax.clear()
|
self.contrast_ax.clear()
|
||||||
|
self.contrast_fig.patch.set_facecolor(palette["bg"])
|
||||||
|
self.contrast_ax.set_facecolor(palette["card_bg"])
|
||||||
self.contrast_ax.set_xlim(0, 1)
|
self.contrast_ax.set_xlim(0, 1)
|
||||||
self.contrast_ax.set_ylim(0, 1)
|
self.contrast_ax.set_ylim(0, 1)
|
||||||
self.contrast_ax.axis("off")
|
self.contrast_ax.axis("off")
|
||||||
@@ -51,6 +55,7 @@ def plot_contrast(self: "PQAutomationApp", contrast_data, test_type):
|
|||||||
fontsize=12,
|
fontsize=12,
|
||||||
y=0.98,
|
y=0.98,
|
||||||
fontweight="bold",
|
fontweight="bold",
|
||||||
|
color=palette["fg"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# ========== 中央大对比度卡片 ==========
|
# ========== 中央大对比度卡片 ==========
|
||||||
@@ -107,16 +112,16 @@ def plot_contrast(self: "PQAutomationApp", contrast_data, test_type):
|
|||||||
"title": "白场亮度",
|
"title": "白场亮度",
|
||||||
"value": f"{max_lum:.2f}",
|
"value": f"{max_lum:.2f}",
|
||||||
"unit": "cd/m²",
|
"unit": "cd/m²",
|
||||||
"color": "#E3F2FD",
|
"color": palette["surface_alt_bg"],
|
||||||
"edge_color": "#2196F3",
|
"edge_color": palette["primary"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": start_x + card_width + gap,
|
"x": start_x + card_width + gap,
|
||||||
"title": "黑场亮度",
|
"title": "黑场亮度",
|
||||||
"value": f"{min_lum:.4f}",
|
"value": f"{min_lum:.4f}",
|
||||||
"unit": "cd/m²",
|
"unit": "cd/m²",
|
||||||
"color": "#F3E5F5",
|
"color": palette["card_bg"],
|
||||||
"edge_color": "#9C27B0",
|
"edge_color": palette["secondary"],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -142,6 +147,7 @@ def plot_contrast(self: "PQAutomationApp", contrast_data, test_type):
|
|||||||
va="top",
|
va="top",
|
||||||
fontsize=10,
|
fontsize=10,
|
||||||
fontweight="bold",
|
fontweight="bold",
|
||||||
|
color=palette["fg"],
|
||||||
transform=self.contrast_ax.transAxes,
|
transform=self.contrast_ax.transAxes,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -154,6 +160,7 @@ def plot_contrast(self: "PQAutomationApp", contrast_data, test_type):
|
|||||||
va="center",
|
va="center",
|
||||||
fontsize=16,
|
fontsize=16,
|
||||||
fontweight="bold",
|
fontweight="bold",
|
||||||
|
color=palette["fg"],
|
||||||
transform=self.contrast_ax.transAxes,
|
transform=self.contrast_ax.transAxes,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -165,7 +172,7 @@ def plot_contrast(self: "PQAutomationApp", contrast_data, test_type):
|
|||||||
ha="center",
|
ha="center",
|
||||||
va="bottom",
|
va="bottom",
|
||||||
fontsize=9,
|
fontsize=9,
|
||||||
color="gray",
|
color=palette["muted_fg"],
|
||||||
transform=self.contrast_ax.transAxes,
|
transform=self.contrast_ax.transAxes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_eotf 原样搬迁。
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from app.views.modern_styles import get_theme_palette
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -14,15 +15,21 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
def plot_eotf(self: "PQAutomationApp", L_bar, results_with_eotf_list, test_type):
|
def plot_eotf(self: "PQAutomationApp", L_bar, results_with_eotf_list, test_type):
|
||||||
"""绘制 EOTF 曲线 + 数据表格(HDR 专用,包含实测亮度)"""
|
"""绘制 EOTF 曲线 + 数据表格(HDR 专用,包含实测亮度)"""
|
||||||
|
palette = get_theme_palette()
|
||||||
|
|
||||||
# ========== 1. 清空并重置左侧曲线 ==========
|
# ========== 1. 清空并重置左侧曲线 ==========
|
||||||
self.eotf_ax.clear()
|
self.eotf_ax.clear()
|
||||||
|
self.eotf_fig.patch.set_facecolor(palette["bg"])
|
||||||
|
self.eotf_ax.set_facecolor(palette["card_bg"])
|
||||||
self.eotf_ax.set_xlim(0, 105)
|
self.eotf_ax.set_xlim(0, 105)
|
||||||
self.eotf_ax.set_ylim(0, 1.1)
|
self.eotf_ax.set_ylim(0, 1.1)
|
||||||
self.eotf_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
self.eotf_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
||||||
self.eotf_ax.set_ylabel("L_bar", fontsize=10)
|
self.eotf_ax.set_ylabel("L_bar", fontsize=10)
|
||||||
self.eotf_ax.grid(True, linestyle="--", alpha=0.3)
|
self.eotf_ax.grid(True, linestyle="--", alpha=0.3)
|
||||||
self.eotf_ax.tick_params(labelsize=9)
|
self.eotf_ax.tick_params(labelsize=9)
|
||||||
|
self.eotf_ax.tick_params(colors=palette["fg"])
|
||||||
|
for spine in self.eotf_ax.spines.values():
|
||||||
|
spine.set_color(palette["border"])
|
||||||
|
|
||||||
# 生成横坐标(灰阶百分比)
|
# 生成横坐标(灰阶百分比)
|
||||||
x_values = np.linspace(0, 100, len(L_bar))
|
x_values = np.linspace(0, 100, len(L_bar))
|
||||||
@@ -120,17 +127,17 @@ def plot_eotf(self: "PQAutomationApp", L_bar, results_with_eotf_list, test_type)
|
|||||||
# 表头样式
|
# 表头样式
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
cell = table[(0, i)]
|
cell = table[(0, i)]
|
||||||
cell.set_facecolor("#4472C4")
|
cell.set_facecolor(palette["primary"])
|
||||||
cell.set_text_props(weight="bold", color="white")
|
cell.set_text_props(weight="bold", color=palette["select_fg"])
|
||||||
|
|
||||||
# 数据行交替颜色
|
# 数据行交替颜色
|
||||||
for i in range(1, len(table_data)):
|
for i in range(1, len(table_data)):
|
||||||
for j in range(4):
|
for j in range(4):
|
||||||
cell = table[(i, j)]
|
cell = table[(i, j)]
|
||||||
if i % 2 == 0:
|
if i % 2 == 0:
|
||||||
cell.set_facecolor("#E7E6E6")
|
cell.set_facecolor(palette["surface_alt_bg"])
|
||||||
else:
|
else:
|
||||||
cell.set_facecolor("#FFFFFF")
|
cell.set_facecolor(palette["card_bg"])
|
||||||
|
|
||||||
# ========== 3. 总标题 ==========
|
# ========== 3. 总标题 ==========
|
||||||
test_type_name = self.get_test_type_name(test_type)
|
test_type_name = self.get_test_type_name(test_type)
|
||||||
@@ -139,6 +146,7 @@ def plot_eotf(self: "PQAutomationApp", L_bar, results_with_eotf_list, test_type)
|
|||||||
fontsize=12,
|
fontsize=12,
|
||||||
y=0.98,
|
y=0.98,
|
||||||
fontweight="bold",
|
fontweight="bold",
|
||||||
|
color=palette["fg"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# 选中 EOTF Tab
|
# 选中 EOTF Tab
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_gamma 原样搬迁。
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from app.views.modern_styles import get_theme_palette
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -11,18 +12,44 @@ if TYPE_CHECKING:
|
|||||||
from pqAutomationApp import PQAutomationApp
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
def _is_dark_palette(palette: dict[str, str]) -> bool:
|
||||||
|
"""根据主题背景色亮度判断是否深色主题。"""
|
||||||
|
bg = palette.get("bg", "#FFFFFF").lstrip("#")
|
||||||
|
try:
|
||||||
|
r = int(bg[0:2], 16)
|
||||||
|
g = int(bg[2:4], 16)
|
||||||
|
b = int(bg[4:6], 16)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return (r * 299 + g * 587 + b * 114) / 1000 < 128
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def plot_gamma(self: "PQAutomationApp", L_bar, results_with_gamma_list, target_gamma, test_type):
|
def plot_gamma(self: "PQAutomationApp", L_bar, results_with_gamma_list, target_gamma, test_type):
|
||||||
"""绘制Gamma曲线 + 数据表格(包含实测亮度)"""
|
"""绘制Gamma曲线 + 数据表格(包含实测亮度)"""
|
||||||
|
palette = get_theme_palette()
|
||||||
|
dark_mode = _is_dark_palette(palette)
|
||||||
|
line_actual = "#3FA7FF" if dark_mode else "#0A4BFF"
|
||||||
|
line_ideal = "#FF6B6B" if dark_mode else "#D62828"
|
||||||
|
grid_color = "#566070" if dark_mode else "#B8BDC3"
|
||||||
|
legend_bg = "#131821" if dark_mode else "#FFFFFF"
|
||||||
|
legend_edge = "#A4B2C6" if dark_mode else palette["border"]
|
||||||
|
table_edge = "#738196" if dark_mode else palette["border"]
|
||||||
|
table_body_fg = "#EEF3FA" if dark_mode else palette["fg"]
|
||||||
|
|
||||||
# ========== 1. 清空并重置左侧曲线 ==========
|
# ========== 1. 清空并重置左侧曲线 ==========
|
||||||
self.gamma_ax.clear()
|
self.gamma_ax.clear()
|
||||||
|
self.gamma_fig.patch.set_facecolor(palette["bg"])
|
||||||
|
self.gamma_ax.set_facecolor(palette["card_bg"])
|
||||||
self.gamma_ax.set_xlim(0, 105)
|
self.gamma_ax.set_xlim(0, 105)
|
||||||
self.gamma_ax.set_ylim(0, 1.1)
|
self.gamma_ax.set_ylim(0, 1.1)
|
||||||
self.gamma_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
self.gamma_ax.set_xlabel("灰阶 (%)", fontsize=10, color=palette["fg"])
|
||||||
self.gamma_ax.set_ylabel("L_bar", fontsize=10)
|
self.gamma_ax.set_ylabel("L_bar", fontsize=10, color=palette["fg"])
|
||||||
self.gamma_ax.grid(True, linestyle="--", alpha=0.3)
|
self.gamma_ax.grid(True, linestyle="--", alpha=0.45 if dark_mode else 0.3, color=grid_color)
|
||||||
self.gamma_ax.tick_params(labelsize=9)
|
self.gamma_ax.tick_params(labelsize=9)
|
||||||
|
self.gamma_ax.tick_params(colors=palette["fg"])
|
||||||
|
for spine in self.gamma_ax.spines.values():
|
||||||
|
spine.set_color(palette["border"])
|
||||||
|
|
||||||
# 生成横坐标(灰阶百分比)
|
# 生成横坐标(灰阶百分比)
|
||||||
x_values = np.linspace(0, 100, len(L_bar))
|
x_values = np.linspace(0, 100, len(L_bar))
|
||||||
@@ -46,7 +73,8 @@ def plot_gamma(self: "PQAutomationApp", L_bar, results_with_gamma_list, target_g
|
|||||||
self.gamma_ax.plot(
|
self.gamma_ax.plot(
|
||||||
x_values,
|
x_values,
|
||||||
L_bar,
|
L_bar,
|
||||||
"b-o",
|
color=line_actual,
|
||||||
|
marker="o",
|
||||||
label=f"实测 (平均γ={avg_gamma:.2f})",
|
label=f"实测 (平均γ={avg_gamma:.2f})",
|
||||||
linewidth=2,
|
linewidth=2,
|
||||||
markersize=4,
|
markersize=4,
|
||||||
@@ -58,15 +86,24 @@ def plot_gamma(self: "PQAutomationApp", L_bar, results_with_gamma_list, target_g
|
|||||||
self.gamma_ax.plot(
|
self.gamma_ax.plot(
|
||||||
x_values,
|
x_values,
|
||||||
ideal_L_bar,
|
ideal_L_bar,
|
||||||
"r--",
|
color=line_ideal,
|
||||||
|
linestyle="--",
|
||||||
label=f"理想 (γ={target_gamma})",
|
label=f"理想 (γ={target_gamma})",
|
||||||
linewidth=2,
|
linewidth=2,
|
||||||
alpha=0.7,
|
alpha=0.9 if dark_mode else 0.7,
|
||||||
zorder=3,
|
zorder=3,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 图例
|
# 图例
|
||||||
self.gamma_ax.legend(fontsize=9, loc="upper left", framealpha=0.95)
|
legend = self.gamma_ax.legend(
|
||||||
|
fontsize=9,
|
||||||
|
loc="upper left",
|
||||||
|
framealpha=0.9 if dark_mode else 0.95,
|
||||||
|
facecolor=legend_bg,
|
||||||
|
edgecolor=legend_edge,
|
||||||
|
)
|
||||||
|
for text in legend.get_texts():
|
||||||
|
text.set_color(palette["fg"])
|
||||||
|
|
||||||
# ========== 2. 清空并绘制右侧表格 ==========
|
# ========== 2. 清空并绘制右侧表格 ==========
|
||||||
self.gamma_table_ax.clear()
|
self.gamma_table_ax.clear()
|
||||||
@@ -120,17 +157,22 @@ def plot_gamma(self: "PQAutomationApp", L_bar, results_with_gamma_list, target_g
|
|||||||
# 表头样式
|
# 表头样式
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
cell = table[(0, i)]
|
cell = table[(0, i)]
|
||||||
cell.set_facecolor("#4472C4")
|
cell.set_facecolor(palette["primary"])
|
||||||
cell.set_text_props(weight="bold", color="white")
|
cell.set_text_props(weight="bold", color=palette["select_fg"])
|
||||||
|
cell.set_edgecolor(table_edge)
|
||||||
|
cell.set_linewidth(0.8)
|
||||||
|
|
||||||
# 数据行交替颜色
|
# 数据行交替颜色
|
||||||
for i in range(1, len(table_data)):
|
for i in range(1, len(table_data)):
|
||||||
for j in range(4):
|
for j in range(4):
|
||||||
cell = table[(i, j)]
|
cell = table[(i, j)]
|
||||||
if i % 2 == 0:
|
if i % 2 == 0:
|
||||||
cell.set_facecolor("#E7E6E6")
|
cell.set_facecolor(palette["surface_alt_bg"])
|
||||||
else:
|
else:
|
||||||
cell.set_facecolor("#FFFFFF")
|
cell.set_facecolor(palette["card_bg"])
|
||||||
|
cell.set_text_props(color=table_body_fg)
|
||||||
|
cell.set_edgecolor(table_edge)
|
||||||
|
cell.set_linewidth(0.6)
|
||||||
|
|
||||||
# ========== 3. 总标题 ==========
|
# ========== 3. 总标题 ==========
|
||||||
test_type_name = self.get_test_type_name(test_type)
|
test_type_name = self.get_test_type_name(test_type)
|
||||||
@@ -139,6 +181,7 @@ def plot_gamma(self: "PQAutomationApp", L_bar, results_with_gamma_list, target_g
|
|||||||
fontsize=12,
|
fontsize=12,
|
||||||
y=0.98,
|
y=0.98,
|
||||||
fontweight="bold",
|
fontweight="bold",
|
||||||
|
color=palette["fg"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# ========== 4. 绘制到画布 ==========
|
# ========== 4. 绘制到画布 ==========
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ def _style_axes(ax, *, title, xlabel, ylabel, xlim, ylim, dark_mode):
|
|||||||
ax.set_ylabel(ylabel, fontsize=10, color=text)
|
ax.set_ylabel(ylabel, fontsize=10, color=text)
|
||||||
ax.set_xlim(*xlim)
|
ax.set_xlim(*xlim)
|
||||||
ax.set_ylim(*ylim)
|
ax.set_ylim(*ylim)
|
||||||
ax.set_aspect("equal", adjustable="datalim")
|
ax.set_aspect("equal", adjustable="box")
|
||||||
ax.grid(True, linestyle=":", linewidth=0.7, color=grid, alpha=0.32)
|
ax.grid(True, linestyle=":", linewidth=0.7, color=grid, alpha=0.32)
|
||||||
ax.tick_params(axis="both", labelsize=9, colors=text)
|
ax.tick_params(axis="both", labelsize=9, colors=text)
|
||||||
for spine in ax.spines.values():
|
for spine in ax.spines.values():
|
||||||
@@ -193,8 +193,10 @@ def _blit_background(ax, background, bbox):
|
|||||||
ax.imshow(
|
ax.imshow(
|
||||||
background,
|
background,
|
||||||
extent=(xmin, xmax, ymin, ymax),
|
extent=(xmin, xmax, ymin, ymax),
|
||||||
origin="upper", # canvas.buffer_rgba 行 0 为顶部
|
# gamut_background._render_chromaticity 已做过 np.flipud,
|
||||||
interpolation="bicubic",
|
# 这里必须使用 lower 才能与真实色度坐标方向一致。
|
||||||
|
origin="lower",
|
||||||
|
interpolation="bilinear",
|
||||||
zorder=0,
|
zorder=0,
|
||||||
aspect="auto", # 由 _style_axes 的 set_aspect("equal") 控制
|
aspect="auto", # 由 _style_axes 的 set_aspect("equal") 控制
|
||||||
)
|
)
|
||||||
@@ -269,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,
|
||||||
@@ -341,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,
|
||||||
|
|||||||
101
app/pq/color_patch_map.py
Normal file
101
app/pq/color_patch_map.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import re
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 标准 ColorChecker / TV Patch 颜色
|
||||||
|
# ============================================================
|
||||||
|
COLOR_PATCH_MAP = {
|
||||||
|
"White": "#FFFFFF",
|
||||||
|
"Gray 80": "#E6E6E6",
|
||||||
|
"Gray 65": "#D1D1D1",
|
||||||
|
"Gray 50": "#BABABA",
|
||||||
|
"Gray 35": "#9E9E9E",
|
||||||
|
"Black": "#000000",
|
||||||
|
|
||||||
|
"Dark Skin": "#735242",
|
||||||
|
"Light Skin": "#C29682",
|
||||||
|
"Blue Sky": "#5E7A9C",
|
||||||
|
"Foliage": "#596B42",
|
||||||
|
"Blue Flower": "#8280B0",
|
||||||
|
"Bluish Green": "#63BDA8",
|
||||||
|
"Orange": "#D97829",
|
||||||
|
"Purplish Blue": "#4A5CA3",
|
||||||
|
"Moderate Red": "#C25461",
|
||||||
|
"Purple": "#5C3D6B",
|
||||||
|
"Yellow Green": "#9EBA40",
|
||||||
|
"Orange Yellow": "#E6A12E",
|
||||||
|
|
||||||
|
"Blue (Legacy)": "#333D96",
|
||||||
|
"Green (Legacy)": "#479447",
|
||||||
|
"Red (Legacy)": "#B0303B",
|
||||||
|
"Yellow (Legacy)": "#EDC721",
|
||||||
|
"Magenta (Legacy)": "#BA5491",
|
||||||
|
"Cyan (Legacy)": "#0085A3",
|
||||||
|
|
||||||
|
"100% Red": "#FF0000",
|
||||||
|
"100% Green": "#00FF00",
|
||||||
|
"100% Blue": "#0000FF",
|
||||||
|
"100% Cyan": "#00FFFF",
|
||||||
|
"100% Magenta": "#FF00FF",
|
||||||
|
"100% Yellow": "#FFFF00",
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 名称标准化
|
||||||
|
# ============================================================
|
||||||
|
def normalize_patch_name(name: str) -> str:
|
||||||
|
if not isinstance(name, str):
|
||||||
|
return ""
|
||||||
|
name = name.strip().lower()
|
||||||
|
# collapse spaces
|
||||||
|
name = " ".join(name.split())
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 获取 patch 颜色
|
||||||
|
# ============================================================
|
||||||
|
def get_patch_color(name: str):
|
||||||
|
norm = normalize_patch_name(name)
|
||||||
|
for key, color in COLOR_PATCH_MAP.items():
|
||||||
|
|
||||||
|
if normalize_patch_name(key) == norm:
|
||||||
|
return color
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# xy → sRGB fallback
|
||||||
|
# ============================================================
|
||||||
|
def xy_to_srgb(x, y, Y=1.0):
|
||||||
|
if y == 0:
|
||||||
|
return "#AAAAAA"
|
||||||
|
|
||||||
|
X = (x * Y) / y
|
||||||
|
Z = (1 - x - y) * Y / y
|
||||||
|
|
||||||
|
rgb = np.dot(
|
||||||
|
[[3.2406, -1.5372, -0.4986],
|
||||||
|
[-0.9689, 1.8758, 0.0415],
|
||||||
|
[0.0557, -0.2040, 1.0570]],
|
||||||
|
[X, Y, Z]
|
||||||
|
)
|
||||||
|
|
||||||
|
rgb = np.clip(rgb, 0, 1)
|
||||||
|
rgb = (rgb ** (1 / 2.2)) * 255
|
||||||
|
|
||||||
|
r, g, b = rgb.astype(int)
|
||||||
|
return "#{:02X}{:02X}{:02X}".format(r, g, b)
|
||||||
|
|
||||||
|
def get_patch_color_from_xy(name, xy=None):
|
||||||
|
color = get_patch_color(name)
|
||||||
|
|
||||||
|
if color:
|
||||||
|
return color
|
||||||
|
|
||||||
|
if xy is not None:
|
||||||
|
x, y = xy
|
||||||
|
return xy_to_srgb(x, y)
|
||||||
|
|
||||||
|
return "#AAAAAA"
|
||||||
@@ -330,9 +330,7 @@ def send_fix_pattern(self: "PQAutomationApp", mode):
|
|||||||
if mode == "screen_module":
|
if mode == "screen_module":
|
||||||
format_changed = True
|
format_changed = True
|
||||||
else:
|
else:
|
||||||
format_changed = bool(
|
format_changed = bool(self.signal_service.format_changed)
|
||||||
getattr(getattr(self, "ucd", None), "format_changed", True)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 预热提交:prepare_session 仅 stage 了新的 color/timing/pattern,
|
# 预热提交:prepare_session 仅 stage 了新的 color/timing/pattern,
|
||||||
# 真正的 ``pg.apply()`` 要到第一次发图时才发生。提前发送首个 pattern,
|
# 真正的 ``pg.apply()`` 要到第一次发图时才发生。提前发送首个 pattern,
|
||||||
@@ -341,9 +339,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:
|
||||||
@@ -394,8 +392,7 @@ def send_fix_pattern(self: "PQAutomationApp", mode):
|
|||||||
# 测量数据
|
# 测量数据
|
||||||
if mode == "custom":
|
if mode == "custom":
|
||||||
result = []
|
result = []
|
||||||
self.ca.set_Display(1)
|
tcp, duv, lv, X, Y, Z = self.read_ca_tcp_duv()
|
||||||
tcp, duv, lv, X, Y, Z = self.ca.readAllDisplay()
|
|
||||||
|
|
||||||
if should_log_detail:
|
if should_log_detail:
|
||||||
self.log_gui.log(
|
self.log_gui.log(
|
||||||
@@ -403,8 +400,7 @@ def send_fix_pattern(self: "PQAutomationApp", mode):
|
|||||||
f"X={X:.4f}, Y={Y:.4f}, Z={Z:.4f}"
|
f"X={X:.4f}, Y={Y:.4f}, Z={Z:.4f}"
|
||||||
, level="success")
|
, level="success")
|
||||||
|
|
||||||
self.ca.set_Display(8)
|
lambda_, Pe, lv, X, Y, Z = self.read_ca_lambda_pe()
|
||||||
lambda_, Pe, lv, X, Y, Z = self.ca.readAllDisplay()
|
|
||||||
|
|
||||||
if should_log_detail:
|
if should_log_detail:
|
||||||
self.log_gui.log(
|
self.log_gui.log(
|
||||||
@@ -449,9 +445,7 @@ def send_fix_pattern(self: "PQAutomationApp", mode):
|
|||||||
self.log_gui.log(f"第 {i+1} 行实时结果写入失败: {str(e)}", level="error")
|
self.log_gui.log(f"第 {i+1} 行实时结果写入失败: {str(e)}", level="error")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.ca.set_xyLv_Display()
|
x, y, lv, X, Y, Z = self.read_ca_xyLv()
|
||||||
|
|
||||||
x, y, lv, X, Y, Z = self.ca.readAllDisplay()
|
|
||||||
results.append([x, y, lv, X, Y, Z])
|
results.append([x, y, lv, X, Y, Z])
|
||||||
|
|
||||||
if should_log_detail:
|
if should_log_detail:
|
||||||
@@ -835,6 +829,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)
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
from app.services.pattern_service import PatternService, PatternSession
|
from app.ucd import PatternService, PatternSession
|
||||||
|
|
||||||
|
__all__ = ["PatternService", "PatternSession"]
|
||||||
|
|||||||
@@ -293,62 +293,107 @@ def _call_pqtest_upload(file_path: str, timeout: float = API_UPLOAD_TIMEOUT, aut
|
|||||||
iw, ih = img.size
|
iw, ih = img.size
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise ValueError(f"无法读取图片: {exc}") from exc
|
raise ValueError(f"无法读取图片: {exc}") from exc
|
||||||
|
|
||||||
# 检查大小,如需则缩放
|
|
||||||
size = os.path.getsize(file_path)
|
size = os.path.getsize(file_path)
|
||||||
needs_resize = (iw > UPLOAD_MAX_PIXELS or ih > UPLOAD_MAX_PIXELS or size > UPLOAD_MAX_BYTES)
|
needs_resize = (iw > UPLOAD_MAX_PIXELS or ih > UPLOAD_MAX_PIXELS or size > UPLOAD_MAX_BYTES)
|
||||||
|
file_stem = os.path.splitext(os.path.basename(file_path))[0]
|
||||||
|
upload_ext = ext
|
||||||
|
mime = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
|
||||||
|
|
||||||
if needs_resize:
|
if needs_resize:
|
||||||
if not auto_resize:
|
if not auto_resize:
|
||||||
if iw > UPLOAD_MAX_PIXELS or ih > UPLOAD_MAX_PIXELS:
|
if iw > UPLOAD_MAX_PIXELS or ih > UPLOAD_MAX_PIXELS:
|
||||||
raise ValueError(f"分辨率超过 4096×4096(当前 {iw}×{ih})")
|
raise ValueError(f"分辨率超过 4096×4096(当前 {iw}×{ih})")
|
||||||
else:
|
raise ValueError(f"图片超过 10MB 限制(当前 {size/1024/1024:.2f}MB)")
|
||||||
raise ValueError(f"图片超过 10MB 限制(当前 {size/1024/1024:.2f}MB)")
|
|
||||||
|
logger.info(
|
||||||
# 自动缩放:等比例缩放至 4096×4096 以内
|
"[AIImage][UPLOAD] 自动处理超限图片 %dx%d (%.2fMB)",
|
||||||
logger.info("[AIImage][UPLOAD] 自动缩放 %dx%d (%.1fMB) 至 ≤4096×4096",
|
iw,
|
||||||
iw, ih, size/1024/1024)
|
ih,
|
||||||
scale = min(UPLOAD_MAX_PIXELS / iw, UPLOAD_MAX_PIXELS / ih, 1.0)
|
size / 1024 / 1024,
|
||||||
new_w, new_h = max(1, int(iw * scale)), max(1, int(ih * scale))
|
)
|
||||||
|
|
||||||
with Image.open(file_path) as img:
|
with Image.open(file_path) as img:
|
||||||
img_resized = img.resize((new_w, new_h), Image.LANCZOS)
|
working = img.copy()
|
||||||
# 重压至 10MB 以下
|
|
||||||
# 首先尝试原格式
|
# 先做一次分辨率约束,避免后续压缩开销过大。
|
||||||
tmp_io = BytesIO()
|
scale = min(UPLOAD_MAX_PIXELS / max(1, working.width), UPLOAD_MAX_PIXELS / max(1, working.height), 1.0)
|
||||||
fmt = "PNG" if ext == ".png" else "JPEG"
|
if scale < 1.0:
|
||||||
save_kw = {"format": fmt}
|
working = working.resize(
|
||||||
img_resized.save(tmp_io, **save_kw)
|
(max(1, int(working.width * scale)), max(1, int(working.height * scale))),
|
||||||
tmp_bytes = tmp_io.getvalue()
|
Image.LANCZOS,
|
||||||
|
)
|
||||||
if len(tmp_bytes) <= UPLOAD_MAX_BYTES:
|
|
||||||
file_bytes = tmp_bytes
|
best_bytes = b""
|
||||||
|
best_mime = mime
|
||||||
|
best_ext = upload_ext
|
||||||
|
|
||||||
|
# 第一优先:保持原格式。
|
||||||
|
try:
|
||||||
|
raw_io = BytesIO()
|
||||||
|
if ext == ".png":
|
||||||
|
working.save(raw_io, format="PNG", optimize=True)
|
||||||
|
raw_mime, raw_ext = "image/png", ".png"
|
||||||
else:
|
else:
|
||||||
# 原格式太大,转换为 JPEG 并压缩
|
rgb = working.convert("RGB") if working.mode not in {"RGB", "L"} else working
|
||||||
logger.info("[AIImage][UPLOAD] 原格式超过限制,转为 JPEG 并压缩")
|
rgb.save(raw_io, format="JPEG", quality=95, optimize=True)
|
||||||
quality = 95
|
raw_mime, raw_ext = "image/jpeg", ".jpg"
|
||||||
while quality >= 50:
|
best_bytes = raw_io.getvalue()
|
||||||
tmp_io = BytesIO()
|
best_mime = raw_mime
|
||||||
img_resized.save(tmp_io, format="JPEG", quality=quality, optimize=True)
|
best_ext = raw_ext
|
||||||
tmp_bytes = tmp_io.getvalue()
|
except Exception as exc:
|
||||||
if len(tmp_bytes) <= UPLOAD_MAX_BYTES:
|
logger.warning("[AIImage][UPLOAD] 原格式编码失败,准备转 JPEG: %s", exc)
|
||||||
file_bytes = tmp_bytes
|
|
||||||
|
# 仍超限时,转 JPEG + 渐进压缩;如仍超限则继续降分辨率。
|
||||||
|
if len(best_bytes) > UPLOAD_MAX_BYTES:
|
||||||
|
if best_ext != ".jpg":
|
||||||
|
logger.info("[AIImage][UPLOAD] 原格式仍超限,切换 JPEG 压缩")
|
||||||
|
working_jpg = working.convert("RGB") if working.mode != "RGB" else working
|
||||||
|
while True:
|
||||||
|
compressed = b""
|
||||||
|
for q in (95, 90, 85, 80, 75, 70, 65, 60, 55, 50):
|
||||||
|
tmp = BytesIO()
|
||||||
|
working_jpg.save(tmp, format="JPEG", quality=q, optimize=True)
|
||||||
|
data = tmp.getvalue()
|
||||||
|
compressed = data
|
||||||
|
if len(data) <= UPLOAD_MAX_BYTES:
|
||||||
break
|
break
|
||||||
quality -= 5
|
best_bytes = compressed
|
||||||
else:
|
best_mime = "image/jpeg"
|
||||||
# 即使 quality=50 还是太大,也用这个版本上传(后端会处理)
|
best_ext = ".jpg"
|
||||||
file_bytes = tmp_bytes
|
if len(best_bytes) <= UPLOAD_MAX_BYTES:
|
||||||
|
break
|
||||||
logger.info("[AIImage][UPLOAD] 缩放完成 %dx%d (%.1fMB)",
|
|
||||||
new_w, new_h, len(file_bytes)/1024/1024)
|
next_w = max(256, int(working_jpg.width * 0.9))
|
||||||
iw, ih = new_w, new_h
|
next_h = max(256, int(working_jpg.height * 0.9))
|
||||||
|
if next_w == working_jpg.width and next_h == working_jpg.height:
|
||||||
|
break
|
||||||
|
if next_w <= 256 or next_h <= 256:
|
||||||
|
break
|
||||||
|
working_jpg = working_jpg.resize((next_w, next_h), Image.LANCZOS)
|
||||||
|
|
||||||
|
if len(best_bytes) > UPLOAD_MAX_BYTES:
|
||||||
|
raise ValueError(
|
||||||
|
f"自动压缩后仍超过 10MB(当前 {len(best_bytes)/1024/1024:.2f}MB),请更换图片"
|
||||||
|
)
|
||||||
|
|
||||||
|
file_bytes = best_bytes
|
||||||
|
mime = best_mime
|
||||||
|
upload_ext = best_ext
|
||||||
|
iw, ih = working.width, working.height
|
||||||
|
logger.info(
|
||||||
|
"[AIImage][UPLOAD] 自动处理完成 %dx%d %.2fMB (%s)",
|
||||||
|
iw,
|
||||||
|
ih,
|
||||||
|
len(file_bytes) / 1024 / 1024,
|
||||||
|
mime,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
with open(file_path, "rb") as f:
|
with open(file_path, "rb") as f:
|
||||||
file_bytes = f.read()
|
file_bytes = f.read()
|
||||||
|
|
||||||
mime = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
|
filename = f"{file_stem}{upload_ext}"
|
||||||
boundary = "----pqAuto" + uuid.uuid4().hex
|
boundary = "----pqAuto" + uuid.uuid4().hex
|
||||||
filename = os.path.basename(file_path)
|
|
||||||
crlf = b"\r\n"
|
crlf = b"\r\n"
|
||||||
body = b"".join([
|
body = b"".join([
|
||||||
b"--", boundary.encode("ascii"), crlf,
|
b"--", boundary.encode("ascii"), crlf,
|
||||||
|
|||||||
@@ -1,199 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import copy
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from app.data_range_converter import convert_pattern_params
|
|
||||||
from app.pq.pq_config import get_pattern
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PatternSession:
|
|
||||||
mode: str
|
|
||||||
test_type: str
|
|
||||||
active_config: object
|
|
||||||
pattern_params: list[list[int]]
|
|
||||||
total_patterns: int
|
|
||||||
display_names: list[str]
|
|
||||||
|
|
||||||
|
|
||||||
class PatternService:
|
|
||||||
def __init__(self, app):
|
|
||||||
self.app = app
|
|
||||||
|
|
||||||
def prepare_session(self, mode, *, test_type=None, log_details=False):
|
|
||||||
test_type = test_type or self.app.config.current_test_type
|
|
||||||
if not self.app.config.set_current_pattern(mode):
|
|
||||||
raise ValueError(f"未知的图案模式: {mode}")
|
|
||||||
|
|
||||||
active_config = self.app.config
|
|
||||||
source_params = self._get_source_pattern_params(mode)
|
|
||||||
|
|
||||||
if test_type == "screen_module":
|
|
||||||
screen_cfg = self.app.config.current_test_types.get("screen_module", {})
|
|
||||||
color_space = (
|
|
||||||
self.app.screen_module_color_space_var.get()
|
|
||||||
if hasattr(self.app, "screen_module_color_space_var")
|
|
||||||
else screen_cfg.get("colorimetry", "sRGB")
|
|
||||||
)
|
|
||||||
data_range = (
|
|
||||||
self.app.screen_module_data_range_var.get()
|
|
||||||
if hasattr(self.app, "screen_module_data_range_var")
|
|
||||||
else screen_cfg.get("data_range", "Full")
|
|
||||||
)
|
|
||||||
bit_depth = (
|
|
||||||
self.app.screen_module_bit_depth_var.get()
|
|
||||||
if hasattr(self.app, "screen_module_bit_depth_var")
|
|
||||||
else f"{int(screen_cfg.get('bpc', 8))}bit"
|
|
||||||
)
|
|
||||||
output_format = (
|
|
||||||
self.app.screen_module_output_format_var.get()
|
|
||||||
if hasattr(self.app, "screen_module_output_format_var")
|
|
||||||
else screen_cfg.get("color_format", "RGB")
|
|
||||||
)
|
|
||||||
|
|
||||||
if log_details:
|
|
||||||
self._log("=" * 50, "separator")
|
|
||||||
self._log("设置屏模组信号格式:", "info")
|
|
||||||
self._log("=" * 50, "separator")
|
|
||||||
for label, value in [
|
|
||||||
("色彩空间", color_space),
|
|
||||||
("色彩格式", output_format),
|
|
||||||
("数据范围", data_range),
|
|
||||||
("编码位深", bit_depth),
|
|
||||||
("Timing", self.app.config.current_test_types[test_type]["timing"]),
|
|
||||||
]:
|
|
||||||
self._log(f" {label}: {value}", "info")
|
|
||||||
self.app.signal_service.apply_config(active_config)
|
|
||||||
success = self.app.signal_service.update_signal_format(
|
|
||||||
color_space=color_space,
|
|
||||||
data_range=data_range,
|
|
||||||
bit_depth=bit_depth,
|
|
||||||
output_format=output_format,
|
|
||||||
)
|
|
||||||
if log_details:
|
|
||||||
self._log(
|
|
||||||
f"屏模组信号格式设置{'成功' if success else '失败'}",
|
|
||||||
"success" if success else "error",
|
|
||||||
)
|
|
||||||
|
|
||||||
elif test_type == "sdr_movie":
|
|
||||||
data_range = self.app.sdr_data_range_var.get()
|
|
||||||
if log_details:
|
|
||||||
self._log("=" * 50, "separator")
|
|
||||||
self._log("设置 SDR 信号格式:", "info")
|
|
||||||
self._log("=" * 50, "separator")
|
|
||||||
for label, value in [
|
|
||||||
("色彩空间", self.app.sdr_color_space_var.get()),
|
|
||||||
("色彩格式", self.app.sdr_output_format_var.get()),
|
|
||||||
("Gamma", self.app.sdr_gamma_type_var.get()),
|
|
||||||
("数据范围", data_range),
|
|
||||||
("编码位深", self.app.sdr_bit_depth_var.get()),
|
|
||||||
]:
|
|
||||||
self._log(f" {label}: {value}", "info")
|
|
||||||
converted_params = convert_pattern_params(
|
|
||||||
source_params, data_range=data_range, verbose=False
|
|
||||||
)
|
|
||||||
active_config = self.app.config.get_temp_config_with_converted_params(
|
|
||||||
mode=mode, converted_params=converted_params
|
|
||||||
)
|
|
||||||
self.app.signal_service.apply_config(active_config)
|
|
||||||
success = self.app.signal_service.update_signal_format(
|
|
||||||
color_space=self.app.sdr_color_space_var.get(),
|
|
||||||
data_range=data_range,
|
|
||||||
bit_depth=self.app.sdr_bit_depth_var.get(),
|
|
||||||
output_format=self.app.sdr_output_format_var.get(),
|
|
||||||
)
|
|
||||||
if log_details:
|
|
||||||
self._log(f"SDR 信号格式设置{'成功' if success else '失败'}", "success" if success else "error")
|
|
||||||
self._log(f"图案参数已设置,共 {len(converted_params)} 个图案", "success")
|
|
||||||
|
|
||||||
elif test_type == "hdr_movie":
|
|
||||||
data_range = self.app.hdr_data_range_var.get()
|
|
||||||
if log_details:
|
|
||||||
self._log("=" * 50, "separator")
|
|
||||||
self._log("设置 HDR 信号格式:", "info")
|
|
||||||
self._log("=" * 50, "separator")
|
|
||||||
for label, value in [
|
|
||||||
("色彩空间", self.app.hdr_color_space_var.get()),
|
|
||||||
("色彩格式", self.app.hdr_output_format_var.get()),
|
|
||||||
("数据范围", data_range),
|
|
||||||
("编码位深", self.app.hdr_bit_depth_var.get()),
|
|
||||||
("MaxCLL", self.app.hdr_maxcll_var.get()),
|
|
||||||
("MaxFALL", self.app.hdr_maxfall_var.get()),
|
|
||||||
]:
|
|
||||||
self._log(f" {label}: {value}", "info")
|
|
||||||
converted_params = convert_pattern_params(
|
|
||||||
source_params, data_range=data_range, verbose=False
|
|
||||||
)
|
|
||||||
active_config = self.app.config.get_temp_config_with_converted_params(
|
|
||||||
mode=mode, converted_params=converted_params
|
|
||||||
)
|
|
||||||
self.app.signal_service.apply_config(active_config)
|
|
||||||
success = self.app.signal_service.update_signal_format(
|
|
||||||
color_space=self.app.hdr_color_space_var.get(),
|
|
||||||
data_range=data_range,
|
|
||||||
bit_depth=self.app.hdr_bit_depth_var.get(),
|
|
||||||
output_format=self.app.hdr_output_format_var.get(),
|
|
||||||
max_cll=self.app.hdr_maxcll_var.get(),
|
|
||||||
max_fall=self.app.hdr_maxfall_var.get(),
|
|
||||||
)
|
|
||||||
if log_details:
|
|
||||||
self._log(f"HDR 信号格式设置{'成功' if success else '失败'}", "success" if success else "error")
|
|
||||||
self._log(f"图案参数已设置,共 {len(converted_params)} 个图案", "success")
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ValueError(f"不支持的测试类型: {test_type}")
|
|
||||||
|
|
||||||
pattern_params = copy.deepcopy(active_config.current_pattern["pattern_params"])
|
|
||||||
return PatternSession(
|
|
||||||
mode=mode,
|
|
||||||
test_type=test_type,
|
|
||||||
active_config=active_config,
|
|
||||||
pattern_params=pattern_params,
|
|
||||||
total_patterns=len(pattern_params),
|
|
||||||
display_names=self._get_display_names(mode, len(pattern_params)),
|
|
||||||
)
|
|
||||||
|
|
||||||
def send_session_pattern(self, session, index):
|
|
||||||
if index < 0 or index >= session.total_patterns:
|
|
||||||
raise IndexError(f"pattern 索引越界: {index}")
|
|
||||||
|
|
||||||
pattern_param = session.pattern_params[index]
|
|
||||||
if not self.app.signal_service.send_pattern_params(pattern_param):
|
|
||||||
raise RuntimeError(f"发送 pattern 失败: {index}")
|
|
||||||
return pattern_param
|
|
||||||
|
|
||||||
def send_rgb(self, rgb, *, session=None, test_type=None):
|
|
||||||
active_session = session or self.prepare_session(
|
|
||||||
"rgb",
|
|
||||||
test_type=test_type,
|
|
||||||
log_details=False,
|
|
||||||
)
|
|
||||||
converted_rgb = self._convert_rgb_for_test_type(rgb, active_session.test_type)
|
|
||||||
self.app.signal_service.send_solid_rgb(converted_rgb)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _get_source_pattern_params(self, mode):
|
|
||||||
return copy.deepcopy(get_pattern(mode)["pattern_params"])
|
|
||||||
|
|
||||||
def _get_display_names(self, mode, total_patterns):
|
|
||||||
if mode == "accuracy":
|
|
||||||
return self.app.config.get_accuracy_color_names()
|
|
||||||
if mode == "custom" and hasattr(self.app.config, "get_temp_pattern_names"):
|
|
||||||
return self.app.config.get_temp_pattern_names()
|
|
||||||
return [f"P {index + 1}" for index in range(total_patterns)]
|
|
||||||
|
|
||||||
def _convert_rgb_for_test_type(self, rgb, test_type):
|
|
||||||
if test_type == "sdr_movie":
|
|
||||||
data_range = self.app.sdr_data_range_var.get()
|
|
||||||
elif test_type == "hdr_movie":
|
|
||||||
data_range = self.app.hdr_data_range_var.get()
|
|
||||||
else:
|
|
||||||
data_range = "Full"
|
|
||||||
|
|
||||||
return convert_pattern_params([list(rgb)], data_range=data_range, verbose=False)[0]
|
|
||||||
|
|
||||||
def _log(self, message, level):
|
|
||||||
if hasattr(self.app, "log_gui"):
|
|
||||||
self.app.log_gui.log(message, level=level)
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
"""UCD 信号 / 图案应用服务层。
|
|
||||||
|
|
||||||
服务层是 GUI ↔ Driver 的唯一通道,负责:
|
|
||||||
- 将 UI 字符串("BT.709"、"10bit"、"YCbCr 4:4:4" 等)翻译成 :class:`SignalFormat`;
|
|
||||||
- 将各 panel 的 timing 字符串翻译成 :class:`TimingSpec`;
|
|
||||||
- 协调 :meth:`IUcdDevice.configure` / ``set_pattern`` / ``apply`` 的调用顺序;
|
|
||||||
- 通过 :class:`EventBus` 让 GUI 订阅状态变化,而非主动轮询。
|
|
||||||
|
|
||||||
本层不直接 import UniTAP,也不读取 :mod:`tkinter` 变量;
|
|
||||||
所有输入都是显式参数,便于单测。
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from app.ucd_domain import (
|
|
||||||
Colorimetry,
|
|
||||||
DynamicRange,
|
|
||||||
EventBus,
|
|
||||||
PatternKind,
|
|
||||||
PatternSpec,
|
|
||||||
SignalFormat,
|
|
||||||
TimingSpec,
|
|
||||||
UcdError,
|
|
||||||
bit_depth_str_to_bpc,
|
|
||||||
color_space_to_colorimetry,
|
|
||||||
data_range_to_dynamic_range,
|
|
||||||
output_format_to_color_format,
|
|
||||||
parse_timing_str,
|
|
||||||
)
|
|
||||||
from drivers.ucd_driver import IUcdDevice
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
# ─── 视图字符串 → 值对象 转换工具 ────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
def build_signal_format(
|
|
||||||
*,
|
|
||||||
color_space: str,
|
|
||||||
output_format: str,
|
|
||||||
bit_depth: str,
|
|
||||||
data_range: str = "Full",
|
|
||||||
) -> SignalFormat:
|
|
||||||
"""根据下拉框字符串组装 :class:`SignalFormat`。
|
|
||||||
|
|
||||||
各参数解析失败抛 :class:`UcdConfigError`。
|
|
||||||
"""
|
|
||||||
return SignalFormat(
|
|
||||||
color_format=output_format_to_color_format(output_format),
|
|
||||||
colorimetry=color_space_to_colorimetry(color_space),
|
|
||||||
bpc=bit_depth_str_to_bpc(bit_depth),
|
|
||||||
dynamic_range=data_range_to_dynamic_range(data_range),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_timing(timing_str: str) -> TimingSpec:
|
|
||||||
"""``"DMT 3840x2160@60Hz"`` → :class:`TimingSpec`。"""
|
|
||||||
return parse_timing_str(timing_str)
|
|
||||||
|
|
||||||
|
|
||||||
def solid_rgb_pattern(rgb: tuple[int, int, int] | list[int]) -> PatternSpec:
|
|
||||||
r, g, b = rgb[0], rgb[1], rgb[2]
|
|
||||||
return PatternSpec(kind=PatternKind.SOLID, solid_rgb=(int(r), int(g), int(b)))
|
|
||||||
|
|
||||||
|
|
||||||
def image_pattern(path: str) -> PatternSpec:
|
|
||||||
return PatternSpec(kind=PatternKind.IMAGE, image_path=path)
|
|
||||||
|
|
||||||
|
|
||||||
# ─── 服务 ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
class SignalService:
|
|
||||||
"""协调 SignalFormat / Timing / Pattern 的写入与提交。
|
|
||||||
|
|
||||||
使用线程锁串行化所有对外的 ``apply_*`` 调用,避免多个测试线程
|
|
||||||
同时操作 UCD 造成 SDK 状态错乱。
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, device: IUcdDevice, bus: EventBus):
|
|
||||||
self._dev = device
|
|
||||||
self._bus = bus
|
|
||||||
self._lock = threading.RLock()
|
|
||||||
|
|
||||||
# -- 高层接口 ------------------------------------------------
|
|
||||||
|
|
||||||
def apply(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
signal: SignalFormat,
|
|
||||||
timing: TimingSpec,
|
|
||||||
pattern: PatternSpec,
|
|
||||||
) -> bool:
|
|
||||||
"""一次性提交信号格式 + timing + 图案。
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
``format_changed``——本次相对上一次 :meth:`apply` 是否变化。
|
|
||||||
"""
|
|
||||||
with self._lock:
|
|
||||||
log.info(
|
|
||||||
"SignalService.apply signal=%s timing=%s pattern=%s",
|
|
||||||
signal,
|
|
||||||
timing,
|
|
||||||
pattern.kind.value,
|
|
||||||
)
|
|
||||||
changed = self._dev.configure(signal, timing)
|
|
||||||
self._dev.set_pattern(pattern)
|
|
||||||
self._dev.apply()
|
|
||||||
return changed
|
|
||||||
|
|
||||||
def send_pattern(self, pattern: PatternSpec) -> None:
|
|
||||||
"""在已 configure 的信号上仅更新图案后 apply。"""
|
|
||||||
with self._lock:
|
|
||||||
log.info("SignalService.send_pattern pattern=%s", pattern.kind.value)
|
|
||||||
self._dev.set_pattern(pattern)
|
|
||||||
self._dev.apply()
|
|
||||||
|
|
||||||
def send_solid_rgb(self, rgb: tuple[int, int, int] | list[int]) -> None:
|
|
||||||
self.send_pattern(solid_rgb_pattern(rgb))
|
|
||||||
|
|
||||||
def send_image(self, path: str) -> None:
|
|
||||||
self.send_pattern(image_pattern(path))
|
|
||||||
|
|
||||||
# -- 过渡期 API(Phase 2)-----------------------------------
|
|
||||||
# 现有 GUI 回调以"仅更新信号格式、不切换图案"的方式调用
|
|
||||||
# ``ucd.apply_signal_format(color_space=..., color_format=..., bit_depth=...)``。
|
|
||||||
# 新代码统一通过本方法走 SignalService;内部仍委托给底层
|
|
||||||
# controller 的同名旧接口,迁移完成后将替换为纯净实现。
|
|
||||||
|
|
||||||
def update_signal_format(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
color_space: str,
|
|
||||||
output_format: str,
|
|
||||||
bit_depth: str,
|
|
||||||
data_range: str = "Full",
|
|
||||||
max_cll: int | None = None,
|
|
||||||
max_fall: int | None = None,
|
|
||||||
) -> bool:
|
|
||||||
"""仅将信号格式 stage 到 SDK(沿用上一次的 timing),不切换图案。
|
|
||||||
|
|
||||||
UI 字符串先经域层解析做参数校验;解析失败抛 :class:`UcdConfigError`。
|
|
||||||
"""
|
|
||||||
# 解析仅做校验;当前实现走 raw controller 的旧 API
|
|
||||||
_ = build_signal_format(
|
|
||||||
color_space=color_space,
|
|
||||||
output_format=output_format,
|
|
||||||
bit_depth=bit_depth,
|
|
||||||
data_range=data_range,
|
|
||||||
)
|
|
||||||
ctrl = getattr(self._dev, "raw_controller", None)
|
|
||||||
if ctrl is None:
|
|
||||||
raise UcdError("update_signal_format 暂仅支持 UCD323Device")
|
|
||||||
with self._lock:
|
|
||||||
return bool(
|
|
||||||
ctrl.apply_signal_format(
|
|
||||||
color_space=color_space,
|
|
||||||
color_format=output_format,
|
|
||||||
bit_depth=bit_depth,
|
|
||||||
data_range=data_range,
|
|
||||||
max_cll=max_cll,
|
|
||||||
max_fall=max_fall,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# -- 透传给上层的查询 ---------------------------------------
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device(self) -> IUcdDevice:
|
|
||||||
return self._dev
|
|
||||||
|
|
||||||
def current_resolution(self) -> tuple[int, int]:
|
|
||||||
return self._dev.current_resolution()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_connected(self) -> bool:
|
|
||||||
"""UCD 设备是否已打开。供 GUI 做前置校验。"""
|
|
||||||
ctrl = getattr(self._dev, "raw_controller", None)
|
|
||||||
return bool(ctrl and getattr(ctrl, "status", False))
|
|
||||||
|
|
||||||
# -- 过渡期 API(Phase 5):config 驱动的写入 -----------------
|
|
||||||
# 现有 GUI / Service 通过 ``PQConfig`` 对象描述当次测试参数,
|
|
||||||
# 由 :class:`UCDController.set_ucd_params` 翻译为色彩/Timing/Pattern。
|
|
||||||
# 在配置层重构落地前,这两个方法作为 SignalService 的统一入口,
|
|
||||||
# 让上层不再直接接触 ``self.app.ucd``。
|
|
||||||
|
|
||||||
def apply_config(self, config) -> bool:
|
|
||||||
"""按 :class:`PQConfig` 写入色彩 / Timing / 当前 Pattern(不 apply 输出)。"""
|
|
||||||
ctrl = getattr(self._dev, "raw_controller", None)
|
|
||||||
if ctrl is None:
|
|
||||||
raise UcdError("apply_config 暂仅支持 UCD323Device")
|
|
||||||
with self._lock:
|
|
||||||
return bool(ctrl.set_ucd_params(config))
|
|
||||||
|
|
||||||
def send_pattern_params(self, params) -> bool:
|
|
||||||
"""以 ``params`` 更新当前 pattern 的参数并 apply。"""
|
|
||||||
ctrl = getattr(self._dev, "raw_controller", None)
|
|
||||||
if ctrl is None:
|
|
||||||
raise UcdError("send_pattern_params 暂仅支持 UCD323Device")
|
|
||||||
with self._lock:
|
|
||||||
return bool(ctrl.send_current_pattern_params(params))
|
|
||||||
|
|
||||||
def apply_and_run(self, config, pattern_params) -> bool:
|
|
||||||
"""``set_ucd_params`` + ``set_pattern`` + ``run`` 的复合操作。
|
|
||||||
|
|
||||||
服务于 custom_template_panel 单步流程。
|
|
||||||
"""
|
|
||||||
ctrl = getattr(self._dev, "raw_controller", None)
|
|
||||||
if ctrl is None:
|
|
||||||
raise UcdError("apply_and_run 暂仅支持 UCD323Device")
|
|
||||||
with self._lock:
|
|
||||||
if not ctrl.set_ucd_params(config):
|
|
||||||
return False
|
|
||||||
if not ctrl.set_pattern(ctrl.current_pattern, pattern_params):
|
|
||||||
return False
|
|
||||||
return bool(ctrl.run())
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"SignalService",
|
|
||||||
"build_signal_format",
|
|
||||||
"build_timing",
|
|
||||||
"solid_rgb_pattern",
|
|
||||||
"image_pattern",
|
|
||||||
# 重导出常用域类型方便上层 import 一次到位
|
|
||||||
"SignalFormat",
|
|
||||||
"TimingSpec",
|
|
||||||
"PatternSpec",
|
|
||||||
"PatternKind",
|
|
||||||
"Colorimetry",
|
|
||||||
"DynamicRange",
|
|
||||||
"UcdError",
|
|
||||||
]
|
|
||||||
@@ -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
|
||||||
@@ -152,38 +158,117 @@ def _ensure_checkerboard_image(width, height, grid_size, center_white):
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
def _build_ld_result_row(test_item, pattern_label, value, x="--", y="--"):
|
def _ld_ucd_params_signature(self: "PQAutomationApp") -> tuple:
|
||||||
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
"""Local Dimming 发图前 UCD 参数签名,用于跳过未变化的重复配置。"""
|
||||||
if isinstance(value, (int, float, np.floating)):
|
test_type = getattr(self.config, "current_test_type", "screen_module")
|
||||||
display_value = f"{float(value):.4f}"
|
cfg = self.config.current_test_types.get(test_type, {})
|
||||||
|
timing = cfg.get("timing", "")
|
||||||
|
|
||||||
|
if test_type == "screen_module":
|
||||||
|
color_space = (
|
||||||
|
self.screen_module_color_space_var.get()
|
||||||
|
if hasattr(self, "screen_module_color_space_var")
|
||||||
|
else cfg.get("colorimetry", "sRGB")
|
||||||
|
)
|
||||||
|
data_range = (
|
||||||
|
self.screen_module_data_range_var.get()
|
||||||
|
if hasattr(self, "screen_module_data_range_var")
|
||||||
|
else cfg.get("data_range", "Full")
|
||||||
|
)
|
||||||
|
bit_depth = (
|
||||||
|
self.screen_module_bit_depth_var.get()
|
||||||
|
if hasattr(self, "screen_module_bit_depth_var")
|
||||||
|
else f"{int(cfg.get('bpc', 8))}bit"
|
||||||
|
)
|
||||||
|
output_format = (
|
||||||
|
self.screen_module_output_format_var.get()
|
||||||
|
if hasattr(self, "screen_module_output_format_var")
|
||||||
|
else cfg.get("color_format", "RGB")
|
||||||
|
)
|
||||||
|
elif test_type == "sdr_movie":
|
||||||
|
color_space = (
|
||||||
|
self.sdr_color_space_var.get()
|
||||||
|
if hasattr(self, "sdr_color_space_var")
|
||||||
|
else cfg.get("colorimetry", "sRGB")
|
||||||
|
)
|
||||||
|
data_range = (
|
||||||
|
self.sdr_data_range_var.get()
|
||||||
|
if hasattr(self, "sdr_data_range_var")
|
||||||
|
else cfg.get("data_range", "Full")
|
||||||
|
)
|
||||||
|
bit_depth = (
|
||||||
|
self.sdr_bit_depth_var.get()
|
||||||
|
if hasattr(self, "sdr_bit_depth_var")
|
||||||
|
else f"{int(cfg.get('bpc', 8))}bit"
|
||||||
|
)
|
||||||
|
output_format = (
|
||||||
|
self.sdr_output_format_var.get()
|
||||||
|
if hasattr(self, "sdr_output_format_var")
|
||||||
|
else cfg.get("color_format", "RGB")
|
||||||
|
)
|
||||||
|
elif test_type == "hdr_movie":
|
||||||
|
color_space = (
|
||||||
|
self.hdr_color_space_var.get()
|
||||||
|
if hasattr(self, "hdr_color_space_var")
|
||||||
|
else cfg.get("colorimetry", "sRGB")
|
||||||
|
)
|
||||||
|
data_range = (
|
||||||
|
self.hdr_data_range_var.get()
|
||||||
|
if hasattr(self, "hdr_data_range_var")
|
||||||
|
else cfg.get("data_range", "Full")
|
||||||
|
)
|
||||||
|
bit_depth = (
|
||||||
|
self.hdr_bit_depth_var.get()
|
||||||
|
if hasattr(self, "hdr_bit_depth_var")
|
||||||
|
else f"{int(cfg.get('bpc', 8))}bit"
|
||||||
|
)
|
||||||
|
output_format = (
|
||||||
|
self.hdr_output_format_var.get()
|
||||||
|
if hasattr(self, "hdr_output_format_var")
|
||||||
|
else cfg.get("color_format", "RGB")
|
||||||
|
)
|
||||||
|
max_cll = self.hdr_maxcll_var.get() if hasattr(self, "hdr_maxcll_var") else None
|
||||||
|
max_fall = self.hdr_maxfall_var.get() if hasattr(self, "hdr_maxfall_var") else None
|
||||||
|
return (test_type, timing, color_space, data_range, bit_depth, output_format, max_cll, max_fall)
|
||||||
|
elif test_type == "local_dimming":
|
||||||
|
color_space = (
|
||||||
|
self.local_dimming_color_space_var.get()
|
||||||
|
if hasattr(self, "local_dimming_color_space_var")
|
||||||
|
else cfg.get("colorimetry", "sRGB")
|
||||||
|
)
|
||||||
|
data_range = (
|
||||||
|
self.local_dimming_data_range_var.get()
|
||||||
|
if hasattr(self, "local_dimming_data_range_var")
|
||||||
|
else cfg.get("data_range", "Full")
|
||||||
|
)
|
||||||
|
bit_depth = (
|
||||||
|
self.local_dimming_bit_depth_var.get()
|
||||||
|
if hasattr(self, "local_dimming_bit_depth_var")
|
||||||
|
else f"{int(cfg.get('bpc', 8))}bit"
|
||||||
|
)
|
||||||
|
output_format = (
|
||||||
|
self.local_dimming_output_format_var.get()
|
||||||
|
if hasattr(self, "local_dimming_output_format_var")
|
||||||
|
else cfg.get("color_format", "RGB")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
display_value = str(value)
|
return (test_type,)
|
||||||
|
|
||||||
return {
|
return (test_type, timing, color_space, data_range, bit_depth, output_format)
|
||||||
"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):
|
def invalidate_ld_ucd_params_cache(self: "PQAutomationApp") -> None:
|
||||||
"""读取一次 CA410 数据并包装为表格行。"""
|
"""信号格式或分辨率变更后,强制下次发图重新写入 UCD 参数。"""
|
||||||
x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay()
|
self._last_ld_ucd_signature = None
|
||||||
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")
|
signature = _ld_ucd_params_signature(self)
|
||||||
|
if getattr(self, "_last_ld_ucd_signature", None) == signature:
|
||||||
|
return True
|
||||||
|
|
||||||
|
test_type = signature[0]
|
||||||
cfg = self.config.current_test_types.get(test_type, {})
|
cfg = self.config.current_test_types.get(test_type, {})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -303,237 +388,452 @@ def _apply_ld_ucd_params(self: "PQAutomationApp") -> bool:
|
|||||||
self._dispatch_ui(self.log_gui.log, "Local Dimming UCD 参数设置失败", "error")
|
self._dispatch_ui(self.log_gui.log, "Local Dimming UCD 参数设置失败", "error")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
self._last_ld_ucd_signature = signature
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._dispatch_ui(self.log_gui.log, f"Local Dimming UCD 参数设置异常: {e}", "error")
|
self._dispatch_ui(self.log_gui.log, f"Local Dimming UCD 参数设置异常: {e}", "error")
|
||||||
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,11 +845,9 @@ 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.ca.readAllDisplay()
|
x, y, lv, _X, _Y, _Z = self.read_ca_xyLv()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._dispatch_ui(self.log_gui.log, f"采集异常: {str(e)}")
|
self._dispatch_ui(self.log_gui.log, f"采集异常: {str(e)}")
|
||||||
return
|
return
|
||||||
@@ -562,7 +860,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 +883,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 +918,112 @@ 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
|
||||||
|
invalidate_ld_ucd_params_cache = invalidate_ld_ucd_params_cache
|
||||||
|
|
||||||
|
_insert_ld_tree_item = _insert_ld_tree_item
|
||||||
|
|||||||
28
app/ucd/__init__.py
Normal file
28
app/ucd/__init__.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""UCD 信号发生器 — domain / enum / device / service。
|
||||||
|
|
||||||
|
GUI 与测试代码通常只需::
|
||||||
|
|
||||||
|
from app.ucd import SignalService, UCD323Device, EventBus
|
||||||
|
"""
|
||||||
|
from app.ucd.domain import * # noqa: F403
|
||||||
|
from app.ucd.enum import UCDEnum
|
||||||
|
from app.ucd.device import (
|
||||||
|
DeviceInfo,
|
||||||
|
IUcdDevice,
|
||||||
|
UCD323Device,
|
||||||
|
list_devices,
|
||||||
|
)
|
||||||
|
from app.ucd.service import PatternService, PatternSession, SignalService
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"SignalService",
|
||||||
|
"PatternService",
|
||||||
|
"PatternSession",
|
||||||
|
"UCD323Device",
|
||||||
|
"IUcdDevice",
|
||||||
|
"DeviceInfo",
|
||||||
|
"UCDEnum",
|
||||||
|
"EventBus",
|
||||||
|
"ConnectionChanged",
|
||||||
|
"DeviceKind",
|
||||||
|
]
|
||||||
1267
app/ucd/device.py
Normal file
1267
app/ucd/device.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,4 @@
|
|||||||
"""UCD 控制 Domain 层。
|
"""UCD 域层:类型、状态机、事件、字符串与 PQConfig 映射。"""
|
||||||
|
|
||||||
纯数据 + 纯函数:枚举、值对象、状态机、错误体系、事件总线、
|
|
||||||
业务字符串解析与映射。本模块**不**依赖 UniTAP / 任何硬件;
|
|
||||||
可用纯单测覆盖。
|
|
||||||
|
|
||||||
文件分区:
|
|
||||||
§1 枚举与值对象
|
|
||||||
§2 状态机
|
|
||||||
§3 错误体系
|
|
||||||
§4 事件总线
|
|
||||||
§5 业务字符串解析 / 映射
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -25,6 +12,14 @@ log = logging.getLogger(__name__)
|
|||||||
# ─── §1 枚举与值对象 ──────────────────────────────────────────────
|
# ─── §1 枚举与值对象 ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceKind(str, Enum):
|
||||||
|
"""连接状态事件所指的设备类型。"""
|
||||||
|
|
||||||
|
UCD = "ucd"
|
||||||
|
CA = "ca"
|
||||||
|
|
||||||
|
|
||||||
class Interface(str, Enum):
|
class Interface(str, Enum):
|
||||||
"""UCD 物理输出接口。"""
|
"""UCD 物理输出接口。"""
|
||||||
|
|
||||||
@@ -192,6 +187,7 @@ class UcdEvent:
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ConnectionChanged(UcdEvent):
|
class ConnectionChanged(UcdEvent):
|
||||||
|
device: DeviceKind
|
||||||
connected: bool
|
connected: bool
|
||||||
serial: str | None = None
|
serial: str | None = None
|
||||||
|
|
||||||
@@ -353,6 +349,7 @@ def parse_timing_str(timing_str: str) -> TimingSpec:
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# §1
|
# §1
|
||||||
|
"DeviceKind",
|
||||||
"Interface",
|
"Interface",
|
||||||
"ColorFormat",
|
"ColorFormat",
|
||||||
"Colorimetry",
|
"Colorimetry",
|
||||||
@@ -386,3 +383,104 @@ __all__ = [
|
|||||||
"is_ycbcr",
|
"is_ycbcr",
|
||||||
"parse_timing_str",
|
"parse_timing_str",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# --- PQConfig / pattern 映射 ---
|
||||||
|
|
||||||
|
# PQ pattern_mode 字符串 → PatternKind(大小写不敏感)
|
||||||
|
_PQ_PATTERN_MODE_TO_KIND: dict[str, PatternKind] = {
|
||||||
|
"disabled": PatternKind.DISABLED,
|
||||||
|
"solidcolor": PatternKind.SOLID,
|
||||||
|
"solidwhite": PatternKind.SOLID_WHITE,
|
||||||
|
"solidred": PatternKind.SOLID_RED,
|
||||||
|
"solidgreen": PatternKind.SOLID_GREEN,
|
||||||
|
"solidblue": PatternKind.SOLID_BLUE,
|
||||||
|
"colorbars": PatternKind.COLOR_BARS,
|
||||||
|
"chessboard": PatternKind.CHESSBOARD,
|
||||||
|
"whitevstrips": PatternKind.WHITE_VSTRIPS,
|
||||||
|
"gradientrgbstripes": PatternKind.GRADIENT_RGB_STRIPES,
|
||||||
|
"colorramp": PatternKind.COLOR_RAMP,
|
||||||
|
"coloursquares": PatternKind.COLOR_SQUARES,
|
||||||
|
"motionpattern": PatternKind.MOTION,
|
||||||
|
"squarewindow": PatternKind.SQUARE_WINDOW,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def pattern_mode_to_kind(pattern_mode: str) -> PatternKind:
|
||||||
|
key = (pattern_mode or "solidcolor").strip().lower()
|
||||||
|
kind = _PQ_PATTERN_MODE_TO_KIND.get(key)
|
||||||
|
if kind is None:
|
||||||
|
raise UcdConfigError(f"不支持的 pattern_mode: {pattern_mode!r}")
|
||||||
|
return kind
|
||||||
|
|
||||||
|
|
||||||
|
def build_profile_from_config(config, test_type: str | None = None):
|
||||||
|
"""从 PQConfig 当前 test_type 条目构建 SignalFormat + TimingSpec。"""
|
||||||
|
test_type = test_type or config.current_test_type
|
||||||
|
profile = config.current_test_types[test_type]
|
||||||
|
signal = build_signal_format_from_profile(
|
||||||
|
color_space=profile["colorimetry"],
|
||||||
|
color_format=profile["color_format"],
|
||||||
|
bpc=int(profile["bpc"]),
|
||||||
|
data_range=profile.get("data_range", "Full"),
|
||||||
|
)
|
||||||
|
timing = build_timing(profile["timing"])
|
||||||
|
return signal, timing
|
||||||
|
|
||||||
|
|
||||||
|
def build_pattern_spec(config, params: list[int] | None = None) -> PatternSpec:
|
||||||
|
"""将 PQConfig 当前 pattern 与一组参数转为 :class:`PatternSpec`。"""
|
||||||
|
pattern_mode = config.current_pattern["pattern_mode"]
|
||||||
|
kind = pattern_mode_to_kind(pattern_mode)
|
||||||
|
if params is None:
|
||||||
|
params = config.current_pattern.get("pattern_params", [[]])[0]
|
||||||
|
if kind is PatternKind.SOLID and params and len(params) >= 3:
|
||||||
|
return PatternSpec(
|
||||||
|
kind=kind,
|
||||||
|
solid_rgb=(int(params[0]), int(params[1]), int(params[2])),
|
||||||
|
)
|
||||||
|
if params:
|
||||||
|
return PatternSpec(kind=kind, extras=tuple(int(v) for v in params))
|
||||||
|
return PatternSpec(kind=kind)
|
||||||
|
|
||||||
|
def build_signal_format(
|
||||||
|
*,
|
||||||
|
color_space: str,
|
||||||
|
output_format: str,
|
||||||
|
bit_depth: str,
|
||||||
|
data_range: str = "Full",
|
||||||
|
) -> SignalFormat:
|
||||||
|
return SignalFormat(
|
||||||
|
color_format=output_format_to_color_format(output_format),
|
||||||
|
colorimetry=color_space_to_colorimetry(color_space),
|
||||||
|
bpc=bit_depth_str_to_bpc(bit_depth),
|
||||||
|
dynamic_range=data_range_to_dynamic_range(data_range),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_signal_format_from_profile(
|
||||||
|
*,
|
||||||
|
color_space: str,
|
||||||
|
color_format: str,
|
||||||
|
bpc: int,
|
||||||
|
data_range: str = "Full",
|
||||||
|
) -> SignalFormat:
|
||||||
|
return build_signal_format(
|
||||||
|
color_space=color_space,
|
||||||
|
output_format=color_format,
|
||||||
|
bit_depth=f"{int(bpc)}bit",
|
||||||
|
data_range=data_range,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_timing(timing_str: str) -> TimingSpec:
|
||||||
|
return parse_timing_str(timing_str)
|
||||||
|
|
||||||
|
|
||||||
|
def solid_rgb_pattern(rgb: tuple[int, int, int] | list[int]) -> PatternSpec:
|
||||||
|
r, g, b = rgb[0], rgb[1], rgb[2]
|
||||||
|
return PatternSpec(kind=PatternKind.SOLID, solid_rgb=(int(r), int(g), int(b)))
|
||||||
|
|
||||||
|
|
||||||
|
def image_pattern(path: str) -> PatternSpec:
|
||||||
|
return PatternSpec(kind=PatternKind.IMAGE, image_path=path)
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"""UCD SDK 枚举与 UI/SDK 字符串映射。"""
|
||||||
|
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
import UniTAP
|
import UniTAP
|
||||||
|
|
||||||
@@ -110,7 +112,16 @@ class UCDEnum:
|
|||||||
}
|
}
|
||||||
if not colorimetry_str:
|
if not colorimetry_str:
|
||||||
return None
|
return None
|
||||||
return colorimetry_map.get(colorimetry_str.lower(), None)
|
# Normalize: strip hyphens, spaces, dots, underscores so that
|
||||||
|
# "DCI-P3" → "dcip3", "BT.709" → "bt709", "BT.2020 YCbCr" → "bt2020ycbcr"
|
||||||
|
normalized = (
|
||||||
|
colorimetry_str.lower()
|
||||||
|
.replace("-", "")
|
||||||
|
.replace(" ", "")
|
||||||
|
.replace(".", "")
|
||||||
|
.replace("_", "")
|
||||||
|
)
|
||||||
|
return colorimetry_map.get(normalized, colorimetry_map.get(colorimetry_str.lower(), None))
|
||||||
|
|
||||||
class VideoPatternInfo:
|
class VideoPatternInfo:
|
||||||
class VideoPattern(IntEnum):
|
class VideoPattern(IntEnum):
|
||||||
@@ -611,3 +622,4 @@ class UCDEnum:
|
|||||||
"DSC": "dsc",
|
"DSC": "dsc",
|
||||||
}
|
}
|
||||||
return fmt_map.get(format_str, "rgb")
|
return fmt_map.get(format_str, "rgb")
|
||||||
|
|
||||||
412
app/ucd/service.py
Normal file
412
app/ucd/service.py
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
"""UCD 服务层:SignalService(硬件编排)+ PatternService(测试发图)。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from app.ucd.domain import (
|
||||||
|
EventBus,
|
||||||
|
PatternSpec,
|
||||||
|
SignalFormat,
|
||||||
|
TimingSpec,
|
||||||
|
UcdState,
|
||||||
|
image_pattern,
|
||||||
|
solid_rgb_pattern,
|
||||||
|
)
|
||||||
|
from app.ucd.device import IUcdDevice
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── SignalService ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class SignalService:
|
||||||
|
"""协调 SignalFormat / Timing / Pattern 的写入与提交。"""
|
||||||
|
|
||||||
|
def __init__(self, device: IUcdDevice, bus: EventBus):
|
||||||
|
self._dev = device
|
||||||
|
self._bus = bus
|
||||||
|
|
||||||
|
# -- 高层接口 ------------------------------------------------
|
||||||
|
|
||||||
|
def apply(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
signal: SignalFormat,
|
||||||
|
timing: TimingSpec,
|
||||||
|
pattern: PatternSpec,
|
||||||
|
) -> bool:
|
||||||
|
"""一次性提交信号格式 + timing + 图案。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
``format_changed``——本次相对上一次 :meth:`apply` 是否变化。
|
||||||
|
"""
|
||||||
|
log.info(
|
||||||
|
"SignalService.apply signal=%s timing=%s pattern=%s",
|
||||||
|
signal,
|
||||||
|
timing,
|
||||||
|
pattern.kind.value,
|
||||||
|
)
|
||||||
|
changed = self._dev.configure(signal, timing)
|
||||||
|
self._dev.set_pattern(pattern)
|
||||||
|
self._dev.apply()
|
||||||
|
return changed
|
||||||
|
|
||||||
|
def send_pattern(self, pattern: PatternSpec) -> None:
|
||||||
|
"""在已 configure 的信号上仅更新图案后 apply。"""
|
||||||
|
log.info("SignalService.send_pattern pattern=%s", pattern.kind.value)
|
||||||
|
self._dev.set_pattern(pattern)
|
||||||
|
self._dev.apply()
|
||||||
|
|
||||||
|
def send_solid_rgb(self, rgb: tuple[int, int, int] | list[int]) -> None:
|
||||||
|
self.send_pattern(solid_rgb_pattern(rgb))
|
||||||
|
|
||||||
|
def send_image(self, path: str) -> None:
|
||||||
|
self.send_pattern(image_pattern(path))
|
||||||
|
|
||||||
|
def update_signal_format(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
color_space: str,
|
||||||
|
output_format: str,
|
||||||
|
bit_depth: str,
|
||||||
|
data_range: str = "Full",
|
||||||
|
max_cll: int | None = None,
|
||||||
|
max_fall: int | None = None,
|
||||||
|
) -> bool:
|
||||||
|
"""仅将信号格式提交到 SDK(沿用上一次的 timing),不切换图案。
|
||||||
|
|
||||||
|
UI 字符串先经域层解析做参数校验;解析失败抛 :class:`UcdConfigError`。
|
||||||
|
"""
|
||||||
|
_ = build_signal_format(
|
||||||
|
color_space=color_space,
|
||||||
|
output_format=output_format,
|
||||||
|
bit_depth=bit_depth,
|
||||||
|
data_range=data_range,
|
||||||
|
)
|
||||||
|
return self._dev.apply_signal_format(
|
||||||
|
color_space=color_space,
|
||||||
|
color_format=output_format,
|
||||||
|
bit_depth=bit_depth,
|
||||||
|
data_range=data_range,
|
||||||
|
max_cll=max_cll,
|
||||||
|
max_fall=max_fall,
|
||||||
|
)
|
||||||
|
|
||||||
|
# -- 透传给上层的查询 ---------------------------------------
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device(self) -> IUcdDevice:
|
||||||
|
return self._dev
|
||||||
|
|
||||||
|
def current_resolution(self) -> tuple[int, int]:
|
||||||
|
return self._dev.current_resolution()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
"""UCD 设备是否已打开。供 GUI 做前置校验。"""
|
||||||
|
return self._dev.state != UcdState.CLOSED
|
||||||
|
|
||||||
|
@property
|
||||||
|
def format_changed(self) -> bool:
|
||||||
|
"""最近一次视频模式提交是否相对上次发生变化。"""
|
||||||
|
return self._dev.format_changed
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_error(self) -> str | None:
|
||||||
|
return self._dev.last_error
|
||||||
|
|
||||||
|
def apply_config(self, config) -> bool:
|
||||||
|
"""按 :class:`PQConfig` 写入色彩 / Timing / 当前 Pattern(不 apply 输出)。"""
|
||||||
|
return bool(self._dev.set_ucd_params(config))
|
||||||
|
|
||||||
|
def send_pattern_params(self, params) -> bool:
|
||||||
|
"""以 ``params`` 更新当前 pattern 的参数并 apply。"""
|
||||||
|
return bool(self._dev.send_current_pattern_params(params))
|
||||||
|
|
||||||
|
def apply_and_run(self, config, pattern_params) -> bool:
|
||||||
|
"""``set_ucd_params`` + ``set_pattern`` + ``run`` 的复合操作。"""
|
||||||
|
return bool(self._dev.apply_config_and_run(config, pattern_params))
|
||||||
|
|
||||||
|
def stage_test_profile(
|
||||||
|
self,
|
||||||
|
config,
|
||||||
|
*,
|
||||||
|
color_space: str,
|
||||||
|
output_format: str,
|
||||||
|
bit_depth: str,
|
||||||
|
data_range: str = "Full",
|
||||||
|
max_cll: int | None = None,
|
||||||
|
max_fall: int | None = None,
|
||||||
|
) -> bool:
|
||||||
|
"""按 PQConfig stage 色彩/Timing/Pattern 类型,并提交 UI 覆盖的信号格式。
|
||||||
|
|
||||||
|
自动化测试在发图前调用;等价于 ``apply_config`` + ``update_signal_format``。
|
||||||
|
"""
|
||||||
|
if not self.apply_config(config):
|
||||||
|
return False
|
||||||
|
return self.update_signal_format(
|
||||||
|
color_space=color_space,
|
||||||
|
output_format=output_format,
|
||||||
|
bit_depth=bit_depth,
|
||||||
|
data_range=data_range,
|
||||||
|
max_cll=max_cll,
|
||||||
|
max_fall=max_fall,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"SignalService",
|
||||||
|
"build_signal_format",
|
||||||
|
"build_signal_format_from_profile",
|
||||||
|
"build_timing",
|
||||||
|
"solid_rgb_pattern",
|
||||||
|
"image_pattern",
|
||||||
|
"SignalFormat",
|
||||||
|
"TimingSpec",
|
||||||
|
"PatternSpec",
|
||||||
|
"PatternKind",
|
||||||
|
"Colorimetry",
|
||||||
|
"DynamicRange",
|
||||||
|
"UcdError",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# --- PatternService ---
|
||||||
|
|
||||||
|
import copy
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from app.data_range_converter import convert_pattern_params
|
||||||
|
from app.pq.pq_config import get_pattern
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PatternSession:
|
||||||
|
mode: str
|
||||||
|
test_type: str
|
||||||
|
active_config: object
|
||||||
|
pattern_params: list[list[int]]
|
||||||
|
total_patterns: int
|
||||||
|
display_names: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
class PatternService:
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
def _build_apply_config_error(self, test_type):
|
||||||
|
timing = self.app.config.current_test_types.get(test_type, {}).get("timing", "-")
|
||||||
|
detail = ""
|
||||||
|
err = self.app.signal_service.last_error
|
||||||
|
if err:
|
||||||
|
detail = f", detail={err}"
|
||||||
|
return f"UCD profile apply_config failed for {test_type}, timing={timing}{detail}"
|
||||||
|
|
||||||
|
def _stage_profile(
|
||||||
|
self,
|
||||||
|
active_config,
|
||||||
|
*,
|
||||||
|
color_space: str,
|
||||||
|
output_format: str,
|
||||||
|
bit_depth: str,
|
||||||
|
data_range: str = "Full",
|
||||||
|
max_cll: int | None = None,
|
||||||
|
max_fall: int | None = None,
|
||||||
|
test_type: str,
|
||||||
|
log_details: bool = False,
|
||||||
|
log_title: str = "",
|
||||||
|
log_fields: list[tuple[str, str]] | None = None,
|
||||||
|
) -> None:
|
||||||
|
if log_details and log_title:
|
||||||
|
self._log("=" * 50, "separator")
|
||||||
|
self._log(log_title, "info")
|
||||||
|
self._log("=" * 50, "separator")
|
||||||
|
for label, value in log_fields or []:
|
||||||
|
self._log(f" {label}: {value}", "info")
|
||||||
|
|
||||||
|
if not self.app.signal_service.stage_test_profile(
|
||||||
|
active_config,
|
||||||
|
color_space=color_space,
|
||||||
|
output_format=output_format,
|
||||||
|
bit_depth=bit_depth,
|
||||||
|
data_range=data_range,
|
||||||
|
max_cll=max_cll,
|
||||||
|
max_fall=max_fall,
|
||||||
|
):
|
||||||
|
raise RuntimeError(self._build_apply_config_error(test_type))
|
||||||
|
|
||||||
|
if log_details:
|
||||||
|
self._log("信号格式设置成功", "success")
|
||||||
|
|
||||||
|
def prepare_session(self, mode, *, test_type=None, log_details=False):
|
||||||
|
test_type = test_type or self.app.config.current_test_type
|
||||||
|
if hasattr(self.app.config, "set_current_test_type"):
|
||||||
|
self.app.config.set_current_test_type(test_type)
|
||||||
|
if not self.app.config.set_current_pattern(mode):
|
||||||
|
raise ValueError(f"未知的图案模式: {mode}")
|
||||||
|
|
||||||
|
active_config = self.app.config
|
||||||
|
source_params = self._get_source_pattern_params(mode)
|
||||||
|
|
||||||
|
if test_type == "screen_module":
|
||||||
|
screen_cfg = self.app.config.current_test_types.get("screen_module", {})
|
||||||
|
color_space = (
|
||||||
|
self.app.screen_module_color_space_var.get()
|
||||||
|
if hasattr(self.app, "screen_module_color_space_var")
|
||||||
|
else screen_cfg.get("colorimetry", "sRGB")
|
||||||
|
)
|
||||||
|
data_range = (
|
||||||
|
self.app.screen_module_data_range_var.get()
|
||||||
|
if hasattr(self.app, "screen_module_data_range_var")
|
||||||
|
else screen_cfg.get("data_range", "Full")
|
||||||
|
)
|
||||||
|
bit_depth = (
|
||||||
|
self.app.screen_module_bit_depth_var.get()
|
||||||
|
if hasattr(self.app, "screen_module_bit_depth_var")
|
||||||
|
else f"{int(screen_cfg.get('bpc', 8))}bit"
|
||||||
|
)
|
||||||
|
output_format = (
|
||||||
|
self.app.screen_module_output_format_var.get()
|
||||||
|
if hasattr(self.app, "screen_module_output_format_var")
|
||||||
|
else screen_cfg.get("color_format", "RGB")
|
||||||
|
)
|
||||||
|
|
||||||
|
self._stage_profile(
|
||||||
|
active_config,
|
||||||
|
color_space=color_space,
|
||||||
|
data_range=data_range,
|
||||||
|
bit_depth=bit_depth,
|
||||||
|
output_format=output_format,
|
||||||
|
test_type=test_type,
|
||||||
|
log_details=log_details,
|
||||||
|
log_title="设置屏模组信号格式:",
|
||||||
|
log_fields=[
|
||||||
|
("色彩空间", color_space),
|
||||||
|
("色彩格式", output_format),
|
||||||
|
("数据范围", data_range),
|
||||||
|
("编码位深", bit_depth),
|
||||||
|
("Timing", self.app.config.current_test_types[test_type]["timing"]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
elif test_type == "sdr_movie":
|
||||||
|
data_range = self.app.sdr_data_range_var.get()
|
||||||
|
converted_params = convert_pattern_params(
|
||||||
|
source_params, data_range=data_range, verbose=False
|
||||||
|
)
|
||||||
|
active_config = self.app.config.get_temp_config_with_converted_params(
|
||||||
|
mode=mode, converted_params=converted_params
|
||||||
|
)
|
||||||
|
if hasattr(active_config, "set_current_test_type"):
|
||||||
|
active_config.set_current_test_type(test_type)
|
||||||
|
|
||||||
|
self._stage_profile(
|
||||||
|
active_config,
|
||||||
|
color_space=self.app.sdr_color_space_var.get(),
|
||||||
|
data_range=data_range,
|
||||||
|
bit_depth=self.app.sdr_bit_depth_var.get(),
|
||||||
|
output_format=self.app.sdr_output_format_var.get(),
|
||||||
|
test_type=test_type,
|
||||||
|
log_details=log_details,
|
||||||
|
log_title="设置 SDR 信号格式:",
|
||||||
|
log_fields=[
|
||||||
|
("色彩空间", self.app.sdr_color_space_var.get()),
|
||||||
|
("色彩格式", self.app.sdr_output_format_var.get()),
|
||||||
|
("Gamma", self.app.sdr_gamma_type_var.get()),
|
||||||
|
("数据范围", data_range),
|
||||||
|
("编码位深", self.app.sdr_bit_depth_var.get()),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if log_details:
|
||||||
|
self._log(f"图案参数已设置,共 {len(converted_params)} 个图案", "success")
|
||||||
|
|
||||||
|
elif test_type == "hdr_movie":
|
||||||
|
data_range = self.app.hdr_data_range_var.get()
|
||||||
|
converted_params = convert_pattern_params(
|
||||||
|
source_params, data_range=data_range, verbose=False
|
||||||
|
)
|
||||||
|
active_config = self.app.config.get_temp_config_with_converted_params(
|
||||||
|
mode=mode, converted_params=converted_params
|
||||||
|
)
|
||||||
|
if hasattr(active_config, "set_current_test_type"):
|
||||||
|
active_config.set_current_test_type(test_type)
|
||||||
|
|
||||||
|
self._stage_profile(
|
||||||
|
active_config,
|
||||||
|
color_space=self.app.hdr_color_space_var.get(),
|
||||||
|
data_range=data_range,
|
||||||
|
bit_depth=self.app.hdr_bit_depth_var.get(),
|
||||||
|
output_format=self.app.hdr_output_format_var.get(),
|
||||||
|
max_cll=self.app.hdr_maxcll_var.get(),
|
||||||
|
max_fall=self.app.hdr_maxfall_var.get(),
|
||||||
|
test_type=test_type,
|
||||||
|
log_details=log_details,
|
||||||
|
log_title="设置 HDR 信号格式:",
|
||||||
|
log_fields=[
|
||||||
|
("色彩空间", self.app.hdr_color_space_var.get()),
|
||||||
|
("色彩格式", self.app.hdr_output_format_var.get()),
|
||||||
|
("数据范围", data_range),
|
||||||
|
("编码位深", self.app.hdr_bit_depth_var.get()),
|
||||||
|
("MaxCLL", self.app.hdr_maxcll_var.get()),
|
||||||
|
("MaxFALL", self.app.hdr_maxfall_var.get()),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if log_details:
|
||||||
|
self._log(f"图案参数已设置,共 {len(converted_params)} 个图案", "success")
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(f"不支持的测试类型: {test_type}")
|
||||||
|
|
||||||
|
pattern_params = copy.deepcopy(active_config.current_pattern["pattern_params"])
|
||||||
|
return PatternSession(
|
||||||
|
mode=mode,
|
||||||
|
test_type=test_type,
|
||||||
|
active_config=active_config,
|
||||||
|
pattern_params=pattern_params,
|
||||||
|
total_patterns=len(pattern_params),
|
||||||
|
display_names=self._get_display_names(mode, len(pattern_params)),
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_session_pattern(self, session, index):
|
||||||
|
if index < 0 or index >= session.total_patterns:
|
||||||
|
raise IndexError(f"pattern 索引越界: {index}")
|
||||||
|
|
||||||
|
pattern_param = session.pattern_params[index]
|
||||||
|
if not self.app.signal_service.send_pattern_params(pattern_param):
|
||||||
|
raise RuntimeError(f"发送 pattern 失败: {index}")
|
||||||
|
return pattern_param
|
||||||
|
|
||||||
|
def send_rgb(self, rgb, *, session=None, test_type=None):
|
||||||
|
active_session = session or self.prepare_session(
|
||||||
|
"rgb",
|
||||||
|
test_type=test_type,
|
||||||
|
log_details=False,
|
||||||
|
)
|
||||||
|
converted_rgb = self._convert_rgb_for_test_type(rgb, active_session.test_type)
|
||||||
|
self.app.signal_service.send_solid_rgb(converted_rgb)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _get_source_pattern_params(self, mode):
|
||||||
|
return copy.deepcopy(get_pattern(mode)["pattern_params"])
|
||||||
|
|
||||||
|
def _get_display_names(self, mode, total_patterns):
|
||||||
|
if mode == "accuracy":
|
||||||
|
return self.app.config.get_accuracy_color_names()
|
||||||
|
if mode == "custom" and hasattr(self.app.config, "get_temp_pattern_names"):
|
||||||
|
return self.app.config.get_temp_pattern_names()
|
||||||
|
return [f"P {index + 1}" for index in range(total_patterns)]
|
||||||
|
|
||||||
|
def _convert_rgb_for_test_type(self, rgb, test_type):
|
||||||
|
if test_type == "sdr_movie":
|
||||||
|
data_range = self.app.sdr_data_range_var.get()
|
||||||
|
elif test_type == "hdr_movie":
|
||||||
|
data_range = self.app.hdr_data_range_var.get()
|
||||||
|
else:
|
||||||
|
data_range = "Full"
|
||||||
|
|
||||||
|
return convert_pattern_params([list(rgb)], data_range=data_range, verbose=False)[0]
|
||||||
|
|
||||||
|
def _log(self, message, level):
|
||||||
|
if hasattr(self.app, "log_gui"):
|
||||||
|
self.app.log_gui.log(message, level=level)
|
||||||
@@ -9,6 +9,7 @@ import ttkbootstrap as ttk
|
|||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||||
from app.views.pq_debug_panel import PQDebugPanel
|
from app.views.pq_debug_panel import PQDebugPanel
|
||||||
|
from app.views.modern_styles import get_theme_palette
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -19,8 +20,7 @@ if TYPE_CHECKING:
|
|||||||
def _result_bg_color() -> str:
|
def _result_bg_color() -> str:
|
||||||
"""根据当前主题返回结果图背景色。"""
|
"""根据当前主题返回结果图背景色。"""
|
||||||
try:
|
try:
|
||||||
from app.views.theme_manager import is_dark
|
return get_theme_palette()["bg"]
|
||||||
return "#1B1F24" if is_dark() else "#FFFFFF"
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return "#FFFFFF"
|
return "#FFFFFF"
|
||||||
|
|
||||||
@@ -55,6 +55,19 @@ def apply_result_chart_theme(self: "PQAutomationApp"):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_axes_theme(ax, palette, *, title=None, xlabel=None, ylabel=None):
|
||||||
|
ax.set_facecolor(palette["card_bg"])
|
||||||
|
for spine in ax.spines.values():
|
||||||
|
spine.set_color(palette["border"])
|
||||||
|
if title is not None:
|
||||||
|
ax.set_title(title, color=palette["fg"])
|
||||||
|
if xlabel is not None:
|
||||||
|
ax.set_xlabel(xlabel, color=palette["fg"])
|
||||||
|
if ylabel is not None:
|
||||||
|
ax.set_ylabel(ylabel, color=palette["fg"])
|
||||||
|
ax.tick_params(axis="both", colors=palette["fg"])
|
||||||
|
|
||||||
|
|
||||||
def init_gamut_chart(self: "PQAutomationApp"):
|
def init_gamut_chart(self: "PQAutomationApp"):
|
||||||
"""初始化色域图表 - 手动设置subplot位置,完全避免重叠"""
|
"""初始化色域图表 - 手动设置subplot位置,完全避免重叠"""
|
||||||
container = ttk.Frame(self.gamut_chart_frame)
|
container = ttk.Frame(self.gamut_chart_frame)
|
||||||
@@ -154,8 +167,10 @@ def init_gamma_chart(self: "PQAutomationApp"):
|
|||||||
"""初始化Gamma曲线图表 - 左侧曲线 + 右侧表格(4列 + 通用说明)"""
|
"""初始化Gamma曲线图表 - 左侧曲线 + 右侧表格(4列 + 通用说明)"""
|
||||||
container = ttk.Frame(self.gamma_chart_frame)
|
container = ttk.Frame(self.gamma_chart_frame)
|
||||||
container.pack(expand=True, fill=tk.BOTH)
|
container.pack(expand=True, fill=tk.BOTH)
|
||||||
|
palette = get_theme_palette()
|
||||||
|
|
||||||
self.gamma_fig = plt.Figure(figsize=(12, 6), dpi=100, constrained_layout=False)
|
self.gamma_fig = plt.Figure(figsize=(12, 6), dpi=100, constrained_layout=False)
|
||||||
|
self.gamma_fig.patch.set_facecolor(palette["bg"])
|
||||||
self.gamma_canvas = FigureCanvasTkAgg(self.gamma_fig, master=container)
|
self.gamma_canvas = FigureCanvasTkAgg(self.gamma_fig, master=container)
|
||||||
|
|
||||||
canvas_widget = self.gamma_canvas.get_tk_widget()
|
canvas_widget = self.gamma_canvas.get_tk_widget()
|
||||||
@@ -163,6 +178,7 @@ def init_gamma_chart(self: "PQAutomationApp"):
|
|||||||
|
|
||||||
# 左侧:Gamma 曲线
|
# 左侧:Gamma 曲线
|
||||||
self.gamma_ax = self.gamma_fig.add_axes([0.08, 0.12, 0.50, 0.78])
|
self.gamma_ax = self.gamma_fig.add_axes([0.08, 0.12, 0.50, 0.78])
|
||||||
|
_apply_axes_theme(self.gamma_ax, palette, xlabel="灰阶 (%)", ylabel="L_bar")
|
||||||
self.gamma_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
self.gamma_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
||||||
self.gamma_ax.set_ylabel("L_bar", fontsize=10)
|
self.gamma_ax.set_ylabel("L_bar", fontsize=10)
|
||||||
self.gamma_ax.set_xlim(0, 105)
|
self.gamma_ax.set_xlim(0, 105)
|
||||||
@@ -182,10 +198,13 @@ def init_gamma_chart(self: "PQAutomationApp"):
|
|||||||
ha="center",
|
ha="center",
|
||||||
va="center",
|
va="center",
|
||||||
fontsize=10,
|
fontsize=10,
|
||||||
color="gray",
|
color=palette["muted_fg"],
|
||||||
transform=self.gamma_ax.transAxes,
|
transform=self.gamma_ax.transAxes,
|
||||||
bbox=dict(
|
bbox=dict(
|
||||||
boxstyle="round,pad=1", facecolor="white", edgecolor="gray", alpha=0.8
|
boxstyle="round,pad=1",
|
||||||
|
facecolor=palette["card_bg"],
|
||||||
|
edgecolor=palette["border"],
|
||||||
|
alpha=0.95,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -223,17 +242,17 @@ def init_gamma_chart(self: "PQAutomationApp"):
|
|||||||
# 表头样式
|
# 表头样式
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
cell = table[(0, i)]
|
cell = table[(0, i)]
|
||||||
cell.set_facecolor("#4472C4")
|
cell.set_facecolor(palette["primary"])
|
||||||
cell.set_text_props(weight="bold", color="white", fontsize=7)
|
cell.set_text_props(weight="bold", color=palette["select_fg"], fontsize=7)
|
||||||
|
|
||||||
# 数据行交替颜色
|
# 数据行交替颜色
|
||||||
for i in range(1, len(table_data)):
|
for i in range(1, len(table_data)):
|
||||||
for j in range(4):
|
for j in range(4):
|
||||||
cell = table[(i, j)]
|
cell = table[(i, j)]
|
||||||
if i % 2 == 0:
|
if i % 2 == 0:
|
||||||
cell.set_facecolor("#E7E6E6")
|
cell.set_facecolor(palette["surface_alt_bg"])
|
||||||
else:
|
else:
|
||||||
cell.set_facecolor("#FFFFFF")
|
cell.set_facecolor(palette["card_bg"])
|
||||||
|
|
||||||
# 底部说明
|
# 底部说明
|
||||||
self.gamma_table_ax.text(
|
self.gamma_table_ax.text(
|
||||||
@@ -246,25 +265,27 @@ def init_gamma_chart(self: "PQAutomationApp"):
|
|||||||
ha="center",
|
ha="center",
|
||||||
va="bottom",
|
va="bottom",
|
||||||
fontsize=7,
|
fontsize=7,
|
||||||
color="gray",
|
color=palette["muted_fg"],
|
||||||
transform=self.gamma_table_ax.transAxes,
|
transform=self.gamma_table_ax.transAxes,
|
||||||
bbox=dict(
|
bbox=dict(
|
||||||
boxstyle="round,pad=0.5",
|
boxstyle="round,pad=0.5",
|
||||||
facecolor="lightyellow",
|
facecolor=palette["surface_alt_bg"],
|
||||||
edgecolor="gray",
|
edgecolor=palette["border"],
|
||||||
alpha=0.8,
|
alpha=0.95,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.gamma_fig.suptitle("Gamma曲线 + 数据表格", fontsize=12, y=0.98)
|
self.gamma_fig.suptitle("Gamma曲线 + 数据表格", fontsize=12, y=0.98, color=palette["fg"])
|
||||||
self.gamma_canvas.draw()
|
self.gamma_canvas.draw()
|
||||||
|
|
||||||
def init_eotf_chart(self: "PQAutomationApp"):
|
def init_eotf_chart(self: "PQAutomationApp"):
|
||||||
"""初始化 EOTF 曲线图表(HDR 专用)- 左侧曲线 + 右侧表格(4列)"""
|
"""初始化 EOTF 曲线图表(HDR 专用)- 左侧曲线 + 右侧表格(4列)"""
|
||||||
container = ttk.Frame(self.eotf_chart_frame)
|
container = ttk.Frame(self.eotf_chart_frame)
|
||||||
container.pack(expand=True, fill=tk.BOTH)
|
container.pack(expand=True, fill=tk.BOTH)
|
||||||
|
palette = get_theme_palette()
|
||||||
|
|
||||||
self.eotf_fig = plt.Figure(figsize=(12, 6), dpi=100, constrained_layout=False)
|
self.eotf_fig = plt.Figure(figsize=(12, 6), dpi=100, constrained_layout=False)
|
||||||
|
self.eotf_fig.patch.set_facecolor(palette["bg"])
|
||||||
self.eotf_canvas = FigureCanvasTkAgg(self.eotf_fig, master=container)
|
self.eotf_canvas = FigureCanvasTkAgg(self.eotf_fig, master=container)
|
||||||
|
|
||||||
canvas_widget = self.eotf_canvas.get_tk_widget()
|
canvas_widget = self.eotf_canvas.get_tk_widget()
|
||||||
@@ -272,6 +293,7 @@ def init_eotf_chart(self: "PQAutomationApp"):
|
|||||||
|
|
||||||
# 左侧:EOTF 曲线
|
# 左侧:EOTF 曲线
|
||||||
self.eotf_ax = self.eotf_fig.add_axes([0.08, 0.12, 0.50, 0.78])
|
self.eotf_ax = self.eotf_fig.add_axes([0.08, 0.12, 0.50, 0.78])
|
||||||
|
_apply_axes_theme(self.eotf_ax, palette, xlabel="灰阶 (%)", ylabel="L_bar (归一化亮度)")
|
||||||
self.eotf_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
self.eotf_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
||||||
self.eotf_ax.set_ylabel("L_bar (归一化亮度)", fontsize=10)
|
self.eotf_ax.set_ylabel("L_bar (归一化亮度)", fontsize=10)
|
||||||
self.eotf_ax.set_xlim(0, 105)
|
self.eotf_ax.set_xlim(0, 105)
|
||||||
@@ -287,10 +309,13 @@ def init_eotf_chart(self: "PQAutomationApp"):
|
|||||||
ha="center",
|
ha="center",
|
||||||
va="center",
|
va="center",
|
||||||
fontsize=11,
|
fontsize=11,
|
||||||
color="gray",
|
color=palette["muted_fg"],
|
||||||
transform=self.eotf_ax.transAxes,
|
transform=self.eotf_ax.transAxes,
|
||||||
bbox=dict(
|
bbox=dict(
|
||||||
boxstyle="round,pad=1", facecolor="white", edgecolor="gray", alpha=0.8
|
boxstyle="round,pad=1",
|
||||||
|
facecolor=palette["card_bg"],
|
||||||
|
edgecolor=palette["border"],
|
||||||
|
alpha=0.95,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -328,17 +353,17 @@ def init_eotf_chart(self: "PQAutomationApp"):
|
|||||||
# 表头样式
|
# 表头样式
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
cell = table[(0, i)]
|
cell = table[(0, i)]
|
||||||
cell.set_facecolor("#4472C4")
|
cell.set_facecolor(palette["primary"])
|
||||||
cell.set_text_props(weight="bold", color="white", fontsize=7)
|
cell.set_text_props(weight="bold", color=palette["select_fg"], fontsize=7)
|
||||||
|
|
||||||
# 数据行交替颜色
|
# 数据行交替颜色
|
||||||
for i in range(1, len(table_data)):
|
for i in range(1, len(table_data)):
|
||||||
for j in range(4):
|
for j in range(4):
|
||||||
cell = table[(i, j)]
|
cell = table[(i, j)]
|
||||||
if i % 2 == 0:
|
if i % 2 == 0:
|
||||||
cell.set_facecolor("#E7E6E6")
|
cell.set_facecolor(palette["surface_alt_bg"])
|
||||||
else:
|
else:
|
||||||
cell.set_facecolor("#FFFFFF")
|
cell.set_facecolor(palette["card_bg"])
|
||||||
|
|
||||||
# 底部说明
|
# 底部说明
|
||||||
self.eotf_table_ax.text(
|
self.eotf_table_ax.text(
|
||||||
@@ -351,25 +376,27 @@ def init_eotf_chart(self: "PQAutomationApp"):
|
|||||||
ha="center",
|
ha="center",
|
||||||
va="bottom",
|
va="bottom",
|
||||||
fontsize=7,
|
fontsize=7,
|
||||||
color="gray",
|
color=palette["muted_fg"],
|
||||||
transform=self.eotf_table_ax.transAxes,
|
transform=self.eotf_table_ax.transAxes,
|
||||||
bbox=dict(
|
bbox=dict(
|
||||||
boxstyle="round,pad=0.5",
|
boxstyle="round,pad=0.5",
|
||||||
facecolor="lightyellow",
|
facecolor=palette["surface_alt_bg"],
|
||||||
edgecolor="gray",
|
edgecolor=palette["border"],
|
||||||
alpha=0.8,
|
alpha=0.95,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.eotf_fig.suptitle("EOTF 曲线 + 数据表格", fontsize=12, y=0.98)
|
self.eotf_fig.suptitle("EOTF 曲线 + 数据表格", fontsize=12, y=0.98, color=palette["fg"])
|
||||||
self.eotf_canvas.draw()
|
self.eotf_canvas.draw()
|
||||||
|
|
||||||
def init_cct_chart(self: "PQAutomationApp"):
|
def init_cct_chart(self: "PQAutomationApp"):
|
||||||
"""初始化色度坐标图表 - 正向横坐标,标题居中最上方"""
|
"""初始化色度坐标图表 - 正向横坐标,标题居中最上方"""
|
||||||
container = ttk.Frame(self.cct_chart_frame)
|
container = ttk.Frame(self.cct_chart_frame)
|
||||||
container.pack(expand=True)
|
container.pack(expand=True)
|
||||||
|
palette = get_theme_palette()
|
||||||
|
|
||||||
self.cct_fig = plt.Figure(figsize=(8, 6), dpi=100, tight_layout=False)
|
self.cct_fig = plt.Figure(figsize=(8, 6), dpi=100, tight_layout=False)
|
||||||
|
self.cct_fig.patch.set_facecolor(palette["bg"])
|
||||||
self.cct_canvas = FigureCanvasTkAgg(self.cct_fig, master=container)
|
self.cct_canvas = FigureCanvasTkAgg(self.cct_fig, master=container)
|
||||||
|
|
||||||
canvas_widget = self.cct_canvas.get_tk_widget()
|
canvas_widget = self.cct_canvas.get_tk_widget()
|
||||||
@@ -378,7 +405,9 @@ def init_cct_chart(self: "PQAutomationApp"):
|
|||||||
canvas_widget.pack_propagate(False)
|
canvas_widget.pack_propagate(False)
|
||||||
|
|
||||||
self.cct_ax1 = self.cct_fig.add_subplot(211)
|
self.cct_ax1 = self.cct_fig.add_subplot(211)
|
||||||
|
self.cct_ax1.set_facecolor(palette["card_bg"])
|
||||||
self.cct_ax2 = self.cct_fig.add_subplot(212)
|
self.cct_ax2 = self.cct_fig.add_subplot(212)
|
||||||
|
self.cct_ax2.set_facecolor(palette["card_bg"])
|
||||||
|
|
||||||
# 上图:x coordinates
|
# 上图:x coordinates
|
||||||
self.cct_ax1.set_xlabel("灰阶 (%)", fontsize=9)
|
self.cct_ax1.set_xlabel("灰阶 (%)", fontsize=9)
|
||||||
@@ -397,7 +426,7 @@ def init_cct_chart(self: "PQAutomationApp"):
|
|||||||
self.cct_ax2.tick_params(labelsize=8)
|
self.cct_ax2.tick_params(labelsize=8)
|
||||||
|
|
||||||
# 调整标题位置:y=0.985(比色域/Gamma略高)
|
# 调整标题位置:y=0.985(比色域/Gamma略高)
|
||||||
self.cct_fig.suptitle("色度一致性测试", fontsize=12, y=0.985)
|
self.cct_fig.suptitle("色度一致性测试", fontsize=12, y=0.985, color=palette["fg"])
|
||||||
|
|
||||||
self.cct_fig.subplots_adjust(
|
self.cct_fig.subplots_adjust(
|
||||||
left=0.12,
|
left=0.12,
|
||||||
@@ -413,12 +442,14 @@ def init_contrast_chart(self: "PQAutomationApp"):
|
|||||||
"""初始化对比度图表 - 固定大小,居中显示"""
|
"""初始化对比度图表 - 固定大小,居中显示"""
|
||||||
container = ttk.Frame(self.contrast_chart_frame)
|
container = ttk.Frame(self.contrast_chart_frame)
|
||||||
container.pack(expand=True)
|
container.pack(expand=True)
|
||||||
|
palette = get_theme_palette()
|
||||||
|
|
||||||
self.contrast_fig = plt.Figure(
|
self.contrast_fig = plt.Figure(
|
||||||
figsize=(6, 6),
|
figsize=(6, 6),
|
||||||
dpi=100,
|
dpi=100,
|
||||||
tight_layout=False,
|
tight_layout=False,
|
||||||
)
|
)
|
||||||
|
self.contrast_fig.patch.set_facecolor(palette["bg"])
|
||||||
self.contrast_canvas = FigureCanvasTkAgg(self.contrast_fig, master=container)
|
self.contrast_canvas = FigureCanvasTkAgg(self.contrast_fig, master=container)
|
||||||
|
|
||||||
canvas_widget = self.contrast_canvas.get_tk_widget()
|
canvas_widget = self.contrast_canvas.get_tk_widget()
|
||||||
@@ -428,12 +459,13 @@ def init_contrast_chart(self: "PQAutomationApp"):
|
|||||||
canvas_widget.pack_propagate(False)
|
canvas_widget.pack_propagate(False)
|
||||||
|
|
||||||
self.contrast_ax = self.contrast_fig.add_subplot(111)
|
self.contrast_ax = self.contrast_fig.add_subplot(111)
|
||||||
|
self.contrast_ax.set_facecolor(palette["card_bg"])
|
||||||
self.contrast_ax.set_xlim(0, 1)
|
self.contrast_ax.set_xlim(0, 1)
|
||||||
self.contrast_ax.set_ylim(0, 1)
|
self.contrast_ax.set_ylim(0, 1)
|
||||||
self.contrast_ax.axis("off")
|
self.contrast_ax.axis("off")
|
||||||
|
|
||||||
# 调整标题位置:y=0.985
|
# 调整标题位置:y=0.985
|
||||||
self.contrast_fig.suptitle("对比度测试", fontsize=12, y=0.985)
|
self.contrast_fig.suptitle("对比度测试", fontsize=12, y=0.985, color=palette["fg"])
|
||||||
|
|
||||||
self.contrast_fig.subplots_adjust(
|
self.contrast_fig.subplots_adjust(
|
||||||
left=0.02,
|
left=0.02,
|
||||||
@@ -448,6 +480,7 @@ def init_accuracy_chart(self: "PQAutomationApp"):
|
|||||||
"""初始化色准图表 - 固定大小,居中显示"""
|
"""初始化色准图表 - 固定大小,居中显示"""
|
||||||
container = ttk.Frame(self.accuracy_chart_frame)
|
container = ttk.Frame(self.accuracy_chart_frame)
|
||||||
container.pack(expand=True, fill=tk.BOTH)
|
container.pack(expand=True, fill=tk.BOTH)
|
||||||
|
palette = get_theme_palette()
|
||||||
container.grid_rowconfigure(0, weight=1)
|
container.grid_rowconfigure(0, weight=1)
|
||||||
container.grid_rowconfigure(1, weight=0, minsize=220)
|
container.grid_rowconfigure(1, weight=0, minsize=220)
|
||||||
container.grid_columnconfigure(0, weight=1)
|
container.grid_columnconfigure(0, weight=1)
|
||||||
@@ -464,18 +497,27 @@ def init_accuracy_chart(self: "PQAutomationApp"):
|
|||||||
dpi=100,
|
dpi=100,
|
||||||
tight_layout=False,
|
tight_layout=False,
|
||||||
)
|
)
|
||||||
|
self.accuracy_fig.patch.set_facecolor(palette["bg"])
|
||||||
|
try:
|
||||||
|
self.accuracy_fig.set_layout_engine(None)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self.accuracy_fig.set_tight_layout(False)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
self.accuracy_canvas = FigureCanvasTkAgg(self.accuracy_fig, master=plot_container)
|
self.accuracy_canvas = FigureCanvasTkAgg(self.accuracy_fig, master=plot_container)
|
||||||
|
|
||||||
canvas_widget = self.accuracy_canvas.get_tk_widget()
|
canvas_widget = self.accuracy_canvas.get_tk_widget()
|
||||||
canvas_widget.pack(fill=tk.BOTH, expand=True)
|
canvas_widget.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
self.accuracy_ax = self.accuracy_fig.add_subplot(111)
|
self.accuracy_ax = self.accuracy_fig.add_subplot(111)
|
||||||
|
self.accuracy_ax.set_facecolor(palette["card_bg"])
|
||||||
self.accuracy_ax.set_xlim(0, 1)
|
self.accuracy_ax.set_xlim(0, 1)
|
||||||
self.accuracy_ax.set_ylim(0, 1)
|
self.accuracy_ax.set_ylim(0, 1)
|
||||||
self.accuracy_ax.axis("off")
|
self.accuracy_ax.axis("off")
|
||||||
|
|
||||||
# 调整标题位置
|
# 调整标题位置
|
||||||
self.accuracy_fig.suptitle("色准测试", fontsize=12, y=0.985)
|
self.accuracy_fig.suptitle("色准测试", fontsize=12, y=0.985, color=palette["fg"])
|
||||||
|
|
||||||
self.accuracy_fig.subplots_adjust(
|
self.accuracy_fig.subplots_adjust(
|
||||||
left=0.05,
|
left=0.05,
|
||||||
@@ -616,6 +658,7 @@ def update_accuracy_result_table(self: "PQAutomationApp", accuracy_data, standar
|
|||||||
|
|
||||||
def clear_chart(self: "PQAutomationApp"):
|
def clear_chart(self: "PQAutomationApp"):
|
||||||
"""清空所有图表"""
|
"""清空所有图表"""
|
||||||
|
palette = get_theme_palette()
|
||||||
|
|
||||||
# ========== 1. 清空色域图表 ==========
|
# ========== 1. 清空色域图表 ==========
|
||||||
if hasattr(self, "gamut_ax_xy") and hasattr(self, "gamut_ax_uv"):
|
if hasattr(self, "gamut_ax_xy") and hasattr(self, "gamut_ax_uv"):
|
||||||
@@ -640,12 +683,17 @@ def clear_chart(self: "PQAutomationApp"):
|
|||||||
if hasattr(self, "gamma_ax") and hasattr(self, "gamma_table_ax"):
|
if hasattr(self, "gamma_ax") and hasattr(self, "gamma_table_ax"):
|
||||||
# 清空左侧曲线
|
# 清空左侧曲线
|
||||||
self.gamma_ax.clear()
|
self.gamma_ax.clear()
|
||||||
|
self.gamma_fig.patch.set_facecolor(palette["bg"])
|
||||||
|
self.gamma_ax.set_facecolor(palette["card_bg"])
|
||||||
self.gamma_ax.set_xlim(0, 105)
|
self.gamma_ax.set_xlim(0, 105)
|
||||||
self.gamma_ax.set_ylim(0, 1.1)
|
self.gamma_ax.set_ylim(0, 1.1)
|
||||||
self.gamma_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
self.gamma_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
||||||
self.gamma_ax.set_ylabel("L_bar", fontsize=10)
|
self.gamma_ax.set_ylabel("L_bar", fontsize=10)
|
||||||
self.gamma_ax.grid(True, linestyle="--", alpha=0.3)
|
self.gamma_ax.grid(True, linestyle="--", alpha=0.3)
|
||||||
self.gamma_ax.tick_params(labelsize=9)
|
self.gamma_ax.tick_params(labelsize=9)
|
||||||
|
self.gamma_ax.tick_params(colors=palette["fg"])
|
||||||
|
for spine in self.gamma_ax.spines.values():
|
||||||
|
spine.set_color(palette["border"])
|
||||||
|
|
||||||
# 左侧提示
|
# 左侧提示
|
||||||
self.gamma_ax.text(
|
self.gamma_ax.text(
|
||||||
@@ -659,13 +707,13 @@ def clear_chart(self: "PQAutomationApp"):
|
|||||||
ha="center",
|
ha="center",
|
||||||
va="center",
|
va="center",
|
||||||
fontsize=10,
|
fontsize=10,
|
||||||
color="gray",
|
color=palette["muted_fg"],
|
||||||
transform=self.gamma_ax.transAxes,
|
transform=self.gamma_ax.transAxes,
|
||||||
bbox=dict(
|
bbox=dict(
|
||||||
boxstyle="round,pad=1",
|
boxstyle="round,pad=1",
|
||||||
facecolor="white",
|
facecolor=palette["card_bg"],
|
||||||
edgecolor="gray",
|
edgecolor=palette["border"],
|
||||||
alpha=0.8,
|
alpha=0.95,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -703,17 +751,17 @@ def clear_chart(self: "PQAutomationApp"):
|
|||||||
# 表头样式
|
# 表头样式
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
cell = table[(0, i)]
|
cell = table[(0, i)]
|
||||||
cell.set_facecolor("#4472C4")
|
cell.set_facecolor(palette["primary"])
|
||||||
cell.set_text_props(weight="bold", color="white", fontsize=7)
|
cell.set_text_props(weight="bold", color=palette["select_fg"], fontsize=7)
|
||||||
|
|
||||||
# 数据行交替颜色
|
# 数据行交替颜色
|
||||||
for i in range(1, len(table_data)):
|
for i in range(1, len(table_data)):
|
||||||
for j in range(4):
|
for j in range(4):
|
||||||
cell = table[(i, j)]
|
cell = table[(i, j)]
|
||||||
if i % 2 == 0:
|
if i % 2 == 0:
|
||||||
cell.set_facecolor("#E7E6E6")
|
cell.set_facecolor(palette["surface_alt_bg"])
|
||||||
else:
|
else:
|
||||||
cell.set_facecolor("#FFFFFF")
|
cell.set_facecolor(palette["card_bg"])
|
||||||
|
|
||||||
# 底部说明
|
# 底部说明
|
||||||
self.gamma_table_ax.text(
|
self.gamma_table_ax.text(
|
||||||
@@ -726,29 +774,34 @@ def clear_chart(self: "PQAutomationApp"):
|
|||||||
ha="center",
|
ha="center",
|
||||||
va="bottom",
|
va="bottom",
|
||||||
fontsize=7,
|
fontsize=7,
|
||||||
color="gray",
|
color=palette["muted_fg"],
|
||||||
transform=self.gamma_table_ax.transAxes,
|
transform=self.gamma_table_ax.transAxes,
|
||||||
bbox=dict(
|
bbox=dict(
|
||||||
boxstyle="round,pad=0.5",
|
boxstyle="round,pad=0.5",
|
||||||
facecolor="lightyellow",
|
facecolor=palette["surface_alt_bg"],
|
||||||
edgecolor="gray",
|
edgecolor=palette["border"],
|
||||||
alpha=0.8,
|
alpha=0.95,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.gamma_fig.suptitle("Gamma曲线 + 数据表格", fontsize=12, y=0.98)
|
self.gamma_fig.suptitle("Gamma曲线 + 数据表格", fontsize=12, y=0.98, color=palette["fg"])
|
||||||
self.gamma_canvas.draw()
|
self.gamma_canvas.draw()
|
||||||
|
|
||||||
# ========== 3. 清空EOTF图表(4列)==========
|
# ========== 3. 清空EOTF图表(4列)==========
|
||||||
if hasattr(self, "eotf_ax") and hasattr(self, "eotf_table_ax"):
|
if hasattr(self, "eotf_ax") and hasattr(self, "eotf_table_ax"):
|
||||||
# 清空左侧曲线
|
# 清空左侧曲线
|
||||||
self.eotf_ax.clear()
|
self.eotf_ax.clear()
|
||||||
|
self.eotf_fig.patch.set_facecolor(palette["bg"])
|
||||||
|
self.eotf_ax.set_facecolor(palette["card_bg"])
|
||||||
self.eotf_ax.set_xlim(0, 105)
|
self.eotf_ax.set_xlim(0, 105)
|
||||||
self.eotf_ax.set_ylim(0, 1.1)
|
self.eotf_ax.set_ylim(0, 1.1)
|
||||||
self.eotf_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
self.eotf_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
||||||
self.eotf_ax.set_ylabel("L_bar (归一化亮度)", fontsize=10)
|
self.eotf_ax.set_ylabel("L_bar (归一化亮度)", fontsize=10)
|
||||||
self.eotf_ax.grid(True, linestyle="--", alpha=0.3)
|
self.eotf_ax.grid(True, linestyle="--", alpha=0.3)
|
||||||
self.eotf_ax.tick_params(labelsize=9)
|
self.eotf_ax.tick_params(labelsize=9)
|
||||||
|
self.eotf_ax.tick_params(colors=palette["fg"])
|
||||||
|
for spine in self.eotf_ax.spines.values():
|
||||||
|
spine.set_color(palette["border"])
|
||||||
|
|
||||||
# 左侧提示
|
# 左侧提示
|
||||||
self.eotf_ax.text(
|
self.eotf_ax.text(
|
||||||
@@ -758,13 +811,13 @@ def clear_chart(self: "PQAutomationApp"):
|
|||||||
ha="center",
|
ha="center",
|
||||||
va="center",
|
va="center",
|
||||||
fontsize=11,
|
fontsize=11,
|
||||||
color="gray",
|
color=palette["muted_fg"],
|
||||||
transform=self.eotf_ax.transAxes,
|
transform=self.eotf_ax.transAxes,
|
||||||
bbox=dict(
|
bbox=dict(
|
||||||
boxstyle="round,pad=1",
|
boxstyle="round,pad=1",
|
||||||
facecolor="white",
|
facecolor=palette["card_bg"],
|
||||||
edgecolor="gray",
|
edgecolor=palette["border"],
|
||||||
alpha=0.8,
|
alpha=0.95,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -802,17 +855,17 @@ def clear_chart(self: "PQAutomationApp"):
|
|||||||
# 表头样式
|
# 表头样式
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
cell = table[(0, i)]
|
cell = table[(0, i)]
|
||||||
cell.set_facecolor("#4472C4")
|
cell.set_facecolor(palette["primary"])
|
||||||
cell.set_text_props(weight="bold", color="white", fontsize=7)
|
cell.set_text_props(weight="bold", color=palette["select_fg"], fontsize=7)
|
||||||
|
|
||||||
# 数据行交替颜色
|
# 数据行交替颜色
|
||||||
for i in range(1, len(table_data)):
|
for i in range(1, len(table_data)):
|
||||||
for j in range(4):
|
for j in range(4):
|
||||||
cell = table[(i, j)]
|
cell = table[(i, j)]
|
||||||
if i % 2 == 0:
|
if i % 2 == 0:
|
||||||
cell.set_facecolor("#E7E6E6")
|
cell.set_facecolor(palette["surface_alt_bg"])
|
||||||
else:
|
else:
|
||||||
cell.set_facecolor("#FFFFFF")
|
cell.set_facecolor(palette["card_bg"])
|
||||||
|
|
||||||
# 底部说明
|
# 底部说明
|
||||||
self.eotf_table_ax.text(
|
self.eotf_table_ax.text(
|
||||||
@@ -825,17 +878,17 @@ def clear_chart(self: "PQAutomationApp"):
|
|||||||
ha="center",
|
ha="center",
|
||||||
va="bottom",
|
va="bottom",
|
||||||
fontsize=7,
|
fontsize=7,
|
||||||
color="gray",
|
color=palette["muted_fg"],
|
||||||
transform=self.eotf_table_ax.transAxes,
|
transform=self.eotf_table_ax.transAxes,
|
||||||
bbox=dict(
|
bbox=dict(
|
||||||
boxstyle="round,pad=0.5",
|
boxstyle="round,pad=0.5",
|
||||||
facecolor="lightyellow",
|
facecolor=palette["surface_alt_bg"],
|
||||||
edgecolor="gray",
|
edgecolor=palette["border"],
|
||||||
alpha=0.8,
|
alpha=0.95,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.eotf_fig.suptitle("EOTF 曲线 + 数据表格", fontsize=12, y=0.98)
|
self.eotf_fig.suptitle("EOTF 曲线 + 数据表格", fontsize=12, y=0.98, color=palette["fg"])
|
||||||
self.eotf_canvas.draw()
|
self.eotf_canvas.draw()
|
||||||
|
|
||||||
# ========== 4. 清空色度图表 ==========
|
# ========== 4. 清空色度图表 ==========
|
||||||
@@ -843,8 +896,10 @@ def clear_chart(self: "PQAutomationApp"):
|
|||||||
# 过期引用。因此清空时必须同样重建,并更新引用,否则清不干净。
|
# 过期引用。因此清空时必须同样重建,并更新引用,否则清不干净。
|
||||||
if hasattr(self, "cct_fig") and hasattr(self, "cct_canvas"):
|
if hasattr(self, "cct_fig") and hasattr(self, "cct_canvas"):
|
||||||
self.cct_fig.clear()
|
self.cct_fig.clear()
|
||||||
|
self.cct_fig.patch.set_facecolor(palette["bg"])
|
||||||
|
|
||||||
self.cct_ax1 = self.cct_fig.add_subplot(211)
|
self.cct_ax1 = self.cct_fig.add_subplot(211)
|
||||||
|
self.cct_ax1.set_facecolor(palette["card_bg"])
|
||||||
self.cct_ax1.set_xlabel("灰阶 (%)", fontsize=9)
|
self.cct_ax1.set_xlabel("灰阶 (%)", fontsize=9)
|
||||||
self.cct_ax1.set_ylabel("CIE x", fontsize=9)
|
self.cct_ax1.set_ylabel("CIE x", fontsize=9)
|
||||||
self.cct_ax1.set_xlim(0, 105)
|
self.cct_ax1.set_xlim(0, 105)
|
||||||
@@ -853,6 +908,7 @@ def clear_chart(self: "PQAutomationApp"):
|
|||||||
self.cct_ax1.tick_params(labelsize=8)
|
self.cct_ax1.tick_params(labelsize=8)
|
||||||
|
|
||||||
self.cct_ax2 = self.cct_fig.add_subplot(212)
|
self.cct_ax2 = self.cct_fig.add_subplot(212)
|
||||||
|
self.cct_ax2.set_facecolor(palette["card_bg"])
|
||||||
self.cct_ax2.set_xlabel("灰阶 (%)", fontsize=9)
|
self.cct_ax2.set_xlabel("灰阶 (%)", fontsize=9)
|
||||||
self.cct_ax2.set_ylabel("CIE y", fontsize=9)
|
self.cct_ax2.set_ylabel("CIE y", fontsize=9)
|
||||||
self.cct_ax2.set_xlim(0, 105)
|
self.cct_ax2.set_xlim(0, 105)
|
||||||
@@ -860,7 +916,7 @@ def clear_chart(self: "PQAutomationApp"):
|
|||||||
self.cct_ax2.grid(True, linestyle="--", alpha=0.3)
|
self.cct_ax2.grid(True, linestyle="--", alpha=0.3)
|
||||||
self.cct_ax2.tick_params(labelsize=8)
|
self.cct_ax2.tick_params(labelsize=8)
|
||||||
|
|
||||||
self.cct_fig.suptitle("色度一致性测试", fontsize=12, y=0.985)
|
self.cct_fig.suptitle("色度一致性测试", fontsize=12, y=0.985, color=palette["fg"])
|
||||||
self.cct_fig.subplots_adjust(
|
self.cct_fig.subplots_adjust(
|
||||||
left=0.12,
|
left=0.12,
|
||||||
right=0.88,
|
right=0.88,
|
||||||
@@ -873,11 +929,13 @@ def clear_chart(self: "PQAutomationApp"):
|
|||||||
# ========== 5. 清空对比度图表 ==========
|
# ========== 5. 清空对比度图表 ==========
|
||||||
if hasattr(self, "contrast_ax"):
|
if hasattr(self, "contrast_ax"):
|
||||||
self.contrast_ax.clear()
|
self.contrast_ax.clear()
|
||||||
|
self.contrast_fig.patch.set_facecolor(palette["bg"])
|
||||||
|
self.contrast_ax.set_facecolor(palette["card_bg"])
|
||||||
self.contrast_ax.set_xlim(0, 1)
|
self.contrast_ax.set_xlim(0, 1)
|
||||||
self.contrast_ax.set_ylim(0, 1)
|
self.contrast_ax.set_ylim(0, 1)
|
||||||
self.contrast_ax.axis("off")
|
self.contrast_ax.axis("off")
|
||||||
|
|
||||||
self.contrast_fig.suptitle("对比度测试", fontsize=12, y=0.985)
|
self.contrast_fig.suptitle("对比度测试", fontsize=12, y=0.985, color=palette["fg"])
|
||||||
|
|
||||||
# 重置布局
|
# 重置布局
|
||||||
self.contrast_fig.subplots_adjust(
|
self.contrast_fig.subplots_adjust(
|
||||||
@@ -892,12 +950,14 @@ def clear_chart(self: "PQAutomationApp"):
|
|||||||
# ========== 6. 清空色准图表 ==========
|
# ========== 6. 清空色准图表 ==========
|
||||||
if hasattr(self, "accuracy_ax"):
|
if hasattr(self, "accuracy_ax"):
|
||||||
self.accuracy_ax.clear()
|
self.accuracy_ax.clear()
|
||||||
|
self.accuracy_fig.patch.set_facecolor(palette["bg"])
|
||||||
|
self.accuracy_ax.set_facecolor(palette["card_bg"])
|
||||||
self.accuracy_ax.set_xlim(0, 1)
|
self.accuracy_ax.set_xlim(0, 1)
|
||||||
self.accuracy_ax.set_ylim(0, 1)
|
self.accuracy_ax.set_ylim(0, 1)
|
||||||
self.accuracy_ax.axis("off")
|
self.accuracy_ax.axis("off")
|
||||||
|
|
||||||
# 标题
|
# 标题
|
||||||
self.accuracy_fig.suptitle("色准测试", fontsize=12, y=0.985)
|
self.accuracy_fig.suptitle("色准测试", fontsize=12, y=0.985, color=palette["fg"])
|
||||||
|
|
||||||
# 重置布局
|
# 重置布局
|
||||||
self.accuracy_fig.subplots_adjust(
|
self.accuracy_fig.subplots_adjust(
|
||||||
|
|||||||
@@ -36,37 +36,188 @@ def _is_dark(color: str) -> bool:
|
|||||||
return (r * 299 + g * 587 + b * 114) / 1000 < 128
|
return (r * 299 + g * 587 + b * 114) / 1000 < 128
|
||||||
|
|
||||||
|
|
||||||
def apply_modern_styles() -> None:
|
def _contrast_text(color: str, *, dark_text: str, light_text: str) -> str:
|
||||||
"""注册或刷新现代化样式集。可在主题切换后再次调用。"""
|
return dark_text if _is_dark(color) else light_text
|
||||||
style = ttk.Style()
|
|
||||||
theme = style.colors # ttkbootstrap.style.Colors
|
|
||||||
|
|
||||||
bg = theme.bg # 主背景
|
|
||||||
fg = theme.fg # 主前景
|
def get_theme_palette() -> dict[str, str]:
|
||||||
|
"""返回当前主题的语义色板,供 ttk / tk 自定义控件共用。"""
|
||||||
|
style = ttk.Style()
|
||||||
|
theme = style.colors
|
||||||
|
|
||||||
|
bg = theme.bg
|
||||||
|
fg = theme.fg
|
||||||
primary = theme.primary
|
primary = theme.primary
|
||||||
secondary = theme.secondary
|
secondary = theme.secondary
|
||||||
|
success = theme.success
|
||||||
info = theme.info
|
info = theme.info
|
||||||
|
warning = theme.warning
|
||||||
|
danger = theme.danger
|
||||||
dark = theme.dark
|
dark = theme.dark
|
||||||
border = theme.border
|
border = theme.border
|
||||||
inputbg = theme.inputbg
|
inputbg = theme.inputbg
|
||||||
|
inputfg = getattr(theme, "inputfg", fg)
|
||||||
|
|
||||||
dark_theme = _is_dark(bg)
|
dark_theme = _is_dark(bg)
|
||||||
|
select_bg = getattr(theme, "selectbg", _mix(primary, bg, 0.30 if dark_theme else 0.12))
|
||||||
|
select_fg = getattr(theme, "selectfg", "#ffffff" if _is_dark(select_bg) else fg)
|
||||||
|
|
||||||
# 卡片背景:在主背景上轻微偏移,营造层级感
|
if dark_theme:
|
||||||
card_bg = _mix(bg, "#ffffff", 0.04) if dark_theme else _mix(bg, "#000000", 0.025)
|
card_bg = _mix(bg, "#ffffff", 0.04)
|
||||||
card_border = _mix(bg, fg, 0.18) if dark_theme else _mix(bg, "#000000", 0.10)
|
card_border = _mix(bg, fg, 0.18)
|
||||||
# 配置项 header 用 secondary 主题色
|
header_fg = _contrast_text(
|
||||||
header_bg = secondary
|
"#444A51",
|
||||||
header_fg = "#ffffff" if _is_dark(secondary) else "#1a1a1a"
|
dark_text="#ffffff",
|
||||||
|
light_text="#1a1a1a",
|
||||||
|
)
|
||||||
|
sidebar_bg = _mix(dark, bg, 0.18)
|
||||||
|
sidebar_hover = _mix(sidebar_bg, "#ffffff", 0.07)
|
||||||
|
sidebar_selected = _mix(sidebar_bg, "#ffffff", 0.14)
|
||||||
|
sidebar_fg = _mix(fg, "#ffffff", 0.04)
|
||||||
|
sidebar_muted = _mix(sidebar_fg, sidebar_bg, 0.45)
|
||||||
|
muted_fg = _mix(fg, bg, 0.32)
|
||||||
|
disabled_fg = _mix(fg, bg, 0.42)
|
||||||
|
disabled_bg = _mix(inputbg, bg, 0.18)
|
||||||
|
disabled_border = _mix(border, fg, 0.22)
|
||||||
|
readonly_bg = _mix(inputbg, "#ffffff", 0.06)
|
||||||
|
success_fg = _mix(success, "#ffffff", 0.08)
|
||||||
|
warning_fg = _mix(warning, "#ffffff", 0.06)
|
||||||
|
info_fg = _mix(info, "#ffffff", 0.06)
|
||||||
|
statusbar_bg = _mix(bg, "#ffffff", 0.06)
|
||||||
|
tooltip_bg = _mix(inputbg, bg, 0.08)
|
||||||
|
tooltip_fg = inputfg
|
||||||
|
tooltip_border = _mix(border, fg, 0.20)
|
||||||
|
surface_alt_bg = _mix(card_bg, "#ffffff", 0.05)
|
||||||
|
surface_hover_bg = _mix(card_bg, "#ffffff", 0.09)
|
||||||
|
badge_bg = _mix(danger, bg, 0.12)
|
||||||
|
badge_fg = "#ffffff"
|
||||||
|
focus = _mix(primary, "#ffffff", 0.18)
|
||||||
|
config_bg = _mix("#444A51", bg, 0.30)
|
||||||
|
else:
|
||||||
|
card_bg = inputbg
|
||||||
|
card_border = border
|
||||||
|
header_fg = bg
|
||||||
|
config_bg = _mix(primary, bg, 0.25)
|
||||||
|
sidebar_bg = _mix(primary, bg, 0.82)
|
||||||
|
sidebar_hover = _mix(primary, bg, 0.72)
|
||||||
|
sidebar_selected = primary
|
||||||
|
sidebar_fg = fg
|
||||||
|
sidebar_muted = _mix(fg, sidebar_bg, 0.35)
|
||||||
|
muted_fg = _mix(fg, bg, 0.38)
|
||||||
|
disabled_fg = _mix(fg, bg, 0.55)
|
||||||
|
disabled_bg = _mix(bg, border, 0.18)
|
||||||
|
disabled_border = _mix(border, bg, 0.18)
|
||||||
|
readonly_bg = _mix(inputbg, primary, 0.04)
|
||||||
|
success_fg = success
|
||||||
|
warning_fg = _mix(warning, fg, 0.18)
|
||||||
|
info_fg = info
|
||||||
|
statusbar_bg = _mix(bg, dark, 0.04)
|
||||||
|
tooltip_bg = inputbg
|
||||||
|
tooltip_fg = inputfg
|
||||||
|
tooltip_border = border
|
||||||
|
surface_alt_bg = _mix(bg, dark, 0.03)
|
||||||
|
surface_hover_bg = _mix(bg, dark, 0.05)
|
||||||
|
badge_bg = danger
|
||||||
|
badge_fg = "#ffffff"
|
||||||
|
focus = _mix(primary, bg, 0.20)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"bg": bg,
|
||||||
|
"fg": fg,
|
||||||
|
"primary": primary,
|
||||||
|
"secondary": secondary,
|
||||||
|
"success": success,
|
||||||
|
"info": info,
|
||||||
|
"warning": warning,
|
||||||
|
"danger": danger,
|
||||||
|
"border": border,
|
||||||
|
"input_bg": inputbg,
|
||||||
|
"input_fg": inputfg,
|
||||||
|
"select_bg": select_bg,
|
||||||
|
"select_fg": select_fg,
|
||||||
|
"card_bg": card_bg,
|
||||||
|
"card_border": card_border,
|
||||||
|
"header_fg": header_fg,
|
||||||
|
"sidebar_bg": sidebar_bg,
|
||||||
|
"sidebar_hover": sidebar_hover,
|
||||||
|
"sidebar_selected": sidebar_selected,
|
||||||
|
"sidebar_fg": sidebar_fg,
|
||||||
|
"sidebar_muted": sidebar_muted,
|
||||||
|
"muted_fg": muted_fg,
|
||||||
|
"disabled_fg": disabled_fg,
|
||||||
|
"disabled_bg": disabled_bg,
|
||||||
|
"disabled_border": disabled_border,
|
||||||
|
"readonly_bg": readonly_bg,
|
||||||
|
"success_fg": success_fg,
|
||||||
|
"warning_fg": warning_fg,
|
||||||
|
"info_fg": info_fg,
|
||||||
|
"statusbar_bg": statusbar_bg,
|
||||||
|
"tooltip_bg": tooltip_bg,
|
||||||
|
"tooltip_fg": tooltip_fg,
|
||||||
|
"tooltip_border": tooltip_border,
|
||||||
|
"surface_alt_bg": surface_alt_bg,
|
||||||
|
"surface_hover_bg": surface_hover_bg,
|
||||||
|
"badge_bg": badge_bg,
|
||||||
|
"badge_fg": badge_fg,
|
||||||
|
"focus": focus,
|
||||||
|
"config_bg": config_bg,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def apply_listbox_theme(widget) -> None:
|
||||||
|
"""将 tk.Listbox 颜色同步到当前主题。"""
|
||||||
|
palette = get_theme_palette()
|
||||||
|
widget.configure(
|
||||||
|
background=palette["input_bg"],
|
||||||
|
foreground=palette["input_fg"],
|
||||||
|
highlightbackground=palette["border"],
|
||||||
|
highlightcolor=palette["focus"],
|
||||||
|
selectbackground=palette["select_bg"],
|
||||||
|
selectforeground=palette["select_fg"],
|
||||||
|
disabledforeground=palette["disabled_fg"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_tooltip_theme(toplevel, label) -> None:
|
||||||
|
"""将 tooltip 的 tk.Toplevel / Label 同步到当前主题。"""
|
||||||
|
palette = get_theme_palette()
|
||||||
|
toplevel.configure(background=palette["tooltip_border"])
|
||||||
|
label.configure(
|
||||||
|
bg=palette["tooltip_bg"],
|
||||||
|
fg=palette["tooltip_fg"],
|
||||||
|
highlightbackground=palette["tooltip_border"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_modern_styles() -> None:
|
||||||
|
"""注册或刷新现代化样式集。可在主题切换后再次调用。"""
|
||||||
|
style = ttk.Style()
|
||||||
|
palette = get_theme_palette()
|
||||||
|
|
||||||
|
bg = palette["bg"]
|
||||||
|
fg = palette["fg"]
|
||||||
|
primary = palette["primary"]
|
||||||
|
secondary = palette["secondary"]
|
||||||
|
info = palette["info"]
|
||||||
|
card_bg = palette["card_bg"]
|
||||||
|
card_border = palette["card_border"]
|
||||||
|
header_bg = palette["config_bg"]
|
||||||
|
header_fg = palette["header_fg"]
|
||||||
|
dark_theme = _is_dark(bg)
|
||||||
header_hover_bg = _mix(secondary, "#ffffff", 0.08) if _is_dark(secondary) else _mix(secondary, "#000000", 0.08)
|
header_hover_bg = _mix(secondary, "#ffffff", 0.08) if _is_dark(secondary) else _mix(secondary, "#000000", 0.08)
|
||||||
|
|
||||||
preview_fg = _mix(header_fg, header_bg, 0.35)
|
preview_fg = _mix(header_fg, header_bg, 0.35)
|
||||||
sidebar_bg = _mix(dark, bg, 0.18) if dark_theme else _mix(primary, "#000000", 0.10)
|
sidebar_bg = palette["sidebar_bg"]
|
||||||
sidebar_hover = _mix(sidebar_bg, "#ffffff", 0.07) if dark_theme else _mix(sidebar_bg, "#000000", 0.06)
|
sidebar_hover = palette["sidebar_hover"]
|
||||||
sidebar_selected = _mix(sidebar_bg, "#ffffff", 0.14) if dark_theme else _mix(sidebar_bg, "#000000", 0.10)
|
sidebar_selected = palette["sidebar_selected"]
|
||||||
# 侧栏背景在浅色主题下也偏深,文字颜色需按侧栏亮度自适应,避免“黑字不明显”。
|
sidebar_fg = palette["sidebar_fg"]
|
||||||
sidebar_fg = "#F4F8FD" if _is_dark(sidebar_bg) else _mix(fg, bg, 0.05)
|
sidebar_muted = palette["sidebar_muted"]
|
||||||
sidebar_muted = _mix(sidebar_fg, sidebar_bg, 0.45)
|
muted_fg = palette["muted_fg"]
|
||||||
|
disabled_fg = palette["disabled_fg"]
|
||||||
|
disabled_bg = palette["disabled_bg"]
|
||||||
|
disabled_border = palette["disabled_border"]
|
||||||
|
readonly_bg = palette["readonly_bg"]
|
||||||
|
success_fg = palette["success_fg"]
|
||||||
|
warning_fg = palette["warning_fg"]
|
||||||
|
|
||||||
# ---------------- 卡片 ----------------
|
# ---------------- 卡片 ----------------
|
||||||
style.configure(
|
style.configure(
|
||||||
@@ -134,6 +285,12 @@ def apply_modern_styles() -> None:
|
|||||||
font=("Segoe UI", 9),
|
font=("Segoe UI", 9),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ---------------- 通用文字语义 ----------------
|
||||||
|
style.configure("Muted.TLabel", background=bg, foreground=muted_fg)
|
||||||
|
style.configure("SuccessState.TLabel", background=bg, foreground=success_fg)
|
||||||
|
style.configure("WarningState.TLabel", background=bg, foreground=warning_fg)
|
||||||
|
style.configure("InfoState.TLabel", background=bg, foreground=palette["info_fg"])
|
||||||
|
|
||||||
# ---------------- 顶部工具条 ----------------
|
# ---------------- 顶部工具条 ----------------
|
||||||
style.configure("Toolbar.TFrame", background=bg, borderwidth=0)
|
style.configure("Toolbar.TFrame", background=bg, borderwidth=0)
|
||||||
# 工具条上的次要按钮(清理配置等)
|
# 工具条上的次要按钮(清理配置等)
|
||||||
@@ -168,9 +325,17 @@ def apply_modern_styles() -> None:
|
|||||||
style.configure(
|
style.configure(
|
||||||
"SidebarBrand.TLabel",
|
"SidebarBrand.TLabel",
|
||||||
background=brand_bg,
|
background=brand_bg,
|
||||||
foreground="#ffffff",
|
foreground=palette["badge_fg"],
|
||||||
font=("Segoe UI Semibold", 12),
|
font=("Segoe UI Semibold", 12),
|
||||||
)
|
)
|
||||||
|
style.configure(
|
||||||
|
"SidebarBadge.TLabel",
|
||||||
|
background=palette["badge_bg"],
|
||||||
|
foreground=palette["badge_fg"],
|
||||||
|
font=("微软雅黑", 8, "bold"),
|
||||||
|
anchor="center",
|
||||||
|
padding=(6, 2),
|
||||||
|
)
|
||||||
|
|
||||||
# ---------------- 结果区无边框标题行 ----------------
|
# ---------------- 结果区无边框标题行 ----------------
|
||||||
style.configure("ResultHeader.TFrame", background=bg, borderwidth=0)
|
style.configure("ResultHeader.TFrame", background=bg, borderwidth=0)
|
||||||
@@ -182,7 +347,7 @@ def apply_modern_styles() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ---------------- 状态栏 ----------------
|
# ---------------- 状态栏 ----------------
|
||||||
statusbar_bg = _mix(bg, "#000000", 0.06) if not dark_theme else _mix(bg, "#ffffff", 0.06)
|
statusbar_bg = palette["statusbar_bg"]
|
||||||
statusbar_fg = _mix(fg, bg, 0.15)
|
statusbar_fg = _mix(fg, bg, 0.15)
|
||||||
style.configure(
|
style.configure(
|
||||||
"StatusBar.TFrame",
|
"StatusBar.TFrame",
|
||||||
@@ -204,6 +369,33 @@ def apply_modern_styles() -> None:
|
|||||||
padding=(10, 4),
|
padding=(10, 4),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ---------------- 深色禁用态 / 只读态增强 ----------------
|
||||||
|
style.map(
|
||||||
|
"TLabel",
|
||||||
|
foreground=[("disabled", disabled_fg)],
|
||||||
|
)
|
||||||
|
style.map(
|
||||||
|
"TButton",
|
||||||
|
foreground=[("disabled", disabled_fg)],
|
||||||
|
background=[("disabled", disabled_bg)],
|
||||||
|
bordercolor=[("disabled", disabled_border)],
|
||||||
|
darkcolor=[("disabled", disabled_bg)],
|
||||||
|
lightcolor=[("disabled", disabled_bg)],
|
||||||
|
)
|
||||||
|
style.map(
|
||||||
|
"TEntry",
|
||||||
|
foreground=[("disabled", disabled_fg)],
|
||||||
|
fieldbackground=[("disabled", disabled_bg), ("readonly", readonly_bg)],
|
||||||
|
bordercolor=[("disabled", disabled_border), ("readonly", disabled_border)],
|
||||||
|
)
|
||||||
|
style.map(
|
||||||
|
"TCombobox",
|
||||||
|
foreground=[("disabled", disabled_fg), ("readonly", fg)],
|
||||||
|
fieldbackground=[("disabled", disabled_bg), ("readonly", readonly_bg)],
|
||||||
|
bordercolor=[("disabled", disabled_border), ("readonly", disabled_border)],
|
||||||
|
arrowcolor=[("disabled", disabled_fg), ("readonly", muted_fg)],
|
||||||
|
)
|
||||||
|
|
||||||
# ---------------- Sidebar 按钮(保留兼容名) ----------------
|
# ---------------- Sidebar 按钮(保留兼容名) ----------------
|
||||||
style.configure(
|
style.configure(
|
||||||
"Sidebar.TButton",
|
"Sidebar.TButton",
|
||||||
@@ -225,7 +417,7 @@ def apply_modern_styles() -> None:
|
|||||||
style.configure(
|
style.configure(
|
||||||
"SidebarSelected.TButton",
|
"SidebarSelected.TButton",
|
||||||
background=sidebar_selected,
|
background=sidebar_selected,
|
||||||
foreground="#ffffff",
|
foreground=_contrast_text(sidebar_selected, dark_text=palette["badge_fg"], light_text=sidebar_fg),
|
||||||
font=("Segoe UI Semibold", 10),
|
font=("Segoe UI Semibold", 10),
|
||||||
padding=(18, 9),
|
padding=(18, 9),
|
||||||
borderwidth=0,
|
borderwidth=0,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
# 隐藏所有面板
|
# 隐藏所有面板
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import ttkbootstrap as ttk
|
|||||||
from PIL import Image, ImageTk
|
from PIL import Image, ImageTk
|
||||||
|
|
||||||
from app.services import ai_image as _svc
|
from app.services import ai_image as _svc
|
||||||
|
from app.views.modern_styles import apply_tooltip_theme, get_theme_palette
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -26,17 +27,19 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def _theme_colors():
|
def _theme_colors():
|
||||||
style = ttk.Style()
|
palette = get_theme_palette()
|
||||||
colors = style.colors
|
|
||||||
return {
|
return {
|
||||||
"bg": colors.bg,
|
"bg": palette["bg"],
|
||||||
"fg": colors.fg,
|
"fg": palette["fg"],
|
||||||
"muted": colors.secondary,
|
"muted": palette["muted_fg"],
|
||||||
"input_bg": colors.inputbg,
|
"input_bg": palette["input_bg"],
|
||||||
"input_fg": colors.inputfg,
|
"input_fg": palette["input_fg"],
|
||||||
"select_bg": colors.selectbg,
|
"select_bg": palette["select_bg"],
|
||||||
"select_fg": colors.selectfg,
|
"select_fg": palette["select_fg"],
|
||||||
"border": colors.border,
|
"border": palette["border"],
|
||||||
|
"tooltip_bg": palette["tooltip_bg"],
|
||||||
|
"tooltip_fg": palette["tooltip_fg"],
|
||||||
|
"tooltip_border": palette["tooltip_border"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -95,8 +98,6 @@ def _show_tree_tooltip(self: "PQAutomationApp", text: str, x_root: int, y_root:
|
|||||||
text="",
|
text="",
|
||||||
justify=tk.LEFT,
|
justify=tk.LEFT,
|
||||||
anchor=tk.W,
|
anchor=tk.W,
|
||||||
bg="#ffffff",
|
|
||||||
fg="#1f2937",
|
|
||||||
relief=tk.SOLID,
|
relief=tk.SOLID,
|
||||||
bd=1,
|
bd=1,
|
||||||
padx=8,
|
padx=8,
|
||||||
@@ -104,6 +105,7 @@ def _show_tree_tooltip(self: "PQAutomationApp", text: str, x_root: int, y_root:
|
|||||||
font=("微软雅黑", 9),
|
font=("微软雅黑", 9),
|
||||||
wraplength=520,
|
wraplength=520,
|
||||||
)
|
)
|
||||||
|
apply_tooltip_theme(tip, label)
|
||||||
label.pack(fill=tk.BOTH, expand=True)
|
label.pack(fill=tk.BOTH, expand=True)
|
||||||
self._ai_image_tooltip = tip
|
self._ai_image_tooltip = tip
|
||||||
self._ai_image_tooltip_label = label
|
self._ai_image_tooltip_label = label
|
||||||
@@ -114,6 +116,7 @@ def _show_tree_tooltip(self: "PQAutomationApp", text: str, x_root: int, y_root:
|
|||||||
|
|
||||||
self._ai_image_tooltip_item = item_id
|
self._ai_image_tooltip_item = item_id
|
||||||
label.configure(text=text)
|
label.configure(text=text)
|
||||||
|
apply_tooltip_theme(tip, label)
|
||||||
tip.geometry(f"+{x_root + 14}+{y_root + 18}")
|
tip.geometry(f"+{x_root + 14}+{y_root + 18}")
|
||||||
tip.deiconify()
|
tip.deiconify()
|
||||||
tip.lift()
|
tip.lift()
|
||||||
@@ -412,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:
|
||||||
@@ -615,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:
|
||||||
@@ -628,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):
|
||||||
@@ -803,17 +802,22 @@ def _on_upload_done(self: "PQAutomationApp", name: str, url: str, exc):
|
|||||||
|
|
||||||
|
|
||||||
def _clear_reference_image(self: "PQAutomationApp"):
|
def _clear_reference_image(self: "PQAutomationApp"):
|
||||||
"""清除手动上传的参考图,同时清除当前会话的自动链路参考。"""
|
"""清除手动上传的参考图。
|
||||||
|
|
||||||
|
v2.1 规则要求:从第二轮开始应使用最近一次成功生成图作为输入,
|
||||||
|
因此这里不清除会话级自动链路参考;若需彻底重置,请点“新对话”。
|
||||||
|
"""
|
||||||
if getattr(self, "_ai_image_requesting", False):
|
if getattr(self, "_ai_image_requesting", False):
|
||||||
return
|
return
|
||||||
self._ai_image_pending_ref_url = ""
|
self._ai_image_pending_ref_url = ""
|
||||||
self._ai_image_pending_ref_name = ""
|
self._ai_image_pending_ref_name = ""
|
||||||
sid = _svc.get_session_id()
|
|
||||||
refs = getattr(self, "_ai_image_session_refs", None)
|
|
||||||
if isinstance(refs, dict):
|
|
||||||
refs.pop(sid, None)
|
|
||||||
_refresh_ref_label(self)
|
_refresh_ref_label(self)
|
||||||
self.ai_image_status_var.set("已清除参考图,切换为文生图模式")
|
sid = _svc.get_session_id()
|
||||||
|
refs = getattr(self, "_ai_image_session_refs", None) or {}
|
||||||
|
if (refs.get(sid) or "").strip():
|
||||||
|
self.ai_image_status_var.set("已清除手动参考图,当前会话仍沿用上一轮生成图")
|
||||||
|
else:
|
||||||
|
self.ai_image_status_var.set("已清除参考图,当前为文生图模式")
|
||||||
|
|
||||||
|
|
||||||
def _session_id_for_item(self: "PQAutomationApp", item_id: str) -> str:
|
def _session_id_for_item(self: "PQAutomationApp", item_id: str) -> str:
|
||||||
@@ -840,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,
|
||||||
@@ -1220,6 +1224,16 @@ def _build_ucd_resized_image(image_path: str, target_w: int, target_h: int) -> s
|
|||||||
return out_path
|
return out_path
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_ai_image_theme(self: "PQAutomationApp"):
|
||||||
|
"""刷新 AI 图片面板中的主题相关控件。"""
|
||||||
|
if hasattr(self, "_apply_ai_image_list_style"):
|
||||||
|
self._apply_ai_image_list_style()
|
||||||
|
tip = getattr(self, "_ai_image_tooltip", None)
|
||||||
|
label = getattr(self, "_ai_image_tooltip_label", None)
|
||||||
|
if tip is not None and label is not None:
|
||||||
|
apply_tooltip_theme(tip, label)
|
||||||
|
|
||||||
|
|
||||||
class AIImagePanelMixin:
|
class AIImagePanelMixin:
|
||||||
"""由 tools/refactor_to_mixins.py 自动生成。
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
@@ -1244,3 +1258,5 @@ class AIImagePanelMixin:
|
|||||||
_rename_current = _rename_current
|
_rename_current = _rename_current
|
||||||
_show_list_context_menu = _show_list_context_menu
|
_show_list_context_menu = _show_list_context_menu
|
||||||
_send_to_ucd = _send_to_ucd
|
_send_to_ucd = _send_to_ucd
|
||||||
|
_apply_ai_image_list_style = _apply_ai_image_list_style
|
||||||
|
refresh_ai_image_theme = refresh_ai_image_theme
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|||||||
from matplotlib.figure import Figure
|
from matplotlib.figure import Figure
|
||||||
|
|
||||||
from app.tests.color_accuracy import calculate_delta_e_2000
|
from app.tests.color_accuracy import calculate_delta_e_2000
|
||||||
|
from app.views.modern_styles import get_theme_palette
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from pqAutomationApp import PQAutomationApp
|
from pqAutomationApp import PQAutomationApp
|
||||||
@@ -35,10 +36,6 @@ D65_X = 0.3127
|
|||||||
D65_Y = 0.3290
|
D65_Y = 0.3290
|
||||||
TARGET_CCT = 6504
|
TARGET_CCT = 6504
|
||||||
TARGET_GAMMA = 2.2
|
TARGET_GAMMA = 2.2
|
||||||
_DARK_BG = "#2f2f2f"
|
|
||||||
_AX_BG = "#262626"
|
|
||||||
_FG = "#d8d8d8"
|
|
||||||
_GRID = "#5b5b5b"
|
|
||||||
|
|
||||||
DE_FORMULAS = ["2000", "94", "76"]
|
DE_FORMULAS = ["2000", "94", "76"]
|
||||||
|
|
||||||
@@ -60,7 +57,7 @@ def _contrast_fg(gray_value: int) -> str:
|
|||||||
def _set_canvas_patch(canvas: tk.Canvas, color: str, text: str) -> None:
|
def _set_canvas_patch(canvas: tk.Canvas, color: str, text: str) -> None:
|
||||||
"""统一设置色块 Canvas 颜色与文字,避免主题对 Label 背景渲染的干扰。"""
|
"""统一设置色块 Canvas 颜色与文字,避免主题对 Label 背景渲染的干扰。"""
|
||||||
gray = int(color[1:3], 16)
|
gray = int(color[1:3], 16)
|
||||||
canvas.configure(bg=color, highlightbackground="#666666")
|
canvas.configure(bg=color, highlightbackground=get_theme_palette()["border"])
|
||||||
canvas.itemconfigure("patch_bg", fill=color, outline=color)
|
canvas.itemconfigure("patch_bg", fill=color, outline=color)
|
||||||
canvas.itemconfigure("patch_text", text=text, fill=_contrast_fg(gray))
|
canvas.itemconfigure("patch_text", text=text, fill=_contrast_fg(gray))
|
||||||
|
|
||||||
@@ -115,6 +112,7 @@ def _get_calman_palette() -> dict[str, str]:
|
|||||||
"""根据当前主题生成 Calman 调试面板色板。"""
|
"""根据当前主题生成 Calman 调试面板色板。"""
|
||||||
style = ttk.Style()
|
style = ttk.Style()
|
||||||
colors = style.colors
|
colors = style.colors
|
||||||
|
theme_palette = get_theme_palette()
|
||||||
bg = colors.bg
|
bg = colors.bg
|
||||||
fg = colors.fg
|
fg = colors.fg
|
||||||
dark_mode = _is_dark_hex(bg)
|
dark_mode = _is_dark_hex(bg)
|
||||||
@@ -131,22 +129,22 @@ def _get_calman_palette() -> dict[str, str]:
|
|||||||
reading_fg = _mix(fg, "#ffffff", 0.06)
|
reading_fg = _mix(fg, "#ffffff", 0.06)
|
||||||
status_fg = _mix(fg, bg, 0.35)
|
status_fg = _mix(fg, bg, 0.35)
|
||||||
reading_accent = colors.info
|
reading_accent = colors.info
|
||||||
xy_series = "#d7dce4"
|
xy_series = _mix(fg, "#ffffff", 0.10)
|
||||||
d65_mark = "#ffffff"
|
d65_mark = _mix(fg, "#ffffff", 0.04)
|
||||||
else:
|
else:
|
||||||
figure_bg = _mix(bg, "#dfe7ef", 0.45)
|
figure_bg = _mix(bg, "#dfe7ef", 0.45)
|
||||||
axes_bg = _mix(bg, "#eff4f9", 0.72)
|
axes_bg = _mix(bg, "#eff4f9", 0.72)
|
||||||
grid = _mix("#5f6f82", axes_bg, 0.55)
|
grid = _mix("#5f6f82", axes_bg, 0.55)
|
||||||
tree_bg = "#ffffff"
|
tree_bg = theme_palette["input_bg"]
|
||||||
tree_even = "#ffffff"
|
tree_even = theme_palette["input_bg"]
|
||||||
tree_odd = "#f3f7fb"
|
tree_odd = "#f3f7fb"
|
||||||
heading_bg = _mix(colors.primary, "#ffffff", 0.82)
|
heading_bg = _mix(colors.primary, "#ffffff", 0.82)
|
||||||
reading_bg = _mix(bg, "#e7eef5", 0.58)
|
reading_bg = _mix(bg, "#e7eef5", 0.58)
|
||||||
reading_fg = fg
|
reading_fg = fg
|
||||||
status_fg = _mix(fg, bg, 0.25)
|
status_fg = _mix(fg, bg, 0.25)
|
||||||
reading_accent = _mix(colors.info, "#000000", 0.25)
|
reading_accent = _mix(colors.info, "#000000", 0.25)
|
||||||
xy_series = "#1f2a36"
|
xy_series = _mix(fg, bg, 0.18)
|
||||||
d65_mark = "#253142"
|
d65_mark = _mix(fg, bg, 0.28)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"figure_bg": figure_bg,
|
"figure_bg": figure_bg,
|
||||||
@@ -166,31 +164,59 @@ def _get_calman_palette() -> dict[str, str]:
|
|||||||
"tree_heading_bg": heading_bg,
|
"tree_heading_bg": heading_bg,
|
||||||
"tree_heading_fg": reading_fg,
|
"tree_heading_fg": reading_fg,
|
||||||
"tree_select": _mix(colors.info, figure_bg, 0.35),
|
"tree_select": _mix(colors.info, figure_bg, 0.35),
|
||||||
|
"patch_border": theme_palette["border"],
|
||||||
|
"patch_border_alt": _mix(theme_palette["border"], theme_palette["fg"], 0.12),
|
||||||
|
"patch_focus": theme_palette["focus"],
|
||||||
"xy_series": xy_series,
|
"xy_series": xy_series,
|
||||||
"d65_mark": d65_mark,
|
"d65_mark": d65_mark,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _xyY_to_rgb_balance(x: float, y: float, big_y: float) -> tuple[float, float, float]:
|
def _xyY_to_rgb_balance(x: float, y: float, big_y: float) -> tuple[float, float, float]:
|
||||||
"""把 xyY 近似映射到 RGB 比例,并归一到平均值 100。"""
|
"""按 D65 同亮度参考计算 RGB Balance(Calman 常见口径)。"""
|
||||||
if y <= 0 or big_y <= 0:
|
if y <= 0 or big_y <= 0:
|
||||||
return float("nan"), float("nan"), float("nan")
|
return float("nan"), float("nan"), float("nan")
|
||||||
|
|
||||||
big_x = (x * big_y) / y
|
def _xyY_to_xyz(cx: float, cy: float, cy_big: float) -> tuple[float, float, float]:
|
||||||
big_z = ((1.0 - x - y) * big_y) / y
|
if cy <= 0:
|
||||||
|
return float("nan"), float("nan"), float("nan")
|
||||||
|
cx_big = (cx * cy_big) / cy
|
||||||
|
cz_big = ((1.0 - cx - cy) * cy_big) / cy
|
||||||
|
return cx_big, cy_big, cz_big
|
||||||
|
|
||||||
r = (3.2406 * big_x) + (-1.5372 * big_y) + (-0.4986 * big_z)
|
def _xyz_to_linear_rgb(cx_big: float, cy_big: float, cz_big: float) -> tuple[float, float, float]:
|
||||||
g = (-0.9689 * big_x) + (1.8758 * big_y) + (0.0415 * big_z)
|
rr = (3.2406 * cx_big) + (-1.5372 * cy_big) + (-0.4986 * cz_big)
|
||||||
b = (0.0557 * big_x) + (-0.2040 * big_y) + (1.0570 * big_z)
|
gg = (-0.9689 * cx_big) + (1.8758 * cy_big) + (0.0415 * cz_big)
|
||||||
|
bb = (0.0557 * cx_big) + (-0.2040 * cy_big) + (1.0570 * cz_big)
|
||||||
|
return rr, gg, bb
|
||||||
|
|
||||||
r = max(r, 0.0)
|
mx, my, mz = _xyY_to_xyz(x, y, big_y)
|
||||||
g = max(g, 0.0)
|
tx, ty, tz = _xyY_to_xyz(D65_X, D65_Y, big_y)
|
||||||
b = max(b, 0.0)
|
mr, mg, mb = _xyz_to_linear_rgb(mx, my, mz)
|
||||||
|
tr, tg, tb = _xyz_to_linear_rgb(tx, ty, tz)
|
||||||
|
|
||||||
avg = (r + g + b) / 3.0
|
eps = 1e-9
|
||||||
if avg <= 0:
|
if tr <= eps or tg <= eps or tb <= eps:
|
||||||
return float("nan"), float("nan"), float("nan")
|
return float("nan"), float("nan"), float("nan")
|
||||||
return (r / avg) * 100.0, (g / avg) * 100.0, (b / avg) * 100.0
|
|
||||||
|
rr = (mr / tr) * 100.0
|
||||||
|
gg = (mg / tg) * 100.0
|
||||||
|
bb = (mb / tb) * 100.0
|
||||||
|
|
||||||
|
# 明显异常值视为无效,避免图表被离群点拉坏。
|
||||||
|
if not (math.isfinite(rr) and math.isfinite(gg) and math.isfinite(bb)):
|
||||||
|
return float("nan"), float("nan"), float("nan")
|
||||||
|
if rr < 0 or gg < 0 or bb < 0:
|
||||||
|
return float("nan"), float("nan"), float("nan")
|
||||||
|
|
||||||
|
return rr, gg, bb
|
||||||
|
|
||||||
|
|
||||||
|
def _target_gamma_loglog_curve(pct: int) -> float:
|
||||||
|
"""Calman风格目标曲线:低灰阶从 1.8 过渡并逐步逼近 2.2。"""
|
||||||
|
if pct <= 0:
|
||||||
|
return 1.8
|
||||||
|
return TARGET_GAMMA - 0.4 * math.exp(-pct / 6.0)
|
||||||
|
|
||||||
|
|
||||||
def _style_axes(self: "PQAutomationApp", ax, title: str) -> None:
|
def _style_axes(self: "PQAutomationApp", ax, title: str) -> None:
|
||||||
@@ -232,6 +258,62 @@ def _apply_calman_tree_style(self: "PQAutomationApp") -> None:
|
|||||||
self.calman_data_tree.configure(style="Calman.Treeview")
|
self.calman_data_tree.configure(style="Calman.Treeview")
|
||||||
|
|
||||||
|
|
||||||
|
def _calman_log(self: "PQAutomationApp", message: str, level: str = "info") -> None:
|
||||||
|
"""统一输出 Calman 面板日志。"""
|
||||||
|
logger = getattr(self, "log_gui", None)
|
||||||
|
if logger is None:
|
||||||
|
return
|
||||||
|
self._dispatch_ui(self.log_gui.log, f"CALMAN: {message}", level)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_calman_config_summary(self: "PQAutomationApp") -> str:
|
||||||
|
"""生成顶部配置摘要,跟随当前测试类型展示 UCD 参数。"""
|
||||||
|
cfg = getattr(self, "config", None)
|
||||||
|
test_type = getattr(cfg, "current_test_type", "screen_module")
|
||||||
|
test_cfg = {}
|
||||||
|
if cfg is not None:
|
||||||
|
test_cfg = getattr(cfg, "current_test_types", {}).get(test_type, {})
|
||||||
|
|
||||||
|
if test_type == "screen_module":
|
||||||
|
color_space = getattr(getattr(self, "screen_module_color_space_var", None), "get", lambda: test_cfg.get("colorimetry", "-"))()
|
||||||
|
output_format = getattr(getattr(self, "screen_module_output_format_var", None), "get", lambda: test_cfg.get("color_format", "-"))()
|
||||||
|
bit_depth = getattr(getattr(self, "screen_module_bit_depth_var", None), "get", lambda: f"{int(test_cfg.get('bpc', 8))}bit")()
|
||||||
|
data_range = getattr(getattr(self, "screen_module_data_range_var", None), "get", lambda: test_cfg.get("data_range", "Full"))()
|
||||||
|
timing = test_cfg.get("timing", "-")
|
||||||
|
profile_name = "Screen"
|
||||||
|
elif test_type == "sdr_movie":
|
||||||
|
color_space = getattr(getattr(self, "sdr_color_space_var", None), "get", lambda: test_cfg.get("colorimetry", "-"))()
|
||||||
|
output_format = getattr(getattr(self, "sdr_output_format_var", None), "get", lambda: test_cfg.get("color_format", "-"))()
|
||||||
|
bit_depth = getattr(getattr(self, "sdr_bit_depth_var", None), "get", lambda: f"{int(test_cfg.get('bpc', 8))}bit")()
|
||||||
|
data_range = getattr(getattr(self, "sdr_data_range_var", None), "get", lambda: test_cfg.get("data_range", "Full"))()
|
||||||
|
timing = test_cfg.get("timing", "-")
|
||||||
|
profile_name = "SDR"
|
||||||
|
elif test_type == "hdr_movie":
|
||||||
|
color_space = getattr(getattr(self, "hdr_color_space_var", None), "get", lambda: test_cfg.get("colorimetry", "-"))()
|
||||||
|
output_format = getattr(getattr(self, "hdr_output_format_var", None), "get", lambda: test_cfg.get("color_format", "-"))()
|
||||||
|
bit_depth = getattr(getattr(self, "hdr_bit_depth_var", None), "get", lambda: f"{int(test_cfg.get('bpc', 10))}bit")()
|
||||||
|
data_range = getattr(getattr(self, "hdr_data_range_var", None), "get", lambda: test_cfg.get("data_range", "Limited"))()
|
||||||
|
timing = test_cfg.get("timing", "-")
|
||||||
|
profile_name = "HDR"
|
||||||
|
else:
|
||||||
|
color_space = test_cfg.get("colorimetry", "-")
|
||||||
|
output_format = test_cfg.get("color_format", "-")
|
||||||
|
bit_depth = test_cfg.get("bpc", "-")
|
||||||
|
data_range = test_cfg.get("data_range", "-")
|
||||||
|
timing = test_cfg.get("timing", "-")
|
||||||
|
profile_name = test_type
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"Profile: {profile_name} | Timing: {timing} | CS: {color_space} | "
|
||||||
|
f"Fmt: {output_format} | Depth: {bit_depth} | Range: {data_range}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _refresh_calman_config_summary(self: "PQAutomationApp") -> None:
|
||||||
|
if hasattr(self, "calman_config_summary_var"):
|
||||||
|
self.calman_config_summary_var.set(_build_calman_config_summary(self))
|
||||||
|
|
||||||
|
|
||||||
def create_calman_panel(self: "PQAutomationApp") -> None:
|
def create_calman_panel(self: "PQAutomationApp") -> None:
|
||||||
"""创建 CALMAN 风格灰阶测试面板,注册到 panel_manager。"""
|
"""创建 CALMAN 风格灰阶测试面板,注册到 panel_manager。"""
|
||||||
palette = _get_calman_palette()
|
palette = _get_calman_palette()
|
||||||
@@ -242,6 +324,7 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
|
|||||||
self.calman_results = {}
|
self.calman_results = {}
|
||||||
self.calman_stop_event = threading.Event()
|
self.calman_stop_event = threading.Event()
|
||||||
self.calman_running = False
|
self.calman_running = False
|
||||||
|
self.calman_patch_send_busy = False
|
||||||
self.calman_current_level = None
|
self.calman_current_level = None
|
||||||
self.calman_last_record = None
|
self.calman_last_record = None
|
||||||
self.calman_last_step_seconds = None
|
self.calman_last_step_seconds = None
|
||||||
@@ -298,6 +381,15 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
|
|||||||
)
|
)
|
||||||
self.calman_elapsed_label.pack(side=tk.LEFT)
|
self.calman_elapsed_label.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
self.calman_config_summary_var = tk.StringVar(value="")
|
||||||
|
self.calman_config_summary_label = ttk.Label(
|
||||||
|
control_row,
|
||||||
|
textvariable=self.calman_config_summary_var,
|
||||||
|
foreground=palette["status_fg"],
|
||||||
|
anchor=tk.W,
|
||||||
|
)
|
||||||
|
self.calman_config_summary_label.pack(side=tk.LEFT, padx=(12, 0), fill=tk.X, expand=True)
|
||||||
|
|
||||||
metrics_row = ttk.Frame(chart_frame)
|
metrics_row = ttk.Frame(chart_frame)
|
||||||
metrics_row.grid(row=2, column=0, sticky=tk.EW, pady=(2, 0))
|
metrics_row.grid(row=2, column=0, sticky=tk.EW, pady=(2, 0))
|
||||||
metrics_row.columnconfigure((0, 1, 2, 3), weight=1)
|
metrics_row.columnconfigure((0, 1, 2, 3), weight=1)
|
||||||
@@ -414,6 +506,7 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
|
|||||||
self.calman_actual_patch_cells = []
|
self.calman_actual_patch_cells = []
|
||||||
self.calman_target_patch_canvases = []
|
self.calman_target_patch_canvases = []
|
||||||
self.calman_target_hexes = []
|
self.calman_target_hexes = []
|
||||||
|
patch_palette = _get_calman_palette()
|
||||||
for idx, pct in enumerate(self.calman_levels):
|
for idx, pct in enumerate(self.calman_levels):
|
||||||
rgb = _pct_to_gray_rgb(pct)
|
rgb = _pct_to_gray_rgb(pct)
|
||||||
color = _rgb_to_hex(rgb)
|
color = _rgb_to_hex(rgb)
|
||||||
@@ -427,7 +520,7 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
|
|||||||
bd=1,
|
bd=1,
|
||||||
relief="solid",
|
relief="solid",
|
||||||
highlightthickness=1,
|
highlightthickness=1,
|
||||||
highlightbackground="#808080",
|
highlightbackground=patch_palette["patch_border_alt"],
|
||||||
cursor="hand2",
|
cursor="hand2",
|
||||||
)
|
)
|
||||||
actual_cell.grid(row=0, column=idx, padx=1, pady=1, sticky=tk.NSEW)
|
actual_cell.grid(row=0, column=idx, padx=1, pady=1, sticky=tk.NSEW)
|
||||||
@@ -454,7 +547,7 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
|
|||||||
bd=1,
|
bd=1,
|
||||||
relief="solid",
|
relief="solid",
|
||||||
highlightthickness=1,
|
highlightthickness=1,
|
||||||
highlightbackground="#9c9c9c",
|
highlightbackground=patch_palette["patch_border"],
|
||||||
cursor="hand2",
|
cursor="hand2",
|
||||||
)
|
)
|
||||||
cell.grid(row=1, column=idx, padx=1, pady=1, sticky=tk.NSEW)
|
cell.grid(row=1, column=idx, padx=1, pady=1, sticky=tk.NSEW)
|
||||||
@@ -478,7 +571,13 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _bind_click(widget, p=pct):
|
def _bind_click(widget, p=pct):
|
||||||
widget.bind("<Button-1>", lambda _e, pp=p: send_patch(self, pp))
|
def _on_click(_e, pp=p):
|
||||||
|
send_patch(self, pp)
|
||||||
|
# Prevent event bubbling from canvas -> parent cell, which would
|
||||||
|
# otherwise trigger duplicated sends for a single click.
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
widget.bind("<Button-1>", _on_click)
|
||||||
|
|
||||||
for w in (cell, target_canvas):
|
for w in (cell, target_canvas):
|
||||||
_bind_click(w)
|
_bind_click(w)
|
||||||
@@ -581,6 +680,7 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
|
|||||||
right.bind("<Configure>", lambda _e: _adaptive_matrix_columns(self))
|
right.bind("<Configure>", lambda _e: _adaptive_matrix_columns(self))
|
||||||
|
|
||||||
_refresh_metric_table(self)
|
_refresh_metric_table(self)
|
||||||
|
_refresh_calman_config_summary(self)
|
||||||
_update_target_strip(self)
|
_update_target_strip(self)
|
||||||
_update_actual_strip(self)
|
_update_actual_strip(self)
|
||||||
_redraw_calman_charts(self)
|
_redraw_calman_charts(self)
|
||||||
@@ -592,6 +692,7 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
|
|||||||
def toggle_calman_panel(self: "PQAutomationApp") -> None:
|
def toggle_calman_panel(self: "PQAutomationApp") -> None:
|
||||||
"""切换 CALMAN 灰阶面板显示。"""
|
"""切换 CALMAN 灰阶面板显示。"""
|
||||||
self.show_panel("calman")
|
self.show_panel("calman")
|
||||||
|
_refresh_calman_config_summary(self)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -604,23 +705,43 @@ def send_patch(self: "PQAutomationApp", pct: int) -> None:
|
|||||||
if not self.signal_service.is_connected:
|
if not self.signal_service.is_connected:
|
||||||
messagebox.showwarning("提示", "请先连接 UCD323 设备")
|
messagebox.showwarning("提示", "请先连接 UCD323 设备")
|
||||||
return
|
return
|
||||||
|
if getattr(self, "calman_patch_send_busy", False):
|
||||||
|
_calman_log(self, f"send busy, ignore click pct={pct}", "warning")
|
||||||
|
self.calman_status_var.set("发送进行中,请稍候...")
|
||||||
|
return
|
||||||
|
|
||||||
rgb_val = int(round(pct * 255 / 100))
|
rgb_val = int(round(pct * 255 / 100))
|
||||||
self.calman_current_level = pct
|
self.calman_current_level = pct
|
||||||
self.calman_status_var.set(f"发送 {pct}%(RGB={rgb_val})...")
|
self.calman_status_var.set(f"发送 {pct}%(RGB={rgb_val})...")
|
||||||
_highlight_patch(self, pct)
|
_highlight_patch(self, pct)
|
||||||
|
_refresh_calman_config_summary(self)
|
||||||
|
_calman_log(self, f"click patch pct={pct}, rgb=({rgb_val}, {rgb_val}, {rgb_val})")
|
||||||
|
self.calman_patch_send_busy = True
|
||||||
|
|
||||||
def worker():
|
def worker():
|
||||||
try:
|
try:
|
||||||
self.signal_service.send_solid_rgb((rgb_val, rgb_val, rgb_val))
|
_calman_log(self, f"send_solid_rgb start pct={pct}")
|
||||||
|
test_type = getattr(self.config, "current_test_type", "screen_module")
|
||||||
|
if hasattr(self, "pattern_service") and self.pattern_service is not None:
|
||||||
|
self.pattern_service.send_rgb(
|
||||||
|
(rgb_val, rgb_val, rgb_val),
|
||||||
|
test_type=test_type,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.signal_service.send_solid_rgb((rgb_val, rgb_val, rgb_val))
|
||||||
|
_calman_log(self, f"send_solid_rgb success pct={pct}")
|
||||||
|
_calman_log(self, f"ucd profile applied test_type={test_type}")
|
||||||
self._dispatch_ui(
|
self._dispatch_ui(
|
||||||
self.log_gui.log, f"CALMAN: 已发送 {pct}% 灰阶 (RGB={rgb_val})",
|
self.log_gui.log, f"CALMAN: 已发送 {pct}% 灰阶 (RGB={rgb_val})",
|
||||||
"info",
|
"info",
|
||||||
)
|
)
|
||||||
self._dispatch_ui(self.calman_status_var.set, f"{pct}% 已发送")
|
self._dispatch_ui(self.calman_status_var.set, f"{pct}% 已发送")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
_calman_log(self, f"send_solid_rgb failed pct={pct}: {exc}", "error")
|
||||||
self._dispatch_ui(self.log_gui.log, f"发送失败: {exc}", "error")
|
self._dispatch_ui(self.log_gui.log, f"发送失败: {exc}", "error")
|
||||||
self._dispatch_ui(self.calman_status_var.set, f"发送失败: {exc}")
|
self._dispatch_ui(self.calman_status_var.set, f"发送失败: {exc}")
|
||||||
|
finally:
|
||||||
|
self.calman_patch_send_busy = False
|
||||||
|
|
||||||
threading.Thread(target=worker, daemon=True).start()
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
|
||||||
@@ -628,7 +749,7 @@ def send_patch(self: "PQAutomationApp", pct: int) -> None:
|
|||||||
def _measure_once(self: "PQAutomationApp", pct: int) -> dict | None:
|
def _measure_once(self: "PQAutomationApp", pct: int) -> dict | None:
|
||||||
"""采集一次 CA410,并组装一条记录(含 CCT/Gamma/ΔE2000)。"""
|
"""采集一次 CA410,并组装一条记录(含 CCT/Gamma/ΔE2000)。"""
|
||||||
try:
|
try:
|
||||||
x, y, lv, X, Y, Z = self.ca.readAllDisplay()
|
x, y, lv, X, Y, Z = self.read_ca_xyLv()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._dispatch_ui(self.log_gui.log, f"CA410 采集异常: {exc}", "error")
|
self._dispatch_ui(self.log_gui.log, f"CA410 采集异常: {exc}", "error")
|
||||||
return None
|
return None
|
||||||
@@ -693,14 +814,32 @@ def measure_current_patch(self: "PQAutomationApp") -> None:
|
|||||||
|
|
||||||
def worker():
|
def worker():
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
|
_calman_log(self, f"measure start pct={pct}")
|
||||||
self._dispatch_ui(self.calman_status_var.set, f"采集 {pct}% 中...")
|
self._dispatch_ui(self.calman_status_var.set, f"采集 {pct}% 中...")
|
||||||
rec = _measure_once(self, pct)
|
rec = _measure_once(self, pct)
|
||||||
if rec is None:
|
if rec is None:
|
||||||
|
_calman_log(self, f"measure failed pct={pct}", "error")
|
||||||
self._dispatch_ui(self.calman_status_var.set, "采集失败")
|
self._dispatch_ui(self.calman_status_var.set, "采集失败")
|
||||||
return
|
return
|
||||||
step_s = time.perf_counter() - t0
|
step_s = time.perf_counter() - t0
|
||||||
self.calman_last_step_seconds = step_s
|
self.calman_last_step_seconds = step_s
|
||||||
self.calman_results[pct] = rec
|
self.calman_results[pct] = rec
|
||||||
|
_calman_log(
|
||||||
|
self,
|
||||||
|
(
|
||||||
|
"measure success pct={pct}, x={x:.4f}, y={y:.4f}, Y={Y:.3f}, "
|
||||||
|
"cct={cct:.0f}, gamma={gamma:.3f}, de2000={de:.3f}, step={step:.2f}s"
|
||||||
|
).format(
|
||||||
|
pct=pct,
|
||||||
|
x=rec["x"],
|
||||||
|
y=rec["y"],
|
||||||
|
Y=rec["Y"],
|
||||||
|
cct=rec["cct"] if rec["cct"] == rec["cct"] else float("nan"),
|
||||||
|
gamma=rec["gamma"] if rec["gamma"] == rec["gamma"] else float("nan"),
|
||||||
|
de=rec["de2000"] if rec["de2000"] == rec["de2000"] else float("nan"),
|
||||||
|
step=step_s,
|
||||||
|
),
|
||||||
|
)
|
||||||
self._dispatch_ui(_apply_record_to_ui, self, rec)
|
self._dispatch_ui(_apply_record_to_ui, self, rec)
|
||||||
self._dispatch_ui(
|
self._dispatch_ui(
|
||||||
self.calman_status_var.set, f"{pct}% 采集完成 ({step_s:.2f}s)"
|
self.calman_status_var.set, f"{pct}% 采集完成 ({step_s:.2f}s)"
|
||||||
@@ -726,15 +865,28 @@ def start_sequence_test(self: "PQAutomationApp") -> None:
|
|||||||
settle = float(getattr(self, "pattern_settle_time", 0.4))
|
settle = float(getattr(self, "pattern_settle_time", 0.4))
|
||||||
self.calman_progress["value"] = 0
|
self.calman_progress["value"] = 0
|
||||||
self.calman_progress_var.set("0 / 0")
|
self.calman_progress_var.set("0 / 0")
|
||||||
|
_refresh_calman_config_summary(self)
|
||||||
|
_calman_log(self, f"sequence start levels={len(self.calman_levels)}, settle={settle:.2f}s")
|
||||||
|
|
||||||
def worker():
|
def worker():
|
||||||
seq_t0 = time.perf_counter()
|
seq_t0 = time.perf_counter()
|
||||||
try:
|
try:
|
||||||
|
test_type = getattr(self.config, "current_test_type", "screen_module")
|
||||||
|
rgb_session = None
|
||||||
|
if hasattr(self, "pattern_service") and self.pattern_service is not None:
|
||||||
|
rgb_session = self.pattern_service.prepare_session(
|
||||||
|
"rgb",
|
||||||
|
test_type=test_type,
|
||||||
|
log_details=False,
|
||||||
|
)
|
||||||
|
_calman_log(self, f"sequence ucd profile applied test_type={test_type}")
|
||||||
|
|
||||||
order = sorted(self.calman_levels, reverse=True)
|
order = sorted(self.calman_levels, reverse=True)
|
||||||
total = len(order)
|
total = len(order)
|
||||||
self._dispatch_ui(self.calman_progress_var.set, f"0 / {total}")
|
self._dispatch_ui(self.calman_progress_var.set, f"0 / {total}")
|
||||||
for i, pct in enumerate(order, 1):
|
for i, pct in enumerate(order, 1):
|
||||||
if self.calman_stop_event.is_set():
|
if self.calman_stop_event.is_set():
|
||||||
|
_calman_log(self, f"sequence stop requested at step={i-1}/{total}", "warning")
|
||||||
self._dispatch_ui(self.log_gui.log, "已停止连续测试", "warning")
|
self._dispatch_ui(self.log_gui.log, "已停止连续测试", "warning")
|
||||||
break
|
break
|
||||||
step_t0 = time.perf_counter()
|
step_t0 = time.perf_counter()
|
||||||
@@ -742,12 +894,20 @@ def start_sequence_test(self: "PQAutomationApp") -> None:
|
|||||||
self._dispatch_ui(
|
self._dispatch_ui(
|
||||||
self.calman_status_var.set, f"[{i}/{total}] 发送 {pct}%"
|
self.calman_status_var.set, f"[{i}/{total}] 发送 {pct}%"
|
||||||
)
|
)
|
||||||
|
_calman_log(self, f"sequence send step={i}/{total}, pct={pct}, rgb={rgb_val}")
|
||||||
self._dispatch_ui(_highlight_patch, self, pct)
|
self._dispatch_ui(_highlight_patch, self, pct)
|
||||||
try:
|
try:
|
||||||
self.signal_service.send_solid_rgb(
|
if rgb_session is not None:
|
||||||
(rgb_val, rgb_val, rgb_val)
|
self.pattern_service.send_rgb(
|
||||||
)
|
(rgb_val, rgb_val, rgb_val),
|
||||||
|
session=rgb_session,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.signal_service.send_solid_rgb(
|
||||||
|
(rgb_val, rgb_val, rgb_val)
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
_calman_log(self, f"sequence send failed step={i}/{total}, pct={pct}: {exc}", "error")
|
||||||
self._dispatch_ui(
|
self._dispatch_ui(
|
||||||
self.log_gui.log, f"发送 {pct}% 失败: {exc}", "error"
|
self.log_gui.log, f"发送 {pct}% 失败: {exc}", "error"
|
||||||
)
|
)
|
||||||
@@ -755,11 +915,30 @@ def start_sequence_test(self: "PQAutomationApp") -> None:
|
|||||||
self.calman_current_level = pct
|
self.calman_current_level = pct
|
||||||
# 等待稳定,停止事件触发时尽快退出
|
# 等待稳定,停止事件触发时尽快退出
|
||||||
if self.calman_stop_event.wait(settle):
|
if self.calman_stop_event.wait(settle):
|
||||||
|
_calman_log(self, f"sequence interrupted during settle step={i}/{total}, pct={pct}", "warning")
|
||||||
break
|
break
|
||||||
rec = _measure_once(self, pct)
|
rec = _measure_once(self, pct)
|
||||||
if rec is None:
|
if rec is None:
|
||||||
|
_calman_log(self, f"sequence measure failed step={i}/{total}, pct={pct}", "error")
|
||||||
continue
|
continue
|
||||||
self.calman_results[pct] = rec
|
self.calman_results[pct] = rec
|
||||||
|
_calman_log(
|
||||||
|
self,
|
||||||
|
(
|
||||||
|
"sequence measure step={i}/{total}, pct={pct}, x={x:.4f}, y={y:.4f}, Y={Y:.3f}, "
|
||||||
|
"cct={cct:.0f}, gamma={gamma:.3f}, de2000={de:.3f}"
|
||||||
|
).format(
|
||||||
|
i=i,
|
||||||
|
total=total,
|
||||||
|
pct=pct,
|
||||||
|
x=rec["x"],
|
||||||
|
y=rec["y"],
|
||||||
|
Y=rec["Y"],
|
||||||
|
cct=rec["cct"] if rec["cct"] == rec["cct"] else float("nan"),
|
||||||
|
gamma=rec["gamma"] if rec["gamma"] == rec["gamma"] else float("nan"),
|
||||||
|
de=rec["de2000"] if rec["de2000"] == rec["de2000"] else float("nan"),
|
||||||
|
),
|
||||||
|
)
|
||||||
self._dispatch_ui(_apply_record_to_ui, self, rec)
|
self._dispatch_ui(_apply_record_to_ui, self, rec)
|
||||||
step_s = time.perf_counter() - step_t0
|
step_s = time.perf_counter() - step_t0
|
||||||
total_s = time.perf_counter() - seq_t0
|
total_s = time.perf_counter() - seq_t0
|
||||||
@@ -772,9 +951,11 @@ def start_sequence_test(self: "PQAutomationApp") -> None:
|
|||||||
total_s,
|
total_s,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
_calman_log(self, f"sequence complete total={total}")
|
||||||
self._dispatch_ui(self.calman_status_var.set, "连续测试完成")
|
self._dispatch_ui(self.calman_status_var.set, "连续测试完成")
|
||||||
self._dispatch_ui(self.log_gui.log, "CALMAN: 连续测试完成", "success")
|
self._dispatch_ui(self.log_gui.log, "CALMAN: 连续测试完成", "success")
|
||||||
return
|
return
|
||||||
|
_calman_log(self, "sequence stopped", "warning")
|
||||||
self._dispatch_ui(self.calman_status_var.set, "已停止")
|
self._dispatch_ui(self.calman_status_var.set, "已停止")
|
||||||
finally:
|
finally:
|
||||||
self.calman_running = False
|
self.calman_running = False
|
||||||
@@ -785,14 +966,17 @@ def start_sequence_test(self: "PQAutomationApp") -> None:
|
|||||||
def stop_sequence_test(self: "PQAutomationApp") -> None:
|
def stop_sequence_test(self: "PQAutomationApp") -> None:
|
||||||
"""请求停止连续测试。"""
|
"""请求停止连续测试。"""
|
||||||
if self.calman_running:
|
if self.calman_running:
|
||||||
|
_calman_log(self, "stop requested", "warning")
|
||||||
self.calman_stop_event.set()
|
self.calman_stop_event.set()
|
||||||
self.calman_status_var.set("正在停止...")
|
self.calman_status_var.set("正在停止...")
|
||||||
else:
|
else:
|
||||||
|
_calman_log(self, "stop requested but no sequence is running", "warning")
|
||||||
self.calman_status_var.set("当前没有运行中的连续测试")
|
self.calman_status_var.set("当前没有运行中的连续测试")
|
||||||
|
|
||||||
|
|
||||||
def clear_results(self: "PQAutomationApp") -> None:
|
def clear_results(self: "PQAutomationApp") -> None:
|
||||||
"""清空结果表和图表。"""
|
"""清空结果表和图表。"""
|
||||||
|
_calman_log(self, "clear results")
|
||||||
self.calman_results.clear()
|
self.calman_results.clear()
|
||||||
self.calman_last_record = None
|
self.calman_last_record = None
|
||||||
self.calman_reading_var.set(
|
self.calman_reading_var.set(
|
||||||
@@ -817,20 +1001,21 @@ def clear_results(self: "PQAutomationApp") -> None:
|
|||||||
|
|
||||||
def _highlight_patch(self: "PQAutomationApp", pct: int) -> None:
|
def _highlight_patch(self: "PQAutomationApp", pct: int) -> None:
|
||||||
"""高亮当前选中色块。"""
|
"""高亮当前选中色块。"""
|
||||||
|
palette = _get_calman_palette()
|
||||||
try:
|
try:
|
||||||
idx = self.calman_levels.index(pct)
|
idx = self.calman_levels.index(pct)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return
|
return
|
||||||
for i, cell in enumerate(self.calman_patch_cells):
|
for i, cell in enumerate(self.calman_patch_cells):
|
||||||
if i == idx:
|
if i == idx:
|
||||||
cell.configure(highlightbackground="#1f6fb2", highlightthickness=2)
|
cell.configure(highlightbackground=palette["patch_focus"], highlightthickness=2)
|
||||||
else:
|
else:
|
||||||
cell.configure(highlightbackground="#9c9c9c", highlightthickness=1)
|
cell.configure(highlightbackground=palette["patch_border"], highlightthickness=1)
|
||||||
for i, cell in enumerate(self.calman_actual_cells):
|
for i, cell in enumerate(self.calman_actual_cells):
|
||||||
if i == idx:
|
if i == idx:
|
||||||
cell.configure(highlightbackground="#1f6fb2", highlightthickness=2)
|
cell.configure(highlightbackground=palette["patch_focus"], highlightthickness=2)
|
||||||
else:
|
else:
|
||||||
cell.configure(highlightbackground="#808080", highlightthickness=1)
|
cell.configure(highlightbackground=palette["patch_border_alt"], highlightthickness=1)
|
||||||
|
|
||||||
total_cols = len(self.calman_levels) + 1 # 含 metric 列
|
total_cols = len(self.calman_levels) + 1 # 含 metric 列
|
||||||
col_index = idx + 1
|
col_index = idx + 1
|
||||||
@@ -925,10 +1110,18 @@ def _redraw_calman_charts(self: "PQAutomationApp") -> None:
|
|||||||
pcts = [r["pct"] for r in recs]
|
pcts = [r["pct"] for r in recs]
|
||||||
de_vals = [r["de2000"] if r["de2000"] == r["de2000"] else 0 for r in recs]
|
de_vals = [r["de2000"] if r["de2000"] == r["de2000"] else 0 for r in recs]
|
||||||
lum_vals = [r["Y"] if r["Y"] == r["Y"] else 0 for r in recs]
|
lum_vals = [r["Y"] if r["Y"] == r["Y"] else 0 for r in recs]
|
||||||
rgb_r = [r["rgb_r"] for r in recs if r["rgb_r"] == r["rgb_r"]]
|
rgb_recs = [
|
||||||
rgb_g = [r["rgb_g"] for r in recs if r["rgb_g"] == r["rgb_g"]]
|
r for r in recs
|
||||||
rgb_b = [r["rgb_b"] for r in recs if r["rgb_b"] == r["rgb_b"]]
|
if (
|
||||||
rgb_pcts = [r["pct"] for r in recs if r["rgb_r"] == r["rgb_r"]]
|
r.get("rgb_r") == r.get("rgb_r")
|
||||||
|
and r.get("rgb_g") == r.get("rgb_g")
|
||||||
|
and r.get("rgb_b") == r.get("rgb_b")
|
||||||
|
)
|
||||||
|
]
|
||||||
|
rgb_pcts = [r["pct"] for r in rgb_recs]
|
||||||
|
rgb_r = [r["rgb_r"] for r in rgb_recs]
|
||||||
|
rgb_g = [r["rgb_g"] for r in rgb_recs]
|
||||||
|
rgb_b = [r["rgb_b"] for r in rgb_recs]
|
||||||
gamma_pcts = [r["pct"] for r in recs if r["gamma"] == r["gamma"]]
|
gamma_pcts = [r["pct"] for r in recs if r["gamma"] == r["gamma"]]
|
||||||
gamma_vals = [r["gamma"] for r in recs if r["gamma"] == r["gamma"]]
|
gamma_vals = [r["gamma"] for r in recs if r["gamma"] == r["gamma"]]
|
||||||
cct_vals = [r["cct"] for r in recs if r["cct"] == r["cct"]]
|
cct_vals = [r["cct"] for r in recs if r["cct"] == r["cct"]]
|
||||||
@@ -966,6 +1159,16 @@ def _redraw_calman_charts(self: "PQAutomationApp") -> None:
|
|||||||
a1.set_ylim(bottom=0)
|
a1.set_ylim(bottom=0)
|
||||||
a1.set_xlabel("", fontsize=8)
|
a1.set_xlabel("", fontsize=8)
|
||||||
|
|
||||||
|
rgb_ylim_low = 95.0
|
||||||
|
rgb_ylim_high = 105.0
|
||||||
|
if rgb_recs:
|
||||||
|
rgb_values = rgb_r + rgb_g + rgb_b
|
||||||
|
rgb_min = min(rgb_values + [100.0])
|
||||||
|
rgb_max = max(rgb_values + [100.0])
|
||||||
|
pad = max(0.8, (rgb_max - rgb_min) * 0.15)
|
||||||
|
rgb_ylim_low = min(95.0, rgb_min - pad)
|
||||||
|
rgb_ylim_high = max(105.0, rgb_max + pad)
|
||||||
|
|
||||||
# RGB Balance 线图
|
# RGB Balance 线图
|
||||||
a2 = self.calman_ax_rgb_line
|
a2 = self.calman_ax_rgb_line
|
||||||
a2.clear()
|
a2.clear()
|
||||||
@@ -976,36 +1179,38 @@ def _redraw_calman_charts(self: "PQAutomationApp") -> None:
|
|||||||
a2.plot(rgb_pcts, rgb_b, "-", color="#4a73ff", linewidth=1.2)
|
a2.plot(rgb_pcts, rgb_b, "-", color="#4a73ff", linewidth=1.2)
|
||||||
a2.axhline(100, color="#9e9e9e", lw=0.8, ls="--")
|
a2.axhline(100, color="#9e9e9e", lw=0.8, ls="--")
|
||||||
a2.set_xlim(-2, 102)
|
a2.set_xlim(-2, 102)
|
||||||
a2.set_ylim(95, 105)
|
a2.set_ylim(rgb_ylim_low, rgb_ylim_high)
|
||||||
a2.set_xlabel("", fontsize=8)
|
a2.set_xlabel("", fontsize=8)
|
||||||
|
|
||||||
# RGB Balance 条图(用最后一个点)
|
# RGB Balance 条图(用最后一个点)
|
||||||
a3 = self.calman_ax_rgb_bar
|
a3 = self.calman_ax_rgb_bar
|
||||||
a3.clear()
|
a3.clear()
|
||||||
_style_axes(self, a3, "RGB Balance")
|
_style_axes(self, a3, "RGB Balance")
|
||||||
if recs:
|
if rgb_recs:
|
||||||
last = recs[-1]
|
last = rgb_recs[-1]
|
||||||
bars = [
|
bars = [
|
||||||
last["rgb_r"] if last["rgb_r"] == last["rgb_r"] else 100,
|
last["rgb_r"],
|
||||||
last["rgb_g"] if last["rgb_g"] == last["rgb_g"] else 100,
|
last["rgb_g"],
|
||||||
last["rgb_b"] if last["rgb_b"] == last["rgb_b"] else 100,
|
last["rgb_b"],
|
||||||
]
|
]
|
||||||
a3.bar([0, 1, 2], bars, color=["#ff4d4d", "#4caf50", "#4a73ff"], width=0.7)
|
a3.bar([0, 1, 2], bars, color=["#ff4d4d", "#4caf50", "#4a73ff"], width=0.7)
|
||||||
a3.set_xticks([0, 1, 2], ["R", "G", "B"])
|
a3.set_xticks([0, 1, 2], ["R", "G", "B"])
|
||||||
else:
|
else:
|
||||||
a3.set_xticks([0, 1, 2], ["R", "G", "B"])
|
a3.set_xticks([0, 1, 2], ["R", "G", "B"])
|
||||||
a3.set_ylim(95, 105)
|
a3.set_ylim(rgb_ylim_low, rgb_ylim_high)
|
||||||
a3.set_xlabel("", fontsize=8)
|
a3.set_xlabel("", fontsize=8)
|
||||||
|
|
||||||
# Gamma
|
# Gamma
|
||||||
a4 = self.calman_ax_gamma
|
a4 = self.calman_ax_gamma
|
||||||
a4.clear()
|
a4.clear()
|
||||||
_style_axes(self, a4, "Gamma Log/Log")
|
_style_axes(self, a4, "Gamma Log/Log")
|
||||||
|
target_pcts = list(self.calman_levels)
|
||||||
|
target_vals = [_target_gamma_loglog_curve(p) for p in target_pcts]
|
||||||
|
a4.plot(target_pcts, target_vals, "-", color="#f4ff00", linewidth=1.8)
|
||||||
if gamma_pcts:
|
if gamma_pcts:
|
||||||
a4.plot(gamma_pcts, gamma_vals, "-", color="#ffe24d", linewidth=1.3)
|
a4.plot(gamma_pcts, gamma_vals, "-", color="#8f8f8f", linewidth=2.0)
|
||||||
a4.axhline(TARGET_GAMMA, color="#9e9e9e", lw=0.8, ls="--")
|
|
||||||
a4.set_xlim(-2, 102)
|
a4.set_xlim(-2, 102)
|
||||||
a4.set_ylim(1.6, 2.8)
|
a4.set_ylim(1.8, 2.8)
|
||||||
a4.set_xlabel("", fontsize=8)
|
a4.set_xlabel("", fontsize=8)
|
||||||
|
|
||||||
self.calman_canvas.draw_idle()
|
self.calman_canvas.draw_idle()
|
||||||
@@ -1075,14 +1280,20 @@ def _refresh_metric_table(self: "PQAutomationApp") -> None:
|
|||||||
"""重绘下方矩阵表。"""
|
"""重绘下方矩阵表。"""
|
||||||
_apply_calman_tree_style(self)
|
_apply_calman_tree_style(self)
|
||||||
palette = _get_calman_palette()
|
palette = _get_calman_palette()
|
||||||
|
ref_white_y = self.calman_results.get(100, {}).get("Y")
|
||||||
|
|
||||||
|
def _target_y_abs(pctx):
|
||||||
|
if pctx is None:
|
||||||
|
return "-"
|
||||||
|
if ref_white_y is None or ref_white_y != ref_white_y or ref_white_y <= 0:
|
||||||
|
return "-"
|
||||||
|
return _safe_float(ref_white_y * ((pctx / 100.0) ** TARGET_GAMMA), "{:.3f}")
|
||||||
|
|
||||||
metrics = [
|
metrics = [
|
||||||
("x CIE31", lambda r: _safe_float(r.get("x")) if r else "-"),
|
("x CIE31", lambda r: _safe_float(r.get("x")) if r else "-"),
|
||||||
("y CIE31", lambda r: _safe_float(r.get("y")) if r else "-"),
|
("y CIE31", lambda r: _safe_float(r.get("y")) if r else "-"),
|
||||||
("Y", lambda r: _safe_float(r.get("Y"), "{:.3f}") if r else "-"),
|
("Y", lambda r: _safe_float(r.get("Y"), "{:.3f}") if r else "-"),
|
||||||
(
|
("Target Y", lambda _r, pctx=None: _target_y_abs(pctx)),
|
||||||
"Target Y",
|
|
||||||
lambda _r, pctx=None: _safe_float((pctx / 100.0) ** TARGET_GAMMA, "{:.4f}") if pctx and pctx > 0 else ("0.0000" if pctx == 0 else "-"),
|
|
||||||
),
|
|
||||||
("Gamma Log/Log", lambda r: _safe_float(r.get("gamma"), "{:.3f}") if r else "-"),
|
("Gamma Log/Log", lambda r: _safe_float(r.get("gamma"), "{:.3f}") if r else "-"),
|
||||||
("CCT", lambda r: _safe_float(r.get("cct"), "{:.0f}") if r else "-"),
|
("CCT", lambda r: _safe_float(r.get("cct"), "{:.0f}") if r else "-"),
|
||||||
("dE 2000", lambda r: _safe_float(r.get("de2000"), "{:.3f}") if r else "-"),
|
("dE 2000", lambda r: _safe_float(r.get("de2000"), "{:.3f}") if r else "-"),
|
||||||
@@ -1135,6 +1346,8 @@ def refresh_calman_theme(self: "PQAutomationApp") -> None:
|
|||||||
|
|
||||||
if hasattr(self, "calman_elapsed_label"):
|
if hasattr(self, "calman_elapsed_label"):
|
||||||
self.calman_elapsed_label.configure(foreground=palette["status_fg"])
|
self.calman_elapsed_label.configure(foreground=palette["status_fg"])
|
||||||
|
if hasattr(self, "calman_config_summary_label"):
|
||||||
|
self.calman_config_summary_label.configure(foreground=palette["status_fg"])
|
||||||
if hasattr(self, "calman_status_label"):
|
if hasattr(self, "calman_status_label"):
|
||||||
self.calman_status_label.configure(foreground=palette["status_fg"])
|
self.calman_status_label.configure(foreground=palette["status_fg"])
|
||||||
if hasattr(self, "calman_reading_summary_label"):
|
if hasattr(self, "calman_reading_summary_label"):
|
||||||
@@ -1151,6 +1364,7 @@ def refresh_calman_theme(self: "PQAutomationApp") -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
_refresh_metric_table(self)
|
_refresh_metric_table(self)
|
||||||
|
_refresh_calman_config_summary(self)
|
||||||
_redraw_calman_charts(self)
|
_redraw_calman_charts(self)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"):
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import colour
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from app.data_range_converter import convert_pattern_params
|
from app.data_range_converter import convert_pattern_params
|
||||||
|
from app.views.modern_styles import get_theme_palette
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -28,37 +29,12 @@ def create_custom_template_result_panel(self: "PQAutomationApp"):
|
|||||||
|
|
||||||
table_container = tk.Frame(
|
table_container = tk.Frame(
|
||||||
self.custom_result_frame,
|
self.custom_result_frame,
|
||||||
bg="#000000",
|
|
||||||
highlightthickness=1,
|
highlightthickness=1,
|
||||||
highlightbackground="#5a5a5a",
|
|
||||||
)
|
)
|
||||||
table_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
table_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
self.custom_result_table_container = table_container
|
||||||
|
|
||||||
style = ttk.Style()
|
_apply_custom_result_theme(self)
|
||||||
style.configure(
|
|
||||||
"CustomResult.Treeview",
|
|
||||||
background="#000000",
|
|
||||||
fieldbackground="#000000",
|
|
||||||
foreground="#ffffff",
|
|
||||||
rowheight=28,
|
|
||||||
borderwidth=0,
|
|
||||||
)
|
|
||||||
style.configure(
|
|
||||||
"CustomResult.Treeview.Heading",
|
|
||||||
background="#2f2f2f",
|
|
||||||
foreground="#f5f5f5",
|
|
||||||
font=("Microsoft YaHei", 10, "bold"),
|
|
||||||
relief="flat",
|
|
||||||
)
|
|
||||||
style.map(
|
|
||||||
"CustomResult.Treeview",
|
|
||||||
background=[("selected", "#1f4e79")],
|
|
||||||
foreground=[("selected", "#ffffff")],
|
|
||||||
)
|
|
||||||
style.map(
|
|
||||||
"CustomResult.Treeview.Heading",
|
|
||||||
background=[("active", "#3b3b3b")],
|
|
||||||
)
|
|
||||||
|
|
||||||
columns = (
|
columns = (
|
||||||
"Pattern",
|
"Pattern",
|
||||||
@@ -157,6 +133,70 @@ def create_custom_template_result_panel(self: "PQAutomationApp"):
|
|||||||
table_container.grid_columnconfigure(0, weight=1)
|
table_container.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_custom_result_theme(self: "PQAutomationApp"):
|
||||||
|
palette = get_theme_palette()
|
||||||
|
container = getattr(self, "custom_result_table_container", None)
|
||||||
|
if container is not None:
|
||||||
|
container.configure(
|
||||||
|
bg=palette["input_bg"],
|
||||||
|
highlightbackground=palette["border"],
|
||||||
|
highlightcolor=palette["border"],
|
||||||
|
)
|
||||||
|
|
||||||
|
style = ttk.Style()
|
||||||
|
style.configure(
|
||||||
|
"CustomResult.Treeview",
|
||||||
|
background=palette["input_bg"],
|
||||||
|
fieldbackground=palette["input_bg"],
|
||||||
|
foreground=palette["input_fg"],
|
||||||
|
rowheight=28,
|
||||||
|
borderwidth=0,
|
||||||
|
)
|
||||||
|
style.configure(
|
||||||
|
"CustomResult.Treeview.Heading",
|
||||||
|
background=palette["surface_alt_bg"],
|
||||||
|
foreground=palette["muted_fg"],
|
||||||
|
font=("Microsoft YaHei", 10, "bold"),
|
||||||
|
relief="flat",
|
||||||
|
)
|
||||||
|
style.map(
|
||||||
|
"CustomResult.Treeview",
|
||||||
|
background=[("selected", palette["select_bg"])],
|
||||||
|
foreground=[("selected", palette["select_fg"])],
|
||||||
|
)
|
||||||
|
style.map(
|
||||||
|
"CustomResult.Treeview.Heading",
|
||||||
|
background=[("active", palette["surface_hover_bg"])],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_custom_template_theme(self: "PQAutomationApp"):
|
||||||
|
"""刷新客户模板结果表的主题色。"""
|
||||||
|
_apply_custom_result_theme(self)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_custom_template_tab_visible(self: "PQAutomationApp", visible: bool):
|
||||||
|
"""控制客户模板结果 TAB 的显示与隐藏。"""
|
||||||
|
if not hasattr(self, "chart_notebook") or not hasattr(self, "custom_template_tab_frame"):
|
||||||
|
return
|
||||||
|
|
||||||
|
tab_id = str(self.custom_template_tab_frame)
|
||||||
|
current_tabs = list(self.chart_notebook.tabs())
|
||||||
|
|
||||||
|
if visible:
|
||||||
|
if tab_id not in current_tabs:
|
||||||
|
self.chart_notebook.add(self.custom_template_tab_frame, text="客户模板结果显示")
|
||||||
|
self.chart_notebook.select(self.custom_template_tab_frame)
|
||||||
|
return
|
||||||
|
|
||||||
|
if tab_id in current_tabs:
|
||||||
|
current_selected = self.chart_notebook.select()
|
||||||
|
self.chart_notebook.forget(self.custom_template_tab_frame)
|
||||||
|
remaining_tabs = list(self.chart_notebook.tabs())
|
||||||
|
if current_selected == tab_id and remaining_tabs:
|
||||||
|
self.chart_notebook.select(remaining_tabs[0])
|
||||||
|
|
||||||
|
|
||||||
def show_custom_result_context_menu(self: "PQAutomationApp", event):
|
def show_custom_result_context_menu(self: "PQAutomationApp", event):
|
||||||
"""显示客户模板结果右键菜单"""
|
"""显示客户模板结果右键菜单"""
|
||||||
if not hasattr(self, "custom_result_tree") or not hasattr(
|
if not hasattr(self, "custom_result_tree") or not hasattr(
|
||||||
@@ -178,7 +218,7 @@ def show_custom_result_context_menu(self: "PQAutomationApp", event):
|
|||||||
can_single_step = (
|
can_single_step = (
|
||||||
has_selection
|
has_selection
|
||||||
and self.ca is not None
|
and self.ca is not None
|
||||||
and self.ucd is not None
|
and self.signal_service.is_connected
|
||||||
and not self.testing
|
and not self.testing
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
@@ -219,7 +259,7 @@ def start_custom_row_single_step(self: "PQAutomationApp"):
|
|||||||
if not hasattr(self, "custom_result_tree"):
|
if not hasattr(self, "custom_result_tree"):
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.ca is None or self.ucd is None:
|
if self.ca is None or not self.signal_service.is_connected:
|
||||||
messagebox.showerror("错误", "请先连接CA410和信号发生器")
|
messagebox.showerror("错误", "请先连接CA410和信号发生器")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -322,11 +362,9 @@ def _run_custom_row_single_step(self: "PQAutomationApp", item_id, row_no):
|
|||||||
time.sleep(self.pattern_settle_time)
|
time.sleep(self.pattern_settle_time)
|
||||||
|
|
||||||
# 测量:显示模式1读取 Tcp/duv/Lv,显示模式8读取 λd/Pe/Lv 与 XYZ。
|
# 测量:显示模式1读取 Tcp/duv/Lv,显示模式8读取 λd/Pe/Lv 与 XYZ。
|
||||||
self.ca.set_Display(1)
|
tcp, duv, lv, _, _, _ = self.read_ca_tcp_duv()
|
||||||
tcp, duv, lv, _, _, _ = self.ca.readAllDisplay()
|
|
||||||
|
|
||||||
self.ca.set_Display(8)
|
lambda_d, pe, lv, X, Y, Z = self.read_ca_lambda_pe()
|
||||||
lambda_d, pe, lv, X, Y, Z = self.ca.readAllDisplay()
|
|
||||||
|
|
||||||
xy = colour.XYZ_to_xy(np.array([X, Y, Z]))
|
xy = colour.XYZ_to_xy(np.array([X, Y, Z]))
|
||||||
u_prime, v_prime, _ = colour.XYZ_to_CIE1976UCS(np.array([X, Y, Z]))
|
u_prime, v_prime, _ = colour.XYZ_to_CIE1976UCS(np.array([X, Y, Z]))
|
||||||
@@ -532,10 +570,7 @@ def append_custom_template_result(self: "PQAutomationApp", row_no, result_data):
|
|||||||
def start_custom_template_test(self: "PQAutomationApp"):
|
def start_custom_template_test(self: "PQAutomationApp"):
|
||||||
"""开始客户模板测试(SDR)"""
|
"""开始客户模板测试(SDR)"""
|
||||||
|
|
||||||
if hasattr(self, "chart_notebook") and hasattr(self, "custom_template_tab_frame"):
|
if self.ca is None or not self.signal_service.is_connected:
|
||||||
self.chart_notebook.select(self.custom_template_tab_frame)
|
|
||||||
|
|
||||||
if self.ca is None or self.ucd is None:
|
|
||||||
messagebox.showerror("错误", "请先连接CA410和信号发生器")
|
messagebox.showerror("错误", "请先连接CA410和信号发生器")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -569,8 +604,10 @@ def start_custom_template_test(self: "PQAutomationApp"):
|
|||||||
self.custom_btn.config(state=tk.NORMAL)
|
self.custom_btn.config(state=tk.NORMAL)
|
||||||
self.status_var.set("测试已取消")
|
self.status_var.set("测试已取消")
|
||||||
self.set_custom_result_table_locked(False)
|
self.set_custom_result_table_locked(False)
|
||||||
|
_set_custom_template_tab_visible(self, False)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
_set_custom_template_tab_visible(self, True)
|
||||||
self.set_custom_result_table_locked(True)
|
self.set_custom_result_table_locked(True)
|
||||||
|
|
||||||
self.test_thread = threading.Thread(target=self.run_custom_sdr_test, args=([],))
|
self.test_thread = threading.Thread(target=self.run_custom_sdr_test, args=([],))
|
||||||
@@ -923,6 +960,7 @@ class CustomTemplatePanelMixin:
|
|||||||
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
"""
|
"""
|
||||||
create_custom_template_result_panel = create_custom_template_result_panel
|
create_custom_template_result_panel = create_custom_template_result_panel
|
||||||
|
_set_custom_template_tab_visible = _set_custom_template_tab_visible
|
||||||
show_custom_result_context_menu = show_custom_result_context_menu
|
show_custom_result_context_menu = show_custom_result_context_menu
|
||||||
set_custom_result_table_locked = set_custom_result_table_locked
|
set_custom_result_table_locked = set_custom_result_table_locked
|
||||||
start_custom_row_single_step = start_custom_row_single_step
|
start_custom_row_single_step = start_custom_row_single_step
|
||||||
@@ -937,3 +975,4 @@ class CustomTemplatePanelMixin:
|
|||||||
update_custom_button_visibility = update_custom_button_visibility
|
update_custom_button_visibility = update_custom_button_visibility
|
||||||
export_custom_template_excel = export_custom_template_excel
|
export_custom_template_excel = export_custom_template_excel
|
||||||
export_custom_template_charts = export_custom_template_charts
|
export_custom_template_charts = export_custom_template_charts
|
||||||
|
refresh_custom_template_theme = refresh_custom_template_theme
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"):
|
|||||||
ttk.Label(
|
ttk.Label(
|
||||||
title_row,
|
title_row,
|
||||||
text="(Gamma / CCT / 对比度 / EOTF 共用此列表)",
|
text="(Gamma / CCT / 对比度 / EOTF 共用此列表)",
|
||||||
foreground="#888",
|
style="Muted.TLabel",
|
||||||
).pack(side=tk.LEFT, padx=(8, 0))
|
).pack(side=tk.LEFT, padx=(8, 0))
|
||||||
|
|
||||||
# ===== 预设管理行 =====
|
# ===== 预设管理行 =====
|
||||||
@@ -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,
|
||||||
@@ -207,7 +201,7 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"):
|
|||||||
).pack(side=tk.LEFT, padx=2)
|
).pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
self._gamma_active_label = ttk.Label(
|
self._gamma_active_label = ttk.Label(
|
||||||
preset_row1, text="", foreground="#0a8", font=("微软雅黑", 9, "bold")
|
preset_row1, text="", style="SuccessState.TLabel", font=("微软雅黑", 9, "bold")
|
||||||
)
|
)
|
||||||
self._gamma_active_label.pack(side=tk.LEFT, padx=(10, 0))
|
self._gamma_active_label.pack(side=tk.LEFT, padx=(10, 0))
|
||||||
|
|
||||||
@@ -230,7 +224,7 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"):
|
|||||||
|
|
||||||
# 描述行
|
# 描述行
|
||||||
self._gamma_meta_label = ttk.Label(
|
self._gamma_meta_label = ttk.Label(
|
||||||
preset_box, text="", foreground="#666", font=("微软雅黑", 9)
|
preset_box, text="", style="Muted.TLabel", font=("微软雅黑", 9)
|
||||||
)
|
)
|
||||||
self._gamma_meta_label.pack(anchor=tk.W, pady=(6, 0))
|
self._gamma_meta_label.pack(anchor=tk.W, pady=(6, 0))
|
||||||
|
|
||||||
@@ -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()
|
||||||
@@ -350,7 +344,7 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"):
|
|||||||
paste_frame.pack(fill=tk.X, pady=(10, 0))
|
paste_frame.pack(fill=tk.X, pady=(10, 0))
|
||||||
ttk.Label(
|
ttk.Label(
|
||||||
paste_frame, text="每行:R,G,B 或 R G B\n或:灰度% (如 50%)",
|
paste_frame, text="每行:R,G,B 或 R G B\n或:灰度% (如 50%)",
|
||||||
foreground="#888", justify=tk.LEFT,
|
style="Muted.TLabel", justify=tk.LEFT,
|
||||||
).pack(anchor=tk.W)
|
).pack(anchor=tk.W)
|
||||||
ttk.Button(
|
ttk.Button(
|
||||||
paste_frame, text="从剪贴板导入",
|
paste_frame, text="从剪贴板导入",
|
||||||
@@ -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="", foreground="#666", 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",
|
||||||
@@ -435,16 +424,16 @@ def _update_active_label(self: "PQAutomationApp"):
|
|||||||
current = self._gamma_current_preset
|
current = self._gamma_current_preset
|
||||||
if active and current == active and not self._gamma_dirty:
|
if active and current == active and not self._gamma_dirty:
|
||||||
self._gamma_active_label.config(
|
self._gamma_active_label.config(
|
||||||
text=f"✔ 当前激活:{active}", foreground="#0a8"
|
text=f"✔ 当前激活:{active}", style="SuccessState.TLabel"
|
||||||
)
|
)
|
||||||
elif active:
|
elif active:
|
||||||
extra = "(有未保存改动)" if self._gamma_dirty else ""
|
extra = "(有未保存改动)" if self._gamma_dirty else ""
|
||||||
self._gamma_active_label.config(
|
self._gamma_active_label.config(
|
||||||
text=f"● 激活:{active} 编辑中:{current or '-'}{extra}",
|
text=f"● 激活:{active} 编辑中:{current or '-'}{extra}",
|
||||||
foreground="#a60" if self._gamma_dirty else "#888",
|
style="WarningState.TLabel" if self._gamma_dirty else "Muted.TLabel",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self._gamma_active_label.config(text="● 未激活任何预设", foreground="#888")
|
self._gamma_active_label.config(text="● 未激活任何预设", style="Muted.TLabel")
|
||||||
|
|
||||||
|
|
||||||
def _on_preset_selected(self: "PQAutomationApp"):
|
def _on_preset_selected(self: "PQAutomationApp"):
|
||||||
@@ -1023,11 +1012,11 @@ def _run_validation(self: "PQAutomationApp"):
|
|||||||
|
|
||||||
if not msgs:
|
if not msgs:
|
||||||
text = f"✔ 校验通过(共 {len(params)} 点)"
|
text = f"✔ 校验通过(共 {len(params)} 点)"
|
||||||
color = "#0a8"
|
style_name = "SuccessState.TLabel"
|
||||||
else:
|
else:
|
||||||
text = f"共 {len(params)} 点 | " + " ".join(msgs)
|
text = f"共 {len(params)} 点 | " + " ".join(msgs)
|
||||||
color = "#a60" if any(m.startswith("⚠") for m in msgs) else "#666"
|
style_name = "WarningState.TLabel" if any(m.startswith("⚠") for m in msgs) else "Muted.TLabel"
|
||||||
self._gamma_validate_label.config(text=text, foreground=color)
|
self._gamma_validate_label.config(text=text, style=style_name)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import re
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
import ttkbootstrap as ttk
|
import ttkbootstrap as ttk
|
||||||
|
|
||||||
from drivers.UCD323_Enum import UCDEnum
|
from app.ucd import UCDEnum
|
||||||
from app.views.collapsing_frame import CollapsingFrame
|
from app.views.collapsing_frame import CollapsingFrame
|
||||||
from app.resources import load_icon
|
from app.resources import load_icon
|
||||||
|
|
||||||
@@ -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(
|
||||||
@@ -600,7 +600,6 @@ def create_connection_content(self: "PQAutomationApp"):
|
|||||||
com_frame, width=15, height=15, bg="gray", highlightthickness=0
|
com_frame, width=15, height=15, bg="gray", highlightthickness=0
|
||||||
)
|
)
|
||||||
self.ucd_status_indicator.grid(row=0, column=2, padx=(10, 20))
|
self.ucd_status_indicator.grid(row=0, column=2, padx=(10, 20))
|
||||||
self.ucd_status_indicator.config(bg="gray")
|
|
||||||
|
|
||||||
# 添加按钮框架
|
# 添加按钮框架
|
||||||
button_frame = ttk.Frame(com_frame)
|
button_frame = ttk.Frame(com_frame)
|
||||||
@@ -608,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,
|
||||||
@@ -622,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(
|
||||||
@@ -634,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(
|
||||||
@@ -647,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(
|
||||||
@@ -669,7 +705,8 @@ def create_connection_content(self: "PQAutomationApp"):
|
|||||||
com_frame, width=15, height=15, bg="gray", highlightthickness=0
|
com_frame, width=15, height=15, bg="gray", highlightthickness=0
|
||||||
)
|
)
|
||||||
self.ca_status_indicator.grid(row=1, column=2, padx=(10, 20))
|
self.ca_status_indicator.grid(row=1, column=2, padx=(10, 20))
|
||||||
self.ca_status_indicator.config(bg="gray")
|
|
||||||
|
self.refresh_connection_indicators()
|
||||||
|
|
||||||
# 添加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:
|
||||||
@@ -757,16 +793,24 @@ def create_test_type_frame(self: "PQAutomationApp"):
|
|||||||
# 测试版水印标签(版本 x.x.0.0 时显示)
|
# 测试版水印标签(版本 x.x.0.0 时显示)
|
||||||
from app_version import is_beta_version, APP_VERSION
|
from app_version import is_beta_version, APP_VERSION
|
||||||
if is_beta_version():
|
if is_beta_version():
|
||||||
beta_lbl = tk.Label(
|
beta_lbl = ttk.Label(
|
||||||
self.sidebar_frame,
|
self.sidebar_frame,
|
||||||
text=f"[测试版] v{APP_VERSION}",
|
text=f"[测试版] v{APP_VERSION}",
|
||||||
foreground="#ffffff",
|
style="SidebarBadge.TLabel",
|
||||||
background="#cc3300",
|
|
||||||
font=("微软雅黑", 8, "bold"),
|
|
||||||
anchor="center",
|
|
||||||
)
|
)
|
||||||
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,
|
||||||
@@ -780,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:
|
||||||
@@ -807,9 +849,24 @@ 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()
|
||||||
_refresh_theme_toggle_label(self)
|
_refresh_theme_toggle_label(self)
|
||||||
|
if hasattr(self, "refresh_connection_indicators"):
|
||||||
|
try:
|
||||||
|
self.refresh_connection_indicators()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
if hasattr(self, "apply_result_chart_theme"):
|
if hasattr(self, "apply_result_chart_theme"):
|
||||||
try:
|
try:
|
||||||
self.apply_result_chart_theme()
|
self.apply_result_chart_theme()
|
||||||
@@ -820,6 +877,21 @@ def _on_toggle_theme(self: "PQAutomationApp") -> None:
|
|||||||
self.log_gui.refresh_log_theme()
|
self.log_gui.refresh_log_theme()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
if hasattr(self, "refresh_ai_image_theme"):
|
||||||
|
try:
|
||||||
|
self.refresh_ai_image_theme()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if hasattr(self, "refresh_single_step_theme"):
|
||||||
|
try:
|
||||||
|
self.refresh_single_step_theme()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if hasattr(self, "refresh_custom_template_theme"):
|
||||||
|
try:
|
||||||
|
self.refresh_custom_template_theme()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
if hasattr(self, "refresh_calman_theme"):
|
if hasattr(self, "refresh_calman_theme"):
|
||||||
try:
|
try:
|
||||||
self.refresh_calman_theme()
|
self.refresh_calman_theme()
|
||||||
@@ -831,6 +903,18 @@ def _on_toggle_theme(self: "PQAutomationApp") -> None:
|
|||||||
self.update_sidebar_selection()
|
self.update_sidebar_selection()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
# 以新的 dark_mode 值重绘当前测试类型的所有图表
|
||||||
|
if hasattr(self, "_chart_snapshots") and hasattr(self, "config"):
|
||||||
|
test_type = getattr(self.config, "current_test_type", None)
|
||||||
|
if test_type:
|
||||||
|
snapshots = self._chart_snapshots.get(test_type, {})
|
||||||
|
for chart_name, args in snapshots.items():
|
||||||
|
plot_fn = getattr(self, f"plot_{chart_name}", None)
|
||||||
|
if plot_fn:
|
||||||
|
try:
|
||||||
|
plot_fn(*args)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def update_config_info_display(self: "PQAutomationApp"):
|
def update_config_info_display(self: "PQAutomationApp"):
|
||||||
@@ -944,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", {})[
|
||||||
@@ -993,7 +1077,7 @@ def on_screen_module_signal_format_changed(self: "PQAutomationApp", event=None):
|
|||||||
self.save_pq_config()
|
self.save_pq_config()
|
||||||
return
|
return
|
||||||
|
|
||||||
if getattr(self.ucd, "status", False):
|
if self.signal_service.is_connected:
|
||||||
ok = self.signal_service.update_signal_format(
|
ok = self.signal_service.update_signal_format(
|
||||||
color_space=color_space,
|
color_space=color_space,
|
||||||
data_range=data_range,
|
data_range=data_range,
|
||||||
@@ -1037,7 +1121,7 @@ def on_sdr_output_format_changed(self: "PQAutomationApp", event=None):
|
|||||||
self.log_gui.log("警告: 测试进行中,格式更改将在下次测试时生效", level="error")
|
self.log_gui.log("警告: 测试进行中,格式更改将在下次测试时生效", level="error")
|
||||||
return
|
return
|
||||||
|
|
||||||
if getattr(self.ucd, "status", False):
|
if self.signal_service.is_connected:
|
||||||
ok = self.signal_service.update_signal_format(
|
ok = self.signal_service.update_signal_format(
|
||||||
color_space=self.sdr_color_space_var.get(),
|
color_space=self.sdr_color_space_var.get(),
|
||||||
data_range=self.sdr_data_range_var.get(),
|
data_range=self.sdr_data_range_var.get(),
|
||||||
@@ -1061,7 +1145,7 @@ def on_hdr_output_format_changed(self: "PQAutomationApp", event=None):
|
|||||||
self.log_gui.log("警告: 测试进行中,格式更改将在下次测试时生效", level="error")
|
self.log_gui.log("警告: 测试进行中,格式更改将在下次测试时生效", level="error")
|
||||||
return
|
return
|
||||||
|
|
||||||
if getattr(self.ucd, "status", False):
|
if self.signal_service.is_connected:
|
||||||
ok = self.signal_service.update_signal_format(
|
ok = self.signal_service.update_signal_format(
|
||||||
color_space=self.hdr_color_space_var.get(),
|
color_space=self.hdr_color_space_var.get(),
|
||||||
data_range=self.hdr_data_range_var.get(),
|
data_range=self.hdr_data_range_var.get(),
|
||||||
@@ -1085,6 +1169,9 @@ def on_local_dimming_timing_changed(self: "PQAutomationApp", event=None):
|
|||||||
|
|
||||||
self.config.current_test_types.setdefault("local_dimming", {})["timing"] = selected_timing
|
self.config.current_test_types.setdefault("local_dimming", {})["timing"] = selected_timing
|
||||||
|
|
||||||
|
if hasattr(self, "invalidate_ld_ucd_params_cache"):
|
||||||
|
self.invalidate_ld_ucd_params_cache()
|
||||||
|
|
||||||
if self.testing:
|
if self.testing:
|
||||||
self.log_gui.log("警告: 测试进行中,分辨率更改将在下次测试时生效", level="error")
|
self.log_gui.log("警告: 测试进行中,分辨率更改将在下次测试时生效", level="error")
|
||||||
|
|
||||||
@@ -1107,6 +1194,9 @@ def on_local_dimming_signal_format_changed(self: "PQAutomationApp", event=None):
|
|||||||
ld_cfg["bpc"] = UCDEnum.SignalFormat.BitDepth.get_bit_value(bit_depth)
|
ld_cfg["bpc"] = UCDEnum.SignalFormat.BitDepth.get_bit_value(bit_depth)
|
||||||
ld_cfg["data_range"] = data_range
|
ld_cfg["data_range"] = data_range
|
||||||
|
|
||||||
|
if hasattr(self, "invalidate_ld_ucd_params_cache"):
|
||||||
|
self.invalidate_ld_ucd_params_cache()
|
||||||
|
|
||||||
self.log_gui.log(
|
self.log_gui.log(
|
||||||
(
|
(
|
||||||
"Local Dimming 信号格式已更新: "
|
"Local Dimming 信号格式已更新: "
|
||||||
@@ -1121,7 +1211,7 @@ def on_local_dimming_signal_format_changed(self: "PQAutomationApp", event=None):
|
|||||||
self.save_pq_config()
|
self.save_pq_config()
|
||||||
return
|
return
|
||||||
|
|
||||||
if getattr(self.ucd, "status", False):
|
if self.signal_service.is_connected:
|
||||||
ok = self.signal_service.update_signal_format(
|
ok = self.signal_service.update_signal_format(
|
||||||
color_space=color_space,
|
color_space=color_space,
|
||||||
data_range=data_range,
|
data_range=data_range,
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ def create_pantone_baseline_panel(self: "PQAutomationApp"):
|
|||||||
ttk.Label(config_row, textvariable=self.pantone_progress_var).pack(
|
ttk.Label(config_row, textvariable=self.pantone_progress_var).pack(
|
||||||
side=tk.RIGHT, padx=(8, 0)
|
side=tk.RIGHT, padx=(8, 0)
|
||||||
)
|
)
|
||||||
ttk.Label(config_row, textvariable=self.pantone_status_var, foreground="#666").pack(
|
ttk.Label(config_row, textvariable=self.pantone_status_var, style="Muted.TLabel").pack(
|
||||||
side=tk.RIGHT
|
side=tk.RIGHT
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -336,7 +336,7 @@ def _launch_worker(self: "PQAutomationApp", start_index, settle):
|
|||||||
end_state = "paused"
|
end_state = "paused"
|
||||||
break
|
break
|
||||||
|
|
||||||
x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay()
|
x, y, lv, _X, _Y, _Z = self.read_ca_xyLv()
|
||||||
if lv is None:
|
if lv is None:
|
||||||
raise RuntimeError(f"第 {i + 1} 组 CA410 采集失败")
|
raise RuntimeError(f"第 {i + 1} 组 CA410 采集失败")
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|
||||||
@@ -57,9 +57,53 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
|
|||||||
window_frame,
|
window_frame,
|
||||||
text="点击按钮发送对应百分比的白色窗口(黑色背景 + 居中白色矩形)",
|
text="点击按钮发送对应百分比的白色窗口(黑色背景 + 居中白色矩形)",
|
||||||
font=("", 9),
|
font=("", 9),
|
||||||
foreground="#28a745",
|
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),
|
|
||||||
foreground="#28a745",
|
|
||||||
).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,
|
||||||
@@ -155,12 +268,12 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
|
|||||||
measure_btn_frame,
|
measure_btn_frame,
|
||||||
text="亮度: -- cd/m² | x: -- | y: --",
|
text="亮度: -- cd/m² | x: -- | y: --",
|
||||||
font=("Consolas", 10),
|
font=("Consolas", 10),
|
||||||
foreground="#007bff",
|
style="InfoState.TLabel",
|
||||||
)
|
)
|
||||||
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"):
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ from tkinter import filedialog, messagebox
|
|||||||
import ttkbootstrap as ttk
|
import ttkbootstrap as ttk
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
from app.views.modern_styles import apply_listbox_theme
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -59,7 +61,7 @@ def create_single_step_panel(self: "PQAutomationApp"):
|
|||||||
ttk.Label(
|
ttk.Label(
|
||||||
title_row,
|
title_row,
|
||||||
text="录入目标色块,发送纯色色块并采集 xyY / ΔE2000。",
|
text="录入目标色块,发送纯色色块并采集 xyY / ΔE2000。",
|
||||||
foreground="#666",
|
style="Muted.TLabel",
|
||||||
).pack(side=tk.LEFT, padx=(12, 0))
|
).pack(side=tk.LEFT, padx=(12, 0))
|
||||||
|
|
||||||
left = ttk.LabelFrame(root, text="样本列表", padding=8)
|
left = ttk.LabelFrame(root, text="样本列表", padding=8)
|
||||||
@@ -73,11 +75,8 @@ def create_single_step_panel(self: "PQAutomationApp"):
|
|||||||
activestyle="none",
|
activestyle="none",
|
||||||
font=("微软雅黑", 9),
|
font=("微软雅黑", 9),
|
||||||
highlightthickness=1,
|
highlightthickness=1,
|
||||||
highlightbackground="#d8d8d8",
|
|
||||||
highlightcolor="#4a90e2",
|
|
||||||
selectbackground="#2b6cb0",
|
|
||||||
selectforeground="#ffffff",
|
|
||||||
)
|
)
|
||||||
|
apply_listbox_theme(self.single_step_listbox)
|
||||||
self.single_step_listbox.pack(fill=tk.BOTH, expand=True)
|
self.single_step_listbox.pack(fill=tk.BOTH, expand=True)
|
||||||
self.single_step_listbox.bind(
|
self.single_step_listbox.bind(
|
||||||
"<<ListboxSelect>>", lambda e: _on_sample_select(self)
|
"<<ListboxSelect>>", lambda e: _on_sample_select(self)
|
||||||
@@ -154,7 +153,7 @@ def create_single_step_panel(self: "PQAutomationApp"):
|
|||||||
ttk.Label(
|
ttk.Label(
|
||||||
form_frame,
|
form_frame,
|
||||||
textvariable=self.single_step_status_var,
|
textvariable=self.single_step_status_var,
|
||||||
foreground="#666",
|
style="Muted.TLabel",
|
||||||
).grid(row=2, column=2, columnspan=4, sticky=tk.W, pady=4)
|
).grid(row=2, column=2, columnspan=4, sticky=tk.W, pady=4)
|
||||||
|
|
||||||
action_row = ttk.Frame(form_frame)
|
action_row = ttk.Frame(form_frame)
|
||||||
@@ -444,7 +443,7 @@ def _measure_current_sample(self: "PQAutomationApp"):
|
|||||||
|
|
||||||
def worker():
|
def worker():
|
||||||
try:
|
try:
|
||||||
x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay()
|
x, y, lv, _X, _Y, _Z = self.read_ca_xyLv()
|
||||||
if lv is None:
|
if lv is None:
|
||||||
raise RuntimeError("CA410 未返回有效亮度")
|
raise RuntimeError("CA410 未返回有效亮度")
|
||||||
self._dispatch_ui(self.single_step_measured_x_var.set, f"{x:.4f}")
|
self._dispatch_ui(self.single_step_measured_x_var.set, f"{x:.4f}")
|
||||||
@@ -556,6 +555,12 @@ def _export_results_csv(self: "PQAutomationApp"):
|
|||||||
messagebox.showerror("导出失败", f"写入 CSV 失败: {exc}")
|
messagebox.showerror("导出失败", f"写入 CSV 失败: {exc}")
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_single_step_theme(self: "PQAutomationApp"):
|
||||||
|
"""刷新单步调试中 tk.Listbox 的主题色。"""
|
||||||
|
if hasattr(self, "single_step_listbox"):
|
||||||
|
apply_listbox_theme(self.single_step_listbox)
|
||||||
|
|
||||||
|
|
||||||
class SingleStepPanelMixin:
|
class SingleStepPanelMixin:
|
||||||
"""由 tools/refactor_to_mixins.py 自动生成。
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
||||||
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||||||
@@ -575,3 +580,4 @@ class SingleStepPanelMixin:
|
|||||||
_commit_result = _commit_result
|
_commit_result = _commit_result
|
||||||
_clear_results = _clear_results
|
_clear_results = _clear_results
|
||||||
_export_results_csv = _export_results_csv
|
_export_results_csv = _export_results_csv
|
||||||
|
refresh_single_step_theme = refresh_single_step_theme
|
||||||
|
|||||||
@@ -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):
|
||||||
"""禁用所有单步调试(新测试开始时调用)"""
|
"""禁用所有单步调试(新测试开始时调用)"""
|
||||||
@@ -802,7 +791,7 @@ class PQDebugPanel:
|
|||||||
time.sleep(1.5)
|
time.sleep(1.5)
|
||||||
|
|
||||||
# 测量数据
|
# 测量数据
|
||||||
x, y, lv, X, Y, Z = self.app.ca.readAllDisplay()
|
x, y, lv, X, Y, Z = self.app.read_ca_xyLv()
|
||||||
|
|
||||||
self.app.log_gui.log(
|
self.app.log_gui.log(
|
||||||
f"测量完成: x={x:.4f}, y={y:.4f}, lv={lv:.2f}, "
|
f"测量完成: x={x:.4f}, y={y:.4f}, lv={lv:.2f}, "
|
||||||
|
|||||||
@@ -19,20 +19,44 @@ from app.views.modern_styles import apply_modern_styles
|
|||||||
|
|
||||||
_PREFS_PATH = Path("settings/ui_preferences.json")
|
_PREFS_PATH = Path("settings/ui_preferences.json")
|
||||||
|
|
||||||
# 浅色主题:沿用旧的 yeti(首发布兼容)
|
# 浅色主题:自定义轻量蓝灰色板,恢复旧版浅色观感
|
||||||
LIGHT_THEME = "yeti"
|
LIGHT_THEME = "calman_light"
|
||||||
# 深色主题:自定义 Calman 风格
|
# 深色主题:自定义 Calman 风格
|
||||||
DARK_THEME = "calman_dark"
|
DARK_THEME = "calman_dark"
|
||||||
|
|
||||||
|
_LEGACY_LIGHT_THEMES = {"yeti"}
|
||||||
|
|
||||||
|
|
||||||
|
_CALMAN_LIGHT_COLORS = {
|
||||||
|
"primary": "#1755a6",
|
||||||
|
"secondary": "#3572B4",
|
||||||
|
"success": "#2F9E44",
|
||||||
|
"info": "#247BA0",
|
||||||
|
"warning": "#C98700",
|
||||||
|
"danger": "#CC3300",
|
||||||
|
"light": "#F7FAFC",
|
||||||
|
"dark": "#1F2A36",
|
||||||
|
"bg": "#F5F8FB",
|
||||||
|
"fg": "#1F2933",
|
||||||
|
"selectbg": "#2B6CB0",
|
||||||
|
"selectfg": "#FFFFFF",
|
||||||
|
"border": "#C8D4E3",
|
||||||
|
"inputfg": "#243240",
|
||||||
|
"inputbg": "#FFFFFF",
|
||||||
|
"active": "#D9E6F2",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
# Calman 风格深色主题色板(参考实测截图取色)
|
# Calman 风格深色主题色板
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
_CALMAN_DARK_COLORS = {
|
_CALMAN_DARK_COLORS = {
|
||||||
"primary": "#343A41", # 主色改为炭灰,避免大面积亮蓝
|
# "primary": "#2A2F36",
|
||||||
"secondary": "#444A51", # 中性深灰(用于 header / 分组背景)
|
# "secondary": "#444A51",
|
||||||
|
"primary": "#6FAFCC",
|
||||||
|
"secondary": "#AEAEAE",
|
||||||
"success": "#4FB960",
|
"success": "#4FB960",
|
||||||
"info": "#6FAFCC", # 降低饱和度,只做少量点缀
|
"info": "#6FAFCC",
|
||||||
"warning": "#F2A93B",
|
"warning": "#F2A93B",
|
||||||
"danger": "#E0524A",
|
"danger": "#E0524A",
|
||||||
"light": "#BFC6CE", # 高亮文本
|
"light": "#BFC6CE", # 高亮文本
|
||||||
@@ -51,14 +75,27 @@ _CALMAN_DARK_COLORS = {
|
|||||||
def register_themes() -> None:
|
def register_themes() -> None:
|
||||||
"""把自定义深色主题注册到 ttkbootstrap(可重复调用,幂等)。"""
|
"""把自定义深色主题注册到 ttkbootstrap(可重复调用,幂等)。"""
|
||||||
style = Style()
|
style = Style()
|
||||||
|
if LIGHT_THEME not in style.theme_names():
|
||||||
|
light_def = ThemeDefinition(
|
||||||
|
name=LIGHT_THEME,
|
||||||
|
themetype="light",
|
||||||
|
colors=_CALMAN_LIGHT_COLORS,
|
||||||
|
)
|
||||||
|
style.register_theme(light_def)
|
||||||
if DARK_THEME in style.theme_names():
|
if DARK_THEME in style.theme_names():
|
||||||
return
|
return
|
||||||
theme_def = ThemeDefinition(
|
dark_def = ThemeDefinition(
|
||||||
name=DARK_THEME,
|
name=DARK_THEME,
|
||||||
themetype="dark",
|
themetype="dark",
|
||||||
colors=_CALMAN_DARK_COLORS,
|
colors=_CALMAN_DARK_COLORS,
|
||||||
)
|
)
|
||||||
style.register_theme(theme_def)
|
style.register_theme(dark_def)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_theme_name(name: Optional[str]) -> str:
|
||||||
|
if not name or name in _LEGACY_LIGHT_THEMES:
|
||||||
|
return LIGHT_THEME
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
@@ -101,7 +138,7 @@ def apply_initial_theme() -> str:
|
|||||||
返回最终生效的主题名。
|
返回最终生效的主题名。
|
||||||
"""
|
"""
|
||||||
register_themes()
|
register_themes()
|
||||||
name = get_saved_theme() or LIGHT_THEME
|
name = _normalize_theme_name(get_saved_theme())
|
||||||
style = Style()
|
style = Style()
|
||||||
if name not in style.theme_names():
|
if name not in style.theme_names():
|
||||||
name = LIGHT_THEME
|
name = LIGHT_THEME
|
||||||
@@ -113,6 +150,7 @@ def apply_initial_theme() -> str:
|
|||||||
def set_theme(name: str) -> str:
|
def set_theme(name: str) -> str:
|
||||||
"""切换到指定主题,持久化偏好,并刷新自定义样式。"""
|
"""切换到指定主题,持久化偏好,并刷新自定义样式。"""
|
||||||
register_themes()
|
register_themes()
|
||||||
|
name = _normalize_theme_name(name)
|
||||||
style = Style()
|
style = Style()
|
||||||
if name not in style.theme_names():
|
if name not in style.theme_names():
|
||||||
name = LIGHT_THEME
|
name = LIGHT_THEME
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ APP_VERSION = "106.26.0.0"
|
|||||||
|
|
||||||
|
|
||||||
def is_beta_version(version: str = APP_VERSION) -> bool:
|
def is_beta_version(version: str = APP_VERSION) -> bool:
|
||||||
"""版本号第3、4段均为 '0' 时(格式 x.x.0.0)判定为测试版。"""
|
"""版本号第3、4段均为 '0' 时(格式 x.x.0.x)判定为测试版。"""
|
||||||
parts = version.split(".")
|
parts = version.split(".")
|
||||||
if len(parts) >= 4:
|
if len(parts) >= 4:
|
||||||
return parts[2] == "0" and parts[3] == "0"
|
return parts[2] == "0"
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
131
cache/pq_ai_api_v21_extracted.txt
vendored
Normal file
131
cache/pq_ai_api_v21_extracted.txt
vendored
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
|
||||||
|
===== PAGE 1 =====
|
||||||
|
接口文档
|
||||||
|
域名地址
|
||||||
|
测试环境:
|
||||||
|
http://10.201.44.70:9008/ai-agent/
|
||||||
|
生产环境:
|
||||||
|
https://r d-mokadisplay .tcl.com/ai-agent/
|
||||||
|
一、上传图片接口:
|
||||||
|
1. 接口明细
|
||||||
|
接口路径:api/v1/pqt est/uplo ad
|
||||||
|
Cont ent-Type:multip art/form-data
|
||||||
|
请求方式:POST
|
||||||
|
2. 请求参数说明
|
||||||
|
参数名 参数类型 是否必填 参数描述
|
||||||
|
file File(二进制文件)是待上传图片的二进制文件。请求体格式必须为multipart/form-data ,且包含一个名
|
||||||
|
为file 的文件字段(如:@"D:\Desktop\PQtest\3-O.png" )
|
||||||
|
备注:仅支持 PNG/JP G/JPEG 格式图片,大小不超过 10MB,分辨率最大为 4096×4096 p x
|
||||||
|
3. 请求及响应示例
|
||||||
|
上传图片请求示例
|
||||||
|
curl -X POST "https://rd-mokadisplay.tcl.com/ai-agent/api/v1/pqtest/upload" \
|
||||||
|
-F "file=@D:\Desktop\PQtest\3-O.png"1 / 5
|
||||||
|
|
||||||
|
===== PAGE 2 =====
|
||||||
|
|
||||||
|
上传图片响应示例
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "",
|
||||||
|
"data": {
|
||||||
|
"upload_image_url": "https://ai.file.qhmoka.com/prod-ai-portal/pq-
|
||||||
|
image/input/2026-05-28/a2264f477d4d487493058306701cdb44/3-O.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
报错响应示例
|
||||||
|
{
|
||||||
|
"code":400,
|
||||||
|
"message":"不支持的图片格式,仅支持 PNG/JPG/JPEG",
|
||||||
|
"data":{"upload_image_url": ""}
|
||||||
|
}
|
||||||
|
二、生图接口
|
||||||
|
1. 接口明细
|
||||||
|
接口路径:api/v1/pqt est/generat e
|
||||||
|
Cont ent-Type:application/json
|
||||||
|
请求方式:POST
|
||||||
|
2. 请求参数说明
|
||||||
|
参数名 参数类型是否必填 参数描述
|
||||||
|
user_message String 是 用户的自然语言需求/指令文本,作为本次生成 PQ 测试图的输入内容2 / 5
|
||||||
|
|
||||||
|
===== PAGE 3 =====
|
||||||
|
参数名 参数类型是否必填 参数描述
|
||||||
|
session_id String 是会话标识,用于把多次请求归到同一会话(便于复用/关联上下文)。在同一会话窗口
|
||||||
|
下请使用同一session_id。使用 uuid 生成唯一字符串
|
||||||
|
upload_image_urlString 否 上传的参考图片地址,来自上传接口返回的 URL
|
||||||
|
备注1:相较于上一版本,本版本新增了 uplo ad_image _url 字段。未传入时,默认为“文生图”模
|
||||||
|
式(与上版本一致);传入时,则启用“图生图”模式。
|
||||||
|
备注2:在多轮对话的“图生图”模式下,需将上一轮对话返回的 imageUrl 作为第二轮对话的
|
||||||
|
upload_image _url 传入。
|
||||||
|
3. 请求及响应示例
|
||||||
|
单轮对话生图请求示例
|
||||||
|
{
|
||||||
|
"user_message":"请复刻这张图,生成标准 PQ 测试图 ",
|
||||||
|
"session_id":"48f1a351-67f2-40db-a57d-20d66249bc93",
|
||||||
|
"upload_image_url":"https://test.file.qhmoka.com/test-ai-portal/pq-
|
||||||
|
image/input/2026-05-21/681136ecf35549bcb7969905fb728991/05213.png"
|
||||||
|
}
|
||||||
|
|
||||||
|
单轮对话生图响应示例
|
||||||
|
{
|
||||||
|
"code":200,
|
||||||
|
"message":"",
|
||||||
|
"data":{"imageUrl":"https://test.file.qhmoka.com/test-ai-portal/pq-
|
||||||
|
image/2026-05-22/19/05be786be62eb9aa.png"}
|
||||||
|
}3 / 5
|
||||||
|
|
||||||
|
===== PAGE 4 =====
|
||||||
|
多轮对话生图请求示例
|
||||||
|
# 第一轮请求
|
||||||
|
{
|
||||||
|
"user_message":"请复刻这张图,生成标准 PQ 测试图 ",
|
||||||
|
"session_id":"48f1a351-67f2-40db-a57d-20d66249bc93",
|
||||||
|
"upload_image_url":"https://test.file.qhmoka.com/test-ai-portal/pq-
|
||||||
|
image/input/2026-05-21/681136ecf35549bcb7969905fb728991/05213.png"
|
||||||
|
}
|
||||||
|
# 第二轮请求
|
||||||
|
{
|
||||||
|
"user_message":"把图片上部分的白和红圆圈对换位置,把图片下部分蓝和绿圆圈对换位置,同时把
|
||||||
|
背景颜色换为黄色 ",
|
||||||
|
"session_id":"48f1a351-67f2-40db-a57d-20d66249bc93", # 确保两轮 session_id
|
||||||
|
相同
|
||||||
|
"upload_image_url":"https://test.file.qhmoka.com/test-ai-portal/pq-
|
||||||
|
image/2026-05-22/19/05be786be62eb9aa.png" # 这里是第一轮响应返回的 imageUrl
|
||||||
|
}
|
||||||
|
多轮对话生图响应示例
|
||||||
|
# 第一轮响应
|
||||||
|
{
|
||||||
|
"code":200,
|
||||||
|
"message":"",
|
||||||
|
"data":{"imageUrl":"https://test.file.qhmoka.com/test-ai-portal/pq-
|
||||||
|
image/2026-05-22/19/05be786be62eb9aa.png"}
|
||||||
|
}
|
||||||
|
# 第二轮响应
|
||||||
|
{
|
||||||
|
"code":200,
|
||||||
|
"message":"",
|
||||||
|
"data":{"imageUrl":"https://test.file.qhmoka.com/test-ai-portal/pq-
|
||||||
|
image/2026-05-22/19/703f0896ffcc8c57.png"}
|
||||||
|
}
|
||||||
|
报错响应示例
|
||||||
|
# 大模型调用超时,超过120s
|
||||||
|
{
|
||||||
|
"code":500,
|
||||||
|
"message":"生成失败 ",4 / 5
|
||||||
|
|
||||||
|
===== PAGE 5 =====
|
||||||
|
"data":{"imageUrl":""}
|
||||||
|
}
|
||||||
|
|
||||||
|
三、关于多轮对话(图片修改)的图片传参逻辑补充说明
|
||||||
|
核心规则
|
||||||
|
从第二轮请求开始,每一轮都必须把"最近一次成功返回的图片"作为本轮请求的输入图片传入。
|
||||||
|
详细说明
|
||||||
|
1.首轮请求
|
||||||
|
•用户可传图、也可不传图(无强制要求)。
|
||||||
|
2.后续每一轮请求(第 2 轮及以后)
|
||||||
|
•必须带上 上一轮成功返回的图片 作为输入。
|
||||||
|
•与首轮用户是否传过图无关,只要是后续轮次就要带。
|
||||||
|
3.异常情况处理
|
||||||
|
•如果上一轮请求失败 / 没有返回图片,则向前回溯,使用 最近一次成功返回图片的那一
|
||||||
|
轮的结果作为本轮输入。
|
||||||
|
•简单说:始终使用"最近一张成功生成的图片"。5 / 5
|
||||||
17
cache/pq_ai_api_v21_summary.txt
vendored
Normal file
17
cache/pq_ai_api_v21_summary.txt
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[upload] 示例 上传图片请求示例 curl -X POST "https://rd-mokadisplay.tcl.com/ai-agent/api/v1/pqtest/upload" \ -F "file=@D:\Desktop\PQtest\3-O.png"1 / 5 ===== PAGE 2 ===== 上传图片响应示例 { "code": 200, "message": "", "data": { "upload_image_url": "https://a
|
||||||
|
|
||||||
|
[session_id] 是 用户的自然语言需求/指令文本,作为本次生成 PQ 测试图的输入内容2 / 5 ===== PAGE 3 ===== 参数名 参数类型是否必填 参数描述 session_id String 是会话标识,用于把多次请求归到同一会话(便于复用/关联上下文)。在同一会话窗口 下请使用同一session_id。使用 uuid 生成唯一字符串 upload_image_urlString 否 上传的参考图片地址,来自上传接口返回的 URL 备注1:相较于上一版本,本版本新增了 uplo ad_ima
|
||||||
|
|
||||||
|
[upload_image_url] === PAGE 2 ===== 上传图片响应示例 { "code": 200, "message": "", "data": { "upload_image_url": "https://ai.file.qhmoka.com/prod-ai-portal/pq- image/input/2026-05-28/a2264f477d4d487493058306701cdb44/3-O.png" } } 报错响应示例 { "code":400, "message":
|
||||||
|
|
||||||
|
[imageUrl] _url 字段。未传入时,默认为“文生图”模 式(与上版本一致);传入时,则启用“图生图”模式。 备注2:在多轮对话的“图生图”模式下,需将上一轮对话返回的 imageUrl 作为第二轮对话的 upload_image _url 传入。 3. 请求及响应示例 单轮对话生图请求示例 { "user_message":"请复刻这张图,生成标准 PQ 测试图 ", "session_id":"48f1a351-67f2-40db-a57d-20d66249bc93",
|
||||||
|
|
||||||
|
[请求示例] png" ) 备注:仅支持 PNG/JP G/JPEG 格式图片,大小不超过 10MB,分辨率最大为 4096×4096 p x 3. 请求及响应示例 上传图片请求示例 curl -X POST "https://rd-mokadisplay.tcl.com/ai-agent/api/v1/pqtest/upload" \ -F "file=@D:\Desktop\PQtest\3-O.png"1 / 5 ===== PAGE 2 ===== 上传图片响应示例 { "
|
||||||
|
|
||||||
|
[响应示例] test\3-O.png" ) 备注:仅支持 PNG/JP G/JPEG 格式图片,大小不超过 10MB,分辨率最大为 4096×4096 p x 3. 请求及响应示例 上传图片请求示例 curl -X POST "https://rd-mokadisplay.tcl.com/ai-agent/api/v1/pqtest/upload" \ -F "file=@D:\Desktop\PQtest\3-O.png"1 / 5 ===== PAGE 2 ===== 上传图片响
|
||||||
|
|
||||||
|
[code] -F "file=@D:\Desktop\PQtest\3-O.png"1 / 5 ===== PAGE 2 ===== 上传图片响应示例 { "code": 200, "message": "", "data": { "upload_image_url": "https://ai.file.qhmoka.com/prod-ai-portal/pq- image/input/2026-05-28/a2264f477d4d487493058306701cd
|
||||||
|
|
||||||
|
[message] esktop\PQtest\3-O.png"1 / 5 ===== PAGE 2 ===== 上传图片响应示例 { "code": 200, "message": "", "data": { "upload_image_url": "https://ai.file.qhmoka.com/prod-ai-portal/pq- image/input/2026-05-28/a2264f477d4d487493058306701cdb44/3-O.png" } }
|
||||||
|
|
||||||
|
[data] nt/ 一、上传图片接口: 1. 接口明细 接口路径:api/v1/pqt est/uplo ad Cont ent-Type:multip art/form-data 请求方式:POST 2. 请求参数说明 参数名 参数类型 是否必填 参数描述 file File(二进制文件)是待上传图片的二进制文件。请求体格式必须为multipart/form-data ,且包含一个名 为file 的文件字段(如:@"D:\Desktop\PQtest\3-O.png" ) 备注:仅支持 PNG
|
||||||
Binary file not shown.
BIN
docs/PQ生图接口文档v2.1.pdf
Normal file
BIN
docs/PQ生图接口文档v2.1.pdf
Normal file
Binary file not shown.
BIN
docs/PQ自动化工具使用指南SOP.pdf
Normal file
BIN
docs/PQ自动化工具使用指南SOP.pdf
Normal file
Binary file not shown.
@@ -1,581 +0,0 @@
|
|||||||
# -*- coding: UTF-8 -*-
|
|
||||||
import UniTAP
|
|
||||||
import time
|
|
||||||
import gc
|
|
||||||
from drivers.UCD323_Enum import UCDEnum
|
|
||||||
|
|
||||||
|
|
||||||
class UCDController:
|
|
||||||
"""UCD323信号发生器控制类"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.lUniTAP = UniTAP.TsiLib()
|
|
||||||
self.dev = None
|
|
||||||
self.role = None
|
|
||||||
self.timing_manager = None
|
|
||||||
self.config = None
|
|
||||||
self.color_info = None
|
|
||||||
self.status = False
|
|
||||||
self.current_interface = "HDMI"
|
|
||||||
|
|
||||||
self.current_timing = None
|
|
||||||
self.current_pattern = None
|
|
||||||
self.current_pattern_param = None
|
|
||||||
self.current_pattern_params = None
|
|
||||||
self.current_pattern_index = 0
|
|
||||||
|
|
||||||
def search_device(self):
|
|
||||||
"""搜索可用设备"""
|
|
||||||
available_devices = self.lUniTAP.get_list_of_available_devices()
|
|
||||||
return available_devices if available_devices else []
|
|
||||||
|
|
||||||
def open(self, device_name):
|
|
||||||
"""打开设备"""
|
|
||||||
temp_dev = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
if self.dev is not None or self.status:
|
|
||||||
self._force_cleanup()
|
|
||||||
|
|
||||||
device_id = int(device_name.split(":")[0])
|
|
||||||
temp_dev = self.lUniTAP.open(device_id)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.role = temp_dev.select_role(UniTAP.dev.UCD323.HDMISource)
|
|
||||||
self.dev = temp_dev
|
|
||||||
self.current_interface = "HDMI"
|
|
||||||
|
|
||||||
except Exception as role_error:
|
|
||||||
self._close_device_object(temp_dev)
|
|
||||||
raise role_error
|
|
||||||
|
|
||||||
pg, _ = self.get_tx_modules()
|
|
||||||
self.timing_manager = pg.timing_manager
|
|
||||||
self.color_info = UniTAP.ColorInfo()
|
|
||||||
self.status = True
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._force_cleanup()
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _reset_state(self):
|
|
||||||
"""重置所有运行时状态(不关闭设备句柄)"""
|
|
||||||
self.dev = None
|
|
||||||
self.role = None
|
|
||||||
self.status = False
|
|
||||||
self.timing_manager = None
|
|
||||||
self.current_timing = None
|
|
||||||
self.current_pattern = None
|
|
||||||
self.current_pattern_param = None
|
|
||||||
self.current_pattern_params = None
|
|
||||||
self.current_pattern_index = 0
|
|
||||||
self.current_interface = "HDMI"
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""关闭设备"""
|
|
||||||
try:
|
|
||||||
if self.dev:
|
|
||||||
self._close_device_object(self.dev)
|
|
||||||
|
|
||||||
self._reset_state()
|
|
||||||
self.lUniTAP = None
|
|
||||||
|
|
||||||
for i in range(3):
|
|
||||||
gc.collect()
|
|
||||||
|
|
||||||
time.sleep(2.0)
|
|
||||||
|
|
||||||
self.lUniTAP = UniTAP.TsiLib()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._reset_state()
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.lUniTAP = None
|
|
||||||
gc.collect()
|
|
||||||
time.sleep(2.0)
|
|
||||||
self.lUniTAP = UniTAP.TsiLib()
|
|
||||||
except Exception as init_error:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _close_device_object(self, dev_obj):
|
|
||||||
"""显式关闭设备对象"""
|
|
||||||
try:
|
|
||||||
if dev_obj is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.lUniTAP and hasattr(self.lUniTAP, "close"):
|
|
||||||
try:
|
|
||||||
self.lUniTAP.close(dev_obj)
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
|
|
||||||
dev_obj = None
|
|
||||||
gc.collect()
|
|
||||||
time.sleep(1.0)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _force_cleanup(self):
|
|
||||||
"""强制清理所有状态"""
|
|
||||||
try:
|
|
||||||
if self.dev:
|
|
||||||
self._close_device_object(self.dev)
|
|
||||||
|
|
||||||
self._reset_state()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_tx_modules(self):
|
|
||||||
"""根据当前接口返回 (pg, ag) 模块。"""
|
|
||||||
if not self.role:
|
|
||||||
raise RuntimeError("UCD 未打开,无法获取 TX 模块")
|
|
||||||
|
|
||||||
interface = getattr(self, "current_interface", None)
|
|
||||||
if interface in (None, "HDMI"):
|
|
||||||
return self.role.hdtx.pg, self.role.hdtx.ag
|
|
||||||
if interface in ("DP", "Type-C"):
|
|
||||||
return self.role.dptx.pg, self.role.dptx.ag
|
|
||||||
raise ValueError(f"不支持的接口类型: {interface}")
|
|
||||||
|
|
||||||
def _resolve_timing(self, pg=None):
|
|
||||||
"""优先从 current_timing 读取 timing,必要时回退到 TX 模块。"""
|
|
||||||
if self.current_timing is not None:
|
|
||||||
return self.current_timing
|
|
||||||
|
|
||||||
if pg is not None:
|
|
||||||
get_vm = getattr(pg, "get_vm", None)
|
|
||||||
if callable(get_vm):
|
|
||||||
try:
|
|
||||||
vm = get_vm()
|
|
||||||
return getattr(vm, "timing", None)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_current_resolution(self, default=(3840, 2160)):
|
|
||||||
"""从当前 timing 获取 (width, height),失败时返回 default。"""
|
|
||||||
try:
|
|
||||||
pg, _ = self.get_tx_modules()
|
|
||||||
timing = self._resolve_timing(pg)
|
|
||||||
if timing and hasattr(timing, "h_active") and hasattr(timing, "v_active"):
|
|
||||||
return timing.h_active, timing.v_active
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return default
|
|
||||||
|
|
||||||
def _cleanup(self):
|
|
||||||
"""清理设备资源"""
|
|
||||||
try:
|
|
||||||
if self.dev:
|
|
||||||
self._close_device_object(self.dev)
|
|
||||||
self.dev = None
|
|
||||||
|
|
||||||
if hasattr(self.lUniTAP, "cleanup"):
|
|
||||||
self.lUniTAP.cleanup()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def set_ucd_params(self, config):
|
|
||||||
"""设置UCD323参数"""
|
|
||||||
self.config = config
|
|
||||||
test_type = self.config.current_test_type
|
|
||||||
|
|
||||||
pg, _ = self.get_tx_modules()
|
|
||||||
self.timing_manager = pg.timing_manager
|
|
||||||
|
|
||||||
color_format = self.config.current_test_types[test_type]["color_format"]
|
|
||||||
bpc = self.config.current_test_types[test_type]["bpc"]
|
|
||||||
colorimetry = self.config.current_test_types[test_type]["colorimetry"]
|
|
||||||
|
|
||||||
if not self.set_color_mode(color_format, bpc, colorimetry):
|
|
||||||
return False
|
|
||||||
|
|
||||||
timing_str = self.config.current_test_types[test_type]["timing"]
|
|
||||||
self.set_timing_from_string(timing_str)
|
|
||||||
|
|
||||||
self.current_pattern_index = 0
|
|
||||||
pattern_mode = self.config.current_pattern["pattern_mode"]
|
|
||||||
pattern = UCDEnum.VideoPatternInfo.get_video_pattern(pattern_mode)
|
|
||||||
|
|
||||||
if pattern is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.current_pattern = pattern
|
|
||||||
self.current_pattern_params = self.config.current_pattern["pattern_params"]
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""运行设备"""
|
|
||||||
self.apply_video_mode()
|
|
||||||
self.apply_pattern()
|
|
||||||
pg, _ = self.get_tx_modules()
|
|
||||||
pg.apply()
|
|
||||||
return True
|
|
||||||
|
|
||||||
def send_image_pattern(self, image_path):
|
|
||||||
"""发送图片 Pattern(依赖当前 timing/color_info 状态)。"""
|
|
||||||
if not self.status or not self.role:
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
pg, _ = self.get_tx_modules()
|
|
||||||
self.apply_video_mode()
|
|
||||||
pg.set_pattern(pattern=image_path)
|
|
||||||
pg.apply()
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def send_solid_rgb_pattern(self, rgb):
|
|
||||||
"""发送纯色 RGB Pattern(依赖当前 timing/color_info 状态)。"""
|
|
||||||
if not self.status or not self.role:
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.current_pattern = UCDEnum.VideoPatternInfo.get_video_pattern("solidcolor")
|
|
||||||
if self.current_pattern is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return self.send_current_pattern_params(list(rgb))
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def send_current_pattern_params(self, pattern_params):
|
|
||||||
"""发送当前已配置的 pattern,并可附带当前 pattern 参数。"""
|
|
||||||
if not self.status or not self.role:
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
if self.current_pattern is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if pattern_params is not None and not self.set_pattern(
|
|
||||||
self.current_pattern,
|
|
||||||
pattern_params,
|
|
||||||
):
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.run()
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def set_color_mode(self, cf, bpc, cm):
|
|
||||||
"""设置颜色模式"""
|
|
||||||
current_dynamic_range = self.color_info.dynamic_range
|
|
||||||
|
|
||||||
color_format = UCDEnum.ColorInfo.get_color_format(cf)
|
|
||||||
if color_format is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not isinstance(bpc, int) or bpc <= 0:
|
|
||||||
return False
|
|
||||||
|
|
||||||
colorimetry = UCDEnum.ColorInfo.get_colorimetry(cm)
|
|
||||||
if colorimetry is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.color_info.color_format = color_format
|
|
||||||
self.color_info.bpc = bpc
|
|
||||||
self.color_info.colorimetry = colorimetry
|
|
||||||
self.color_info.dynamic_range = current_dynamic_range
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def apply_video_mode(self):
|
|
||||||
"""应用当前 color_info 和 timing"""
|
|
||||||
if self.current_timing:
|
|
||||||
self.set_video_mode()
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def set_video_mode(self):
|
|
||||||
"""设置视频模式"""
|
|
||||||
# 对比上次发出的配置,判断是否会触发电视重新锁定信号
|
|
||||||
current_config = (
|
|
||||||
self.current_timing,
|
|
||||||
self.color_info.color_format,
|
|
||||||
self.color_info.colorimetry,
|
|
||||||
self.color_info.dynamic_range,
|
|
||||||
self.color_info.bpc,
|
|
||||||
)
|
|
||||||
self.format_changed = (current_config != getattr(self, "_last_sent_config", None))
|
|
||||||
video_mode = UniTAP.VideoMode(
|
|
||||||
timing=self.current_timing, color_info=self.color_info
|
|
||||||
)
|
|
||||||
pg, _ = self.get_tx_modules()
|
|
||||||
pg.set_vm(vm=video_mode)
|
|
||||||
self._last_sent_config = current_config
|
|
||||||
return True
|
|
||||||
|
|
||||||
def set_pattern(self, pattern, pattern_params=None):
|
|
||||||
"""设置pattern"""
|
|
||||||
if self.current_timing:
|
|
||||||
needs_params = {
|
|
||||||
UCDEnum.VideoPatternInfo.VideoPatternParams.SolidColor,
|
|
||||||
UCDEnum.VideoPatternInfo.VideoPatternParams.WhiteVStrips,
|
|
||||||
UCDEnum.VideoPatternInfo.VideoPatternParams.GradientRGBStripes,
|
|
||||||
UCDEnum.VideoPatternInfo.VideoPatternParams.MotionPattern,
|
|
||||||
UCDEnum.VideoPatternInfo.VideoPatternParams.SquareWindow,
|
|
||||||
}
|
|
||||||
if pattern in needs_params and pattern_params:
|
|
||||||
self.set_pattern_params(pattern, pattern_params)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def set_next_pattern(self):
|
|
||||||
"""设置下一个pattern"""
|
|
||||||
if self.current_pattern_index < len(self.current_pattern_params):
|
|
||||||
p = self.current_pattern_params[self.current_pattern_index]
|
|
||||||
self.set_pattern(self.current_pattern, p)
|
|
||||||
self.current_pattern_index += 1
|
|
||||||
else:
|
|
||||||
error_msg = (
|
|
||||||
f"No more patterns to set. (已设置 {self.current_pattern_index} 个图案)"
|
|
||||||
)
|
|
||||||
raise IndexError(error_msg)
|
|
||||||
|
|
||||||
def set_pattern_params(self, pattern, pattern_params):
|
|
||||||
"""设置pattern参数"""
|
|
||||||
if pattern:
|
|
||||||
if pattern == UCDEnum.VideoPatternInfo.VideoPatternParams.SolidColor:
|
|
||||||
self.current_pattern_param = UniTAP.SolidColorParams(
|
|
||||||
first=pattern_params[0],
|
|
||||||
second=pattern_params[1],
|
|
||||||
third=pattern_params[2],
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def apply_pattern(self):
|
|
||||||
"""应用当前pattern"""
|
|
||||||
if self.current_pattern:
|
|
||||||
pg, _ = self.get_tx_modules()
|
|
||||||
pg.set_pattern(self.current_pattern)
|
|
||||||
|
|
||||||
if self.current_pattern_param:
|
|
||||||
pg.set_pattern_params(self.current_pattern_param)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def search_timing(self, width, height, refresh_rate, resolution_type=None):
|
|
||||||
"""根据分辨率参数搜索合适的timing"""
|
|
||||||
if resolution_type:
|
|
||||||
resolution_type = resolution_type.lower()
|
|
||||||
standard = None
|
|
||||||
if resolution_type == "dmt":
|
|
||||||
standard = UniTAP.common.timing.Timing.Standard.SD_DMT
|
|
||||||
elif resolution_type == "cta":
|
|
||||||
standard = UniTAP.common.timing.Timing.Standard.SD_CTA
|
|
||||||
elif resolution_type == "cvt":
|
|
||||||
standard = UniTAP.common.timing.Timing.Standard.SD_CVT
|
|
||||||
|
|
||||||
timing = self.timing_manager.search(
|
|
||||||
h_active=width,
|
|
||||||
v_active=height,
|
|
||||||
f_rate=int(refresh_rate) * 1000,
|
|
||||||
standard=standard,
|
|
||||||
)
|
|
||||||
|
|
||||||
if timing:
|
|
||||||
return timing
|
|
||||||
else:
|
|
||||||
for res_type in ["dmt", "cta", "cvt", "ovt"]:
|
|
||||||
result = self.search_timing(width, height, refresh_rate, res_type)
|
|
||||||
if result:
|
|
||||||
return result
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def parse_formatted_timing(self, timing_str):
|
|
||||||
"""解析格式化的timing字符串"""
|
|
||||||
if not isinstance(timing_str, str):
|
|
||||||
raise ValueError("timing_str 必须是字符串")
|
|
||||||
|
|
||||||
s = " ".join(timing_str.strip().split())
|
|
||||||
s = s.replace(" x", "x").replace("x ", "x")
|
|
||||||
|
|
||||||
parts = s.split(" ", 1)
|
|
||||||
if len(parts) < 2:
|
|
||||||
raise ValueError(f"无法解析timing: {timing_str}")
|
|
||||||
type_str = parts[0].strip().upper()
|
|
||||||
rest = parts[1].strip()
|
|
||||||
|
|
||||||
if "@" not in rest:
|
|
||||||
raise ValueError(f"无法解析timing(缺少 @): {timing_str}")
|
|
||||||
left, right = [p.strip() for p in rest.split("@", 1)]
|
|
||||||
|
|
||||||
if "x" not in left:
|
|
||||||
raise ValueError(f"无法解析分辨率(缺少 x): {timing_str}")
|
|
||||||
wh = left.split("x")
|
|
||||||
if len(wh) != 2:
|
|
||||||
raise ValueError(f"无法解析分辨率: {timing_str}")
|
|
||||||
try:
|
|
||||||
width = int(wh[0])
|
|
||||||
height = int(wh[1])
|
|
||||||
except Exception:
|
|
||||||
raise ValueError(f"分辨率数字解析失败: {timing_str}")
|
|
||||||
|
|
||||||
hz_str = right.replace("Hz", "").replace("HZ", "").strip()
|
|
||||||
try:
|
|
||||||
refresh_rate = float(hz_str)
|
|
||||||
except Exception:
|
|
||||||
raise ValueError(f"刷新率解析失败: {timing_str}")
|
|
||||||
|
|
||||||
rtype_map = {
|
|
||||||
"DMT": "dmt",
|
|
||||||
"CTA": "cta",
|
|
||||||
"CVT": "cvt",
|
|
||||||
"OVT": "ovt",
|
|
||||||
}
|
|
||||||
if type_str not in rtype_map:
|
|
||||||
raise ValueError(f"未知的分辨率类型: {type_str}")
|
|
||||||
resolution_type = rtype_map[type_str]
|
|
||||||
|
|
||||||
def find_best_id_in_dict(res_map):
|
|
||||||
best_id, best_diff = None, float("inf")
|
|
||||||
for rid, info in res_map.items():
|
|
||||||
if info["width"] == width and info["height"] == height:
|
|
||||||
diff = abs(float(info["refresh_rate"]) - refresh_rate)
|
|
||||||
if diff < best_diff:
|
|
||||||
best_diff = diff
|
|
||||||
best_id = rid
|
|
||||||
return best_id if best_diff <= 1.0 else None
|
|
||||||
|
|
||||||
def find_best_id_in_list_map(res_map):
|
|
||||||
best_id, best_diff = None, float("inf")
|
|
||||||
for rid, infos in res_map.items():
|
|
||||||
for info in infos:
|
|
||||||
if info["width"] == width and info["height"] == height:
|
|
||||||
diff = abs(float(info["refresh_rate"]) - refresh_rate)
|
|
||||||
if diff < best_diff:
|
|
||||||
best_diff = diff
|
|
||||||
best_id = rid
|
|
||||||
return best_id if best_diff <= 1.0 else None
|
|
||||||
|
|
||||||
resolution_id = None
|
|
||||||
if resolution_type == "dmt":
|
|
||||||
resolution_id = find_best_id_in_dict(UCDEnum.TimingInfo.dmt_resolution_map)
|
|
||||||
elif resolution_type == "cta":
|
|
||||||
resolution_id = find_best_id_in_dict(UCDEnum.TimingInfo.cta_resolution_map)
|
|
||||||
elif resolution_type == "cvt":
|
|
||||||
resolution_id = find_best_id_in_list_map(
|
|
||||||
UCDEnum.TimingInfo.cvt_resolution_map
|
|
||||||
)
|
|
||||||
elif resolution_type == "ovt":
|
|
||||||
resolution_id = find_best_id_in_list_map(
|
|
||||||
UCDEnum.TimingInfo.ovt_resolution_map
|
|
||||||
)
|
|
||||||
|
|
||||||
result = {
|
|
||||||
"resolution_type": resolution_type,
|
|
||||||
"width": width,
|
|
||||||
"height": height,
|
|
||||||
"refresh_rate": refresh_rate,
|
|
||||||
"resolution_id": resolution_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def set_timing_from_string(self, timing_str):
|
|
||||||
"""根据格式化timing字符串设置设备timing"""
|
|
||||||
spec = self.parse_formatted_timing(timing_str)
|
|
||||||
rtype = spec["resolution_type"]
|
|
||||||
width = spec["width"]
|
|
||||||
height = spec["height"]
|
|
||||||
fr = spec["refresh_rate"]
|
|
||||||
|
|
||||||
if rtype != "ovt":
|
|
||||||
timing = self.search_timing(width, height, fr, rtype)
|
|
||||||
if timing:
|
|
||||||
self.current_timing = timing
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def set_timing_from_id(self, rtype, rid):
|
|
||||||
"""根据(type, id)设置设备timing"""
|
|
||||||
timing = None
|
|
||||||
if rtype.lower() == "dmt":
|
|
||||||
timing = self.timing_manager.get_dmt(rid)
|
|
||||||
elif rtype.lower() == "cta":
|
|
||||||
timing = self.timing_manager.get_cta(rid)
|
|
||||||
elif rtype.lower() == "cvt":
|
|
||||||
timing = self.timing_manager.get_cvt(rid)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"不支持的分辨率类型: {rtype}")
|
|
||||||
|
|
||||||
if timing:
|
|
||||||
self.current_timing = timing
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def apply_signal_format(
|
|
||||||
self, color_space=None, data_range=None, bit_depth=None, color_format=None, **_
|
|
||||||
):
|
|
||||||
"""统一设置信号格式(color_format / colorimetry / dynamic_range / bpc)。
|
|
||||||
注:Gamma/EOTF 传输特性在 ColorInfo API 中不存在;
|
|
||||||
max_cll / max_fall 暂无对应 SDK 接口,通过 **_ 接收后忽略。
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if color_format:
|
|
||||||
fmt_key = UCDEnum.SignalFormat.OutputFormat.get_format_key(color_format)
|
|
||||||
cf = UCDEnum.ColorInfo.get_color_format(fmt_key)
|
|
||||||
if cf is not None:
|
|
||||||
self.color_info.color_format = cf
|
|
||||||
|
|
||||||
if color_space:
|
|
||||||
colorimetry = self._get_colorimetry_from_color_space(color_space, color_format)
|
|
||||||
if colorimetry:
|
|
||||||
self.color_info.colorimetry = colorimetry
|
|
||||||
|
|
||||||
if data_range:
|
|
||||||
if data_range == "Full":
|
|
||||||
self.color_info.dynamic_range = UniTAP.ColorInfo.DynamicRange.DR_VESA
|
|
||||||
elif data_range == "Limited":
|
|
||||||
self.color_info.dynamic_range = UniTAP.ColorInfo.DynamicRange.DR_CTA
|
|
||||||
|
|
||||||
if bit_depth:
|
|
||||||
bpc = UCDEnum.SignalFormat.BitDepth.get_bit_value(bit_depth)
|
|
||||||
self.color_info.bpc = bpc
|
|
||||||
|
|
||||||
if self.current_timing:
|
|
||||||
self.set_video_mode()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _get_colorimetry_from_color_space(self, color_space, color_format=None):
|
|
||||||
"""将色彩空间字符串转换为UniTAP.ColorInfo.Colorimetry。
|
|
||||||
BT.2020 在 YCbCr 输出时使用 CM_ITUR_BT2020_YCbCr,RGB 输出时使用 CM_ITUR_BT2020_RGB。
|
|
||||||
"""
|
|
||||||
is_ycbcr = UCDEnum.SignalFormat.OutputFormat.is_ycbcr(color_format)
|
|
||||||
bt2020_cm = (
|
|
||||||
UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_YCbCr
|
|
||||||
if is_ycbcr
|
|
||||||
else UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT2020_RGB
|
|
||||||
)
|
|
||||||
colorimetry_map = {
|
|
||||||
"sRGB": UniTAP.ColorInfo.Colorimetry.CM_sRGB,
|
|
||||||
"BT.709": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT709,
|
|
||||||
"BT.601": UniTAP.ColorInfo.Colorimetry.CM_ITUR_BT601,
|
|
||||||
"BT.2020": bt2020_cm,
|
|
||||||
"DCI-P3": UniTAP.ColorInfo.Colorimetry.CM_DCI_P3,
|
|
||||||
}
|
|
||||||
return colorimetry_map.get(color_space)
|
|
||||||
@@ -1,441 +0,0 @@
|
|||||||
"""UCD 驱动层。
|
|
||||||
|
|
||||||
唯一暴露给上层的入口为 :class:`IUcdDevice` 抽象接口,以及实现:
|
|
||||||
:class:`UCD323Device`(生产)和 :class:`FakeUcdDevice`(单测)。
|
|
||||||
|
|
||||||
Phase 1 实现策略
|
|
||||||
-----------------
|
|
||||||
为保证零行为变更,:class:`UCD323Device` 当前**内部委托**给已有的
|
|
||||||
:class:`drivers.UCD323_Function.UCDController`。后续 Phase 2 会将
|
|
||||||
SDK 调用直接搬入本模块,届时可删除旧 ``UCDController`` 文件。
|
|
||||||
|
|
||||||
文件分区:
|
|
||||||
§1 DeviceInfo / list_devices
|
|
||||||
§2 IUcdDevice 抽象接口
|
|
||||||
§3 UCD323Device 真实实现
|
|
||||||
§4 FakeUcdDevice 单测实现
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import threading
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from app.ucd_domain import (
|
|
||||||
ConnectionChanged,
|
|
||||||
EventBus,
|
|
||||||
Interface,
|
|
||||||
PatternApplied,
|
|
||||||
PatternKind,
|
|
||||||
PatternSpec,
|
|
||||||
SignalApplied,
|
|
||||||
SignalFormat,
|
|
||||||
TimingSpec,
|
|
||||||
UcdApplyFailed,
|
|
||||||
UcdConfigError,
|
|
||||||
UcdNotConnected,
|
|
||||||
UcdSdkError,
|
|
||||||
UcdState,
|
|
||||||
UcdStateError,
|
|
||||||
assert_transition,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from drivers.UCD323_Function import UCDController
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
# ─── §1 DeviceInfo / list_devices ────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class DeviceInfo:
|
|
||||||
"""UCD 设备发现条目。
|
|
||||||
|
|
||||||
``display`` 是 SDK 给出的完整字符串(``"0: UCD-323 #12345678"``);
|
|
||||||
``index`` / ``serial`` / ``model`` 通过解析得到,解析失败时为 None。
|
|
||||||
"""
|
|
||||||
|
|
||||||
display: str
|
|
||||||
index: int | None = None
|
|
||||||
serial: str | None = None
|
|
||||||
model: str | None = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parse(cls, display: str) -> "DeviceInfo":
|
|
||||||
idx: int | None = None
|
|
||||||
model: str | None = None
|
|
||||||
serial: str | None = None
|
|
||||||
try:
|
|
||||||
head, rest = display.split(":", 1)
|
|
||||||
idx = int(head.strip())
|
|
||||||
rest = rest.strip()
|
|
||||||
# 形如 "UCD-323 #12345678" 或 "UCD-323 #12345678 (in use)"
|
|
||||||
tokens = rest.split()
|
|
||||||
if tokens:
|
|
||||||
model = tokens[0]
|
|
||||||
for tok in tokens[1:]:
|
|
||||||
if tok.startswith("#") and len(tok) >= 2:
|
|
||||||
serial = tok.lstrip("#")
|
|
||||||
break
|
|
||||||
except Exception: # noqa: BLE001 - 解析失败保留原 display 即可
|
|
||||||
pass
|
|
||||||
return cls(display=display, index=idx, serial=serial, model=model)
|
|
||||||
|
|
||||||
|
|
||||||
def list_devices(controller: "UCDController") -> list[DeviceInfo]:
|
|
||||||
"""通过给定的底层 controller 枚举可用 UCD 设备。"""
|
|
||||||
try:
|
|
||||||
raw_list = controller.search_device()
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
raise UcdSdkError("枚举 UCD 设备失败") from exc
|
|
||||||
return [DeviceInfo.parse(s) for s in (raw_list or [])]
|
|
||||||
|
|
||||||
|
|
||||||
# ─── §2 IUcdDevice 抽象接口 ──────────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
class IUcdDevice(ABC):
|
|
||||||
"""UCD 信号发生器抽象设备。
|
|
||||||
|
|
||||||
上层(Service / GUI)**只**通过本接口操作硬件,不得穿透到
|
|
||||||
UniTAP SDK 或具体实现细节。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def state(self) -> UcdState: ...
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def info(self) -> DeviceInfo | None: ...
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def open(self, info: DeviceInfo, *, interface: Interface = Interface.HDMI) -> None:
|
|
||||||
"""打开设备并选择接口角色。失败抛 :class:`UcdSdkError` 等。"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def close(self) -> None:
|
|
||||||
"""关闭设备(幂等)。"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def configure(self, signal: SignalFormat, timing: TimingSpec) -> bool:
|
|
||||||
"""写入信号格式与 timing(未 apply)。返回 ``format_changed``。"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def set_pattern(self, pattern: PatternSpec) -> None:
|
|
||||||
"""设置当前图案(未 apply)。"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def apply(self) -> None:
|
|
||||||
"""将已配置的信号格式 + 图案一次性提交给硬件。"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def current_resolution(self) -> tuple[int, int]:
|
|
||||||
"""读取当前 timing 的 (width, height);未连接时返回默认 (3840, 2160)。"""
|
|
||||||
|
|
||||||
|
|
||||||
# ─── §3 UCD323Device 真实实现 ────────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
class UCD323Device(IUcdDevice):
|
|
||||||
"""生产环境实现。内部委托给传统 :class:`UCDController`(Phase 1)。"""
|
|
||||||
|
|
||||||
def __init__(self, bus: EventBus, controller: "UCDController | None" = None):
|
|
||||||
from drivers.UCD323_Function import UCDController as _UCDController
|
|
||||||
|
|
||||||
self._bus = bus
|
|
||||||
self._controller: "UCDController" = controller or _UCDController()
|
|
||||||
self._state: UcdState = UcdState.CLOSED
|
|
||||||
self._info: DeviceInfo | None = None
|
|
||||||
self._interface: Interface = Interface.HDMI
|
|
||||||
self._lock = threading.RLock()
|
|
||||||
|
|
||||||
self._curr_signal: SignalFormat | None = None
|
|
||||||
self._curr_timing: TimingSpec | None = None
|
|
||||||
self._curr_pattern: PatternSpec | None = None
|
|
||||||
self._last_applied: tuple[SignalFormat, TimingSpec] | None = None
|
|
||||||
|
|
||||||
# -- 读访问 --------------------------------------------------
|
|
||||||
|
|
||||||
@property
|
|
||||||
def state(self) -> UcdState:
|
|
||||||
return self._state
|
|
||||||
|
|
||||||
@property
|
|
||||||
def info(self) -> DeviceInfo | None:
|
|
||||||
return self._info
|
|
||||||
|
|
||||||
@property
|
|
||||||
def raw_controller(self) -> "UCDController":
|
|
||||||
"""Phase 1 过渡期:给暂未迁移的旧调用点的逃生通道。
|
|
||||||
|
|
||||||
新代码**不**应使用本属性,迁移完成后即可删除。
|
|
||||||
"""
|
|
||||||
return self._controller
|
|
||||||
|
|
||||||
# -- 生命周期 ------------------------------------------------
|
|
||||||
|
|
||||||
def open(self, info: DeviceInfo, *, interface: Interface = Interface.HDMI) -> None:
|
|
||||||
with self._lock:
|
|
||||||
assert_transition(self._state, UcdState.OPENED)
|
|
||||||
if interface is not Interface.HDMI:
|
|
||||||
# Phase 1:底层 UCDController.open() 写死了 HDMISource。
|
|
||||||
raise UcdConfigError(
|
|
||||||
f"暂不支持接口 {interface.value};当前仅实现 HDMI"
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
ok = self._controller.open(info.display)
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
raise UcdSdkError(f"打开设备失败: {info.display}") from exc
|
|
||||||
if not ok:
|
|
||||||
raise UcdSdkError(f"打开设备失败: {info.display}")
|
|
||||||
self._info = info
|
|
||||||
self._interface = interface
|
|
||||||
self._state = UcdState.OPENED
|
|
||||||
self._bus.publish(ConnectionChanged(True, info.serial))
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
with self._lock:
|
|
||||||
if self._state == UcdState.CLOSED:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
self._controller.close()
|
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
log.exception("关闭 UCD 时发生异常")
|
|
||||||
self._state = UcdState.CLOSED
|
|
||||||
self._curr_signal = None
|
|
||||||
self._curr_timing = None
|
|
||||||
self._curr_pattern = None
|
|
||||||
self._last_applied = None
|
|
||||||
prev_serial = self._info.serial if self._info else None
|
|
||||||
self._info = None
|
|
||||||
self._bus.publish(ConnectionChanged(False, prev_serial))
|
|
||||||
|
|
||||||
# -- 配置 ----------------------------------------------------
|
|
||||||
|
|
||||||
def configure(self, signal: SignalFormat, timing: TimingSpec) -> bool:
|
|
||||||
with self._lock:
|
|
||||||
if self._state == UcdState.CLOSED:
|
|
||||||
raise UcdNotConnected("UCD 未连接,无法 configure")
|
|
||||||
try:
|
|
||||||
# 颜色模式(color_format / bpc / colorimetry)
|
|
||||||
if not self._controller.set_color_mode(
|
|
||||||
signal.color_format.value,
|
|
||||||
int(signal.bpc),
|
|
||||||
_colorimetry_to_legacy_key(signal),
|
|
||||||
):
|
|
||||||
raise UcdConfigError(
|
|
||||||
f"set_color_mode 失败: {signal!r}"
|
|
||||||
)
|
|
||||||
# dynamic_range 在新接口中是一等公民
|
|
||||||
self._apply_dynamic_range(signal)
|
|
||||||
|
|
||||||
# Timing
|
|
||||||
if not self._controller.set_timing_from_string(str(timing)):
|
|
||||||
raise UcdConfigError(f"set_timing_from_string 失败: {timing}")
|
|
||||||
except UcdConfigError:
|
|
||||||
raise
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
raise UcdSdkError("configure 异常") from exc
|
|
||||||
|
|
||||||
self._curr_signal = signal
|
|
||||||
self._curr_timing = timing
|
|
||||||
self._state = UcdState.CONFIGURED
|
|
||||||
return (signal, timing) != self._last_applied
|
|
||||||
|
|
||||||
def set_pattern(self, pattern: PatternSpec) -> None:
|
|
||||||
with self._lock:
|
|
||||||
# Phase 2 过渡:允许从 OPENED 直接 set_pattern——遗留路径
|
|
||||||
# (test_runner 等)通过旧 controller.apply_signal_format 写入
|
|
||||||
# 信号格式,未经过本设备的 configure。此时 self._state 仍为
|
|
||||||
# OPENED,但硬件实际已处于可接收 pattern 状态。
|
|
||||||
if self._state == UcdState.CLOSED:
|
|
||||||
raise UcdNotConnected("UCD 未连接,无法 set_pattern")
|
|
||||||
self._curr_pattern = pattern
|
|
||||||
# 仅本地暂存,真正写硬件在 apply()
|
|
||||||
|
|
||||||
def apply(self) -> None:
|
|
||||||
with self._lock:
|
|
||||||
if self._state == UcdState.CLOSED:
|
|
||||||
raise UcdNotConnected("UCD 未连接,无法 apply")
|
|
||||||
if self._curr_pattern is None:
|
|
||||||
raise UcdStateError("apply 前必须先 set_pattern")
|
|
||||||
try:
|
|
||||||
ok = self._apply_pattern_via_controller(self._curr_pattern)
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
raise UcdSdkError("apply 异常") from exc
|
|
||||||
if not ok:
|
|
||||||
raise UcdApplyFailed(
|
|
||||||
f"apply 失败: pattern={self._curr_pattern.kind.value}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# SignalApplied 事件仅在通过新 API configure 过时发出;
|
|
||||||
# 遗留路径下 self._curr_signal/_curr_timing 可能为 None。
|
|
||||||
if self._curr_signal is not None and self._curr_timing is not None:
|
|
||||||
changed = (self._curr_signal, self._curr_timing) != self._last_applied
|
|
||||||
self._last_applied = (self._curr_signal, self._curr_timing)
|
|
||||||
self._bus.publish(
|
|
||||||
SignalApplied(self._curr_signal, self._curr_timing, changed)
|
|
||||||
)
|
|
||||||
self._state = UcdState.APPLIED
|
|
||||||
self._bus.publish(PatternApplied(self._curr_pattern))
|
|
||||||
|
|
||||||
# -- 查询 ----------------------------------------------------
|
|
||||||
|
|
||||||
def current_resolution(self) -> tuple[int, int]:
|
|
||||||
try:
|
|
||||||
return self._controller.get_current_resolution((3840, 2160))
|
|
||||||
except Exception: # noqa: BLE001
|
|
||||||
return (3840, 2160)
|
|
||||||
|
|
||||||
# -- 内部辅助 ------------------------------------------------
|
|
||||||
|
|
||||||
def _apply_dynamic_range(self, signal: SignalFormat) -> None:
|
|
||||||
import UniTAP # 局部导入,避免本模块在无 SDK 环境下导入即失败
|
|
||||||
|
|
||||||
from app.ucd_domain import DynamicRange
|
|
||||||
|
|
||||||
ci = self._controller.color_info
|
|
||||||
if ci is None:
|
|
||||||
return
|
|
||||||
if signal.dynamic_range is DynamicRange.FULL:
|
|
||||||
ci.dynamic_range = UniTAP.ColorInfo.DynamicRange.DR_VESA
|
|
||||||
else:
|
|
||||||
ci.dynamic_range = UniTAP.ColorInfo.DynamicRange.DR_CTA
|
|
||||||
|
|
||||||
def _apply_pattern_via_controller(self, pattern: PatternSpec) -> bool:
|
|
||||||
"""根据 PatternKind 走最合适的旧 controller 路径。"""
|
|
||||||
if pattern.kind is PatternKind.IMAGE:
|
|
||||||
if not pattern.image_path:
|
|
||||||
raise UcdConfigError("IMAGE pattern 必须提供 image_path")
|
|
||||||
return bool(self._controller.send_image_pattern(pattern.image_path))
|
|
||||||
|
|
||||||
# 预定义图案路径:复用 controller.set_pattern + run()
|
|
||||||
from drivers.UCD323_Enum import UCDEnum # 局部导入避免循环
|
|
||||||
|
|
||||||
video_pattern = UCDEnum.VideoPatternInfo.get_video_pattern(pattern.kind.value)
|
|
||||||
if video_pattern is None:
|
|
||||||
raise UcdConfigError(f"不支持的 PatternKind: {pattern.kind!r}")
|
|
||||||
self._controller.current_pattern = video_pattern
|
|
||||||
|
|
||||||
params: list[int] | None = None
|
|
||||||
if pattern.kind is PatternKind.SOLID:
|
|
||||||
if pattern.solid_rgb is None:
|
|
||||||
raise UcdConfigError("SOLID pattern 必须提供 solid_rgb")
|
|
||||||
params = list(pattern.solid_rgb)
|
|
||||||
elif pattern.extras:
|
|
||||||
params = list(pattern.extras)
|
|
||||||
|
|
||||||
if not self._controller.set_pattern(video_pattern, params):
|
|
||||||
raise UcdApplyFailed("controller.set_pattern 返回 False")
|
|
||||||
return bool(self._controller.run())
|
|
||||||
|
|
||||||
|
|
||||||
def _colorimetry_to_legacy_key(signal: SignalFormat) -> str:
|
|
||||||
"""新 :class:`Colorimetry` → 旧 ``UCDEnum.ColorInfo.get_colorimetry`` 的 key。
|
|
||||||
|
|
||||||
BT.2020 在 YCbCr / RGB 输出下走不同 SDK 枚举(参考旧
|
|
||||||
``_get_colorimetry_from_color_space`` 的逻辑),这里也做同样的分支。
|
|
||||||
"""
|
|
||||||
from app.ucd_domain import Colorimetry, is_ycbcr
|
|
||||||
|
|
||||||
cm = signal.colorimetry
|
|
||||||
ycbcr = is_ycbcr(signal.color_format)
|
|
||||||
|
|
||||||
if cm is Colorimetry.BT2020:
|
|
||||||
return "bt2020ycbcr" if ycbcr else "bt2020rgb"
|
|
||||||
return {
|
|
||||||
Colorimetry.SRGB: "srgb",
|
|
||||||
Colorimetry.BT709: "bt709",
|
|
||||||
Colorimetry.BT601: "bt601",
|
|
||||||
Colorimetry.DCI_P3: "dcip3",
|
|
||||||
Colorimetry.ADOBE_RGB: "adobergb",
|
|
||||||
}.get(cm, "srgb")
|
|
||||||
|
|
||||||
|
|
||||||
# ─── §4 FakeUcdDevice 单测实现 ───────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
class FakeUcdDevice(IUcdDevice):
|
|
||||||
"""无硬件依赖的 Fake 实现;记录调用序列供单测断言。"""
|
|
||||||
|
|
||||||
def __init__(self, bus: EventBus | None = None) -> None:
|
|
||||||
self._bus = bus or EventBus()
|
|
||||||
self._state = UcdState.CLOSED
|
|
||||||
self._info: DeviceInfo | None = None
|
|
||||||
self._signal: SignalFormat | None = None
|
|
||||||
self._timing: TimingSpec | None = None
|
|
||||||
self._pattern: PatternSpec | None = None
|
|
||||||
self._last_applied: tuple[SignalFormat, TimingSpec] | None = None
|
|
||||||
self.calls: list[tuple] = [] # ("open", info) / ("configure", ...) ...
|
|
||||||
|
|
||||||
@property
|
|
||||||
def state(self) -> UcdState:
|
|
||||||
return self._state
|
|
||||||
|
|
||||||
@property
|
|
||||||
def info(self) -> DeviceInfo | None:
|
|
||||||
return self._info
|
|
||||||
|
|
||||||
def open(self, info: DeviceInfo, *, interface: Interface = Interface.HDMI) -> None:
|
|
||||||
assert_transition(self._state, UcdState.OPENED)
|
|
||||||
self.calls.append(("open", info, interface))
|
|
||||||
self._info = info
|
|
||||||
self._state = UcdState.OPENED
|
|
||||||
self._bus.publish(ConnectionChanged(True, info.serial))
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
if self._state == UcdState.CLOSED:
|
|
||||||
return
|
|
||||||
self.calls.append(("close",))
|
|
||||||
self._state = UcdState.CLOSED
|
|
||||||
prev = self._info.serial if self._info else None
|
|
||||||
self._info = None
|
|
||||||
self._signal = self._timing = self._pattern = None
|
|
||||||
self._last_applied = None
|
|
||||||
self._bus.publish(ConnectionChanged(False, prev))
|
|
||||||
|
|
||||||
def configure(self, signal: SignalFormat, timing: TimingSpec) -> bool:
|
|
||||||
if self._state == UcdState.CLOSED:
|
|
||||||
raise UcdNotConnected()
|
|
||||||
self.calls.append(("configure", signal, timing))
|
|
||||||
self._signal, self._timing = signal, timing
|
|
||||||
self._state = UcdState.CONFIGURED
|
|
||||||
return (signal, timing) != self._last_applied
|
|
||||||
|
|
||||||
def set_pattern(self, pattern: PatternSpec) -> None:
|
|
||||||
if self._state not in (UcdState.CONFIGURED, UcdState.APPLIED):
|
|
||||||
raise UcdStateError(f"非法状态 {self._state.name}")
|
|
||||||
self.calls.append(("set_pattern", pattern))
|
|
||||||
self._pattern = pattern
|
|
||||||
|
|
||||||
def apply(self) -> None:
|
|
||||||
if self._signal is None or self._timing is None:
|
|
||||||
raise UcdStateError("apply 前必须 configure")
|
|
||||||
if self._pattern is None:
|
|
||||||
raise UcdStateError("apply 前必须 set_pattern")
|
|
||||||
self.calls.append(("apply",))
|
|
||||||
changed = (self._signal, self._timing) != self._last_applied
|
|
||||||
self._last_applied = (self._signal, self._timing)
|
|
||||||
self._state = UcdState.APPLIED
|
|
||||||
self._bus.publish(SignalApplied(self._signal, self._timing, changed))
|
|
||||||
self._bus.publish(PatternApplied(self._pattern))
|
|
||||||
|
|
||||||
def current_resolution(self) -> tuple[int, int]:
|
|
||||||
if self._timing is None:
|
|
||||||
return (3840, 2160)
|
|
||||||
return (self._timing.width, self._timing.height)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"DeviceInfo",
|
|
||||||
"list_devices",
|
|
||||||
"IUcdDevice",
|
|
||||||
"UCD323Device",
|
|
||||||
"FakeUcdDevice",
|
|
||||||
]
|
|
||||||
@@ -6,12 +6,17 @@ import time
|
|||||||
import os
|
import os
|
||||||
import datetime
|
import datetime
|
||||||
import traceback
|
import traceback
|
||||||
|
import matplotlib
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
from app_version import APP_NAME, APP_VERSION, get_app_title
|
from app_version import APP_NAME, APP_VERSION, get_app_title
|
||||||
from drivers.UCD323_Function import UCDController
|
from app.ucd import (
|
||||||
from drivers.ucd_driver import UCD323Device
|
ConnectionChanged,
|
||||||
from app.ucd_domain import EventBus
|
DeviceKind,
|
||||||
from app.services.ucd_service import SignalService
|
EventBus,
|
||||||
|
PatternService,
|
||||||
|
SignalService,
|
||||||
|
UCD323Device,
|
||||||
|
)
|
||||||
from app.pq.pq_config import PQConfig
|
from app.pq.pq_config import PQConfig
|
||||||
from app.pq.pq_result import PQResultStore
|
from app.pq.pq_result import PQResultStore
|
||||||
from app.export import (
|
from app.export import (
|
||||||
@@ -55,14 +60,12 @@ from app.plots.plot_gamut import PlotGamutMixin
|
|||||||
from app.views.chart_frame import ChartFrameMixin
|
from app.views.chart_frame import ChartFrameMixin
|
||||||
from app.config_io import ConfigIOMixin
|
from app.config_io import ConfigIOMixin
|
||||||
from app.tests.local_dimming import LocalDimmingMixin
|
from app.tests.local_dimming import LocalDimmingMixin
|
||||||
from app.services import PatternService
|
|
||||||
from app.device.connection import DeviceConnectionMixin
|
from app.device.connection import DeviceConnectionMixin
|
||||||
from app.runner.test_runner import TestRunnerMixin
|
from app.runner.test_runner import TestRunnerMixin
|
||||||
|
|
||||||
plt.rcParams["font.family"] = ["sans-serif"]
|
plt.rcParams["font.family"] = ["sans-serif"]
|
||||||
plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"]
|
plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"]
|
||||||
|
|
||||||
|
|
||||||
class PQAutomationApp(
|
class PQAutomationApp(
|
||||||
ConfigIOMixin,
|
ConfigIOMixin,
|
||||||
ChartFrameMixin,
|
ChartFrameMixin,
|
||||||
@@ -100,13 +103,10 @@ class PQAutomationApp(
|
|||||||
|
|
||||||
# 初始化设备连接状态
|
# 初始化设备连接状态
|
||||||
self.ca = None # CA410色度计
|
self.ca = None # CA410色度计
|
||||||
self.ucd = UCDController() # 信号发生器(旧接口,过渡期保留)
|
|
||||||
|
|
||||||
# 新架构:EventBus + 设备抽象 + 服务层。
|
# UCD:EventBus + 设备抽象 + 服务层;上层统一走 signal_service / ucd_device。
|
||||||
# UCD323Device 内部委托 self.ucd,保证零行为变更;
|
|
||||||
# 新代码统一走 self.signal_service。
|
|
||||||
self.event_bus = EventBus()
|
self.event_bus = EventBus()
|
||||||
self.ucd_device = UCD323Device(self.event_bus, self.ucd)
|
self.ucd_device = UCD323Device(self.event_bus)
|
||||||
self.signal_service = SignalService(self.ucd_device, self.event_bus)
|
self.signal_service = SignalService(self.ucd_device, self.event_bus)
|
||||||
|
|
||||||
# 连接控制器:统一管理 CA/UCD 生命周期。
|
# 连接控制器:统一管理 CA/UCD 生命周期。
|
||||||
@@ -205,6 +205,7 @@ class PQAutomationApp(
|
|||||||
self.create_calman_panel()
|
self.create_calman_panel()
|
||||||
# 创建测试类型选择区域
|
# 创建测试类型选择区域
|
||||||
self.create_test_type_frame()
|
self.create_test_type_frame()
|
||||||
|
self._setup_connection_event_handlers()
|
||||||
# 创建操作按钮区域
|
# 创建操作按钮区域
|
||||||
self.create_operation_frame()
|
self.create_operation_frame()
|
||||||
# 创建结果图表区域
|
# 创建结果图表区域
|
||||||
@@ -233,6 +234,23 @@ class PQAutomationApp(
|
|||||||
anchor=tk.E,
|
anchor=tk.E,
|
||||||
).pack(side=tk.RIGHT)
|
).pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
def _setup_connection_event_handlers(self) -> None:
|
||||||
|
"""订阅连接事件,驱动 UCD / CA 指示灯(替代轮询 controller.status)。"""
|
||||||
|
|
||||||
|
def on_connection_changed(evt: ConnectionChanged) -> None:
|
||||||
|
if evt.device is DeviceKind.UCD:
|
||||||
|
indicator = getattr(self, "ucd_status_indicator", None)
|
||||||
|
elif evt.device is DeviceKind.CA:
|
||||||
|
indicator = getattr(self, "ca_status_indicator", None)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
if indicator is None:
|
||||||
|
return
|
||||||
|
state = "green" if evt.connected else "gray"
|
||||||
|
self._dispatch_ui(self.update_connection_indicator, indicator, state)
|
||||||
|
|
||||||
|
self.event_bus.subscribe(ConnectionChanged, on_connection_changed)
|
||||||
|
|
||||||
def _dispatch_ui(self, fn, *args, **kwargs):
|
def _dispatch_ui(self, fn, *args, **kwargs):
|
||||||
"""把 ``fn(*args, **kwargs)`` 调度到 Tk 主线程执行。
|
"""把 ``fn(*args, **kwargs)`` 调度到 Tk 主线程执行。
|
||||||
|
|
||||||
@@ -388,54 +406,37 @@ class PQAutomationApp(
|
|||||||
if hasattr(self, "log_gui"):
|
if hasattr(self, "log_gui"):
|
||||||
self.log_gui.log(f"切换信号格式失败: {str(e)}", level="error")
|
self.log_gui.log(f"切换信号格式失败: {str(e)}", level="error")
|
||||||
|
|
||||||
def _switch_chart_tabs_by_test_type(self, test_type):
|
def _sync_custom_template_tab_visibility(self, test_type):
|
||||||
"""按测试类型切换 Gamma/EOTF 与客户模板结果 Tab。"""
|
"""按测试类型与客户模板结果状态同步客户模板 Tab 可见性。"""
|
||||||
if not hasattr(self, "chart_notebook"):
|
if not hasattr(self, "_set_custom_template_tab_visible"):
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
# 客户模板结果 Tab 只属于 SDR Movie
|
||||||
current_tabs = list(self.chart_notebook.tabs())
|
if test_type != "sdr_movie":
|
||||||
gamma_tab_id = str(self.gamma_chart_frame)
|
self._set_custom_template_tab_visible(False)
|
||||||
eotf_tab_id = str(self.eotf_chart_frame)
|
return
|
||||||
|
|
||||||
if test_type == "hdr_movie":
|
has_custom_rows = False
|
||||||
if gamma_tab_id in current_tabs:
|
tree = getattr(self, "custom_result_tree", None)
|
||||||
self.chart_notebook.forget(self.gamma_chart_frame)
|
if tree is not None:
|
||||||
if eotf_tab_id not in current_tabs:
|
try:
|
||||||
insert_pos = min(1, len(self.chart_notebook.tabs()))
|
has_custom_rows = len(tree.get_children()) > 0
|
||||||
self.chart_notebook.insert(insert_pos, self.eotf_chart_frame, text="EOTF 曲线")
|
except Exception:
|
||||||
else:
|
has_custom_rows = False
|
||||||
if eotf_tab_id in current_tabs:
|
|
||||||
self.chart_notebook.forget(self.eotf_chart_frame)
|
|
||||||
if gamma_tab_id not in current_tabs:
|
|
||||||
insert_pos = min(1, len(self.chart_notebook.tabs()))
|
|
||||||
self.chart_notebook.insert(insert_pos, self.gamma_chart_frame, text="Gamma 曲线")
|
|
||||||
|
|
||||||
custom_tab_id = str(self.custom_template_tab_frame)
|
# SDR 下仅在客户模板测试进行中,或已有客户模板结果时显示。
|
||||||
current_tabs = list(self.chart_notebook.tabs())
|
show_tab = has_custom_rows or (
|
||||||
|
getattr(self, "testing", False)
|
||||||
if test_type == "sdr_movie":
|
and getattr(self, "test_type_var", None) is not None
|
||||||
if custom_tab_id not in current_tabs:
|
and self.test_type_var.get() == "sdr_movie"
|
||||||
self.chart_notebook.add(self.custom_template_tab_frame, text="客户模板结果显示")
|
)
|
||||||
else:
|
self._set_custom_template_tab_visible(show_tab)
|
||||||
if custom_tab_id in current_tabs:
|
|
||||||
self.chart_notebook.forget(self.custom_template_tab_frame)
|
|
||||||
|
|
||||||
self.chart_notebook.update_idletasks()
|
|
||||||
except Exception as e:
|
|
||||||
if hasattr(self, "log_gui"):
|
|
||||||
self.log_gui.log(f"切换 Gamma/EOTF Tab 失败: {str(e)}", level="error")
|
|
||||||
|
|
||||||
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()
|
||||||
@@ -443,10 +444,15 @@ class PQAutomationApp(
|
|||||||
|
|
||||||
# 更新测试项目和侧边栏
|
# 更新测试项目和侧边栏
|
||||||
self.update_test_items()
|
self.update_test_items()
|
||||||
|
if hasattr(self, "refresh_connection_indicators"):
|
||||||
|
try:
|
||||||
|
self.refresh_connection_indicators()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
self.update_sidebar_selection()
|
self.update_sidebar_selection()
|
||||||
self.on_test_type_change()
|
self.on_test_type_change()
|
||||||
self._switch_signal_format_tabs(test_type)
|
self._switch_signal_format_tabs(test_type)
|
||||||
self._switch_chart_tabs_by_test_type(test_type)
|
self._sync_custom_template_tab_visibility(test_type)
|
||||||
self.sync_gamut_toolbar()
|
self.sync_gamut_toolbar()
|
||||||
self._restore_charts_for_type(test_type)
|
self._restore_charts_for_type(test_type)
|
||||||
|
|
||||||
@@ -479,7 +485,7 @@ class PQAutomationApp(
|
|||||||
|
|
||||||
def _check_start_preconditions(self):
|
def _check_start_preconditions(self):
|
||||||
"""检查开始测试前置条件:设备连接 & 未在测试中。"""
|
"""检查开始测试前置条件:设备连接 & 未在测试中。"""
|
||||||
if self.ca is None or self.ucd is None:
|
if self.ca is None or not self.signal_service.is_connected:
|
||||||
messagebox.showerror("错误", "请先连接CA410和信号发生器")
|
messagebox.showerror("错误", "请先连接CA410和信号发生器")
|
||||||
return False
|
return False
|
||||||
if self.testing:
|
if self.testing:
|
||||||
@@ -820,8 +826,8 @@ class PQAutomationApp(
|
|||||||
print("配置已清理,不再保存")
|
print("配置已清理,不再保存")
|
||||||
|
|
||||||
# 断开设备连接
|
# 断开设备连接
|
||||||
if self.ucd.status:
|
if self.signal_service.is_connected:
|
||||||
self.ucd.close()
|
self.connection.disconnect_ucd()
|
||||||
if self.ca is not None:
|
if self.ca is not None:
|
||||||
self.ca.close()
|
self.ca.close()
|
||||||
|
|
||||||
|
|||||||
@@ -108,11 +108,11 @@ a = Analysis(
|
|||||||
'drivers.baseSerail',
|
'drivers.baseSerail',
|
||||||
'drivers.caSerail',
|
'drivers.caSerail',
|
||||||
'drivers.tvSerail',
|
'drivers.tvSerail',
|
||||||
'drivers.UCD323_Enum',
|
'app.ucd',
|
||||||
'drivers.UCD323_Function',
|
'app.ucd.domain',
|
||||||
'drivers.ucd_driver',
|
'app.ucd.enum',
|
||||||
'app.ucd_domain',
|
'app.ucd.device',
|
||||||
'app.services.ucd_service',
|
'app.ucd.service',
|
||||||
],
|
],
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
hooksconfig={},
|
hooksconfig={},
|
||||||
|
|||||||
@@ -9,11 +9,11 @@
|
|||||||
"cct",
|
"cct",
|
||||||
"contrast"
|
"contrast"
|
||||||
],
|
],
|
||||||
"timing": "OVT 1280x 720 @ 120Hz",
|
"timing": "DMT 1920x 1080 @ 60Hz",
|
||||||
"data_range": "Full",
|
"data_range": "Full",
|
||||||
"color_format": "RGB",
|
"color_format": "RGB",
|
||||||
"bpc": 8,
|
"bpc": 8,
|
||||||
"colorimetry": "DCI-P3",
|
"colorimetry": "sRGB",
|
||||||
"patterns": {
|
"patterns": {
|
||||||
"gamut": "rgb",
|
"gamut": "rgb",
|
||||||
"gamma": "gray",
|
"gamma": "gray",
|
||||||
@@ -25,16 +25,18 @@
|
|||||||
"x_tolerance": 0.003,
|
"x_tolerance": 0.003,
|
||||||
"y_ideal": 0.329,
|
"y_ideal": 0.329,
|
||||||
"y_tolerance": 0.003
|
"y_tolerance": 0.003
|
||||||
},
|
}
|
||||||
"gamut_reference": "DCI-P3"
|
|
||||||
},
|
},
|
||||||
"sdr_movie": {
|
"sdr_movie": {
|
||||||
"name": "SDR Movie测试",
|
"name": "SDR Movie测试",
|
||||||
"test_items": [
|
"test_items": [
|
||||||
"gamut",
|
"gamut",
|
||||||
|
"gamma",
|
||||||
|
"cct",
|
||||||
|
"contrast",
|
||||||
"accuracy"
|
"accuracy"
|
||||||
],
|
],
|
||||||
"timing": "OVT 1280x 720 @ 120Hz",
|
"timing": "DMT 1920x 1080 @ 60Hz",
|
||||||
"data_range": "Full",
|
"data_range": "Full",
|
||||||
"color_format": "RGB",
|
"color_format": "RGB",
|
||||||
"bpc": 8,
|
"bpc": 8,
|
||||||
@@ -52,7 +54,7 @@
|
|||||||
"y_ideal": 0.329,
|
"y_ideal": 0.329,
|
||||||
"y_tolerance": 0.003
|
"y_tolerance": 0.003
|
||||||
},
|
},
|
||||||
"gamut_reference": "DCI-P3"
|
"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