Files
pqAutomationApp/app/plots/plot_accuracy.py

414 lines
12 KiB
Python
Raw Normal View History

2026-04-20 10:00:44 +08:00
"""色准测试结果绘制。
2026-05-27 14:58:44 +08:00
布局
- 左侧大尺寸 ColorChecker 条形图每个条形使用对应颜色
- 右侧CIE 1976 u'v' 色度图目标点/实测点/偏移连线
2026-04-20 10:00:44 +08:00
"""
2026-05-27 14:58:44 +08:00
from typing import TYPE_CHECKING
2026-04-20 10:00:44 +08:00
from matplotlib.patches import Rectangle
2026-05-27 14:58:44 +08:00
from matplotlib.lines import Line2D
from matplotlib.patches import Circle
2026-04-20 10:00:44 +08:00
2026-05-27 14:58:44 +08:00
from app.plots.gamut_background import get_cie1976_background
from app.tests.color_accuracy import get_accuracy_color_standards
from app.pq.color_patch_map import get_patch_color
from app.pq.color_patch_map import get_patch_color_from_xy
2026-04-20 10:00:44 +08:00
if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp
2026-05-27 14:58:44 +08:00
# ============================================================
# 常量
# ============================================================
_COLOR_MAP = {
"White": "#FFFFFF",
"Gray 80": "#E6E6E6",
"Gray 65": "#D1D1D1",
"Gray 50": "#BABABA",
"Gray 35": "#9E9E9E",
"Black": "#000000",
"Dark Skin": "#735242",
"Light Skin": "#C29682",
"Blue Sky": "#5E7A9C",
"Foliage": "#596B42",
"Blue Flower": "#8280B0",
"Bluish Green": "#63BDA8",
"Orange": "#D97829",
"Purplish Blue": "#4A5CA3",
"Moderate Red": "#C25461",
"Purple": "#5C3D6B",
"Yellow Green": "#9EBA40",
"Orange Yellow": "#E6A12E",
"Blue (Legacy)": "#333D96",
"Green (Legacy)": "#479447",
"Red (Legacy)": "#B0303B",
"Yellow (Legacy)": "#EDC721",
"Magenta (Legacy)": "#BA5491",
"Cyan (Legacy)": "#0085A3",
"100% Red": "#FF0000",
"100% Green": "#00FF00",
"100% Blue": "#0000FF",
"100% Cyan": "#00FFFF",
"100% Magenta": "#FF00FF",
"100% Yellow": "#FFFF00",
}
def _xy_to_uv(x: float, y: float):
"""CIE 1931 xy → CIE 1976 u'v'"""
denom = -2.0 * x + 12.0 * y + 3.0
if abs(denom) < 1e-10:
return 0.0, 0.0
return (4.0 * x) / denom, (9.0 * y) / denom
# ============================================================
# 子图:左侧 Calman 风格面板
# ============================================================
2026-05-28 16:41:52 +08:00
def _draw_left_panel(ax, color_patches, delta_e_values, font_scale=1.0, dark_mode=False):
"""左侧仅保留大条形图"""
2026-05-27 14:58:44 +08:00
ax.clear()
n = len(color_patches)
if n == 0:
ax.set_axis_off()
return
y_pos = list(range(n))
bar_colors = [_COLOR_MAP.get(name, "#888888") for name in color_patches]
ax.barh(
y_pos,
delta_e_values,
height=0.72,
color=bar_colors,
edgecolor="#202020",
linewidth=0.5,
2026-05-27 14:58:44 +08:00
zorder=3,
)
2026-05-28 16:41:52 +08:00
text_color = "#F3F5F7" if dark_mode else "#111111"
bg_color = "#0F1115" if dark_mode else "#FFFFFF"
spine_color = "#8C8F94" if dark_mode else "#9A9A9A"
2026-05-27 14:58:44 +08:00
ax.set_yticks(y_pos)
2026-05-28 16:41:52 +08:00
ax.set_yticklabels(color_patches, fontsize=max(5, 7 * font_scale), color=text_color)
2026-05-27 14:58:44 +08:00
ax.invert_yaxis()
x_max = max(15.0, max(delta_e_values) * 1.15)
ax.set_xlim(0, x_max)
2026-05-28 16:41:52 +08:00
ax.tick_params(axis="x", labelsize=max(6, 8 * font_scale), colors=text_color)
2026-05-27 14:58:44 +08:00
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)
2026-05-28 16:41:52 +08:00
ax.set_facecolor(bg_color)
2026-05-27 14:58:44 +08:00
for spine in ax.spines.values():
2026-05-28 16:41:52 +08:00
spine.set_color(spine_color)
2026-05-27 14:58:44 +08:00
spine.set_linewidth(0.9)
# ============================================================
# 子图CIE 1976 u'v' 色度图(目标 vs 实测)
# ============================================================
2026-05-28 16:41:52 +08:00
def _draw_uv_diagram(ax, color_patches, measurements, standards, font_scale=1.0, dark_mode=False):
2026-05-27 14:58:44 +08:00
"""绘制 CIE 1976 u'v' 上的色准对比。"""
ax.clear()
try:
bg, bbox = get_cie1976_background()
xmin, xmax, ymin, ymax = bbox
ax.imshow(
bg,
extent=(xmin, xmax, ymin, ymax),
origin="lower",
interpolation="bicubic",
zorder=0,
aspect="auto",
2026-05-27 14:58:44 +08:00
)
ax.set_xlim(xmin, xmax)
ax.set_ylim(ymin, ymax)
except Exception:
ax.set_xlim(0.0, 0.65)
ax.set_ylim(0.0, 0.60)
2026-05-28 16:41:52 +08:00
text_color = "#F3F5F7" if dark_mode else "#111111"
sub_text_color = "#D3D7DD" if dark_mode else "#222222"
tick_color = "#D3D7DD" if dark_mode else "#222222"
legend_label_color = "#FFF" if dark_mode else "#111"
legend_bg = "#111" if dark_mode else "#FFFFFF"
legend_edge = "#FFF" if dark_mode else "#333"
outer_edge = "#FFFFFF" if dark_mode else "#222222"
2026-05-28 16:41:52 +08:00
ax.set_facecolor("#000" if dark_mode else "#FFFFFF")
2026-05-27 14:58:44 +08:00
ax.set_aspect("equal", adjustable="box")
ax.set_title(
"CIE 1976 u'v'",
fontsize=max(8, 11 * font_scale),
fontweight="bold",
color=text_color,
pad=4,
)
2026-05-28 16:41:52 +08:00
ax.set_xlabel("u'", fontsize=max(7, 9 * font_scale), color=sub_text_color, labelpad=1)
ax.set_ylabel("v'", fontsize=max(7, 9 * font_scale), color=sub_text_color, labelpad=1)
2026-05-28 16:41:52 +08:00
ax.tick_params(axis="both", labelsize=max(6, 8 * font_scale), colors=tick_color)
2026-05-27 14:58:44 +08:00
for sp in ax.spines.values():
2026-05-28 16:41:52 +08:00
sp.set_color(outer_edge)
2026-05-27 14:58:44 +08:00
sp.set_linewidth(0.9)
for name, meas in zip(color_patches, measurements):
2026-05-27 14:58:44 +08:00
if meas is None or len(meas) < 2:
continue
2026-05-27 14:58:44 +08:00
mx, my = meas[0], meas[1]
sxy = standards.get(name)
2026-05-27 14:58:44 +08:00
if sxy is None:
continue
2026-05-27 14:58:44 +08:00
sx, sy = sxy
m_u, m_v = _xy_to_uv(mx, my)
s_u, s_v = _xy_to_uv(sx, sy)
# face = get_patch_color_from_xy(name, (sx, sy)).strip().upper()
face = _COLOR_MAP.get(name, "#888888")
print(name, face)
2026-05-27 14:58:44 +08:00
# 目标点Target 空心方框
2026-05-27 14:58:44 +08:00
ax.scatter(
s_u,
s_v,
s=90,
marker="s",
facecolors="none",
edgecolors=outer_edge,
linewidths=1.6,
zorder=18,
2026-05-27 14:58:44 +08:00
)
# 实测点Actual 彩色实心 + 白色描边
2026-05-27 14:58:44 +08:00
ax.scatter(
[m_u],
[m_v],
s=80,
marker="o",
color=face,
edgecolors=outer_edge,
linewidths=1.2,
zorder=20,
2026-05-27 14:58:44 +08:00
)
# # Δu'v' 偏差连线
# ax.plot(
# [s_u, m_u],
# [s_v, m_v],
# color=face,
# linewidth=1.0,
# alpha=0.8,
# zorder=15,
# )
2026-05-27 14:58:44 +08:00
legend_handles = [
Line2D(
[0],
[0],
marker="s",
linestyle="none",
markerfacecolor="none",
markeredgecolor=outer_edge,
markersize=9,
markeredgewidth=1.4,
label="目标 (Target)",
),
Line2D(
[0],
[0],
marker="o",
linestyle="none",
markerfacecolor="#AAAAAA",
markeredgecolor=outer_edge,
markersize=9,
markeredgewidth=1.2,
label="实测 (Actual)",
),
2026-05-27 14:58:44 +08:00
]
2026-05-27 14:58:44 +08:00
leg = ax.legend(
handles=legend_handles,
loc="lower right",
fontsize=max(6, 8 * font_scale),
framealpha=0.9,
labelcolor=legend_label_color,
2026-05-27 14:58:44 +08:00
)
if leg:
2026-05-28 16:41:52 +08:00
leg.get_frame().set_facecolor(legend_bg)
leg.get_frame().set_edgecolor(legend_edge)
2026-05-27 14:58:44 +08:00
leg.set_zorder(50)
2026-05-28 16:41:52 +08:00
def _draw_result_judgement(ax, accuracy_data, font_scale=1.0, dark_mode=False):
2026-05-27 14:58:44 +08:00
"""底部结果条"""
ax.clear()
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.axis("off")
avg = accuracy_data.get("avg_delta_e", 0.0)
mx = accuracy_data.get("max_delta_e", 0.0)
2026-05-28 16:41:52 +08:00
panel_bg = "#1A1E24" if dark_mode else "#FFFFFF"
panel_edge = "#4A5058" if dark_mode else "#C6C6C6"
text_color = "#F3F5F7" if dark_mode else "#111111"
2026-05-27 14:58:44 +08:00
ax.add_patch(Rectangle(
(0.0, 0.10), 1.0, 0.80,
transform=ax.transAxes,
2026-05-28 16:41:52 +08:00
facecolor=panel_bg, edgecolor=panel_edge, linewidth=1.0,
2026-05-27 14:58:44 +08:00
))
ax.text(
0.03, 0.50,
f"Avg dE2000: {avg:.2f}",
ha="left", va="center",
2026-05-28 16:41:52 +08:00
fontsize=max(11, 20 * font_scale), fontweight="normal", color=text_color,
2026-05-27 14:58:44 +08:00
transform=ax.transAxes,
)
ax.text(
0.52, 0.50,
f"Max dE2000: {mx:.2f}",
ha="left", va="center",
2026-05-28 16:41:52 +08:00
fontsize=max(11, 20 * font_scale), fontweight="normal", color=text_color,
2026-05-27 14:58:44 +08:00
transform=ax.transAxes,
2026-04-20 10:00:44 +08:00
)
2026-05-27 14:58:44 +08:00
# ============================================================
# 主入口
# ============================================================
def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type):
"""绘制色准测试结果 - Calman 风格(色块 + CIE 1976 u'v' + 统计)。"""
fig = self.accuracy_fig
fig.clear()
2026-05-28 16:41:52 +08:00
try:
from app.views.theme_manager import is_dark
dark_mode = is_dark()
except Exception:
dark_mode = False
fig.patch.set_facecolor("#1B1F24" if dark_mode else "#FFFFFF")
# 根据当前画布像素尺寸动态缩放字体,避免窗口缩小时文字挤压重叠。
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
2026-05-27 14:58:44 +08:00
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 []
try:
target_gamma = float(accuracy_data.get("target_gamma", 2.2))
except (TypeError, ValueError):
target_gamma = 2.2
2026-04-20 10:00:44 +08:00
test_type_name = self.get_test_type_name(test_type)
if test_type == "sdr_movie":
title = f"{test_type_name} - 色准测试(全 29色 | Gamma {target_gamma}"
elif test_type == "hdr_movie":
title = f"{test_type_name} - 色准测试(全 29色 | PQ EOTF"
2026-05-27 14:58:44 +08:00
else:
2026-04-20 10:00:44 +08:00
title = f"{test_type_name} - 色准测试(全 29色 | Gamma {target_gamma}"
2026-05-28 16:41:52 +08:00
title_color = "#F3F5F7" if dark_mode else "#111"
fig.suptitle(
title,
fontsize=max(8, 11 * font_scale),
y=0.975,
fontweight="bold",
2026-05-28 16:41:52 +08:00
color=title_color,
)
2026-04-20 10:00:44 +08:00
2026-05-27 14:58:44 +08:00
gs = fig.add_gridspec(
2, 2,
width_ratios=[1.12, 1.0],
height_ratios=[4.8, 0.48],
2026-05-27 14:58:44 +08:00
left=0.08, right=0.985,
top=0.92, bottom=0.05,
wspace=0.14, hspace=0.08,
2026-04-20 10:00:44 +08:00
)
2026-05-27 14:58:44 +08:00
ax_left = fig.add_subplot(gs[0, 0])
ax_uv = fig.add_subplot(gs[0, 1])
ax_judge = fig.add_subplot(gs[1, :])
2026-04-20 10:00:44 +08:00
2026-05-27 14:58:44 +08:00
# 兼容外部对 self.accuracy_ax 的引用
self.accuracy_ax = ax_judge
2026-04-20 10:00:44 +08:00
2026-05-28 16:41:52 +08:00
_draw_left_panel(
ax_left,
color_patches,
delta_e_values,
font_scale=font_scale,
dark_mode=dark_mode,
)
2026-04-20 10:00:44 +08:00
2026-05-27 14:58:44 +08:00
try:
standards = get_accuracy_color_standards(test_type)
except Exception:
standards = {}
_draw_uv_diagram(
ax_uv,
color_patches,
measurements,
standards,
font_scale=font_scale,
2026-05-28 16:41:52 +08:00
dark_mode=dark_mode,
)
_draw_result_judgement(
ax_judge,
accuracy_data,
font_scale=font_scale,
dark_mode=dark_mode,
)
try:
self.update_accuracy_result_table(accuracy_data, standards)
except Exception:
pass
2026-04-20 10:00:44 +08:00
# Select the tab first so the canvas is visible and winfo_width/height
# return real pixel dimensions before the figure is rendered.
2026-04-20 10:16:31 +08:00
self.chart_notebook.select(self.accuracy_chart_frame)
self.accuracy_canvas.get_tk_widget().update_idletasks()
self.accuracy_canvas.draw()
class PlotAccuracyMixin:
"""由 tools/refactor_to_mixins.py 自动生成。
把本模块的自由函数挂到 PQAutomationApp 便于 F12 跳转与类型推断
"""
plot_accuracy = plot_accuracy