2026-05-07 11:21:17 +08:00
|
|
|
|
"""单步调试面板。"""
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
2026-05-24 11:21:30 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-07 11:21:17 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_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):
|
2026-05-24 11:21:30 +08:00
|
|
|
|
if not self.signal_service.is_connected:
|
2026-05-07 11:21:17 +08:00
|
|
|
|
raise RuntimeError("请先连接 UCD323 设备")
|
2026-05-24 11:21:30 +08:00
|
|
|
|
width, height = self.signal_service.current_resolution()
|
2026-05-07 11:21:17 +08:00
|
|
|
|
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"])
|
2026-05-24 11:02:37 +08:00
|
|
|
|
self.signal_service.send_image(image_path)
|
2026-05-07 11:21:17 +08:00
|
|
|
|
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}")
|