2026-05-18 15:57:11 +08:00
|
|
|
|
"""色域图(Gamut)绘制 - Calman 风格。
|
|
|
|
|
|
|
|
|
|
|
|
架构:**图像渲染层** 与 **基础数据/框架层** 分离
|
|
|
|
|
|
------------------------------------------------
|
|
|
|
|
|
- 图像渲染层(重):CIE 1931 / 1976 谱迹色域底图。
|
|
|
|
|
|
由 `app.plots.gamut_background` 通过 colour-science 离屏渲染一次,
|
|
|
|
|
|
结果以 numpy RGBA 数组缓存在内存与磁盘(settings/cache/),后续直接
|
|
|
|
|
|
`ax.imshow(bg, extent=bbox)` 复用 → 主线程绘制开销可忽略。
|
|
|
|
|
|
- 基础数据/框架层(轻):参考色域三角形、实测色域三角形、顶点标签、
|
|
|
|
|
|
覆盖率信息框等矢量元素,每次绘制都在真实色度坐标系上重画。
|
|
|
|
|
|
|
|
|
|
|
|
视觉风格:参照 Calman colorspace 显示:
|
|
|
|
|
|
- 当前选中的参考标准:亮色实线 + 顶点空心方框;
|
|
|
|
|
|
- 其他参考标准:半透明虚线(便于对比,不喧宾夺主);
|
|
|
|
|
|
- 实测色域:红色粗边 + 淡红填充 + 顶点圆点 + 浮动坐标标签;
|
|
|
|
|
|
- 右下角白底红字覆盖率信息框。
|
2026-04-20 10:00:44 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
2026-05-18 15:57:11 +08:00
|
|
|
|
from matplotlib.patches import Polygon
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
from matplotlib.patches import PathPatch
|
|
|
|
|
|
from matplotlib.path import Path
|
2026-04-20 10:00:44 +08:00
|
|
|
|
|
|
|
|
|
|
import algorithm.pq_algorithm as pq_algorithm
|
2026-05-18 15:57:11 +08:00
|
|
|
|
from app.plots.gamut_background import (
|
|
|
|
|
|
get_cie1931_background,
|
|
|
|
|
|
get_cie1976_background,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-27 11:26:28 +08:00
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
|
from pqAutomationApp import PQAutomationApp
|
|
|
|
|
|
|
2026-05-18 15:57:11 +08:00
|
|
|
|
|
|
|
|
|
|
# ============ 参考色域定义(CIE 1931 xy)============
|
|
|
|
|
|
_REF_GAMUTS_XY = {
|
|
|
|
|
|
"BT.601": [(0.6300, 0.3400), (0.3100, 0.5950), (0.1550, 0.0700)],
|
|
|
|
|
|
"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)],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_REF_COLORS = {
|
|
|
|
|
|
"BT.601": "#FBD985",
|
|
|
|
|
|
"BT.709": "#FFFFFF",
|
|
|
|
|
|
"DCI-P3": "#6AA2F7",
|
|
|
|
|
|
"BT.2020": "#73FC9C",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 坐标转换
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
def _xy_to_uv(x, y):
|
|
|
|
|
|
"""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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ref_gamut_uv(name):
|
|
|
|
|
|
return [_xy_to_uv(x, y) for x, y in _REF_GAMUTS_XY[name]]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 数据/框架层绘制原语
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
def _draw_reference_triangle(ax, vertices, color, *, is_current, label):
|
|
|
|
|
|
"""参考色域三角形"""
|
|
|
|
|
|
xs = [p[0] for p in vertices] + [vertices[0][0]]
|
|
|
|
|
|
ys = [p[1] for p in vertices] + [vertices[0][1]]
|
|
|
|
|
|
|
|
|
|
|
|
if is_current:
|
|
|
|
|
|
poly = Polygon(
|
|
|
|
|
|
vertices, closed=True,
|
|
|
|
|
|
facecolor=(1, 1, 1, 0.18), # 半透明白
|
|
|
|
|
|
edgecolor=color, linewidth=2.2, zorder=8,
|
|
|
|
|
|
)
|
|
|
|
|
|
ax.add_patch(poly)
|
|
|
|
|
|
ax.plot(
|
|
|
|
|
|
xs, ys,
|
|
|
|
|
|
color=color, linewidth=2.2, linestyle="-",
|
|
|
|
|
|
label=label, zorder=9,
|
|
|
|
|
|
)
|
|
|
|
|
|
ax.scatter(
|
|
|
|
|
|
xs[:-1], ys[:-1],
|
|
|
|
|
|
s=60, facecolors="none", edgecolors=color,
|
|
|
|
|
|
linewidths=2, marker="s", zorder=10,
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
ax.plot(
|
|
|
|
|
|
xs, ys,
|
|
|
|
|
|
color=color, linewidth=1.2, linestyle="--",
|
|
|
|
|
|
alpha=0.55, label=label, zorder=5,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _draw_measured_triangle(ax, vertices, *, uv_space=False):
|
|
|
|
|
|
xs = [p[0] for p in vertices] + [vertices[0][0]]
|
|
|
|
|
|
ys = [p[1] for p in vertices] + [vertices[0][1]]
|
|
|
|
|
|
|
|
|
|
|
|
# 半透明红色填充
|
|
|
|
|
|
poly = Polygon(
|
|
|
|
|
|
vertices, closed=True,
|
|
|
|
|
|
facecolor=(1.0, 0.1, 0.1, 0),
|
|
|
|
|
|
edgecolor="#FF2A2A",
|
|
|
|
|
|
linewidth=2.8, zorder=12,
|
|
|
|
|
|
joinstyle="round"
|
|
|
|
|
|
)
|
|
|
|
|
|
ax.add_patch(poly)
|
|
|
|
|
|
|
|
|
|
|
|
# 顶点(白边红心)
|
|
|
|
|
|
ax.scatter(
|
|
|
|
|
|
xs[:-1], ys[:-1],
|
|
|
|
|
|
s=60, facecolors="#FF2A2A",
|
|
|
|
|
|
edgecolors="white", linewidths=1.5,
|
|
|
|
|
|
marker="o", zorder=13,
|
|
|
|
|
|
)
|
|
|
|
|
|
# for (x, y), name in zip(vertices, label_prefix):
|
|
|
|
|
|
# dx, dy = x - cx, y - cy
|
|
|
|
|
|
# norm = max(1e-6, (dx * dx + dy * dy) ** 0.5)
|
|
|
|
|
|
# ox = dx / norm * offset_pix
|
|
|
|
|
|
# oy = dy / norm * offset_pix
|
|
|
|
|
|
|
|
|
|
|
|
# ax.annotate(
|
|
|
|
|
|
# f"{name} ({x:.3f}, {y:.3f})",
|
|
|
|
|
|
# xy=(x, y),
|
|
|
|
|
|
# xytext=(ox, oy),
|
|
|
|
|
|
# textcoords="offset points",
|
|
|
|
|
|
# fontsize=8.5, color="white", fontweight="bold",
|
|
|
|
|
|
# ha="center", va="center",
|
|
|
|
|
|
# bbox=dict(
|
|
|
|
|
|
# boxstyle="round,pad=0.35",
|
|
|
|
|
|
# facecolor="#D81B1B",
|
|
|
|
|
|
# edgecolor="white",
|
|
|
|
|
|
# linewidth=1.2,
|
|
|
|
|
|
# alpha=0.95,
|
|
|
|
|
|
# ),
|
|
|
|
|
|
# arrowprops=dict(
|
|
|
|
|
|
# arrowstyle="-", color="#FF1F1F", lw=1.2, alpha=0.9,
|
|
|
|
|
|
# ),
|
|
|
|
|
|
# zorder=12,
|
|
|
|
|
|
# clip_on=False,
|
|
|
|
|
|
# )
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-28 16:41:52 +08:00
|
|
|
|
def _draw_coverage_box(ax, x_pos, y_pos, current_ref, coverage, *, dark_mode):
|
|
|
|
|
|
text_color = "#FFF" if dark_mode else "#111"
|
|
|
|
|
|
box_face = "#111" if dark_mode else "#FFFFFF"
|
|
|
|
|
|
box_edge = "#FFF" if dark_mode else "#333"
|
2026-05-18 15:57:11 +08:00
|
|
|
|
ax.text(
|
|
|
|
|
|
x_pos, y_pos,
|
|
|
|
|
|
f"{current_ref}\n覆盖率: {coverage:.1f}%",
|
|
|
|
|
|
ha="right", va="bottom",
|
|
|
|
|
|
fontsize=11, fontweight="bold",
|
2026-05-28 16:41:52 +08:00
|
|
|
|
color=text_color,
|
2026-05-18 15:57:11 +08:00
|
|
|
|
bbox=dict(
|
|
|
|
|
|
boxstyle="round,pad=0.38",
|
2026-05-28 16:41:52 +08:00
|
|
|
|
facecolor=box_face,
|
|
|
|
|
|
edgecolor=box_edge,
|
2026-05-18 15:57:11 +08:00
|
|
|
|
linewidth=1.7,
|
|
|
|
|
|
alpha=0.98,
|
|
|
|
|
|
),
|
|
|
|
|
|
zorder=30,
|
|
|
|
|
|
)
|
2026-04-20 10:00:44 +08:00
|
|
|
|
|
2026-05-28 16:41:52 +08:00
|
|
|
|
def _style_axes(ax, *, title, xlabel, ylabel, xlim, ylim, dark_mode):
|
|
|
|
|
|
text = "#F4F6F8" if dark_mode else "#111"
|
|
|
|
|
|
grid = "#444" if dark_mode else "#B8BDC3"
|
|
|
|
|
|
spine_color = "#888" if dark_mode else "#666"
|
|
|
|
|
|
ax.set_facecolor("#000" if dark_mode else "#FFFFFF")
|
|
|
|
|
|
ax.set_title(title, fontsize=12, fontweight="bold", color=text, pad=8)
|
|
|
|
|
|
ax.set_xlabel(xlabel, fontsize=10, color=text)
|
|
|
|
|
|
ax.set_ylabel(ylabel, fontsize=10, color=text)
|
2026-05-18 15:57:11 +08:00
|
|
|
|
ax.set_xlim(*xlim)
|
|
|
|
|
|
ax.set_ylim(*ylim)
|
|
|
|
|
|
ax.set_aspect("equal", adjustable="datalim")
|
2026-05-28 16:41:52 +08:00
|
|
|
|
ax.grid(True, linestyle=":", linewidth=0.7, color=grid, alpha=0.32)
|
|
|
|
|
|
ax.tick_params(axis="both", labelsize=9, colors=text)
|
2026-05-18 15:57:11 +08:00
|
|
|
|
for spine in ax.spines.values():
|
2026-05-28 16:41:52 +08:00
|
|
|
|
spine.set_color(spine_color)
|
2026-05-18 15:57:11 +08:00
|
|
|
|
spine.set_linewidth(0.8)
|
|
|
|
|
|
ax.set_clip_on(False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _blit_background(ax, background, bbox):
|
|
|
|
|
|
"""渲染层贴底:将预渲染的谱迹底图贴到真实色度坐标。"""
|
|
|
|
|
|
xmin, xmax, ymin, ymax = bbox
|
|
|
|
|
|
ax.imshow(
|
|
|
|
|
|
background,
|
|
|
|
|
|
extent=(xmin, xmax, ymin, ymax),
|
|
|
|
|
|
origin="upper", # canvas.buffer_rgba 行 0 为顶部
|
|
|
|
|
|
interpolation="bicubic",
|
|
|
|
|
|
zorder=0,
|
|
|
|
|
|
aspect="auto", # 由 _style_axes 的 set_aspect("equal") 控制
|
|
|
|
|
|
)
|
2026-04-20 10:00:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-18 15:57:11 +08:00
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 主入口
|
|
|
|
|
|
# ============================================================
|
2026-04-20 10:00:44 +08:00
|
|
|
|
|
2026-05-27 11:26:28 +08:00
|
|
|
|
def plot_gamut(self: "PQAutomationApp", results, coverage, test_type):
|
2026-05-18 15:57:11 +08:00
|
|
|
|
"""绘制色域图(图像层 + 框架层分离架构)。"""
|
2026-04-20 10:00:44 +08:00
|
|
|
|
|
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
|
|
|
|
|
|
|
2026-05-18 15:57:11 +08:00
|
|
|
|
ax_xy = self.gamut_ax_xy
|
|
|
|
|
|
ax_uv = self.gamut_ax_uv
|
|
|
|
|
|
|
|
|
|
|
|
ax_xy.clear()
|
|
|
|
|
|
ax_uv.clear()
|
2026-05-28 16:41:52 +08:00
|
|
|
|
# 全局背景跟随浅/深色主题
|
|
|
|
|
|
self.gamut_fig.patch.set_facecolor("#0D1014" if dark_mode else "#FFFFFF")
|
2026-04-20 10:00:44 +08:00
|
|
|
|
|
2026-04-21 15:31:48 +08:00
|
|
|
|
# ========== 读取用户选择的参考标准 ==========
|
2026-04-20 10:00:44 +08:00
|
|
|
|
if test_type == "screen_module":
|
|
|
|
|
|
current_ref = self.screen_gamut_ref_var.get()
|
|
|
|
|
|
elif test_type == "sdr_movie":
|
|
|
|
|
|
current_ref = self.sdr_gamut_ref_var.get()
|
|
|
|
|
|
elif test_type == "hdr_movie":
|
|
|
|
|
|
current_ref = self.hdr_gamut_ref_var.get()
|
|
|
|
|
|
else:
|
|
|
|
|
|
current_ref = "DCI-P3"
|
|
|
|
|
|
|
2026-05-18 15:57:11 +08:00
|
|
|
|
if current_ref not in _REF_GAMUTS_XY:
|
|
|
|
|
|
self.log_gui.log(f"未知参考标准 '{current_ref}',使用 DCI-P3", level="error")
|
|
|
|
|
|
current_ref = "DCI-P3"
|
2026-04-20 10:00:44 +08:00
|
|
|
|
|
2026-05-18 15:57:11 +08:00
|
|
|
|
# ========== 重新计算 xy 覆盖率 ==========
|
|
|
|
|
|
xy_coverage = coverage
|
|
|
|
|
|
uv_coverage = 0.0
|
|
|
|
|
|
measured_xy = None
|
2026-04-20 10:00:44 +08:00
|
|
|
|
|
2026-05-18 15:57:11 +08:00
|
|
|
|
if len(results) >= 3:
|
|
|
|
|
|
measured_xy = [(float(r[0]), float(r[1])) for r in results[:3]]
|
|
|
|
|
|
try:
|
2026-04-20 10:00:44 +08:00
|
|
|
|
if current_ref == "BT.2020":
|
2026-05-18 15:57:11 +08:00
|
|
|
|
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_BT2020(measured_xy)
|
2026-04-20 10:00:44 +08:00
|
|
|
|
elif current_ref == "BT.709":
|
2026-05-18 15:57:11 +08:00
|
|
|
|
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_BT709(measured_xy)
|
2026-04-20 10:00:44 +08:00
|
|
|
|
elif current_ref == "DCI-P3":
|
2026-05-18 15:57:11 +08:00
|
|
|
|
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_DCIP3(measured_xy)
|
2026-04-20 10:00:44 +08:00
|
|
|
|
elif current_ref == "BT.601":
|
2026-05-18 15:57:11 +08:00
|
|
|
|
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_BT601(measured_xy)
|
2026-04-20 10:00:44 +08:00
|
|
|
|
self.log_gui.log(
|
2026-05-18 15:57:11 +08:00
|
|
|
|
f"XY 空间覆盖率({current_ref}): {xy_coverage:.1f}%", level="success"
|
2026-04-20 10:00:44 +08:00
|
|
|
|
)
|
2026-05-18 15:57:11 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
self.log_gui.log(f"重新计算 XY 覆盖率失败: {str(e)}", level="error")
|
|
|
|
|
|
xy_coverage = coverage
|
|
|
|
|
|
|
|
|
|
|
|
# 需要叠加的次要参考色域
|
|
|
|
|
|
other_refs = [
|
|
|
|
|
|
r for r in _REF_GAMUTS_XY.keys()
|
|
|
|
|
|
if r != current_ref
|
|
|
|
|
|
and (r != "BT.601" or test_type == "sdr_movie")
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 左图:CIE 1931 xy
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
try:
|
|
|
|
|
|
bg_xy, bbox_xy = get_cie1931_background()
|
|
|
|
|
|
_blit_background(ax_xy, bg_xy, bbox_xy)
|
|
|
|
|
|
_style_axes(
|
|
|
|
|
|
ax_xy,
|
|
|
|
|
|
title="CIE 1931 xy",
|
|
|
|
|
|
xlabel="x", ylabel="y",
|
|
|
|
|
|
xlim=(bbox_xy[0], bbox_xy[1]),
|
|
|
|
|
|
ylim=(bbox_xy[2], bbox_xy[3]),
|
2026-05-28 16:41:52 +08:00
|
|
|
|
dark_mode=dark_mode,
|
2026-05-18 15:57:11 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
for ref_name in other_refs:
|
|
|
|
|
|
_draw_reference_triangle(
|
|
|
|
|
|
ax_xy, _REF_GAMUTS_XY[ref_name],
|
|
|
|
|
|
_REF_COLORS[ref_name],
|
|
|
|
|
|
is_current=False, label=ref_name,
|
2026-04-20 10:00:44 +08:00
|
|
|
|
)
|
2026-05-18 15:57:11 +08:00
|
|
|
|
_draw_reference_triangle(
|
|
|
|
|
|
ax_xy, _REF_GAMUTS_XY[current_ref],
|
|
|
|
|
|
_REF_COLORS[current_ref],
|
|
|
|
|
|
is_current=True, label=f"{current_ref} (参考)",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if measured_xy is not None:
|
|
|
|
|
|
r_xy, g_xy, b_xy = measured_xy
|
|
|
|
|
|
self.log_gui.log(
|
|
|
|
|
|
f"测量色域: R({r_xy[0]:.4f},{r_xy[1]:.4f}) "
|
|
|
|
|
|
f"G({g_xy[0]:.4f},{g_xy[1]:.4f}) B({b_xy[0]:.4f},{b_xy[1]:.4f})",
|
|
|
|
|
|
level="info",
|
2026-04-20 10:00:44 +08:00
|
|
|
|
)
|
2026-05-18 15:57:11 +08:00
|
|
|
|
_draw_measured_triangle(ax_xy, measured_xy, uv_space=False)
|
|
|
|
|
|
|
|
|
|
|
|
_draw_coverage_box(
|
2026-05-28 16:41:52 +08:00
|
|
|
|
ax_xy, bbox_xy[1] - 0.02, bbox_xy[2] + 0.02, current_ref, xy_coverage,
|
|
|
|
|
|
dark_mode=dark_mode,
|
2026-05-18 15:57:11 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 暗化三角形外部区域(黑色半透明遮罩)
|
|
|
|
|
|
x0, x1 = bbox_xy[0], bbox_xy[1]
|
|
|
|
|
|
y0, y1 = bbox_xy[2], bbox_xy[3]
|
|
|
|
|
|
# 多边形路径:外框+三角形
|
|
|
|
|
|
verts = [
|
|
|
|
|
|
(x0, y0), (x0, y1), (x1, y1), (x1, y0), (x0, y0),
|
|
|
|
|
|
*_REF_GAMUTS_XY[current_ref], _REF_GAMUTS_XY[current_ref][0]
|
|
|
|
|
|
]
|
|
|
|
|
|
codes = [Path.MOVETO] + [Path.LINETO]*3 + [Path.CLOSEPOLY]
|
|
|
|
|
|
codes += [Path.MOVETO] + [Path.LINETO]*2 + [Path.CLOSEPOLY]
|
|
|
|
|
|
path = Path(verts, codes)
|
2026-05-28 16:41:52 +08:00
|
|
|
|
mask_face = (0, 0, 0, 0.65) if dark_mode else (1, 1, 1, 0.50)
|
|
|
|
|
|
patch = PathPatch(path, facecolor=mask_face, lw=0, zorder=7)
|
2026-05-18 15:57:11 +08:00
|
|
|
|
ax_xy.add_patch(patch)
|
|
|
|
|
|
|
|
|
|
|
|
legend = ax_xy.legend(
|
|
|
|
|
|
loc="upper right", fontsize=8.5,
|
|
|
|
|
|
framealpha=0.0, edgecolor="#000", fancybox=True,
|
2026-05-28 16:41:52 +08:00
|
|
|
|
labelcolor="#FFF" if dark_mode else "#111"
|
2026-05-18 15:57:11 +08:00
|
|
|
|
)
|
|
|
|
|
|
legend.set_zorder(200)
|
2026-05-28 16:41:52 +08:00
|
|
|
|
legend.get_frame().set_facecolor("#000" if dark_mode else "#FFFFFF")
|
|
|
|
|
|
legend.get_frame().set_alpha(0.5 if dark_mode else 0.78)
|
|
|
|
|
|
legend.get_frame().set_edgecolor("#FFF" if dark_mode else "#333")
|
2026-05-18 15:57:11 +08:00
|
|
|
|
ax_xy.add_artist(legend)
|
2026-04-20 10:00:44 +08:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2026-04-21 15:31:48 +08:00
|
|
|
|
self.log_gui.log(f"XY 图绘制失败: {str(e)}", level="error")
|
2026-04-20 10:00:44 +08:00
|
|
|
|
import traceback
|
2026-04-21 15:31:48 +08:00
|
|
|
|
self.log_gui.log(traceback.format_exc(), level="error")
|
2026-04-20 10:00:44 +08:00
|
|
|
|
|
2026-05-18 15:57:11 +08:00
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 右图:CIE 1976 u'v'
|
|
|
|
|
|
# ============================================================
|
2026-04-20 10:00:44 +08:00
|
|
|
|
try:
|
2026-05-18 15:57:11 +08:00
|
|
|
|
bg_uv, bbox_uv = get_cie1976_background()
|
|
|
|
|
|
_blit_background(ax_uv, bg_uv, bbox_uv)
|
|
|
|
|
|
_style_axes(
|
|
|
|
|
|
ax_uv,
|
|
|
|
|
|
title="CIE 1976 u'v'",
|
|
|
|
|
|
xlabel="u'", ylabel="v'",
|
|
|
|
|
|
xlim=(bbox_uv[0], bbox_uv[1]),
|
|
|
|
|
|
ylim=(bbox_uv[2], bbox_uv[3]),
|
2026-05-28 16:41:52 +08:00
|
|
|
|
dark_mode=dark_mode,
|
2026-05-18 15:57:11 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
measured_uv = None
|
|
|
|
|
|
if measured_xy is not None:
|
|
|
|
|
|
measured_uv = [_xy_to_uv(x, y) for x, y in measured_xy]
|
2026-04-20 10:00:44 +08:00
|
|
|
|
try:
|
|
|
|
|
|
uv_coverage = pq_algorithm.calculate_uv_gamut_coverage(
|
2026-05-18 15:57:11 +08:00
|
|
|
|
[list(uv) for uv in measured_uv], reference=current_ref
|
2026-04-20 10:00:44 +08:00
|
|
|
|
)
|
|
|
|
|
|
self.log_gui.log(
|
2026-05-18 15:57:11 +08:00
|
|
|
|
f"UV 空间覆盖率({current_ref}): {uv_coverage:.1f}%",
|
|
|
|
|
|
level="success",
|
|
|
|
|
|
)
|
2026-04-20 10:00:44 +08:00
|
|
|
|
except Exception as e:
|
2026-04-21 15:31:48 +08:00
|
|
|
|
self.log_gui.log(f"计算 UV 覆盖率失败: {str(e)}", level="error")
|
2026-04-20 10:00:44 +08:00
|
|
|
|
uv_coverage = 0.0
|
|
|
|
|
|
|
2026-05-18 15:57:11 +08:00
|
|
|
|
for ref_name in other_refs:
|
|
|
|
|
|
_draw_reference_triangle(
|
|
|
|
|
|
ax_uv, _ref_gamut_uv(ref_name),
|
|
|
|
|
|
_REF_COLORS[ref_name],
|
|
|
|
|
|
is_current=False, label=ref_name,
|
2026-04-20 10:00:44 +08:00
|
|
|
|
)
|
2026-05-18 15:57:11 +08:00
|
|
|
|
_draw_reference_triangle(
|
|
|
|
|
|
ax_uv, _ref_gamut_uv(current_ref),
|
|
|
|
|
|
_REF_COLORS[current_ref],
|
|
|
|
|
|
is_current=True, label=f"{current_ref} (参考)",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if measured_uv is not None:
|
|
|
|
|
|
_draw_measured_triangle(ax_uv, measured_uv, uv_space=True)
|
|
|
|
|
|
|
|
|
|
|
|
_draw_coverage_box(
|
2026-05-28 16:41:52 +08:00
|
|
|
|
ax_uv, bbox_uv[1] - 0.015, bbox_uv[2] + 0.015, current_ref, uv_coverage,
|
|
|
|
|
|
dark_mode=dark_mode,
|
2026-05-18 15:57:11 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
u0, u1 = bbox_uv[0], bbox_uv[1]
|
|
|
|
|
|
v0, v1 = bbox_uv[2], bbox_uv[3]
|
|
|
|
|
|
verts = [
|
|
|
|
|
|
(u0, v0), (u0, v1), (u1, v1), (u1, v0), (u0, v0),
|
|
|
|
|
|
*_ref_gamut_uv(current_ref), _ref_gamut_uv(current_ref)[0]
|
|
|
|
|
|
]
|
|
|
|
|
|
codes = [Path.MOVETO] + [Path.LINETO]*3 + [Path.CLOSEPOLY]
|
|
|
|
|
|
codes += [Path.MOVETO] + [Path.LINETO]*2 + [Path.CLOSEPOLY]
|
|
|
|
|
|
path = Path(verts, codes)
|
2026-05-28 16:41:52 +08:00
|
|
|
|
mask_face = (0, 0, 0, 0.65) if dark_mode else (1, 1, 1, 0.50)
|
|
|
|
|
|
patch = PathPatch(path, facecolor=mask_face, lw=0, zorder=7)
|
2026-05-18 15:57:11 +08:00
|
|
|
|
ax_uv.add_patch(patch)
|
|
|
|
|
|
|
|
|
|
|
|
legend_uv = ax_uv.legend(
|
|
|
|
|
|
loc="upper right", fontsize=8.5,
|
|
|
|
|
|
framealpha=0.0, edgecolor="#000", fancybox=True,
|
2026-05-28 16:41:52 +08:00
|
|
|
|
labelcolor="#FFF" if dark_mode else "#111"
|
2026-05-18 15:57:11 +08:00
|
|
|
|
)
|
|
|
|
|
|
legend_uv.set_zorder(200)
|
2026-05-28 16:41:52 +08:00
|
|
|
|
legend_uv.get_frame().set_facecolor("#000" if dark_mode else "#FFFFFF")
|
|
|
|
|
|
legend_uv.get_frame().set_alpha(0.72 if dark_mode else 0.82)
|
|
|
|
|
|
legend_uv.get_frame().set_edgecolor("#FFF" if dark_mode else "#333")
|
2026-05-18 15:57:11 +08:00
|
|
|
|
ax_uv.add_artist(legend_uv)
|
2026-04-20 10:00:44 +08:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2026-04-21 15:31:48 +08:00
|
|
|
|
self.log_gui.log(f"UV 图绘制失败: {str(e)}", level="error")
|
2026-04-20 10:00:44 +08:00
|
|
|
|
import traceback
|
2026-04-21 15:31:48 +08:00
|
|
|
|
self.log_gui.log(traceback.format_exc(), level="error")
|
2026-04-20 10:00:44 +08:00
|
|
|
|
|
|
|
|
|
|
# ========== 总标题 ==========
|
|
|
|
|
|
test_type_name = self.get_test_type_name(test_type)
|
|
|
|
|
|
self.gamut_fig.suptitle(
|
2026-05-18 15:57:11 +08:00
|
|
|
|
f"{test_type_name} - 色域测试",
|
|
|
|
|
|
fontsize=12, y=0.98, fontweight="bold",
|
2026-04-20 10:00:44 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self.gamut_canvas.draw()
|
2026-04-20 10:16:31 +08:00
|
|
|
|
self.chart_notebook.select(self.gamut_chart_frame)
|
2026-04-20 10:00:44 +08:00
|
|
|
|
|
2026-05-18 15:57:11 +08:00
|
|
|
|
# 同步工具栏按钮选中状态
|
|
|
|
|
|
if hasattr(self, "sync_gamut_toolbar"):
|
|
|
|
|
|
self.sync_gamut_toolbar()
|
|
|
|
|
|
|
2026-04-21 15:31:48 +08:00
|
|
|
|
self.log_gui.log("色域图绘制完成", level="success")
|
2026-05-27 11:26:28 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PlotGamutMixin:
|
|
|
|
|
|
"""由 tools/refactor_to_mixins.py 自动生成。
|
|
|
|
|
|
把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。
|
|
|
|
|
|
"""
|
|
|
|
|
|
plot_gamut = plot_gamut
|