重构提取plot函数

This commit is contained in:
xinzhu.yin
2026-04-20 10:00:44 +08:00
parent 22c46632ac
commit 77c92ffc05
8 changed files with 1660 additions and 1584 deletions

0
app/plots/__init__.py Normal file
View File

318
app/plots/plot_accuracy.py Normal file
View File

@@ -0,0 +1,318 @@
"""色准测试结果绘制。
Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_accuracy 原样搬迁。
"""
from matplotlib.patches import Rectangle
def plot_accuracy(app, accuracy_data, test_type):
"""绘制色准测试结果 - 29色显示 - 简洁版布局(显示 Gamma"""
self = app
self.accuracy_ax.clear()
self.accuracy_ax.set_xlim(0, 1)
self.accuracy_ax.set_ylim(0, 1)
self.accuracy_ax.axis("off")
self.accuracy_fig.subplots_adjust(
left=0.05,
right=0.95,
top=0.95,
bottom=0.02,
)
# 获取色准数据
color_patches = accuracy_data.get("color_patches", [])
delta_e_values = accuracy_data.get("delta_e_values", [])
avg_delta_e = accuracy_data.get("avg_delta_e", 0)
max_delta_e = accuracy_data.get("max_delta_e", 0)
min_delta_e = accuracy_data.get("min_delta_e", 0)
excellent_count = accuracy_data.get("excellent_count", 0)
good_count = accuracy_data.get("good_count", 0)
poor_count = accuracy_data.get("poor_count", 0)
# 获取 Gamma 值
target_gamma = accuracy_data.get("target_gamma", 2.2)
test_type_name = self.get_test_type_name(test_type)
# ========== 标题(动态显示 Gamma==========
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"
else: # screen_module
title = f"{test_type_name} - 色准测试(全 29色 | Gamma {target_gamma}"
self.accuracy_fig.suptitle(
title,
fontsize=11,
y=0.98,
fontweight="bold",
)
# ========== 29色6行5列布局 ==========
cols = 5
rows = 6
patch_width = 0.135
patch_height = 0.085
x_start = 0.08
y_start = 0.90
x_gap = 0.035
y_gap = 0.050
# ========== 绘制色块 ==========
for i, (color_name, delta_e) in enumerate(zip(color_patches, delta_e_values)):
row = i // cols
col = i % cols
x = x_start + col * (patch_width + x_gap)
y = y_start - row * (patch_height + y_gap)
# 颜色映射
color_map = {
# 灰阶
"White": "#FFFFFF",
"Gray 80": "#E6E6E6",
"Gray 65": "#D1D1D1",
"Gray 50": "#BABABA",
"Gray 35": "#9E9E9E",
# 饱和色
"100% Red": "#FF0000",
"100% Green": "#00FF00",
"100% Blue": "#0000FF",
"100% Cyan": "#00FFFF",
"100% Magenta": "#FF00FF",
"100% Yellow": "#FFFF00",
# ColorChecker 颜色
"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",
}
patch_color = color_map.get(color_name, "#808080")
# ΔE 等级颜色
if delta_e < 3:
edge_color = "green"
elif delta_e < 5:
edge_color = "orange"
else:
edge_color = "red"
# 绘制色块
rect = Rectangle(
(x, y),
patch_width,
patch_height,
transform=self.accuracy_ax.transAxes,
facecolor=patch_color,
edgecolor=edge_color,
linewidth=1.8,
)
self.accuracy_ax.add_patch(rect)
# ========== 标注色块名称(上方)==========
self.accuracy_ax.text(
x + patch_width / 2,
y + patch_height + 0.015,
color_name,
ha="center",
va="bottom",
fontsize=5.5,
fontweight="bold",
transform=self.accuracy_ax.transAxes,
clip_on=False,
)
# ========== 标注 ΔE 值(中心)==========
dark_colors = [
"100% Red",
"100% Green",
"100% Blue",
"Gray 35",
"Dark Skin",
"Foliage",
"Purple",
"Purplish Blue",
"Blue (Legacy)",
"Green (Legacy)",
"Red (Legacy)",
"Magenta (Legacy)",
"Cyan (Legacy)",
]
text_color = "white" if color_name in dark_colors else "black"
self.accuracy_ax.text(
x + patch_width / 2,
y + patch_height / 2,
f"ΔE\n{delta_e:.2f}",
ha="center",
va="center",
fontsize=5.2,
fontweight="bold",
color=text_color,
transform=self.accuracy_ax.transAxes,
bbox=dict(
boxstyle="round,pad=0.22",
facecolor="white" if text_color == "black" else "black",
alpha=0.75,
edgecolor=edge_color,
linewidth=1.0,
),
)
# ========== 统计信息卡片(只保留外框)==========
card_width = 0.84
card_height = 0.15
card_x = 0.08
card_y = 0.01
info_card = Rectangle(
(card_x, card_y),
card_width,
card_height,
transform=self.accuracy_ax.transAxes,
facecolor="#F0F0F0",
edgecolor="black",
linewidth=1.5,
)
self.accuracy_ax.add_patch(info_card)
# ========== 标题(带说明)==========
self.accuracy_ax.text(
card_x + card_width / 2,
card_y + card_height - 0.008,
"色准统计5灰阶 + 18 ColorChecker + 6饱和色 | ΔE 2000 标准)",
ha="center",
va="top",
fontsize=7.5,
fontweight="bold",
transform=self.accuracy_ax.transAxes,
)
# ========== 统计内容(无内部框)==========
stats_y = card_y + card_height * 0.55
# 左侧ΔE 统计
left_x = card_x + 0.02
stats_text = [
f"平均 ΔE: {avg_delta_e:.2f}",
f"最大 ΔE: {max_delta_e:.2f}",
f"最小 ΔE: {min_delta_e:.2f}",
]
for i, text in enumerate(stats_text):
self.accuracy_ax.text(
left_x,
stats_y - i * 0.030,
text,
ha="left",
va="center",
fontsize=7,
fontweight="bold",
transform=self.accuracy_ax.transAxes,
)
# 中间:色块统计
middle_x = card_x + card_width * 0.32
self.accuracy_ax.text(
middle_x,
stats_y,
f"优秀 (ΔE<3): {excellent_count}",
ha="left",
va="center",
fontsize=7,
color="green",
fontweight="bold",
transform=self.accuracy_ax.transAxes,
)
self.accuracy_ax.text(
middle_x,
stats_y - 0.030,
f"良好 (3≤ΔE<5): {good_count}",
ha="left",
va="center",
fontsize=7,
color="orange",
fontweight="bold",
transform=self.accuracy_ax.transAxes,
)
self.accuracy_ax.text(
middle_x,
stats_y - 0.060,
f"偏差 (ΔE≥5): {poor_count}",
ha="left",
va="center",
fontsize=7,
color="red",
fontweight="bold",
transform=self.accuracy_ax.transAxes,
)
# 右侧:总体评价
right_x = card_x + card_width - 0.02
if avg_delta_e < 2:
grade = "专业级"
grade_icon = "★★★"
grade_color = "darkgreen"
elif avg_delta_e < 3:
grade = "优秀"
grade_icon = "✓✓"
grade_color = "green"
elif avg_delta_e < 5:
grade = "良好"
grade_icon = ""
grade_color = "orange"
else:
grade = "需要校准"
grade_icon = ""
grade_color = "red"
self.accuracy_ax.text(
right_x,
stats_y + 0.020,
"总体评价:",
ha="right",
va="bottom",
fontsize=7,
fontweight="bold",
transform=self.accuracy_ax.transAxes,
)
self.accuracy_ax.text(
right_x,
stats_y - 0.025,
f"{grade} {grade_icon}",
ha="right",
va="top",
fontsize=11,
fontweight="bold",
color=grade_color,
transform=self.accuracy_ax.transAxes,
)
self.accuracy_canvas.draw()
self.chart_notebook.select(4)

325
app/plots/plot_cct.py Normal file
View File

@@ -0,0 +1,325 @@
"""CCT / 色度一致性绘制。
Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_cct 原样搬迁。
"""
import numpy as np
def plot_cct(app, test_type):
"""绘制 x 和 y 坐标分离图 - 每个点标注纵坐标值"""
self = app
self.cct_fig.clear()
gray_data = self.results.get_intermediate_data("shared", "gray")
if not gray_data:
gray_data = self.results.get_intermediate_data("cct", "gray")
if not gray_data or len(gray_data) < 2:
self.log_gui.log("⚠️ 无 xy 数据可用")
ax = self.cct_fig.add_subplot(111)
ax.text(
0.5,
0.5,
"无可用数据",
ha="center",
va="center",
fontsize=14,
color="red",
)
ax.axis("off")
self.cct_canvas.draw()
return
x_measured = [data[0] for data in gray_data]
y_measured = [data[1] for data in gray_data]
# 反转数据顺序(从暗到亮)
x_measured = x_measured[::-1]
y_measured = y_measured[::-1]
# 去掉第一个点
x_measured = x_measured[1:]
y_measured = y_measured[1:]
# 重新生成灰阶坐标
total_points = len(gray_data)
grayscale = np.linspace(100 / total_points, 100, len(x_measured))
self.log_gui.log(f"✓ 已移除第一个数据点,当前数据点数: {len(x_measured)}")
self.log_gui.log(f" x范围: {min(x_measured):.6f} - {max(x_measured):.6f}")
self.log_gui.log(f" y范围: {min(y_measured):.6f} - {max(y_measured):.6f}")
# ========== 根据测试类型读取对应参数 ==========
if test_type == "sdr_movie":
try:
x_ideal = float(self.sdr_cct_x_ideal_var.get())
x_tolerance = float(self.sdr_cct_x_tolerance_var.get())
y_ideal = float(self.sdr_cct_y_ideal_var.get())
y_tolerance = float(self.sdr_cct_y_tolerance_var.get())
self.log_gui.log("✓ 使用 SDR 色度参数")
except:
x_ideal = 0.3127
x_tolerance = 0.003
y_ideal = 0.3290
y_tolerance = 0.003
self.log_gui.log("⚠️ SDR 参数读取失败,使用默认值")
elif test_type == "hdr_movie":
try:
x_ideal = float(self.hdr_cct_x_ideal_var.get())
x_tolerance = float(self.hdr_cct_x_tolerance_var.get())
y_ideal = float(self.hdr_cct_y_ideal_var.get())
y_tolerance = float(self.hdr_cct_y_tolerance_var.get())
self.log_gui.log("✓ 使用 HDR 色度参数")
except:
x_ideal = 0.3127
x_tolerance = 0.003
y_ideal = 0.3290
y_tolerance = 0.003
self.log_gui.log("⚠️ HDR 参数读取失败,使用默认值")
else: # screen_module
try:
x_ideal = float(self.cct_x_ideal_var.get())
x_tolerance = float(self.cct_x_tolerance_var.get())
y_ideal = float(self.cct_y_ideal_var.get())
y_tolerance = float(self.cct_y_tolerance_var.get())
self.log_gui.log("✓ 使用屏模组色度参数")
except:
x_ideal = 0.306
x_tolerance = 0.003
y_ideal = 0.318
y_tolerance = 0.003
self.log_gui.log("⚠️ 屏模组参数读取失败,使用默认值")
x_low = x_ideal - x_tolerance
x_high = x_ideal + x_tolerance
y_low = y_ideal - y_tolerance
y_high = y_ideal + y_tolerance
self.log_gui.log(f"✓ 用户设置参数:")
self.log_gui.log(f" x-ideal={x_ideal:.4f}, tolerance={x_tolerance:.4f}")
self.log_gui.log(f" x范围: [{x_low:.4f}, {x_high:.4f}]")
self.log_gui.log(f" y-ideal={y_ideal:.4f}, tolerance={y_tolerance:.4f}")
self.log_gui.log(f" y范围: [{y_low:.4f}, {y_high:.4f}]")
# 为所有测试类型创建子图
ax1 = self.cct_fig.add_subplot(211)
ax2 = self.cct_fig.add_subplot(212)
# ========== 上图x coordinates ==========
ax1.plot(
grayscale,
x_measured,
"b-o",
label="屏本体",
linewidth=2,
markersize=4,
zorder=5,
)
# 为每个点添加数值标注x 坐标)
for i, (gs, x_val) in enumerate(zip(grayscale, x_measured)):
ax1.annotate(
f"{x_val:.5f}",
xy=(gs, x_val),
xytext=(0, 8),
textcoords="offset points",
ha="center",
va="bottom",
fontsize=7,
color="blue",
bbox=dict(
boxstyle="round,pad=0.2",
facecolor="white",
edgecolor="blue",
alpha=0.8,
linewidth=0.5,
),
)
# 绘制完整的参考线
full_grayscale = np.linspace(0, 100, 100)
ax1.axhline(
y=x_ideal,
color="green",
linestyle="--",
linewidth=1.5,
label=f"x-ideal ({x_ideal:.4f})",
zorder=3,
)
ax1.axhline(
y=x_low,
color="red",
linestyle=":",
linewidth=1,
alpha=0.7,
label=f"x-low ({x_low:.4f})",
zorder=2,
)
ax1.axhline(
y=x_high,
color="red",
linestyle=":",
linewidth=1,
alpha=0.7,
label=f"x-high ({x_high:.4f})",
zorder=2,
)
ax1.fill_between(
full_grayscale, x_low, x_high, alpha=0.15, color="blue", zorder=1
)
ax1.set_xlabel("灰阶 (%)", fontsize=9)
ax1.set_ylabel("CIE x", fontsize=9)
ax1.grid(True, linestyle="--", alpha=0.3)
ax1.tick_params(labelsize=8)
ax1.set_xlim(0, 105)
# 纵坐标范围由用户参数控制
x_min_data = min(x_measured)
x_max_data = max(x_measured)
data_range_x = x_max_data - x_min_data
self.log_gui.log(f" x数据波动: {data_range_x:.6f}")
range_span = x_tolerance * 2
margin_ratio = 0.20
extra_margin = range_span * margin_ratio
final_y_min = min(x_min_data, x_low) - extra_margin
final_y_max = max(x_max_data, x_high) + extra_margin
if x_min_data >= x_low and x_max_data <= x_high:
self.log_gui.log(f" x数据在tolerance范围内使用tolerance范围显示")
final_y_min = x_low - extra_margin
final_y_max = x_high + extra_margin
else:
self.log_gui.log(f" x数据超出tolerance范围扩展显示范围")
ax1.set_ylim(final_y_min, final_y_max)
self.log_gui.log(
f" x轴显示范围: {final_y_min:.6f} - {final_y_max:.6f} (跨度: {final_y_max - final_y_min:.6f})"
)
# ========== 下图y coordinates ==========
ax2.plot(
grayscale,
y_measured,
"r-o",
label="屏本体",
linewidth=2,
markersize=4,
zorder=5,
)
# 为每个点添加数值标注y 坐标)
for i, (gs, y_val) in enumerate(zip(grayscale, y_measured)):
ax2.annotate(
f"{y_val:.5f}",
xy=(gs, y_val),
xytext=(0, 8),
textcoords="offset points",
ha="center",
va="bottom",
fontsize=7,
color="red",
bbox=dict(
boxstyle="round,pad=0.2",
facecolor="white",
edgecolor="red",
alpha=0.8,
linewidth=0.5,
),
)
ax2.axhline(
y=y_ideal,
color="green",
linestyle="--",
linewidth=1.5,
label=f"y-ideal ({y_ideal:.4f})",
zorder=3,
)
ax2.axhline(
y=y_low,
color="orange",
linestyle=":",
linewidth=1,
alpha=0.7,
label=f"y-low ({y_low:.4f})",
zorder=2,
)
ax2.axhline(
y=y_high,
color="orange",
linestyle=":",
linewidth=1,
alpha=0.7,
label=f"y-high ({y_high:.4f})",
zorder=2,
)
ax2.fill_between(
full_grayscale, y_low, y_high, alpha=0.15, color="orange", zorder=1
)
ax2.set_xlabel("灰阶 (%)", fontsize=9)
ax2.set_ylabel("CIE y", fontsize=9)
ax2.grid(True, linestyle="--", alpha=0.3)
ax2.tick_params(labelsize=8)
ax2.set_xlim(0, 105)
# 纵坐标范围由用户参数控制
y_min_data = min(y_measured)
y_max_data = max(y_measured)
data_range_y = y_max_data - y_min_data
self.log_gui.log(f" y数据波动: {data_range_y:.6f}")
range_span = y_tolerance * 2
extra_margin = range_span * margin_ratio
final_y_min = min(y_min_data, y_low) - extra_margin
final_y_max = max(y_max_data, y_high) + extra_margin
if y_min_data >= y_low and y_max_data <= y_high:
self.log_gui.log(f" y数据在tolerance范围内使用tolerance范围显示")
final_y_min = y_low - extra_margin
final_y_max = y_high + extra_margin
else:
self.log_gui.log(f" y数据超出tolerance范围扩展显示范围")
ax2.set_ylim(final_y_min, final_y_max)
self.log_gui.log(
f" y轴显示范围: {final_y_min:.6f} - {final_y_max:.6f} (跨度: {final_y_max - final_y_min:.6f})"
)
# ========== 总标题 - 统一格式(去掉统计信息)==========
test_type_name = self.get_test_type_name(test_type)
self.cct_fig.suptitle(
f"{test_type_name} - 色度一致性测试",
fontsize=12,
y=0.98,
fontweight="bold",
)
self.cct_fig.subplots_adjust(
left=0.12,
right=0.82,
top=0.92,
bottom=0.08,
hspace=0.30,
)
ax1.legend(
fontsize=7, loc="center left", bbox_to_anchor=(1.05, 0.5), framealpha=1.0
)
ax2.legend(
fontsize=7, loc="center left", bbox_to_anchor=(1.05, 0.5), framealpha=1.0
)
self.cct_canvas.draw()
self.chart_notebook.select(2)
self.log_gui.log("✓ xy 色度坐标图绘制完成")

168
app/plots/plot_contrast.py Normal file
View File

@@ -0,0 +1,168 @@
"""对比度测试结果绘制。
Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_contrast 原样搬迁。
"""
from matplotlib.patches import Rectangle
def plot_contrast(app, contrast_data, test_type):
"""绘制对比度测试结果 - 固定布局版本"""
self = app
# 清空并重置
self.contrast_ax.clear()
self.contrast_ax.set_xlim(0, 1)
self.contrast_ax.set_ylim(0, 1)
self.contrast_ax.axis("off")
# 强制重置布局
self.contrast_fig.subplots_adjust(
left=0.02,
right=0.98,
top=0.90,
bottom=0.02,
)
max_lum = contrast_data["max_luminance"]
min_lum = contrast_data["min_luminance"]
contrast = contrast_data["contrast_ratio"]
# 确定等级和颜色
if contrast >= 5000:
grade, grade_color = "优秀", "#4CAF50"
elif contrast >= 3000:
grade, grade_color = "良好", "#8BC34A"
elif contrast >= 1000:
grade, grade_color = "合格", "#FFC107"
else:
grade, grade_color = "不合格", "#F44336"
test_type_name = self.get_test_type_name(test_type)
# ========== 顶部标题 - 统一格式 ==========
self.contrast_fig.suptitle(
f"{test_type_name} - 对比度测试",
fontsize=12,
y=0.98,
fontweight="bold",
)
# ========== 中央大对比度卡片 ==========
center_card = Rectangle(
(0.15, 0.48),
0.70,
0.32,
transform=self.contrast_ax.transAxes,
facecolor=grade_color,
edgecolor="black",
linewidth=2.5,
alpha=0.15,
)
self.contrast_ax.add_patch(center_card)
# 对比度数值
self.contrast_ax.text(
0.5,
0.65,
f"{contrast:.0f} : 1",
ha="center",
va="center",
fontsize=36,
fontweight="bold",
color=grade_color,
transform=self.contrast_ax.transAxes,
)
# 等级标签
self.contrast_ax.text(
0.5,
0.51,
f"等级: {grade}",
ha="center",
va="center",
fontsize=12,
fontweight="bold",
color=grade_color,
transform=self.contrast_ax.transAxes,
)
# ========== 两个信息卡片(缩小)==========
card_width = 0.32
card_height = 0.22
card_y = 0.12
gap = 0.05
total_width = card_width * 2 + gap
start_x = (1 - total_width) / 2
cards_data = [
{
"x": start_x,
"title": "白场亮度",
"value": f"{max_lum:.2f}",
"unit": "cd/m²",
"color": "#E3F2FD",
"edge_color": "#2196F3",
},
{
"x": start_x + card_width + gap,
"title": "黑场亮度",
"value": f"{min_lum:.4f}",
"unit": "cd/m²",
"color": "#F3E5F5",
"edge_color": "#9C27B0",
},
]
for card in cards_data:
# 绘制卡片背景
rect = Rectangle(
(card["x"], card_y),
card_width,
card_height,
transform=self.contrast_ax.transAxes,
facecolor=card["color"],
edgecolor=card["edge_color"],
linewidth=2,
)
self.contrast_ax.add_patch(rect)
# 标题
self.contrast_ax.text(
card["x"] + card_width / 2,
card_y + card_height - 0.03,
card["title"],
ha="center",
va="top",
fontsize=10,
fontweight="bold",
transform=self.contrast_ax.transAxes,
)
# 数值
self.contrast_ax.text(
card["x"] + card_width / 2,
card_y + card_height / 2,
card["value"],
ha="center",
va="center",
fontsize=16,
fontweight="bold",
transform=self.contrast_ax.transAxes,
)
# 单位
self.contrast_ax.text(
card["x"] + card_width / 2,
card_y + 0.03,
card["unit"],
ha="center",
va="bottom",
fontsize=9,
color="gray",
transform=self.contrast_ax.transAxes,
)
self.contrast_canvas.draw()
self.chart_notebook.select(3)

149
app/plots/plot_eotf.py Normal file
View File

@@ -0,0 +1,149 @@
"""EOTF 曲线绘制HDR
Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_eotf 原样搬迁。
"""
import numpy as np
def plot_eotf(app, L_bar, results_with_eotf_list, test_type):
"""绘制 EOTF 曲线 + 数据表格HDR 专用,包含实测亮度)"""
self = app
# ========== 1. 清空并重置左侧曲线 ==========
self.eotf_ax.clear()
self.eotf_ax.set_xlim(0, 105)
self.eotf_ax.set_ylim(0, 1.1)
self.eotf_ax.set_xlabel("灰阶 (%)", fontsize=10)
self.eotf_ax.set_ylabel("L_bar", fontsize=10)
self.eotf_ax.grid(True, linestyle="--", alpha=0.3)
self.eotf_ax.tick_params(labelsize=9)
# 生成横坐标(灰阶百分比)
x_values = np.linspace(0, 100, len(L_bar))
# 反转 L_bar
if len(L_bar) > 1 and L_bar[0] > L_bar[-1]:
L_bar = L_bar[::-1]
results_with_eotf_list = results_with_eotf_list[::-1]
# 计算平均 EOTF Gamma
eotf_values = []
for item in results_with_eotf_list:
if isinstance(item, (list, tuple)) and len(item) >= 4:
eotf = item[3]
if 0.5 < eotf < 5.0:
eotf_values.append(eotf)
avg_eotf = np.mean(eotf_values) if eotf_values else 2.2
# 绘制实测曲线
self.eotf_ax.plot(
x_values,
L_bar,
"b-o",
label=f"实测 (平均γ={avg_eotf:.2f})",
linewidth=2,
markersize=4,
zorder=5,
)
# 绘制 PQ (ST.2084) 理想曲线
pq_L_bar = self.calculate_pq_curve(x_values)
self.eotf_ax.plot(
x_values,
pq_L_bar,
"r--",
label="理想 PQ (ST.2084)",
linewidth=2,
alpha=0.7,
zorder=3,
)
# 图例
self.eotf_ax.legend(fontsize=9, loc="upper left", framealpha=0.95)
# ========== 2. 清空并绘制右侧表格 ==========
self.eotf_table_ax.clear()
self.eotf_table_ax.axis("off")
# 构建表格数据4列
table_data = [["灰阶", "实测亮度\n(cd/m²)", "L_bar", "EOTF γ"]]
for i, (x_val, L_val, result) in enumerate(
zip(x_values, L_bar, results_with_eotf_list)
):
# 提取实测亮度
if isinstance(result, (list, tuple)) and len(result) >= 3:
measured_lv = result[2]
measured_lv_str = f"{measured_lv:.2f}"
else:
measured_lv_str = "--"
# 提取 EOTF
if isinstance(result, (list, tuple)) and len(result) >= 4:
eotf = result[3]
if eotf < 0.5 or eotf > 5.0:
eotf_str = "--"
else:
eotf_str = f"{eotf:.2f}"
else:
eotf_str = "--"
table_data.append(
[
f"{x_val:.0f}%",
measured_lv_str,
f"{L_val:.3f}",
eotf_str,
]
)
# 绘制表格4列
table = self.eotf_table_ax.table(
cellText=table_data,
cellLoc="center",
loc="center",
colWidths=[0.18, 0.28, 0.27, 0.27],
)
# 美化表格
table.auto_set_font_size(False)
table.set_fontsize(7.5)
table.scale(1, 1.5)
# 表头样式
for i in range(4):
cell = table[(0, i)]
cell.set_facecolor("#4472C4")
cell.set_text_props(weight="bold", color="white")
# 数据行交替颜色
for i in range(1, len(table_data)):
for j in range(4):
cell = table[(i, j)]
if i % 2 == 0:
cell.set_facecolor("#E7E6E6")
else:
cell.set_facecolor("#FFFFFF")
# ========== 3. 总标题 ==========
test_type_name = self.get_test_type_name(test_type)
self.eotf_fig.suptitle(
f"{test_type_name} - EOTF 曲线PQ ST.2084",
fontsize=12,
y=0.98,
fontweight="bold",
)
# 选中 EOTF Tab
try:
eotf_tab_id = str(self.eotf_chart_frame)
current_tabs = list(self.chart_notebook.tabs())
if eotf_tab_id in current_tabs:
eotf_index = current_tabs.index(eotf_tab_id)
self.chart_notebook.select(eotf_index)
except:
pass
self.log_gui.log("EOTF 曲线 + 数据表格绘制完成")

143
app/plots/plot_gamma.py Normal file
View File

@@ -0,0 +1,143 @@
"""Gamma 曲线绘制。
Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_gamma 原样搬迁。
"""
import numpy as np
def plot_gamma(app, L_bar, results_with_gamma_list, target_gamma, test_type):
"""绘制Gamma曲线 + 数据表格(包含实测亮度)"""
self = app
# ========== 1. 清空并重置左侧曲线 ==========
self.gamma_ax.clear()
self.gamma_ax.set_xlim(0, 105)
self.gamma_ax.set_ylim(0, 1.1)
self.gamma_ax.set_xlabel("灰阶 (%)", fontsize=10)
self.gamma_ax.set_ylabel("L_bar", fontsize=10)
self.gamma_ax.grid(True, linestyle="--", alpha=0.3)
self.gamma_ax.tick_params(labelsize=9)
# 生成横坐标(灰阶百分比)
x_values = np.linspace(0, 100, len(L_bar))
# 反转 L_bar确保从左到右是 0% → 100%
if len(L_bar) > 1 and L_bar[0] > L_bar[-1]:
L_bar = L_bar[::-1]
results_with_gamma_list = results_with_gamma_list[::-1]
# 计算平均Gamma
gamma_values = []
for item in results_with_gamma_list:
if isinstance(item, (list, tuple)) and len(item) >= 4:
gamma = item[3]
if 0.5 < gamma < 5.0:
gamma_values.append(gamma)
avg_gamma = np.mean(gamma_values) if gamma_values else target_gamma
# 绘制实测曲线
self.gamma_ax.plot(
x_values,
L_bar,
"b-o",
label=f"实测 (平均γ={avg_gamma:.2f})",
linewidth=2,
markersize=4,
zorder=5,
)
# 绘制理想曲线(使用 target_gamma
ideal_L_bar = [(x / 100) ** target_gamma for x in x_values]
self.gamma_ax.plot(
x_values,
ideal_L_bar,
"r--",
label=f"理想 (γ={target_gamma})",
linewidth=2,
alpha=0.7,
zorder=3,
)
# 图例
self.gamma_ax.legend(fontsize=9, loc="upper left", framealpha=0.95)
# ========== 2. 清空并绘制右侧表格 ==========
self.gamma_table_ax.clear()
self.gamma_table_ax.axis("off")
# 构建表格数据4列
table_data = [["灰阶", "实测亮度\n(cd/m²)", "L_bar", "Gamma"]]
for i, (x_val, L_val, result) in enumerate(
zip(x_values, L_bar, results_with_gamma_list)
):
# 提取实测亮度
if isinstance(result, (list, tuple)) and len(result) >= 3:
measured_lv = result[2]
measured_lv_str = f"{measured_lv:.2f}"
else:
measured_lv_str = "--"
# 提取 Gamma
if isinstance(result, (list, tuple)) and len(result) >= 4:
gamma = result[3]
if gamma < 0.5 or gamma > 5.0:
gamma_str = "--"
else:
gamma_str = f"{gamma:.2f}"
else:
gamma_str = "--"
table_data.append(
[
f"{x_val:.0f}%",
measured_lv_str,
f"{L_val:.3f}",
gamma_str,
]
)
# 绘制表格4列
table = self.gamma_table_ax.table(
cellText=table_data,
cellLoc="center",
loc="center",
colWidths=[0.18, 0.28, 0.27, 0.27],
)
# 美化表格
table.auto_set_font_size(False)
table.set_fontsize(7.5)
table.scale(1, 1.5)
# 表头样式
for i in range(4):
cell = table[(0, i)]
cell.set_facecolor("#4472C4")
cell.set_text_props(weight="bold", color="white")
# 数据行交替颜色
for i in range(1, len(table_data)):
for j in range(4):
cell = table[(i, j)]
if i % 2 == 0:
cell.set_facecolor("#E7E6E6")
else:
cell.set_facecolor("#FFFFFF")
# ========== 3. 总标题 ==========
test_type_name = self.get_test_type_name(test_type)
self.gamma_fig.suptitle(
f"{test_type_name} - Gamma曲线",
fontsize=12,
y=0.98,
fontweight="bold",
)
# ========== 4. 绘制到画布 ==========
self.gamma_canvas.draw()
self.chart_notebook.select(1)
self.log_gui.log("Gamma曲线 + 数据表格绘制完成")

539
app/plots/plot_gamut.py Normal file
View File

@@ -0,0 +1,539 @@
"""色域图Gamut绘制。
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(app, results, coverage, test_type):
"""绘制色域图 - 根据用户选择的参考标准动态计算覆盖率"""
# 实现从原 PQAutomationApp 方法体原样搬迁,为减少修改面
# 范围、保持行为一致,给 self 赋值为传入的 app 实例。
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
# ========== ✅ 读取用户选择的参考标准 ==========
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"
# ========== ✅✅✅ 根据参考标准重新计算覆盖率XY 空间)==========
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:
self.log_gui.log(f"⚠️ 未知参考标准 '{current_ref}',使用 DCI-P3")
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_DCIP3(
xy_points
)
current_ref = "DCI-P3"
self.log_gui.log(
f"✓ XY 空间覆盖率({current_ref}: {xy_coverage:.1f}%"
)
except Exception as e:
self.log_gui.log(f"⚠️ 重新计算 XY 覆盖率失败: {str(e)}")
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]
self.log_gui.log(f"加载 XY 色域图: {w_xy}x{h_xy}")
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})"
)
# ========== 绘制测量三角形 ==========
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,
)
# ========== ✅ 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,
)
except Exception as e:
self.log_gui.log(f"XY 图绘制失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
# ========== 右图CIE 1976 u'v' ==========
try:
img_uv = mpimg.imread(get_resource_path("assets/cie_uv.png"))
h_uv, w_uv = img_uv.shape[:2]
self.log_gui.log(f"加载 UV 色域图: {w_uv}x{h_uv}")
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}")
# ========== ✅✅✅ 计算 u'v' 覆盖率(使用参考标准)==========
try:
uv_coverage = pq_algorithm.calculate_uv_gamut_coverage(
uv_coords, reference=current_ref
)
self.log_gui.log(
f"✓ UV 空间覆盖率({current_ref}: {uv_coverage:.1f}%"
)
except Exception as e:
self.log_gui.log(f"⚠️ 计算 UV 覆盖率失败: {str(e)}")
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,
)
# ========== ✅ 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,
)
except Exception as e:
self.log_gui.log(f"UV 图绘制失败: {str(e)}")
import traceback
self.log_gui.log(traceback.format_exc())
# ========== 总标题 ==========
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()
self.chart_notebook.select(0)
self.log_gui.log("色域图绘制完成")

File diff suppressed because it is too large Load Diff