修改色域画图及重绘方式
This commit is contained in:
165
app/plots/gamut_background.py
Normal file
165
app/plots/gamut_background.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""CIE 色度图底图渲染与缓存。
|
||||
|
||||
将"重型图像渲染"(colour-science 的谱迹颜色填充)与"轻量框架数据层"
|
||||
(参考/实测三角形、标签、覆盖率)解耦。
|
||||
|
||||
底图:
|
||||
- 仅在首次调用或缓存失效时通过 colour-science 渲染一次;
|
||||
- 渲染结果保存为 numpy RGBA 数组,同时落盘到 settings/cache/,
|
||||
下次启动直接 imread 加载,避免重新跑色彩科学计算。
|
||||
|
||||
调用方在每次绘图时只需 `ax.imshow(bg, extent=bbox)`,再叠加自己的矢量层。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import threading
|
||||
from typing import Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
# 谱迹底图分辨率(边长,单位像素)。1024 对于 14 inch 画布足够细腻,
|
||||
# 文件大小 ~1-2MB,单次渲染 ~0.5-1 s,缓存后毫秒级加载。
|
||||
_DIAGRAM_RES = 1024
|
||||
|
||||
# 缓存版本号:当渲染参数或风格调整时递增,强制重新生成。
|
||||
_CACHE_VERSION = "v1"
|
||||
|
||||
_BBox = Tuple[float, float, float, float] # (xmin, xmax, ymin, ymax)
|
||||
|
||||
_CIE1931_BBOX: _BBox = (0.0, 0.8, 0.0, 0.9)
|
||||
_CIE1976_BBOX: _BBox = (0.0, 0.65, 0.0, 0.6)
|
||||
|
||||
|
||||
_memory_cache: dict[str, np.ndarray] = {}
|
||||
_lock = threading.Lock()
|
||||
|
||||
|
||||
def _cache_dir() -> str:
|
||||
# 项目根目录通过本文件位置反推:app/plots/ -> 项目根
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
root = os.path.abspath(os.path.join(here, "..", ".."))
|
||||
d = os.path.join(root, "settings", "cache")
|
||||
os.makedirs(d, exist_ok=True)
|
||||
return d
|
||||
|
||||
|
||||
def _cache_key(kind: str, bbox: _BBox) -> str:
|
||||
sig = f"{kind}|{bbox}|{_DIAGRAM_RES}|{_CACHE_VERSION}"
|
||||
h = hashlib.md5(sig.encode("utf-8")).hexdigest()[:10]
|
||||
return f"chromaticity_{kind}_{h}.npy"
|
||||
|
||||
|
||||
def _cache_path(kind: str, bbox: _BBox) -> str:
|
||||
return os.path.join(_cache_dir(), _cache_key(kind, bbox))
|
||||
|
||||
|
||||
def _render_chromaticity(kind: str, bbox: _BBox) -> np.ndarray:
|
||||
"""通过 colour-science 离屏渲染谱迹底图,返回 RGBA float 数组。"""
|
||||
# 延迟导入:仅在缓存未命中时支付 colour.plotting 的加载开销。
|
||||
import matplotlib
|
||||
prev_backend = matplotlib.get_backend()
|
||||
try:
|
||||
matplotlib.use("Agg", force=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
from colour.plotting import (
|
||||
plot_chromaticity_diagram_CIE1931,
|
||||
plot_chromaticity_diagram_CIE1976UCS,
|
||||
)
|
||||
|
||||
xmin, xmax, ymin, ymax = bbox
|
||||
aspect = (xmax - xmin) / (ymax - ymin)
|
||||
height = _DIAGRAM_RES
|
||||
width = int(round(height * aspect))
|
||||
|
||||
fig = plt.figure(figsize=(width / 100.0, height / 100.0), dpi=100)
|
||||
ax = fig.add_axes([0.0, 0.0, 1.0, 1.0])
|
||||
|
||||
if kind == "cie1931":
|
||||
plot_chromaticity_diagram_CIE1931(
|
||||
axes=ax, show=False, title=False,
|
||||
tight_layout=False, transparent_background=True,
|
||||
bounding_box=bbox,
|
||||
)
|
||||
elif kind == "cie1976":
|
||||
plot_chromaticity_diagram_CIE1976UCS(
|
||||
axes=ax, show=False, title=False,
|
||||
tight_layout=False, transparent_background=True,
|
||||
bounding_box=bbox,
|
||||
)
|
||||
else:
|
||||
plt.close(fig)
|
||||
raise ValueError(f"unknown diagram kind: {kind!r}")
|
||||
|
||||
ax.set_xlim(xmin, xmax)
|
||||
ax.set_ylim(ymin, ymax)
|
||||
ax.set_axis_off()
|
||||
ax.set_position([0.0, 0.0, 1.0, 1.0])
|
||||
|
||||
fig.canvas.draw()
|
||||
# 从 canvas 抓取 RGBA 数组
|
||||
buf = np.asarray(fig.canvas.buffer_rgba()).copy()
|
||||
plt.close(fig)
|
||||
|
||||
try:
|
||||
matplotlib.use(prev_backend, force=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return buf
|
||||
|
||||
|
||||
def _load_or_render(kind: str, bbox: _BBox) -> np.ndarray:
|
||||
key = _cache_key(kind, bbox)
|
||||
with _lock:
|
||||
if key in _memory_cache:
|
||||
return _memory_cache[key]
|
||||
|
||||
disk = _cache_path(kind, bbox)
|
||||
if os.path.isfile(disk):
|
||||
try:
|
||||
arr = np.load(disk)
|
||||
_memory_cache[key] = arr
|
||||
return arr
|
||||
except Exception:
|
||||
# 缓存损坏则重新渲染
|
||||
try:
|
||||
os.remove(disk)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
arr = _render_chromaticity(kind, bbox)
|
||||
_memory_cache[key] = arr
|
||||
try:
|
||||
np.save(disk, arr)
|
||||
except Exception:
|
||||
pass
|
||||
return arr
|
||||
|
||||
|
||||
def get_cie1931_background() -> Tuple[np.ndarray, _BBox]:
|
||||
"""返回 (RGBA 数组, bbox),可直接 ax.imshow(arr, extent=[*bbox])。"""
|
||||
return _load_or_render("cie1931", _CIE1931_BBOX), _CIE1931_BBOX
|
||||
|
||||
|
||||
def get_cie1976_background() -> Tuple[np.ndarray, _BBox]:
|
||||
return _load_or_render("cie1976", _CIE1976_BBOX), _CIE1976_BBOX
|
||||
|
||||
|
||||
def clear_cache(*, disk: bool = False) -> None:
|
||||
"""清空内存缓存(可选连同磁盘)。供调试/样式调整时使用。"""
|
||||
with _lock:
|
||||
_memory_cache.clear()
|
||||
if disk:
|
||||
d = _cache_dir()
|
||||
for name in os.listdir(d):
|
||||
if name.startswith("chromaticity_") and name.endswith(".npy"):
|
||||
try:
|
||||
os.remove(os.path.join(d, name))
|
||||
except OSError:
|
||||
pass
|
||||
@@ -1,34 +1,208 @@
|
||||
"""色域图(Gamut)绘制。
|
||||
"""色域图(Gamut)绘制 - Calman 风格。
|
||||
|
||||
Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_gamut 整体搬迁,
|
||||
实现与原方法完全一致;原方法仅保留为一行转发。
|
||||
架构:**图像渲染层** 与 **基础数据/框架层** 分离
|
||||
------------------------------------------------
|
||||
- 图像渲染层(重):CIE 1931 / 1976 谱迹色域底图。
|
||||
由 `app.plots.gamut_background` 通过 colour-science 离屏渲染一次,
|
||||
结果以 numpy RGBA 数组缓存在内存与磁盘(settings/cache/),后续直接
|
||||
`ax.imshow(bg, extent=bbox)` 复用 → 主线程绘制开销可忽略。
|
||||
- 基础数据/框架层(轻):参考色域三角形、实测色域三角形、顶点标签、
|
||||
覆盖率信息框等矢量元素,每次绘制都在真实色度坐标系上重画。
|
||||
|
||||
视觉风格:参照 Calman colorspace 显示:
|
||||
- 当前选中的参考标准:亮色实线 + 顶点空心方框;
|
||||
- 其他参考标准:半透明虚线(便于对比,不喧宾夺主);
|
||||
- 实测色域:红色粗边 + 淡红填充 + 顶点圆点 + 浮动坐标标签;
|
||||
- 右下角白底红字覆盖率信息框。
|
||||
"""
|
||||
|
||||
import matplotlib.image as mpimg
|
||||
from matplotlib.patches import Polygon
|
||||
import numpy as np
|
||||
from matplotlib.patches import PathPatch
|
||||
from matplotlib.path import Path
|
||||
|
||||
import algorithm.pq_algorithm as pq_algorithm
|
||||
from app.resources import get_resource_path
|
||||
from app.plots.gamut_background import (
|
||||
get_cie1931_background,
|
||||
get_cie1976_background,
|
||||
)
|
||||
|
||||
|
||||
# ============ 参考色域定义(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,
|
||||
# )
|
||||
|
||||
|
||||
def _draw_coverage_box(ax, x_pos, y_pos, current_ref, coverage):
|
||||
ax.text(
|
||||
x_pos, y_pos,
|
||||
f"{current_ref}\n覆盖率: {coverage:.1f}%",
|
||||
ha="right", va="bottom",
|
||||
fontsize=11, fontweight="bold",
|
||||
color="#FFF",
|
||||
bbox=dict(
|
||||
boxstyle="round,pad=0.38",
|
||||
facecolor="#111",
|
||||
edgecolor="#FFF",
|
||||
linewidth=1.7,
|
||||
alpha=0.98,
|
||||
),
|
||||
zorder=30,
|
||||
)
|
||||
|
||||
def _style_axes(ax, *, title, xlabel, ylabel, xlim, ylim):
|
||||
ax.set_facecolor("#000")
|
||||
ax.set_title(title, fontsize=12, fontweight="bold", color="#FFF", pad=8)
|
||||
ax.set_xlabel(xlabel, fontsize=10, color="#FFF")
|
||||
ax.set_ylabel(ylabel, fontsize=10, color="#FFF")
|
||||
ax.set_xlim(*xlim)
|
||||
ax.set_ylim(*ylim)
|
||||
ax.set_aspect("equal", adjustable="datalim")
|
||||
ax.grid(True, linestyle=":", linewidth=0.7, color="#444", alpha=0.32)
|
||||
ax.tick_params(axis="both", labelsize=9, colors="#FFF")
|
||||
for spine in ax.spines.values():
|
||||
spine.set_color("#888")
|
||||
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") 控制
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 主入口
|
||||
# ============================================================
|
||||
|
||||
def plot_gamut(self, results, coverage, test_type):
|
||||
"""绘制色域图 - 根据用户选择的参考标准动态计算覆盖率"""
|
||||
# 实现从原 PQAutomationApp 方法体原样搬迁,为减少修改面
|
||||
# 范围、保持行为一致,给 self 赋值为传入的 app 实例。
|
||||
"""绘制色域图(图像层 + 框架层分离架构)。"""
|
||||
|
||||
self.gamut_ax_xy.clear()
|
||||
self.gamut_ax_uv.clear()
|
||||
ax_xy = self.gamut_ax_xy
|
||||
ax_uv = self.gamut_ax_uv
|
||||
|
||||
# ==================== XY 图校准参数 ====================
|
||||
XY_ORIGIN_X = 20.55
|
||||
XY_ORIGIN_Y = 378.00
|
||||
XY_PIXELS_PER_X = 510.6818
|
||||
XY_PIXELS_PER_Y = 429.8844
|
||||
|
||||
# ==================== UV 图校准参数 ====================
|
||||
UV_ORIGIN_U = 26.91
|
||||
UV_ORIGIN_V = 377.16
|
||||
UV_PIXELS_PER_U = 615.7260
|
||||
UV_PIXELS_PER_V = 599.8432
|
||||
ax_xy.clear()
|
||||
ax_uv.clear()
|
||||
# 全局黑色背景
|
||||
self.gamut_fig.patch.set_facecolor("#000")
|
||||
|
||||
# ========== 读取用户选择的参考标准 ==========
|
||||
if test_type == "screen_module":
|
||||
@@ -40,499 +214,197 @@ def plot_gamut(self, results, coverage, test_type):
|
||||
else:
|
||||
current_ref = "DCI-P3"
|
||||
|
||||
# ========== ✅✅根据参考标准重新计算覆盖率(XY 空间)==========
|
||||
xy_coverage = coverage # 默认使用传入的值
|
||||
if current_ref not in _REF_GAMUTS_XY:
|
||||
self.log_gui.log(f"未知参考标准 '{current_ref}',使用 DCI-P3", level="error")
|
||||
current_ref = "DCI-P3"
|
||||
|
||||
# ========== 重新计算 xy 覆盖率 ==========
|
||||
xy_coverage = coverage
|
||||
uv_coverage = 0.0
|
||||
measured_xy = None
|
||||
|
||||
try:
|
||||
# 提取前 3 个 RGB 点的 xy 坐标
|
||||
if len(results) >= 3:
|
||||
xy_points = [[result[0], result[1]] for result in results[:3]]
|
||||
|
||||
# 根据参考标准计算 XY 覆盖率
|
||||
if len(results) >= 3:
|
||||
measured_xy = [(float(r[0]), float(r[1])) for r in results[:3]]
|
||||
try:
|
||||
if current_ref == "BT.2020":
|
||||
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_BT2020(
|
||||
xy_points
|
||||
)
|
||||
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_BT2020(measured_xy)
|
||||
elif current_ref == "BT.709":
|
||||
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_BT709(
|
||||
xy_points
|
||||
)
|
||||
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_BT709(measured_xy)
|
||||
elif current_ref == "DCI-P3":
|
||||
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_DCIP3(
|
||||
xy_points
|
||||
)
|
||||
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_DCIP3(measured_xy)
|
||||
elif current_ref == "BT.601":
|
||||
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_BT601(
|
||||
xy_points
|
||||
)
|
||||
else:
|
||||
self.log_gui.log(f"未知参考标准 '{current_ref}',使用 DCI-P3", level="error")
|
||||
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_DCIP3(
|
||||
xy_points
|
||||
)
|
||||
current_ref = "DCI-P3"
|
||||
|
||||
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_BT601(measured_xy)
|
||||
self.log_gui.log(
|
||||
f"XY 空间覆盖率({current_ref}): {xy_coverage:.1f}%"
|
||||
, level="success")
|
||||
f"XY 空间覆盖率({current_ref}): {xy_coverage:.1f}%", level="success"
|
||||
)
|
||||
except Exception as e:
|
||||
self.log_gui.log(f"重新计算 XY 覆盖率失败: {str(e)}", level="error")
|
||||
xy_coverage = coverage
|
||||
|
||||
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 ==========
|
||||
# ============================================================
|
||||
# 左图:CIE 1931 xy
|
||||
# ============================================================
|
||||
try:
|
||||
img_xy = mpimg.imread(get_resource_path("assets/cie.png"))
|
||||
h_xy, w_xy = img_xy.shape[:2]
|
||||
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]),
|
||||
)
|
||||
|
||||
self.log_gui.log(f"加载 XY 色域图: {w_xy}x{h_xy}", level="info")
|
||||
|
||||
self.gamut_ax_xy.imshow(img_xy, extent=[0, w_xy, h_xy, 0], aspect="equal")
|
||||
self.gamut_ax_xy.set_xlim(0, w_xy)
|
||||
self.gamut_ax_xy.set_ylim(h_xy, 0)
|
||||
self.gamut_ax_xy.axis("off")
|
||||
self.gamut_ax_xy.set_clip_on(False)
|
||||
|
||||
def cie_xy_to_pixel(x, y):
|
||||
"""CIE xy → 像素坐标"""
|
||||
px = XY_ORIGIN_X + x * XY_PIXELS_PER_X
|
||||
py = XY_ORIGIN_Y - y * XY_PIXELS_PER_Y
|
||||
return px, py
|
||||
|
||||
if len(results) >= 3:
|
||||
red_x, red_y = results[0][0], results[0][1]
|
||||
green_x, green_y = results[1][0], results[1][1]
|
||||
blue_x, blue_y = results[2][0], results[2][1]
|
||||
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,
|
||||
)
|
||||
_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({red_x:.4f},{red_y:.4f}) "
|
||||
f"G({green_x:.4f},{green_y:.4f}) B({blue_x:.4f},{blue_y:.4f})"
|
||||
, level="info")
|
||||
|
||||
# ========== 绘制测量三角形 ==========
|
||||
points = [
|
||||
cie_xy_to_pixel(red_x, red_y),
|
||||
cie_xy_to_pixel(green_x, green_y),
|
||||
cie_xy_to_pixel(blue_x, blue_y),
|
||||
cie_xy_to_pixel(red_x, red_y),
|
||||
]
|
||||
|
||||
xs = [p[0] for p in points]
|
||||
ys = [p[1] for p in points]
|
||||
|
||||
self.gamut_ax_xy.plot(
|
||||
xs,
|
||||
ys,
|
||||
color="red",
|
||||
linewidth=2.5,
|
||||
marker="o",
|
||||
markersize=10,
|
||||
markerfacecolor="red",
|
||||
markeredgecolor="white",
|
||||
markeredgewidth=2,
|
||||
label="测量色域",
|
||||
zorder=10,
|
||||
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",
|
||||
)
|
||||
_draw_measured_triangle(ax_xy, measured_xy, uv_space=False)
|
||||
|
||||
# ========== 标注 RGB 点 ==========
|
||||
labels = ["R", "G", "B"]
|
||||
coords = [(red_x, red_y), (green_x, green_y), (blue_x, blue_y)]
|
||||
_draw_coverage_box(
|
||||
ax_xy, bbox_xy[1] - 0.02, bbox_xy[2] + 0.02, current_ref, xy_coverage
|
||||
)
|
||||
|
||||
for (x_cie, y_cie), label in zip(coords, labels):
|
||||
px, py = cie_xy_to_pixel(x_cie, y_cie)
|
||||
# 暗化三角形外部区域(黑色半透明遮罩)
|
||||
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)
|
||||
patch = PathPatch(path, facecolor=(0,0,0,0.65), lw=0, zorder=7)
|
||||
ax_xy.add_patch(patch)
|
||||
|
||||
# 自适应偏移
|
||||
if label == "R":
|
||||
offset = (-60, -40) if x_cie > 0.6 else (0, -60)
|
||||
elif label == "G":
|
||||
offset = (0, -60)
|
||||
else: # B
|
||||
offset = (60, 40)
|
||||
|
||||
self.gamut_ax_xy.annotate(
|
||||
f"{label}\n({x_cie:.3f},{y_cie:.3f})",
|
||||
xy=(px, py),
|
||||
xytext=offset,
|
||||
textcoords="offset points",
|
||||
fontsize=9,
|
||||
color="white",
|
||||
fontweight="bold",
|
||||
bbox=dict(
|
||||
boxstyle="round,pad=0.5",
|
||||
facecolor="red",
|
||||
alpha=0.9,
|
||||
edgecolor="white",
|
||||
linewidth=2,
|
||||
),
|
||||
arrowprops=dict(arrowstyle="->", color="red", lw=2),
|
||||
zorder=11,
|
||||
clip_on=False,
|
||||
)
|
||||
|
||||
# ========== 绘制所有参考标准 ==========
|
||||
# DCI-P3
|
||||
dcip3 = [
|
||||
(0.6800, 0.3200),
|
||||
(0.2650, 0.6900),
|
||||
(0.1500, 0.0600),
|
||||
(0.6800, 0.3200),
|
||||
]
|
||||
dcip3_px = [cie_xy_to_pixel(x, y) for x, y in dcip3]
|
||||
self.gamut_ax_xy.plot(
|
||||
[p[0] for p in dcip3_px],
|
||||
[p[1] for p in dcip3_px],
|
||||
color="blue",
|
||||
linewidth=1.5,
|
||||
linestyle="--",
|
||||
marker="s",
|
||||
markersize=6,
|
||||
alpha=0.7,
|
||||
label="DCI-P3",
|
||||
zorder=5,
|
||||
)
|
||||
|
||||
# BT.2020
|
||||
bt2020 = [
|
||||
(0.7080, 0.2920),
|
||||
(0.1700, 0.7970),
|
||||
(0.1310, 0.0460),
|
||||
(0.7080, 0.2920),
|
||||
]
|
||||
bt2020_px = [cie_xy_to_pixel(x, y) for x, y in bt2020]
|
||||
self.gamut_ax_xy.plot(
|
||||
[p[0] for p in bt2020_px],
|
||||
[p[1] for p in bt2020_px],
|
||||
color="green",
|
||||
linewidth=1.5,
|
||||
linestyle="-.",
|
||||
marker="D",
|
||||
markersize=5,
|
||||
alpha=0.7,
|
||||
label="BT.2020",
|
||||
zorder=4,
|
||||
)
|
||||
|
||||
# BT.709
|
||||
bt709 = [
|
||||
(0.6400, 0.3300),
|
||||
(0.3000, 0.6000),
|
||||
(0.1500, 0.0600),
|
||||
(0.6400, 0.3300),
|
||||
]
|
||||
bt709_px = [cie_xy_to_pixel(x, y) for x, y in bt709]
|
||||
self.gamut_ax_xy.plot(
|
||||
[p[0] for p in bt709_px],
|
||||
[p[1] for p in bt709_px],
|
||||
color="gray",
|
||||
linewidth=1.2,
|
||||
linestyle=":",
|
||||
marker="^",
|
||||
markersize=5,
|
||||
alpha=0.6,
|
||||
label="BT.709",
|
||||
zorder=3,
|
||||
)
|
||||
|
||||
# BT.601(仅 SDR 测试)
|
||||
if test_type == "sdr_movie":
|
||||
bt601 = [
|
||||
(0.6300, 0.3400),
|
||||
(0.3100, 0.5950),
|
||||
(0.1550, 0.0700),
|
||||
(0.6300, 0.3400),
|
||||
]
|
||||
bt601_px = [cie_xy_to_pixel(x, y) for x, y in bt601]
|
||||
self.gamut_ax_xy.plot(
|
||||
[p[0] for p in bt601_px],
|
||||
[p[1] for p in bt601_px],
|
||||
color="purple",
|
||||
linewidth=1.2,
|
||||
linestyle="-",
|
||||
marker="o",
|
||||
markersize=5,
|
||||
alpha=0.6,
|
||||
label="BT.601",
|
||||
zorder=3,
|
||||
)
|
||||
|
||||
# ========== XY 覆盖率标注(使用重新计算的值)==========
|
||||
self.gamut_ax_xy.text(
|
||||
w_xy * 0.85,
|
||||
h_xy * 0.92,
|
||||
f"参考: {current_ref}\n覆盖率: {xy_coverage:.1f}%",
|
||||
ha="right",
|
||||
va="bottom",
|
||||
fontsize=11,
|
||||
fontweight="bold",
|
||||
color="red",
|
||||
bbox=dict(
|
||||
boxstyle="round,pad=0.5",
|
||||
facecolor="white",
|
||||
alpha=0.95,
|
||||
edgecolor="red",
|
||||
linewidth=2,
|
||||
),
|
||||
zorder=12,
|
||||
)
|
||||
|
||||
# 图例
|
||||
self.gamut_ax_xy.legend(
|
||||
loc="upper right",
|
||||
fontsize=7,
|
||||
framealpha=0.95,
|
||||
edgecolor="black",
|
||||
fancybox=True,
|
||||
)
|
||||
legend = ax_xy.legend(
|
||||
loc="upper right", fontsize=8.5,
|
||||
framealpha=0.0, edgecolor="#000", fancybox=True,
|
||||
labelcolor="#FFF"
|
||||
)
|
||||
legend.set_zorder(200)
|
||||
legend.get_frame().set_facecolor("#000")
|
||||
legend.get_frame().set_alpha(0.5)
|
||||
legend.get_frame().set_edgecolor("#FFF")
|
||||
ax_xy.add_artist(legend)
|
||||
|
||||
except Exception as e:
|
||||
self.log_gui.log(f"XY 图绘制失败: {str(e)}", level="error")
|
||||
import traceback
|
||||
|
||||
self.log_gui.log(traceback.format_exc(), level="error")
|
||||
|
||||
# ========== 右图:CIE 1976 u'v' ==========
|
||||
# ============================================================
|
||||
# 右图:CIE 1976 u'v'
|
||||
# ============================================================
|
||||
try:
|
||||
img_uv = mpimg.imread(get_resource_path("assets/cie_uv.png"))
|
||||
h_uv, w_uv = img_uv.shape[:2]
|
||||
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]),
|
||||
)
|
||||
|
||||
self.log_gui.log(f"加载 UV 色域图: {w_uv}x{h_uv}", level="info")
|
||||
|
||||
self.gamut_ax_uv.imshow(img_uv, extent=[0, w_uv, h_uv, 0], aspect="equal")
|
||||
self.gamut_ax_uv.set_xlim(0, w_uv)
|
||||
self.gamut_ax_uv.set_ylim(h_uv, 0)
|
||||
self.gamut_ax_uv.axis("off")
|
||||
self.gamut_ax_uv.set_clip_on(False)
|
||||
|
||||
def cie_uv_to_pixel(u, v):
|
||||
"""CIE u'v' → 像素坐标"""
|
||||
px = UV_ORIGIN_U + u * UV_PIXELS_PER_U
|
||||
py = UV_ORIGIN_V - v * UV_PIXELS_PER_V
|
||||
return px, py
|
||||
|
||||
if len(results) >= 3:
|
||||
# 只取前 3 个 RGB 点
|
||||
rgb_results = results[:3]
|
||||
|
||||
# 转换为 u'v' 坐标
|
||||
def xy_to_uv(x, y):
|
||||
"""xy → u'v' 转换"""
|
||||
denom = -2 * x + 12 * y + 3
|
||||
if abs(denom) < 1e-10:
|
||||
return 0, 0
|
||||
u = (4 * x) / denom
|
||||
v = (9 * y) / denom
|
||||
return u, v
|
||||
|
||||
uv_coords = [
|
||||
[u, v] for u, v in [xy_to_uv(r[0], r[1]) for r in rgb_results]
|
||||
]
|
||||
|
||||
self.log_gui.log(f"UV 坐标: {uv_coords}", level="info")
|
||||
|
||||
# ========== ✅✅计算 u'v' 覆盖率(使用参考标准)==========
|
||||
measured_uv = None
|
||||
if measured_xy is not None:
|
||||
measured_uv = [_xy_to_uv(x, y) for x, y in measured_xy]
|
||||
try:
|
||||
uv_coverage = pq_algorithm.calculate_uv_gamut_coverage(
|
||||
uv_coords, reference=current_ref
|
||||
[list(uv) for uv in measured_uv], reference=current_ref
|
||||
)
|
||||
self.log_gui.log(
|
||||
f"UV 空间覆盖率({current_ref}): {uv_coverage:.1f}%"
|
||||
, level="success")
|
||||
f"UV 空间覆盖率({current_ref}): {uv_coverage:.1f}%",
|
||||
level="success",
|
||||
)
|
||||
except Exception as e:
|
||||
self.log_gui.log(f"计算 UV 覆盖率失败: {str(e)}", level="error")
|
||||
uv_coverage = 0.0
|
||||
# =================================================
|
||||
|
||||
# ========== 绘制测量三角形 ==========
|
||||
uv_coords_plot = uv_coords + [uv_coords[0]]
|
||||
points_uv = [cie_uv_to_pixel(u, v) for u, v in uv_coords_plot]
|
||||
xs_uv = [p[0] for p in points_uv]
|
||||
ys_uv = [p[1] for p in points_uv]
|
||||
|
||||
self.gamut_ax_uv.plot(
|
||||
xs_uv,
|
||||
ys_uv,
|
||||
color="red",
|
||||
linewidth=2.5,
|
||||
marker="o",
|
||||
markersize=10,
|
||||
markerfacecolor="red",
|
||||
markeredgecolor="white",
|
||||
markeredgewidth=2,
|
||||
label="测量色域",
|
||||
zorder=10,
|
||||
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,
|
||||
)
|
||||
_draw_reference_triangle(
|
||||
ax_uv, _ref_gamut_uv(current_ref),
|
||||
_REF_COLORS[current_ref],
|
||||
is_current=True, label=f"{current_ref} (参考)",
|
||||
)
|
||||
|
||||
# ========== 标注 RGB 点 ==========
|
||||
labels = ["R", "G", "B"]
|
||||
for (u, v), label in zip(uv_coords, labels):
|
||||
px, py = cie_uv_to_pixel(u, v)
|
||||
if measured_uv is not None:
|
||||
_draw_measured_triangle(ax_uv, measured_uv, uv_space=True)
|
||||
|
||||
# 自适应偏移
|
||||
if label == "R":
|
||||
if u > 0.42 and v > 0.50:
|
||||
offset = (-70, 20)
|
||||
elif u > 0.45:
|
||||
offset = (30, 50)
|
||||
else:
|
||||
offset = (50, 45)
|
||||
elif label == "G":
|
||||
offset = (0, -60)
|
||||
else: # B
|
||||
offset = (60, 40)
|
||||
_draw_coverage_box(
|
||||
ax_uv, bbox_uv[1] - 0.015, bbox_uv[2] + 0.015, current_ref, uv_coverage
|
||||
)
|
||||
|
||||
self.gamut_ax_uv.annotate(
|
||||
f"{label}\n({u:.3f},{v:.3f})",
|
||||
xy=(px, py),
|
||||
xytext=offset,
|
||||
textcoords="offset points",
|
||||
fontsize=9,
|
||||
color="white",
|
||||
fontweight="bold",
|
||||
bbox=dict(
|
||||
boxstyle="round,pad=0.5",
|
||||
facecolor="red",
|
||||
alpha=0.9,
|
||||
edgecolor="white",
|
||||
linewidth=2,
|
||||
),
|
||||
arrowprops=dict(arrowstyle="->", color="red", lw=2),
|
||||
zorder=11,
|
||||
clip_on=False,
|
||||
)
|
||||
|
||||
# ========== DCI-P3 参考(蓝色)==========
|
||||
dcip3_uv = [
|
||||
[0.4970, 0.5260],
|
||||
[0.0999, 0.5780],
|
||||
[0.1754, 0.1576],
|
||||
[0.4970, 0.5260],
|
||||
]
|
||||
dcip3_uv_px = [cie_uv_to_pixel(u, v) for u, v in dcip3_uv]
|
||||
|
||||
self.gamut_ax_uv.plot(
|
||||
[p[0] for p in dcip3_uv_px],
|
||||
[p[1] for p in dcip3_uv_px],
|
||||
color="blue",
|
||||
linewidth=1.5,
|
||||
linestyle="--",
|
||||
marker="s",
|
||||
markersize=6,
|
||||
alpha=0.7,
|
||||
label="DCI-P3",
|
||||
zorder=5,
|
||||
)
|
||||
|
||||
# ========== BT.2020 参考(绿色)==========
|
||||
bt2020_uv = [
|
||||
[0.5566, 0.5165],
|
||||
[0.0556, 0.5868],
|
||||
[0.1593, 0.1258],
|
||||
[0.5566, 0.5165],
|
||||
]
|
||||
bt2020_uv_px = [cie_uv_to_pixel(u, v) for u, v in bt2020_uv]
|
||||
|
||||
self.gamut_ax_uv.plot(
|
||||
[p[0] for p in bt2020_uv_px],
|
||||
[p[1] for p in bt2020_uv_px],
|
||||
color="green",
|
||||
linewidth=1.5,
|
||||
linestyle="-.",
|
||||
marker="D",
|
||||
markersize=5,
|
||||
alpha=0.7,
|
||||
label="BT.2020",
|
||||
zorder=4,
|
||||
)
|
||||
|
||||
# ========== BT.709 参考(灰色)==========
|
||||
bt709_uv = [
|
||||
[0.4507, 0.5229],
|
||||
[0.1250, 0.5625],
|
||||
[0.1754, 0.1576],
|
||||
[0.4507, 0.5229],
|
||||
]
|
||||
bt709_uv_px = [cie_uv_to_pixel(u, v) for u, v in bt709_uv]
|
||||
|
||||
self.gamut_ax_uv.plot(
|
||||
[p[0] for p in bt709_uv_px],
|
||||
[p[1] for p in bt709_uv_px],
|
||||
color="gray",
|
||||
linewidth=1.2,
|
||||
linestyle=":",
|
||||
marker="^",
|
||||
markersize=5,
|
||||
alpha=0.6,
|
||||
label="BT.709",
|
||||
zorder=3,
|
||||
)
|
||||
|
||||
# ========== BT.601 参考(紫色)- 仅 SDR 测试显示 ==========
|
||||
if test_type == "sdr_movie":
|
||||
bt601_uv = [
|
||||
[0.4510, 0.5236],
|
||||
[0.1291, 0.5606],
|
||||
[0.1787, 0.1610],
|
||||
[0.4510, 0.5236],
|
||||
]
|
||||
bt601_uv_px = [cie_uv_to_pixel(u, v) for u, v in bt601_uv]
|
||||
|
||||
self.gamut_ax_uv.plot(
|
||||
[p[0] for p in bt601_uv_px],
|
||||
[p[1] for p in bt601_uv_px],
|
||||
color="purple",
|
||||
linewidth=1.2,
|
||||
linestyle="-",
|
||||
marker="o",
|
||||
markersize=5,
|
||||
alpha=0.6,
|
||||
label="BT.601",
|
||||
zorder=3,
|
||||
)
|
||||
|
||||
# ========== UV 覆盖率标注(使用动态计算的值)==========
|
||||
self.gamut_ax_uv.text(
|
||||
w_uv * 0.85,
|
||||
h_uv * 0.92,
|
||||
f"参考: {current_ref}\n覆盖率: {uv_coverage:.1f}%",
|
||||
ha="right",
|
||||
va="bottom",
|
||||
fontsize=11,
|
||||
fontweight="bold",
|
||||
color="red",
|
||||
bbox=dict(
|
||||
boxstyle="round,pad=0.5",
|
||||
facecolor="white",
|
||||
alpha=0.95,
|
||||
edgecolor="red",
|
||||
linewidth=2,
|
||||
),
|
||||
zorder=12,
|
||||
)
|
||||
|
||||
# 图例
|
||||
self.gamut_ax_uv.legend(
|
||||
loc="upper right",
|
||||
fontsize=7,
|
||||
framealpha=0.95,
|
||||
edgecolor="black",
|
||||
fancybox=True,
|
||||
)
|
||||
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)
|
||||
patch = PathPatch(path, facecolor=(0,0,0,0.65), lw=0, zorder=7)
|
||||
ax_uv.add_patch(patch)
|
||||
|
||||
legend_uv = ax_uv.legend(
|
||||
loc="upper right", fontsize=8.5,
|
||||
framealpha=0.0, edgecolor="#000", fancybox=True,
|
||||
labelcolor="#FFF"
|
||||
)
|
||||
legend_uv.set_zorder(200)
|
||||
legend_uv.get_frame().set_facecolor("#000")
|
||||
legend_uv.get_frame().set_alpha(0.72)
|
||||
legend_uv.get_frame().set_edgecolor("#FFF")
|
||||
ax_uv.add_artist(legend_uv)
|
||||
|
||||
except Exception as e:
|
||||
self.log_gui.log(f"UV 图绘制失败: {str(e)}", level="error")
|
||||
import traceback
|
||||
|
||||
self.log_gui.log(traceback.format_exc(), level="error")
|
||||
|
||||
# ========== 总标题 ==========
|
||||
test_type_name = self.get_test_type_name(test_type)
|
||||
self.gamut_fig.suptitle(
|
||||
f"{test_type_name} - 色域测试", fontsize=12, y=0.98, fontweight="bold"
|
||||
f"{test_type_name} - 色域测试",
|
||||
fontsize=12, y=0.98, fontweight="bold",
|
||||
)
|
||||
|
||||
self.gamut_canvas.draw()
|
||||
self.chart_notebook.select(self.gamut_chart_frame)
|
||||
|
||||
# 同步工具栏按钮选中状态
|
||||
if hasattr(self, "sync_gamut_toolbar"):
|
||||
self.sync_gamut_toolbar()
|
||||
|
||||
self.log_gui.log("色域图绘制完成", level="success")
|
||||
|
||||
Reference in New Issue
Block a user