"""CCT / 色度一致性绘制。 Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_cct 原样搬迁。 """ import numpy as np from app.views.modern_styles import get_theme_palette from typing import TYPE_CHECKING if TYPE_CHECKING: from pqAutomationApp import PQAutomationApp def _is_dark_palette(palette: dict[str, str]) -> bool: """根据主题背景色亮度判断是否深色主题。""" bg = palette.get("bg", "#FFFFFF").lstrip("#") try: r = int(bg[0:2], 16) g = int(bg[2:4], 16) b = int(bg[4:6], 16) except Exception: return False return (r * 299 + g * 587 + b * 114) / 1000 < 128 def plot_cct(self: "PQAutomationApp", test_type): """绘制 x 和 y 坐标分离图 - 每个点标注纵坐标值""" palette = get_theme_palette() dark_mode = _is_dark_palette(palette) x_line_color = "#2F8BFF" if dark_mode else "#0A4BFF" y_line_color = "#FF4D4D" if dark_mode else "#D90429" ideal_line_color = "#00C853" if dark_mode else "#198754" x_tol_color = "#FF7070" if dark_mode else "#C0392B" y_tol_color = "#FFB74D" if dark_mode else "#D68910" grid_color = "#566070" if dark_mode else "#B8BDC3" axis_text = "#EDF2FA" if dark_mode else palette["fg"] axis_sub_text = "#C8D2E0" if dark_mode else "#222222" legend_bg = "#131821" if dark_mode else "#FFFFFF" legend_edge = "#A4B2C6" if dark_mode else palette["border"] self.cct_fig.clear() self.cct_fig.patch.set_facecolor(palette["bg"]) 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 数据可用", level="error") ax = self.cct_fig.add_subplot(111) ax.text( 0.5, 0.5, "无可用数据", ha="center", va="center", fontsize=14, color=palette["danger"], ) 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)}", level="success") self.log_gui.log(f" x范围: {min(x_measured):.6f} - {max(x_measured):.6f}", level="info") self.log_gui.log(f" y范围: {min(y_measured):.6f} - {max(y_measured):.6f}", level="info") # ========== 根据测试类型读取对应参数 ========== 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 色度参数", level="success") except: x_ideal = 0.3127 x_tolerance = 0.003 y_ideal = 0.3290 y_tolerance = 0.003 self.log_gui.log("SDR 参数读取失败,使用默认值", level="error") 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 色度参数", level="success") except: x_ideal = 0.3127 x_tolerance = 0.003 y_ideal = 0.3290 y_tolerance = 0.003 self.log_gui.log("HDR 参数读取失败,使用默认值", level="error") 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("使用屏模组色度参数", level="success") except: x_ideal = 0.306 x_tolerance = 0.003 y_ideal = 0.318 y_tolerance = 0.003 self.log_gui.log("屏模组参数读取失败,使用默认值", level="error") 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"用户设置参数:", level="success") self.log_gui.log(f" x-ideal={x_ideal:.4f}, tolerance={x_tolerance:.4f}", level="info") self.log_gui.log(f" x范围: [{x_low:.4f}, {x_high:.4f}]", level="info") self.log_gui.log(f" y-ideal={y_ideal:.4f}, tolerance={y_tolerance:.4f}", level="info") self.log_gui.log(f" y范围: [{y_low:.4f}, {y_high:.4f}]", level="info") # 为所有测试类型创建子图 ax1 = self.cct_fig.add_subplot(211) ax2 = self.cct_fig.add_subplot(212) for ax in (ax1, ax2): ax.set_facecolor(palette["card_bg"]) for spine in ax.spines.values(): spine.set_color(palette["border"]) ax.tick_params(labelsize=8, colors=axis_sub_text) ax.xaxis.label.set_color(axis_text) ax.yaxis.label.set_color(axis_text) # ========== 上图:x coordinates ========== ax1.plot( grayscale, x_measured, color=x_line_color, marker="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=x_line_color, bbox=dict( boxstyle="round,pad=0.2", facecolor=palette["card_bg"], edgecolor=x_line_color, alpha=0.92 if dark_mode else 0.85, linewidth=0.8, ), ) # 绘制完整的参考线 full_grayscale = np.linspace(0, 100, 100) ax1.axhline( y=x_ideal, color=ideal_line_color, linestyle="--", linewidth=1.5, label=f"x-ideal ({x_ideal:.4f})", zorder=3, ) ax1.axhline( y=x_low, color=x_tol_color, linestyle=":", linewidth=1, alpha=0.95 if dark_mode else 0.7, label=f"x-low ({x_low:.4f})", zorder=2, ) ax1.axhline( y=x_high, color=x_tol_color, linestyle=":", linewidth=1, alpha=0.95 if dark_mode else 0.7, label=f"x-high ({x_high:.4f})", zorder=2, ) ax1.fill_between( full_grayscale, x_low, x_high, alpha=0.22 if dark_mode else 0.15, color=x_line_color, zorder=1, ) ax1.set_xlabel("灰阶 (%)", fontsize=9, color=axis_text) ax1.set_ylabel("CIE x", fontsize=9, color=axis_text) ax1.grid(True, linestyle="--", alpha=0.45 if dark_mode else 0.3, color=grid_color) 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}", level="info") 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范围显示", level="info") final_y_min = x_low - extra_margin final_y_max = x_high + extra_margin else: self.log_gui.log(f" x数据超出tolerance范围,扩展显示范围", level="info") 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})" , level="info") # ========== 下图:y coordinates ========== ax2.plot( grayscale, y_measured, color=y_line_color, marker="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=y_line_color, bbox=dict( boxstyle="round,pad=0.2", facecolor=palette["card_bg"], edgecolor=y_line_color, alpha=0.92 if dark_mode else 0.85, linewidth=0.8, ), ) ax2.axhline( y=y_ideal, color=ideal_line_color, linestyle="--", linewidth=1.5, label=f"y-ideal ({y_ideal:.4f})", zorder=3, ) ax2.axhline( y=y_low, color=y_tol_color, linestyle=":", linewidth=1, alpha=0.95 if dark_mode else 0.7, label=f"y-low ({y_low:.4f})", zorder=2, ) ax2.axhline( y=y_high, color=y_tol_color, linestyle=":", linewidth=1, alpha=0.95 if dark_mode else 0.7, label=f"y-high ({y_high:.4f})", zorder=2, ) ax2.fill_between( full_grayscale, y_low, y_high, alpha=0.22 if dark_mode else 0.15, color=y_tol_color, zorder=1, ) ax2.set_xlabel("灰阶 (%)", fontsize=9, color=axis_text) ax2.set_ylabel("CIE y", fontsize=9, color=axis_text) ax2.grid(True, linestyle="--", alpha=0.45 if dark_mode else 0.3, color=grid_color) 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}", level="info") 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范围显示", level="info") final_y_min = y_low - extra_margin final_y_max = y_high + extra_margin else: self.log_gui.log(f" y数据超出tolerance范围,扩展显示范围", level="info") 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})" , level="info") # ========== 总标题 - 统一格式(去掉统计信息)========== 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", color=palette["fg"], ) self.cct_fig.subplots_adjust( left=0.12, right=0.82, top=0.92, bottom=0.08, hspace=0.30, ) legend1 = ax1.legend( fontsize=7, loc="center left", bbox_to_anchor=(1.05, 0.5), framealpha=0.92 if dark_mode else 1.0, facecolor=legend_bg, edgecolor=legend_edge, ) legend2 = ax2.legend( fontsize=7, loc="center left", bbox_to_anchor=(1.05, 0.5), framealpha=0.92 if dark_mode else 1.0, facecolor=legend_bg, edgecolor=legend_edge, ) for legend in (legend1, legend2): for text in legend.get_texts(): text.set_color(axis_text) self.cct_canvas.draw() self.chart_notebook.select(self.cct_chart_frame) self.log_gui.log("xy 色度坐标图绘制完成", level="success") class PlotCctMixin: """由 tools/refactor_to_mixins.py 自动生成。 把本模块的自由函数挂到 PQAutomationApp 上,便于 F12 跳转与类型推断。 """ plot_cct = plot_cct