添加Pantone 认证摸底测试面板UI
This commit is contained in:
@@ -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):
|
||||
|
||||
506
app/views/panels/pantone_baseline_panel.py
Normal file
506
app/views/panels/pantone_baseline_panel.py
Normal 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}")
|
||||
551
app/views/panels/single_step_panel.py
Normal file
551
app/views/panels/single_step_panel.py
Normal 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}")
|
||||
Reference in New Issue
Block a user