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
|
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
|
2026-04-20 10:00:44 +08:00
|
|
|
|
|
2026-05-27 11:26:28 +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 _grade_color(delta_e: float) -> str:
|
|
|
|
|
|
if delta_e < 3:
|
|
|
|
|
|
return "#1FAE45" # 绿
|
|
|
|
|
|
if delta_e < 5:
|
|
|
|
|
|
return "#E08A00" # 橙
|
|
|
|
|
|
return "#D81B1B" # 红
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 风格面板
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
def _draw_left_panel(ax, color_patches, delta_e_values):
|
|
|
|
|
|
"""左侧仅保留大条形图。"""
|
|
|
|
|
|
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]
|
|
|
|
|
|
edge_colors = [_grade_color(dE) for dE in delta_e_values]
|
|
|
|
|
|
|
|
|
|
|
|
ax.barh(
|
|
|
|
|
|
y_pos,
|
|
|
|
|
|
delta_e_values,
|
|
|
|
|
|
height=0.72,
|
|
|
|
|
|
color=bar_colors,
|
|
|
|
|
|
edgecolor=edge_colors,
|
|
|
|
|
|
linewidth=1.0,
|
|
|
|
|
|
zorder=3,
|
|
|
|
|
|
)
|
2026-05-27 11:26:28 +08:00
|
|
|
|
|
2026-05-27 14:58:44 +08:00
|
|
|
|
ax.set_yticks(y_pos)
|
|
|
|
|
|
ax.set_yticklabels(color_patches, fontsize=7)
|
|
|
|
|
|
ax.invert_yaxis()
|
|
|
|
|
|
|
|
|
|
|
|
x_max = max(15.0, max(delta_e_values) * 1.15)
|
|
|
|
|
|
ax.set_xlim(0, x_max)
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
ax.set_facecolor("#FFFFFF")
|
|
|
|
|
|
for spine in ax.spines.values():
|
|
|
|
|
|
spine.set_color("#9A9A9A")
|
|
|
|
|
|
spine.set_linewidth(0.9)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 子图:CIE 1976 u'v' 色度图(目标 vs 实测)
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
def _draw_uv_diagram(ax, color_patches, measurements, standards):
|
|
|
|
|
|
"""绘制 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="upper", interpolation="bicubic",
|
|
|
|
|
|
zorder=0, aspect="auto",
|
|
|
|
|
|
)
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
ax.set_facecolor("#000")
|
|
|
|
|
|
ax.set_aspect("equal", adjustable="box")
|
|
|
|
|
|
ax.set_title("CIE 1976 u'v'", fontsize=11, 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")
|
|
|
|
|
|
for sp in ax.spines.values():
|
|
|
|
|
|
sp.set_color("#666")
|
|
|
|
|
|
sp.set_linewidth(0.9)
|
|
|
|
|
|
|
|
|
|
|
|
for name, meas in zip(color_patches, measurements):
|
|
|
|
|
|
if meas is None or len(meas) < 2:
|
|
|
|
|
|
continue
|
|
|
|
|
|
mx, my = meas[0], meas[1]
|
|
|
|
|
|
sxy = standards.get(name)
|
|
|
|
|
|
if sxy is None:
|
|
|
|
|
|
continue
|
|
|
|
|
|
sx, sy = sxy
|
|
|
|
|
|
|
|
|
|
|
|
m_u, m_v = _xy_to_uv(mx, my)
|
|
|
|
|
|
s_u, s_v = _xy_to_uv(sx, sy)
|
|
|
|
|
|
|
|
|
|
|
|
face = _COLOR_MAP.get(name, "#FFFFFF")
|
|
|
|
|
|
|
|
|
|
|
|
# 目标点:仅空心方框(不填充标准颜色)
|
|
|
|
|
|
ax.scatter(
|
|
|
|
|
|
[s_u], [s_v],
|
|
|
|
|
|
s=56, marker="s",
|
|
|
|
|
|
facecolors="none", edgecolors="#FFFFFF",
|
|
|
|
|
|
linewidths=1.25, zorder=18,
|
|
|
|
|
|
)
|
|
|
|
|
|
# 实测点:白色外圈 + 内层圆点
|
|
|
|
|
|
ax.scatter(
|
|
|
|
|
|
[m_u], [m_v],
|
|
|
|
|
|
s=52, marker="o",
|
|
|
|
|
|
facecolors="none", edgecolors="#FFFFFF",
|
|
|
|
|
|
linewidths=1.0, zorder=19,
|
|
|
|
|
|
)
|
|
|
|
|
|
ax.scatter(
|
|
|
|
|
|
[m_u], [m_v],
|
|
|
|
|
|
s=24, marker="o",
|
|
|
|
|
|
facecolors=face, edgecolors="#111111",
|
|
|
|
|
|
linewidths=0.85, zorder=20,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
legend_handles = [
|
|
|
|
|
|
Line2D([0], [0], marker="s", linestyle="none",
|
|
|
|
|
|
markerfacecolor="#CCCCCC", markeredgecolor="#FFFFFF",
|
|
|
|
|
|
markersize=7, label="目标 (Target)"),
|
|
|
|
|
|
Line2D([0], [0], marker="o", linestyle="none",
|
|
|
|
|
|
markerfacecolor="#CCCCCC", markeredgecolor="#000000",
|
|
|
|
|
|
markersize=7, label="实测 (Actual)"),
|
|
|
|
|
|
]
|
|
|
|
|
|
leg = ax.legend(
|
|
|
|
|
|
handles=legend_handles,
|
|
|
|
|
|
loc="lower right", fontsize=8,
|
|
|
|
|
|
framealpha=0.88, labelcolor="#FFF",
|
|
|
|
|
|
)
|
|
|
|
|
|
if leg is not None:
|
|
|
|
|
|
leg.get_frame().set_facecolor("#111")
|
|
|
|
|
|
leg.get_frame().set_edgecolor("#FFF")
|
|
|
|
|
|
leg.set_zorder(50)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _draw_result_judgement(ax, accuracy_data):
|
|
|
|
|
|
"""底部结果条"""
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
ax.add_patch(Rectangle(
|
|
|
|
|
|
(0.0, 0.10), 1.0, 0.80,
|
|
|
|
|
|
transform=ax.transAxes,
|
|
|
|
|
|
facecolor="#FFFFFF", edgecolor="#C6C6C6", linewidth=1.0,
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
ax.text(
|
|
|
|
|
|
0.03, 0.50,
|
|
|
|
|
|
f"Avg dE2000: {avg:.2f}",
|
|
|
|
|
|
ha="left", va="center",
|
|
|
|
|
|
fontsize=20, 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",
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
|
|
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-27 14:58:44 +08:00
|
|
|
|
fig.suptitle(title, fontsize=11, y=0.975, fontweight="bold", color="#111")
|
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.0, 0.62],
|
|
|
|
|
|
left=0.08, right=0.985,
|
|
|
|
|
|
top=0.91, bottom=0.06,
|
|
|
|
|
|
wspace=0.14, hspace=0.10,
|
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-27 14:58:44 +08:00
|
|
|
|
_draw_left_panel(ax_left, color_patches, delta_e_values)
|
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)
|
|
|
|
|
|
_draw_result_judgement(ax_judge, accuracy_data)
|
2026-04-20 10:00:44 +08:00
|
|
|
|
|
|
|
|
|
|
self.accuracy_canvas.draw()
|
2026-04-20 10:16:31 +08:00
|
|
|
|
self.chart_notebook.select(self.accuracy_chart_frame)
|
2026-05-27 11:26:28 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PlotAccuracyMixin:
|
|
|
|
|
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
|
|
|
|
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
|
|
|
|
|
"""
|
|
|
|
|
|
plot_accuracy = plot_accuracy
|