Files
pqAutomationApp/app/views/chart_frame.py
2026-06-05 16:58:46 +08:00

1139 lines
39 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""图表框架相关逻辑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 app.views.modern_styles import get_theme_palette
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp
def _result_bg_color() -> str:
"""根据当前主题返回结果图背景色。"""
try:
return get_theme_palette()["bg"]
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 _apply_axes_theme(ax, palette, *, title=None, xlabel=None, ylabel=None):
ax.set_facecolor(palette["card_bg"])
for spine in ax.spines.values():
spine.set_color(palette["border"])
if title is not None:
ax.set_title(title, color=palette["fg"])
if xlabel is not None:
ax.set_xlabel(xlabel, color=palette["fg"])
if ylabel is not None:
ax.set_ylabel(ylabel, color=palette["fg"])
ax.tick_params(axis="both", colors=palette["fg"])
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)
palette = get_theme_palette()
self.gamma_fig = plt.Figure(figsize=(12, 6), dpi=100, constrained_layout=False)
self.gamma_fig.patch.set_facecolor(palette["bg"])
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])
_apply_axes_theme(self.gamma_ax, palette, xlabel="灰阶 (%)", ylabel="L_bar")
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=palette["muted_fg"],
transform=self.gamma_ax.transAxes,
bbox=dict(
boxstyle="round,pad=1",
facecolor=palette["card_bg"],
edgecolor=palette["border"],
alpha=0.95,
),
)
# 右侧:数据表格
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(palette["primary"])
cell.set_text_props(weight="bold", color=palette["select_fg"], 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(palette["surface_alt_bg"])
else:
cell.set_facecolor(palette["card_bg"])
# 底部说明
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=palette["muted_fg"],
transform=self.gamma_table_ax.transAxes,
bbox=dict(
boxstyle="round,pad=0.5",
facecolor=palette["surface_alt_bg"],
edgecolor=palette["border"],
alpha=0.95,
),
)
self.gamma_fig.suptitle("Gamma曲线 + 数据表格", fontsize=12, y=0.98, color=palette["fg"])
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)
palette = get_theme_palette()
self.eotf_fig = plt.Figure(figsize=(12, 6), dpi=100, constrained_layout=False)
self.eotf_fig.patch.set_facecolor(palette["bg"])
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])
_apply_axes_theme(self.eotf_ax, palette, xlabel="灰阶 (%)", ylabel="L_bar (归一化亮度)")
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=palette["muted_fg"],
transform=self.eotf_ax.transAxes,
bbox=dict(
boxstyle="round,pad=1",
facecolor=palette["card_bg"],
edgecolor=palette["border"],
alpha=0.95,
),
)
# 右侧:数据表格
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(palette["primary"])
cell.set_text_props(weight="bold", color=palette["select_fg"], 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(palette["surface_alt_bg"])
else:
cell.set_facecolor(palette["card_bg"])
# 底部说明
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=palette["muted_fg"],
transform=self.eotf_table_ax.transAxes,
bbox=dict(
boxstyle="round,pad=0.5",
facecolor=palette["surface_alt_bg"],
edgecolor=palette["border"],
alpha=0.95,
),
)
self.eotf_fig.suptitle("EOTF 曲线 + 数据表格", fontsize=12, y=0.98, color=palette["fg"])
self.eotf_canvas.draw()
def init_cct_chart(self: "PQAutomationApp"):
"""初始化色度坐标图表 - 正向横坐标,标题居中最上方"""
container = ttk.Frame(self.cct_chart_frame)
container.pack(expand=True)
palette = get_theme_palette()
self.cct_fig = plt.Figure(figsize=(8, 6), dpi=100, tight_layout=False)
self.cct_fig.patch.set_facecolor(palette["bg"])
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_ax1.set_facecolor(palette["card_bg"])
self.cct_ax2 = self.cct_fig.add_subplot(212)
self.cct_ax2.set_facecolor(palette["card_bg"])
# 上图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, color=palette["fg"])
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)
palette = get_theme_palette()
self.contrast_fig = plt.Figure(
figsize=(6, 6),
dpi=100,
tight_layout=False,
)
self.contrast_fig.patch.set_facecolor(palette["bg"])
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_facecolor(palette["card_bg"])
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, color=palette["fg"])
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)
palette = get_theme_palette()
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_fig.patch.set_facecolor(palette["bg"])
try:
self.accuracy_fig.set_layout_engine(None)
except Exception:
try:
self.accuracy_fig.set_tight_layout(False)
except Exception:
pass
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_facecolor(palette["card_bg"])
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, color=palette["fg"])
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"):
"""清空所有图表"""
palette = get_theme_palette()
# ========== 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_fig.patch.set_facecolor(palette["bg"])
self.gamma_ax.set_facecolor(palette["card_bg"])
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.tick_params(colors=palette["fg"])
for spine in self.gamma_ax.spines.values():
spine.set_color(palette["border"])
# 左侧提示
self.gamma_ax.text(
0.5,
0.5,
"等待测试数据...\n\n"
"将显示:\n"
"• 实测曲线 (蓝色)\n"
"• 理想 Gamma 曲线 (红色)\n\n"
"Gamma 值由测试配置决定",
ha="center",
va="center",
fontsize=10,
color=palette["muted_fg"],
transform=self.gamma_ax.transAxes,
bbox=dict(
boxstyle="round,pad=1",
facecolor=palette["card_bg"],
edgecolor=palette["border"],
alpha=0.95,
),
)
# 清空右侧表格
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(palette["primary"])
cell.set_text_props(weight="bold", color=palette["select_fg"], 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(palette["surface_alt_bg"])
else:
cell.set_facecolor(palette["card_bg"])
# 底部说明
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=palette["muted_fg"],
transform=self.gamma_table_ax.transAxes,
bbox=dict(
boxstyle="round,pad=0.5",
facecolor=palette["surface_alt_bg"],
edgecolor=palette["border"],
alpha=0.95,
),
)
self.gamma_fig.suptitle("Gamma曲线 + 数据表格", fontsize=12, y=0.98, color=palette["fg"])
self.gamma_canvas.draw()
# ========== 3. 清空EOTF图表4列==========
if hasattr(self, "eotf_ax") and hasattr(self, "eotf_table_ax"):
# 清空左侧曲线
self.eotf_ax.clear()
self.eotf_fig.patch.set_facecolor(palette["bg"])
self.eotf_ax.set_facecolor(palette["card_bg"])
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.tick_params(colors=palette["fg"])
for spine in self.eotf_ax.spines.values():
spine.set_color(palette["border"])
# 左侧提示
self.eotf_ax.text(
0.5,
0.5,
"等待测试数据...\n\n将显示:\n• 实测 EOTF 曲线 (蓝色)\n• 理想 PQ 曲线 (红色)",
ha="center",
va="center",
fontsize=11,
color=palette["muted_fg"],
transform=self.eotf_ax.transAxes,
bbox=dict(
boxstyle="round,pad=1",
facecolor=palette["card_bg"],
edgecolor=palette["border"],
alpha=0.95,
),
)
# 清空右侧表格
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(palette["primary"])
cell.set_text_props(weight="bold", color=palette["select_fg"], 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(palette["surface_alt_bg"])
else:
cell.set_facecolor(palette["card_bg"])
# 底部说明
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=palette["muted_fg"],
transform=self.eotf_table_ax.transAxes,
bbox=dict(
boxstyle="round,pad=0.5",
facecolor=palette["surface_alt_bg"],
edgecolor=palette["border"],
alpha=0.95,
),
)
self.eotf_fig.suptitle("EOTF 曲线 + 数据表格", fontsize=12, y=0.98, color=palette["fg"])
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_fig.patch.set_facecolor(palette["bg"])
self.cct_ax1 = self.cct_fig.add_subplot(211)
self.cct_ax1.set_facecolor(palette["card_bg"])
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_facecolor(palette["card_bg"])
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, color=palette["fg"])
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_fig.patch.set_facecolor(palette["bg"])
self.contrast_ax.set_facecolor(palette["card_bg"])
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, color=palette["fg"])
# 重置布局
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_fig.patch.set_facecolor(palette["bg"])
self.accuracy_ax.set_facecolor(palette["card_bg"])
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, color=palette["fg"])
# 重置布局
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个独立TabGamma 和 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