"""色域图(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(self.gamut_chart_frame) self.log_gui.log("色域图绘制完成")