继续优化色准测试结果显示模块

This commit is contained in:
xinzhu.yin
2026-05-27 16:26:19 +08:00
parent 59c9424218
commit c63b9ef615
6 changed files with 428 additions and 37 deletions

View File

@@ -75,7 +75,7 @@ def _xy_to_uv(x: float, y: float):
# 子图:左侧 Calman 风格面板
# ============================================================
def _draw_left_panel(ax, color_patches, delta_e_values):
def _draw_left_panel(ax, color_patches, delta_e_values, font_scale=1.0):
"""左侧仅保留大条形图。"""
ax.clear()
@@ -99,11 +99,12 @@ def _draw_left_panel(ax, color_patches, delta_e_values):
)
ax.set_yticks(y_pos)
ax.set_yticklabels(color_patches, fontsize=7)
ax.set_yticklabels(color_patches, fontsize=max(5, 7 * font_scale))
ax.invert_yaxis()
x_max = max(15.0, max(delta_e_values) * 1.15)
ax.set_xlim(0, x_max)
ax.tick_params(axis="x", labelsize=max(6, 8 * font_scale))
ax.grid(axis="x", linestyle="-", linewidth=0.6, alpha=0.3, zorder=0)
ax.grid(axis="y", linestyle=":", linewidth=0.35, alpha=0.15, zorder=0)
@@ -117,7 +118,7 @@ def _draw_left_panel(ax, color_patches, delta_e_values):
# 子图CIE 1976 u'v' 色度图(目标 vs 实测)
# ============================================================
def _draw_uv_diagram(ax, color_patches, measurements, standards):
def _draw_uv_diagram(ax, color_patches, measurements, standards, font_scale=1.0):
"""绘制 CIE 1976 u'v' 上的色准对比。"""
ax.clear()
try:
@@ -136,11 +137,11 @@ def _draw_uv_diagram(ax, color_patches, measurements, standards):
ax.set_facecolor("#000")
ax.set_aspect("equal", adjustable="box")
ax.set_title("CIE 1976 u'v'", fontsize=11, fontweight="bold",
ax.set_title("CIE 1976 u'v'", fontsize=max(8, 11 * font_scale), fontweight="bold",
color="#111", pad=4)
ax.set_xlabel("u'", fontsize=9, color="#222", labelpad=1)
ax.set_ylabel("v'", fontsize=9, color="#222", labelpad=1)
ax.tick_params(axis="both", labelsize=8, colors="#222")
ax.set_xlabel("u'", fontsize=max(7, 9 * font_scale), color="#222", labelpad=1)
ax.set_ylabel("v'", fontsize=max(7, 9 * font_scale), color="#222", labelpad=1)
ax.tick_params(axis="both", labelsize=max(6, 8 * font_scale), colors="#222")
for sp in ax.spines.values():
sp.set_color("#666")
sp.set_linewidth(0.9)
@@ -190,7 +191,7 @@ def _draw_uv_diagram(ax, color_patches, measurements, standards):
]
leg = ax.legend(
handles=legend_handles,
loc="lower right", fontsize=8,
loc="lower right", fontsize=max(6, 8 * font_scale),
framealpha=0.88, labelcolor="#FFF",
)
if leg is not None:
@@ -199,7 +200,7 @@ def _draw_uv_diagram(ax, color_patches, measurements, standards):
leg.set_zorder(50)
def _draw_result_judgement(ax, accuracy_data):
def _draw_result_judgement(ax, accuracy_data, font_scale=1.0):
"""底部结果条"""
ax.clear()
ax.set_xlim(0, 1)
@@ -219,14 +220,14 @@ def _draw_result_judgement(ax, accuracy_data):
0.03, 0.50,
f"Avg dE2000: {avg:.2f}",
ha="left", va="center",
fontsize=20, fontweight="normal", color="#111111",
fontsize=max(11, 20 * font_scale), fontweight="normal", color="#111111",
transform=ax.transAxes,
)
ax.text(
0.52, 0.50,
f"Max dE2000: {mx:.2f}",
ha="left", va="center",
fontsize=20, fontweight="normal", color="#111111",
fontsize=max(11, 20 * font_scale), fontweight="normal", color="#111111",
transform=ax.transAxes,
)
@@ -241,6 +242,17 @@ def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type):
fig = self.accuracy_fig
fig.clear()
# 根据当前画布像素尺寸动态缩放字体,避免窗口缩小时文字挤压重叠。
font_scale = 1.0
try:
canvas_widget = self.accuracy_canvas.get_tk_widget()
cw = max(1, int(canvas_widget.winfo_width()))
ch = max(1, int(canvas_widget.winfo_height()))
font_scale = min(cw / 1000.0, ch / 600.0)
font_scale = max(0.60, min(1.0, font_scale))
except Exception:
font_scale = 1.0
color_patches = accuracy_data.get("color_patches", []) or []
delta_e_values = accuracy_data.get("delta_e_values", []) or []
measurements = accuracy_data.get("color_measurements", []) or []
@@ -259,15 +271,21 @@ def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type):
else:
title = f"{test_type_name} - 色准测试(全 29色 | Gamma {target_gamma}"
fig.suptitle(title, fontsize=11, y=0.975, fontweight="bold", color="#111")
fig.suptitle(
title,
fontsize=max(8, 11 * font_scale),
y=0.975,
fontweight="bold",
color="#111",
)
gs = fig.add_gridspec(
2, 2,
width_ratios=[1.12, 1.0],
height_ratios=[4.0, 0.62],
height_ratios=[4.8, 0.48],
left=0.08, right=0.985,
top=0.91, bottom=0.06,
wspace=0.14, hspace=0.10,
top=0.92, bottom=0.05,
wspace=0.14, hspace=0.08,
)
ax_left = fig.add_subplot(gs[0, 0])
@@ -277,14 +295,26 @@ def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type):
# 兼容外部对 self.accuracy_ax 的引用
self.accuracy_ax = ax_judge
_draw_left_panel(ax_left, color_patches, delta_e_values)
_draw_left_panel(ax_left, color_patches, delta_e_values, font_scale=font_scale)
try:
standards = get_accuracy_color_standards(test_type)
except Exception:
standards = {}
_draw_uv_diagram(ax_uv, color_patches, measurements, standards)
_draw_result_judgement(ax_judge, accuracy_data)
_draw_uv_diagram(
ax_uv,
color_patches,
measurements,
standards,
font_scale=font_scale,
)
_draw_result_judgement(ax_judge, accuracy_data, font_scale=font_scale)
try:
self.update_accuracy_result_table(accuracy_data, standards)
except Exception:
pass
self.accuracy_canvas.draw()
self.chart_notebook.select(self.accuracy_chart_frame)

View File

@@ -1073,6 +1073,226 @@ def test_color_accuracy(self: "PQAutomationApp", test_type):
self.log_gui.log("色准测试完成", level="success")
def run_simulation_test(self: "PQAutomationApp"):
"""运行模拟测试(无需 UCD/CA直接在 UI 展示结果。"""
try:
test_type = self.config.current_test_type
selected_items = self.get_selected_test_items()
if not selected_items:
self.log_gui.log("未选择测试项目,无法执行模拟测试", level="error")
messagebox.showwarning("提示", "请先勾选至少一个测试项目")
return
self.log_gui.log("=" * 60, level="separator")
self.log_gui.log("开始执行模拟测试(无需 UCD/CA 设备)", level="info")
self.log_gui.log(f"测试类型: {self.get_test_type_name(test_type)}", level="info")
self.log_gui.log(
f"测试项目: {', '.join(self.config.get_test_item_chinese_names(selected_items))}",
level="info",
)
if hasattr(self, "update_chart_tabs_state"):
self.update_chart_tabs_state()
if hasattr(self, "clear_chart"):
self.clear_chart()
self.new_pq_results(test_type, f"{self.get_test_type_name(test_type)} 模拟测试")
self.status_var.set("模拟测试进行中...")
rng = np.random.default_rng()
def _read_ideal_xy():
try:
if test_type == "sdr_movie":
return float(self.sdr_cct_x_ideal_var.get()), float(self.sdr_cct_y_ideal_var.get())
if test_type == "hdr_movie":
return float(self.hdr_cct_x_ideal_var.get()), float(self.hdr_cct_y_ideal_var.get())
return float(self.cct_x_ideal_var.get()), float(self.cct_y_ideal_var.get())
except Exception:
return 0.3127, 0.3290
def _xyY_to_xyz_row(x, y, lv):
if y <= 1e-8:
return [x, y, lv, 0.0, lv, 0.0]
X = x * lv / y
Z = (1 - x - y) * lv / y
return [x, y, lv, X, lv, Z]
# 共享灰阶数据:用于 Gamma/EOTF/CCT/对比度
gray_results = []
x_ideal, y_ideal = _read_ideal_xy()
peak_lv = 900.0 if test_type == "hdr_movie" else 220.0
gamma_shape = 2.25 if test_type == "hdr_movie" else 2.20
for i in range(11):
p = i / 10.0
lv = 0.08 + peak_lv * (p ** gamma_shape)
lv *= 1.0 + float(rng.normal(0.0, 0.015))
lv = max(lv, 0.03)
x = x_ideal + float(rng.normal(0.0, 0.0012))
y = y_ideal + float(rng.normal(0.0, 0.0012))
gray_results.append(_xyY_to_xyz_row(x, y, lv))
if any(item in selected_items for item in ("gamma", "eotf", "cct", "contrast")):
self.results.add_intermediate_data("shared", "gray", gray_results)
# 色域模拟
if "gamut" in selected_items:
ref_map = {
"BT.709": [(0.6400, 0.3300), (0.3000, 0.6000), (0.1500, 0.0600)],
"DCI-P3": [(0.6800, 0.3200), (0.2650, 0.6900), (0.1500, 0.0600)],
"BT.2020": [(0.7080, 0.2920), (0.1700, 0.7970), (0.1310, 0.0460)],
"BT.601": [(0.6300, 0.3400), (0.3100, 0.5950), (0.1550, 0.0700)],
}
if test_type == "hdr_movie":
reference = self.hdr_gamut_ref_var.get() if hasattr(self, "hdr_gamut_ref_var") else "BT.2020"
elif test_type == "sdr_movie":
reference = self.sdr_gamut_ref_var.get() if hasattr(self, "sdr_gamut_ref_var") else "BT.709"
else:
reference = self.screen_gamut_ref_var.get() if hasattr(self, "screen_gamut_ref_var") else "DCI-P3"
if reference not in ref_map:
reference = "DCI-P3"
gamut_results = []
for rx, ry in ref_map[reference]:
mx = rx + float(rng.normal(0.0, 0.006))
my = ry + float(rng.normal(0.0, 0.006))
gamut_results.append(_xyY_to_xyz_row(mx, my, 120.0))
self.results.add_intermediate_data("gamut", "rgb", gamut_results)
self.results.set_test_item_result(
"gamut",
{
"area": 0.0,
"coverage": 95.0,
"uv_coverage": 93.0,
"reference": reference,
},
)
self.plot_gamut(gamut_results, 95.0, test_type)
# Gamma / EOTF 模拟
if "gamma" in selected_items and test_type != "hdr_movie":
pattern_params = self.config.default_pattern_gray.get("pattern_params", None)
results_with_gamma, L_bar = self.calculate_gamma(
gray_results, len(gray_results) - 1, pattern_params
)
self.results.set_test_item_result("gamma", {"gamma": results_with_gamma, "L_bar": L_bar})
try:
target_gamma = float(self.sdr_gamma_type_var.get()) if test_type == "sdr_movie" else 2.2
except Exception:
target_gamma = 2.2
self.plot_gamma(L_bar, results_with_gamma, target_gamma, test_type)
if "eotf" in selected_items and test_type == "hdr_movie":
pattern_params = self.config.default_pattern_gray.get("pattern_params", None)
results_with_eotf, L_bar = self.calculate_gamma(
gray_results, len(gray_results) - 1, pattern_params
)
self.results.set_test_item_result("eotf", {"eotf": results_with_eotf, "L_bar": L_bar})
self.plot_eotf(L_bar, results_with_eotf, test_type)
# CCT 模拟
if "cct" in selected_items:
cct_values = pq_algorithm.calculate_cct_from_results(gray_results)
self.results.set_test_item_result("cct", {"cct_values": cct_values})
self.plot_cct(test_type)
# 对比度模拟
if "contrast" in selected_items:
luminance_values = [row[2] for row in gray_results]
max_luminance = max(luminance_values)
min_luminance = max(min(luminance_values), 0.001)
contrast_ratio = max_luminance / min_luminance
contrast_data = {
"max_luminance": max_luminance,
"min_luminance": min_luminance,
"contrast_ratio": contrast_ratio,
"luminance_values": luminance_values,
}
self.results.set_test_item_result("contrast", contrast_data)
self.plot_contrast(contrast_data, test_type)
# 色准模拟
if "accuracy" in selected_items:
color_names = self.config.get_accuracy_color_names()
standards = self.get_accuracy_color_standards(test_type)
color_patches = []
measured_data = []
delta_e_values = []
for idx, name in enumerate(color_names):
sx, sy = standards.get(name, (0.3127, 0.3290))
# 前 20 个色块偏差更小,后 9 个稍大,方便 UI 看出差异
noise_sigma = 0.0008 if idx < 20 else 0.0018
mx = sx + float(rng.normal(0.0, noise_sigma))
my = sy + float(rng.normal(0.0, noise_sigma))
lv = max(5.0, 40.0 + idx * 2.3 + float(rng.normal(0.0, 4.0)))
row = _xyY_to_xyz_row(mx, my, lv)
measured_data.append(row)
color_patches.append(name)
delta_e = self.calculate_delta_e_2000(mx, my, lv, sx, sy)
delta_e_values.append(delta_e)
avg_delta_e = float(np.mean(delta_e_values)) if delta_e_values else 0.0
max_delta_e = float(np.max(delta_e_values)) if delta_e_values else 0.0
min_delta_e = float(np.min(delta_e_values)) if delta_e_values else 0.0
excellent_count = sum(1 for d in delta_e_values if d < 3)
good_count = sum(1 for d in delta_e_values if 3 <= d < 5)
poor_count = sum(1 for d in delta_e_values if d >= 5)
delta_e_gray = delta_e_values[0:5]
delta_e_colorchecker = delta_e_values[5:23]
delta_e_saturated = delta_e_values[23:29]
try:
target_gamma = float(self.sdr_gamma_type_var.get()) if test_type == "sdr_movie" else 2.2
except Exception:
target_gamma = 2.2
accuracy_data = {
"color_patches": color_patches,
"delta_e_values": delta_e_values,
"color_measurements": measured_data,
"avg_delta_e": avg_delta_e,
"max_delta_e": max_delta_e,
"min_delta_e": min_delta_e,
"excellent_count": excellent_count,
"good_count": good_count,
"poor_count": poor_count,
"avg_delta_e_gray": float(np.mean(delta_e_gray)) if delta_e_gray else 0.0,
"avg_delta_e_colorchecker": float(np.mean(delta_e_colorchecker)) if delta_e_colorchecker else 0.0,
"avg_delta_e_saturated": float(np.mean(delta_e_saturated)) if delta_e_saturated else 0.0,
"target_gamma": target_gamma,
}
self.results.add_intermediate_data("accuracy", "measured", measured_data)
self.results.set_test_item_result("accuracy", accuracy_data)
self.plot_accuracy(accuracy_data, test_type)
self.save_btn.config(state=tk.NORMAL)
self.status_var.set("模拟测试完成")
self.log_gui.log("模拟测试完成,结果已显示到 UI", level="success")
self.log_gui.log("=" * 60, level="separator")
messagebox.showinfo("完成", "模拟测试已完成(无需 UCD/CA")
except Exception as e:
self.status_var.set("模拟测试失败")
self.log_gui.log(f"模拟测试失败: {str(e)}", level="error")
import traceback
self.log_gui.log(traceback.format_exc(), level="error")
messagebox.showerror("错误", f"模拟测试失败: {str(e)}")
def on_test_completed(self: "PQAutomationApp"):
"""测试完成后的UI更新"""
self.testing = False
@@ -1310,6 +1530,7 @@ class TestRunnerMixin:
test_cct = test_cct
test_contrast = test_contrast
test_color_accuracy = test_color_accuracy
run_simulation_test = run_simulation_test
on_test_completed = on_test_completed
on_custom_template_test_completed = on_custom_template_test_completed
get_current_test_result = get_current_test_result

View File

@@ -408,20 +408,27 @@ def init_contrast_chart(self: "PQAutomationApp"):
def init_accuracy_chart(self: "PQAutomationApp"):
"""初始化色准图表 - 固定大小,居中显示"""
container = ttk.Frame(self.accuracy_chart_frame)
container.pack(expand=True)
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=container)
self.accuracy_canvas = FigureCanvasTkAgg(self.accuracy_fig, master=plot_container)
canvas_widget = self.accuracy_canvas.get_tk_widget()
canvas_widget.pack()
canvas_widget.config(width=1000, height=600)
canvas_widget.pack_propagate(False)
canvas_widget.pack(fill=tk.BOTH, expand=True)
self.accuracy_ax = self.accuracy_fig.add_subplot(111)
self.accuracy_ax.set_xlim(0, 1)
@@ -439,6 +446,134 @@ def init_accuracy_chart(self: "PQAutomationApp"):
)
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"):
"""清空所有图表"""
@@ -735,6 +870,9 @@ def clear_chart(self: "PQAutomationApp"):
self.accuracy_canvas.draw()
# 清空色准明细表格
self.clear_accuracy_result_table()
def update_chart_tabs_state(self: "PQAutomationApp"):
"""根据测试项目复选框状态动态增删图表 Tab保持规范顺序
@@ -888,6 +1026,9 @@ class ChartFrameMixin:
init_cct_chart = init_cct_chart
init_contrast_chart = init_contrast_chart
init_accuracy_chart = init_accuracy_chart
_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

View File

@@ -634,16 +634,6 @@ def recalculate_gamut(self: "PQAutomationApp"):
# 10. 重新绘制色域图
self.plot_gamut(rgb_data, coverage_xy, test_type)
self.log_gui.log("色域图已重新绘制", level="success")
self.log_gui.log("=" * 50, level="separator")
messagebox.showinfo(
"成功",
f"色域图已根据新参考标准 {reference_standard} 重新绘制!\n\n"
f"XY 覆盖率: {coverage_xy:.1f}%\n"
f"UV 覆盖率: {coverage_uv:.1f}%",
)
except Exception as e:
self.log_gui.log(f"重新计算失败: {str(e)}", level="error")
self.log_gui.log(traceback.format_exc(), level="error")

View File

@@ -590,6 +590,14 @@ def create_operation_frame(self: "PQAutomationApp"):
)
self.start_btn.pack(side=tk.LEFT, padx=5)
self.simulate_btn = ttk.Button(
operation_frame,
text="模拟测试",
command=self.run_simulation_test,
style="warning.TButton",
)
self.simulate_btn.pack(side=tk.LEFT, padx=5)
self.stop_btn = ttk.Button(
operation_frame,
text="停止测试",