添加Pantone 认证摸底测试面板UI

This commit is contained in:
xinzhu.yin
2026-05-07 11:21:17 +08:00
parent 7bc8fd8557
commit 3c519dde20
4 changed files with 1100 additions and 1 deletions

View File

@@ -435,6 +435,24 @@ def create_test_type_frame(self):
)
self.ai_image_btn.pack(fill=tk.X, padx=0, pady=1)
self.single_step_btn = ttk.Button(
self.sidebar_frame,
text="单步调试",
style="Sidebar.TButton",
command=self.toggle_single_step_panel,
takefocus=False,
)
self.single_step_btn.pack(fill=tk.X, padx=0, pady=1)
self.pantone_baseline_btn = ttk.Button(
self.sidebar_frame,
text="Pantone认证摸底测试",
style="Sidebar.TButton",
command=self.toggle_pantone_baseline_panel,
takefocus=False,
)
self.pantone_baseline_btn.pack(fill=tk.X, padx=0, pady=1)
# 注册面板按钮
if hasattr(self, "panels"):
if "log" in self.panels:
@@ -443,6 +461,10 @@ def create_test_type_frame(self):
self.panels["local_dimming"]["button"] = self.local_dimming_btn
if "ai_image" in self.panels:
self.panels["ai_image"]["button"] = self.ai_image_btn
if "single_step" in self.panels:
self.panels["single_step"]["button"] = self.single_step_btn
if "pantone_baseline" in self.panels:
self.panels["pantone_baseline"]["button"] = self.pantone_baseline_btn
def update_config_info_display(self):

View File

@@ -0,0 +1,506 @@
"""Pantone 认证摸底测试面板。"""
from __future__ import annotations
import csv
import datetime
import os
import tempfile
import threading
import tkinter as tk
from tkinter import filedialog, messagebox
import ttkbootstrap as ttk
from PIL import Image
from drivers.ucd_helpers import get_current_resolution, send_image_pattern
_PATTERN_FILE = "pantone_patterns_2670.csv"
_TEMPLATE_FILE = "pantone\xa02670\xa0colors.xlsx"
_TARGET_RESULT_COUNT = 2760
def create_pantone_baseline_panel(self):
"""创建 Pantone 认证摸底测试面板。"""
frame = ttk.Frame(self.content_frame)
self.pantone_baseline_frame = frame
self.pantone_baseline_visible = False
self.pantone_patterns = []
self.pantone_results = []
self._pantone_control_event = None
self._pantone_running = False
self._pantone_paused = False
self._pantone_pause_requested = False
self._pantone_stop_requested = False
self._pantone_next_index = 0
root = ttk.Frame(frame, padding=10)
root.pack(fill=tk.BOTH, expand=True)
title_row = ttk.Frame(root)
title_row.pack(fill=tk.X, pady=(0, 8))
ttk.Label(
title_row,
text="Pantone认证摸底测试",
font=("微软雅黑", 14, "bold"),
).pack(side=tk.LEFT)
self.pantone_status_var = tk.StringVar(value="未开始")
self.pantone_progress_var = tk.StringVar(value="0 / 2670")
self.pantone_settle_var = tk.StringVar(value="0.35")
config_row = ttk.LabelFrame(root, text="任务配置", padding=8)
config_row.pack(fill=tk.X)
ttk.Label(config_row, text=f"Pattern来源: settings/{_PATTERN_FILE}").pack(
side=tk.LEFT
)
ttk.Label(config_row, text="稳定等待(s):").pack(side=tk.LEFT, padx=(14, 4))
ttk.Entry(config_row, textvariable=self.pantone_settle_var, width=8).pack(
side=tk.LEFT
)
ttk.Label(config_row, textvariable=self.pantone_progress_var).pack(
side=tk.RIGHT, padx=(8, 0)
)
ttk.Label(config_row, textvariable=self.pantone_status_var, foreground="#666").pack(
side=tk.RIGHT
)
btn_row = ttk.Frame(root)
btn_row.pack(fill=tk.X, pady=(8, 8))
self.pantone_start_btn = ttk.Button(
btn_row,
text="开始",
bootstyle="primary",
command=lambda: _start_pantone_baseline(self),
)
self.pantone_start_btn.pack(side=tk.LEFT, padx=(0, 6))
self.pantone_pause_btn = ttk.Button(
btn_row,
text="暂停",
bootstyle="warning-outline",
command=lambda: _pause_pantone_baseline(self),
state=tk.DISABLED,
)
self.pantone_pause_btn.pack(side=tk.LEFT, padx=(0, 6))
self.pantone_resume_btn = ttk.Button(
btn_row,
text="继续",
bootstyle="info-outline",
command=lambda: _resume_pantone_baseline(self),
state=tk.DISABLED,
)
self.pantone_resume_btn.pack(side=tk.LEFT, padx=(0, 6))
self.pantone_end_btn = ttk.Button(
btn_row,
text="结束",
bootstyle="danger-outline",
command=lambda: _end_pantone_baseline(self),
state=tk.DISABLED,
)
self.pantone_end_btn.pack(side=tk.LEFT, padx=(0, 6))
ttk.Button(
btn_row,
text="清空结果",
bootstyle="secondary-outline",
command=lambda: _clear_results(self),
).pack(side=tk.LEFT, padx=(0, 6))
ttk.Button(
btn_row,
text="另存为模板",
bootstyle="success",
command=lambda: _save_as_template(self),
).pack(side=tk.LEFT)
table_frame = ttk.LabelFrame(root, text="测试结果R,G,B,L,x,y", padding=8)
table_frame.pack(fill=tk.BOTH, expand=True)
columns = ("idx", "r", "g", "b", "l", "x", "y", "time")
self.pantone_tree = ttk.Treeview(
table_frame,
columns=columns,
show="headings",
height=18,
)
headings = {
"idx": "序号",
"r": "R",
"g": "G",
"b": "B",
"l": "L",
"x": "x",
"y": "y",
"time": "时间",
}
widths = {
"idx": 70,
"r": 70,
"g": 70,
"b": 70,
"l": 90,
"x": 90,
"y": 90,
"time": 110,
}
for col in columns:
self.pantone_tree.heading(col, text=headings[col])
self.pantone_tree.column(col, width=widths[col], anchor=tk.CENTER)
self.pantone_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.pantone_tree.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.pantone_tree.configure(yscrollcommand=scrollbar.set)
self.register_panel("pantone_baseline", frame, None, "pantone_baseline_visible")
_set_button_states(self)
def toggle_pantone_baseline_panel(self):
"""切换 Pantone 认证摸底测试面板。"""
self.show_panel("pantone_baseline")
def _load_patterns(self):
path = os.path.join("settings", _PATTERN_FILE)
if not os.path.isfile(path):
raise FileNotFoundError(f"未找到 pattern 文件: {path}")
patterns = []
with open(path, "r", encoding="utf-8-sig", newline="") as fp:
reader = csv.DictReader(fp)
for row in reader:
try:
r = int(row.get("R", "").strip())
g = int(row.get("G", "").strip())
b = int(row.get("B", "").strip())
except Exception:
continue
if min(r, g, b) < 0 or max(r, g, b) > 255:
continue
patterns.append((r, g, b))
if not patterns:
raise RuntimeError("pattern 文件为空或格式不正确,需包含列 R,G,B")
return patterns
def _build_temp_patch(self, rgb):
width, height = get_current_resolution(self.ucd)
temp_dir = os.path.join(tempfile.gettempdir(), "pq_pantone_baseline")
os.makedirs(temp_dir, exist_ok=True)
file_path = os.path.join(temp_dir, "pantone_current_patch.png")
Image.new("RGB", (width, height), rgb).save(file_path, format="PNG")
return file_path
def _start_pantone_baseline(self):
if self._pantone_running:
messagebox.showinfo("提示", "Pantone 任务正在执行")
return
if not getattr(self, "ucd", None) or not self.ucd.status:
messagebox.showwarning("警告", "请先连接 UCD323")
return
if not getattr(self, "ca", None):
messagebox.showwarning("警告", "请先连接 CA410")
return
try:
settle = float(self.pantone_settle_var.get().strip())
if settle < 0:
raise ValueError()
except Exception:
messagebox.showerror("参数错误", "稳定等待时间必须是非负数字")
return
try:
self.pantone_patterns = _load_patterns(self)
except Exception as exc:
messagebox.showerror("读取失败", str(exc))
return
if self.pantone_results:
if not messagebox.askyesno("确认开始", "开始将清空当前内存中的测试结果,是否继续?"):
return
self._pantone_running = True
self._pantone_paused = False
self._pantone_pause_requested = False
self._pantone_stop_requested = False
self._pantone_control_event = threading.Event()
self._pantone_next_index = 0
self.pantone_status_var.set("执行中")
self.pantone_progress_var.set(f"0 / {_TARGET_RESULT_COUNT}")
self.pantone_results = []
for item in self.pantone_tree.get_children():
self.pantone_tree.delete(item)
_set_button_states(self)
_launch_worker(self, start_index=0, settle=settle)
def _resume_pantone_baseline(self):
if self._pantone_running:
messagebox.showinfo("提示", "Pantone 任务正在执行")
return
if not self._pantone_paused:
messagebox.showinfo("提示", "当前没有可继续的暂停任务")
return
if self._pantone_next_index >= _TARGET_RESULT_COUNT:
messagebox.showinfo("提示", "任务已完成,无需继续")
return
if not getattr(self, "ucd", None) or not self.ucd.status:
messagebox.showwarning("警告", "请先连接 UCD323")
return
if not getattr(self, "ca", None):
messagebox.showwarning("警告", "请先连接 CA410")
return
try:
settle = float(self.pantone_settle_var.get().strip())
if settle < 0:
raise ValueError()
except Exception:
messagebox.showerror("参数错误", "稳定等待时间必须是非负数字")
return
try:
self.pantone_patterns = _load_patterns(self)
except Exception as exc:
messagebox.showerror("读取失败", str(exc))
return
self._pantone_running = True
self._pantone_paused = False
self._pantone_pause_requested = False
self._pantone_stop_requested = False
self._pantone_control_event = threading.Event()
self.pantone_status_var.set("执行中")
_set_button_states(self)
_launch_worker(self, start_index=self._pantone_next_index, settle=settle)
def _launch_worker(self, start_index, settle):
total = _TARGET_RESULT_COUNT
def worker():
end_state = "completed"
try:
src = self.pantone_patterns
src_count = len(src)
self._dispatch_ui(
self.log_gui.log,
f"Pantone 认证摸底启动: source={src_count}, target={total}, start={start_index + 1}",
"info",
)
for i in range(start_index, total):
if self._pantone_stop_requested:
end_state = "stopped"
break
if self._pantone_pause_requested:
end_state = "paused"
break
r, g, b = src[i % src_count]
image_path = _build_temp_patch(self, (r, g, b))
if not send_image_pattern(self.ucd, image_path):
raise RuntimeError(f"{i + 1} 组发送失败")
if settle > 0 and self._pantone_control_event is not None:
self._pantone_control_event.clear()
self._pantone_control_event.wait(timeout=settle)
if self._pantone_stop_requested:
end_state = "stopped"
break
if self._pantone_pause_requested:
end_state = "paused"
break
x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay()
if lv is None:
raise RuntimeError(f"{i + 1} 组 CA410 采集失败")
record = {
"idx": i + 1,
"r": r,
"g": g,
"b": b,
"l": float(lv),
"x": float(x),
"y": float(y),
"time": datetime.datetime.now().strftime("%H:%M:%S"),
}
self.pantone_results.append(record)
self._pantone_next_index = i + 1
self._dispatch_ui(_append_result_row, self, record, total)
if end_state == "paused":
self._pantone_paused = True
self._dispatch_ui(self.pantone_status_var.set, "已暂停")
self._dispatch_ui(
self.log_gui.log,
f"Pantone 任务已暂停,断点 {self._pantone_next_index} / {total}",
"warning",
)
elif end_state == "stopped":
self._pantone_paused = False
self._pantone_next_index = 0
self._dispatch_ui(self.pantone_status_var.set, "已结束")
self._dispatch_ui(
self.log_gui.log,
f"Pantone 任务已结束,保留 {len(self.pantone_results)} 条内存结果",
"warning",
)
else:
self._pantone_paused = False
self._pantone_next_index = total
self._dispatch_ui(self.pantone_status_var.set, "已完成")
self._dispatch_ui(
self.log_gui.log,
f"Pantone 任务完成,共 {len(self.pantone_results)} 条数据",
"success",
)
except Exception as exc:
self._pantone_paused = False
self._dispatch_ui(self.pantone_status_var.set, "执行失败")
self._dispatch_ui(self.log_gui.log, f"Pantone 任务失败: {exc}", "error")
self._dispatch_ui(messagebox.showerror, "执行失败", str(exc))
finally:
self._pantone_running = False
self._pantone_pause_requested = False
self._pantone_stop_requested = False
self._dispatch_ui(_set_button_states, self)
threading.Thread(target=worker, daemon=True).start()
def _append_result_row(self, record, total):
self.pantone_tree.insert(
"",
tk.END,
values=(
record["idx"],
record["r"],
record["g"],
record["b"],
f"{record['l']:.2f}",
f"{record['x']:.4f}",
f"{record['y']:.4f}",
record["time"],
),
)
self.pantone_progress_var.set(f"{record['idx']} / {total}")
# 每次插入都滚到末尾,便于观察采集进度。
children = self.pantone_tree.get_children()
if children:
self.pantone_tree.see(children[-1])
def _pause_pantone_baseline(self):
if not self._pantone_running:
messagebox.showinfo("提示", "当前没有运行中的任务")
return
self._pantone_pause_requested = True
self.pantone_status_var.set("暂停中...")
if self._pantone_control_event is not None:
self._pantone_control_event.set()
def _end_pantone_baseline(self):
if self._pantone_running:
self._pantone_stop_requested = True
self.pantone_status_var.set("结束中...")
if self._pantone_control_event is not None:
self._pantone_control_event.set()
return
# 非运行态下点击“结束”:清除断点,保留当前内存结果。
self._pantone_paused = False
self._pantone_next_index = 0
self.pantone_status_var.set("已结束")
_set_button_states(self)
def _clear_results(self):
if self._pantone_running:
messagebox.showinfo("提示", "任务执行中,无法清空")
return
self.pantone_results = []
self._pantone_paused = False
self._pantone_next_index = 0
for item in self.pantone_tree.get_children():
self.pantone_tree.delete(item)
self.pantone_progress_var.set(f"0 / {_TARGET_RESULT_COUNT}")
self.pantone_status_var.set("结果已清空")
_set_button_states(self)
def _set_button_states(self):
if self._pantone_running:
self.pantone_start_btn.configure(state=tk.DISABLED)
self.pantone_pause_btn.configure(state=tk.NORMAL)
self.pantone_resume_btn.configure(state=tk.DISABLED)
self.pantone_end_btn.configure(state=tk.NORMAL)
return
self.pantone_start_btn.configure(state=tk.NORMAL)
self.pantone_pause_btn.configure(state=tk.DISABLED)
self.pantone_end_btn.configure(state=tk.NORMAL if (self._pantone_paused or self.pantone_results) else tk.DISABLED)
can_resume = self._pantone_paused and self._pantone_next_index < _TARGET_RESULT_COUNT
self.pantone_resume_btn.configure(state=tk.NORMAL if can_resume else tk.DISABLED)
def _save_as_template(self):
if not self.pantone_results:
messagebox.showinfo("提示", "暂无可导出的结果")
return
default_name = "pantone 2670 colors.xlsx"
path = filedialog.asksaveasfilename(
title="另存为 Pantone 模板",
defaultextension=".xlsx",
initialfile=default_name,
filetypes=[("Excel 文件", "*.xlsx")],
)
if not path:
return
# 优先复制 settings 模板,再覆盖数据区;没有模板时自动创建同结构表。
template_path = os.path.join("settings", _TEMPLATE_FILE)
try:
from openpyxl import load_workbook, Workbook
if os.path.isfile(template_path):
wb = load_workbook(template_path)
ws = wb.active
else:
wb = Workbook()
ws = wb.active
ws.title = "Sheet1"
ws.cell(row=1, column=1, value="R")
ws.cell(row=1, column=2, value="G")
ws.cell(row=1, column=3, value="B")
ws.cell(row=1, column=4, value="L")
ws.cell(row=1, column=5, value="x")
ws.cell(row=1, column=6, value="y")
# 清空旧数据
max_row = max(ws.max_row, 2)
for row in range(2, max_row + 1):
for col in range(1, 7):
ws.cell(row=row, column=col, value=None)
for idx, item in enumerate(self.pantone_results, start=2):
ws.cell(row=idx, column=1, value=int(item["r"]))
ws.cell(row=idx, column=2, value=int(item["g"]))
ws.cell(row=idx, column=3, value=int(item["b"]))
ws.cell(row=idx, column=4, value=float(item["l"]))
ws.cell(row=idx, column=5, value=float(item["x"]))
ws.cell(row=idx, column=6, value=float(item["y"]))
wb.save(path)
self.log_gui.log(f"Pantone 模板已保存: {path}", level="success")
self.pantone_status_var.set(f"已保存: {os.path.basename(path)}")
except Exception as exc:
messagebox.showerror("保存失败", f"写入 xlsx 失败: {exc}")

View File

@@ -0,0 +1,551 @@
"""单步调试面板。"""
from __future__ import annotations
import csv
import datetime
import os
import tempfile
import threading
import tkinter as tk
from tkinter import filedialog, messagebox
import ttkbootstrap as ttk
from PIL import Image
from drivers.ucd_helpers import get_current_resolution, send_image_pattern
_DEFAULT_SAMPLES = [
{"name": "Red Sample", "hex": "#D22630", "x": "0.6400", "y": "0.3300"},
{"name": "Green Sample", "hex": "#00A651", "x": "0.3000", "y": "0.6000"},
{"name": "Blue Sample", "hex": "#21409A", "x": "0.1500", "y": "0.0600"},
{"name": "Orange Sample", "hex": "#FF6A13", "x": "0.5500", "y": "0.4000"},
{"name": "Violet Sample", "hex": "#6A0DAD", "x": "0.2700", "y": "0.1400"},
{"name": "Gray Sample", "hex": "#8A8D8F", "x": "0.3127", "y": "0.3290"},
]
def create_single_step_panel(self):
"""创建单步调试面板。"""
frame = ttk.Frame(self.content_frame)
self.single_step_frame = frame
self.single_step_visible = False
self.single_step_samples = []
self.single_step_results = []
self.single_step_current_index = None
self.single_step_current_image_path = None
root = ttk.Frame(frame, padding=10)
root.pack(fill=tk.BOTH, expand=True)
root.columnconfigure(0, weight=0)
root.columnconfigure(1, weight=1)
root.rowconfigure(1, weight=1)
title_row = ttk.Frame(root)
title_row.grid(row=0, column=0, columnspan=2, sticky=tk.EW, pady=(0, 10))
ttk.Label(
title_row,
text="单步调试",
font=("微软雅黑", 14, "bold"),
).pack(side=tk.LEFT)
ttk.Label(
title_row,
text="录入目标色块,发送纯色色块并采集 xyY / ΔE2000。",
foreground="#666",
).pack(side=tk.LEFT, padx=(12, 0))
left = ttk.LabelFrame(root, text="样本列表", padding=8)
left.grid(row=1, column=0, sticky=tk.NS, padx=(0, 10))
left.grid_propagate(False)
left.configure(width=340)
self.single_step_listbox = tk.Listbox(
left,
width=34,
activestyle="none",
font=("微软雅黑", 9),
highlightthickness=1,
highlightbackground="#d8d8d8",
highlightcolor="#4a90e2",
selectbackground="#2b6cb0",
selectforeground="#ffffff",
)
self.single_step_listbox.pack(fill=tk.BOTH, expand=True)
self.single_step_listbox.bind(
"<<ListboxSelect>>", lambda e: _on_sample_select(self)
)
list_btn_row = ttk.Frame(left)
list_btn_row.pack(fill=tk.X, pady=(8, 0))
ttk.Button(
list_btn_row,
text="导入 CSV",
bootstyle="secondary-outline",
command=lambda: _import_samples_csv(self),
).pack(side=tk.LEFT, padx=(0, 4))
ttk.Button(
list_btn_row,
text="载入示例",
bootstyle="secondary-outline",
command=lambda: _load_default_samples(self),
).pack(side=tk.LEFT, padx=(0, 4))
ttk.Button(
list_btn_row,
text="删除",
bootstyle="danger-outline",
command=lambda: _delete_current_sample(self),
).pack(side=tk.LEFT)
right = ttk.Frame(root)
right.grid(row=1, column=1, sticky=tk.NSEW)
form_frame = ttk.LabelFrame(right, text="样本配置", padding=8)
form_frame.pack(fill=tk.X)
for column in range(6):
form_frame.columnconfigure(column, weight=1 if column in (1, 3, 5) else 0)
self.single_step_name_var = tk.StringVar()
self.single_step_hex_var = tk.StringVar(value="#FFFFFF")
self.single_step_target_x_var = tk.StringVar()
self.single_step_target_y_var = tk.StringVar()
self.single_step_measured_x_var = tk.StringVar()
self.single_step_measured_y_var = tk.StringVar()
self.single_step_measured_lv_var = tk.StringVar()
self.single_step_status_var = tk.StringVar(value="未选择样本")
ttk.Label(form_frame, text="名称").grid(row=0, column=0, sticky=tk.W, pady=4)
ttk.Entry(form_frame, textvariable=self.single_step_name_var).grid(
row=0, column=1, sticky=tk.EW, padx=(0, 8), pady=4
)
ttk.Label(form_frame, text="HEX").grid(row=0, column=2, sticky=tk.W, pady=4)
ttk.Entry(form_frame, textvariable=self.single_step_hex_var, width=12).grid(
row=0, column=3, sticky=tk.EW, padx=(0, 8), pady=4
)
ttk.Label(form_frame, text="目标 x").grid(row=0, column=4, sticky=tk.W, pady=4)
ttk.Entry(form_frame, textvariable=self.single_step_target_x_var, width=10).grid(
row=0, column=5, sticky=tk.EW, pady=4
)
ttk.Label(form_frame, text="目标 y").grid(row=1, column=0, sticky=tk.W, pady=4)
ttk.Entry(form_frame, textvariable=self.single_step_target_y_var, width=10).grid(
row=1, column=1, sticky=tk.EW, padx=(0, 8), pady=4
)
ttk.Label(form_frame, text="实测 x").grid(row=1, column=2, sticky=tk.W, pady=4)
ttk.Entry(form_frame, textvariable=self.single_step_measured_x_var, width=10).grid(
row=1, column=3, sticky=tk.EW, padx=(0, 8), pady=4
)
ttk.Label(form_frame, text="实测 y").grid(row=1, column=4, sticky=tk.W, pady=4)
ttk.Entry(form_frame, textvariable=self.single_step_measured_y_var, width=10).grid(
row=1, column=5, sticky=tk.EW, pady=4
)
ttk.Label(form_frame, text="实测 Lv").grid(row=2, column=0, sticky=tk.W, pady=4)
ttk.Entry(form_frame, textvariable=self.single_step_measured_lv_var, width=10).grid(
row=2, column=1, sticky=tk.EW, padx=(0, 8), pady=4
)
ttk.Label(
form_frame,
textvariable=self.single_step_status_var,
foreground="#666",
).grid(row=2, column=2, columnspan=4, sticky=tk.W, pady=4)
action_row = ttk.Frame(form_frame)
action_row.grid(row=3, column=0, columnspan=6, sticky=tk.EW, pady=(6, 0))
ttk.Button(
action_row,
text="新增 / 更新样本",
bootstyle="primary",
command=lambda: _upsert_sample(self),
).pack(side=tk.LEFT, padx=(0, 6))
ttk.Button(
action_row,
text="发送当前色块",
bootstyle="info-outline",
command=lambda: _send_current_patch(self),
).pack(side=tk.LEFT, padx=(0, 6))
ttk.Button(
action_row,
text="CA410 采集",
bootstyle="success-outline",
command=lambda: _measure_current_sample(self),
).pack(side=tk.LEFT, padx=(0, 6))
ttk.Button(
action_row,
text="记录结果",
bootstyle="warning-outline",
command=lambda: _commit_result(self),
).pack(side=tk.LEFT)
result_frame = ttk.LabelFrame(right, text="调试结果", padding=8)
result_frame.pack(fill=tk.BOTH, expand=True, pady=(10, 0))
columns = (
"name",
"hex",
"target_x",
"target_y",
"measured_x",
"measured_y",
"lv",
"delta_e",
"time",
)
self.single_step_result_tree = ttk.Treeview(
result_frame,
columns=columns,
show="headings",
height=12,
)
headings = {
"name": "名称",
"hex": "HEX",
"target_x": "目标 x",
"target_y": "目标 y",
"measured_x": "实测 x",
"measured_y": "实测 y",
"lv": "Lv",
"delta_e": "ΔE2000",
"time": "时间",
}
widths = {
"name": 130,
"hex": 90,
"target_x": 80,
"target_y": 80,
"measured_x": 80,
"measured_y": 80,
"lv": 80,
"delta_e": 90,
"time": 120,
}
for column in columns:
self.single_step_result_tree.heading(column, text=headings[column])
self.single_step_result_tree.column(
column, width=widths[column], anchor=tk.CENTER
)
self.single_step_result_tree.pack(fill=tk.BOTH, expand=True)
bottom_row = ttk.Frame(right)
bottom_row.pack(fill=tk.X, pady=(8, 0))
ttk.Button(
bottom_row,
text="导出结果 CSV",
bootstyle="success",
command=lambda: _export_results_csv(self),
).pack(side=tk.LEFT, padx=(0, 6))
ttk.Button(
bottom_row,
text="清空结果",
bootstyle="danger-outline",
command=lambda: _clear_results(self),
).pack(side=tk.LEFT)
self.register_panel("single_step", frame, None, "single_step_visible")
_load_default_samples(self)
def toggle_single_step_panel(self):
"""切换单步调试面板。"""
self.show_panel("single_step")
def _load_default_samples(self):
self.single_step_samples = [dict(item) for item in _DEFAULT_SAMPLES]
_refresh_sample_list(self, select_index=0 if self.single_step_samples else None)
self.single_step_status_var.set(
f"已载入 {len(self.single_step_samples)} 个示例样本"
)
def _refresh_sample_list(self, select_index=None):
self.single_step_listbox.delete(0, tk.END)
for sample in self.single_step_samples:
self.single_step_listbox.insert(
tk.END,
f"{sample['name']} {sample['hex']} ({sample['x']}, {sample['y']})",
)
if select_index is not None and 0 <= select_index < len(self.single_step_samples):
self.single_step_listbox.selection_clear(0, tk.END)
self.single_step_listbox.selection_set(select_index)
self.single_step_listbox.activate(select_index)
_select_sample(self, select_index)
elif not self.single_step_samples:
self.single_step_current_index = None
self.single_step_name_var.set("")
self.single_step_hex_var.set("#FFFFFF")
self.single_step_target_x_var.set("")
self.single_step_target_y_var.set("")
self.single_step_status_var.set("样本列表为空")
def _on_sample_select(self):
selection = self.single_step_listbox.curselection()
if not selection:
return
_select_sample(self, selection[0])
def _select_sample(self, index):
sample = self.single_step_samples[index]
self.single_step_current_index = index
self.single_step_name_var.set(sample["name"])
self.single_step_hex_var.set(sample["hex"])
self.single_step_target_x_var.set(sample["x"])
self.single_step_target_y_var.set(sample["y"])
self.single_step_status_var.set(f"当前样本: {sample['name']}")
def _import_samples_csv(self):
path = filedialog.askopenfilename(
title="选择单步调试样本 CSV",
filetypes=[("CSV 文件", "*.csv"), ("所有文件", "*.*")],
)
if not path:
return
samples = []
try:
with open(path, "r", encoding="utf-8-sig", newline="") as fp:
reader = csv.DictReader(fp)
for row in reader:
name = (row.get("name") or row.get("sample") or "").strip()
hex_value = (row.get("hex") or row.get("rgb") or "").strip()
target_x = (row.get("x") or "").strip()
target_y = (row.get("y") or "").strip()
if not name or not hex_value or not target_x or not target_y:
continue
samples.append(
{
"name": name,
"hex": _normalize_hex(hex_value),
"x": target_x,
"y": target_y,
}
)
except Exception as exc:
messagebox.showerror("导入失败", f"读取 CSV 失败: {exc}")
return
if not samples:
messagebox.showwarning("导入失败", "CSV 中没有有效样本,要求列包含 name/hex/x/y")
return
self.single_step_samples = samples
_refresh_sample_list(self, select_index=0)
self.log_gui.log(f"单步调试样本已导入: {len(samples)}", level="success")
def _delete_current_sample(self):
if self.single_step_current_index is None:
return
removed = self.single_step_samples.pop(self.single_step_current_index)
next_index = min(self.single_step_current_index, len(self.single_step_samples) - 1)
_refresh_sample_list(self, select_index=next_index if next_index >= 0 else None)
self.single_step_status_var.set(f"已删除样本: {removed['name']}")
def _upsert_sample(self):
try:
sample = {
"name": self.single_step_name_var.get().strip(),
"hex": _normalize_hex(self.single_step_hex_var.get()),
"x": _format_float(self.single_step_target_x_var.get()),
"y": _format_float(self.single_step_target_y_var.get()),
}
except ValueError as exc:
messagebox.showerror("参数错误", str(exc))
return
if not sample["name"]:
messagebox.showwarning("提示", "请输入样本名称")
return
if self.single_step_current_index is None:
self.single_step_samples.append(sample)
select_index = len(self.single_step_samples) - 1
self.single_step_status_var.set(f"已新增样本: {sample['name']}")
else:
self.single_step_samples[self.single_step_current_index] = sample
select_index = self.single_step_current_index
self.single_step_status_var.set(f"已更新样本: {sample['name']}")
_refresh_sample_list(self, select_index=select_index)
def _normalize_hex(value):
text = (value or "").strip().upper()
if not text:
raise ValueError("HEX 不能为空")
if not text.startswith("#"):
text = "#" + text
if len(text) != 7 or any(ch not in "#0123456789ABCDEF" for ch in text):
raise ValueError("HEX 格式应为 #RRGGBB")
return text
def _format_float(value):
try:
number = float(str(value).strip())
except Exception as exc:
raise ValueError("xy 值必须是数字") from exc
return f"{number:.4f}"
def _build_color_patch(self, hex_value):
if not getattr(self, "ucd", None) or not self.ucd.status:
raise RuntimeError("请先连接 UCD323 设备")
width, height = get_current_resolution(self.ucd)
rgb = tuple(int(hex_value[i:i + 2], 16) for i in (1, 3, 5))
temp_dir = os.path.join(tempfile.gettempdir(), "pq_single_step_patches")
os.makedirs(temp_dir, exist_ok=True)
file_path = os.path.join(
temp_dir, f"single_step_{hex_value[1:]}_{width}x{height}.png"
)
Image.new("RGB", (width, height), rgb).save(file_path, format="PNG")
return file_path
def _send_current_patch(self):
if self.single_step_current_index is None:
messagebox.showinfo("提示", "请先选择一个样本")
return
sample = self.single_step_samples[self.single_step_current_index]
def worker():
try:
image_path = _build_color_patch(self, sample["hex"])
ok = send_image_pattern(self.ucd, image_path)
if not ok:
raise RuntimeError("UCD323 发送失败")
self.single_step_current_image_path = image_path
self._dispatch_ui(
self.single_step_status_var.set,
f"已发送色块: {sample['name']} {sample['hex']}",
)
self._dispatch_ui(
self.log_gui.log,
f"单步调试色块已发送: {sample['name']} {sample['hex']}",
"success",
)
except Exception as exc:
self._dispatch_ui(self.single_step_status_var.set, f"发送失败: {exc}")
self._dispatch_ui(self.log_gui.log, f"单步调试色块发送失败: {exc}", "error")
threading.Thread(target=worker, daemon=True).start()
def _measure_current_sample(self):
if self.single_step_current_index is None:
messagebox.showinfo("提示", "请先选择一个样本")
return
if not getattr(self, "ca", None):
messagebox.showwarning("警告", "请先连接 CA410 色度计")
return
def worker():
try:
x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay()
if lv is None:
raise RuntimeError("CA410 未返回有效亮度")
self._dispatch_ui(self.single_step_measured_x_var.set, f"{x:.4f}")
self._dispatch_ui(self.single_step_measured_y_var.set, f"{y:.4f}")
self._dispatch_ui(self.single_step_measured_lv_var.set, f"{lv:.2f}")
self._dispatch_ui(self.single_step_status_var.set, "采集完成,可记录结果")
self._dispatch_ui(
self.log_gui.log,
f"单步调试采集完成: x={x:.4f}, y={y:.4f}, Lv={lv:.2f}",
"success",
)
except Exception as exc:
self._dispatch_ui(self.single_step_status_var.set, f"采集失败: {exc}")
self._dispatch_ui(self.log_gui.log, f"单步调试采集失败: {exc}", "error")
threading.Thread(target=worker, daemon=True).start()
def _commit_result(self):
if self.single_step_current_index is None:
messagebox.showinfo("提示", "请先选择一个样本")
return
sample = self.single_step_samples[self.single_step_current_index]
try:
measured_x = float(self.single_step_measured_x_var.get().strip())
measured_y = float(self.single_step_measured_y_var.get().strip())
measured_lv = float(self.single_step_measured_lv_var.get().strip())
target_x = float(sample["x"])
target_y = float(sample["y"])
delta_e = self.calculate_delta_e_2000(
measured_x, measured_y, measured_lv, target_x, target_y
)
except Exception as exc:
messagebox.showerror("记录失败", f"请先准备完整的目标值与实测值: {exc}")
return
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
record = {
"name": sample["name"],
"hex": sample["hex"],
"target_x": f"{target_x:.4f}",
"target_y": f"{target_y:.4f}",
"measured_x": f"{measured_x:.4f}",
"measured_y": f"{measured_y:.4f}",
"lv": f"{measured_lv:.2f}",
"delta_e": f"{delta_e:.3f}",
"time": timestamp,
}
self.single_step_results.append(record)
self.single_step_result_tree.insert(
"",
tk.END,
values=tuple(
record[key]
for key in (
"name",
"hex",
"target_x",
"target_y",
"measured_x",
"measured_y",
"lv",
"delta_e",
"time",
)
),
)
self.single_step_status_var.set(f"已记录结果ΔE2000={record['delta_e']}")
def _clear_results(self):
self.single_step_results = []
for item in self.single_step_result_tree.get_children():
self.single_step_result_tree.delete(item)
self.single_step_status_var.set("结果已清空")
def _export_results_csv(self):
if not self.single_step_results:
messagebox.showinfo("提示", "暂无可导出的调试结果")
return
path = filedialog.asksaveasfilename(
title="导出单步调试结果",
defaultextension=".csv",
initialfile=f"single_step_debug_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
filetypes=[("CSV 文件", "*.csv")],
)
if not path:
return
fieldnames = [
"name",
"hex",
"target_x",
"target_y",
"measured_x",
"measured_y",
"lv",
"delta_e",
"time",
]
try:
with open(path, "w", encoding="utf-8-sig", newline="") as fp:
writer = csv.DictWriter(fp, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(self.single_step_results)
self.log_gui.log(f"单步调试结果已导出: {path}", level="success")
self.single_step_status_var.set(f"结果已导出: {os.path.basename(path)}")
except Exception as exc:
messagebox.showerror("导出失败", f"写入 CSV 失败: {exc}")