"""色准测试结果绘制。 布局: - 左侧:大尺寸 ColorChecker 条形图(每个条形使用对应颜色)。 - 右侧:CIE 1976 u'v' 色度图(目标点/实测点/偏移连线)。 """ from typing import TYPE_CHECKING from matplotlib.patches import Rectangle from matplotlib.lines import Line2D from matplotlib.patches import Circle from app.plots.gamut_background import get_cie1976_background from app.tests.color_accuracy import get_accuracy_color_standards from app.pq.color_patch_map import get_patch_color from app.pq.color_patch_map import get_patch_color_from_xy if TYPE_CHECKING: from pqAutomationApp import PQAutomationApp # ============================================================ # 常量 # ============================================================ _COLOR_MAP = { "White": "#FFFFFF", "Gray 80": "#E6E6E6", "Gray 65": "#D1D1D1", "Gray 50": "#BABABA", "Gray 35": "#9E9E9E", "Black": "#000000", "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", "100% Red": "#FF0000", "100% Green": "#00FF00", "100% Blue": "#0000FF", "100% Cyan": "#00FFFF", "100% Magenta": "#FF00FF", "100% Yellow": "#FFFF00", } def _xy_to_uv(x: float, y: float): """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 # ============================================================ # 子图:左侧 Calman 风格面板 # ============================================================ def _draw_left_panel(ax, color_patches, delta_e_values, font_scale=1.0, dark_mode=False): """左侧仅保留大条形图""" ax.clear() n = len(color_patches) if n == 0: ax.set_axis_off() return y_pos = list(range(n)) bar_colors = [_COLOR_MAP.get(name, "#888888") for name in color_patches] ax.barh( y_pos, delta_e_values, height=0.72, color=bar_colors, edgecolor="#202020", linewidth=0.5, zorder=3, ) text_color = "#F3F5F7" if dark_mode else "#111111" bg_color = "#0F1115" if dark_mode else "#FFFFFF" spine_color = "#8C8F94" if dark_mode else "#9A9A9A" ax.set_yticks(y_pos) ax.set_yticklabels(color_patches, fontsize=max(5, 7 * font_scale), color=text_color) ax.invert_yaxis() x_max = max(15.0, max(delta_e_values) * 1.15) ax.set_xlim(0, x_max) ax.tick_params(axis="x", labelsize=max(6, 8 * font_scale), colors=text_color) ax.grid(axis="x", linestyle="-", linewidth=0.6, alpha=0.3, zorder=0) ax.grid(axis="y", linestyle=":", linewidth=0.35, alpha=0.15, zorder=0) ax.set_facecolor(bg_color) for spine in ax.spines.values(): spine.set_color(spine_color) spine.set_linewidth(0.9) # ============================================================ # 子图:CIE 1976 u'v' 色度图(目标 vs 实测) # ============================================================ def _draw_uv_diagram(ax, color_patches, measurements, standards, font_scale=1.0, dark_mode=False): """绘制 CIE 1976 u'v' 上的色准对比。""" ax.clear() try: bg, bbox = get_cie1976_background() xmin, xmax, ymin, ymax = bbox ax.imshow( bg, extent=(xmin, xmax, ymin, ymax), origin="lower", interpolation="bicubic", zorder=0, aspect="auto", ) ax.set_xlim(xmin, xmax) ax.set_ylim(ymin, ymax) except Exception: ax.set_xlim(0.0, 0.65) ax.set_ylim(0.0, 0.60) text_color = "#F3F5F7" if dark_mode else "#111111" sub_text_color = "#D3D7DD" if dark_mode else "#222222" tick_color = "#D3D7DD" if dark_mode else "#222222" legend_label_color = "#FFF" if dark_mode else "#111" legend_bg = "#111" if dark_mode else "#FFFFFF" legend_edge = "#FFF" if dark_mode else "#333" outer_edge = "#FFFFFF" if dark_mode else "#222222" ax.set_facecolor("#000" if dark_mode else "#FFFFFF") ax.set_aspect("equal", adjustable="box") ax.set_title( "CIE 1976 u'v'", fontsize=max(8, 11 * font_scale), fontweight="bold", color=text_color, pad=4, ) ax.set_xlabel("u'", fontsize=max(7, 9 * font_scale), color=sub_text_color, labelpad=1) ax.set_ylabel("v'", fontsize=max(7, 9 * font_scale), color=sub_text_color, labelpad=1) ax.tick_params(axis="both", labelsize=max(6, 8 * font_scale), colors=tick_color) for sp in ax.spines.values(): sp.set_color(outer_edge) sp.set_linewidth(0.9) for name, meas in zip(color_patches, measurements): if meas is None or len(meas) < 2: continue mx, my = meas[0], meas[1] sxy = standards.get(name) if sxy is None: continue sx, sy = sxy m_u, m_v = _xy_to_uv(mx, my) s_u, s_v = _xy_to_uv(sx, sy) # face = get_patch_color_from_xy(name, (sx, sy)).strip().upper() face = _COLOR_MAP.get(name, "#888888") print(name, face) # 目标点(Target) 空心方框 ax.scatter( s_u, s_v, s=90, marker="s", facecolors="none", edgecolors=outer_edge, linewidths=1.6, zorder=18, ) # 实测点(Actual) 彩色实心 + 白色描边 ax.scatter( [m_u], [m_v], s=80, marker="o", color=face, edgecolors=outer_edge, linewidths=1.2, zorder=20, ) # # Δu'v' 偏差连线 # ax.plot( # [s_u, m_u], # [s_v, m_v], # color=face, # linewidth=1.0, # alpha=0.8, # zorder=15, # ) legend_handles = [ Line2D( [0], [0], marker="s", linestyle="none", markerfacecolor="none", markeredgecolor=outer_edge, markersize=9, markeredgewidth=1.4, label="目标 (Target)", ), Line2D( [0], [0], marker="o", linestyle="none", markerfacecolor="#AAAAAA", markeredgecolor=outer_edge, markersize=9, markeredgewidth=1.2, label="实测 (Actual)", ), ] leg = ax.legend( handles=legend_handles, loc="lower right", fontsize=max(6, 8 * font_scale), framealpha=0.9, labelcolor=legend_label_color, ) if leg: leg.get_frame().set_facecolor(legend_bg) leg.get_frame().set_edgecolor(legend_edge) leg.set_zorder(50) def _draw_result_judgement(ax, accuracy_data, font_scale=1.0, dark_mode=False): """底部结果条""" ax.clear() ax.set_xlim(0, 1) ax.set_ylim(0, 1) ax.axis("off") avg = accuracy_data.get("avg_delta_e", 0.0) mx = accuracy_data.get("max_delta_e", 0.0) panel_bg = "#1A1E24" if dark_mode else "#FFFFFF" panel_edge = "#4A5058" if dark_mode else "#C6C6C6" text_color = "#F3F5F7" if dark_mode else "#111111" ax.add_patch(Rectangle( (0.0, 0.10), 1.0, 0.80, transform=ax.transAxes, facecolor=panel_bg, edgecolor=panel_edge, linewidth=1.0, )) ax.text( 0.03, 0.50, f"Avg dE2000: {avg:.2f}", ha="left", va="center", fontsize=max(11, 20 * font_scale), fontweight="normal", color=text_color, transform=ax.transAxes, ) ax.text( 0.52, 0.50, f"Max dE2000: {mx:.2f}", ha="left", va="center", fontsize=max(11, 20 * font_scale), fontweight="normal", color=text_color, transform=ax.transAxes, ) # ============================================================ # 主入口 # ============================================================ def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type): """绘制色准测试结果 - Calman 风格(色块 + CIE 1976 u'v' + 统计)。""" fig = self.accuracy_fig fig.clear() try: from app.views.theme_manager import is_dark dark_mode = is_dark() except Exception: dark_mode = False fig.patch.set_facecolor("#1B1F24" if dark_mode else "#FFFFFF") # 根据当前画布像素尺寸动态缩放字体,避免窗口缩小时文字挤压重叠。 font_scale = 1.0 try: canvas_widget = self.accuracy_canvas.get_tk_widget() cw = max(1, int(canvas_widget.winfo_width())) ch = max(1, int(canvas_widget.winfo_height())) font_scale = min(cw / 1000.0, ch / 600.0) font_scale = max(0.60, min(1.0, font_scale)) except Exception: font_scale = 1.0 color_patches = accuracy_data.get("color_patches", []) or [] delta_e_values = accuracy_data.get("delta_e_values", []) or [] measurements = accuracy_data.get("color_measurements", []) or [] try: target_gamma = float(accuracy_data.get("target_gamma", 2.2)) except (TypeError, ValueError): target_gamma = 2.2 test_type_name = self.get_test_type_name(test_type) 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: title = f"{test_type_name} - 色准测试(全 29色 | Gamma {target_gamma})" title_color = "#F3F5F7" if dark_mode else "#111" fig.suptitle( title, fontsize=max(8, 11 * font_scale), y=0.975, fontweight="bold", color=title_color, ) gs = fig.add_gridspec( 2, 2, width_ratios=[1.12, 1.0], height_ratios=[4.8, 0.48], left=0.08, right=0.985, top=0.92, bottom=0.05, wspace=0.14, hspace=0.08, ) ax_left = fig.add_subplot(gs[0, 0]) ax_uv = fig.add_subplot(gs[0, 1]) ax_judge = fig.add_subplot(gs[1, :]) # 兼容外部对 self.accuracy_ax 的引用 self.accuracy_ax = ax_judge _draw_left_panel( ax_left, color_patches, delta_e_values, font_scale=font_scale, dark_mode=dark_mode, ) try: standards = get_accuracy_color_standards(test_type) except Exception: standards = {} _draw_uv_diagram( ax_uv, color_patches, measurements, standards, font_scale=font_scale, dark_mode=dark_mode, ) _draw_result_judgement( ax_judge, accuracy_data, font_scale=font_scale, dark_mode=dark_mode, ) try: self.update_accuracy_result_table(accuracy_data, standards) except Exception: pass # Select the tab first so the canvas is visible and winfo_width/height # return real pixel dimensions before the figure is rendered. self.chart_notebook.select(self.accuracy_chart_frame) self.accuracy_canvas.get_tk_widget().update_idletasks() self.accuracy_canvas.draw() class PlotAccuracyMixin: """由 tools/refactor_to_mixins.py 自动生成。 把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。 """ plot_accuracy = plot_accuracy