修改calman灰阶点击异常、修改色准结果显示异常
This commit is contained in:
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
@@ -93,6 +94,11 @@ class TkLogHandler(logging.Handler):
|
||||
def emit(self, record: logging.LogRecord) -> None: # noqa: D401
|
||||
if getattr(record, _FROM_GUI_FLAG, False):
|
||||
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:
|
||||
message = self.format(record)
|
||||
except Exception:
|
||||
|
||||
@@ -104,6 +104,7 @@ def _render_chromaticity(kind: str, bbox: _BBox) -> np.ndarray:
|
||||
fig.canvas.draw()
|
||||
# 从 canvas 抓取 RGBA 数组
|
||||
buf = np.asarray(fig.canvas.buffer_rgba()).copy()
|
||||
buf = np.flipud(buf)
|
||||
plt.close(fig)
|
||||
|
||||
try:
|
||||
|
||||
@@ -9,9 +9,12 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from matplotlib.patches import Rectangle
|
||||
from matplotlib.lines import Line2D
|
||||
from matplotlib.patches import Circle
|
||||
|
||||
from app.plots.gamut_background import get_cie1976_background
|
||||
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:
|
||||
from pqAutomationApp import PQAutomationApp
|
||||
@@ -55,14 +58,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):
|
||||
"""CIE 1931 xy → CIE 1976 u'v'"""
|
||||
denom = -2.0 * x + 12.0 * y + 3.0
|
||||
@@ -76,7 +71,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):
|
||||
"""左侧仅保留大条形图。"""
|
||||
"""左侧仅保留大条形图"""
|
||||
ax.clear()
|
||||
|
||||
n = len(color_patches)
|
||||
@@ -86,15 +81,14 @@ def _draw_left_panel(ax, color_patches, delta_e_values, font_scale=1.0, dark_mod
|
||||
|
||||
y_pos = list(range(n))
|
||||
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(
|
||||
y_pos,
|
||||
delta_e_values,
|
||||
height=0.72,
|
||||
color=bar_colors,
|
||||
edgecolor=edge_colors,
|
||||
linewidth=1.0,
|
||||
edgecolor="#202020",
|
||||
linewidth=0.5,
|
||||
zorder=3,
|
||||
)
|
||||
|
||||
@@ -129,9 +123,12 @@ def _draw_uv_diagram(ax, color_patches, measurements, standards, font_scale=1.0,
|
||||
bg, bbox = get_cie1976_background()
|
||||
xmin, xmax, ymin, ymax = bbox
|
||||
ax.imshow(
|
||||
bg, extent=(xmin, xmax, ymin, ymax),
|
||||
origin="upper", interpolation="bicubic",
|
||||
zorder=0, aspect="auto",
|
||||
bg,
|
||||
extent=(xmin, xmax, ymin, ymax),
|
||||
origin="lower",
|
||||
interpolation="bicubic",
|
||||
zorder=0,
|
||||
aspect="auto",
|
||||
)
|
||||
ax.set_xlim(xmin, xmax)
|
||||
ax.set_ylim(ymin, ymax)
|
||||
@@ -145,73 +142,122 @@ def _draw_uv_diagram(ax, color_patches, measurements, standards, font_scale=1.0,
|
||||
legend_label_color = "#FFF" if dark_mode else "#111"
|
||||
legend_bg = "#111" if dark_mode else "#FFFFFF"
|
||||
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_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_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)
|
||||
|
||||
for sp in ax.spines.values():
|
||||
sp.set_color(outer_edge)
|
||||
sp.set_linewidth(0.9)
|
||||
|
||||
for name, meas in zip(color_patches, measurements):
|
||||
|
||||
if meas is None or len(meas) < 2:
|
||||
continue
|
||||
|
||||
mx, my = meas[0], meas[1]
|
||||
sxy = standards.get(name)
|
||||
|
||||
if sxy is None:
|
||||
continue
|
||||
|
||||
sx, sy = sxy
|
||||
|
||||
m_u, m_v = _xy_to_uv(mx, my)
|
||||
s_u, s_v = _xy_to_uv(sx, sy)
|
||||
|
||||
face = _COLOR_MAP.get(name, "#FFFFFF")
|
||||
# face = get_patch_color_from_xy(name, (sx, sy)).strip().upper()
|
||||
face = _COLOR_MAP.get(name, "#888888")
|
||||
print(name, face)
|
||||
|
||||
# 目标点:仅空心方框(不填充标准颜色)
|
||||
# 目标点(Target) 空心方框
|
||||
ax.scatter(
|
||||
[s_u], [s_v],
|
||||
s=56, marker="s",
|
||||
facecolors="none", edgecolors=outer_edge,
|
||||
linewidths=1.25, zorder=18,
|
||||
s_u,
|
||||
s_v,
|
||||
s=90,
|
||||
marker="s",
|
||||
facecolors="none",
|
||||
edgecolors=outer_edge,
|
||||
linewidths=1.6,
|
||||
zorder=18,
|
||||
)
|
||||
# 实测点:白色外圈 + 内层圆点
|
||||
|
||||
# 实测点(Actual) 彩色实心 + 白色描边
|
||||
ax.scatter(
|
||||
[m_u], [m_v],
|
||||
s=52, marker="o",
|
||||
facecolors="none", edgecolors=outer_edge,
|
||||
linewidths=1.0, zorder=19,
|
||||
)
|
||||
ax.scatter(
|
||||
[m_u], [m_v],
|
||||
s=24, marker="o",
|
||||
facecolors=face, edgecolors="#111111",
|
||||
linewidths=0.85, zorder=20,
|
||||
[m_u],
|
||||
[m_v],
|
||||
s=80,
|
||||
marker="o",
|
||||
color=face,
|
||||
edgecolors=outer_edge,
|
||||
linewidths=1.2,
|
||||
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 = [
|
||||
Line2D([0], [0], marker="s", linestyle="none",
|
||||
markerfacecolor="#CCCCCC", markeredgecolor=outer_edge,
|
||||
markersize=7, label="目标 (Target)"),
|
||||
Line2D([0], [0], marker="o", linestyle="none",
|
||||
markerfacecolor="#CCCCCC", markeredgecolor="#000000",
|
||||
markersize=7, label="实测 (Actual)"),
|
||||
Line2D(
|
||||
[0],
|
||||
[0],
|
||||
marker="s",
|
||||
linestyle="none",
|
||||
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(
|
||||
handles=legend_handles,
|
||||
loc="lower right", fontsize=max(6, 8 * font_scale),
|
||||
framealpha=0.88, labelcolor=legend_label_color,
|
||||
loc="lower right",
|
||||
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_edgecolor(legend_edge)
|
||||
leg.set_zorder(50)
|
||||
|
||||
|
||||
|
||||
def _draw_result_judgement(ax, accuracy_data, font_scale=1.0, dark_mode=False):
|
||||
"""底部结果条"""
|
||||
ax.clear()
|
||||
@@ -353,8 +399,11 @@ def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type):
|
||||
except Exception:
|
||||
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.accuracy_canvas.get_tk_widget().update_idletasks()
|
||||
self.accuracy_canvas.draw()
|
||||
|
||||
|
||||
class PlotAccuracyMixin:
|
||||
|
||||
101
app/pq/color_patch_map.py
Normal file
101
app/pq/color_patch_map.py
Normal file
@@ -0,0 +1,101 @@
|
||||
import re
|
||||
import numpy as np
|
||||
|
||||
# ============================================================
|
||||
# 标准 ColorChecker / TV Patch 颜色
|
||||
# ============================================================
|
||||
COLOR_PATCH_MAP = {
|
||||
"White": "#FFFFFF",
|
||||
"Gray 80": "#E6E6E6",
|
||||
"Gray 65": "#D1D1D1",
|
||||
"Gray 50": "#BABABA",
|
||||
"Gray 35": "#9E9E9E",
|
||||
"Black": "#000000",
|
||||
|
||||
"Dark Skin": "#735242",
|
||||
"Light Skin": "#C29682",
|
||||
"Blue Sky": "#5E7A9C",
|
||||
"Foliage": "#596B42",
|
||||
"Blue Flower": "#8280B0",
|
||||
"Bluish Green": "#63BDA8",
|
||||
"Orange": "#D97829",
|
||||
"Purplish Blue": "#4A5CA3",
|
||||
"Moderate Red": "#C25461",
|
||||
"Purple": "#5C3D6B",
|
||||
"Yellow Green": "#9EBA40",
|
||||
"Orange Yellow": "#E6A12E",
|
||||
|
||||
"Blue (Legacy)": "#333D96",
|
||||
"Green (Legacy)": "#479447",
|
||||
"Red (Legacy)": "#B0303B",
|
||||
"Yellow (Legacy)": "#EDC721",
|
||||
"Magenta (Legacy)": "#BA5491",
|
||||
"Cyan (Legacy)": "#0085A3",
|
||||
|
||||
"100% Red": "#FF0000",
|
||||
"100% Green": "#00FF00",
|
||||
"100% Blue": "#0000FF",
|
||||
"100% Cyan": "#00FFFF",
|
||||
"100% Magenta": "#FF00FF",
|
||||
"100% Yellow": "#FFFF00",
|
||||
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# 名称标准化
|
||||
# ============================================================
|
||||
def normalize_patch_name(name: str) -> str:
|
||||
if not isinstance(name, str):
|
||||
return ""
|
||||
name = name.strip().lower()
|
||||
# collapse spaces
|
||||
name = " ".join(name.split())
|
||||
return name
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 获取 patch 颜色
|
||||
# ============================================================
|
||||
def get_patch_color(name: str):
|
||||
norm = normalize_patch_name(name)
|
||||
for key, color in COLOR_PATCH_MAP.items():
|
||||
|
||||
if normalize_patch_name(key) == norm:
|
||||
return color
|
||||
|
||||
return None
|
||||
|
||||
# ============================================================
|
||||
# xy → sRGB fallback
|
||||
# ============================================================
|
||||
def xy_to_srgb(x, y, Y=1.0):
|
||||
if y == 0:
|
||||
return "#AAAAAA"
|
||||
|
||||
X = (x * Y) / y
|
||||
Z = (1 - x - y) * Y / y
|
||||
|
||||
rgb = np.dot(
|
||||
[[3.2406, -1.5372, -0.4986],
|
||||
[-0.9689, 1.8758, 0.0415],
|
||||
[0.0557, -0.2040, 1.0570]],
|
||||
[X, Y, Z]
|
||||
)
|
||||
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
rgb = (rgb ** (1 / 2.2)) * 255
|
||||
|
||||
r, g, b = rgb.astype(int)
|
||||
return "#{:02X}{:02X}{:02X}".format(r, g, b)
|
||||
|
||||
def get_patch_color_from_xy(name, xy=None):
|
||||
color = get_patch_color(name)
|
||||
|
||||
if color:
|
||||
return color
|
||||
|
||||
if xy is not None:
|
||||
x, y = xy
|
||||
return xy_to_srgb(x, y)
|
||||
|
||||
return "#AAAAAA"
|
||||
@@ -293,62 +293,107 @@ def _call_pqtest_upload(file_path: str, timeout: float = API_UPLOAD_TIMEOUT, aut
|
||||
iw, ih = img.size
|
||||
except Exception as exc:
|
||||
raise ValueError(f"无法读取图片: {exc}") from exc
|
||||
|
||||
# 检查大小,如需则缩放
|
||||
|
||||
size = os.path.getsize(file_path)
|
||||
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 not auto_resize:
|
||||
if iw > UPLOAD_MAX_PIXELS or ih > UPLOAD_MAX_PIXELS:
|
||||
raise ValueError(f"分辨率超过 4096×4096(当前 {iw}×{ih})")
|
||||
else:
|
||||
raise ValueError(f"图片超过 10MB 限制(当前 {size/1024/1024:.2f}MB)")
|
||||
|
||||
# 自动缩放:等比例缩放至 4096×4096 以内
|
||||
logger.info("[AIImage][UPLOAD] 自动缩放 %dx%d (%.1fMB) 至 ≤4096×4096",
|
||||
iw, ih, size/1024/1024)
|
||||
scale = min(UPLOAD_MAX_PIXELS / iw, UPLOAD_MAX_PIXELS / ih, 1.0)
|
||||
new_w, new_h = max(1, int(iw * scale)), max(1, int(ih * scale))
|
||||
|
||||
raise ValueError(f"图片超过 10MB 限制(当前 {size/1024/1024:.2f}MB)")
|
||||
|
||||
logger.info(
|
||||
"[AIImage][UPLOAD] 自动处理超限图片 %dx%d (%.2fMB)",
|
||||
iw,
|
||||
ih,
|
||||
size / 1024 / 1024,
|
||||
)
|
||||
|
||||
with Image.open(file_path) as img:
|
||||
img_resized = img.resize((new_w, new_h), Image.LANCZOS)
|
||||
# 重压至 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
|
||||
working = img.copy()
|
||||
|
||||
# 先做一次分辨率约束,避免后续压缩开销过大。
|
||||
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:
|
||||
# 原格式太大,转换为 JPEG 并压缩
|
||||
logger.info("[AIImage][UPLOAD] 原格式超过限制,转为 JPEG 并压缩")
|
||||
quality = 95
|
||||
while quality >= 50:
|
||||
tmp_io = BytesIO()
|
||||
img_resized.save(tmp_io, format="JPEG", quality=quality, optimize=True)
|
||||
tmp_bytes = tmp_io.getvalue()
|
||||
if len(tmp_bytes) <= UPLOAD_MAX_BYTES:
|
||||
file_bytes = tmp_bytes
|
||||
rgb = working.convert("RGB") if working.mode not in {"RGB", "L"} else working
|
||||
rgb.save(raw_io, format="JPEG", quality=95, optimize=True)
|
||||
raw_mime, raw_ext = "image/jpeg", ".jpg"
|
||||
best_bytes = raw_io.getvalue()
|
||||
best_mime = raw_mime
|
||||
best_ext = raw_ext
|
||||
except Exception as exc:
|
||||
logger.warning("[AIImage][UPLOAD] 原格式编码失败,准备转 JPEG: %s", exc)
|
||||
|
||||
# 仍超限时,转 JPEG + 渐进压缩;如仍超限则继续降分辨率。
|
||||
if len(best_bytes) > UPLOAD_MAX_BYTES:
|
||||
if best_ext != ".jpg":
|
||||
logger.info("[AIImage][UPLOAD] 原格式仍超限,切换 JPEG 压缩")
|
||||
working_jpg = working.convert("RGB") if working.mode != "RGB" else working
|
||||
while True:
|
||||
compressed = b""
|
||||
for q in (95, 90, 85, 80, 75, 70, 65, 60, 55, 50):
|
||||
tmp = BytesIO()
|
||||
working_jpg.save(tmp, format="JPEG", quality=q, optimize=True)
|
||||
data = tmp.getvalue()
|
||||
compressed = data
|
||||
if len(data) <= UPLOAD_MAX_BYTES:
|
||||
break
|
||||
quality -= 5
|
||||
else:
|
||||
# 即使 quality=50 还是太大,也用这个版本上传(后端会处理)
|
||||
file_bytes = tmp_bytes
|
||||
|
||||
logger.info("[AIImage][UPLOAD] 缩放完成 %dx%d (%.1fMB)",
|
||||
new_w, new_h, len(file_bytes)/1024/1024)
|
||||
iw, ih = new_w, new_h
|
||||
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:
|
||||
with open(file_path, "rb") as f:
|
||||
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
|
||||
filename = os.path.basename(file_path)
|
||||
crlf = b"\r\n"
|
||||
body = b"".join([
|
||||
b"--", boundary.encode("ascii"), crlf,
|
||||
|
||||
@@ -21,8 +21,23 @@ class PatternService:
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
def _build_apply_config_error(self, test_type):
|
||||
timing = self.app.config.current_test_types.get(test_type, {}).get("timing", "-")
|
||||
detail = ""
|
||||
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):
|
||||
test_type = test_type or self.app.config.current_test_type
|
||||
if hasattr(self.app.config, "set_current_test_type"):
|
||||
self.app.config.set_current_test_type(test_type)
|
||||
if not self.app.config.set_current_pattern(mode):
|
||||
raise ValueError(f"未知的图案模式: {mode}")
|
||||
|
||||
@@ -64,7 +79,8 @@ class PatternService:
|
||||
("Timing", self.app.config.current_test_types[test_type]["timing"]),
|
||||
]:
|
||||
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(
|
||||
color_space=color_space,
|
||||
data_range=data_range,
|
||||
@@ -97,7 +113,10 @@ class PatternService:
|
||||
active_config = self.app.config.get_temp_config_with_converted_params(
|
||||
mode=mode, converted_params=converted_params
|
||||
)
|
||||
self.app.signal_service.apply_config(active_config)
|
||||
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(
|
||||
color_space=self.app.sdr_color_space_var.get(),
|
||||
data_range=data_range,
|
||||
@@ -129,7 +148,10 @@ class PatternService:
|
||||
active_config = self.app.config.get_temp_config_with_converted_params(
|
||||
mode=mode, converted_params=converted_params
|
||||
)
|
||||
self.app.signal_service.apply_config(active_config)
|
||||
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(
|
||||
color_space=self.app.hdr_color_space_var.get(),
|
||||
data_range=data_range,
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
import logging
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from app.ucd_domain import (
|
||||
@@ -34,6 +36,10 @@ from drivers.ucd_driver import IUcdDevice
|
||||
|
||||
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._bus = bus
|
||||
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:
|
||||
``format_changed``——本次相对上一次 :meth:`apply` 是否变化。
|
||||
"""
|
||||
with self._lock:
|
||||
with self._acquire_service_lock("apply"):
|
||||
log.info(
|
||||
"SignalService.apply signal=%s timing=%s pattern=%s",
|
||||
signal,
|
||||
@@ -114,7 +167,7 @@ class SignalService:
|
||||
|
||||
def send_pattern(self, pattern: PatternSpec) -> None:
|
||||
"""在已 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)
|
||||
self._dev.set_pattern(pattern)
|
||||
self._dev.apply()
|
||||
@@ -155,7 +208,7 @@ class SignalService:
|
||||
ctrl = getattr(self._dev, "raw_controller", None)
|
||||
if ctrl is None:
|
||||
raise UcdError("update_signal_format 暂仅支持 UCD323Device")
|
||||
with self._lock:
|
||||
with self._acquire_service_lock("update_signal_format"):
|
||||
return bool(
|
||||
ctrl.apply_signal_format(
|
||||
color_space=color_space,
|
||||
@@ -193,7 +246,7 @@ class SignalService:
|
||||
ctrl = getattr(self._dev, "raw_controller", None)
|
||||
if ctrl is None:
|
||||
raise UcdError("apply_config 暂仅支持 UCD323Device")
|
||||
with self._lock:
|
||||
with self._acquire_service_lock("apply_config"):
|
||||
return bool(ctrl.set_ucd_params(config))
|
||||
|
||||
def send_pattern_params(self, params) -> bool:
|
||||
@@ -201,7 +254,7 @@ class SignalService:
|
||||
ctrl = getattr(self._dev, "raw_controller", None)
|
||||
if ctrl is None:
|
||||
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))
|
||||
|
||||
def apply_and_run(self, config, pattern_params) -> bool:
|
||||
@@ -212,7 +265,7 @@ class SignalService:
|
||||
ctrl = getattr(self._dev, "raw_controller", None)
|
||||
if ctrl is None:
|
||||
raise UcdError("apply_and_run 暂仅支持 UCD323Device")
|
||||
with self._lock:
|
||||
with self._acquire_service_lock("apply_and_run"):
|
||||
if not ctrl.set_ucd_params(config):
|
||||
return False
|
||||
if not ctrl.set_pattern(ctrl.current_pattern, pattern_params):
|
||||
|
||||
@@ -803,17 +803,22 @@ def _on_upload_done(self: "PQAutomationApp", name: str, url: str, exc):
|
||||
|
||||
|
||||
def _clear_reference_image(self: "PQAutomationApp"):
|
||||
"""清除手动上传的参考图,同时清除当前会话的自动链路参考。"""
|
||||
"""清除手动上传的参考图。
|
||||
|
||||
v2.1 规则要求:从第二轮开始应使用最近一次成功生成图作为输入,
|
||||
因此这里不清除会话级自动链路参考;若需彻底重置,请点“新对话”。
|
||||
"""
|
||||
if getattr(self, "_ai_image_requesting", False):
|
||||
return
|
||||
self._ai_image_pending_ref_url = ""
|
||||
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)
|
||||
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:
|
||||
|
||||
@@ -232,6 +232,62 @@ def _apply_calman_tree_style(self: "PQAutomationApp") -> None:
|
||||
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:
|
||||
"""创建 CALMAN 风格灰阶测试面板,注册到 panel_manager。"""
|
||||
palette = _get_calman_palette()
|
||||
@@ -242,6 +298,7 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
|
||||
self.calman_results = {}
|
||||
self.calman_stop_event = threading.Event()
|
||||
self.calman_running = False
|
||||
self.calman_patch_send_busy = False
|
||||
self.calman_current_level = None
|
||||
self.calman_last_record = None
|
||||
self.calman_last_step_seconds = None
|
||||
@@ -298,6 +355,15 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
|
||||
)
|
||||
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.grid(row=2, column=0, sticky=tk.EW, pady=(2, 0))
|
||||
metrics_row.columnconfigure((0, 1, 2, 3), weight=1)
|
||||
@@ -478,7 +544,13 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
|
||||
)
|
||||
|
||||
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):
|
||||
_bind_click(w)
|
||||
@@ -581,6 +653,7 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
|
||||
right.bind("<Configure>", lambda _e: _adaptive_matrix_columns(self))
|
||||
|
||||
_refresh_metric_table(self)
|
||||
_refresh_calman_config_summary(self)
|
||||
_update_target_strip(self)
|
||||
_update_actual_strip(self)
|
||||
_redraw_calman_charts(self)
|
||||
@@ -592,6 +665,7 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
|
||||
def toggle_calman_panel(self: "PQAutomationApp") -> None:
|
||||
"""切换 CALMAN 灰阶面板显示。"""
|
||||
self.show_panel("calman")
|
||||
_refresh_calman_config_summary(self)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -604,23 +678,43 @@ def send_patch(self: "PQAutomationApp", pct: int) -> None:
|
||||
if not self.signal_service.is_connected:
|
||||
messagebox.showwarning("提示", "请先连接 UCD323 设备")
|
||||
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))
|
||||
self.calman_current_level = pct
|
||||
self.calman_status_var.set(f"发送 {pct}%(RGB={rgb_val})...")
|
||||
_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():
|
||||
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.log_gui.log, f"CALMAN: 已发送 {pct}% 灰阶 (RGB={rgb_val})",
|
||||
"info",
|
||||
)
|
||||
self._dispatch_ui(self.calman_status_var.set, f"{pct}% 已发送")
|
||||
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.calman_status_var.set, f"发送失败: {exc}")
|
||||
finally:
|
||||
self.calman_patch_send_busy = False
|
||||
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
@@ -693,14 +787,32 @@ def measure_current_patch(self: "PQAutomationApp") -> None:
|
||||
|
||||
def worker():
|
||||
t0 = time.perf_counter()
|
||||
_calman_log(self, f"measure start pct={pct}")
|
||||
self._dispatch_ui(self.calman_status_var.set, f"采集 {pct}% 中...")
|
||||
rec = _measure_once(self, pct)
|
||||
if rec is None:
|
||||
_calman_log(self, f"measure failed pct={pct}", "error")
|
||||
self._dispatch_ui(self.calman_status_var.set, "采集失败")
|
||||
return
|
||||
step_s = time.perf_counter() - t0
|
||||
self.calman_last_step_seconds = step_s
|
||||
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(
|
||||
self.calman_status_var.set, f"{pct}% 采集完成 ({step_s:.2f}s)"
|
||||
@@ -726,15 +838,28 @@ def start_sequence_test(self: "PQAutomationApp") -> None:
|
||||
settle = float(getattr(self, "pattern_settle_time", 0.4))
|
||||
self.calman_progress["value"] = 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():
|
||||
seq_t0 = time.perf_counter()
|
||||
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)
|
||||
total = len(order)
|
||||
self._dispatch_ui(self.calman_progress_var.set, f"0 / {total}")
|
||||
for i, pct in enumerate(order, 1):
|
||||
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")
|
||||
break
|
||||
step_t0 = time.perf_counter()
|
||||
@@ -742,12 +867,20 @@ def start_sequence_test(self: "PQAutomationApp") -> None:
|
||||
self._dispatch_ui(
|
||||
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)
|
||||
try:
|
||||
self.signal_service.send_solid_rgb(
|
||||
(rgb_val, rgb_val, rgb_val)
|
||||
)
|
||||
if rgb_session is not None:
|
||||
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:
|
||||
_calman_log(self, f"sequence send failed step={i}/{total}, pct={pct}: {exc}", "error")
|
||||
self._dispatch_ui(
|
||||
self.log_gui.log, f"发送 {pct}% 失败: {exc}", "error"
|
||||
)
|
||||
@@ -755,11 +888,30 @@ def start_sequence_test(self: "PQAutomationApp") -> None:
|
||||
self.calman_current_level = pct
|
||||
# 等待稳定,停止事件触发时尽快退出
|
||||
if self.calman_stop_event.wait(settle):
|
||||
_calman_log(self, f"sequence interrupted during settle step={i}/{total}, pct={pct}", "warning")
|
||||
break
|
||||
rec = _measure_once(self, pct)
|
||||
if rec is None:
|
||||
_calman_log(self, f"sequence measure failed step={i}/{total}, pct={pct}", "error")
|
||||
continue
|
||||
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)
|
||||
step_s = time.perf_counter() - step_t0
|
||||
total_s = time.perf_counter() - seq_t0
|
||||
@@ -772,9 +924,11 @@ def start_sequence_test(self: "PQAutomationApp") -> None:
|
||||
total_s,
|
||||
)
|
||||
else:
|
||||
_calman_log(self, f"sequence complete total={total}")
|
||||
self._dispatch_ui(self.calman_status_var.set, "连续测试完成")
|
||||
self._dispatch_ui(self.log_gui.log, "CALMAN: 连续测试完成", "success")
|
||||
return
|
||||
_calman_log(self, "sequence stopped", "warning")
|
||||
self._dispatch_ui(self.calman_status_var.set, "已停止")
|
||||
finally:
|
||||
self.calman_running = False
|
||||
@@ -785,14 +939,17 @@ def start_sequence_test(self: "PQAutomationApp") -> None:
|
||||
def stop_sequence_test(self: "PQAutomationApp") -> None:
|
||||
"""请求停止连续测试。"""
|
||||
if self.calman_running:
|
||||
_calman_log(self, "stop requested", "warning")
|
||||
self.calman_stop_event.set()
|
||||
self.calman_status_var.set("正在停止...")
|
||||
else:
|
||||
_calman_log(self, "stop requested but no sequence is running", "warning")
|
||||
self.calman_status_var.set("当前没有运行中的连续测试")
|
||||
|
||||
|
||||
def clear_results(self: "PQAutomationApp") -> None:
|
||||
"""清空结果表和图表。"""
|
||||
_calman_log(self, "clear results")
|
||||
self.calman_results.clear()
|
||||
self.calman_last_record = None
|
||||
self.calman_reading_var.set(
|
||||
@@ -1135,6 +1292,8 @@ def refresh_calman_theme(self: "PQAutomationApp") -> None:
|
||||
|
||||
if hasattr(self, "calman_elapsed_label"):
|
||||
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"):
|
||||
self.calman_status_label.configure(foreground=palette["status_fg"])
|
||||
if hasattr(self, "calman_reading_summary_label"):
|
||||
@@ -1151,6 +1310,7 @@ def refresh_calman_theme(self: "PQAutomationApp") -> None:
|
||||
)
|
||||
|
||||
_refresh_metric_table(self)
|
||||
_refresh_calman_config_summary(self)
|
||||
_redraw_calman_charts(self)
|
||||
|
||||
|
||||
|
||||
@@ -831,6 +831,18 @@ def _on_toggle_theme(self: "PQAutomationApp") -> None:
|
||||
self.update_sidebar_selection()
|
||||
except Exception:
|
||||
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"):
|
||||
|
||||
Reference in New Issue
Block a user