1079 lines
36 KiB
Python
1079 lines
36 KiB
Python
"""图表框架相关逻辑(Step 3 重构)。
|
||
|
||
从 pqAutomationApp.PQAutomationApp 中搬迁而来。每个函数第一行 `self = app`
|
||
以保留原有 `self.xxx` 属性访问不变。
|
||
"""
|
||
|
||
import tkinter as tk
|
||
import ttkbootstrap as ttk
|
||
import matplotlib.pyplot as plt
|
||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||
from app.views.pq_debug_panel import PQDebugPanel
|
||
|
||
from typing import TYPE_CHECKING
|
||
|
||
if TYPE_CHECKING:
|
||
from pqAutomationApp import PQAutomationApp
|
||
|
||
|
||
def _result_bg_color() -> str:
|
||
"""根据当前主题返回结果图背景色。"""
|
||
try:
|
||
from app.views.theme_manager import is_dark
|
||
return "#1B1F24" if is_dark() else "#FFFFFF"
|
||
except Exception:
|
||
return "#FFFFFF"
|
||
|
||
|
||
def apply_result_chart_theme(self: "PQAutomationApp"):
|
||
"""统一刷新结果图画布背景,使其跟随浅/深色主题。"""
|
||
bg = _result_bg_color()
|
||
|
||
chart_pairs = [
|
||
("gamut_fig", "gamut_canvas"),
|
||
("gamma_fig", "gamma_canvas"),
|
||
("eotf_fig", "eotf_canvas"),
|
||
("cct_fig", "cct_canvas"),
|
||
("contrast_fig", "contrast_canvas"),
|
||
("accuracy_fig", "accuracy_canvas"),
|
||
]
|
||
|
||
for fig_attr, canvas_attr in chart_pairs:
|
||
fig = getattr(self, fig_attr, None)
|
||
canvas = getattr(self, canvas_attr, None)
|
||
if fig is not None:
|
||
fig.patch.set_facecolor(bg)
|
||
if canvas is not None:
|
||
try:
|
||
widget = canvas.get_tk_widget()
|
||
widget.configure(bg=bg, highlightthickness=0)
|
||
except Exception:
|
||
pass
|
||
try:
|
||
canvas.draw_idle()
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def init_gamut_chart(self: "PQAutomationApp"):
|
||
"""初始化色域图表 - 手动设置subplot位置,完全避免重叠"""
|
||
container = ttk.Frame(self.gamut_chart_frame)
|
||
container.pack(expand=True, fill=tk.BOTH)
|
||
|
||
# ---- 参考色域切换工具栏 ----
|
||
toolbar = ttk.Frame(container)
|
||
toolbar.pack(fill=tk.X, padx=8, pady=(4, 2))
|
||
|
||
ttk.Label(toolbar, text="参考色域标准:").pack(side=tk.LEFT, padx=(0, 8))
|
||
|
||
self._gamut_ref_toolbar_var = tk.StringVar(value="DCI-P3")
|
||
for std in ["BT.709", "DCI-P3", "BT.2020", "BT.601"]:
|
||
rb = ttk.Radiobutton(
|
||
toolbar, text=std,
|
||
variable=self._gamut_ref_toolbar_var,
|
||
value=std,
|
||
bootstyle="toolbutton",
|
||
command=lambda s=std: self._on_gamut_toolbar_changed(s),
|
||
)
|
||
rb.pack(side=tk.LEFT, padx=2)
|
||
|
||
# ---- matplotlib 图表 ----
|
||
self.gamut_fig = plt.Figure(figsize=(14, 6), dpi=100)
|
||
self.gamut_canvas = FigureCanvasTkAgg(self.gamut_fig, master=container)
|
||
|
||
canvas_widget = self.gamut_canvas.get_tk_widget()
|
||
canvas_widget.pack(expand=True, fill=tk.BOTH)
|
||
|
||
# 恢复原来的大尺寸:0.84 高度
|
||
self.gamut_ax_xy = self.gamut_fig.add_axes(
|
||
[0.02, 0.08, 0.46, 0.84]
|
||
) # ← 改回 0.84
|
||
self.gamut_ax_uv = self.gamut_fig.add_axes(
|
||
[0.52, 0.08, 0.46, 0.84]
|
||
) # ← 改回 0.84
|
||
|
||
# 初始化 XY 图(占位坐标系,真实绘制时由 plot_gamut 设置 CIE 1931 范围)
|
||
self.gamut_ax_xy.set_xlim(0.0, 0.8)
|
||
self.gamut_ax_xy.set_ylim(0.0, 0.9)
|
||
self.gamut_ax_xy.set_aspect("equal", adjustable="datalim")
|
||
self.gamut_ax_xy.set_clip_on(False)
|
||
|
||
# 初始化 UV 图(占位坐标系,真实绘制时由 plot_gamut 设置 CIE 1976 范围)
|
||
self.gamut_ax_uv.set_xlim(0.0, 0.65)
|
||
self.gamut_ax_uv.set_ylim(0.0, 0.6)
|
||
self.gamut_ax_uv.set_aspect("equal", adjustable="datalim")
|
||
self.gamut_ax_uv.set_clip_on(False)
|
||
|
||
# 调整标题位置:y=0.98
|
||
self.gamut_fig.suptitle("色域测试", fontsize=12, y=0.98)
|
||
|
||
self.gamut_canvas.draw()
|
||
|
||
|
||
def sync_gamut_toolbar(self: "PQAutomationApp"):
|
||
"""将工具栏参考标准按钮同步为当前测试类型的 ref var 值。"""
|
||
if not hasattr(self, "_gamut_ref_toolbar_var"):
|
||
return
|
||
test_type = getattr(self.config, "current_test_type", "screen_module")
|
||
var_map = {
|
||
"screen_module": "screen_gamut_ref_var",
|
||
"sdr_movie": "sdr_gamut_ref_var",
|
||
"hdr_movie": "hdr_gamut_ref_var",
|
||
}
|
||
attr = var_map.get(test_type)
|
||
if attr and hasattr(self, attr):
|
||
self._gamut_ref_toolbar_var.set(getattr(self, attr).get())
|
||
|
||
|
||
def _on_gamut_toolbar_changed(self: "PQAutomationApp", std):
|
||
"""用户点击工具栏参考标准按钮时:更新 var → 保存配置 → 重绘(有数据时)。"""
|
||
test_type = self.config.current_test_type
|
||
var_map = {
|
||
"screen_module": "screen_gamut_ref_var",
|
||
"sdr_movie": "sdr_gamut_ref_var",
|
||
"hdr_movie": "hdr_gamut_ref_var",
|
||
}
|
||
attr = var_map.get(test_type)
|
||
if attr and hasattr(self, attr):
|
||
getattr(self, attr).set(std)
|
||
|
||
# 保存到配置
|
||
if test_type not in self.config.current_test_types:
|
||
self.config.current_test_types[test_type] = {}
|
||
self.config.current_test_types[test_type]["gamut_reference"] = std
|
||
self.save_pq_config()
|
||
|
||
# 仅在有色域数据时才重新绘制,避免无数据时弹出警告框
|
||
if hasattr(self, "results") and self.results:
|
||
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
|
||
if rgb_data and len(rgb_data) >= 3:
|
||
self.recalculate_gamut()
|
||
|
||
|
||
def init_gamma_chart(self: "PQAutomationApp"):
|
||
"""初始化Gamma曲线图表 - 左侧曲线 + 右侧表格(4列 + 通用说明)"""
|
||
container = ttk.Frame(self.gamma_chart_frame)
|
||
container.pack(expand=True, fill=tk.BOTH)
|
||
|
||
self.gamma_fig = plt.Figure(figsize=(12, 6), dpi=100, constrained_layout=False)
|
||
self.gamma_canvas = FigureCanvasTkAgg(self.gamma_fig, master=container)
|
||
|
||
canvas_widget = self.gamma_canvas.get_tk_widget()
|
||
canvas_widget.pack(expand=True, fill=tk.BOTH)
|
||
|
||
# 左侧:Gamma 曲线
|
||
self.gamma_ax = self.gamma_fig.add_axes([0.08, 0.12, 0.50, 0.78])
|
||
self.gamma_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
||
self.gamma_ax.set_ylabel("L_bar", fontsize=10)
|
||
self.gamma_ax.set_xlim(0, 105)
|
||
self.gamma_ax.set_ylim(0, 1.1)
|
||
self.gamma_ax.grid(True, linestyle="--", alpha=0.3)
|
||
self.gamma_ax.tick_params(labelsize=9)
|
||
|
||
# 左侧提示(通用说明,不显示具体 Gamma 值)
|
||
self.gamma_ax.text(
|
||
0.5,
|
||
0.5,
|
||
"等待测试数据...\n\n"
|
||
"将显示:\n"
|
||
"• 实测曲线 (蓝色)\n"
|
||
"• 理想 Gamma 曲线 (红色)\n\n"
|
||
"Gamma 值由测试配置决定",
|
||
ha="center",
|
||
va="center",
|
||
fontsize=10,
|
||
color="gray",
|
||
transform=self.gamma_ax.transAxes,
|
||
bbox=dict(
|
||
boxstyle="round,pad=1", facecolor="white", edgecolor="gray", alpha=0.8
|
||
),
|
||
)
|
||
|
||
# 右侧:数据表格
|
||
self.gamma_table_ax = self.gamma_fig.add_axes([0.62, 0.12, 0.35, 0.78])
|
||
self.gamma_table_ax.axis("off")
|
||
|
||
# 4列表格数据
|
||
table_data = [
|
||
["灰阶", "实测亮度\n(cd/m²)", "L_bar\n(计算)", "Gamma"],
|
||
["0%", "--", "--", "--"],
|
||
["10%", "--", "--", "--"],
|
||
["20%", "--", "--", "--"],
|
||
["30%", "--", "--", "--"],
|
||
["40%", "--", "--", "--"],
|
||
["50%", "--", "--", "--"],
|
||
["60%", "--", "--", "--"],
|
||
["70%", "--", "--", "--"],
|
||
["80%", "--", "--", "--"],
|
||
["90%", "--", "--", "--"],
|
||
["100%", "--", "--", "--"],
|
||
]
|
||
|
||
table = self.gamma_table_ax.table(
|
||
cellText=table_data,
|
||
cellLoc="center",
|
||
loc="center",
|
||
colWidths=[0.18, 0.28, 0.27, 0.27], # ← 4列宽度
|
||
)
|
||
|
||
table.auto_set_font_size(False)
|
||
table.set_fontsize(7.5)
|
||
table.scale(1, 1.5)
|
||
|
||
# 表头样式
|
||
for i in range(4):
|
||
cell = table[(0, i)]
|
||
cell.set_facecolor("#4472C4")
|
||
cell.set_text_props(weight="bold", color="white", fontsize=7)
|
||
|
||
# 数据行交替颜色
|
||
for i in range(1, len(table_data)):
|
||
for j in range(4):
|
||
cell = table[(i, j)]
|
||
if i % 2 == 0:
|
||
cell.set_facecolor("#E7E6E6")
|
||
else:
|
||
cell.set_facecolor("#FFFFFF")
|
||
|
||
# 底部说明
|
||
self.gamma_table_ax.text(
|
||
0.5,
|
||
0.02,
|
||
"表格说明:\n"
|
||
"• 实测亮度: 色度计测量值 (cd/m²)\n"
|
||
"• L_bar: 归一化亮度 (0-1)\n"
|
||
"• Gamma: 实际 Gamma 值",
|
||
ha="center",
|
||
va="bottom",
|
||
fontsize=7,
|
||
color="gray",
|
||
transform=self.gamma_table_ax.transAxes,
|
||
bbox=dict(
|
||
boxstyle="round,pad=0.5",
|
||
facecolor="lightyellow",
|
||
edgecolor="gray",
|
||
alpha=0.8,
|
||
),
|
||
)
|
||
|
||
self.gamma_fig.suptitle("Gamma曲线 + 数据表格", fontsize=12, y=0.98)
|
||
self.gamma_canvas.draw()
|
||
|
||
def init_eotf_chart(self: "PQAutomationApp"):
|
||
"""初始化 EOTF 曲线图表(HDR 专用)- 左侧曲线 + 右侧表格(4列)"""
|
||
container = ttk.Frame(self.eotf_chart_frame)
|
||
container.pack(expand=True, fill=tk.BOTH)
|
||
|
||
self.eotf_fig = plt.Figure(figsize=(12, 6), dpi=100, constrained_layout=False)
|
||
self.eotf_canvas = FigureCanvasTkAgg(self.eotf_fig, master=container)
|
||
|
||
canvas_widget = self.eotf_canvas.get_tk_widget()
|
||
canvas_widget.pack(expand=True, fill=tk.BOTH)
|
||
|
||
# 左侧:EOTF 曲线
|
||
self.eotf_ax = self.eotf_fig.add_axes([0.08, 0.12, 0.50, 0.78])
|
||
self.eotf_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
||
self.eotf_ax.set_ylabel("L_bar (归一化亮度)", fontsize=10)
|
||
self.eotf_ax.set_xlim(0, 105)
|
||
self.eotf_ax.set_ylim(0, 1.1)
|
||
self.eotf_ax.grid(True, linestyle="--", alpha=0.3)
|
||
self.eotf_ax.tick_params(labelsize=9)
|
||
|
||
# 左侧提示
|
||
self.eotf_ax.text(
|
||
0.5,
|
||
0.5,
|
||
"等待测试数据...\n\n将显示:\n• 实测 EOTF 曲线 (蓝色)\n• 理想 PQ 曲线 (红色)",
|
||
ha="center",
|
||
va="center",
|
||
fontsize=11,
|
||
color="gray",
|
||
transform=self.eotf_ax.transAxes,
|
||
bbox=dict(
|
||
boxstyle="round,pad=1", facecolor="white", edgecolor="gray", alpha=0.8
|
||
),
|
||
)
|
||
|
||
# 右侧:数据表格
|
||
self.eotf_table_ax = self.eotf_fig.add_axes([0.62, 0.12, 0.35, 0.78])
|
||
self.eotf_table_ax.axis("off")
|
||
|
||
# 4列表格数据
|
||
table_data = [
|
||
["灰阶", "实测亮度\n(cd/m²)", "L_bar\n(计算)", "EOTF γ"],
|
||
["0%", "--", "--", "--"],
|
||
["10%", "--", "--", "--"],
|
||
["20%", "--", "--", "--"],
|
||
["30%", "--", "--", "--"],
|
||
["40%", "--", "--", "--"],
|
||
["50%", "--", "--", "--"],
|
||
["60%", "--", "--", "--"],
|
||
["70%", "--", "--", "--"],
|
||
["80%", "--", "--", "--"],
|
||
["90%", "--", "--", "--"],
|
||
["100%", "--", "--", "--"],
|
||
]
|
||
|
||
table = self.eotf_table_ax.table(
|
||
cellText=table_data,
|
||
cellLoc="center",
|
||
loc="center",
|
||
colWidths=[0.18, 0.28, 0.27, 0.27],
|
||
)
|
||
|
||
table.auto_set_font_size(False)
|
||
table.set_fontsize(7.5)
|
||
table.scale(1, 1.5)
|
||
|
||
# 表头样式
|
||
for i in range(4):
|
||
cell = table[(0, i)]
|
||
cell.set_facecolor("#4472C4")
|
||
cell.set_text_props(weight="bold", color="white", fontsize=7)
|
||
|
||
# 数据行交替颜色
|
||
for i in range(1, len(table_data)):
|
||
for j in range(4):
|
||
cell = table[(i, j)]
|
||
if i % 2 == 0:
|
||
cell.set_facecolor("#E7E6E6")
|
||
else:
|
||
cell.set_facecolor("#FFFFFF")
|
||
|
||
# 底部说明
|
||
self.eotf_table_ax.text(
|
||
0.5,
|
||
0.02,
|
||
"表格说明:\n"
|
||
"• 实测亮度: 色度计测量值 (cd/m²)\n"
|
||
"• L_bar: 归一化亮度 (0-1)\n"
|
||
"• EOTF γ: HDR 实际 Gamma 值",
|
||
ha="center",
|
||
va="bottom",
|
||
fontsize=7,
|
||
color="gray",
|
||
transform=self.eotf_table_ax.transAxes,
|
||
bbox=dict(
|
||
boxstyle="round,pad=0.5",
|
||
facecolor="lightyellow",
|
||
edgecolor="gray",
|
||
alpha=0.8,
|
||
),
|
||
)
|
||
|
||
self.eotf_fig.suptitle("EOTF 曲线 + 数据表格", fontsize=12, y=0.98)
|
||
self.eotf_canvas.draw()
|
||
|
||
def init_cct_chart(self: "PQAutomationApp"):
|
||
"""初始化色度坐标图表 - 正向横坐标,标题居中最上方"""
|
||
container = ttk.Frame(self.cct_chart_frame)
|
||
container.pack(expand=True)
|
||
|
||
self.cct_fig = plt.Figure(figsize=(8, 6), dpi=100, tight_layout=False)
|
||
self.cct_canvas = FigureCanvasTkAgg(self.cct_fig, master=container)
|
||
|
||
canvas_widget = self.cct_canvas.get_tk_widget()
|
||
canvas_widget.pack()
|
||
canvas_widget.config(width=800, height=600)
|
||
canvas_widget.pack_propagate(False)
|
||
|
||
self.cct_ax1 = self.cct_fig.add_subplot(211)
|
||
self.cct_ax2 = self.cct_fig.add_subplot(212)
|
||
|
||
# 上图:x coordinates
|
||
self.cct_ax1.set_xlabel("灰阶 (%)", fontsize=9)
|
||
self.cct_ax1.set_ylabel("CIE x", fontsize=9)
|
||
self.cct_ax1.set_xlim(0, 105)
|
||
self.cct_ax1.set_ylim(0.25, 0.35)
|
||
self.cct_ax1.grid(True, linestyle="--", alpha=0.3)
|
||
self.cct_ax1.tick_params(labelsize=8)
|
||
|
||
# 下图:y coordinates
|
||
self.cct_ax2.set_xlabel("灰阶 (%)", fontsize=9)
|
||
self.cct_ax2.set_ylabel("CIE y", fontsize=9)
|
||
self.cct_ax2.set_xlim(0, 105)
|
||
self.cct_ax2.set_ylim(0.25, 0.35)
|
||
self.cct_ax2.grid(True, linestyle="--", alpha=0.3)
|
||
self.cct_ax2.tick_params(labelsize=8)
|
||
|
||
# 调整标题位置:y=0.985(比色域/Gamma略高)
|
||
self.cct_fig.suptitle("色度一致性测试", fontsize=12, y=0.985)
|
||
|
||
self.cct_fig.subplots_adjust(
|
||
left=0.12,
|
||
right=0.88,
|
||
top=0.90,
|
||
bottom=0.08,
|
||
hspace=0.25,
|
||
)
|
||
|
||
self.cct_canvas.draw()
|
||
|
||
def init_contrast_chart(self: "PQAutomationApp"):
|
||
"""初始化对比度图表 - 固定大小,居中显示"""
|
||
container = ttk.Frame(self.contrast_chart_frame)
|
||
container.pack(expand=True)
|
||
|
||
self.contrast_fig = plt.Figure(
|
||
figsize=(6, 6),
|
||
dpi=100,
|
||
tight_layout=False,
|
||
)
|
||
self.contrast_canvas = FigureCanvasTkAgg(self.contrast_fig, master=container)
|
||
|
||
canvas_widget = self.contrast_canvas.get_tk_widget()
|
||
canvas_widget.pack()
|
||
|
||
canvas_widget.config(width=600, height=600)
|
||
canvas_widget.pack_propagate(False)
|
||
|
||
self.contrast_ax = self.contrast_fig.add_subplot(111)
|
||
self.contrast_ax.set_xlim(0, 1)
|
||
self.contrast_ax.set_ylim(0, 1)
|
||
self.contrast_ax.axis("off")
|
||
|
||
# 调整标题位置:y=0.985
|
||
self.contrast_fig.suptitle("对比度测试", fontsize=12, y=0.985)
|
||
|
||
self.contrast_fig.subplots_adjust(
|
||
left=0.02,
|
||
right=0.98,
|
||
top=0.90,
|
||
bottom=0.02,
|
||
)
|
||
|
||
self.contrast_canvas.draw()
|
||
|
||
def init_accuracy_chart(self: "PQAutomationApp"):
|
||
"""初始化色准图表 - 固定大小,居中显示"""
|
||
container = ttk.Frame(self.accuracy_chart_frame)
|
||
container.pack(expand=True, fill=tk.BOTH)
|
||
container.grid_rowconfigure(0, weight=1)
|
||
container.grid_rowconfigure(1, weight=0, minsize=220)
|
||
container.grid_columnconfigure(0, weight=1)
|
||
|
||
# 上方图表优先显示;下方表格固定高度,避免挤占图表区域。
|
||
plot_container = ttk.Frame(container)
|
||
plot_container.grid(row=0, column=0, sticky="nsew")
|
||
|
||
table_container = ttk.LabelFrame(container, text="色准明细")
|
||
table_container.grid(row=1, column=0, sticky="ew", padx=4, pady=(2, 4))
|
||
|
||
self.accuracy_fig = plt.Figure(
|
||
figsize=(10, 6),
|
||
dpi=100,
|
||
tight_layout=False,
|
||
)
|
||
self.accuracy_canvas = FigureCanvasTkAgg(self.accuracy_fig, master=plot_container)
|
||
|
||
canvas_widget = self.accuracy_canvas.get_tk_widget()
|
||
canvas_widget.pack(fill=tk.BOTH, expand=True)
|
||
|
||
self.accuracy_ax = self.accuracy_fig.add_subplot(111)
|
||
self.accuracy_ax.set_xlim(0, 1)
|
||
self.accuracy_ax.set_ylim(0, 1)
|
||
self.accuracy_ax.axis("off")
|
||
|
||
# 调整标题位置
|
||
self.accuracy_fig.suptitle("色准测试", fontsize=12, y=0.985)
|
||
|
||
self.accuracy_fig.subplots_adjust(
|
||
left=0.05,
|
||
right=0.95,
|
||
top=0.90,
|
||
bottom=0.05,
|
||
)
|
||
|
||
self.accuracy_canvas.draw()
|
||
self._init_accuracy_result_table(table_container)
|
||
|
||
|
||
def _init_accuracy_result_table(self: "PQAutomationApp", parent):
|
||
"""创建色准结果表格(支持横向/纵向滚动)。"""
|
||
table_wrap = ttk.Frame(parent)
|
||
table_wrap.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)
|
||
|
||
self.accuracy_result_table = ttk.Treeview(
|
||
table_wrap,
|
||
show="headings",
|
||
height=7,
|
||
)
|
||
|
||
x_scroll = ttk.Scrollbar(
|
||
table_wrap,
|
||
orient=tk.HORIZONTAL,
|
||
command=self.accuracy_result_table.xview,
|
||
)
|
||
y_scroll = ttk.Scrollbar(
|
||
table_wrap,
|
||
orient=tk.VERTICAL,
|
||
command=self.accuracy_result_table.yview,
|
||
)
|
||
self.accuracy_result_table.configure(
|
||
xscrollcommand=x_scroll.set,
|
||
yscrollcommand=y_scroll.set,
|
||
)
|
||
|
||
self.accuracy_result_table.grid(row=0, column=0, sticky="nsew")
|
||
y_scroll.grid(row=0, column=1, sticky="ns")
|
||
x_scroll.grid(row=1, column=0, sticky="ew")
|
||
table_wrap.grid_rowconfigure(0, weight=1)
|
||
table_wrap.grid_columnconfigure(0, weight=1)
|
||
|
||
self.clear_accuracy_result_table()
|
||
|
||
|
||
def clear_accuracy_result_table(self: "PQAutomationApp"):
|
||
"""清空色准表格并恢复占位内容。"""
|
||
if not hasattr(self, "accuracy_result_table"):
|
||
return
|
||
|
||
tree = self.accuracy_result_table
|
||
tree.delete(*tree.get_children())
|
||
|
||
columns = ("metric", "value")
|
||
tree.configure(columns=columns)
|
||
tree.heading("metric", text="项目")
|
||
tree.heading("value", text="值")
|
||
tree.column("metric", width=150, anchor="w", stretch=False)
|
||
tree.column("value", width=300, anchor="w", stretch=True)
|
||
|
||
tree.insert("", tk.END, values=("状态", "等待色准测试数据..."))
|
||
|
||
|
||
def update_accuracy_result_table(self: "PQAutomationApp", accuracy_data, standards):
|
||
"""更新色准表格:按指标行 + 色块列展示,可横向滚动浏览。"""
|
||
if not hasattr(self, "accuracy_result_table"):
|
||
return
|
||
|
||
tree = self.accuracy_result_table
|
||
tree.delete(*tree.get_children())
|
||
|
||
color_patches = accuracy_data.get("color_patches", []) or []
|
||
measurements = accuracy_data.get("color_measurements", []) or []
|
||
delta_e_values = accuracy_data.get("delta_e_values", []) or []
|
||
delta_e_itp_values = accuracy_data.get("delta_e_itp_values", []) or []
|
||
|
||
if not color_patches:
|
||
self.clear_accuracy_result_table()
|
||
return
|
||
|
||
columns = ["metric"] + [f"c{i}" for i in range(len(color_patches))]
|
||
tree.configure(columns=columns)
|
||
|
||
tree.heading("metric", text="项目")
|
||
tree.column("metric", width=140, anchor="w", stretch=False)
|
||
|
||
for i, name in enumerate(color_patches):
|
||
col = f"c{i}"
|
||
tree.heading(col, text=name)
|
||
tree.column(col, width=96, anchor="center", stretch=False)
|
||
|
||
def fmt(v, digits=4):
|
||
if isinstance(v, (int, float)):
|
||
return f"{v:.{digits}f}"
|
||
return "N/A"
|
||
|
||
row_x = ["x: CIE31"]
|
||
row_y = ["y: CIE31"]
|
||
row_Y = ["Y"]
|
||
row_tx = ["Target x:CIE31"]
|
||
row_ty = ["Target y:CIE31"]
|
||
row_de2000 = ["ΔE 2000"]
|
||
|
||
include_itp = bool(delta_e_itp_values)
|
||
row_deitp = ["ΔE ITP"] if include_itp else None
|
||
|
||
for i, name in enumerate(color_patches):
|
||
m = measurements[i] if i < len(measurements) else None
|
||
sx, sy = standards.get(name, (None, None))
|
||
|
||
if m is not None and len(m) >= 3:
|
||
row_x.append(fmt(m[0], 4))
|
||
row_y.append(fmt(m[1], 4))
|
||
row_Y.append(fmt(m[2], 4))
|
||
else:
|
||
row_x.append("N/A")
|
||
row_y.append("N/A")
|
||
row_Y.append("N/A")
|
||
|
||
row_tx.append(fmt(sx, 4))
|
||
row_ty.append(fmt(sy, 4))
|
||
|
||
de = delta_e_values[i] if i < len(delta_e_values) else None
|
||
row_de2000.append(fmt(de, 4))
|
||
|
||
if include_itp and row_deitp is not None:
|
||
ditp = delta_e_itp_values[i] if i < len(delta_e_itp_values) else None
|
||
row_deitp.append(fmt(ditp, 4))
|
||
|
||
rows = [row_x, row_y, row_Y, row_tx, row_ty, row_de2000]
|
||
if include_itp and row_deitp is not None:
|
||
rows.append(row_deitp)
|
||
|
||
for row in rows:
|
||
tree.insert("", tk.END, values=row)
|
||
|
||
def clear_chart(self: "PQAutomationApp"):
|
||
"""清空所有图表"""
|
||
|
||
# ========== 1. 清空色域图表 ==========
|
||
if hasattr(self, "gamut_ax_xy") and hasattr(self, "gamut_ax_uv"):
|
||
# 清空XY图
|
||
self.gamut_ax_xy.clear()
|
||
self.gamut_ax_xy.set_xlim(0, 600)
|
||
self.gamut_ax_xy.set_ylim(600, 0)
|
||
self.gamut_ax_xy.axis("off")
|
||
self.gamut_ax_xy.set_clip_on(False)
|
||
|
||
# 清空UV图
|
||
self.gamut_ax_uv.clear()
|
||
self.gamut_ax_uv.set_xlim(0, 600)
|
||
self.gamut_ax_uv.set_ylim(600, 0)
|
||
self.gamut_ax_uv.axis("off")
|
||
self.gamut_ax_uv.set_clip_on(False)
|
||
|
||
self.gamut_fig.suptitle("色域测试", fontsize=12, y=0.98)
|
||
self.gamut_canvas.draw()
|
||
|
||
# ========== 2. 清空Gamma图表(4列 + 通用说明)==========
|
||
if hasattr(self, "gamma_ax") and hasattr(self, "gamma_table_ax"):
|
||
# 清空左侧曲线
|
||
self.gamma_ax.clear()
|
||
self.gamma_ax.set_xlim(0, 105)
|
||
self.gamma_ax.set_ylim(0, 1.1)
|
||
self.gamma_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
||
self.gamma_ax.set_ylabel("L_bar", fontsize=10)
|
||
self.gamma_ax.grid(True, linestyle="--", alpha=0.3)
|
||
self.gamma_ax.tick_params(labelsize=9)
|
||
|
||
# 左侧提示
|
||
self.gamma_ax.text(
|
||
0.5,
|
||
0.5,
|
||
"等待测试数据...\n\n"
|
||
"将显示:\n"
|
||
"• 实测曲线 (蓝色)\n"
|
||
"• 理想 Gamma 曲线 (红色)\n\n"
|
||
"Gamma 值由测试配置决定",
|
||
ha="center",
|
||
va="center",
|
||
fontsize=10,
|
||
color="gray",
|
||
transform=self.gamma_ax.transAxes,
|
||
bbox=dict(
|
||
boxstyle="round,pad=1",
|
||
facecolor="white",
|
||
edgecolor="gray",
|
||
alpha=0.8,
|
||
),
|
||
)
|
||
|
||
# 清空右侧表格
|
||
self.gamma_table_ax.clear()
|
||
self.gamma_table_ax.axis("off")
|
||
|
||
# 4列表格
|
||
table_data = [
|
||
["灰阶", "实测亮度\n(cd/m²)", "L_bar\n(计算)", "Gamma"],
|
||
["0%", "--", "--", "--"],
|
||
["10%", "--", "--", "--"],
|
||
["20%", "--", "--", "--"],
|
||
["30%", "--", "--", "--"],
|
||
["40%", "--", "--", "--"],
|
||
["50%", "--", "--", "--"],
|
||
["60%", "--", "--", "--"],
|
||
["70%", "--", "--", "--"],
|
||
["80%", "--", "--", "--"],
|
||
["90%", "--", "--", "--"],
|
||
["100%", "--", "--", "--"],
|
||
]
|
||
|
||
table = self.gamma_table_ax.table(
|
||
cellText=table_data,
|
||
cellLoc="center",
|
||
loc="center",
|
||
colWidths=[0.18, 0.28, 0.27, 0.27],
|
||
)
|
||
|
||
table.auto_set_font_size(False)
|
||
table.set_fontsize(7.5)
|
||
table.scale(1, 1.5)
|
||
|
||
# 表头样式
|
||
for i in range(4):
|
||
cell = table[(0, i)]
|
||
cell.set_facecolor("#4472C4")
|
||
cell.set_text_props(weight="bold", color="white", fontsize=7)
|
||
|
||
# 数据行交替颜色
|
||
for i in range(1, len(table_data)):
|
||
for j in range(4):
|
||
cell = table[(i, j)]
|
||
if i % 2 == 0:
|
||
cell.set_facecolor("#E7E6E6")
|
||
else:
|
||
cell.set_facecolor("#FFFFFF")
|
||
|
||
# 底部说明
|
||
self.gamma_table_ax.text(
|
||
0.5,
|
||
0.02,
|
||
"表格说明:\n"
|
||
"• 实测亮度: 色度计测量值 (cd/m²)\n"
|
||
"• L_bar: 归一化亮度 (0-1)\n"
|
||
"• Gamma: 实际 Gamma 值",
|
||
ha="center",
|
||
va="bottom",
|
||
fontsize=7,
|
||
color="gray",
|
||
transform=self.gamma_table_ax.transAxes,
|
||
bbox=dict(
|
||
boxstyle="round,pad=0.5",
|
||
facecolor="lightyellow",
|
||
edgecolor="gray",
|
||
alpha=0.8,
|
||
),
|
||
)
|
||
|
||
self.gamma_fig.suptitle("Gamma曲线 + 数据表格", fontsize=12, y=0.98)
|
||
self.gamma_canvas.draw()
|
||
|
||
# ========== 3. 清空EOTF图表(4列)==========
|
||
if hasattr(self, "eotf_ax") and hasattr(self, "eotf_table_ax"):
|
||
# 清空左侧曲线
|
||
self.eotf_ax.clear()
|
||
self.eotf_ax.set_xlim(0, 105)
|
||
self.eotf_ax.set_ylim(0, 1.1)
|
||
self.eotf_ax.set_xlabel("灰阶 (%)", fontsize=10)
|
||
self.eotf_ax.set_ylabel("L_bar (归一化亮度)", fontsize=10)
|
||
self.eotf_ax.grid(True, linestyle="--", alpha=0.3)
|
||
self.eotf_ax.tick_params(labelsize=9)
|
||
|
||
# 左侧提示
|
||
self.eotf_ax.text(
|
||
0.5,
|
||
0.5,
|
||
"等待测试数据...\n\n将显示:\n• 实测 EOTF 曲线 (蓝色)\n• 理想 PQ 曲线 (红色)",
|
||
ha="center",
|
||
va="center",
|
||
fontsize=11,
|
||
color="gray",
|
||
transform=self.eotf_ax.transAxes,
|
||
bbox=dict(
|
||
boxstyle="round,pad=1",
|
||
facecolor="white",
|
||
edgecolor="gray",
|
||
alpha=0.8,
|
||
),
|
||
)
|
||
|
||
# 清空右侧表格
|
||
self.eotf_table_ax.clear()
|
||
self.eotf_table_ax.axis("off")
|
||
|
||
# 4列表格
|
||
table_data = [
|
||
["灰阶", "实测亮度\n(cd/m²)", "L_bar\n(计算)", "EOTF γ"],
|
||
["0%", "--", "--", "--"],
|
||
["10%", "--", "--", "--"],
|
||
["20%", "--", "--", "--"],
|
||
["30%", "--", "--", "--"],
|
||
["40%", "--", "--", "--"],
|
||
["50%", "--", "--", "--"],
|
||
["60%", "--", "--", "--"],
|
||
["70%", "--", "--", "--"],
|
||
["80%", "--", "--", "--"],
|
||
["90%", "--", "--", "--"],
|
||
["100%", "--", "--", "--"],
|
||
]
|
||
|
||
table = self.eotf_table_ax.table(
|
||
cellText=table_data,
|
||
cellLoc="center",
|
||
loc="center",
|
||
colWidths=[0.18, 0.28, 0.27, 0.27],
|
||
)
|
||
|
||
table.auto_set_font_size(False)
|
||
table.set_fontsize(7.5)
|
||
table.scale(1, 1.5)
|
||
|
||
# 表头样式
|
||
for i in range(4):
|
||
cell = table[(0, i)]
|
||
cell.set_facecolor("#4472C4")
|
||
cell.set_text_props(weight="bold", color="white", fontsize=7)
|
||
|
||
# 数据行交替颜色
|
||
for i in range(1, len(table_data)):
|
||
for j in range(4):
|
||
cell = table[(i, j)]
|
||
if i % 2 == 0:
|
||
cell.set_facecolor("#E7E6E6")
|
||
else:
|
||
cell.set_facecolor("#FFFFFF")
|
||
|
||
# 底部说明
|
||
self.eotf_table_ax.text(
|
||
0.5,
|
||
0.02,
|
||
"表格说明:\n"
|
||
"• 实测亮度: 色度计测量值 (cd/m²)\n"
|
||
"• L_bar: 归一化亮度 (0-1)\n"
|
||
"• EOTF γ: HDR 实际 Gamma 值",
|
||
ha="center",
|
||
va="bottom",
|
||
fontsize=7,
|
||
color="gray",
|
||
transform=self.eotf_table_ax.transAxes,
|
||
bbox=dict(
|
||
boxstyle="round,pad=0.5",
|
||
facecolor="lightyellow",
|
||
edgecolor="gray",
|
||
alpha=0.8,
|
||
),
|
||
)
|
||
|
||
self.eotf_fig.suptitle("EOTF 曲线 + 数据表格", fontsize=12, y=0.98)
|
||
self.eotf_canvas.draw()
|
||
|
||
# ========== 4. 清空色度图表 ==========
|
||
# 注意:plot_cct 会调用 cct_fig.clear() 并重建 subplots,导致 self.cct_ax1/ax2 变成
|
||
# 过期引用。因此清空时必须同样重建,并更新引用,否则清不干净。
|
||
if hasattr(self, "cct_fig") and hasattr(self, "cct_canvas"):
|
||
self.cct_fig.clear()
|
||
|
||
self.cct_ax1 = self.cct_fig.add_subplot(211)
|
||
self.cct_ax1.set_xlabel("灰阶 (%)", fontsize=9)
|
||
self.cct_ax1.set_ylabel("CIE x", fontsize=9)
|
||
self.cct_ax1.set_xlim(0, 105)
|
||
self.cct_ax1.set_ylim(0.25, 0.35)
|
||
self.cct_ax1.grid(True, linestyle="--", alpha=0.3)
|
||
self.cct_ax1.tick_params(labelsize=8)
|
||
|
||
self.cct_ax2 = self.cct_fig.add_subplot(212)
|
||
self.cct_ax2.set_xlabel("灰阶 (%)", fontsize=9)
|
||
self.cct_ax2.set_ylabel("CIE y", fontsize=9)
|
||
self.cct_ax2.set_xlim(0, 105)
|
||
self.cct_ax2.set_ylim(0.25, 0.35)
|
||
self.cct_ax2.grid(True, linestyle="--", alpha=0.3)
|
||
self.cct_ax2.tick_params(labelsize=8)
|
||
|
||
self.cct_fig.suptitle("色度一致性测试", fontsize=12, y=0.985)
|
||
self.cct_fig.subplots_adjust(
|
||
left=0.12,
|
||
right=0.88,
|
||
top=0.90,
|
||
bottom=0.08,
|
||
hspace=0.25,
|
||
)
|
||
self.cct_canvas.draw()
|
||
|
||
# ========== 5. 清空对比度图表 ==========
|
||
if hasattr(self, "contrast_ax"):
|
||
self.contrast_ax.clear()
|
||
self.contrast_ax.set_xlim(0, 1)
|
||
self.contrast_ax.set_ylim(0, 1)
|
||
self.contrast_ax.axis("off")
|
||
|
||
self.contrast_fig.suptitle("对比度测试", fontsize=12, y=0.985)
|
||
|
||
# 重置布局
|
||
self.contrast_fig.subplots_adjust(
|
||
left=0.02,
|
||
right=0.98,
|
||
top=0.90,
|
||
bottom=0.02,
|
||
)
|
||
|
||
self.contrast_canvas.draw()
|
||
|
||
# ========== 6. 清空色准图表 ==========
|
||
if hasattr(self, "accuracy_ax"):
|
||
self.accuracy_ax.clear()
|
||
self.accuracy_ax.set_xlim(0, 1)
|
||
self.accuracy_ax.set_ylim(0, 1)
|
||
self.accuracy_ax.axis("off")
|
||
|
||
# 标题
|
||
self.accuracy_fig.suptitle("色准测试", fontsize=12, y=0.985)
|
||
|
||
# 重置布局
|
||
self.accuracy_fig.subplots_adjust(
|
||
left=0.05,
|
||
right=0.95,
|
||
top=0.90,
|
||
bottom=0.05,
|
||
)
|
||
|
||
self.accuracy_canvas.draw()
|
||
|
||
# 清空色准明细表格
|
||
self.clear_accuracy_result_table()
|
||
|
||
def update_chart_tabs_state(self: "PQAutomationApp"):
|
||
"""根据测试项目复选框状态动态增删图表 Tab(保持规范顺序)。
|
||
|
||
- 色域 / Gamma 或 EOTF / 色度一致性 / 对比度 / 色准 全部走动态 add/forget
|
||
- Gamma 与 EOTF 二选一,由 current_test_type 决定
|
||
- 屏模组测试强制隐藏色准 Tab
|
||
- 客户模板 Tab 由 change_test_type 独立管理,这里不动
|
||
"""
|
||
if not hasattr(self, "chart_notebook"):
|
||
return
|
||
|
||
selected_items = self.get_selected_test_items()
|
||
current_test_type = self.config.current_test_type
|
||
|
||
# 根据测试类型决定 gamma/eotf 显示哪一个
|
||
if current_test_type == "hdr_movie":
|
||
gamma_like_frame = self.eotf_chart_frame
|
||
gamma_like_text = "EOTF 曲线"
|
||
gamma_like_other = self.gamma_chart_frame
|
||
else:
|
||
gamma_like_frame = self.gamma_chart_frame
|
||
gamma_like_text = "Gamma 曲线"
|
||
gamma_like_other = self.eotf_chart_frame
|
||
|
||
want_gamut = "gamut" in selected_items
|
||
want_gamma_like = "gamma" in selected_items or "eotf" in selected_items
|
||
want_cct = "cct" in selected_items
|
||
want_contrast = "contrast" in selected_items
|
||
want_accuracy = (
|
||
"accuracy" in selected_items and current_test_type != "screen_module"
|
||
)
|
||
|
||
# 规范顺序:色域 → Gamma/EOTF → 色度一致性 → 对比度 → 色准
|
||
spec = [
|
||
(want_gamut, self.gamut_chart_frame, "色域图"),
|
||
(want_gamma_like, gamma_like_frame, gamma_like_text),
|
||
(want_cct, self.cct_chart_frame, "色度一致性"),
|
||
(want_contrast, self.contrast_chart_frame, "对比度"),
|
||
(want_accuracy, self.accuracy_chart_frame, "色准"),
|
||
]
|
||
|
||
try:
|
||
# 始终先把"另一个" gamma/eotf frame 从 Notebook 移除,保持互斥
|
||
current_ids = list(self.chart_notebook.tabs())
|
||
if str(gamma_like_other) in current_ids:
|
||
self.chart_notebook.forget(gamma_like_other)
|
||
|
||
# 按规范顺序处理 add/forget
|
||
for idx_in_spec, (want, frame, text) in enumerate(spec):
|
||
fid = str(frame)
|
||
present = fid in self.chart_notebook.tabs()
|
||
|
||
if want and not present:
|
||
# 统计该 frame 在 spec 中前面、当前实际存在的 tab 数 → 插入位置
|
||
current_ids = list(self.chart_notebook.tabs())
|
||
pos = sum(
|
||
1 for pre_want, pre_frame, _ in spec[:idx_in_spec]
|
||
if str(pre_frame) in current_ids
|
||
)
|
||
try:
|
||
self.chart_notebook.insert(pos, frame, text=text)
|
||
except Exception:
|
||
# fallback:尾部 add
|
||
self.chart_notebook.add(frame, text=text)
|
||
|
||
elif not want and present:
|
||
self.chart_notebook.forget(frame)
|
||
|
||
except Exception as e:
|
||
if hasattr(self, "log_gui"):
|
||
self.log_gui.log(f"更新Tab状态失败: {str(e)}", level="error")
|
||
|
||
def create_result_chart_frame(self: "PQAutomationApp"):
|
||
"""创建结果图表区域 - 6个独立Tab(Gamma 和 EOTF 分离)"""
|
||
# 创建Notebook用于图表切换
|
||
self.chart_notebook = ttk.Notebook(self.result_frame)
|
||
self.chart_notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||
|
||
# ========== 创建6个独立的Tab页面 ==========
|
||
# 1. 色域图页面
|
||
self.gamut_chart_frame = ttk.Frame(self.chart_notebook)
|
||
|
||
# 2. Gamma图页面(SDR/屏模组使用)
|
||
self.gamma_chart_frame = ttk.Frame(self.chart_notebook)
|
||
|
||
# 3. EOTF图页面(HDR专用)
|
||
self.eotf_chart_frame = ttk.Frame(self.chart_notebook)
|
||
|
||
# 4. 色度一致性页面
|
||
self.cct_chart_frame = ttk.Frame(self.chart_notebook)
|
||
|
||
# 5. 对比度页面
|
||
self.contrast_chart_frame = ttk.Frame(self.chart_notebook)
|
||
|
||
# 6. 色准页面
|
||
self.accuracy_chart_frame = ttk.Frame(self.chart_notebook)
|
||
|
||
# 7. 客户模板结果页面
|
||
self.custom_template_tab_frame = ttk.Frame(self.chart_notebook)
|
||
|
||
# ========== 添加到Notebook(初始只添加前5个)==========
|
||
self.chart_notebook.add(self.gamut_chart_frame, text="色域图")
|
||
self.chart_notebook.add(self.gamma_chart_frame, text="Gamma曲线")
|
||
# ← EOTF 不添加,由 change_test_type() 动态控制
|
||
self.chart_notebook.add(self.cct_chart_frame, text="色度一致性")
|
||
self.chart_notebook.add(self.contrast_chart_frame, text="对比度")
|
||
self.chart_notebook.add(self.accuracy_chart_frame, text="色准")
|
||
|
||
# 初始化六个独立的图表
|
||
self.init_gamut_chart()
|
||
self.init_gamma_chart()
|
||
self.init_eotf_chart()
|
||
self.init_cct_chart()
|
||
self.init_contrast_chart()
|
||
self.init_accuracy_chart()
|
||
self.apply_result_chart_theme()
|
||
|
||
# 绑定Tab切换事件
|
||
self.chart_notebook.bind("<<NotebookTabChanged>>", self.on_chart_tab_changed)
|
||
|
||
# ==================== 在图表下方创建单步调试面板 ====================
|
||
self.debug_container = ttk.LabelFrame(
|
||
self.result_frame, # ← 放在 result_frame 内,图表正下方
|
||
text=" 单步调试",
|
||
padding=10,
|
||
)
|
||
# 默认不显示
|
||
|
||
# 创建单步调试面板实例
|
||
self.debug_panel = PQDebugPanel(self.debug_container, self)
|
||
|
||
def on_chart_tab_changed(self: "PQAutomationApp", event):
|
||
"""Tab切换时的事件处理"""
|
||
try:
|
||
selected_tab = self.chart_notebook.select()
|
||
# 在动态 add/forget tab 的过程中,可能短暂出现“无选中页签”。
|
||
if not selected_tab:
|
||
return
|
||
self._last_tab_index = self.chart_notebook.index(selected_tab)
|
||
except Exception as e:
|
||
self.log_gui.log(f"Tab切换事件处理失败: {str(e)}", level="error")
|
||
|
||
|
||
|
||
class ChartFrameMixin:
|
||
"""由 tools/refactor_to_mixins.py 自动生成。
|
||
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
||
"""
|
||
init_gamut_chart = init_gamut_chart
|
||
sync_gamut_toolbar = sync_gamut_toolbar
|
||
_on_gamut_toolbar_changed = _on_gamut_toolbar_changed
|
||
init_gamma_chart = init_gamma_chart
|
||
init_eotf_chart = init_eotf_chart
|
||
init_cct_chart = init_cct_chart
|
||
init_contrast_chart = init_contrast_chart
|
||
init_accuracy_chart = init_accuracy_chart
|
||
apply_result_chart_theme = apply_result_chart_theme
|
||
_init_accuracy_result_table = _init_accuracy_result_table
|
||
clear_accuracy_result_table = clear_accuracy_result_table
|
||
update_accuracy_result_table = update_accuracy_result_table
|
||
clear_chart = clear_chart
|
||
update_chart_tabs_state = update_chart_tabs_state
|
||
create_result_chart_frame = create_result_chart_frame
|
||
on_chart_tab_changed = on_chart_tab_changed
|