From 8916f2fff06c5657c81ce3b421a831d49c05ac87 Mon Sep 17 00:00:00 2001 From: "xinzhu.yin" Date: Tue, 9 Jun 2026 11:02:55 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9SDR=E8=89=B2=E5=87=86?= =?UTF-8?q?=E6=B7=B1=E8=89=B2=E5=BC=82=E5=B8=B8=E3=80=81=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E7=BB=93=E6=9E=9C=E6=B7=B1=E8=89=B2=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/export/image_exporter.py | 19 ++- app/plots/gamut_background.py | 199 ++++++++++++++++++++++++-------- app/plots/plot_accuracy.py | 50 ++++---- app/plots/plot_gamut.py | 4 +- app/views/panels/main_layout.py | 9 ++ settings/pq_config.json | 8 +- 6 files changed, 206 insertions(+), 83 deletions(-) diff --git a/app/export/image_exporter.py b/app/export/image_exporter.py index f5dd3a4..7a99dba 100644 --- a/app/export/image_exporter.py +++ b/app/export/image_exporter.py @@ -2,15 +2,14 @@ import os -_EXPORT_BG_COLOR = "#FFFFFF" - - -def _save_with_light_background(fig, path, *, dpi=300, bbox_inches=None): - """导出统一浅色背景,避免深色主题下图片背景变暗。""" +def _save_with_theme_background(fig, path, *, dpi=300, bbox_inches=None): + """按图表当前主题背景导出,避免深色模式下被强制写成白底。""" + bg = fig.get_facecolor() kwargs = { "dpi": dpi, - "facecolor": _EXPORT_BG_COLOR, - "edgecolor": _EXPORT_BG_COLOR, + "facecolor": bg, + "edgecolor": bg, + "transparent": False, } if bbox_inches is not None: kwargs["bbox_inches"] = bbox_inches @@ -85,7 +84,7 @@ def save_result_images(result_dir, current_test_type, selected_items, continue per_ref_name = f"色域测试结果_{ref}.png" path = os.path.join(result_dir, per_ref_name) - _save_with_light_background(fig, path, dpi=300) + _save_with_theme_background(fig, path, dpi=300) log(f"已保存: {per_ref_name}") finally: ref_var.set(original_ref) @@ -97,7 +96,7 @@ def save_result_images(result_dir, current_test_type, selected_items, continue path = os.path.join(result_dir, filename) if default_bbox: - _save_with_light_background(fig, path, dpi=300) + _save_with_theme_background(fig, path, dpi=300) else: - _save_with_light_background(fig, path, dpi=300, bbox_inches="tight") + _save_with_theme_background(fig, path, dpi=300, bbox_inches="tight") log(f"已保存: {filename}") diff --git a/app/plots/gamut_background.py b/app/plots/gamut_background.py index 97c811c..3a4b690 100644 --- a/app/plots/gamut_background.py +++ b/app/plots/gamut_background.py @@ -1,14 +1,19 @@ -"""CIE 色度图底图渲染与缓存。 +""" +CIE 色度图底图渲染与缓存(工业版) -将"重型图像渲染"(colour-science 的谱迹颜色填充)与"轻量框架数据层" -(参考/实测三角形、标签、覆盖率)解耦。 +特点: +- colour-science 谱迹渲染 +- numpy RGBA 缓存 +- 内存 + 磁盘缓存 +- 支持 light / dark UI +- 启动预热 +- 线程安全 -底图: -- 仅在首次调用或缓存失效时通过 colour-science 渲染一次; -- 渲染结果保存为 numpy RGBA 数组,同时落盘到 settings/cache/, - 下次启动直接 imread 加载,避免重新跑色彩科学计算。 +调用方式: + +bg, bbox = get_cie1931_background(mode="dark") +ax.imshow(bg, extent=bbox) -调用方在每次绘图时只需 `ax.imshow(bg, extent=bbox)`,再叠加自己的矢量层。 """ from __future__ import annotations @@ -20,91 +25,140 @@ from typing import Tuple import numpy as np -# 谱迹底图分辨率(边长,单位像素)。1024 对于 14 inch 画布足够细腻, -# 文件大小 ~1-2MB,单次渲染 ~0.5-1 s,缓存后毫秒级加载。 +# ---------------------------- +# 配置 +# ---------------------------- + +# 渲染分辨率 _DIAGRAM_RES = 1024 -# 缓存版本号:当渲染参数或风格调整时递增,强制重新生成。 -_CACHE_VERSION = "v1" +# 缓存版本(风格变化时递增) +_CACHE_VERSION = "v2" -_BBox = Tuple[float, float, float, float] # (xmin, xmax, ymin, ymax) +# UI 颜色 +_DARK_BG = "#0f1115" +_LIGHT_BG = "#ffffff" + +_BBox = Tuple[float, float, float, float] _CIE1931_BBOX: _BBox = (0.0, 0.8, 0.0, 0.9) _CIE1976_BBOX: _BBox = (0.0, 0.65, 0.0, 0.6) - _memory_cache: dict[str, np.ndarray] = {} _lock = threading.Lock() +# ---------------------------- +# cache path +# ---------------------------- + def _cache_dir() -> str: - # 项目根目录通过本文件位置反推:app/plots/ -> 项目根 here = os.path.dirname(os.path.abspath(__file__)) root = os.path.abspath(os.path.join(here, "..", "..")) + d = os.path.join(root, "settings", "cache") os.makedirs(d, exist_ok=True) + return d -def _cache_key(kind: str, bbox: _BBox) -> str: - sig = f"{kind}|{bbox}|{_DIAGRAM_RES}|{_CACHE_VERSION}" - h = hashlib.md5(sig.encode("utf-8")).hexdigest()[:10] - return f"chromaticity_{kind}_{h}.npy" +def _cache_key(kind: str, bbox: _BBox, mode: str) -> str: + sig = f"{kind}|{bbox}|{mode}|{_DIAGRAM_RES}|{_CACHE_VERSION}" + h = hashlib.md5(sig.encode()).hexdigest()[:10] + + return f"chromaticity_{kind}_{mode}_{h}.npy" -def _cache_path(kind: str, bbox: _BBox) -> str: - return os.path.join(_cache_dir(), _cache_key(kind, bbox)) +def _cache_path(kind: str, bbox: _BBox, mode: str) -> str: + return os.path.join(_cache_dir(), _cache_key(kind, bbox, mode)) -def _render_chromaticity(kind: str, bbox: _BBox) -> np.ndarray: - """通过 colour-science 离屏渲染谱迹底图,返回 RGBA float 数组。""" - # 延迟导入:仅在缓存未命中时支付 colour.plotting 的加载开销。 +# ---------------------------- +# 渲染 +# ---------------------------- + +def _render_chromaticity(kind: str, bbox: _BBox, mode: str) -> np.ndarray: + """ + 通过 colour-science 渲染 chromaticity 图。 + """ + import matplotlib + prev_backend = matplotlib.get_backend() + try: matplotlib.use("Agg", force=True) except Exception: pass import matplotlib.pyplot as plt + import colour from colour.plotting import ( plot_chromaticity_diagram_CIE1931, plot_chromaticity_diagram_CIE1976UCS, ) + if mode == "dark": + colour.plotting.colour_style("dark") + bg_color = _DARK_BG + else: + colour.plotting.colour_style("light") + bg_color = _LIGHT_BG + xmin, xmax, ymin, ymax = bbox + aspect = (xmax - xmin) / (ymax - ymin) + height = _DIAGRAM_RES width = int(round(height * aspect)) - fig = plt.figure(figsize=(width / 100.0, height / 100.0), dpi=100) - ax = fig.add_axes([0.0, 0.0, 1.0, 1.0]) + fig = plt.figure( + figsize=(width / 100.0, height / 100.0), + dpi=100 + ) + + fig.patch.set_facecolor(bg_color) + + ax = fig.add_axes([0, 0, 1, 1]) + ax.set_facecolor(bg_color) if kind == "cie1931": + plot_chromaticity_diagram_CIE1931( - axes=ax, show=False, title=False, - tight_layout=False, transparent_background=True, + axes=ax, + show=False, + title=False, + tight_layout=False, bounding_box=bbox, + transparent_background=False, ) + elif kind == "cie1976": + plot_chromaticity_diagram_CIE1976UCS( - axes=ax, show=False, title=False, - tight_layout=False, transparent_background=True, + axes=ax, + show=False, + title=False, + tight_layout=False, bounding_box=bbox, + transparent_background=False, ) + else: plt.close(fig) - raise ValueError(f"unknown diagram kind: {kind!r}") + raise ValueError(f"unknown diagram kind: {kind}") ax.set_xlim(xmin, xmax) ax.set_ylim(ymin, ymax) + ax.set_axis_off() - ax.set_position([0.0, 0.0, 1.0, 1.0]) + ax.set_position([0, 0, 1, 1]) fig.canvas.draw() - # 从 canvas 抓取 RGBA 数组 + buf = np.asarray(fig.canvas.buffer_rgba()).copy() buf = np.flipud(buf) + plt.close(fig) try: @@ -115,52 +169,107 @@ def _render_chromaticity(kind: str, bbox: _BBox) -> np.ndarray: return buf -def _load_or_render(kind: str, bbox: _BBox) -> np.ndarray: - key = _cache_key(kind, bbox) +# ---------------------------- +# load / render +# ---------------------------- + +def _load_or_render(kind: str, bbox: _BBox, mode: str) -> np.ndarray: + + key = _cache_key(kind, bbox, mode) + with _lock: + if key in _memory_cache: return _memory_cache[key] - disk = _cache_path(kind, bbox) + disk = _cache_path(kind, bbox, mode) + if os.path.isfile(disk): + try: arr = np.load(disk) + _memory_cache[key] = arr + return arr + except Exception: - # 缓存损坏则重新渲染 + try: os.remove(disk) except OSError: pass - arr = _render_chromaticity(kind, bbox) + arr = _render_chromaticity(kind, bbox, mode) + _memory_cache[key] = arr + try: np.save(disk, arr) except Exception: pass + return arr -def get_cie1931_background() -> Tuple[np.ndarray, _BBox]: - """返回 (RGBA 数组, bbox),可直接 ax.imshow(arr, extent=[*bbox])。""" - return _load_or_render("cie1931", _CIE1931_BBOX), _CIE1931_BBOX +# ---------------------------- +# public API +# ---------------------------- + +def get_cie1931_background(mode: str = "dark") -> Tuple[np.ndarray, _BBox]: + """ + 获取 CIE1931 背景图 + + mode: + "dark" + "light" + """ + + return _load_or_render("cie1931", _CIE1931_BBOX, mode), _CIE1931_BBOX -def get_cie1976_background() -> Tuple[np.ndarray, _BBox]: - return _load_or_render("cie1976", _CIE1976_BBOX), _CIE1976_BBOX +def get_cie1976_background(mode: str = "dark") -> Tuple[np.ndarray, _BBox]: + return _load_or_render("cie1976", _CIE1976_BBOX, mode), _CIE1976_BBOX + + +# ---------------------------- +# cache control +# ---------------------------- def clear_cache(*, disk: bool = False) -> None: - """清空内存缓存(可选连同磁盘)。供调试/样式调整时使用。""" + """ + 清空缓存 + """ + with _lock: + _memory_cache.clear() + if disk: + d = _cache_dir() + for name in os.listdir(d): - if name.startswith("chromaticity_") and name.endswith(".npy"): + + if name.startswith("chromaticity_"): + try: os.remove(os.path.join(d, name)) except OSError: pass + + +# ---------------------------- +# warmup +# ---------------------------- + +def warmup_cache(mode: str = "dark") -> None: + """ + 启动预热缓存 + + 可在软件启动时调用,避免首次绘图卡顿。 + """ + + get_cie1931_background(mode) + get_cie1976_background(mode) \ No newline at end of file diff --git a/app/plots/plot_accuracy.py b/app/plots/plot_accuracy.py index 6bc0155..9b75e18 100644 --- a/app/plots/plot_accuracy.py +++ b/app/plots/plot_accuracy.py @@ -9,15 +9,10 @@ from typing import TYPE_CHECKING from matplotlib.patches import Rectangle from matplotlib.lines import Line2D -import matplotlib.colors as mcolors -import numpy as np - +from matplotlib.ticker import MultipleLocator, AutoMinorLocator from app.views.modern_styles import get_theme_palette - 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 @@ -68,13 +63,13 @@ def _xy_to_uv(x: float, y: float): 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) @@ -85,37 +80,43 @@ def _draw_left_panel(ax, color_patches, delta_e_values, font_scale=1.0, dark_mod y_pos = list(range(n)) bar_colors = [_COLOR_MAP.get(name, "#888888") for name in color_patches] + edgecolor = "#F3F5F7" if dark_mode else "#202020" + text_color = "#F3F5F7" if dark_mode else "#111111" + bg_color = "#0F1115" if dark_mode else "#FFFFFF" + ax.barh( y_pos, delta_e_values, height=0.72, color=bar_colors, - edgecolor="#202020", + edgecolor=edgecolor, 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.tick_params( + axis="x", + labelsize=max(6, 8 * font_scale), + colors=text_color + ) + + ax.tick_params( + axis="y", + labelsize=max(5, 7 * font_scale), + colors=text_color + ) ax.set_facecolor(bg_color) - for spine in ax.spines.values(): - spine.set_color(spine_color) - spine.set_linewidth(0.9) - - + # 自动 minor tick + ax.xaxis.set_minor_locator(AutoMinorLocator(2)) # ============================================================ @@ -126,7 +127,7 @@ def _draw_uv_diagram(ax, color_patches, measurements, standards, font_scale=1.0, """绘制 CIE 1976 u'v' 上的色准对比。""" ax.clear() try: - bg, bbox = get_cie1976_background() + bg, bbox = get_cie1976_background(mode="dark" if dark_mode else "light") if bg.shape[-1] == 4: bg = bg[:, :, :3] xmin, xmax, ymin, ymax = bbox @@ -154,6 +155,11 @@ def _draw_uv_diagram(ax, color_patches, measurements, standards, font_scale=1.0, ax.set_facecolor("#000" if dark_mode else "#FFFFFF") ax.set_aspect("equal", adjustable="box") + ax.xaxis.set_major_locator(MultipleLocator(0.1)) + ax.yaxis.set_major_locator(MultipleLocator(0.1)) + + ax.xaxis.set_minor_locator(MultipleLocator(0.02)) + ax.yaxis.set_minor_locator(MultipleLocator(0.02)) ax.set_title( "CIE 1976 u'v'", @@ -309,7 +315,7 @@ def _draw_result_judgement(ax, accuracy_data, font_scale=1.0, dark_mode=False): # ============================================================ def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type): - """绘制色准测试结果 - Calman 风格(色块 + CIE 1976 u'v' + 统计)。""" + """绘制色准测试结果""" palette = get_theme_palette() try: from app.views.theme_manager import is_dark diff --git a/app/plots/plot_gamut.py b/app/plots/plot_gamut.py index d2366e0..bbb8111 100644 --- a/app/plots/plot_gamut.py +++ b/app/plots/plot_gamut.py @@ -271,7 +271,7 @@ def plot_gamut(self: "PQAutomationApp", results, coverage, test_type): # 左图:CIE 1931 xy # ============================================================ try: - bg_xy, bbox_xy = get_cie1931_background() + bg_xy, bbox_xy = get_cie1931_background(mode="dark" if dark_mode else "light") _blit_background(ax_xy, bg_xy, bbox_xy) _style_axes( ax_xy, @@ -343,7 +343,7 @@ def plot_gamut(self: "PQAutomationApp", results, coverage, test_type): # 右图:CIE 1976 u'v' # ============================================================ try: - bg_uv, bbox_uv = get_cie1976_background() + bg_uv, bbox_uv = get_cie1976_background(mode="dark" if dark_mode else "light") _blit_background(ax_uv, bg_uv, bbox_uv) _style_axes( ax_uv, diff --git a/app/views/panels/main_layout.py b/app/views/panels/main_layout.py index c50256b..9953fa2 100644 --- a/app/views/panels/main_layout.py +++ b/app/views/panels/main_layout.py @@ -841,6 +841,15 @@ def _refresh_theme_toggle_label(self: "PQAutomationApp") -> None: def _on_toggle_theme(self: "PQAutomationApp") -> None: """切换主题:重新应用 ttk 样式并刷新所有自定义样式相关的标签。""" + # 在测试进行时禁止切换主题,避免影响测量稳定性 + if getattr(self, "testing", False): + try: + if hasattr(self, "log_gui"): + self.log_gui.log("警告: 测试进行中,禁止切换主题", level="error") + except Exception: + pass + return + from app.views.theme_manager import toggle_theme toggle_theme() # apply_modern_styles() diff --git a/settings/pq_config.json b/settings/pq_config.json index c03d6d4..73fceaf 100644 --- a/settings/pq_config.json +++ b/settings/pq_config.json @@ -1,5 +1,5 @@ { - "current_test_type": "local_dimming", + "current_test_type": "sdr_movie", "test_types": { "screen_module": { "name": "屏模组性能测试", @@ -21,9 +21,9 @@ "contrast": "rgb" }, "cct_params": { - "x_ideal": 0.3127, + "x_ideal": 0.303696, "x_tolerance": 0.003, - "y_ideal": 0.329, + "y_ideal": 0.312349, "y_tolerance": 0.003 } }, @@ -54,7 +54,7 @@ "y_ideal": 0.329, "y_tolerance": 0.003 }, - "gamut_reference": "BT.601" + "gamut_reference": "BT.709" }, "hdr_movie": { "name": "HDR Movie测试",