Files
pqAutomationApp/app/plots/plot_gamut.py

539 lines
18 KiB
Python
Raw Normal View History

2026-04-21 15:31:48 +08:00
"""色域图Gamut绘制。
2026-04-20 10:00:44 +08:00
Step 2 重构 pqAutomationApp.PQAutomationApp.plot_gamut 整体搬迁
实现与原方法完全一致原方法仅保留为一行转发
"""
import matplotlib.image as mpimg
import algorithm.pq_algorithm as pq_algorithm
from app.resources import get_resource_path
def plot_gamut(self, results, coverage, test_type):
2026-04-20 10:00:44 +08:00
"""绘制色域图 - 根据用户选择的参考标准动态计算覆盖率"""
# 实现从原 PQAutomationApp 方法体原样搬迁,为减少修改面
# 范围、保持行为一致,给 self 赋值为传入的 app 实例。
self.gamut_ax_xy.clear()
self.gamut_ax_uv.clear()
# ==================== 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
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-04-21 15:31:48 +08:00
# ========== ✅✅根据参考标准重新计算覆盖率XY 空间)==========
2026-04-20 10:00:44 +08:00
xy_coverage = coverage # 默认使用传入的值
uv_coverage = 0.0
try:
# 提取前 3 个 RGB 点的 xy 坐标
if len(results) >= 3:
xy_points = [[result[0], result[1]] for result in results[:3]]
# 根据参考标准计算 XY 覆盖率
if current_ref == "BT.2020":
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_BT2020(
xy_points
)
elif current_ref == "BT.709":
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_BT709(
xy_points
)
elif current_ref == "DCI-P3":
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_DCIP3(
xy_points
)
elif current_ref == "BT.601":
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_BT601(
xy_points
)
else:
2026-04-21 15:31:48 +08:00
self.log_gui.log(f"未知参考标准 '{current_ref}',使用 DCI-P3", level="error")
2026-04-20 10:00:44 +08:00
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_DCIP3(
xy_points
)
current_ref = "DCI-P3"
self.log_gui.log(
2026-04-21 15:31:48 +08:00
f"XY 空间覆盖率({current_ref}: {xy_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"重新计算 XY 覆盖率失败: {str(e)}", level="error")
2026-04-20 10:00:44 +08:00
xy_coverage = coverage # 回退到传入值
# =================================================
# ========== 左图CIE 1931 xy ==========
try:
img_xy = mpimg.imread(get_resource_path("assets/cie.png"))
h_xy, w_xy = img_xy.shape[:2]
2026-04-21 15:31:48 +08:00
self.log_gui.log(f"加载 XY 色域图: {w_xy}x{h_xy}", level="info")
2026-04-20 10:00:44 +08:00
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]
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})"
2026-04-21 15:31:48 +08:00
, level="info")
2026-04-20 10:00:44 +08:00
# ========== 绘制测量三角形 ==========
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,
)
# ========== 标注 RGB 点 ==========
labels = ["R", "G", "B"]
coords = [(red_x, red_y), (green_x, green_y), (blue_x, blue_y)]
for (x_cie, y_cie), label in zip(coords, labels):
px, py = cie_xy_to_pixel(x_cie, y_cie)
# 自适应偏移
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,
)
2026-04-21 15:31:48 +08:00
# ========== XY 覆盖率标注(使用重新计算的值)==========
2026-04-20 10:00:44 +08:00
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,
)
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
# ========== 右图CIE 1976 u'v' ==========
try:
img_uv = mpimg.imread(get_resource_path("assets/cie_uv.png"))
h_uv, w_uv = img_uv.shape[:2]
2026-04-21 15:31:48 +08:00
self.log_gui.log(f"加载 UV 色域图: {w_uv}x{h_uv}", level="info")
2026-04-20 10:00:44 +08:00
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]
]
2026-04-21 15:31:48 +08:00
self.log_gui.log(f"UV 坐标: {uv_coords}", level="info")
2026-04-20 10:00:44 +08:00
2026-04-21 15:31:48 +08:00
# ========== ✅✅计算 u'v' 覆盖率(使用参考标准)==========
2026-04-20 10:00:44 +08:00
try:
uv_coverage = pq_algorithm.calculate_uv_gamut_coverage(
uv_coords, reference=current_ref
)
self.log_gui.log(
2026-04-21 15:31:48 +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
# =================================================
# ========== 绘制测量三角形 ==========
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,
)
# ========== 标注 RGB 点 ==========
labels = ["R", "G", "B"]
for (u, v), label in zip(uv_coords, labels):
px, py = cie_uv_to_pixel(u, v)
# 自适应偏移
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)
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,
)
2026-04-21 15:31:48 +08:00
# ========== UV 覆盖率标注(使用动态计算的值)==========
2026-04-20 10:00:44 +08:00
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,
)
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(
f"{test_type_name} - 色域测试", fontsize=12, y=0.98, fontweight="bold"
)
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-04-21 15:31:48 +08:00
self.log_gui.log("色域图绘制完成", level="success")