修改SDR色准深色异常、修改保存结果深色模式异常

This commit is contained in:
xinzhu.yin
2026-06-09 11:02:55 +08:00
parent 9ad9cf9aa0
commit 8916f2fff0
6 changed files with 206 additions and 83 deletions

View File

@@ -2,15 +2,14 @@
import os import os
_EXPORT_BG_COLOR = "#FFFFFF" def _save_with_theme_background(fig, path, *, dpi=300, bbox_inches=None):
"""按图表当前主题背景导出,避免深色模式下被强制写成白底。"""
bg = fig.get_facecolor()
def _save_with_light_background(fig, path, *, dpi=300, bbox_inches=None):
"""导出统一浅色背景,避免深色主题下图片背景变暗。"""
kwargs = { kwargs = {
"dpi": dpi, "dpi": dpi,
"facecolor": _EXPORT_BG_COLOR, "facecolor": bg,
"edgecolor": _EXPORT_BG_COLOR, "edgecolor": bg,
"transparent": False,
} }
if bbox_inches is not None: if bbox_inches is not None:
kwargs["bbox_inches"] = bbox_inches kwargs["bbox_inches"] = bbox_inches
@@ -85,7 +84,7 @@ def save_result_images(result_dir, current_test_type, selected_items,
continue continue
per_ref_name = f"色域测试结果_{ref}.png" per_ref_name = f"色域测试结果_{ref}.png"
path = os.path.join(result_dir, per_ref_name) 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}") log(f"已保存: {per_ref_name}")
finally: finally:
ref_var.set(original_ref) ref_var.set(original_ref)
@@ -97,7 +96,7 @@ def save_result_images(result_dir, current_test_type, selected_items,
continue continue
path = os.path.join(result_dir, filename) path = os.path.join(result_dir, filename)
if default_bbox: if default_bbox:
_save_with_light_background(fig, path, dpi=300) _save_with_theme_background(fig, path, dpi=300)
else: 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}") log(f"已保存: {filename}")

View File

@@ -1,14 +1,19 @@
"""CIE 色度图底图渲染与缓存。 """
CIE 色度图底图渲染与缓存(工业版)
"重型图像渲染"colour-science 的谱迹颜色填充)与"轻量框架数据层" 特点:
(参考/实测三角形、标签、覆盖率)解耦。 - colour-science 谱迹渲染
- numpy RGBA 缓存
- 内存 + 磁盘缓存
- 支持 light / dark UI
- 启动预热
- 线程安全
底图 调用方式
- 仅在首次调用或缓存失效时通过 colour-science 渲染一次;
- 渲染结果保存为 numpy RGBA 数组,同时落盘到 settings/cache/ bg, bbox = get_cie1931_background(mode="dark")
下次启动直接 imread 加载,避免重新跑色彩科学计算。 ax.imshow(bg, extent=bbox)
调用方在每次绘图时只需 `ax.imshow(bg, extent=bbox)`,再叠加自己的矢量层。
""" """
from __future__ import annotations from __future__ import annotations
@@ -20,91 +25,140 @@ from typing import Tuple
import numpy as np import numpy as np
# 谱迹底图分辨率边长单位像素。1024 对于 14 inch 画布足够细腻, # ----------------------------
# 文件大小 ~1-2MB单次渲染 ~0.5-1 s缓存后毫秒级加载。 # 配置
# ----------------------------
# 渲染分辨率
_DIAGRAM_RES = 1024 _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) _CIE1931_BBOX: _BBox = (0.0, 0.8, 0.0, 0.9)
_CIE1976_BBOX: _BBox = (0.0, 0.65, 0.0, 0.6) _CIE1976_BBOX: _BBox = (0.0, 0.65, 0.0, 0.6)
_memory_cache: dict[str, np.ndarray] = {} _memory_cache: dict[str, np.ndarray] = {}
_lock = threading.Lock() _lock = threading.Lock()
# ----------------------------
# cache path
# ----------------------------
def _cache_dir() -> str: def _cache_dir() -> str:
# 项目根目录通过本文件位置反推app/plots/ -> 项目根
here = os.path.dirname(os.path.abspath(__file__)) here = os.path.dirname(os.path.abspath(__file__))
root = os.path.abspath(os.path.join(here, "..", "..")) root = os.path.abspath(os.path.join(here, "..", ".."))
d = os.path.join(root, "settings", "cache") d = os.path.join(root, "settings", "cache")
os.makedirs(d, exist_ok=True) os.makedirs(d, exist_ok=True)
return d return d
def _cache_key(kind: str, bbox: _BBox) -> str: def _cache_key(kind: str, bbox: _BBox, mode: str) -> str:
sig = f"{kind}|{bbox}|{_DIAGRAM_RES}|{_CACHE_VERSION}" sig = f"{kind}|{bbox}|{mode}|{_DIAGRAM_RES}|{_CACHE_VERSION}"
h = hashlib.md5(sig.encode("utf-8")).hexdigest()[:10] h = hashlib.md5(sig.encode()).hexdigest()[:10]
return f"chromaticity_{kind}_{h}.npy"
return f"chromaticity_{kind}_{mode}_{h}.npy"
def _cache_path(kind: str, bbox: _BBox) -> str: def _cache_path(kind: str, bbox: _BBox, mode: str) -> str:
return os.path.join(_cache_dir(), _cache_key(kind, bbox)) 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 import matplotlib
prev_backend = matplotlib.get_backend() prev_backend = matplotlib.get_backend()
try: try:
matplotlib.use("Agg", force=True) matplotlib.use("Agg", force=True)
except Exception: except Exception:
pass pass
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import colour
from colour.plotting import ( from colour.plotting import (
plot_chromaticity_diagram_CIE1931, plot_chromaticity_diagram_CIE1931,
plot_chromaticity_diagram_CIE1976UCS, 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 xmin, xmax, ymin, ymax = bbox
aspect = (xmax - xmin) / (ymax - ymin) aspect = (xmax - xmin) / (ymax - ymin)
height = _DIAGRAM_RES height = _DIAGRAM_RES
width = int(round(height * aspect)) width = int(round(height * aspect))
fig = plt.figure(figsize=(width / 100.0, height / 100.0), dpi=100) fig = plt.figure(
ax = fig.add_axes([0.0, 0.0, 1.0, 1.0]) 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": if kind == "cie1931":
plot_chromaticity_diagram_CIE1931( plot_chromaticity_diagram_CIE1931(
axes=ax, show=False, title=False, axes=ax,
tight_layout=False, transparent_background=True, show=False,
title=False,
tight_layout=False,
bounding_box=bbox, bounding_box=bbox,
transparent_background=False,
) )
elif kind == "cie1976": elif kind == "cie1976":
plot_chromaticity_diagram_CIE1976UCS( plot_chromaticity_diagram_CIE1976UCS(
axes=ax, show=False, title=False, axes=ax,
tight_layout=False, transparent_background=True, show=False,
title=False,
tight_layout=False,
bounding_box=bbox, bounding_box=bbox,
transparent_background=False,
) )
else: else:
plt.close(fig) 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_xlim(xmin, xmax)
ax.set_ylim(ymin, ymax) ax.set_ylim(ymin, ymax)
ax.set_axis_off() 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() fig.canvas.draw()
# 从 canvas 抓取 RGBA 数组
buf = np.asarray(fig.canvas.buffer_rgba()).copy() buf = np.asarray(fig.canvas.buffer_rgba()).copy()
buf = np.flipud(buf) buf = np.flipud(buf)
plt.close(fig) plt.close(fig)
try: try:
@@ -115,52 +169,107 @@ def _render_chromaticity(kind: str, bbox: _BBox) -> np.ndarray:
return buf 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: with _lock:
if key in _memory_cache: if key in _memory_cache:
return _memory_cache[key] return _memory_cache[key]
disk = _cache_path(kind, bbox) disk = _cache_path(kind, bbox, mode)
if os.path.isfile(disk): if os.path.isfile(disk):
try: try:
arr = np.load(disk) arr = np.load(disk)
_memory_cache[key] = arr _memory_cache[key] = arr
return arr return arr
except Exception: except Exception:
# 缓存损坏则重新渲染
try: try:
os.remove(disk) os.remove(disk)
except OSError: except OSError:
pass pass
arr = _render_chromaticity(kind, bbox) arr = _render_chromaticity(kind, bbox, mode)
_memory_cache[key] = arr _memory_cache[key] = arr
try: try:
np.save(disk, arr) np.save(disk, arr)
except Exception: except Exception:
pass pass
return arr return arr
def get_cie1931_background() -> Tuple[np.ndarray, _BBox]: # ----------------------------
"""返回 (RGBA 数组, bbox),可直接 ax.imshow(arr, extent=[*bbox])。""" # public API
return _load_or_render("cie1931", _CIE1931_BBOX), _CIE1931_BBOX # ----------------------------
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]: def get_cie1976_background(mode: str = "dark") -> Tuple[np.ndarray, _BBox]:
return _load_or_render("cie1976", _CIE1976_BBOX), _CIE1976_BBOX
return _load_or_render("cie1976", _CIE1976_BBOX, mode), _CIE1976_BBOX
# ----------------------------
# cache control
# ----------------------------
def clear_cache(*, disk: bool = False) -> None: def clear_cache(*, disk: bool = False) -> None:
"""清空内存缓存(可选连同磁盘)。供调试/样式调整时使用。""" """
清空缓存
"""
with _lock: with _lock:
_memory_cache.clear() _memory_cache.clear()
if disk: if disk:
d = _cache_dir() d = _cache_dir()
for name in os.listdir(d): for name in os.listdir(d):
if name.startswith("chromaticity_") and name.endswith(".npy"):
if name.startswith("chromaticity_"):
try: try:
os.remove(os.path.join(d, name)) os.remove(os.path.join(d, name))
except OSError: except OSError:
pass pass
# ----------------------------
# warmup
# ----------------------------
def warmup_cache(mode: str = "dark") -> None:
"""
启动预热缓存
可在软件启动时调用,避免首次绘图卡顿。
"""
get_cie1931_background(mode)
get_cie1976_background(mode)

View File

@@ -9,15 +9,10 @@ from typing import TYPE_CHECKING
from matplotlib.patches import Rectangle from matplotlib.patches import Rectangle
from matplotlib.lines import Line2D from matplotlib.lines import Line2D
import matplotlib.colors as mcolors from matplotlib.ticker import MultipleLocator, AutoMinorLocator
import numpy as np
from app.views.modern_styles import get_theme_palette from app.views.modern_styles import get_theme_palette
from app.plots.gamut_background import get_cie1976_background from app.plots.gamut_background import get_cie1976_background
from app.tests.color_accuracy import get_accuracy_color_standards 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: if TYPE_CHECKING:
from pqAutomationApp import PQAutomationApp from pqAutomationApp import PQAutomationApp
@@ -68,13 +63,13 @@ def _xy_to_uv(x: float, y: float):
return 0.0, 0.0 return 0.0, 0.0
return (4.0 * x) / denom, (9.0 * y) / denom return (4.0 * x) / denom, (9.0 * y) / denom
# ============================================================ # ============================================================
# 子图:左侧 Calman 风格面板 # 子图:左侧 Calman 风格面板
# ============================================================ # ============================================================
def _draw_left_panel(ax, color_patches, delta_e_values, font_scale=1.0, dark_mode=False): def _draw_left_panel(ax, color_patches, delta_e_values, font_scale=1.0, dark_mode=False):
"""左侧仅保留大条形图""" """左侧仅保留大条形图"""
ax.clear() ax.clear()
n = len(color_patches) 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)) y_pos = list(range(n))
bar_colors = [_COLOR_MAP.get(name, "#888888") for name in color_patches] 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( ax.barh(
y_pos, y_pos,
delta_e_values, delta_e_values,
height=0.72, height=0.72,
color=bar_colors, color=bar_colors,
edgecolor="#202020", edgecolor=edgecolor,
linewidth=0.5, linewidth=0.5,
zorder=3, 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_yticks(y_pos)
ax.set_yticklabels(color_patches, fontsize=max(5, 7 * font_scale), color=text_color) ax.set_yticklabels(color_patches, fontsize=max(5, 7 * font_scale), color=text_color)
ax.invert_yaxis() ax.invert_yaxis()
x_max = max(15.0, max(delta_e_values) * 1.15) x_max = max(15.0, max(delta_e_values) * 1.15)
ax.set_xlim(0, x_max) 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.tick_params(
ax.grid(axis="y", linestyle=":", linewidth=0.35, alpha=0.15, zorder=0) 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) 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' 上的色准对比。""" """绘制 CIE 1976 u'v' 上的色准对比。"""
ax.clear() ax.clear()
try: try:
bg, bbox = get_cie1976_background() bg, bbox = get_cie1976_background(mode="dark" if dark_mode else "light")
if bg.shape[-1] == 4: if bg.shape[-1] == 4:
bg = bg[:, :, :3] bg = bg[:, :, :3]
xmin, xmax, ymin, ymax = bbox 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_facecolor("#000" if dark_mode else "#FFFFFF")
ax.set_aspect("equal", adjustable="box") 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( ax.set_title(
"CIE 1976 u'v'", "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): def plot_accuracy(self: "PQAutomationApp", accuracy_data, test_type):
"""绘制色准测试结果 - Calman 风格(色块 + CIE 1976 u'v' + 统计)。""" """绘制色准测试结果"""
palette = get_theme_palette() palette = get_theme_palette()
try: try:
from app.views.theme_manager import is_dark from app.views.theme_manager import is_dark

View File

@@ -271,7 +271,7 @@ def plot_gamut(self: "PQAutomationApp", results, coverage, test_type):
# 左图CIE 1931 xy # 左图CIE 1931 xy
# ============================================================ # ============================================================
try: 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) _blit_background(ax_xy, bg_xy, bbox_xy)
_style_axes( _style_axes(
ax_xy, ax_xy,
@@ -343,7 +343,7 @@ def plot_gamut(self: "PQAutomationApp", results, coverage, test_type):
# 右图CIE 1976 u'v' # 右图CIE 1976 u'v'
# ============================================================ # ============================================================
try: 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) _blit_background(ax_uv, bg_uv, bbox_uv)
_style_axes( _style_axes(
ax_uv, ax_uv,

View File

@@ -841,6 +841,15 @@ def _refresh_theme_toggle_label(self: "PQAutomationApp") -> None:
def _on_toggle_theme(self: "PQAutomationApp") -> None: def _on_toggle_theme(self: "PQAutomationApp") -> None:
"""切换主题:重新应用 ttk 样式并刷新所有自定义样式相关的标签。""" """切换主题:重新应用 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 from app.views.theme_manager import toggle_theme
toggle_theme() toggle_theme()
# apply_modern_styles() # apply_modern_styles()

View File

@@ -1,5 +1,5 @@
{ {
"current_test_type": "local_dimming", "current_test_type": "sdr_movie",
"test_types": { "test_types": {
"screen_module": { "screen_module": {
"name": "屏模组性能测试", "name": "屏模组性能测试",
@@ -21,9 +21,9 @@
"contrast": "rgb" "contrast": "rgb"
}, },
"cct_params": { "cct_params": {
"x_ideal": 0.3127, "x_ideal": 0.303696,
"x_tolerance": 0.003, "x_tolerance": 0.003,
"y_ideal": 0.329, "y_ideal": 0.312349,
"y_tolerance": 0.003 "y_tolerance": 0.003
} }
}, },
@@ -54,7 +54,7 @@
"y_ideal": 0.329, "y_ideal": 0.329,
"y_tolerance": 0.003 "y_tolerance": 0.003
}, },
"gamut_reference": "BT.601" "gamut_reference": "BT.709"
}, },
"hdr_movie": { "hdr_movie": {
"name": "HDR Movie测试", "name": "HDR Movie测试",