Compare commits

..

3 Commits

Author SHA1 Message Date
xinzhu.yin
e9a591bf6e 修改深色模式下结果图片显示异常 2026-06-05 16:58:46 +08:00
xinzhu.yin
49d82da8b9 修改Calman灰阶中结果图显示、修改UI主题样式应用 2026-06-04 10:36:15 +08:00
xinzhu.yin
3aa975c4d3 修改calman灰阶点击异常、修改色准结果显示异常 2026-06-02 17:34:46 +08:00
36 changed files with 1924 additions and 481 deletions

View File

@@ -25,6 +25,7 @@ from typing import TYPE_CHECKING
from app.ucd_domain import ConnectionChanged, UcdError from app.ucd_domain import ConnectionChanged, UcdError
from drivers.caSerail import CASerail from drivers.caSerail import CASerail
from drivers.ucd_driver import DeviceInfo 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
@@ -189,8 +190,7 @@ class ConnectionController:
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")
@@ -212,10 +212,7 @@ 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()
@@ -254,7 +251,45 @@ def check_com_connections(self: "PQAutomationApp"):
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 = bool(getattr(self.ucd, "status", False))
_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 +307,49 @@ def disconnect_com_connections(self: "PQAutomationApp"):
self.connection.disconnect_all() self.connection.disconnect_all()
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/XYZDisplay 0"""
return _read_ca_display(self, 0)
def read_ca_tcp_duv(self: "PQAutomationApp"):
"""读取 Tcp/duv/Lv/XYZDisplay 1"""
return _read_ca_display(self, 1)
def read_ca_uvLv(self: "PQAutomationApp"):
"""读取 u'/v'/Lv/XYZDisplay 5"""
return _read_ca_display(self, 5)
def read_ca_xyz(self: "PQAutomationApp"):
"""读取 XYZDisplay 7"""
return _read_ca_display(self, 7)
def read_ca_lambda_pe(self: "PQAutomationApp"):
"""读取 λd/Pe/Lv/XYZDisplay 8"""
return _read_ca_display(self, 8)
__all__ = [ __all__ = [
"ConnectionController", "ConnectionController",
# 兼容层 # 兼容层
@@ -280,6 +358,7 @@ __all__ = [
"refresh_com_ports", "refresh_com_ports",
"check_com_connections", "check_com_connections",
"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",
@@ -295,6 +374,14 @@ class DeviceConnectionMixin:
refresh_com_ports = refresh_com_ports refresh_com_ports = refresh_com_ports
check_com_connections = check_com_connections check_com_connections = check_com_connections
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
_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

View File

@@ -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:

View File

@@ -104,6 +104,7 @@ def _render_chromaticity(kind: str, bbox: _BBox) -> np.ndarray:
fig.canvas.draw() fig.canvas.draw()
# 从 canvas 抓取 RGBA 数组 # 从 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:

View File

@@ -9,9 +9,15 @@ 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
import numpy as np
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
@@ -55,14 +61,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
@@ -76,7 +74,7 @@ def _xy_to_uv(x: float, y: float):
# ============================================================ # ============================================================
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,15 +84,14 @@ 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]
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="#202020",
linewidth=1.0, linewidth=0.5,
zorder=3, zorder=3,
) )
@@ -118,6 +115,9 @@ def _draw_left_panel(ax, color_patches, delta_e_values, font_scale=1.0, dark_mod
spine.set_linewidth(0.9) spine.set_linewidth(0.9)
# ============================================================ # ============================================================
# 子图CIE 1976 u'v' 色度图(目标 vs 实测) # 子图CIE 1976 u'v' 色度图(目标 vs 实测)
# ============================================================ # ============================================================
@@ -127,11 +127,16 @@ def _draw_uv_diagram(ax, color_patches, measurements, standards, font_scale=1.0,
ax.clear() ax.clear()
try: try:
bg, bbox = get_cie1976_background() bg, bbox = get_cie1976_background()
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 +150,124 @@ 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",
color=text_color, pad=4) 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()
@@ -254,17 +310,24 @@ 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' + 统计)。""" """绘制色准测试结果 - 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
@@ -295,13 +358,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 +378,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 +417,11 @@ def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type):
except Exception: except Exception:
pass pass
self.accuracy_canvas.draw() # Select the tab first so the canvas is visible and winfo_width/height
# 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()
self.accuracy_canvas.draw()
class PlotAccuracyMixin: class PlotAccuracyMixin:

View File

@@ -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()
@@ -111,12 +137,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 +167,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 +181,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 +189,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 +249,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 +267,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 +287,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 +350,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 +361,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)

View File

@@ -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,
) )

View File

@@ -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

View File

@@ -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. 绘制到画布 ==========

View File

@@ -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") 控制
) )

101
app/pq/color_patch_map.py Normal file
View 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"

View File

@@ -394,8 +394,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 +402,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 +447,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:

View File

@@ -294,61 +294,106 @@ def _call_pqtest_upload(file_path: str, timeout: float = API_UPLOAD_TIMEOUT, aut
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")
# 自动缩放:等比例缩放至 4096×4096 以内 logger.info(
logger.info("[AIImage][UPLOAD] 自动缩放 %dx%d (%.1fMB) 至 ≤4096×4096", "[AIImage][UPLOAD] 自动处理超限图片 %dx%d (%.2fMB)",
iw, ih, size/1024/1024) iw,
scale = min(UPLOAD_MAX_PIXELS / iw, UPLOAD_MAX_PIXELS / ih, 1.0) ih,
new_w, new_h = max(1, int(iw * scale)), max(1, int(ih * scale)) size / 1024 / 1024,
)
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()
fmt = "PNG" if ext == ".png" else "JPEG"
save_kw = {"format": fmt}
img_resized.save(tmp_io, **save_kw)
tmp_bytes = tmp_io.getvalue()
if len(tmp_bytes) <= UPLOAD_MAX_BYTES: # 先做一次分辨率约束,避免后续压缩开销过大。
file_bytes = tmp_bytes scale = min(UPLOAD_MAX_PIXELS / max(1, working.width), UPLOAD_MAX_PIXELS / max(1, working.height), 1.0)
if scale < 1.0:
working = working.resize(
(max(1, int(working.width * scale)), max(1, int(working.height * scale))),
Image.LANCZOS,
)
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
break
quality -= 5
else:
# 即使 quality=50 还是太大,也用这个版本上传(后端会处理)
file_bytes = tmp_bytes
logger.info("[AIImage][UPLOAD] 缩放完成 %dx%d (%.1fMB)", # 仍超限时,转 JPEG + 渐进压缩;如仍超限则继续降分辨率。
new_w, new_h, len(file_bytes)/1024/1024) if len(best_bytes) > UPLOAD_MAX_BYTES:
iw, ih = new_w, new_h 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
best_bytes = compressed
best_mime = "image/jpeg"
best_ext = ".jpg"
if len(best_bytes) <= UPLOAD_MAX_BYTES:
break
next_w = max(256, int(working_jpg.width * 0.9))
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,

View File

@@ -21,8 +21,23 @@ class PatternService:
def __init__(self, app): def __init__(self, app):
self.app = 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 = ""
try:
ctrl = getattr(self.app.signal_service.device, "raw_controller", None)
if ctrl is not None:
d = getattr(ctrl, "last_error", None)
if d:
detail = f", detail={d}"
except Exception:
pass
return f"UCD profile apply_config failed for {test_type}, timing={timing}{detail}"
def prepare_session(self, mode, *, test_type=None, log_details=False): def prepare_session(self, mode, *, test_type=None, log_details=False):
test_type = test_type or self.app.config.current_test_type 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): if not self.app.config.set_current_pattern(mode):
raise ValueError(f"未知的图案模式: {mode}") raise ValueError(f"未知的图案模式: {mode}")
@@ -64,7 +79,8 @@ class PatternService:
("Timing", self.app.config.current_test_types[test_type]["timing"]), ("Timing", self.app.config.current_test_types[test_type]["timing"]),
]: ]:
self._log(f" {label}: {value}", "info") self._log(f" {label}: {value}", "info")
self.app.signal_service.apply_config(active_config) if not self.app.signal_service.apply_config(active_config):
raise RuntimeError(self._build_apply_config_error(test_type))
success = self.app.signal_service.update_signal_format( success = self.app.signal_service.update_signal_format(
color_space=color_space, color_space=color_space,
data_range=data_range, data_range=data_range,
@@ -97,7 +113,10 @@ class PatternService:
active_config = self.app.config.get_temp_config_with_converted_params( active_config = self.app.config.get_temp_config_with_converted_params(
mode=mode, converted_params=converted_params mode=mode, converted_params=converted_params
) )
self.app.signal_service.apply_config(active_config) if hasattr(active_config, "set_current_test_type"):
active_config.set_current_test_type(test_type)
if not self.app.signal_service.apply_config(active_config):
raise RuntimeError(self._build_apply_config_error(test_type))
success = self.app.signal_service.update_signal_format( success = self.app.signal_service.update_signal_format(
color_space=self.app.sdr_color_space_var.get(), color_space=self.app.sdr_color_space_var.get(),
data_range=data_range, data_range=data_range,
@@ -129,7 +148,10 @@ class PatternService:
active_config = self.app.config.get_temp_config_with_converted_params( active_config = self.app.config.get_temp_config_with_converted_params(
mode=mode, converted_params=converted_params mode=mode, converted_params=converted_params
) )
self.app.signal_service.apply_config(active_config) if hasattr(active_config, "set_current_test_type"):
active_config.set_current_test_type(test_type)
if not self.app.signal_service.apply_config(active_config):
raise RuntimeError(self._build_apply_config_error(test_type))
success = self.app.signal_service.update_signal_format( success = self.app.signal_service.update_signal_format(
color_space=self.app.hdr_color_space_var.get(), color_space=self.app.hdr_color_space_var.get(),
data_range=data_range, data_range=data_range,

View File

@@ -12,7 +12,9 @@
from __future__ import annotations from __future__ import annotations
from contextlib import contextmanager
import logging import logging
import sys
import threading import threading
from app.ucd_domain import ( from app.ucd_domain import (
@@ -34,6 +36,10 @@ from drivers.ucd_driver import IUcdDevice
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
_LOCK_TIMEOUT_SECONDS = 8.0
_DEBUG_LOCK_TIMEOUT_SECONDS = 0.3
_PATTERN_LOCK_TIMEOUT_SECONDS = 0.8
# ─── 视图字符串 → 值对象 转换工具 ──────────────────────────────── # ─── 视图字符串 → 值对象 转换工具 ────────────────────────────────
@@ -85,6 +91,53 @@ class SignalService:
self._dev = device self._dev = device
self._bus = bus self._bus = bus
self._lock = threading.RLock() self._lock = threading.RLock()
self._lock_owner_tid: int | None = None
self._lock_owner_name: str | None = None
def _effective_lock_timeout(self, timeout_override: float | None = None) -> float:
"""调试模式下缩短锁等待,避免单步时表现为 UI 长时间无响应。"""
if timeout_override is not None:
return timeout_override
if sys.gettrace() is not None:
return _DEBUG_LOCK_TIMEOUT_SECONDS
return _LOCK_TIMEOUT_SECONDS
@contextmanager
def _acquire_service_lock(self, op_name: str, timeout_override: float | None = None):
timeout = self._effective_lock_timeout(timeout_override)
current = threading.current_thread()
log.info(
"SignalService.%s acquiring lock timeout=%.1fs tid=%s thread=%s owner_tid=%s owner_thread=%s",
op_name,
timeout,
threading.get_ident(),
current.name,
self._lock_owner_tid,
self._lock_owner_name,
)
acquired = self._lock.acquire(timeout=timeout)
if not acquired:
raise UcdError(
"UCD busy: lock timeout in "
f"SignalService.{op_name} ({timeout:.1f}s), "
f"owner_tid={self._lock_owner_tid}, owner_thread={self._lock_owner_name}"
)
prev_owner_tid = self._lock_owner_tid
prev_owner_name = self._lock_owner_name
self._lock_owner_tid = threading.get_ident()
self._lock_owner_name = current.name
log.info(
"SignalService.%s lock acquired tid=%s thread=%s",
op_name,
self._lock_owner_tid,
self._lock_owner_name,
)
try:
yield
finally:
self._lock_owner_tid = prev_owner_tid
self._lock_owner_name = prev_owner_name
self._lock.release()
# -- 高层接口 ------------------------------------------------ # -- 高层接口 ------------------------------------------------
@@ -100,7 +153,7 @@ class SignalService:
Returns: Returns:
``format_changed``——本次相对上一次 :meth:`apply` 是否变化。 ``format_changed``——本次相对上一次 :meth:`apply` 是否变化。
""" """
with self._lock: with self._acquire_service_lock("apply"):
log.info( log.info(
"SignalService.apply signal=%s timing=%s pattern=%s", "SignalService.apply signal=%s timing=%s pattern=%s",
signal, signal,
@@ -114,7 +167,7 @@ class SignalService:
def send_pattern(self, pattern: PatternSpec) -> None: def send_pattern(self, pattern: PatternSpec) -> None:
"""在已 configure 的信号上仅更新图案后 apply。""" """在已 configure 的信号上仅更新图案后 apply。"""
with self._lock: with self._acquire_service_lock("send_pattern", _PATTERN_LOCK_TIMEOUT_SECONDS):
log.info("SignalService.send_pattern pattern=%s", pattern.kind.value) log.info("SignalService.send_pattern pattern=%s", pattern.kind.value)
self._dev.set_pattern(pattern) self._dev.set_pattern(pattern)
self._dev.apply() self._dev.apply()
@@ -155,7 +208,7 @@ class SignalService:
ctrl = getattr(self._dev, "raw_controller", None) ctrl = getattr(self._dev, "raw_controller", None)
if ctrl is None: if ctrl is None:
raise UcdError("update_signal_format 暂仅支持 UCD323Device") raise UcdError("update_signal_format 暂仅支持 UCD323Device")
with self._lock: with self._acquire_service_lock("update_signal_format"):
return bool( return bool(
ctrl.apply_signal_format( ctrl.apply_signal_format(
color_space=color_space, color_space=color_space,
@@ -193,7 +246,7 @@ class SignalService:
ctrl = getattr(self._dev, "raw_controller", None) ctrl = getattr(self._dev, "raw_controller", None)
if ctrl is None: if ctrl is None:
raise UcdError("apply_config 暂仅支持 UCD323Device") raise UcdError("apply_config 暂仅支持 UCD323Device")
with self._lock: with self._acquire_service_lock("apply_config"):
return bool(ctrl.set_ucd_params(config)) return bool(ctrl.set_ucd_params(config))
def send_pattern_params(self, params) -> bool: def send_pattern_params(self, params) -> bool:
@@ -201,7 +254,7 @@ class SignalService:
ctrl = getattr(self._dev, "raw_controller", None) ctrl = getattr(self._dev, "raw_controller", None)
if ctrl is None: if ctrl is None:
raise UcdError("send_pattern_params 暂仅支持 UCD323Device") raise UcdError("send_pattern_params 暂仅支持 UCD323Device")
with self._lock: with self._acquire_service_lock("send_pattern_params"):
return bool(ctrl.send_current_pattern_params(params)) return bool(ctrl.send_current_pattern_params(params))
def apply_and_run(self, config, pattern_params) -> bool: def apply_and_run(self, config, pattern_params) -> bool:
@@ -212,7 +265,7 @@ class SignalService:
ctrl = getattr(self._dev, "raw_controller", None) ctrl = getattr(self._dev, "raw_controller", None)
if ctrl is None: if ctrl is None:
raise UcdError("apply_and_run 暂仅支持 UCD323Device") raise UcdError("apply_and_run 暂仅支持 UCD323Device")
with self._lock: with self._acquire_service_lock("apply_and_run"):
if not ctrl.set_ucd_params(config): if not ctrl.set_ucd_params(config):
return False return False
if not ctrl.set_pattern(ctrl.current_pattern, pattern_params): if not ctrl.set_pattern(ctrl.current_pattern, pattern_params):

View File

@@ -171,7 +171,7 @@ def _build_ld_result_row(test_item, pattern_label, value, x="--", y="--"):
def _measure_ld_row(self: "PQAutomationApp", test_item, pattern_label): def _measure_ld_row(self: "PQAutomationApp", test_item, pattern_label):
"""读取一次 CA410 数据并包装为表格行。""" """读取一次 CA410 数据并包装为表格行。"""
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"{pattern_label} 采集失败") raise RuntimeError(f"{pattern_label} 采集失败")
return _build_ld_result_row(test_item, pattern_label, lv, x, y), lv return _build_ld_result_row(test_item, pattern_label, lv, x, y), lv
@@ -549,7 +549,7 @@ def measure_ld_luminance(self: "PQAutomationApp"):
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

View File

@@ -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(

View File

@@ -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,

View File

@@ -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()
@@ -803,17 +806,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:
@@ -1220,6 +1228,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 +1262,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

View File

@@ -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 BalanceCalman 常见口径)"""
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)

View File

@@ -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(
@@ -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,9 +570,6 @@ 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"):
self.chart_notebook.select(self.custom_template_tab_frame)
if self.ca is None or self.ucd is None: 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

View File

@@ -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))
# ===== 预设管理行 ===== # ===== 预设管理行 =====
@@ -207,7 +207,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 +230,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))
@@ -350,7 +350,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="从剪贴板导入",
@@ -363,7 +363,7 @@ def create_gamma_pattern_panel(self: "PQAutomationApp"):
bottom.pack(fill=tk.X, pady=(10, 0)) bottom.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 bottom, text="", style="Muted.TLabel", justify=tk.LEFT
) )
self._gamma_validate_label.pack(anchor=tk.W) self._gamma_validate_label.pack(anchor=tk.W)
@@ -435,16 +435,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 +1023,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)
# ============================================================ # ============================================================

View File

@@ -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)
@@ -669,7 +668,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(
@@ -757,13 +757,10 @@ 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))
@@ -809,7 +806,13 @@ def _on_toggle_theme(self: "PQAutomationApp") -> None:
"""切换主题:重新应用 ttk 样式并刷新所有自定义样式相关的标签。""" """切换主题:重新应用 ttk 样式并刷新所有自定义样式相关的标签。"""
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 +823,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 +849,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"):

View File

@@ -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 采集失败")

View File

@@ -57,7 +57,7 @@ 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))
# 第一行1%, 2%, 5%, 10%, 18% # 第一行1%, 2%, 5%, 10%, 18%
@@ -96,7 +96,7 @@ def create_local_dimming_panel(self: "PQAutomationApp"):
pattern_frame, pattern_frame,
text="手动发送棋盘格、瞬时峰值、黑场图案,再点击采集当前亮度", text="手动发送棋盘格、瞬时峰值、黑场图案,再点击采集当前亮度",
font=("", 9), font=("", 9),
foreground="#28a745", style="SuccessState.TLabel",
).pack(pady=(0, 8)) ).pack(pady=(0, 8))
pattern_row = ttk.Frame(pattern_frame) pattern_row = ttk.Frame(pattern_frame)
@@ -155,7 +155,7 @@ 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))

View File

@@ -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

View File

@@ -802,7 +802,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}, "

View File

@@ -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": "#2B6CB0",
"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

131
cache/pq_ai_api_v21_extracted.txt vendored Normal file
View 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-Typemultip 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-Typeapplication/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
View 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-Typemultip art/form-data 请求方式POST 2. 请求参数说明 参数名 参数类型 是否必填 参数描述 file File二进制文件是待上传图片的二进制文件。请求体格式必须为multipart/form-data ,且包含一个名 为file 的文件字段(如:@"D:\Desktop\PQtest\3-O.png" 备注:仅支持 PNG

Binary file not shown.

View File

@@ -110,7 +110,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):

View File

@@ -1,10 +1,14 @@
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
import logging
import UniTAP import UniTAP
import time import time
import gc import gc
from drivers.UCD323_Enum import UCDEnum from drivers.UCD323_Enum import UCDEnum
log = logging.getLogger(__name__)
class UCDController: class UCDController:
"""UCD323信号发生器控制类""" """UCD323信号发生器控制类"""
@@ -23,6 +27,7 @@ class UCDController:
self.current_pattern_param = None self.current_pattern_param = None
self.current_pattern_params = None self.current_pattern_params = None
self.current_pattern_index = 0 self.current_pattern_index = 0
self.last_error = None
def search_device(self): def search_device(self):
"""搜索可用设备""" """搜索可用设备"""
@@ -140,6 +145,7 @@ class UCDController:
raise RuntimeError("UCD 未打开,无法获取 TX 模块") raise RuntimeError("UCD 未打开,无法获取 TX 模块")
interface = getattr(self, "current_interface", None) interface = getattr(self, "current_interface", None)
log.info("UCDController.get_tx_modules interface=%s", interface)
if interface in (None, "HDMI"): if interface in (None, "HDMI"):
return self.role.hdtx.pg, self.role.hdtx.ag return self.role.hdtx.pg, self.role.hdtx.ag
if interface in ("DP", "Type-C"): if interface in ("DP", "Type-C"):
@@ -189,6 +195,7 @@ class UCDController:
def set_ucd_params(self, config): def set_ucd_params(self, config):
"""设置UCD323参数""" """设置UCD323参数"""
self.last_error = None
self.config = config self.config = config
test_type = self.config.current_test_type test_type = self.config.current_test_type
@@ -200,16 +207,34 @@ class UCDController:
colorimetry = self.config.current_test_types[test_type]["colorimetry"] colorimetry = self.config.current_test_types[test_type]["colorimetry"]
if not self.set_color_mode(color_format, bpc, colorimetry): if not self.set_color_mode(color_format, bpc, colorimetry):
self.last_error = (
f"set_color_mode failed: color_format={color_format}, bpc={bpc}, colorimetry={colorimetry}"
)
log.error(
"UCDController.set_ucd_params set_color_mode failed test_type=%s color_format=%s bpc=%s colorimetry=%s",
test_type,
color_format,
bpc,
colorimetry,
)
return False return False
timing_str = self.config.current_test_types[test_type]["timing"] timing_str = self.config.current_test_types[test_type]["timing"]
self.set_timing_from_string(timing_str) if not self.set_timing_from_string(timing_str):
self.last_error = f"set_timing_from_string failed: timing={timing_str}"
log.error(
"UCDController.set_ucd_params set_timing_from_string failed test_type=%s timing=%s",
test_type,
timing_str,
)
return False
self.current_pattern_index = 0 self.current_pattern_index = 0
pattern_mode = self.config.current_pattern["pattern_mode"] pattern_mode = self.config.current_pattern["pattern_mode"]
pattern = UCDEnum.VideoPatternInfo.get_video_pattern(pattern_mode) pattern = UCDEnum.VideoPatternInfo.get_video_pattern(pattern_mode)
if pattern is None: if pattern is None:
self.last_error = f"get_video_pattern failed: pattern_mode={pattern_mode}"
return False return False
self.current_pattern = pattern self.current_pattern = pattern
@@ -219,10 +244,17 @@ class UCDController:
def run(self): def run(self):
"""运行设备""" """运行设备"""
log.info(
"UCDController.run start current_pattern=%s has_pattern_param=%s",
getattr(self.current_pattern, "name", self.current_pattern),
self.current_pattern_param is not None,
)
self.apply_video_mode() self.apply_video_mode()
self.apply_pattern() self.apply_pattern()
pg, _ = self.get_tx_modules() pg, _ = self.get_tx_modules()
log.info("UCDController.run calling pg.apply()")
pg.apply() pg.apply()
log.info("UCDController.run done")
return True return True
def send_image_pattern(self, image_path): def send_image_pattern(self, image_path):
@@ -245,12 +277,15 @@ class UCDController:
return False return False
try: try:
log.info("UCDController.send_solid_rgb_pattern rgb=%s", rgb)
self.current_pattern = UCDEnum.VideoPatternInfo.get_video_pattern("solidcolor") self.current_pattern = UCDEnum.VideoPatternInfo.get_video_pattern("solidcolor")
if self.current_pattern is None: if self.current_pattern is None:
log.error("UCDController.send_solid_rgb_pattern failed: solidcolor pattern not found")
return False return False
return self.send_current_pattern_params(list(rgb)) return self.send_current_pattern_params(list(rgb))
except Exception: except Exception:
log.exception("UCDController.send_solid_rgb_pattern exception")
return False return False
def send_current_pattern_params(self, pattern_params): def send_current_pattern_params(self, pattern_params):
@@ -260,17 +295,27 @@ class UCDController:
try: try:
if self.current_pattern is None: if self.current_pattern is None:
log.error("UCDController.send_current_pattern_params failed: current_pattern is None")
return False return False
log.info(
"UCDController.send_current_pattern_params pattern=%s params=%s",
getattr(self.current_pattern, "name", self.current_pattern),
pattern_params,
)
if pattern_params is not None and not self.set_pattern( if pattern_params is not None and not self.set_pattern(
self.current_pattern, self.current_pattern,
pattern_params, pattern_params,
): ):
log.error("UCDController.send_current_pattern_params failed: set_pattern returned False")
return False return False
log.info("UCDController.send_current_pattern_params calling run()")
self.run() self.run()
log.info("UCDController.send_current_pattern_params done")
return True return True
except Exception: except Exception:
log.exception("UCDController.send_current_pattern_params exception")
return False return False
def set_color_mode(self, cf, bpc, cm): def set_color_mode(self, cf, bpc, cm):
@@ -298,8 +343,11 @@ class UCDController:
def apply_video_mode(self): def apply_video_mode(self):
"""应用当前 color_info 和 timing""" """应用当前 color_info 和 timing"""
if self.current_timing: if self.current_timing:
log.info("UCDController.apply_video_mode start timing=%s", self.current_timing)
self.set_video_mode() self.set_video_mode()
log.info("UCDController.apply_video_mode done")
return True return True
log.warning("UCDController.apply_video_mode skipped: current_timing is None")
return False return False
def set_video_mode(self): def set_video_mode(self):
@@ -313,28 +361,48 @@ class UCDController:
self.color_info.bpc, self.color_info.bpc,
) )
self.format_changed = (current_config != getattr(self, "_last_sent_config", None)) self.format_changed = (current_config != getattr(self, "_last_sent_config", None))
log.info(
"UCDController.set_video_mode format_changed=%s color_format=%s colorimetry=%s dynamic_range=%s bpc=%s",
self.format_changed,
self.color_info.color_format,
self.color_info.colorimetry,
self.color_info.dynamic_range,
self.color_info.bpc,
)
video_mode = UniTAP.VideoMode( video_mode = UniTAP.VideoMode(
timing=self.current_timing, color_info=self.color_info timing=self.current_timing, color_info=self.color_info
) )
pg, _ = self.get_tx_modules() pg, _ = self.get_tx_modules()
log.info("UCDController.set_video_mode calling pg.set_vm()")
pg.set_vm(vm=video_mode) pg.set_vm(vm=video_mode)
log.info("UCDController.set_video_mode done")
self._last_sent_config = current_config self._last_sent_config = current_config
return True return True
def set_pattern(self, pattern, pattern_params=None): def set_pattern(self, pattern, pattern_params=None):
"""设置pattern""" """设置pattern"""
if self.current_timing: if self.current_timing is None:
needs_params = { # Pattern-only updates (e.g. Calman patch click) can still be applied on
UCDEnum.VideoPatternInfo.VideoPatternParams.SolidColor, # an already active output mode. Missing timing should not block pattern staging.
UCDEnum.VideoPatternInfo.VideoPatternParams.WhiteVStrips, log.warning("UCDController.set_pattern current_timing is None; continue with pattern-only apply")
UCDEnum.VideoPatternInfo.VideoPatternParams.GradientRGBStripes,
UCDEnum.VideoPatternInfo.VideoPatternParams.MotionPattern, needs_params = {
UCDEnum.VideoPatternInfo.VideoPatternParams.SquareWindow, UCDEnum.VideoPatternInfo.VideoPatternParams.SolidColor,
} UCDEnum.VideoPatternInfo.VideoPattern.SolidColor,
if pattern in needs_params and pattern_params: UCDEnum.VideoPatternInfo.VideoPatternParams.WhiteVStrips,
self.set_pattern_params(pattern, pattern_params) UCDEnum.VideoPatternInfo.VideoPatternParams.GradientRGBStripes,
return True UCDEnum.VideoPatternInfo.VideoPatternParams.MotionPattern,
return False UCDEnum.VideoPatternInfo.VideoPatternParams.SquareWindow,
}
log.info(
"UCDController.set_pattern pattern=%s pattern_params=%s needs_params=%s",
getattr(pattern, "name", pattern),
pattern_params,
pattern in needs_params,
)
if pattern in needs_params and pattern_params is not None:
self.set_pattern_params(pattern, pattern_params)
return True
def set_next_pattern(self): def set_next_pattern(self):
"""设置下一个pattern""" """设置下一个pattern"""
@@ -350,25 +418,40 @@ class UCDController:
def set_pattern_params(self, pattern, pattern_params): def set_pattern_params(self, pattern, pattern_params):
"""设置pattern参数""" """设置pattern参数"""
if pattern: if pattern is not None:
if pattern == UCDEnum.VideoPatternInfo.VideoPatternParams.SolidColor: solid_color_patterns = {
UCDEnum.VideoPatternInfo.VideoPatternParams.SolidColor,
UCDEnum.VideoPatternInfo.VideoPattern.SolidColor,
}
if pattern in solid_color_patterns:
log.info("UCDController.set_pattern_params solid_color rgb=%s", pattern_params)
self.current_pattern_param = UniTAP.SolidColorParams( self.current_pattern_param = UniTAP.SolidColorParams(
first=pattern_params[0], first=pattern_params[0],
second=pattern_params[1], second=pattern_params[1],
third=pattern_params[2], third=pattern_params[2],
) )
return True return True
log.warning("UCDController.set_pattern_params unsupported pattern=%s", getattr(pattern, "name", pattern))
return False return False
def apply_pattern(self): def apply_pattern(self):
"""应用当前pattern""" """应用当前pattern"""
if self.current_pattern: if self.current_pattern is not None:
log.info(
"UCDController.apply_pattern start pattern=%s has_params=%s",
getattr(self.current_pattern, "name", self.current_pattern),
self.current_pattern_param is not None,
)
pg, _ = self.get_tx_modules() pg, _ = self.get_tx_modules()
log.info("UCDController.apply_pattern calling pg.set_pattern()")
pg.set_pattern(self.current_pattern) pg.set_pattern(self.current_pattern)
if self.current_pattern_param: if self.current_pattern_param is not None:
log.info("UCDController.apply_pattern calling pg.set_pattern_params()")
pg.set_pattern_params(self.current_pattern_param) pg.set_pattern_params(self.current_pattern_param)
log.info("UCDController.apply_pattern done")
return True return True
log.warning("UCDController.apply_pattern skipped: current_pattern is None")
return False return False
def search_timing(self, width, height, refresh_rate, resolution_type=None): def search_timing(self, width, height, refresh_rate, resolution_type=None):
@@ -383,15 +466,30 @@ class UCDController:
elif resolution_type == "cvt": elif resolution_type == "cvt":
standard = UniTAP.common.timing.Timing.Standard.SD_CVT standard = UniTAP.common.timing.Timing.Standard.SD_CVT
timing = self.timing_manager.search( rr = float(refresh_rate)
h_active=width, # Try both exact and NTSC-compatible rates (e.g. 120000 / 119880).
v_active=height, f_rate_candidates = [
f_rate=int(refresh_rate) * 1000, int(round(rr * 1000)),
standard=standard, int(rr * 1000),
) int(round((rr * 1000.0) * 1000.0 / 1001.0)),
]
# 去重并保持顺序
f_rate_candidates = list(dict.fromkeys(f_rate_candidates))
if timing: standards = [standard]
return timing if standard is not None:
standards.append(None)
for std in standards:
for f_rate in f_rate_candidates:
timing = self.timing_manager.search(
h_active=width,
v_active=height,
f_rate=f_rate,
standard=std,
)
if timing:
return timing
else: else:
for res_type in ["dmt", "cta", "cvt", "ovt"]: for res_type in ["dmt", "cta", "cvt", "ovt"]:
result = self.search_timing(width, height, refresh_rate, res_type) result = self.search_timing(width, height, refresh_rate, res_type)
@@ -492,18 +590,54 @@ class UCDController:
def set_timing_from_string(self, timing_str): def set_timing_from_string(self, timing_str):
"""根据格式化timing字符串设置设备timing""" """根据格式化timing字符串设置设备timing"""
spec = self.parse_formatted_timing(timing_str) try:
spec = self.parse_formatted_timing(timing_str)
except Exception:
log.exception("UCDController.set_timing_from_string parse failed timing=%s", timing_str)
return False
rtype = spec["resolution_type"] rtype = spec["resolution_type"]
rid = spec.get("resolution_id")
width = spec["width"] width = spec["width"]
height = spec["height"] height = spec["height"]
fr = spec["refresh_rate"] fr = spec["refresh_rate"]
if rtype != "ovt": if rid is not None and self.set_timing_from_id(rtype, rid):
timing = self.search_timing(width, height, fr, rtype) log.info(
if timing: "UCDController.set_timing_from_string success by id timing=%s parsed=(%s id=%s)",
self.current_timing = timing timing_str,
return True rtype,
rid,
)
return True
# Respect selected timing family first (DMT/CTA/CVT/OVT).
timing = self.search_timing(width, height, fr, rtype)
if timing is None:
# Fallback only for robustness: some SDKs may not classify a timing
# exactly as requested family even though width/height/fps matches.
timing = self.search_timing(width, height, fr, None)
if timing:
self.current_timing = timing
log.info(
"UCDController.set_timing_from_string success timing=%s parsed=(%s %sx%s@%s)",
timing_str,
rtype,
width,
height,
fr,
)
return True
log.error(
"UCDController.set_timing_from_string no timing matched timing=%s parsed=(%s %sx%s@%s)",
timing_str,
rtype,
width,
height,
fr,
)
return False return False
def set_timing_from_id(self, rtype, rid): def set_timing_from_id(self, rtype, rid):
@@ -515,6 +649,12 @@ class UCDController:
timing = self.timing_manager.get_cta(rid) timing = self.timing_manager.get_cta(rid)
elif rtype.lower() == "cvt": elif rtype.lower() == "cvt":
timing = self.timing_manager.get_cvt(rid) timing = self.timing_manager.get_cvt(rid)
elif rtype.lower() == "ovt":
get_ovt = getattr(self.timing_manager, "get_ovt", None)
if callable(get_ovt):
timing = get_ovt(rid)
else:
return False
else: else:
raise ValueError(f"不支持的分辨率类型: {rtype}") raise ValueError(f"不支持的分辨率类型: {rtype}")

View File

@@ -18,6 +18,7 @@ SDK 调用直接搬入本模块,届时可删除旧 ``UCDController`` 文件。
from __future__ import annotations from __future__ import annotations
from contextlib import contextmanager
import logging import logging
import threading import threading
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
@@ -48,6 +49,8 @@ if TYPE_CHECKING:
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
_DEVICE_LOCK_TIMEOUT_SECONDS = 8.0
# ─── §1 DeviceInfo / list_devices ──────────────────────────────── # ─── §1 DeviceInfo / list_devices ────────────────────────────────
@@ -154,12 +157,50 @@ class UCD323Device(IUcdDevice):
self._info: DeviceInfo | None = None self._info: DeviceInfo | None = None
self._interface: Interface = Interface.HDMI self._interface: Interface = Interface.HDMI
self._lock = threading.RLock() self._lock = threading.RLock()
self._lock_owner_tid: int | None = None
self._lock_owner_name: str | None = None
self._curr_signal: SignalFormat | None = None self._curr_signal: SignalFormat | None = None
self._curr_timing: TimingSpec | None = None self._curr_timing: TimingSpec | None = None
self._curr_pattern: PatternSpec | None = None self._curr_pattern: PatternSpec | None = None
self._last_applied: tuple[SignalFormat, TimingSpec] | None = None self._last_applied: tuple[SignalFormat, TimingSpec] | None = None
@contextmanager
def _acquire_device_lock(self, op_name: str):
current = threading.current_thread()
log.info(
"UCD323Device.%s acquiring lock timeout=%.1fs tid=%s thread=%s owner_tid=%s owner_thread=%s",
op_name,
_DEVICE_LOCK_TIMEOUT_SECONDS,
threading.get_ident(),
current.name,
self._lock_owner_tid,
self._lock_owner_name,
)
acquired = self._lock.acquire(timeout=_DEVICE_LOCK_TIMEOUT_SECONDS)
if not acquired:
raise UcdStateError(
"UCD device busy: lock timeout in "
f"UCD323Device.{op_name}, "
f"owner_tid={self._lock_owner_tid}, owner_thread={self._lock_owner_name}"
)
prev_owner_tid = self._lock_owner_tid
prev_owner_name = self._lock_owner_name
self._lock_owner_tid = threading.get_ident()
self._lock_owner_name = current.name
log.info(
"UCD323Device.%s lock acquired tid=%s thread=%s",
op_name,
self._lock_owner_tid,
self._lock_owner_name,
)
try:
yield
finally:
self._lock_owner_tid = prev_owner_tid
self._lock_owner_name = prev_owner_name
self._lock.release()
# -- 读访问 -------------------------------------------------- # -- 读访问 --------------------------------------------------
@property @property
@@ -181,7 +222,7 @@ class UCD323Device(IUcdDevice):
# -- 生命周期 ------------------------------------------------ # -- 生命周期 ------------------------------------------------
def open(self, info: DeviceInfo, *, interface: Interface = Interface.HDMI) -> None: def open(self, info: DeviceInfo, *, interface: Interface = Interface.HDMI) -> None:
with self._lock: with self._acquire_device_lock("open"):
assert_transition(self._state, UcdState.OPENED) assert_transition(self._state, UcdState.OPENED)
if interface is not Interface.HDMI: if interface is not Interface.HDMI:
# Phase 1底层 UCDController.open() 写死了 HDMISource。 # Phase 1底层 UCDController.open() 写死了 HDMISource。
@@ -200,7 +241,7 @@ class UCD323Device(IUcdDevice):
self._bus.publish(ConnectionChanged(True, info.serial)) self._bus.publish(ConnectionChanged(True, info.serial))
def close(self) -> None: def close(self) -> None:
with self._lock: with self._acquire_device_lock("close"):
if self._state == UcdState.CLOSED: if self._state == UcdState.CLOSED:
return return
try: try:
@@ -219,7 +260,7 @@ class UCD323Device(IUcdDevice):
# -- 配置 ---------------------------------------------------- # -- 配置 ----------------------------------------------------
def configure(self, signal: SignalFormat, timing: TimingSpec) -> bool: def configure(self, signal: SignalFormat, timing: TimingSpec) -> bool:
with self._lock: with self._acquire_device_lock("configure"):
if self._state == UcdState.CLOSED: if self._state == UcdState.CLOSED:
raise UcdNotConnected("UCD 未连接,无法 configure") raise UcdNotConnected("UCD 未连接,无法 configure")
try: try:
@@ -249,7 +290,7 @@ class UCD323Device(IUcdDevice):
return (signal, timing) != self._last_applied return (signal, timing) != self._last_applied
def set_pattern(self, pattern: PatternSpec) -> None: def set_pattern(self, pattern: PatternSpec) -> None:
with self._lock: with self._acquire_device_lock("set_pattern"):
# Phase 2 过渡:允许从 OPENED 直接 set_pattern——遗留路径 # Phase 2 过渡:允许从 OPENED 直接 set_pattern——遗留路径
# test_runner 等)通过旧 controller.apply_signal_format 写入 # test_runner 等)通过旧 controller.apply_signal_format 写入
# 信号格式,未经过本设备的 configure。此时 self._state 仍为 # 信号格式,未经过本设备的 configure。此时 self._state 仍为
@@ -260,7 +301,7 @@ class UCD323Device(IUcdDevice):
# 仅本地暂存,真正写硬件在 apply() # 仅本地暂存,真正写硬件在 apply()
def apply(self) -> None: def apply(self) -> None:
with self._lock: with self._acquire_device_lock("apply"):
if self._state == UcdState.CLOSED: if self._state == UcdState.CLOSED:
raise UcdNotConnected("UCD 未连接,无法 apply") raise UcdNotConnected("UCD 未连接,无法 apply")
if self._curr_pattern is None: if self._curr_pattern is None:
@@ -268,7 +309,9 @@ class UCD323Device(IUcdDevice):
try: try:
ok = self._apply_pattern_via_controller(self._curr_pattern) ok = self._apply_pattern_via_controller(self._curr_pattern)
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
raise UcdSdkError("apply 异常") from exc raise UcdSdkError(
f"apply 异常: {type(exc).__name__}: {exc}"
) from exc
if not ok: if not ok:
raise UcdApplyFailed( raise UcdApplyFailed(
f"apply 失败: pattern={self._curr_pattern.kind.value}" f"apply 失败: pattern={self._curr_pattern.kind.value}"
@@ -333,7 +376,21 @@ class UCD323Device(IUcdDevice):
if not self._controller.set_pattern(video_pattern, params): if not self._controller.set_pattern(video_pattern, params):
raise UcdApplyFailed("controller.set_pattern 返回 False") raise UcdApplyFailed("controller.set_pattern 返回 False")
return bool(self._controller.run()) # Skip apply_video_mode() (i.e. pg.set_vm) the video format is already
# configured by the main signal panel and re-applying it blocks until the
# device re-locks, causing an apparent UI freeze for pattern-only sends.
if not self._controller.apply_pattern():
raise UcdApplyFailed("controller.apply_pattern 返回 False")
if getattr(self._controller, "current_timing", None) is None:
raise UcdConfigError(
"current_timing is None; please apply selected test profile/timing before sending pattern"
)
try:
pg, _ = self._controller.get_tx_modules()
pg.apply()
except Exception as exc:
raise UcdSdkError("pg.apply() 失败") from exc
return True
def _colorimetry_to_legacy_key(signal: SignalFormat) -> str: def _colorimetry_to_legacy_key(signal: SignalFormat) -> str:

View File

@@ -6,6 +6,7 @@ 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 drivers.UCD323_Function import UCDController
@@ -62,7 +63,6 @@ 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,
@@ -388,43 +388,31 @@ 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):
"""切换测试类型""" """切换测试类型"""
@@ -443,10 +431,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)

View File

@@ -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.601"
}, },
"hdr_movie": { "hdr_movie": {
"name": "HDR Movie测试", "name": "HDR Movie测试",