Compare commits

...

8 Commits

Author SHA1 Message Date
xinzhu.yin
38222ff002 修改UI细节错误 2026-06-10 11:39:08 +08:00
xinzhu.yin
3206079c63 修复色准结果图偏移 2026-06-09 17:15:54 +08:00
xinzhu.yin
25be4b7f4a 修复日志切换错误 2026-06-09 15:40:44 +08:00
xinzhu.yin
f33984affa 修复LocalDimming测试错误 2026-06-09 15:22:20 +08:00
xinzhu.yin
8916f2fff0 修改SDR色准深色异常、修改保存结果深色模式异常 2026-06-09 11:02:55 +08:00
xinzhu.yin
9ad9cf9aa0 添加手动设置窗口亮度、曲线图生成 2026-06-08 11:39:54 +08:00
xinzhu.yin
e4890d9d8d 添加瞬时峰值测试 2026-06-08 11:14:12 +08:00
xinzhu.yin
febbb28a4c 修改部分UI、修改module中心点设定、添加单独连接 2026-06-08 11:03:10 +08:00
21 changed files with 1162 additions and 636 deletions

View File

@@ -160,8 +160,7 @@ class ConnectionController:
def check_all_async(self) -> None: def check_all_async(self) -> None:
"""异步并联检测 UCD + CA通过 ``_dispatch_ui`` 回主线程更新 UI。""" """异步并联检测 UCD + CA通过 ``_dispatch_ui`` 回主线程更新 UI。"""
app = self._app app = self._app
app.check_button.configure(state="disabled") self._set_connect_widgets_state("disabled")
app.refresh_button.configure(state="disabled")
app.status_var.set("正在检测连接...") app.status_var.set("正在检测连接...")
app.root.update() app.root.update()
@@ -185,6 +184,58 @@ class ConnectionController:
threading.Thread(target=worker, daemon=True).start() threading.Thread(target=worker, daemon=True).start()
def check_ucd_async(self) -> None:
"""仅异步连接 UCD323。"""
app = self._app
self._set_connect_widgets_state("disabled")
app.status_var.set("正在连接 UCD323...")
app.root.update()
def worker():
try:
ucd_ok = self.connect_ucd(app.ucd_list_var.get())
app._dispatch_ui(
app.update_connection_indicator,
app.ucd_status_indicator,
ucd_ok,
)
app._dispatch_ui(
app.status_var.set,
"UCD323 连接成功" if ucd_ok else "UCD323 连接失败",
)
app._dispatch_ui(self._enable_widgets)
except Exception as exc: # noqa: BLE001
app._dispatch_ui(app.log_gui.log, f"UCD323 连接出错: {exc}")
app._dispatch_ui(self._enable_widgets)
threading.Thread(target=worker, daemon=True).start()
def check_ca_async(self) -> None:
"""仅异步连接 CA410。"""
app = self._app
self._set_connect_widgets_state("disabled")
app.status_var.set("正在连接 CA410...")
app.root.update()
def worker():
try:
ca_ok = self.connect_ca()
app._dispatch_ui(
app.update_connection_indicator,
app.ca_status_indicator,
ca_ok,
)
app._dispatch_ui(
app.status_var.set,
"CA410 连接成功" if ca_ok else "CA410 连接失败",
)
app._dispatch_ui(self._enable_widgets)
except Exception as exc: # noqa: BLE001
app._dispatch_ui(app.log_gui.log, f"CA410 连接出错: {exc}")
app._dispatch_ui(self._enable_widgets)
threading.Thread(target=worker, daemon=True).start()
def disconnect_all(self) -> None: def disconnect_all(self) -> None:
try: try:
self.disconnect_ucd() self.disconnect_ucd()
@@ -196,6 +247,24 @@ class ConnectionController:
self._log(f"断开连接时发生错误: {exc}", level="info") self._log(f"断开连接时发生错误: {exc}", level="info")
messagebox.showerror("错误", f"断开连接失败: {exc}") messagebox.showerror("错误", f"断开连接失败: {exc}")
def disconnect_ucd_only(self) -> None:
try:
self.disconnect_ucd()
self._app.refresh_connection_indicators()
self._app.status_var.set("UCD323 已断开")
except Exception as exc: # noqa: BLE001
self._log(f"断开 UCD323 时发生错误: {exc}", level="info")
messagebox.showerror("错误", f"断开 UCD323 失败: {exc}")
def disconnect_ca_only(self) -> None:
try:
self.disconnect_ca()
self._app.refresh_connection_indicators()
self._app.status_var.set("CA410 已断开")
except Exception as exc: # noqa: BLE001
self._log(f"断开 CA410 时发生错误: {exc}", level="info")
messagebox.showerror("错误", f"断开 CA410 失败: {exc}")
def refresh_ports(self) -> None: def refresh_ports(self) -> None:
"""刷新 UCD + COM 端口下拉框;指示器复位。""" """刷新 UCD + COM 端口下拉框;指示器复位。"""
app = self._app app = self._app
@@ -219,8 +288,21 @@ class ConnectionController:
# -- 内部 ---------------------------------------------------- # -- 内部 ----------------------------------------------------
def _enable_widgets(self) -> None: def _enable_widgets(self) -> None:
self._app.check_button.configure(state="normal") self._set_connect_widgets_state("normal")
self._app.refresh_button.configure(state="normal")
def _set_connect_widgets_state(self, state: str) -> None:
for attr in (
"check_button",
"ucd_connect_button",
"ca_connect_button",
"refresh_button",
):
widget = getattr(self._app, attr, None)
if widget is not None:
try:
widget.configure(state=state)
except Exception: # noqa: BLE001
pass
def _log(self, msg: str, *, level: str = "info") -> None: def _log(self, msg: str, *, level: str = "info") -> None:
log_gui = getattr(self._app, "log_gui", None) log_gui = getattr(self._app, "log_gui", None)
@@ -250,6 +332,14 @@ def check_com_connections(self: "PQAutomationApp"):
self.connection.check_all_async() self.connection.check_all_async()
def check_ucd_connection(self: "PQAutomationApp"):
self.connection.check_ucd_async()
def check_ca_connection(self: "PQAutomationApp"):
self.connection.check_ca_async()
def update_connection_indicator(self: "PQAutomationApp", indicator, connected): def update_connection_indicator(self: "PQAutomationApp", indicator, connected):
_draw_connection_indicator(indicator, "green" if connected else "red") _draw_connection_indicator(indicator, "green" if connected else "red")
@@ -307,6 +397,14 @@ def disconnect_com_connections(self: "PQAutomationApp"):
self.connection.disconnect_all() self.connection.disconnect_all()
def disconnect_ucd_connection(self: "PQAutomationApp"):
self.connection.disconnect_ucd_only()
def disconnect_ca_connection(self: "PQAutomationApp"):
self.connection.disconnect_ca_only()
def _get_ca_measure_lock(self: "PQAutomationApp"): def _get_ca_measure_lock(self: "PQAutomationApp"):
lock = getattr(self, "_ca_measure_lock", None) lock = getattr(self, "_ca_measure_lock", None)
if lock is None: if lock is None:
@@ -357,11 +455,15 @@ __all__ = [
"get_available_com_ports", "get_available_com_ports",
"refresh_com_ports", "refresh_com_ports",
"check_com_connections", "check_com_connections",
"check_ucd_connection",
"check_ca_connection",
"update_connection_indicator", "update_connection_indicator",
"refresh_connection_indicators", "refresh_connection_indicators",
"check_port_connection", "check_port_connection",
"enable_com_widgets", "enable_com_widgets",
"disconnect_com_connections", "disconnect_com_connections",
"disconnect_ucd_connection",
"disconnect_ca_connection",
] ]
@@ -373,11 +475,15 @@ class DeviceConnectionMixin:
get_available_com_ports = get_available_com_ports get_available_com_ports = get_available_com_ports
refresh_com_ports = refresh_com_ports refresh_com_ports = refresh_com_ports
check_com_connections = check_com_connections check_com_connections = check_com_connections
check_ucd_connection = check_ucd_connection
check_ca_connection = check_ca_connection
update_connection_indicator = update_connection_indicator update_connection_indicator = update_connection_indicator
refresh_connection_indicators = refresh_connection_indicators refresh_connection_indicators = refresh_connection_indicators
check_port_connection = check_port_connection check_port_connection = check_port_connection
enable_com_widgets = enable_com_widgets enable_com_widgets = enable_com_widgets
disconnect_com_connections = disconnect_com_connections disconnect_com_connections = disconnect_com_connections
disconnect_ucd_connection = disconnect_ucd_connection
disconnect_ca_connection = disconnect_ca_connection
_get_ca_measure_lock = _get_ca_measure_lock _get_ca_measure_lock = _get_ca_measure_lock
_read_ca_display = _read_ca_display _read_ca_display = _read_ca_display
read_ca_xyLv = read_ca_xyLv read_ca_xyLv = read_ca_xyLv

View File

@@ -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}")

View File

@@ -1,14 +1,19 @@
"""CIE 色度图底图渲染与缓存。 """
CIE 色度图底图渲染与缓存(工业版)
"重型图像渲染"colour-science 的谱迹颜色填充)与"轻量框架数据层" 特点:
(参考/实测三角形、标签、覆盖率)解耦。 - colour-science 谱迹渲染
- numpy RGBA 缓存
- 内存 + 磁盘缓存
- 支持 light / dark UI
- 启动预热
- 线程安全
底图 调用方式
- 仅在首次调用或缓存失效时通过 colour-science 渲染一次;
- 渲染结果保存为 numpy RGBA 数组,同时落盘到 settings/cache/ bg, bbox = get_cie1931_background(mode="dark")
下次启动直接 imread 加载,避免重新跑色彩科学计算。 ax.imshow(bg, extent=bbox)
调用方在每次绘图时只需 `ax.imshow(bg, extent=bbox)`,再叠加自己的矢量层。
""" """
from __future__ import annotations from __future__ import annotations
@@ -20,91 +25,140 @@ from typing import Tuple
import numpy as np import numpy as np
# 谱迹底图分辨率边长单位像素。1024 对于 14 inch 画布足够细腻, # ----------------------------
# 文件大小 ~1-2MB单次渲染 ~0.5-1 s缓存后毫秒级加载。 # 配置
# ----------------------------
# 渲染分辨率
_DIAGRAM_RES = 1024 _DIAGRAM_RES = 1024
# 缓存版本号:当渲染参数或风格调整时递增,强制重新生成。 # 缓存版本(风格变化时递增)
_CACHE_VERSION = "v1" _CACHE_VERSION = "v2"
_BBox = Tuple[float, float, float, float] # (xmin, xmax, ymin, ymax) # UI 颜色
_DARK_BG = "#0f1115"
_LIGHT_BG = "#ffffff"
_BBox = Tuple[float, float, float, float]
_CIE1931_BBOX: _BBox = (0.0, 0.8, 0.0, 0.9) _CIE1931_BBOX: _BBox = (0.0, 0.8, 0.0, 0.9)
_CIE1976_BBOX: _BBox = (0.0, 0.65, 0.0, 0.6) _CIE1976_BBOX: _BBox = (0.0, 0.65, 0.0, 0.6)
_memory_cache: dict[str, np.ndarray] = {} _memory_cache: dict[str, np.ndarray] = {}
_lock = threading.Lock() _lock = threading.Lock()
# ----------------------------
# cache path
# ----------------------------
def _cache_dir() -> str: def _cache_dir() -> str:
# 项目根目录通过本文件位置反推app/plots/ -> 项目根
here = os.path.dirname(os.path.abspath(__file__)) here = os.path.dirname(os.path.abspath(__file__))
root = os.path.abspath(os.path.join(here, "..", "..")) root = os.path.abspath(os.path.join(here, "..", ".."))
d = os.path.join(root, "settings", "cache") d = os.path.join(root, "settings", "cache")
os.makedirs(d, exist_ok=True) os.makedirs(d, exist_ok=True)
return d return d
def _cache_key(kind: str, bbox: _BBox) -> str: def _cache_key(kind: str, bbox: _BBox, mode: str) -> str:
sig = f"{kind}|{bbox}|{_DIAGRAM_RES}|{_CACHE_VERSION}" sig = f"{kind}|{bbox}|{mode}|{_DIAGRAM_RES}|{_CACHE_VERSION}"
h = hashlib.md5(sig.encode("utf-8")).hexdigest()[:10] h = hashlib.md5(sig.encode()).hexdigest()[:10]
return f"chromaticity_{kind}_{h}.npy"
return f"chromaticity_{kind}_{mode}_{h}.npy"
def _cache_path(kind: str, bbox: _BBox) -> str: def _cache_path(kind: str, bbox: _BBox, mode: str) -> str:
return os.path.join(_cache_dir(), _cache_key(kind, bbox)) return os.path.join(_cache_dir(), _cache_key(kind, bbox, mode))
def _render_chromaticity(kind: str, bbox: _BBox) -> np.ndarray: # ----------------------------
"""通过 colour-science 离屏渲染谱迹底图,返回 RGBA float 数组。""" # 渲染
# 延迟导入:仅在缓存未命中时支付 colour.plotting 的加载开销。 # ----------------------------
def _render_chromaticity(kind: str, bbox: _BBox, mode: str) -> np.ndarray:
"""
通过 colour-science 渲染 chromaticity 图。
"""
import matplotlib import matplotlib
prev_backend = matplotlib.get_backend() prev_backend = matplotlib.get_backend()
try: try:
matplotlib.use("Agg", force=True) matplotlib.use("Agg", force=True)
except Exception: except Exception:
pass pass
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import colour
from colour.plotting import ( from colour.plotting import (
plot_chromaticity_diagram_CIE1931, plot_chromaticity_diagram_CIE1931,
plot_chromaticity_diagram_CIE1976UCS, plot_chromaticity_diagram_CIE1976UCS,
) )
if mode == "dark":
colour.plotting.colour_style("dark")
bg_color = _DARK_BG
else:
colour.plotting.colour_style("light")
bg_color = _LIGHT_BG
xmin, xmax, ymin, ymax = bbox xmin, xmax, ymin, ymax = bbox
aspect = (xmax - xmin) / (ymax - ymin) aspect = (xmax - xmin) / (ymax - ymin)
height = _DIAGRAM_RES height = _DIAGRAM_RES
width = int(round(height * aspect)) width = int(round(height * aspect))
fig = plt.figure(figsize=(width / 100.0, height / 100.0), dpi=100) fig = plt.figure(
ax = fig.add_axes([0.0, 0.0, 1.0, 1.0]) figsize=(width / 100.0, height / 100.0),
dpi=100
)
fig.patch.set_facecolor(bg_color)
ax = fig.add_axes([0, 0, 1, 1])
ax.set_facecolor(bg_color)
if kind == "cie1931": if kind == "cie1931":
plot_chromaticity_diagram_CIE1931( plot_chromaticity_diagram_CIE1931(
axes=ax, show=False, title=False, axes=ax,
tight_layout=False, transparent_background=True, show=False,
title=False,
tight_layout=False,
bounding_box=bbox, bounding_box=bbox,
transparent_background=False,
) )
elif kind == "cie1976": elif kind == "cie1976":
plot_chromaticity_diagram_CIE1976UCS( plot_chromaticity_diagram_CIE1976UCS(
axes=ax, show=False, title=False, axes=ax,
tight_layout=False, transparent_background=True, show=False,
title=False,
tight_layout=False,
bounding_box=bbox, bounding_box=bbox,
transparent_background=False,
) )
else: else:
plt.close(fig) plt.close(fig)
raise ValueError(f"unknown diagram kind: {kind!r}") raise ValueError(f"unknown diagram kind: {kind}")
ax.set_xlim(xmin, xmax) ax.set_xlim(xmin, xmax)
ax.set_ylim(ymin, ymax) ax.set_ylim(ymin, ymax)
ax.set_axis_off() ax.set_axis_off()
ax.set_position([0.0, 0.0, 1.0, 1.0]) ax.set_position([0, 0, 1, 1])
fig.canvas.draw() fig.canvas.draw()
# 从 canvas 抓取 RGBA 数组
buf = np.asarray(fig.canvas.buffer_rgba()).copy() buf = np.asarray(fig.canvas.buffer_rgba()).copy()
buf = np.flipud(buf) buf = np.flipud(buf)
plt.close(fig) plt.close(fig)
try: try:
@@ -115,52 +169,107 @@ def _render_chromaticity(kind: str, bbox: _BBox) -> np.ndarray:
return buf return buf
def _load_or_render(kind: str, bbox: _BBox) -> np.ndarray: # ----------------------------
key = _cache_key(kind, bbox) # load / render
# ----------------------------
def _load_or_render(kind: str, bbox: _BBox, mode: str) -> np.ndarray:
key = _cache_key(kind, bbox, mode)
with _lock: with _lock:
if key in _memory_cache: if key in _memory_cache:
return _memory_cache[key] return _memory_cache[key]
disk = _cache_path(kind, bbox) disk = _cache_path(kind, bbox, mode)
if os.path.isfile(disk): if os.path.isfile(disk):
try: try:
arr = np.load(disk) arr = np.load(disk)
_memory_cache[key] = arr _memory_cache[key] = arr
return arr return arr
except Exception: except Exception:
# 缓存损坏则重新渲染
try: try:
os.remove(disk) os.remove(disk)
except OSError: except OSError:
pass pass
arr = _render_chromaticity(kind, bbox) arr = _render_chromaticity(kind, bbox, mode)
_memory_cache[key] = arr _memory_cache[key] = arr
try: try:
np.save(disk, arr) np.save(disk, arr)
except Exception: except Exception:
pass pass
return arr return arr
def get_cie1931_background() -> Tuple[np.ndarray, _BBox]: # ----------------------------
"""返回 (RGBA 数组, bbox),可直接 ax.imshow(arr, extent=[*bbox])。""" # public API
return _load_or_render("cie1931", _CIE1931_BBOX), _CIE1931_BBOX # ----------------------------
def get_cie1931_background(mode: str = "dark") -> Tuple[np.ndarray, _BBox]:
"""
获取 CIE1931 背景图
mode:
"dark"
"light"
"""
return _load_or_render("cie1931", _CIE1931_BBOX, mode), _CIE1931_BBOX
def get_cie1976_background() -> Tuple[np.ndarray, _BBox]: def get_cie1976_background(mode: str = "dark") -> Tuple[np.ndarray, _BBox]:
return _load_or_render("cie1976", _CIE1976_BBOX), _CIE1976_BBOX
return _load_or_render("cie1976", _CIE1976_BBOX, mode), _CIE1976_BBOX
# ----------------------------
# cache control
# ----------------------------
def clear_cache(*, disk: bool = False) -> None: def clear_cache(*, disk: bool = False) -> None:
"""清空内存缓存(可选连同磁盘)。供调试/样式调整时使用。""" """
清空缓存
"""
with _lock: with _lock:
_memory_cache.clear() _memory_cache.clear()
if disk: if disk:
d = _cache_dir() d = _cache_dir()
for name in os.listdir(d): for name in os.listdir(d):
if name.startswith("chromaticity_") and name.endswith(".npy"):
if name.startswith("chromaticity_"):
try: try:
os.remove(os.path.join(d, name)) os.remove(os.path.join(d, name))
except OSError: except OSError:
pass pass
# ----------------------------
# warmup
# ----------------------------
def warmup_cache(mode: str = "dark") -> None:
"""
启动预热缓存
可在软件启动时调用,避免首次绘图卡顿。
"""
get_cie1931_background(mode)
get_cie1976_background(mode)

View File

@@ -9,15 +9,10 @@ from typing import TYPE_CHECKING
from matplotlib.patches import Rectangle from matplotlib.patches import Rectangle
from matplotlib.lines import Line2D from matplotlib.lines import Line2D
import matplotlib.colors as mcolors from matplotlib.ticker import MultipleLocator, AutoMinorLocator
import numpy as np
from app.views.modern_styles import get_theme_palette from app.views.modern_styles import get_theme_palette
from app.plots.gamut_background import get_cie1976_background from app.plots.gamut_background import get_cie1976_background
from app.tests.color_accuracy import get_accuracy_color_standards from app.tests.color_accuracy import get_accuracy_color_standards
from app.pq.color_patch_map import get_patch_color
from app.pq.color_patch_map import get_patch_color_from_xy
if TYPE_CHECKING: if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp from pqAutomationApp import PQAutomationApp
@@ -68,13 +63,13 @@ def _xy_to_uv(x: float, y: float):
return 0.0, 0.0 return 0.0, 0.0
return (4.0 * x) / denom, (9.0 * y) / denom return (4.0 * x) / denom, (9.0 * y) / denom
# ============================================================ # ============================================================
# 子图:左侧 Calman 风格面板 # 子图:左侧 Calman 风格面板
# ============================================================ # ============================================================
def _draw_left_panel(ax, color_patches, delta_e_values, font_scale=1.0, dark_mode=False): def _draw_left_panel(ax, color_patches, delta_e_values, font_scale=1.0, dark_mode=False):
"""左侧仅保留大条形图""" """左侧仅保留大条形图"""
ax.clear() ax.clear()
n = len(color_patches) n = len(color_patches)
@@ -85,37 +80,43 @@ def _draw_left_panel(ax, color_patches, delta_e_values, font_scale=1.0, dark_mod
y_pos = list(range(n)) y_pos = list(range(n))
bar_colors = [_COLOR_MAP.get(name, "#888888") for name in color_patches] bar_colors = [_COLOR_MAP.get(name, "#888888") for name in color_patches]
edgecolor = "#F3F5F7" if dark_mode else "#202020"
text_color = "#F3F5F7" if dark_mode else "#111111"
bg_color = "#0F1115" if dark_mode else "#FFFFFF"
ax.barh( ax.barh(
y_pos, y_pos,
delta_e_values, delta_e_values,
height=0.72, height=0.72,
color=bar_colors, color=bar_colors,
edgecolor="#202020", edgecolor=edgecolor,
linewidth=0.5, linewidth=0.5,
zorder=3, zorder=3,
) )
text_color = "#F3F5F7" if dark_mode else "#111111"
bg_color = "#0F1115" if dark_mode else "#FFFFFF"
spine_color = "#8C8F94" if dark_mode else "#9A9A9A"
ax.set_yticks(y_pos) ax.set_yticks(y_pos)
ax.set_yticklabels(color_patches, fontsize=max(5, 7 * font_scale), color=text_color) ax.set_yticklabels(color_patches, fontsize=max(5, 7 * font_scale), color=text_color)
ax.invert_yaxis() ax.invert_yaxis()
x_max = max(15.0, max(delta_e_values) * 1.15) x_max = max(15.0, max(delta_e_values) * 1.15)
ax.set_xlim(0, x_max) ax.set_xlim(0, x_max)
ax.tick_params(axis="x", labelsize=max(6, 8 * font_scale), colors=text_color)
ax.grid(axis="x", linestyle="-", linewidth=0.6, alpha=0.3, zorder=0) ax.tick_params(
ax.grid(axis="y", linestyle=":", linewidth=0.35, alpha=0.15, zorder=0) axis="x",
labelsize=max(6, 8 * font_scale),
colors=text_color
)
ax.tick_params(
axis="y",
labelsize=max(5, 7 * font_scale),
colors=text_color
)
ax.set_facecolor(bg_color) ax.set_facecolor(bg_color)
for spine in ax.spines.values():
spine.set_color(spine_color)
spine.set_linewidth(0.9)
# 自动 minor tick
ax.xaxis.set_minor_locator(AutoMinorLocator(2))
# ============================================================ # ============================================================
@@ -126,7 +127,7 @@ def _draw_uv_diagram(ax, color_patches, measurements, standards, font_scale=1.0,
"""绘制 CIE 1976 u'v' 上的色准对比。""" """绘制 CIE 1976 u'v' 上的色准对比。"""
ax.clear() ax.clear()
try: try:
bg, bbox = get_cie1976_background() bg, bbox = get_cie1976_background(mode="dark" if dark_mode else "light")
if bg.shape[-1] == 4: if bg.shape[-1] == 4:
bg = bg[:, :, :3] bg = bg[:, :, :3]
xmin, xmax, ymin, ymax = bbox xmin, xmax, ymin, ymax = bbox
@@ -154,6 +155,11 @@ def _draw_uv_diagram(ax, color_patches, measurements, standards, font_scale=1.0,
ax.set_facecolor("#000" if dark_mode else "#FFFFFF") ax.set_facecolor("#000" if dark_mode else "#FFFFFF")
ax.set_aspect("equal", adjustable="box") ax.set_aspect("equal", adjustable="box")
ax.xaxis.set_major_locator(MultipleLocator(0.1))
ax.yaxis.set_major_locator(MultipleLocator(0.1))
ax.xaxis.set_minor_locator(MultipleLocator(0.02))
ax.yaxis.set_minor_locator(MultipleLocator(0.02))
ax.set_title( ax.set_title(
"CIE 1976 u'v'", "CIE 1976 u'v'",
@@ -309,7 +315,7 @@ def _draw_result_judgement(ax, accuracy_data, font_scale=1.0, dark_mode=False):
# ============================================================ # ============================================================
def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type): def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type):
"""绘制色准测试结果 - Calman 风格(色块 + CIE 1976 u'v' + 统计)。""" """绘制色准测试结果"""
palette = get_theme_palette() palette = get_theme_palette()
try: try:
from app.views.theme_manager import is_dark from app.views.theme_manager import is_dark
@@ -329,10 +335,14 @@ def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type):
fig.patch.set_facecolor(palette["bg"]) fig.patch.set_facecolor(palette["bg"])
# 根据当前画布像素尺寸动态缩放字体,避免窗口缩小时文字挤压重叠 # 先确保色准页签已激活,再读取真实画布尺寸进行动态缩放
font_scale = 1.0 font_scale = 1.0
try: try:
self.chart_notebook.select(self.accuracy_chart_frame)
self.chart_notebook.update_idletasks()
canvas_widget = self.accuracy_canvas.get_tk_widget() canvas_widget = self.accuracy_canvas.get_tk_widget()
canvas_widget.update_idletasks()
canvas_widget.update()
cw = max(1, int(canvas_widget.winfo_width())) cw = max(1, int(canvas_widget.winfo_width()))
ch = max(1, int(canvas_widget.winfo_height())) ch = max(1, int(canvas_widget.winfo_height()))
font_scale = min(cw / 1000.0, ch / 600.0) font_scale = min(cw / 1000.0, ch / 600.0)
@@ -417,11 +427,13 @@ def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type):
except Exception: except Exception:
pass pass
# Select the tab first so the canvas is visible and winfo_width/height # 重新刷新布局并绘制,确保画布尺寸与 notebook tab 对齐。
# return real pixel dimensions before the figure is rendered.
self.chart_notebook.select(self.accuracy_chart_frame) self.chart_notebook.select(self.accuracy_chart_frame)
self.accuracy_canvas.get_tk_widget().update_idletasks() canvas_widget = self.accuracy_canvas.get_tk_widget()
canvas_widget.update_idletasks()
canvas_widget.update()
self.accuracy_canvas.draw() self.accuracy_canvas.draw()
canvas_widget.update_idletasks()
class PlotAccuracyMixin: class PlotAccuracyMixin:

View File

@@ -83,6 +83,15 @@ def plot_cct(self: "PQAutomationApp", test_type):
self.log_gui.log(f" y范围: {min(y_measured):.6f} - {max(y_measured):.6f}", level="info") self.log_gui.log(f" y范围: {min(y_measured):.6f} - {max(y_measured):.6f}", level="info")
# ========== 根据测试类型读取对应参数 ========== # ========== 根据测试类型读取对应参数 ==========
# 屏模组中心坐标优先使用实测 100% 灰阶点gray 数据第 1 个点)
screen_center_xy = None
if test_type == "screen_module":
try:
if gray_data and len(gray_data[0]) >= 2:
screen_center_xy = (float(gray_data[0][0]), float(gray_data[0][1]))
except Exception:
screen_center_xy = None
if test_type == "sdr_movie": if test_type == "sdr_movie":
try: try:
x_ideal = float(self.sdr_cct_x_ideal_var.get()) x_ideal = float(self.sdr_cct_x_ideal_var.get())
@@ -111,11 +120,23 @@ def plot_cct(self: "PQAutomationApp", test_type):
self.log_gui.log("HDR 参数读取失败,使用默认值", level="error") self.log_gui.log("HDR 参数读取失败,使用默认值", level="error")
else: # screen_module else: # screen_module
try: try:
x_ideal = float(self.cct_x_ideal_var.get())
x_tolerance = float(self.cct_x_tolerance_var.get()) x_tolerance = float(self.cct_x_tolerance_var.get())
y_ideal = float(self.cct_y_ideal_var.get())
y_tolerance = float(self.cct_y_tolerance_var.get()) y_tolerance = float(self.cct_y_tolerance_var.get())
self.log_gui.log("使用屏模组色度参数", level="success") if screen_center_xy is not None:
x_ideal, y_ideal = screen_center_xy
# 同步到输入框,避免界面显示和实际计算不一致
try:
self.cct_x_ideal_var.set(f"{x_ideal:.6f}")
self.cct_y_ideal_var.set(f"{y_ideal:.6f}")
except Exception:
pass
self.log_gui.log(
f"屏模组中心使用实测100%坐标: x={x_ideal:.6f}, y={y_ideal:.6f}"
, level="success")
else:
x_ideal = float(self.cct_x_ideal_var.get())
y_ideal = float(self.cct_y_ideal_var.get())
self.log_gui.log("未取到实测100%点,回退到屏模组色度参数", level="error")
except: except:
x_ideal = 0.306 x_ideal = 0.306
x_tolerance = 0.003 x_tolerance = 0.003

View File

@@ -271,7 +271,7 @@ def plot_gamut(self: "PQAutomationApp", results, coverage, test_type):
# 左图CIE 1931 xy # 左图CIE 1931 xy
# ============================================================ # ============================================================
try: try:
bg_xy, bbox_xy = get_cie1931_background() bg_xy, bbox_xy = get_cie1931_background(mode="dark" if dark_mode else "light")
_blit_background(ax_xy, bg_xy, bbox_xy) _blit_background(ax_xy, bg_xy, bbox_xy)
_style_axes( _style_axes(
ax_xy, ax_xy,
@@ -343,7 +343,7 @@ def plot_gamut(self: "PQAutomationApp", results, coverage, test_type):
# 右图CIE 1976 u'v' # 右图CIE 1976 u'v'
# ============================================================ # ============================================================
try: try:
bg_uv, bbox_uv = get_cie1976_background() bg_uv, bbox_uv = get_cie1976_background(mode="dark" if dark_mode else "light")
_blit_background(ax_uv, bg_uv, bbox_uv) _blit_background(ax_uv, bg_uv, bbox_uv)
_style_axes( _style_axes(
ax_uv, ax_uv,

View File

@@ -341,9 +341,9 @@ def send_fix_pattern(self: "PQAutomationApp", mode):
self.pattern_service.send_session_pattern(session, 0) self.pattern_service.send_session_pattern(session, 0)
if format_changed: if format_changed:
signal_settle = max(1.0, float(getattr(self, "signal_settle_time", 5.0))) signal_settle = max(1.0, float(getattr(self, "signal_settle_time", 4.0)))
self.log_gui.log( self.log_gui.log(
f"信号格式已变化,等待电视重新锁定: {signal_settle:.1f}s(可通过 signal_settle_time 调整)", f"信号格式已变化,等待电视重新锁定: {signal_settle:.1f}s",
level="info", level="info",
) )
else: else:
@@ -831,6 +831,20 @@ def test_cct(self: "PQAutomationApp", test_type, gray_data=None):
self.log_gui.log(f"使用 {len(results)} 个灰阶数据点进行色度计算", level="info") self.log_gui.log(f"使用 {len(results)} 个灰阶数据点进行色度计算", level="info")
# 屏模组测试:中心坐标直接使用本次灰阶 100% 实测值(第 1 个点)
if test_type == "screen_module":
try:
if results and len(results[0]) >= 2:
x_100 = float(results[0][0])
y_100 = float(results[0][1])
self.cct_x_ideal_var.set(f"{x_100:.6f}")
self.cct_y_ideal_var.set(f"{y_100:.6f}")
self.log_gui.log(
f"屏模组 CCT 中心采用 100% 实测值: x={x_100:.6f}, y={y_100:.6f}"
, level="success")
except Exception as e:
self.log_gui.log(f"同步屏模组100%中心坐标失败: {str(e)}", level="error")
# 提取色度坐标 # 提取色度坐标
cct_values = pq_algorithm.calculate_cct_from_results(results) cct_values = pq_algorithm.calculate_cct_from_results(results)

View File

@@ -8,6 +8,7 @@ import atexit
import csv import csv
import datetime import datetime
import os import os
import re
import shutil import shutil
import sys import sys
import threading import threading
@@ -15,6 +16,7 @@ import time
import tkinter as tk import tkinter as tk
from tkinter import filedialog, messagebox from tkinter import filedialog, messagebox
import matplotlib.pyplot as plt
import numpy as np import numpy as np
from PIL import Image from PIL import Image
@@ -34,6 +36,9 @@ DEFAULT_WINDOW_PERCENTAGES = [1, 2, 5, 10, 18, 25, 50, 75, 100]
DEFAULT_CHESSBOARD_GRID = 5 DEFAULT_CHESSBOARD_GRID = 5
INSTANT_PEAK_WINDOW_PERCENTAGE = 10 INSTANT_PEAK_WINDOW_PERCENTAGE = 10
INSTANT_PEAK_CAPTURE_DELAY = 0.5 INSTANT_PEAK_CAPTURE_DELAY = 0.5
INSTANT_PEAK_DROP_RATIO = 0.97
INSTANT_PEAK_MIN_DROP_NITS = 2.0
INSTANT_PEAK_SAMPLE_INTERVAL = 0.3
_TEMP_DIR = None _TEMP_DIR = None
_IMAGE_CACHE = {} # {(width, height, percentage): file_path} _IMAGE_CACHE = {} # {(width, height, percentage): file_path}
@@ -63,8 +68,8 @@ def _get_temp_dir():
return _TEMP_DIR return _TEMP_DIR
def _make_window_image_array(width, height, percentage): def _make_window_image_array(width, height, percentage, window_level=255):
"""生成黑底+居中白窗的 numpy 图像,保持屏幕比例。""" """生成黑底+居中窗口图像,保持屏幕比例。"""
image = np.zeros((height, width, 3), dtype=np.uint8) image = np.zeros((height, width, 3), dtype=np.uint8)
if percentage >= 100: if percentage >= 100:
ww, wh = width, height ww, wh = width, height
@@ -74,18 +79,19 @@ def _make_window_image_array(width, height, percentage):
wh = int(height * scale) wh = int(height * scale)
x1 = (width - ww) // 2 x1 = (width - ww) // 2
y1 = (height - wh) // 2 y1 = (height - wh) // 2
image[y1:y1 + wh, x1:x1 + ww] = 255 image[y1:y1 + wh, x1:x1 + ww] = int(window_level)
return image return image
def _ensure_window_image(width, height, percentage): def _ensure_window_image(width, height, percentage, window_level=255):
"""生成或复用缓存的窗口 PNG 文件,返回路径。""" """生成或复用缓存的窗口 PNG 文件,返回路径。"""
key = (width, height, percentage) level = max(0, min(255, int(window_level)))
key = (width, height, percentage, level)
cached = _IMAGE_CACHE.get(key) cached = _IMAGE_CACHE.get(key)
if cached and os.path.exists(cached): if cached and os.path.exists(cached):
return cached return cached
arr = _make_window_image_array(width, height, percentage) arr = _make_window_image_array(width, height, percentage, level)
fname = f"window_{width}x{height}_{percentage:03d}percent.png" fname = f"window_{width}x{height}_{percentage:03d}percent_{level:03d}lv.png"
path = os.path.join(_get_temp_dir(), fname) path = os.path.join(_get_temp_dir(), fname)
Image.fromarray(arr, mode="RGB").save(path, format="PNG") Image.fromarray(arr, mode="RGB").save(path, format="PNG")
_IMAGE_CACHE[key] = path _IMAGE_CACHE[key] = path
@@ -151,36 +157,6 @@ def _ensure_checkerboard_image(width, height, grid_size, center_white):
_IMAGE_CACHE[key] = path _IMAGE_CACHE[key] = path
return path return path
def _build_ld_result_row(test_item, pattern_label, value, x="--", y="--"):
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
if isinstance(value, (int, float, np.floating)):
display_value = f"{float(value):.4f}"
else:
display_value = str(value)
return {
"test_item": test_item,
"pattern": pattern_label,
"value": display_value,
"x": x if isinstance(x, str) else f"{x:.4f}",
"y": y if isinstance(y, str) else f"{y:.4f}",
"time": timestamp,
}
def _measure_ld_row(self: "PQAutomationApp", test_item, pattern_label):
"""读取一次 CA410 数据并包装为表格行。"""
x, y, lv, _X, _Y, _Z = self.read_ca_xyLv()
if lv is None:
raise RuntimeError(f"{pattern_label} 采集失败")
return _build_ld_result_row(test_item, pattern_label, lv, x, y), lv
def _send_ld_image(self: "PQAutomationApp", image_path):
self.signal_service.send_image(image_path)
def _apply_ld_ucd_params(self: "PQAutomationApp") -> bool: def _apply_ld_ucd_params(self: "PQAutomationApp") -> bool:
"""发送 Local Dimming 图案前,按当前测试类型写入 UCD 参数。""" """发送 Local Dimming 图案前,按当前测试类型写入 UCD 参数。"""
test_type = getattr(self.config, "current_test_type", "screen_module") test_type = getattr(self.config, "current_test_type", "screen_module")
@@ -309,231 +285,445 @@ def _apply_ld_ucd_params(self: "PQAutomationApp") -> bool:
return False return False
def _run_ld_measurement_step(self: "PQAutomationApp", width, height, wait_time, step, log):
label = step["label"]
test_item = step["test_item"]
kind = step["kind"]
if kind == "window":
percentage = step["percentage"]
image_path = _ensure_window_image(width, height, percentage)
_send_ld_image(self, image_path)
settle_time = wait_time
elif kind == "black":
image_path = _ensure_solid_image(width, height, (0, 0, 0), "black")
_send_ld_image(self, image_path)
settle_time = wait_time
elif kind == "checkerboard":
image_path = _ensure_checkerboard_image(
width,
height,
DEFAULT_CHESSBOARD_GRID,
step["center_white"],
)
_send_ld_image(self, image_path)
settle_time = wait_time
elif kind == "instant_peak":
black_image = _ensure_solid_image(width, height, (0, 0, 0), "black")
peak_image = _ensure_window_image(
width,
height,
step["percentage"],
)
_send_ld_image(self, black_image)
log(f" 黑场预置 {wait_time:.1f}", level="info")
time.sleep(wait_time)
_send_ld_image(self, peak_image)
settle_time = min(wait_time, INSTANT_PEAK_CAPTURE_DELAY)
else:
raise ValueError(f"未知 Local Dimming 测试步骤: {kind}")
log(f" 等待 {settle_time:.1f} 秒后采集...", level="info")
time.sleep(settle_time)
return _measure_ld_row(self, test_item, label)
def _set_current_ld_pattern(self: "PQAutomationApp", test_item, pattern_label, percentage=None): def _set_current_ld_pattern(self: "PQAutomationApp", test_item, pattern_label, percentage=None):
self.current_ld_test_item = test_item self.current_ld_test_item = test_item
self.current_ld_pattern_label = pattern_label self.current_ld_pattern_label = pattern_label
self.current_ld_percentage = percentage self.current_ld_percentage = percentage
def _send_ld_pattern_async(self: "PQAutomationApp", image_builder, success_msg, fail_msg):
"""统一的 Local Dimming 图案发送线程"""
# -------------------------------------------------------------------------- def worker():
# GUI 入口(绑定为 PQAutomationApp 方法)
# --------------------------------------------------------------------------
def start_local_dimming_test(self: "PQAutomationApp"):
"""Local Dimming 不再提供自动测试,保留接口仅提示用户使用手动模式。"""
messagebox.showinfo("提示", "Local Dimming 请使用手动发送图案后再采集亮度")
def update_ld_results(self: "PQAutomationApp", results):
"""把批量测试结果填入 Treeview。"""
for row in results:
self.ld_tree.insert(
"", tk.END,
values=(
row["test_item"],
row["pattern"],
row["value"],
row["x"],
row["y"],
row["time"],
),
)
def stop_local_dimming_test(self: "PQAutomationApp"):
"""兼容旧接口,无操作。"""
return
def send_ld_window(self: "PQAutomationApp", percentage):
"""发送指定百分比的白色窗口(手动模式)。"""
if not self.signal_service.is_connected:
messagebox.showwarning("警告", "请先连接 UCD323 设备")
return
self.log_gui.log(f"🔆 发送 {percentage}% 窗口...", level="info")
_set_current_ld_pattern(self, "峰值亮度", f"{percentage}%窗口", percentage)
def send():
if not _apply_ld_ucd_params(self): if not _apply_ld_ucd_params(self):
return return
width, height = self.signal_service.current_resolution() width, height = self.signal_service.current_resolution()
try: try:
image_path = _ensure_window_image(width, height, percentage) image_path = image_builder(width, height)
except Exception as e: except Exception as e:
self._dispatch_ui(self.log_gui.log, f"图像生成失败: {e}") self._dispatch_ui(self.log_gui.log, f"图像生成失败: {e}")
return return
try: try:
self.signal_service.send_image(image_path) self.signal_service.send_image(image_path)
ok = True ok = True
except Exception: except Exception:
ok = False ok = False
msg = (
f"{percentage}% 窗口已发送" if ok msg = success_msg if ok else fail_msg
else f"{percentage}% 窗口发送失败"
)
self._dispatch_ui(self.log_gui.log, msg) self._dispatch_ui(self.log_gui.log, msg)
threading.Thread(target=send, daemon=True).start() threading.Thread(target=worker, daemon=True).start()
# --------------------------------------------------------------------------
# GUI 入口(绑定为 PQAutomationApp 方法)
# --------------------------------------------------------------------------
def send_ld_window(self: "PQAutomationApp", percentage):
FIXED_WINDOW_PERCENTAGE = 40
try:
luminance_percent = float(percentage)
if luminance_percent < 1 or luminance_percent > 100:
raise ValueError
except Exception:
messagebox.showwarning("参数错误", "亮度范围应为 1-100")
return
if not self.signal_service.is_connected:
messagebox.showwarning("警告", "请先连接 UCD323 设备")
return
window_level = int(round(luminance_percent / 100 * 255))
self.log_gui.log(
f"发送 {FIXED_WINDOW_PERCENTAGE}%窗口(亮度{luminance_percent:.0f}%...",
level="info",
)
_set_current_ld_pattern(
self,
"峰值亮度",
f"{FIXED_WINDOW_PERCENTAGE}%窗口({luminance_percent:.0f}%亮度)",
FIXED_WINDOW_PERCENTAGE,
)
def builder(width, height):
return _ensure_window_image(
width,
height,
FIXED_WINDOW_PERCENTAGE,
window_level,
)
_send_ld_pattern_async(
self,
builder,
f"{FIXED_WINDOW_PERCENTAGE}%窗口({luminance_percent:.0f}%亮度)已发送",
f"{FIXED_WINDOW_PERCENTAGE}%窗口({luminance_percent:.0f}%亮度)发送失败",
)
def send_ld_manual_window(self: "PQAutomationApp"):
"""按手动输入的窗口大小和亮度发送窗口图案。"""
if not self.signal_service.is_connected:
messagebox.showwarning("警告", "请先连接 UCD323 设备")
return
try:
percentage = int(float(self.ld_window_percentage_var.get()))
if percentage < 1 or percentage > 100:
raise ValueError("窗口范围应为 1-100")
except Exception as e:
messagebox.showwarning("参数错误", f"窗口百分比无效: {e}")
return
try:
luminance_percent = float(self.ld_window_luminance_var.get())
if luminance_percent < 1 or luminance_percent > 100:
raise ValueError("亮度范围应为 1-100")
except Exception as e:
messagebox.showwarning("参数错误", f"窗口亮度无效: {e}")
return
window_level = int(round(luminance_percent / 100.0 * 255.0))
self.log_gui.log(
f"发送 {percentage}%窗口(亮度{luminance_percent:.0f}%...",
level="info",
)
_set_current_ld_pattern(
self,
"峰值亮度",
f"{percentage}%窗口({luminance_percent:.0f}%亮度)",
percentage,
)
def builder(width, height):
return _ensure_window_image(
width,
height,
percentage,
window_level,
)
_send_ld_pattern_async(
self,
builder,
f"{percentage}%窗口({luminance_percent:.0f}%亮度)已发送",
f"{percentage}%窗口({luminance_percent:.0f}%亮度)发送失败",
)
def send_ld_checkerboard(self: "PQAutomationApp", center_white): def send_ld_checkerboard(self: "PQAutomationApp", center_white):
"""发送棋盘格图案(手动模式)。"""
if not self.signal_service.is_connected: if not self.signal_service.is_connected:
messagebox.showwarning("警告", "请先连接 UCD323 设备") messagebox.showwarning("警告", "请先连接 UCD323 设备")
return return
pattern_label = "棋盘格(中心白)" if center_white else "棋盘格(中心黑)" pattern_label = "棋盘格(中心白)" if center_white else "棋盘格(中心黑)"
self.log_gui.log(f"🔲 发送 {pattern_label}...", level="info")
self.log_gui.log(f"发送 {pattern_label}...", level="info")
_set_current_ld_pattern(self, "棋盘格对比度", pattern_label) _set_current_ld_pattern(self, "棋盘格对比度", pattern_label)
def send(): def builder(width, height):
if not _apply_ld_ucd_params(self): return _ensure_checkerboard_image(
return
width, height = self.signal_service.current_resolution()
try:
image_path = _ensure_checkerboard_image(
width, width,
height, height,
DEFAULT_CHESSBOARD_GRID, DEFAULT_CHESSBOARD_GRID,
center_white, 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"):
self.ld_peak_start_btn.configure(state="disabled")
if hasattr(self, "ld_peak_stop_btn"):
self.ld_peak_stop_btn.configure(state="normal")
def run():
peak_lv = None
peak_time = None
drop_time = None
curve_count = 0
try:
if not _apply_ld_ucd_params(self): if not _apply_ld_ucd_params(self):
return return
width, height = self.signal_service.current_resolution() width, height = self.signal_service.current_resolution()
try:
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 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",
) )
self._dispatch_ui(self.log_gui.log, msg) break
threading.Thread(target=send, daemon=True).start() x, y, lv, _X, _Y, _Z = self.read_ca_xyLv()
if lv is None:
time.sleep(sample_interval)
continue
lv = float(lv)
# 更新峰值
if peak_lv is None or lv > peak_lv:
peak_lv = lv
peak_time = elapsed
# 曲线记录
if record_curve:
curve_count += 1
self._dispatch_ui(
self._insert_ld_tree_item,
values=(
"瞬时峰值曲线",
f"{window_percentage}%窗口@{window_luminance_percent:.0f}% t={elapsed:.2f}s",
f"{lv:.4f}",
f"{x:.4f}",
f"{y:.4f}",
datetime.datetime.now().strftime("%H:%M:%S"),
),
)
# 回落检测
if peak_lv is not None:
drop_threshold = peak_lv * (1 - drop_percent / 100.0)
if lv < drop_threshold and elapsed > (peak_time or 0):
drop_time = elapsed
break
self._dispatch_ui(
self.ld_result_label.config,
text=f"亮度:{lv:.2f} cd/m² | 峰值:{(peak_lv or lv):.2f} cd/m² | t:{elapsed:.2f}s",
)
time.sleep(sample_interval)
if peak_lv is None:
self._dispatch_ui(
self.log_gui.log,
"瞬时峰值测试未采到有效亮度",
"warning",
)
return
end_time = drop_time if drop_time is not None else (time.time() - started)
sustain_time = max(0.0, end_time - (peak_time or 0))
result_label = (
f"峰值={peak_lv:.2f} cd/m², 持续={sustain_time:.2f}s"
if drop_time is not None
else f"峰值={peak_lv:.2f} cd/m², 持续>{sustain_time:.2f}s"
)
self._dispatch_ui(
self._insert_ld_tree_item,
values=(
"瞬时峰值亮度",
pattern_label,
result_label,
"--",
"--",
datetime.datetime.now().strftime("%H:%M:%S"),
),
)
self._dispatch_ui(
self.log_gui.log,
f"瞬时峰值测试完成: {result_label},曲线点 {curve_count}",
"success",
)
except Exception as e:
self._dispatch_ui(
self.log_gui.log,
f"瞬时峰值测试异常: {e}",
"error",
)
finally:
self.ld_peak_tracking = False
if hasattr(self, "ld_peak_start_btn"):
self._dispatch_ui(
self.ld_peak_start_btn.configure,
state="normal",
)
if hasattr(self, "ld_peak_stop_btn"):
self._dispatch_ui(
self.ld_peak_stop_btn.configure,
state="disabled",
)
threading.Thread(target=run, daemon=True).start()
def stop_ld_instant_peak_tracking(self: "PQAutomationApp"):
"""停止独立瞬时峰值连续采样"""
if getattr(self, "ld_peak_tracking", False):
self.ld_peak_tracking = False
self.log_gui.log("已请求停止瞬时峰值测试", level="info")
def _insert_ld_tree_item(self, parent="", index=tk.END, **kwargs):
item = self.ld_tree.insert(parent, index, **kwargs)
try:
self.ld_tree.see(item)
except Exception:
pass
return item
def measure_ld_luminance(self: "PQAutomationApp"): def measure_ld_luminance(self: "PQAutomationApp"):
@@ -545,8 +735,6 @@ def measure_ld_luminance(self: "PQAutomationApp"):
messagebox.showinfo("提示", "请先发送一个窗口图案") messagebox.showinfo("提示", "请先发送一个窗口图案")
return return
self.log_gui.log("📏 正在采集亮度...", level="info")
def measure(): def measure():
try: try:
x, y, lv, _X, _Y, _Z = self.read_ca_xyLv() x, y, lv, _X, _Y, _Z = self.read_ca_xyLv()
@@ -562,7 +750,7 @@ def measure_ld_luminance(self: "PQAutomationApp"):
text=f"亮度: {lv:.2f} cd/m² | x: {x:.4f} | y: {y:.4f}", text=f"亮度: {lv:.2f} cd/m² | x: {x:.4f} | y: {y:.4f}",
) )
self._dispatch_ui( self._dispatch_ui(
self.ld_tree.insert, "", tk.END, self._insert_ld_tree_item,
values=( values=(
getattr(self, "current_ld_test_item", "手动采集"), getattr(self, "current_ld_test_item", "手动采集"),
self.current_ld_pattern_label, self.current_ld_pattern_label,
@@ -585,6 +773,7 @@ def clear_ld_records(self: "PQAutomationApp"):
self.current_ld_percentage = None self.current_ld_percentage = None
self.current_ld_test_item = None self.current_ld_test_item = None
self.current_ld_pattern_label = None self.current_ld_pattern_label = None
self.ld_peak_tracking = False
self.log_gui.log("测试记录已清空", level="info") self.log_gui.log("测试记录已清空", level="info")
@@ -619,17 +808,111 @@ def save_local_dimming_results(self: "PQAutomationApp"):
messagebox.showerror("错误", f"保存失败: {str(e)}") messagebox.showerror("错误", f"保存失败: {str(e)}")
def plot_ld_instant_peak_curve(self: "PQAutomationApp"):
"""绘制最近一次瞬时峰值测试的亮度-时间曲线"""
pattern = re.compile(r"t\s*=\s*([0-9]+(?:\.[0-9]+)?)s")
curve_points = []
# 从表格底部向上找最近一次曲线
items = list(self.ld_tree.get_children())[::-1]
collecting = False
for item in items:
values = self.ld_tree.item(item, "values")
if len(values) < 3:
continue
test_item = str(values[0])
pattern_text = str(values[1])
lv_text = str(values[2])
if test_item == "瞬时峰值曲线":
collecting = True
else:
if collecting:
break
continue
match = pattern.search(pattern_text)
if not match:
continue
try:
t_sec = float(match.group(1))
lv = float(lv_text)
except Exception:
continue
curve_points.append((t_sec, lv))
if not curve_points:
messagebox.showinfo("提示", "没有可绘制的瞬时峰值曲线数据")
return
# 时间排序
curve_points.sort(key=lambda x: x[0])
t_data = [p[0] for p in curve_points]
lv_data = [p[1] for p in curve_points]
fig = plt.figure(figsize=(8.6, 4.6))
ax = fig.add_subplot(111)
ax.plot(
t_data,
lv_data,
"-o",
linewidth=1.8,
markersize=3.5,
color="#2a9d8f",
)
ax.set_title("Instant Peak Luminance Curve")
ax.set_xlabel("Time (s)")
ax.set_ylabel("Luminance (cd/m²)")
ax.grid(True, linestyle="--", alpha=0.35)
# 标记峰值
peak_idx = int(np.argmax(lv_data))
ax.scatter(
[t_data[peak_idx]],
[lv_data[peak_idx]],
color="#e76f51",
zorder=3,
)
ax.annotate(
f"Peak: {lv_data[peak_idx]:.2f} cd/m² @ {t_data[peak_idx]:.2f}s",
(t_data[peak_idx], lv_data[peak_idx]),
xytext=(8, 10),
textcoords="offset points",
fontsize=9,
color="#333333",
)
fig.tight_layout()
plt.show(block=False)
self.log_gui.log("已生成本次瞬时峰值曲线图", level="success")
class LocalDimmingMixin: class LocalDimmingMixin:
"""由 tools/refactor_to_mixins.py 自动生成。 """由 tools/refactor_to_mixins.py 自动生成。
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。 把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
""" """
start_local_dimming_test = start_local_dimming_test
update_ld_results = update_ld_results
stop_local_dimming_test = stop_local_dimming_test
send_ld_window = send_ld_window send_ld_window = send_ld_window
send_ld_manual_window = send_ld_manual_window
send_ld_checkerboard = send_ld_checkerboard send_ld_checkerboard = send_ld_checkerboard
send_ld_black_pattern = send_ld_black_pattern send_ld_black_pattern = send_ld_black_pattern
send_ld_instant_peak = send_ld_instant_peak start_ld_instant_peak_tracking = start_ld_instant_peak_tracking
stop_ld_instant_peak_tracking = stop_ld_instant_peak_tracking
measure_ld_luminance = measure_ld_luminance measure_ld_luminance = measure_ld_luminance
clear_ld_records = clear_ld_records clear_ld_records = clear_ld_records
save_local_dimming_results = save_local_dimming_results save_local_dimming_results = save_local_dimming_results
plot_ld_instant_peak_curve = plot_ld_instant_peak_curve
_insert_ld_tree_item = _insert_ld_tree_item

View File

@@ -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
# 隐藏所有面板 # 隐藏所有面板

View File

@@ -415,9 +415,7 @@ def toggle_ai_image_panel(self: "PQAutomationApp"):
self.show_panel("ai_image") self.show_panel("ai_image")
_apply_ai_image_list_style(self) _apply_ai_image_list_style(self)
if not getattr(self, "_ai_image_list_loaded", False): if not getattr(self, "_ai_image_list_loaded", False):
logger.info("[AIImagePanel] 首次显示面板,开始加载列表") _start_new_session(self)
reload_ai_image_list(self)
self._ai_image_list_loaded = True
def _get_app_base_dir(self: "PQAutomationApp") -> str: def _get_app_base_dir(self: "PQAutomationApp") -> str:
@@ -618,7 +616,6 @@ def _on_list_select(self: "PQAutomationApp"):
if getattr(self, "_ai_image_reloading", False): if getattr(self, "_ai_image_reloading", False):
return return
if getattr(self, "_ai_image_select_guard", False): if getattr(self, "_ai_image_select_guard", False):
logger.debug("[AIImagePanel] 忽略重入选择事件")
return return
sel = self.ai_image_tree.selection() sel = self.ai_image_tree.selection()
if not sel: if not sel:
@@ -631,7 +628,6 @@ def _on_list_select(self: "PQAutomationApp"):
if ridx is None: if ridx is None:
session_id = _session_id_for_item(self, item_id) session_id = _session_id_for_item(self, item_id)
if session_id: if session_id:
logger.info("[AIImagePanel] 选中会话头 sid=%s", session_id[:8])
_switch_to_session(self, session_id, show_message=False, refresh_list=False) _switch_to_session(self, session_id, show_message=False, refresh_list=False)
return return
if 0 <= ridx < len(self.ai_image_records): if 0 <= ridx < len(self.ai_image_records):
@@ -848,7 +844,7 @@ def _switch_to_session(
if sid == _svc.get_session_id(): if sid == _svc.get_session_id():
return return
_svc.set_session_id(sid) _svc.set_session_id(sid)
logger.info( logger.debug(
"[AIImagePanel] 切换会话 sid=%s refresh=%s target=%s", "[AIImagePanel] 切换会话 sid=%s refresh=%s target=%s",
sid[:8], sid[:8],
refresh_list, refresh_list,

View File

@@ -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,6 +70,10 @@ 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% 点自动决定,避免手动误改。
if key in ("x_ideal", "y_ideal"):
entry.configure(state="readonly")
else:
# 绑定失去焦点事件 # 绑定失去焦点事件
default_val = screen_default_cct_params[key] default_val = screen_default_cct_params[key]
entry.bind( entry.bind(
@@ -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"):

View File

@@ -194,12 +194,6 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"):
"<<ComboboxSelected>>", lambda e: _on_preset_selected(self) "<<ComboboxSelected>>", lambda e: _on_preset_selected(self)
) )
ttk.Button(
preset_row1, text="加载",
bootstyle="info-outline", width=8,
command=lambda: _load_selected_preset(self),
).pack(side=tk.LEFT, padx=2)
ttk.Button( ttk.Button(
preset_row1, text="应用为当前", preset_row1, text="应用为当前",
bootstyle="success", width=12, bootstyle="success", width=12,
@@ -274,7 +268,7 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"):
right = ttk.Frame(mid) right = ttk.Frame(mid)
right.grid(row=0, column=1, sticky=tk.NS) right.grid(row=0, column=1, sticky=tk.NS)
edit_frame = ttk.LabelFrame(right, text="编辑选中点", padding=8) edit_frame = ttk.LabelFrame(right, text="编辑选中点", padding=8)
edit_frame.pack(fill=tk.X) edit_frame.pack(fill=tk.X)
self._gamma_edit_r_var = tk.StringVar() self._gamma_edit_r_var = tk.StringVar()
@@ -358,27 +352,22 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"):
command=lambda: _paste_from_clipboard(self), command=lambda: _paste_from_clipboard(self),
).pack(fill=tk.X, pady=(6, 0)) ).pack(fill=tk.X, pady=(6, 0))
# ===== 底部 ===== # ---- 右侧:校验与保存(与编辑区放在一起) ----
bottom = ttk.LabelFrame(root, text="校验与保存", padding=8) save_box = ttk.LabelFrame(right, text="校验与保存", padding=8)
bottom.pack(fill=tk.X, pady=(10, 0)) save_box.pack(fill=tk.X, pady=(10, 0))
self._gamma_validate_label = ttk.Label( self._gamma_validate_label = ttk.Label(
bottom, text="", style="Muted.TLabel", justify=tk.LEFT save_box, text="", style="Muted.TLabel", justify=tk.LEFT
) )
self._gamma_validate_label.pack(anchor=tk.W) self._gamma_validate_label.pack(anchor=tk.W)
save_row = ttk.Frame(bottom) save_row = ttk.Frame(save_box)
save_row.pack(fill=tk.X, pady=(6, 0)) save_row.pack(fill=tk.X, pady=(6, 0))
ttk.Button( ttk.Button(
save_row, text="保存改动到当前预设", save_row, text="保存改动到当前预设",
bootstyle="primary", bootstyle="primary",
command=lambda: _save_to_current_preset(self), command=lambda: _save_to_current_preset(self),
).pack(side=tk.LEFT) ).pack(side=tk.LEFT)
ttk.Button(
save_row, text="应用到运行时 (gray.json)",
bootstyle="success",
command=lambda: _apply_current_to_runtime(self),
).pack(side=tk.LEFT, padx=(6, 0))
ttk.Button( ttk.Button(
save_row, text="另存为新预设...", save_row, text="另存为新预设...",
bootstyle="info-outline", bootstyle="info-outline",

View File

@@ -292,65 +292,9 @@ def create_signal_format_content(self: "PQAutomationApp"):
self.sdr_signal_frame.grid_columnconfigure(1, weight=1) self.sdr_signal_frame.grid_columnconfigure(1, weight=1)
self.signal_tabs.add(self.sdr_signal_frame, text="SDR测试") self.signal_tabs.add(self.sdr_signal_frame, text="SDR测试")
# 色彩空间
ttk.Label(self.sdr_signal_frame, text="色彩空间:").grid(
row=0, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_color_space_var = tk.StringVar(value="BT.709")
sdr_color_space_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_color_space_var,
values=["sRGB", "BT.709", "BT.601", "BT.2020", "DCI-P3"],
width=10,
state="readonly",
)
sdr_color_space_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
# Gamma测试参考值用于Gamma曲线绘制和色准计算
ttk.Label(self.sdr_signal_frame, text="Gamma:").grid(
row=1, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_gamma_type_var = tk.StringVar(value=UCDEnum.SignalFormat.GammaType.GAMMA_22)
sdr_gamma_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_gamma_type_var,
values=UCDEnum.SignalFormat.GammaType.get_list(),
width=10,
state="readonly",
)
sdr_gamma_combo.grid(row=1, column=1, sticky=tk.W, padx=5, pady=2)
# 数据范围
ttk.Label(self.sdr_signal_frame, text="数据范围:").grid(
row=2, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_data_range_var = tk.StringVar(value=UCDEnum.SignalFormat.DataRange.FULL)
sdr_range_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_data_range_var,
values=UCDEnum.SignalFormat.DataRange.get_list(),
width=10,
state="readonly",
)
sdr_range_combo.grid(row=2, column=1, sticky=tk.W, padx=5, pady=2)
# 编码位深
ttk.Label(self.sdr_signal_frame, text="编码位深:").grid(
row=3, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_bit_depth_var = tk.StringVar(value=UCDEnum.SignalFormat.BitDepth.BIT_8)
sdr_bit_depth_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_bit_depth_var,
values=UCDEnum.SignalFormat.BitDepth.get_list(),
width=10,
state="readonly",
)
sdr_bit_depth_combo.grid(row=3, column=1, sticky=tk.W, padx=5, pady=2)
# 分辨率 # 分辨率
ttk.Label(self.sdr_signal_frame, text="分辨率:").grid( ttk.Label(self.sdr_signal_frame, text="分辨率:").grid(
row=4, column=0, sticky=tk.W, padx=5, pady=2 row=0, column=0, sticky=tk.W, padx=5, pady=2
) )
self.sdr_timing_var = tk.StringVar( self.sdr_timing_var = tk.StringVar(
value=self.config.current_test_types.get("sdr_movie", {}).get( value=self.config.current_test_types.get("sdr_movie", {}).get(
@@ -365,7 +309,63 @@ def create_signal_format_content(self: "PQAutomationApp"):
state="readonly", state="readonly",
) )
sdr_timing_combo.bind("<<ComboboxSelected>>", self.on_sdr_timing_changed) sdr_timing_combo.bind("<<ComboboxSelected>>", self.on_sdr_timing_changed)
sdr_timing_combo.grid(row=4, column=1, sticky=tk.W, padx=5, pady=2) sdr_timing_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
# 色彩空间
ttk.Label(self.sdr_signal_frame, text="色彩空间:").grid(
row=1, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_color_space_var = tk.StringVar(value="BT.709")
sdr_color_space_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_color_space_var,
values=["sRGB", "BT.709", "BT.601", "BT.2020", "DCI-P3"],
width=10,
state="readonly",
)
sdr_color_space_combo.grid(row=1, column=1, sticky=tk.W, padx=5, pady=2)
# Gamma测试参考值用于Gamma曲线绘制和色准计算
ttk.Label(self.sdr_signal_frame, text="Gamma:").grid(
row=2, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_gamma_type_var = tk.StringVar(value=UCDEnum.SignalFormat.GammaType.GAMMA_22)
sdr_gamma_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_gamma_type_var,
values=UCDEnum.SignalFormat.GammaType.get_list(),
width=10,
state="readonly",
)
sdr_gamma_combo.grid(row=2, column=1, sticky=tk.W, padx=5, pady=2)
# 数据范围
ttk.Label(self.sdr_signal_frame, text="数据范围:").grid(
row=3, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_data_range_var = tk.StringVar(value=UCDEnum.SignalFormat.DataRange.FULL)
sdr_range_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_data_range_var,
values=UCDEnum.SignalFormat.DataRange.get_list(),
width=10,
state="readonly",
)
sdr_range_combo.grid(row=3, column=1, sticky=tk.W, padx=5, pady=2)
# 编码位深
ttk.Label(self.sdr_signal_frame, text="编码位深:").grid(
row=4, column=0, sticky=tk.W, padx=5, pady=2
)
self.sdr_bit_depth_var = tk.StringVar(value=UCDEnum.SignalFormat.BitDepth.BIT_8)
sdr_bit_depth_combo = ttk.Combobox(
self.sdr_signal_frame,
textvariable=self.sdr_bit_depth_var,
values=UCDEnum.SignalFormat.BitDepth.get_list(),
width=10,
state="readonly",
)
sdr_bit_depth_combo.grid(row=4, column=1, sticky=tk.W, padx=5, pady=2)
# 色彩格式 # 色彩格式
ttk.Label(self.sdr_signal_frame, text="色彩格式:").grid( ttk.Label(self.sdr_signal_frame, text="色彩格式:").grid(
@@ -607,13 +607,14 @@ def create_connection_content(self: "PQAutomationApp"):
button_frame.grid_columnconfigure(0, weight=1) button_frame.grid_columnconfigure(0, weight=1)
button_frame.grid_columnconfigure(1, weight=1) button_frame.grid_columnconfigure(1, weight=1)
button_frame.grid_columnconfigure(2, weight=1) button_frame.grid_columnconfigure(2, weight=1)
button_frame.grid_columnconfigure(3, weight=1)
# connect_icon = load_icon("assets/connect-svgrepo-com.png") # connect_icon = load_icon("assets/connect-svgrepo-com.png")
self.check_button = ttk.Button( self.check_button = ttk.Button(
button_frame, button_frame,
# image=connect_icon, # image=connect_icon,
# bootstyle="link", # bootstyle="link",
text="连接", text="全部连接",
bootstyle="success", bootstyle="success",
takefocus=False, takefocus=False,
command=self.check_com_connections, command=self.check_com_connections,
@@ -621,6 +622,42 @@ def create_connection_content(self: "PQAutomationApp"):
# self.check_button.image = connect_icon # self.check_button.image = connect_icon
self.check_button.grid(row=0, column=0, padx=(0, 4), pady=3, sticky="ew") self.check_button.grid(row=0, column=0, padx=(0, 4), pady=3, sticky="ew")
self.ucd_connect_button = ttk.Button(
button_frame,
text="连接UCD",
bootstyle="success-outline",
takefocus=False,
command=self.check_ucd_connection,
)
self.ucd_connect_button.grid(row=0, column=1, padx=4, pady=3, sticky="ew")
self.ca_connect_button = ttk.Button(
button_frame,
text="连接CA410",
bootstyle="success-outline",
takefocus=False,
command=self.check_ca_connection,
)
self.ca_connect_button.grid(row=0, column=2, padx=4, pady=3, sticky="ew")
self.ucd_disconnect_button = ttk.Button(
button_frame,
text="断开UCD",
bootstyle="danger-outline",
takefocus=False,
command=self.disconnect_ucd_connection,
)
self.ucd_disconnect_button.grid(row=1, column=1, padx=4, pady=3, sticky="ew")
self.ca_disconnect_button = ttk.Button(
button_frame,
text="断开CA410",
bootstyle="danger-outline",
takefocus=False,
command=self.disconnect_ca_connection,
)
self.ca_disconnect_button.grid(row=1, column=2, padx=4, pady=3, sticky="ew")
# disconnect_icon = load_icon("assets/disconnect-svgrepo-com.png") # disconnect_icon = load_icon("assets/disconnect-svgrepo-com.png")
# 断开连接按钮 # 断开连接按钮
self.disconnect_button = ttk.Button( self.disconnect_button = ttk.Button(
@@ -633,7 +670,7 @@ def create_connection_content(self: "PQAutomationApp"):
command=self.disconnect_com_connections, command=self.disconnect_com_connections,
) )
# self.disconnect_button.image = disconnect_icon # 防止图标被垃圾回收 # self.disconnect_button.image = disconnect_icon # 防止图标被垃圾回收
self.disconnect_button.grid(row=0, column=1, padx=4, pady=3, sticky="ew") self.disconnect_button.grid(row=1, column=0, padx=(0, 4), pady=3, sticky="ew")
# refresh_icon = load_icon("assets/refresh-svgrepo-com.png") # refresh_icon = load_icon("assets/refresh-svgrepo-com.png")
self.refresh_button = ttk.Button( self.refresh_button = ttk.Button(
@@ -646,7 +683,7 @@ def create_connection_content(self: "PQAutomationApp"):
command=self.refresh_com_ports, command=self.refresh_com_ports,
) )
# self.refresh_button.image = refresh_icon # 防止图标被垃圾回收 # self.refresh_button.image = refresh_icon # 防止图标被垃圾回收
self.refresh_button.grid(row=0, column=2, padx=(4, 0), pady=3, sticky="ew") self.refresh_button.grid(row=1, column=3, padx=(4, 0), pady=3, sticky="ew")
# CA端口 # CA端口
ttk.Label(com_frame, text="CA端口:").grid( ttk.Label(com_frame, text="CA端口:").grid(
@@ -737,10 +774,9 @@ def create_test_type_frame(self: "PQAutomationApp"):
).pack(fill=tk.X, padx=16, pady=(16, 6), anchor="w") ).pack(fill=tk.X, padx=16, pady=(16, 6), anchor="w")
panel_buttons = [ panel_buttons = [
("log_btn", "测试日志", self.toggle_log_panel),
("ai_image_btn", "AI 图片", self.toggle_ai_image_panel), ("ai_image_btn", "AI 图片", self.toggle_ai_image_panel),
("pantone_baseline_btn", "Pantone 摸底", self.toggle_pantone_baseline_panel), ("pantone_baseline_btn", "Pantone 摸底", self.toggle_pantone_baseline_panel),
("gamma_pattern_btn", "Gamma 图案", self.toggle_gamma_pattern_panel), ("gamma_pattern_btn", "Gamma Pattern编辑", self.toggle_gamma_pattern_panel),
("calman_btn", "CALMAN 灰阶", self.toggle_calman_panel), ("calman_btn", "CALMAN 灰阶", self.toggle_calman_panel),
] ]
for attr, text, cmd in panel_buttons: for attr, text, cmd in panel_buttons:
@@ -764,6 +800,17 @@ def create_test_type_frame(self: "PQAutomationApp"):
) )
beta_lbl.pack(fill=tk.X, side=tk.BOTTOM, padx=4, pady=(6, 4)) beta_lbl.pack(fill=tk.X, side=tk.BOTTOM, padx=4, pady=(6, 4))
# ---------- 测试日志(底部固定) ----------
self.log_btn = ttk.Button(
self.sidebar_frame,
text="测试日志",
style="Sidebar.TButton",
command=self.toggle_log_panel,
takefocus=False,
)
self.log_btn.pack(fill=tk.X, padx=0, pady=(0, 2), side=tk.BOTTOM)
# ---------- 主题切换(底部固定) ---------- # ---------- 主题切换(底部固定) ----------
self.theme_toggle_btn = ttk.Button( self.theme_toggle_btn = ttk.Button(
self.sidebar_frame, self.sidebar_frame,
@@ -777,8 +824,6 @@ def create_test_type_frame(self: "PQAutomationApp"):
# 注册面板按钮 # 注册面板按钮
if hasattr(self, "panels"): if hasattr(self, "panels"):
if "log" in self.panels:
self.panels["log"]["button"] = self.log_btn
if "ai_image" in self.panels: if "ai_image" in self.panels:
self.panels["ai_image"]["button"] = self.ai_image_btn self.panels["ai_image"]["button"] = self.ai_image_btn
if "single_step" in self.panels: if "single_step" in self.panels:
@@ -804,6 +849,15 @@ def _refresh_theme_toggle_label(self: "PQAutomationApp") -> None:
def _on_toggle_theme(self: "PQAutomationApp") -> None: def _on_toggle_theme(self: "PQAutomationApp") -> None:
"""切换主题:重新应用 ttk 样式并刷新所有自定义样式相关的标签。""" """切换主题:重新应用 ttk 样式并刷新所有自定义样式相关的标签。"""
# 在测试进行时禁止切换主题,避免影响测量稳定性
if getattr(self, "testing", False):
try:
if hasattr(self, "log_gui"):
self.log_gui.log("警告: 测试进行中,禁止切换主题", level="error")
except Exception:
pass
return
from app.views.theme_manager import toggle_theme from app.views.theme_manager import toggle_theme
toggle_theme() toggle_theme()
# apply_modern_styles() # apply_modern_styles()
@@ -974,10 +1028,10 @@ def on_screen_module_timing_changed(self: "PQAutomationApp", event=None):
# 根据分辨率给出提示 # 根据分辨率给出提示
if width >= 3840: # 4K及以上 if width >= 3840: # 4K及以上
self.log_gui.log(" 检测到4K分辨率", level="info") self.log_gui.log("检测到4K分辨率", level="info")
if refresh_rate >= 120: if refresh_rate >= 120:
self.log_gui.log(" 检测到高刷新率", level="info") self.log_gui.log("检测到高刷新率", level="info")
# 更新屏模组配置(独立于 current_test_type # 更新屏模组配置(独立于 current_test_type
self.config.current_test_types.setdefault("screen_module", {})[ self.config.current_test_types.setdefault("screen_module", {})[

View File

@@ -42,13 +42,13 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
ttk.Label( ttk.Label(
title_frame, title_frame,
text="🔆 Local Dimming 窗口测试", text="Local Dimming 窗口测试",
font=("微软雅黑", 14, "bold"), font=("微软雅黑", 14, "bold"),
).pack(side=tk.LEFT) ).pack(side=tk.LEFT)
# ==================== 2. 窗口百分比按钮 ==================== # ==================== 2. 窗口百分比按钮 ====================
window_frame = ttk.LabelFrame( window_frame = ttk.LabelFrame(
main_container, text="🔆 窗口百分比(点击发送)", padding=10 main_container, text="窗口百分比", padding=10
) )
window_frame.pack(fill=tk.X, pady=(0, 10)) window_frame.pack(fill=tk.X, pady=(0, 10))
@@ -60,6 +60,50 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
style="SuccessState.TLabel", style="SuccessState.TLabel",
).pack(pady=(0, 8)) ).pack(pady=(0, 8))
window_level_row = ttk.Frame(window_frame)
window_level_row.pack(fill=tk.X, pady=(0, 8))
ttk.Label(window_level_row, text="窗口(%):").pack(side=tk.LEFT)
self.ld_window_percentage_var = tk.StringVar(value="10")
ld_window_percentage_entry = ttk.Entry(
window_level_row,
textvariable=self.ld_window_percentage_var,
width=8,
)
ld_window_percentage_entry.pack(side=tk.LEFT, padx=(6, 10))
ttk.Label(window_level_row, text="窗口亮度(%):").pack(side=tk.LEFT)
self.ld_window_luminance_var = tk.StringVar(value="100")
ld_window_luminance_entry = ttk.Entry(
window_level_row,
textvariable=self.ld_window_luminance_var,
width=8,
)
ld_window_luminance_entry.pack(side=tk.LEFT, padx=(6, 10))
ttk.Button(
window_level_row,
text="生成窗口",
command=self.send_ld_manual_window,
bootstyle="success-outline",
width=12,
).pack(side=tk.LEFT)
ld_window_percentage_entry.bind(
"<Return>",
lambda _event: self.send_ld_manual_window(),
)
ld_window_luminance_entry.bind(
"<Return>",
lambda _event: self.send_ld_manual_window(),
)
ttk.Label(
window_level_row,
text="输入后可直接点生成或回车",
style="InfoState.TLabel",
).pack(side=tk.LEFT, padx=(8, 0))
# 第一行1%, 2%, 5%, 10%, 18% # 第一行1%, 2%, 5%, 10%, 18%
row1 = ttk.Frame(window_frame) row1 = ttk.Frame(window_frame)
row1.pack(fill=tk.X, pady=(0, 5)) row1.pack(fill=tk.X, pady=(0, 5))
@@ -89,16 +133,9 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
).pack(side=tk.LEFT, padx=3) ).pack(side=tk.LEFT, padx=3)
# ==================== 3. 其他手动图案 ==================== # ==================== 3. 其他手动图案 ====================
pattern_frame = ttk.LabelFrame(main_container, text="🧩 其他测试图案", padding=10) pattern_frame = ttk.LabelFrame(main_container, text="其他测试图案", padding=10)
pattern_frame.pack(fill=tk.X, pady=(0, 10)) pattern_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Label(
pattern_frame,
text="手动发送棋盘格、瞬时峰值、黑场图案,再点击采集当前亮度",
font=("", 9),
style="SuccessState.TLabel",
).pack(pady=(0, 8))
pattern_row = ttk.Frame(pattern_frame) pattern_row = ttk.Frame(pattern_frame)
pattern_row.pack(fill=tk.X) pattern_row.pack(fill=tk.X)
@@ -118,14 +155,6 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
width=14, width=14,
).pack(side=tk.LEFT, padx=3) ).pack(side=tk.LEFT, padx=3)
ttk.Button(
pattern_row,
text="瞬时峰值",
command=self.send_ld_instant_peak,
bootstyle="warning",
width=12,
).pack(side=tk.LEFT, padx=3)
ttk.Button( ttk.Button(
pattern_row, pattern_row,
text="全黑画面", text="全黑画面",
@@ -134,8 +163,92 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
width=12, width=12,
).pack(side=tk.LEFT, padx=3) ).pack(side=tk.LEFT, padx=3)
# ==================== 4. CA410 采集按钮 ==================== # ==================== 4. 独立瞬时峰值连续测试 ====================
measure_frame = ttk.LabelFrame(main_container, text="📊 CA410 测量", padding=10) peak_frame = ttk.LabelFrame(main_container, text="瞬时峰值独立测试", padding=10)
peak_frame.pack(fill=tk.X, pady=(0, 10))
self.ld_peak_window_size_var = tk.StringVar(value="10")
self.ld_peak_window_luminance_var = tk.StringVar(value="100")
self.ld_peak_duration_var = tk.StringVar(value="20")
self.ld_peak_sample_interval_var = tk.StringVar(value="0.3")
self.ld_peak_record_curve_var = tk.BooleanVar(value=True)
self.ld_peak_no_limit_var = tk.BooleanVar(value=False)
self.ld_peak_drop_percent_var = tk.StringVar(value="3")
ttk.Label(peak_frame, text="窗口(%):").grid(row=1, column=0, sticky=tk.W, padx=(0, 4))
ttk.Entry(peak_frame, textvariable=self.ld_peak_window_size_var, width=8).grid(
row=1, column=1, sticky=tk.W, padx=(0, 10)
)
ttk.Label(peak_frame, text="窗口亮度(%):").grid(
row=1, column=2, sticky=tk.W, padx=(0, 4)
)
ttk.Entry(peak_frame, textvariable=self.ld_peak_window_luminance_var, width=8).grid(
row=1, column=3, sticky=tk.W, padx=(0, 10)
)
ttk.Label(peak_frame, text="连续时长(s):").grid(
row=1, column=4, sticky=tk.W, padx=(0, 4)
)
ttk.Entry(peak_frame, textvariable=self.ld_peak_duration_var, width=8).grid(
row=1, column=5, sticky=tk.W, padx=(0, 10)
)
ttk.Label(peak_frame, text="采样间隔(s):").grid(
row=1, column=6, sticky=tk.W, padx=(0, 4)
)
ttk.Entry(peak_frame, textvariable=self.ld_peak_sample_interval_var, width=8).grid(
row=1, column=7, sticky=tk.W
)
ttk.Checkbutton(
peak_frame,
text="记录曲线点到表格",
variable=self.ld_peak_record_curve_var,
bootstyle="round-toggle",
).grid(row=2, column=0, columnspan=3, sticky=tk.W, pady=(8, 0))
peak_btn_row = ttk.Frame(peak_frame)
peak_btn_row.grid(row=2, column=4, columnspan=4, sticky=tk.EW, pady=(8, 0))
self.ld_peak_start_btn = ttk.Button(
peak_btn_row,
text="开始峰值追踪",
command=self.start_ld_instant_peak_tracking,
bootstyle="warning",
width=14,
)
self.ld_peak_start_btn.pack(side=tk.LEFT, padx=(0, 5))
self.ld_peak_stop_btn = ttk.Button(
peak_btn_row,
text="停止",
command=self.stop_ld_instant_peak_tracking,
bootstyle="danger-outline",
width=10,
state="disabled",
)
self.ld_peak_stop_btn.pack(side=tk.LEFT)
ttk.Label(peak_frame, text="亮度回落(%):").grid(
row=2, column=0, sticky=tk.W, padx=(0, 4), pady=(6, 0)
)
ttk.Entry(
peak_frame,
textvariable=self.ld_peak_drop_percent_var,
width=8
).grid(row=2, column=1, sticky=tk.W, pady=(6, 0))
ttk.Checkbutton(
peak_frame,
text="不固定测试时间",
variable=self.ld_peak_no_limit_var,
bootstyle="round-toggle",
).grid(row=2, column=2, columnspan=3, sticky=tk.W, pady=(6, 0))
# ==================== 5. CA410 采集按钮 ====================
measure_frame = ttk.LabelFrame(main_container, text="CA410 测量", padding=10)
measure_frame.pack(fill=tk.X, pady=(0, 10)) measure_frame.pack(fill=tk.X, pady=(0, 10))
measure_btn_frame = ttk.Frame(measure_frame) measure_btn_frame = ttk.Frame(measure_frame)
@@ -143,7 +256,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
self.ld_measure_btn = ttk.Button( self.ld_measure_btn = ttk.Button(
measure_btn_frame, measure_btn_frame,
text="📏 采集当前亮度", text="采集当前亮度",
command=self.measure_ld_luminance, command=self.measure_ld_luminance,
bootstyle="primary", bootstyle="primary",
width=15, width=15,
@@ -159,8 +272,8 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
) )
self.ld_result_label.pack(side=tk.LEFT, padx=(10, 0)) self.ld_result_label.pack(side=tk.LEFT, padx=(10, 0))
# ==================== 5. 测试结果表格 ==================== # ==================== 6. 测试结果表格 ====================
result_frame = ttk.LabelFrame(main_container, text="📋 测试记录", padding=10) result_frame = ttk.LabelFrame(main_container, text="测试记录", padding=10)
result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# Treeview # Treeview
@@ -191,7 +304,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
scrollbar.pack(side=tk.RIGHT, fill=tk.Y) scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.ld_tree.configure(yscrollcommand=scrollbar.set) self.ld_tree.configure(yscrollcommand=scrollbar.set)
# ==================== 6. 底部操作按钮 ==================== # ==================== 7. 底部操作按钮 ====================
bottom_frame = ttk.Frame(main_container) bottom_frame = ttk.Frame(main_container)
bottom_frame.pack(fill=tk.X) bottom_frame.pack(fill=tk.X)
@@ -206,13 +319,22 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
self.ld_save_btn = ttk.Button( self.ld_save_btn = ttk.Button(
bottom_frame, bottom_frame,
text="💾 保存结果", text="保存结果",
command=self.save_local_dimming_results, command=self.save_local_dimming_results,
bootstyle="info", bootstyle="info",
width=12, width=12,
) )
self.ld_save_btn.pack(side=tk.LEFT) self.ld_save_btn.pack(side=tk.LEFT)
self.ld_plot_btn = ttk.Button(
bottom_frame,
text="生成峰值曲线",
command=self.plot_ld_instant_peak_curve,
bootstyle="warning-outline",
width=14,
)
self.ld_plot_btn.pack(side=tk.LEFT, padx=(5, 0))
# 默认隐藏 # 默认隐藏
self.local_dimming_visible = False self.local_dimming_visible = False
@@ -228,6 +350,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
self.current_ld_percentage = None self.current_ld_percentage = None
self.current_ld_test_item = None self.current_ld_test_item = None
self.current_ld_pattern_label = None self.current_ld_pattern_label = None
self.ld_peak_tracking = False
def toggle_local_dimming_panel(self: "PQAutomationApp"): def toggle_local_dimming_panel(self: "PQAutomationApp"):

View File

@@ -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):
"""禁用所有单步调试(新测试开始时调用)""" """禁用所有单步调试(新测试开始时调用)"""

View File

@@ -29,7 +29,7 @@ _LEGACY_LIGHT_THEMES = {"yeti"}
_CALMAN_LIGHT_COLORS = { _CALMAN_LIGHT_COLORS = {
"primary": "#1755a6", "primary": "#1755a6",
"secondary": "#2B6CB0", "secondary": "#3572B4",
"success": "#2F9E44", "success": "#2F9E44",
"info": "#247BA0", "info": "#247BA0",
"warning": "#C98700", "warning": "#C98700",

Binary file not shown.

View File

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

View File

@@ -54,7 +54,7 @@
"y_ideal": 0.329, "y_ideal": 0.329,
"y_tolerance": 0.003 "y_tolerance": 0.003
}, },
"gamut_reference": "BT.601" "gamut_reference": "BT.709"
}, },
"hdr_movie": { "hdr_movie": {
"name": "HDR Movie测试", "name": "HDR Movie测试",

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 545 KiB